diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index a6feac71a2..cac4757218 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -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.
+LMS: Forums. Added handling for case where discussion module can get `None` as
+value of lms.start in `lms/djangoapps/django_comment_client/utils.py`
Studio, LMS: Make ModelTypes more strict about their expected content (for
instance, Boolean, Integer, String), but also allow them to hold either the
@@ -14,6 +16,8 @@ an Integer can contain 3 or '3'. This changed an update to the xblock library.
LMS: Courses whose id matches a regex in the COURSES_WITH_UNSAFE_CODE Django
setting now run entirely outside the Python sandbox.
+Blades: Added tests for Video Alpha player.
+
Blades: Video Alpha bug fix for speed changing to 1.0 in Firefox.
Blades: Additional event tracking added to Video Alpha: fullscreen switch, show/hide
@@ -49,6 +53,9 @@ Blades: Staff debug info is now accessible for Graphical Slider Tool problems.
Blades: For Video Alpha the events ready, play, pause, seek, and speed change
are logged on the server (in the logs).
+Common: all dates and times are not time zone aware datetimes. No code should create or use struct_times nor naive
+datetimes.
+
Common: Developers can now have private Django settings files.
Common: Safety code added to prevent anything above the vertical level in the
diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py
index f769652493..f7f330f91e 100644
--- a/cms/djangoapps/contentstore/tests/tests.py
+++ b/cms/djangoapps/contentstore/tests/tests.py
@@ -3,6 +3,10 @@ from django.core.urlresolvers import reverse
from .utils import parse_json, user, registration
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
+from contentstore.tests.test_course_settings import CourseTestCase
+from xmodule.modulestore.tests.factories import CourseFactory
+import datetime
+from pytz import UTC
class ContentStoreTestCase(ModuleStoreTestCase):
@@ -162,3 +166,21 @@ class AuthTestCase(ContentStoreTestCase):
self.assertEqual(resp.status_code, 302)
# Logged in should work.
+
+
+class ForumTestCase(CourseTestCase):
+ def setUp(self):
+ """ Creates the test course. """
+ super(ForumTestCase, self).setUp()
+ self.course = CourseFactory.create(org='testX', number='727', display_name='Forum Course')
+
+ def test_blackouts(self):
+ now = datetime.datetime.now(UTC)
+ self.course.discussion_blackouts = [(t.isoformat(), t2.isoformat()) for t, t2 in
+ [(now - datetime.timedelta(days=14), now - datetime.timedelta(days=11)),
+ (now + datetime.timedelta(days=24), now + datetime.timedelta(days=30))]]
+ self.assertTrue(self.course.forum_posts_allowed)
+ self.course.discussion_blackouts = [(t.isoformat(), t2.isoformat()) for t, t2 in
+ [(now - datetime.timedelta(days=14), now + datetime.timedelta(days=2)),
+ (now + datetime.timedelta(days=24), now + datetime.timedelta(days=30))]]
+ self.assertFalse(self.course.forum_posts_allowed)
diff --git a/cms/envs/aws.py b/cms/envs/aws.py
index 35b15fe6ba..c6a383211f 100644
--- a/cms/envs/aws.py
+++ b/cms/envs/aws.py
@@ -112,9 +112,6 @@ TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
MITX_FEATURES[feature] = value
-# load segment.io key, provide a dummy if it does not exist
-SEGMENT_IO_KEY = ENV_TOKENS.get('SEGMENT_IO_KEY', '***REMOVED***')
-
LOGGING = get_logger_config(LOG_DIR,
logging_env=ENV_TOKENS['LOGGING_ENV'],
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
@@ -126,6 +123,13 @@ LOGGING = get_logger_config(LOG_DIR,
with open(ENV_ROOT / CONFIG_PREFIX + "auth.json") as auth_file:
AUTH_TOKENS = json.load(auth_file)
+# If Segment.io key specified, load it and turn on Segment.io if the feature flag is set
+# Note that this is the Studio key. There is a separate key for the LMS.
+SEGMENT_IO_KEY = AUTH_TOKENS.get('SEGMENT_IO_KEY')
+if SEGMENT_IO_KEY:
+ MITX_FEATURES['SEGMENT_IO'] = ENV_TOKENS.get('SEGMENT_IO', False)
+
+
AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"]
AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
DATABASES = AUTH_TOKENS['DATABASES']
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 3afc6cdcbd..8551a56c41 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -32,13 +32,23 @@ from path import path
MITX_FEATURES = {
'USE_DJANGO_PIPELINE': True,
+
'GITHUB_PUSH': False,
+
'ENABLE_DISCUSSION_SERVICE': False,
+
'AUTH_USE_MIT_CERTIFICATES': False,
- 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
- 'STAFF_EMAIL': '', # email address for staff (eg to request course creation)
+
+ # do not display video when running automated acceptance tests
+ 'STUB_VIDEO_FOR_TESTING': False,
+
+ # email address for staff (eg to request course creation)
+ 'STAFF_EMAIL': '',
+
'STUDIO_NPS_SURVEY': True,
- 'SEGMENT_IO': True,
+
+ # Segment.io - must explicitly turn it on for production
+ 'SEGMENT_IO': False,
# Enable URL that shows information about the status of various services
'ENABLE_SERVICE_STATUS': False,
diff --git a/cms/envs/dev.py b/cms/envs/dev.py
index 988856dbf4..07630bdf31 100644
--- a/cms/envs/dev.py
+++ b/cms/envs/dev.py
@@ -168,8 +168,14 @@ MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
# Enable URL that shows information about the status of variuous services
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
-# segment-io key for dev
-SEGMENT_IO_KEY = 'mty8edrrsg'
+############################# SEGMENT-IO ##################################
+
+# If there's an environment variable set, grab it and turn on Segment.io
+# Note that this is the Studio key. There is a separate key for the LMS.
+import os
+SEGMENT_IO_KEY = os.environ.get('SEGMENT_IO_KEY')
+if SEGMENT_IO_KEY:
+ MITX_FEATURES['SEGMENT_IO'] = True
#####################################################################
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index d0333cbe36..945c3a3cfa 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -10,7 +10,6 @@ import dateutil.parser
from xmodule.modulestore import Location
from xmodule.seq_module import SequenceDescriptor, SequenceModule
-from xmodule.timeparse import parse_time
from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf
import json
@@ -645,8 +644,11 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
def start_date_text(self):
def try_parse_iso_8601(text):
try:
- result = datetime.strptime(text, "%Y-%m-%dT%H:%M")
- result = result.strftime("%b %d, %Y")
+ result = Date().from_json(text)
+ if result is None:
+ result = text.title()
+ else:
+ result = result.strftime("%b %d, %Y")
except ValueError:
result = text.title()
@@ -670,8 +672,10 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
@property
def forum_posts_allowed(self):
+ date_proxy = Date()
try:
- blackout_periods = [(parse_time(start), parse_time(end))
+ blackout_periods = [(date_proxy.from_json(start),
+ date_proxy.from_json(end))
for start, end
in self.discussion_blackouts]
now = datetime.now(UTC())
@@ -701,7 +705,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
if self.last_eligible_appointment_date is None:
raise ValueError("Last appointment date must be specified")
self.registration_start_date = (self._try_parse_time('Registration_Start_Date') or
- datetime.utcfromtimestamp(0))
+ datetime.fromtimestamp(0, UTC()))
self.registration_end_date = self._try_parse_time('Registration_End_Date') or self.last_eligible_appointment_date
# do validation within the exam info:
if self.registration_start_date > self.registration_end_date:
@@ -720,7 +724,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
"""
if key in self.exam_info:
try:
- return parse_time(self.exam_info[key])
+ return Date().from_json(self.exam_info[key])
except ValueError as e:
msg = "Exam {0} in course {1} loaded with a bad exam_info key '{2}': '{3}'".format(self.exam_name, self.course_id, self.exam_info[key], e)
log.warning(msg)
diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py
index 963b70204e..8a74856fc1 100644
--- a/common/lib/xmodule/xmodule/fields.py
+++ b/common/lib/xmodule/xmodule/fields.py
@@ -6,7 +6,7 @@ from xblock.core import ModelType
import datetime
import dateutil.parser
-from django.utils.timezone import UTC
+from pytz import UTC
log = logging.getLogger(__name__)
@@ -15,6 +15,28 @@ class Date(ModelType):
'''
Date fields know how to parse and produce json (iso) compatible formats. Converts to tz aware datetimes.
'''
+ # See note below about not defaulting these
+ CURRENT_YEAR = datetime.datetime.now(UTC).year
+ PREVENT_DEFAULT_DAY_MON_SEED1 = datetime.datetime(CURRENT_YEAR, 1, 1, tzinfo=UTC)
+ PREVENT_DEFAULT_DAY_MON_SEED2 = datetime.datetime(CURRENT_YEAR, 2, 2, tzinfo=UTC)
+
+ def _parse_date_wo_default_month_day(self, field):
+ """
+ Parse the field as an iso string but prevent dateutils from defaulting the day or month while
+ allowing it to default the other fields.
+ """
+ # It's not trivial to replace dateutil b/c parsing timezones as Z, +03:30, -400 is hard in python
+ # however, we don't want dateutil to default the month or day (but some tests at least expect
+ # us to default year); so, we'll see if dateutil uses the defaults for these the hard way
+ result = dateutil.parser.parse(field, default=self.PREVENT_DEFAULT_DAY_MON_SEED1)
+ result_other = dateutil.parser.parse(field, default=self.PREVENT_DEFAULT_DAY_MON_SEED2)
+ if result != result_other:
+ log.warning("Field {0} is missing month or day".format(self._name, field))
+ return None
+ if result.tzinfo is None:
+ result = result.replace(tzinfo=UTC)
+ return result
+
def from_json(self, field):
"""
Parse an optional metadata key containing a time: if present, complain
@@ -26,14 +48,11 @@ class Date(ModelType):
elif field is "":
return None
elif isinstance(field, basestring):
- result = dateutil.parser.parse(field)
- if result.tzinfo is None:
- result = result.replace(tzinfo=UTC())
- return result
+ return self._parse_date_wo_default_month_day(field)
elif isinstance(field, (int, long, float)):
- return datetime.datetime.fromtimestamp(field / 1000, UTC())
+ return datetime.datetime.fromtimestamp(field / 1000, UTC)
elif isinstance(field, time.struct_time):
- return datetime.datetime.fromtimestamp(time.mktime(field), UTC())
+ return datetime.datetime.fromtimestamp(time.mktime(field), UTC)
elif isinstance(field, datetime.datetime):
return field
else:
diff --git a/common/lib/xmodule/xmodule/js/fixtures/videoalpha.html b/common/lib/xmodule/xmodule/js/fixtures/videoalpha.html
new file mode 100644
index 0000000000..bccf5df2cc
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/fixtures/videoalpha.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/js/fixtures/videoalpha_html5.html b/common/lib/xmodule/xmodule/js/fixtures/videoalpha_html5.html
new file mode 100644
index 0000000000..6088d07f2b
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/fixtures/videoalpha_html5.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/js/spec/helper.coffee b/common/lib/xmodule/xmodule/js/spec/helper.coffee
index 5cf75366d8..5f7fc27be0 100644
--- a/common/lib/xmodule/xmodule/js/spec/helper.coffee
+++ b/common/lib/xmodule/xmodule/js/spec/helper.coffee
@@ -20,10 +20,25 @@ jasmine.stubbedMetadata =
bogus:
duration: 100
+jasmine.fireEvent = (el, eventName) ->
+ if document.createEvent
+ event = document.createEvent "HTMLEvents"
+ event.initEvent eventName, true, true
+ else
+ event = document.createEventObject()
+ event.eventType = eventName
+ event.eventName = eventName
+ if document.createEvent
+ el.dispatchEvent(event)
+ else
+ el.fireEvent("on" + event.eventType, event)
+
jasmine.stubbedCaption =
start: [0, 10000, 20000, 30000]
text: ['Caption at 0', 'Caption at 10000', 'Caption at 20000', 'Caption at 30000']
+jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50']
+
jasmine.stubRequests = ->
spyOn($, 'ajax').andCallFake (settings) ->
if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/
@@ -41,9 +56,12 @@ jasmine.stubRequests = ->
throw "External request attempted for #{settings.url}, which is not defined."
jasmine.stubYoutubePlayer = ->
- YT.Player = -> jasmine.createSpyObj 'YT.Player', ['cueVideoById', 'getVideoEmbedCode',
+ YT.Player = ->
+ obj = jasmine.createSpyObj 'YT.Player', ['cueVideoById', 'getVideoEmbedCode',
'getCurrentTime', 'getPlayerState', 'getVolume', 'setVolume', 'loadVideoById',
- 'playVideo', 'pauseVideo', 'seekTo']
+ 'playVideo', 'pauseVideo', 'seekTo', 'getDuration', 'getAvailablePlaybackRates', 'setPlaybackRate']
+ obj['getAvailablePlaybackRates'] = jasmine.createSpy('getAvailablePlaybackRates').andReturn [0.75, 1.0, 1.25, 1.5]
+ obj
jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) ->
enableParts = [enableParts] unless $.isArray(enableParts)
@@ -60,6 +78,21 @@ jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) ->
if createPlayer
return new VideoPlayer(video: context.video)
+jasmine.stubVideoPlayerAlpha = (context, enableParts, createPlayer=true, html5=false) ->
+ suite = context.suite
+ currentPartName = suite.description while suite = suite.parentSuite
+ if html5 == false
+ loadFixtures 'videoalpha.html'
+ else
+ loadFixtures 'videoalpha_html5.html'
+ jasmine.stubRequests()
+ YT.Player = undefined
+ window.OldVideoPlayerAlpha = undefined
+ context.video = new VideoAlpha '#example', '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
+ jasmine.stubYoutubePlayer()
+ if createPlayer
+ return new VideoPlayerAlpha(video: context.video)
+
# Stub jQuery.cookie
$.cookie = jasmine.createSpy('jQuery.cookie').andReturn '1.0'
diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display/html5_video.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/html5_video.coffee
new file mode 100644
index 0000000000..176ceb7827
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/html5_video.coffee
@@ -0,0 +1,311 @@
+describe 'VideoAlpha HTML5Video', ->
+ playbackRates = [0.75, 1.0, 1.25, 1.5]
+ STATUS = window.YT.PlayerState
+ playerVars =
+ controls: 0
+ wmode: 'transparent'
+ rel: 0
+ showinfo: 0
+ enablejsapi: 1
+ modestbranding: 1
+ html5: 1
+ file = window.location.href.replace(/\/common(.*)$/, '') + '/test_root/data/videoalpha/gizmo'
+ html5Sources =
+ mp4: "#{file}.mp4"
+ webm: "#{file}.webm"
+ ogg: "#{file}.ogv"
+ onReady = jasmine.createSpy 'onReady'
+ onStateChange = jasmine.createSpy 'onStateChange'
+
+ beforeEach ->
+ loadFixtures 'videoalpha_html5.html'
+ @el = $('#example').find('.video')
+ @player = new window.HTML5Video.Player @el,
+ playerVars: playerVars,
+ videoSources: html5Sources,
+ events:
+ onReady: onReady
+ onStateChange: onStateChange
+
+ @videoEl = @el.find('.video-player video').get(0)
+
+ it 'PlayerState', ->
+ expect(HTML5Video.PlayerState).toEqual STATUS
+
+ describe 'constructor', ->
+ it 'create an html5 video element', ->
+ expect(@el.find('.video-player div')).toContain 'video'
+
+ it 'check if sources are created in correct way', ->
+ sources = $(@videoEl).find('source')
+ videoTypes = []
+ videoSources = []
+ $.each html5Sources, (index, source) ->
+ videoTypes.push index
+ videoSources.push source
+ $.each sources, (index, source) ->
+ s = $(source)
+ expect($.inArray(s.attr('src'), videoSources)).not.toEqual -1
+ expect($.inArray(s.attr('type').replace('video/', ''), videoTypes))
+ .not.toEqual -1
+
+ it 'check if click event is handled on the player', ->
+ expect(@videoEl).toHandle 'click'
+
+ # NOTE: According to
+ #
+ # https://github.com/ariya/phantomjs/wiki/Supported-Web-Standards#unsupported-features
+ #
+ # Video and Audio (due to the nature of PhantomJS) are not supported. After discussion
+ # with William Daly, some tests are disabled (Jenkins uses phantomjs for running tests
+ # and those tests fail).
+ #
+ # During code review, please enable the test below (change "xdescribe" to "describe"
+ # to enable the test).
+ xdescribe 'events:', ->
+
+ beforeEach ->
+ spyOn(@player, 'callStateChangeCallback').andCallThrough()
+
+ describe 'click', ->
+ describe 'when player is paused', ->
+ beforeEach ->
+ spyOn(@videoEl, 'play').andCallThrough()
+ @player.playerState = STATUS.PAUSED
+ $(@videoEl).trigger('click')
+
+ it 'native play event was called', ->
+ expect(@videoEl.play).toHaveBeenCalled()
+
+ it 'player state was changed', ->
+ expect(@player.playerState).toBe STATUS.PLAYING
+
+ it 'callback was called', ->
+ expect(@player.callStateChangeCallback).toHaveBeenCalled()
+
+ describe 'when player is played', ->
+
+ beforeEach ->
+ spyOn(@videoEl, 'pause').andCallThrough()
+ @player.playerState = STATUS.PLAYING
+ $(@videoEl).trigger('click')
+
+ it 'native pause event was called', ->
+ expect(@videoEl.pause).toHaveBeenCalled()
+
+ it 'player state was changed', ->
+ expect(@player.playerState).toBe STATUS.PAUSED
+
+ it 'callback was called', ->
+ expect(@player.callStateChangeCallback).toHaveBeenCalled()
+
+ describe 'play', ->
+
+ beforeEach ->
+ spyOn(@videoEl, 'play').andCallThrough()
+ @player.playerState = STATUS.PAUSED
+ @videoEl.play()
+
+ it 'native event was called', ->
+ expect(@videoEl.play).toHaveBeenCalled()
+
+ it 'player state was changed', ->
+ waitsFor ( ->
+ @player.playerState != HTML5Video.PlayerState.PAUSED
+ ), 'Player state should be changed', 1000
+
+ runs ->
+ expect(@player.playerState).toBe STATUS.PLAYING
+
+ it 'callback was called', ->
+ waitsFor ( ->
+ @player.playerState != STATUS.PAUSED
+ ), 'Player state should be changed', 1000
+
+ runs ->
+ expect(@player.callStateChangeCallback).toHaveBeenCalled()
+
+ describe 'pause', ->
+
+ beforeEach ->
+ spyOn(@videoEl, 'pause').andCallThrough()
+ @videoEl.play()
+ @videoEl.pause()
+
+ it 'native event was called', ->
+ expect(@videoEl.pause).toHaveBeenCalled()
+
+ it 'player state was changed', ->
+ waitsFor ( ->
+ @player.playerState != STATUS.UNSTARTED
+ ), 'Player state should be changed', 1000
+
+ runs ->
+ expect(@player.playerState).toBe STATUS.PAUSED
+
+ it 'callback was called', ->
+ waitsFor ( ->
+ @player.playerState != HTML5Video.PlayerState.UNSTARTED
+ ), 'Player state should be changed', 1000
+
+ runs ->
+ expect(@player.callStateChangeCallback).toHaveBeenCalled()
+
+ describe 'canplay', ->
+
+ beforeEach ->
+ waitsFor ( ->
+ @player.playerState != STATUS.UNSTARTED
+ ), 'Video cannot be played', 1000
+
+ it 'player state was changed', ->
+ runs ->
+ expect(@player.playerState).toBe STATUS.PAUSED
+
+ it 'end property was defined', ->
+ runs ->
+ expect(@player.end).not.toBeNull()
+
+ it 'start position was defined', ->
+ runs ->
+ expect(@videoEl.currentTime).toBe(@player.start)
+
+ it 'callback was called', ->
+ runs ->
+ expect(@player.config.events.onReady).toHaveBeenCalled()
+
+ describe 'ended', ->
+ beforeEach ->
+ waitsFor ( ->
+ @player.playerState != STATUS.UNSTARTED
+ ), 'Video cannot be played', 1000
+
+ it 'player state was changed', ->
+ runs ->
+ jasmine.fireEvent @videoEl, "ended"
+ expect(@player.playerState).toBe STATUS.ENDED
+
+ it 'callback was called', ->
+ jasmine.fireEvent @videoEl, "ended"
+ expect(@player.callStateChangeCallback).toHaveBeenCalled()
+
+ describe 'timeupdate', ->
+
+ beforeEach ->
+ spyOn(@videoEl, 'pause').andCallThrough()
+ waitsFor ( ->
+ @player.playerState != STATUS.UNSTARTED
+ ), 'Video cannot be played', 1000
+
+ it 'player should be paused', ->
+ runs ->
+ @player.end = 3
+ @videoEl.currentTime = 5
+ jasmine.fireEvent @videoEl, "timeupdate"
+ expect(@videoEl.pause).toHaveBeenCalled()
+
+ it 'end param should be re-defined', ->
+ runs ->
+ @player.end = 3
+ @videoEl.currentTime = 5
+ jasmine.fireEvent @videoEl, "timeupdate"
+ expect(@player.end).toBe @videoEl.duration
+
+ # NOTE: According to
+ #
+ # https://github.com/ariya/phantomjs/wiki/Supported-Web-Standards#unsupported-features
+ #
+ # Video and Audio (due to the nature of PhantomJS) are not supported. After discussion
+ # with William Daly, some tests are disabled (Jenkins uses phantomjs for running tests
+ # and those tests fail).
+ #
+ # During code review, please enable the test below (change "xdescribe" to "describe"
+ # to enable the test).
+ xdescribe 'methods:', ->
+
+ beforeEach ->
+ waitsFor ( ->
+ @volume = @videoEl.volume
+ @seek = @videoEl.currentTime
+ @player.playerState == STATUS.PAUSED
+ ), 'Video cannot be played', 1000
+
+
+ it 'pauseVideo', ->
+ spyOn(@videoEl, 'pause').andCallThrough()
+ @player.pauseVideo()
+ expect(@videoEl.pause).toHaveBeenCalled()
+
+ describe 'seekTo', ->
+
+ it 'set new correct value', ->
+ runs ->
+ @player.seekTo(2)
+ expect(@videoEl.currentTime).toBe 2
+
+ it 'set new inccorrect values', ->
+ runs ->
+ @player.seekTo(-50)
+ expect(@videoEl.currentTime).toBe @seek
+ @player.seekTo('5')
+ expect(@videoEl.currentTime).toBe @seek
+ @player.seekTo(500000)
+ expect(@videoEl.currentTime).toBe @seek
+
+ describe 'setVolume', ->
+
+ it 'set new correct value', ->
+ runs ->
+ @player.setVolume(50)
+ expect(@videoEl.volume).toBe 50*0.01
+
+ it 'set new inccorrect values', ->
+ runs ->
+ @player.setVolume(-50)
+ expect(@videoEl.volume).toBe @volume
+ @player.setVolume('5')
+ expect(@videoEl.volume).toBe @volume
+ @player.setVolume(500000)
+ expect(@videoEl.volume).toBe @volume
+
+ it 'getCurrentTime', ->
+ runs ->
+ @videoEl.currentTime = 3
+ expect(@player.getCurrentTime()).toBe @videoEl.currentTime
+
+ it 'playVideo', ->
+ runs ->
+ spyOn(@videoEl, 'play').andCallThrough()
+ @player.playVideo()
+ expect(@videoEl.play).toHaveBeenCalled()
+
+ it 'getPlayerState', ->
+ runs ->
+ @player.playerState = STATUS.PLAYING
+ expect(@player.getPlayerState()).toBe STATUS.PLAYING
+ @player.playerState = STATUS.ENDED
+ expect(@player.getPlayerState()).toBe STATUS.ENDED
+
+ it 'getVolume', ->
+ runs ->
+ @volume = @videoEl.volume = 0.5
+ expect(@player.getVolume()).toBe @volume
+
+ it 'getDuration', ->
+ runs ->
+ @duration = @videoEl.duration
+ expect(@player.getDuration()).toBe @duration
+
+ describe 'setPlaybackRate', ->
+ it 'set a correct value', ->
+ @playbackRate = 1.5
+ @player.setPlaybackRate @playbackRate
+ expect(@videoEl.playbackRate).toBe @playbackRate
+
+ it 'set NaN value', ->
+ @playbackRate = NaN
+ @player.setPlaybackRate @playbackRate
+ expect(@videoEl.playbackRate).toBe 1.0
+
+ it 'getAvailablePlaybackRates', ->
+ expect(@player.getAvailablePlaybackRates()).toEqual playbackRates
diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_caption_spec.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_caption_spec.coffee
new file mode 100644
index 0000000000..4bd237b81d
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_caption_spec.coffee
@@ -0,0 +1,373 @@
+describe 'VideoCaptionAlpha', ->
+
+ beforeEach ->
+ spyOn(VideoCaptionAlpha.prototype, 'fetchCaption').andCallThrough()
+ spyOn($, 'ajaxWithPrefix').andCallThrough()
+ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
+
+ afterEach ->
+ YT.Player = undefined
+ $.fn.scrollTo.reset()
+ $('.subtitles').remove()
+
+ describe 'constructor', ->
+
+ describe 'always', ->
+
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ @caption = @player.caption
+
+ it 'set the youtube id', ->
+ expect(@caption.youtubeId).toEqual 'normalSpeedYoutubeId'
+
+ it 'create the caption element', ->
+ expect($('.video')).toContain 'ol.subtitles'
+
+ it 'add caption control to video player', ->
+ expect($('.video')).toContain 'a.hide-subtitles'
+
+ it 'fetch the caption', ->
+ expect(@caption.loaded).toBeTruthy()
+ expect(@caption.fetchCaption).toHaveBeenCalled()
+ expect($.ajaxWithPrefix).toHaveBeenCalledWith
+ url: @caption.captionURL()
+ notifyOnError: false
+ success: jasmine.any(Function)
+
+ it 'bind window resize event', ->
+ expect($(window)).toHandleWith 'resize', @caption.resize
+
+ it 'bind the hide caption button', ->
+ expect($('.hide-subtitles')).toHandleWith 'click', @caption.toggle
+
+ it 'bind the mouse movement', ->
+ expect($('.subtitles')).toHandleWith 'mouseover', @caption.onMouseEnter
+ expect($('.subtitles')).toHandleWith 'mouseout', @caption.onMouseLeave
+ expect($('.subtitles')).toHandleWith 'mousemove', @caption.onMovement
+ expect($('.subtitles')).toHandleWith 'mousewheel', @caption.onMovement
+ expect($('.subtitles')).toHandleWith 'DOMMouseScroll', @caption.onMovement
+
+ describe 'when on a non touch-based device', ->
+
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ @caption = @player.caption
+
+ it 'render the caption', ->
+ captionsData = jasmine.stubbedCaption
+ $('.subtitles li[data-index]').each (index, link) =>
+ expect($(link)).toHaveData 'index', index
+ expect($(link)).toHaveData 'start', captionsData.start[index]
+ expect($(link)).toHaveText captionsData.text[index]
+
+ it 'add a padding element to caption', ->
+ expect($('.subtitles li:first')).toBe '.spacing'
+ expect($('.subtitles li:last')).toBe '.spacing'
+
+ it 'bind all the caption link', ->
+ $('.subtitles li[data-index]').each (index, link) =>
+ expect($(link)).toHandleWith 'click', @caption.seekPlayer
+
+ it 'set rendered to true', ->
+ expect(@caption.rendered).toBeTruthy()
+
+ describe 'when on a touch-based device', ->
+
+ beforeEach ->
+ window.onTouchBasedDevice.andReturn true
+ @player = jasmine.stubVideoPlayerAlpha @
+ @caption = @player.caption
+
+ it 'show explaination message', ->
+ expect($('.subtitles li')).toHaveHtml "Caption will be displayed when you start playing the video."
+
+ it 'does not set rendered to true', ->
+ expect(@caption.rendered).toBeFalsy()
+
+ describe 'mouse movement', ->
+
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ @caption = @player.caption
+ window.setTimeout.andReturn(100)
+ spyOn window, 'clearTimeout'
+
+ describe 'when cursor is outside of the caption box', ->
+
+ beforeEach ->
+ $(window).trigger jQuery.Event 'mousemove'
+
+ it 'does not set freezing timeout', ->
+ expect(@caption.frozen).toBeFalsy()
+
+ describe 'when cursor is in the caption box', ->
+
+ beforeEach ->
+ $('.subtitles').trigger jQuery.Event 'mouseenter'
+
+ it 'set the freezing timeout', ->
+ expect(@caption.frozen).toEqual 100
+
+ describe 'when the cursor is moving', ->
+ beforeEach ->
+ $('.subtitles').trigger jQuery.Event 'mousemove'
+
+ it 'reset the freezing timeout', ->
+ expect(window.clearTimeout).toHaveBeenCalledWith 100
+
+ describe 'when the mouse is scrolling', ->
+ beforeEach ->
+ $('.subtitles').trigger jQuery.Event 'mousewheel'
+
+ it 'reset the freezing timeout', ->
+ expect(window.clearTimeout).toHaveBeenCalledWith 100
+
+ describe 'when cursor is moving out of the caption box', ->
+ beforeEach ->
+ @caption.frozen = 100
+ $.fn.scrollTo.reset()
+
+ describe 'always', ->
+ beforeEach ->
+ $('.subtitles').trigger jQuery.Event 'mouseout'
+
+ it 'reset the freezing timeout', ->
+ expect(window.clearTimeout).toHaveBeenCalledWith 100
+
+ it 'unfreeze the caption', ->
+ expect(@caption.frozen).toBeNull()
+
+ describe 'when the player is playing', ->
+ beforeEach ->
+ @caption.playing = true
+ $('.subtitles li[data-index]:first').addClass 'current'
+ $('.subtitles').trigger jQuery.Event 'mouseout'
+
+ it 'scroll the caption', ->
+ expect($.fn.scrollTo).toHaveBeenCalled()
+
+ describe 'when the player is not playing', ->
+ beforeEach ->
+ @caption.playing = false
+ $('.subtitles').trigger jQuery.Event 'mouseout'
+
+ it 'does not scroll the caption', ->
+ expect($.fn.scrollTo).not.toHaveBeenCalled()
+
+ describe 'search', ->
+
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ @caption = @player.caption
+
+ it 'return a correct caption index', ->
+ expect(@caption.search(0)).toEqual 0
+ expect(@caption.search(9999)).toEqual 0
+ expect(@caption.search(10000)).toEqual 1
+ expect(@caption.search(15000)).toEqual 1
+ expect(@caption.search(30000)).toEqual 3
+ expect(@caption.search(30001)).toEqual 3
+
+ describe 'play', ->
+ describe 'when the caption was not rendered', ->
+ beforeEach ->
+ window.onTouchBasedDevice.andReturn true
+ @player = jasmine.stubVideoPlayerAlpha @
+ @caption = @player.caption
+ @caption.play()
+
+ it 'render the caption', ->
+ captionsData = jasmine.stubbedCaption
+ $('.subtitles li[data-index]').each (index, link) =>
+ expect($(link)).toHaveData 'index', index
+ expect($(link)).toHaveData 'start', captionsData.start[index]
+ expect($(link)).toHaveText captionsData.text[index]
+
+ it 'add a padding element to caption', ->
+ expect($('.subtitles li:first')).toBe '.spacing'
+ expect($('.subtitles li:last')).toBe '.spacing'
+
+ it 'bind all the caption link', ->
+ $('.subtitles li[data-index]').each (index, link) =>
+ expect($(link)).toHandleWith 'click', @caption.seekPlayer
+
+ it 'set rendered to true', ->
+ expect(@caption.rendered).toBeTruthy()
+
+ it 'set playing to true', ->
+ expect(@caption.playing).toBeTruthy()
+
+ describe 'pause', ->
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ @caption = @player.caption
+ @caption.playing = true
+ @caption.pause()
+
+ it 'set playing to false', ->
+ expect(@caption.playing).toBeFalsy()
+
+ describe 'updatePlayTime', ->
+
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ @caption = @player.caption
+
+ describe 'when the video speed is 1.0x', ->
+ beforeEach ->
+ @caption.currentSpeed = '1.0'
+ @caption.updatePlayTime 25.000
+
+ it 'search the caption based on time', ->
+ expect(@caption.currentIndex).toEqual 2
+
+ describe 'when the video speed is not 1.0x', ->
+ beforeEach ->
+ @caption.currentSpeed = '0.75'
+ @caption.updatePlayTime 25.000
+
+ it 'search the caption based on 1.0x speed', ->
+ expect(@caption.currentIndex).toEqual 1
+
+ describe 'when the index is not the same', ->
+ beforeEach ->
+ @caption.currentIndex = 1
+ $('.subtitles li[data-index=1]').addClass 'current'
+ @caption.updatePlayTime 25.000
+
+ it 'deactivate the previous caption', ->
+ expect($('.subtitles li[data-index=1]')).not.toHaveClass 'current'
+
+ it 'activate new caption', ->
+ expect($('.subtitles li[data-index=2]')).toHaveClass 'current'
+
+ it 'save new index', ->
+ expect(@caption.currentIndex).toEqual 2
+
+ it 'scroll caption to new position', ->
+ expect($.fn.scrollTo).toHaveBeenCalled()
+
+ describe 'when the index is the same', ->
+ beforeEach ->
+ @caption.currentIndex = 1
+ $('.subtitles li[data-index=1]').addClass 'current'
+ @caption.updatePlayTime 15.000
+
+ it 'does not change current subtitle', ->
+ expect($('.subtitles li[data-index=1]')).toHaveClass 'current'
+
+ describe 'resize', ->
+
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ @caption = @player.caption
+ $('.subtitles li[data-index=1]').addClass 'current'
+ @caption.resize()
+
+ it 'set the height of caption container', ->
+ expect(parseInt($('.subtitles').css('maxHeight'))).toBeCloseTo $('.video-wrapper').height(), 2
+
+ it 'set the height of caption spacing', ->
+ firstSpacing = Math.abs(parseInt($('.subtitles .spacing:first').css('height')))
+ lastSpacing = Math.abs(parseInt($('.subtitles .spacing:last').css('height')))
+
+ expect(firstSpacing - @caption.topSpacingHeight()).toBeLessThan 1
+ expect(lastSpacing - @caption.bottomSpacingHeight()).toBeLessThan 1
+
+ it 'scroll caption to new position', ->
+ expect($.fn.scrollTo).toHaveBeenCalled()
+
+ describe 'scrollCaption', ->
+
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ @caption = @player.caption
+
+ describe 'when frozen', ->
+ beforeEach ->
+ @caption.frozen = true
+ $('.subtitles li[data-index=1]').addClass 'current'
+ @caption.scrollCaption()
+
+ it 'does not scroll the caption', ->
+ expect($.fn.scrollTo).not.toHaveBeenCalled()
+
+ describe 'when not frozen', ->
+ beforeEach ->
+ @caption.frozen = false
+
+ describe 'when there is no current caption', ->
+ beforeEach ->
+ @caption.scrollCaption()
+
+ it 'does not scroll the caption', ->
+ expect($.fn.scrollTo).not.toHaveBeenCalled()
+
+ describe 'when there is a current caption', ->
+ beforeEach ->
+ $('.subtitles li[data-index=1]').addClass 'current'
+ @caption.scrollCaption()
+
+ it 'scroll to current caption', ->
+ offset = -0.5 * ($('.video-wrapper').height() - $('.subtitles .current:first').height())
+
+ expect($.fn.scrollTo).toHaveBeenCalledWith $('.subtitles .current:first', @caption.el),
+ offset: offset
+
+ describe 'seekPlayer', ->
+
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ @caption = @player.caption
+ $(@caption).bind 'seek', (event, time) => @time = time
+
+ describe 'when the video speed is 1.0x', ->
+ beforeEach ->
+ @caption.currentSpeed = '1.0'
+ $('.subtitles li[data-start="30000"]').trigger('click')
+
+ it 'trigger seek event with the correct time', ->
+ expect(@player.currentTime).toEqual 30.000
+
+ describe 'when the video speed is not 1.0x', ->
+ beforeEach ->
+ @caption.currentSpeed = '0.75'
+ $('.subtitles li[data-start="30000"]').trigger('click')
+
+ it 'trigger seek event with the correct time', ->
+ expect(@player.currentTime).toEqual 40.000
+
+ describe 'toggle', ->
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ spyOn @video, 'log'
+ @caption = @player.caption
+ $('.subtitles li[data-index=1]').addClass 'current'
+
+ describe 'when the caption is visible', ->
+ beforeEach ->
+ @caption.el.removeClass 'closed'
+ @caption.toggle jQuery.Event('click')
+
+ it 'log the hide_transcript event', ->
+ expect(@video.log).toHaveBeenCalledWith 'hide_transcript',
+ currentTime: @player.currentTime
+
+ it 'hide the caption', ->
+ expect(@caption.el).toHaveClass 'closed'
+
+ describe 'when the caption is hidden', ->
+ beforeEach ->
+ @caption.el.addClass 'closed'
+ @caption.toggle jQuery.Event('click')
+
+ it 'log the show_transcript event', ->
+ expect(@video.log).toHaveBeenCalledWith 'show_transcript',
+ currentTime: @player.currentTime
+
+ it 'show the caption', ->
+ expect(@caption.el).not.toHaveClass 'closed'
+
+ it 'scroll the caption', ->
+ expect($.fn.scrollTo).toHaveBeenCalled()
diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_control_spec.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_control_spec.coffee
new file mode 100644
index 0000000000..a4dc8739d8
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_control_spec.coffee
@@ -0,0 +1,103 @@
+describe 'VideoControlAlpha', ->
+ beforeEach ->
+ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
+ loadFixtures 'videoalpha.html'
+ $('.video-controls').html ''
+
+ describe 'constructor', ->
+
+ it 'render the video controls', ->
+ @control = new window.VideoControlAlpha(el: $('.video-controls'))
+ expect($('.video-controls')).toContain
+ ['.slider', 'ul.vcr', 'a.play', '.vidtime', '.add-fullscreen'].join(',')
+ expect($('.video-controls').find('.vidtime')).toHaveText '0:00 / 0:00'
+
+ it 'bind the playback button', ->
+ @control = new window.VideoControlAlpha(el: $('.video-controls'))
+ expect($('.video_control')).toHandleWith 'click', @control.togglePlayback
+
+ describe 'when on a touch based device', ->
+ beforeEach ->
+ window.onTouchBasedDevice.andReturn true
+ @control = new window.VideoControlAlpha(el: $('.video-controls'))
+
+ it 'does not add the play class to video control', ->
+ expect($('.video_control')).not.toHaveClass 'play'
+ expect($('.video_control')).not.toHaveHtml 'Play'
+
+
+ describe 'when on a non-touch based device', ->
+
+ beforeEach ->
+ @control = new window.VideoControlAlpha(el: $('.video-controls'))
+
+ it 'add the play class to video control', ->
+ expect($('.video_control')).toHaveClass 'play'
+ expect($('.video_control')).toHaveHtml 'Play'
+
+ describe 'play', ->
+
+ beforeEach ->
+ @control = new window.VideoControlAlpha(el: $('.video-controls'))
+ @control.play()
+
+ it 'switch playback button to play state', ->
+ expect($('.video_control')).not.toHaveClass 'play'
+ expect($('.video_control')).toHaveClass 'pause'
+ expect($('.video_control')).toHaveHtml 'Pause'
+
+ describe 'pause', ->
+
+ beforeEach ->
+ @control = new window.VideoControlAlpha(el: $('.video-controls'))
+ @control.pause()
+
+ it 'switch playback button to pause state', ->
+ expect($('.video_control')).not.toHaveClass 'pause'
+ expect($('.video_control')).toHaveClass 'play'
+ expect($('.video_control')).toHaveHtml 'Play'
+
+ describe 'togglePlayback', ->
+
+ beforeEach ->
+ @control = new window.VideoControlAlpha(el: $('.video-controls'))
+
+ describe 'when the control does not have play or pause class', ->
+ beforeEach ->
+ $('.video_control').removeClass('play').removeClass('pause')
+
+ describe 'when the video is playing', ->
+ beforeEach ->
+ $('.video_control').addClass('play')
+ spyOnEvent @control, 'pause'
+ @control.togglePlayback jQuery.Event('click')
+
+ it 'does not trigger the pause event', ->
+ expect('pause').not.toHaveBeenTriggeredOn @control
+
+ describe 'when the video is paused', ->
+ beforeEach ->
+ $('.video_control').addClass('pause')
+ spyOnEvent @control, 'play'
+ @control.togglePlayback jQuery.Event('click')
+
+ it 'does not trigger the play event', ->
+ expect('play').not.toHaveBeenTriggeredOn @control
+
+ describe 'when the video is playing', ->
+ beforeEach ->
+ spyOnEvent @control, 'pause'
+ $('.video_control').addClass 'pause'
+ @control.togglePlayback jQuery.Event('click')
+
+ it 'trigger the pause event', ->
+ expect('pause').toHaveBeenTriggeredOn @control
+
+ describe 'when the video is paused', ->
+ beforeEach ->
+ spyOnEvent @control, 'play'
+ $('.video_control').addClass 'play'
+ @control.togglePlayback jQuery.Event('click')
+
+ it 'trigger the play event', ->
+ expect('play').toHaveBeenTriggeredOn @control
diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_player_spec.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_player_spec.coffee
new file mode 100644
index 0000000000..e9a5ca30b4
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_player_spec.coffee
@@ -0,0 +1,561 @@
+describe 'VideoPlayerAlpha', ->
+ playerVars =
+ controls: 0
+ wmode: 'transparent'
+ rel: 0
+ showinfo: 0
+ enablejsapi: 1
+ modestbranding: 1
+ html5: 1
+
+ beforeEach ->
+ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
+ # It tries to call methods of VideoProgressSlider on Spy
+ for part in ['VideoCaptionAlpha', 'VideoSpeedControlAlpha', 'VideoVolumeControlAlpha', 'VideoProgressSliderAlpha', 'VideoControlAlpha']
+ spyOn(window[part].prototype, 'initialize').andCallThrough()
+
+
+ afterEach ->
+ YT.Player = undefined
+
+ describe 'constructor', ->
+ beforeEach ->
+ $.fn.qtip.andCallFake ->
+ $(this).data('qtip', true)
+
+ describe 'always', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ @player = new VideoPlayerAlpha video: @video
+
+ it 'instanticate current time to zero', ->
+ expect(@player.currentTime).toEqual 0
+
+ it 'set the element', ->
+ expect(@player.el).toHaveId 'video_id'
+
+ it 'create video control', ->
+ expect(window.VideoControlAlpha.prototype.initialize).toHaveBeenCalled()
+ expect(@player.control).toBeDefined()
+ expect(@player.control.el).toBe $('.video-controls', @player.el)
+
+ it 'create video caption', ->
+ expect(window.VideoCaptionAlpha.prototype.initialize).toHaveBeenCalled()
+ expect(@player.caption).toBeDefined()
+ expect(@player.caption.el).toBe @player.el
+ expect(@player.caption.youtubeId).toEqual 'normalSpeedYoutubeId'
+ expect(@player.caption.currentSpeed).toEqual '1.0'
+ expect(@player.caption.captionAssetPath).toEqual '/static/subs/'
+
+ it 'create video speed control', ->
+ expect(window.VideoSpeedControlAlpha.prototype.initialize).toHaveBeenCalled()
+ expect(@player.speedControl).toBeDefined()
+ expect(@player.speedControl.el).toBe $('.secondary-controls', @player.el)
+ expect(@player.speedControl.speeds).toEqual ['0.75', '1.0']
+ expect(@player.speedControl.currentSpeed).toEqual '1.0'
+
+ it 'create video progress slider', ->
+ expect(window.VideoSpeedControlAlpha.prototype.initialize).toHaveBeenCalled()
+ expect(@player.progressSlider).toBeDefined()
+ expect(@player.progressSlider.el).toBe $('.slider', @player.el)
+
+ it 'bind to video control play event', ->
+ expect($(@player.control)).toHandleWith 'play', @player.play
+
+ it 'bind to video control pause event', ->
+ expect($(@player.control)).toHandleWith 'pause', @player.pause
+
+ it 'bind to video caption seek event', ->
+ expect($(@player.caption)).toHandleWith 'caption_seek', @player.onSeek
+
+ it 'bind to video speed control speedChange event', ->
+ expect($(@player.speedControl)).toHandleWith 'speedChange', @player.onSpeedChange
+
+ it 'bind to video progress slider seek event', ->
+ expect($(@player.progressSlider)).toHandleWith 'slide_seek', @player.onSeek
+
+ it 'bind to video volume control volumeChange event', ->
+ expect($(@player.volumeControl)).toHandleWith 'volumeChange', @player.onVolumeChange
+
+ it 'bind to key press', ->
+ expect($(document.documentElement)).toHandleWith 'keyup', @player.bindExitFullScreen
+
+ it 'bind to fullscreen switching button', ->
+ expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen
+
+ it 'create Youtube player', ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ spyOn YT, 'Player'
+ @player = new VideoPlayerAlpha video: @video
+ expect(YT.Player).toHaveBeenCalledWith('id', {
+ playerVars: playerVars
+ videoId: 'normalSpeedYoutubeId'
+ events:
+ onReady: @player.onReady
+ onStateChange: @player.onStateChange
+ onPlaybackQualityChange: @player.onPlaybackQualityChange
+ })
+
+ it 'create HTML5 player', ->
+ jasmine.stubVideoPlayerAlpha @, [], false, true
+ spyOn HTML5Video, 'Player'
+ $('.video').append $('')
+ @player = new VideoPlayerAlpha video: @video
+ expect(HTML5Video.Player).toHaveBeenCalledWith @video.el,
+ playerVars: playerVars
+ videoSources: @video.html5Sources
+ events:
+ onReady: @player.onReady
+ onStateChange: @player.onStateChange
+
+ describe 'when not on a touch based device', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ $('.add-fullscreen, .hide-subtitles').removeData 'qtip'
+ @player = new VideoPlayerAlpha video: @video
+
+ it 'add the tooltip to fullscreen and subtitle button', ->
+ expect($('.add-fullscreen')).toHaveData 'qtip'
+ expect($('.hide-subtitles')).toHaveData 'qtip'
+
+ it 'create video volume control', ->
+ expect(window.VideoVolumeControlAlpha.prototype.initialize).toHaveBeenCalled()
+ expect(@player.volumeControl).toBeDefined()
+ expect(@player.volumeControl.el).toBe $('.secondary-controls', @player.el)
+
+ describe 'when on a touch based device', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ window.onTouchBasedDevice.andReturn true
+ $('.add-fullscreen, .hide-subtitles').removeData 'qtip'
+ @player = new VideoPlayerAlpha video: @video
+
+ it 'does not add the tooltip to fullscreen and subtitle button', ->
+ expect($('.add-fullscreen')).not.toHaveData 'qtip'
+ expect($('.hide-subtitles')).not.toHaveData 'qtip'
+
+ it 'does not create video volume control', ->
+ expect(window.VideoVolumeControlAlpha.prototype.initialize).not.toHaveBeenCalled()
+ expect(@player.volumeControl).not.toBeDefined()
+
+ describe 'onReady', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ spyOn @video, 'log'
+ $('.video').append $('')
+ @video.embed()
+ @player = @video.player
+ spyOnEvent @player, 'ready'
+ spyOnEvent @player, 'updatePlayTime'
+ @player.onReady()
+
+ it 'log the load_video event', ->
+ expect(@video.log).toHaveBeenCalledWith 'load_video'
+
+ describe 'when not on a touch based device', ->
+ beforeEach ->
+ spyOn @player, 'play'
+ @player.onReady()
+
+ it 'autoplay the first video', ->
+ expect(@player.play).toHaveBeenCalled()
+
+ describe 'when on a touch based device', ->
+ beforeEach ->
+ window.onTouchBasedDevice.andReturn true
+ spyOn @player, 'play'
+ @player.onReady()
+
+ it 'does not autoplay the first video', ->
+ expect(@player.play).not.toHaveBeenCalled()
+
+ describe 'onStateChange', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+
+ describe 'when the video is unstarted', ->
+ beforeEach ->
+ @player = new VideoPlayerAlpha video: @video
+ spyOn @player.control, 'pause'
+ @player.caption.pause = jasmine.createSpy('VideoCaptionAlpha.pause')
+ @player.onStateChange data: YT.PlayerState.UNSTARTED
+
+ it 'pause the video control', ->
+ expect(@player.control.pause).toHaveBeenCalled()
+
+ it 'pause the video caption', ->
+ expect(@player.caption.pause).toHaveBeenCalled()
+
+ describe 'when the video is playing', ->
+ beforeEach ->
+ @anotherPlayer = jasmine.createSpyObj 'AnotherPlayer', ['onPause']
+ window.OldVideoPlayerAlpha = @anotherPlayer
+ @player = new VideoPlayerAlpha video: @video
+ spyOn @video, 'log'
+ spyOn(window, 'setInterval').andReturn 100
+ spyOn @player.control, 'play'
+ @player.caption.play = jasmine.createSpy('VideoCaptionAlpha.play')
+ @player.progressSlider.play = jasmine.createSpy('VideoProgressSliderAlpha.play')
+ @player.player.getVideoEmbedCode.andReturn 'embedCode'
+ @player.onStateChange data: YT.PlayerState.PLAYING
+
+ it 'log the play_video event', ->
+ expect(@video.log).toHaveBeenCalledWith 'play_video', {currentTime: 0}
+
+ it 'pause other video player', ->
+ expect(@anotherPlayer.onPause).toHaveBeenCalled()
+
+ it 'set current video player as active player', ->
+ expect(window.OldVideoPlayerAlpha).toEqual @player
+
+ it 'set update interval', ->
+ expect(window.setInterval).toHaveBeenCalledWith @player.update, 200
+ expect(@player.player.interval).toEqual 100
+
+ it 'play the video control', ->
+ expect(@player.control.play).toHaveBeenCalled()
+
+ it 'play the video caption', ->
+ expect(@player.caption.play).toHaveBeenCalled()
+
+ it 'play the video progress slider', ->
+ expect(@player.progressSlider.play).toHaveBeenCalled()
+
+ describe 'when the video is paused', ->
+ beforeEach ->
+ @player = new VideoPlayerAlpha video: @video
+ spyOn @video, 'log'
+ spyOn window, 'clearInterval'
+ spyOn @player.control, 'pause'
+ @player.caption.pause = jasmine.createSpy('VideoCaptionAlpha.pause')
+ @player.player.interval = 100
+ @player.player.getVideoEmbedCode.andReturn 'embedCode'
+ @player.onStateChange data: YT.PlayerState.PAUSED
+
+ it 'log the pause_video event', ->
+ expect(@video.log).toHaveBeenCalledWith 'pause_video', {currentTime: 0}
+
+ it 'clear update interval', ->
+ expect(window.clearInterval).toHaveBeenCalledWith 100
+ expect(@player.player.interval).toBeNull()
+
+ it 'pause the video control', ->
+ expect(@player.control.pause).toHaveBeenCalled()
+
+ it 'pause the video caption', ->
+ expect(@player.caption.pause).toHaveBeenCalled()
+
+ describe 'when the video is ended', ->
+ beforeEach ->
+ @player = new VideoPlayerAlpha video: @video
+ spyOn @player.control, 'pause'
+ @player.caption.pause = jasmine.createSpy('VideoCaptionAlpha.pause')
+ @player.onStateChange data: YT.PlayerState.ENDED
+
+ it 'pause the video control', ->
+ expect(@player.control.pause).toHaveBeenCalled()
+
+ it 'pause the video caption', ->
+ expect(@player.caption.pause).toHaveBeenCalled()
+
+ describe 'onSeek', ->
+ conf = [{
+ desc : 'check if seek_video is logged with slide_seek type',
+ type: 'slide_seek',
+ obj: 'progressSlider'
+ },{
+ desc : 'check if seek_video is logged with caption_seek type',
+ type: 'caption_seek',
+ obj: 'caption'
+ }]
+
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ @player = new VideoPlayerAlpha video: @video
+ spyOn window, 'clearInterval'
+ @player.player.interval = 100
+ spyOn @player, 'updatePlayTime'
+ spyOn @video, 'log'
+
+ $.each conf, (key, value) ->
+ it value.desc, ->
+ type = value.type
+ old_time = 0
+ new_time = 60
+ $(@player[value.obj]).trigger value.type, new_time
+ expect(@video.log).toHaveBeenCalledWith 'seek_video',
+ old_time: old_time
+ new_time: new_time
+ type: value.type
+
+ it 'seek the player', ->
+ $(@player.progressSlider).trigger 'slide_seek', 60
+ expect(@player.player.seekTo).toHaveBeenCalledWith 60, true
+
+ it 'call updatePlayTime on player', ->
+ $(@player.progressSlider).trigger 'slide_seek', 60
+ expect(@player.updatePlayTime).toHaveBeenCalledWith 60
+
+ describe 'when the player is playing', ->
+ beforeEach ->
+ $(@player.progressSlider).trigger 'slide_seek', 60
+ @player.player.getPlayerState.andReturn YT.PlayerState.PLAYING
+ @player.onSeek {}, 60
+
+ it 'reset the update interval', ->
+ expect(window.clearInterval).toHaveBeenCalledWith 100
+
+ describe 'when the player is not playing', ->
+ beforeEach ->
+ $(@player.progressSlider).trigger 'slide_seek', 60
+ @player.player.getPlayerState.andReturn YT.PlayerState.PAUSED
+ @player.onSeek {}, 60
+
+ it 'set the current time', ->
+ expect(@player.currentTime).toEqual 60
+
+ describe 'onSpeedChange', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ @player = new VideoPlayerAlpha video: @video
+ @player.currentTime = 60
+ spyOn @player, 'updatePlayTime'
+ spyOn(@video, 'setSpeed').andCallThrough()
+ spyOn(@video, 'log')
+
+ describe 'always', ->
+ beforeEach ->
+ @player.onSpeedChange {}, '0.75', false
+
+ it 'check if speed_change_video is logged', ->
+ expect(@video.log).toHaveBeenCalledWith 'speed_change_video',
+ currentTime: @player.currentTime
+ old_speed: '1.0'
+ new_speed: '0.75'
+
+ it 'convert the current time to the new speed', ->
+ expect(@player.currentTime).toEqual '80.000'
+
+ it 'set video speed to the new speed', ->
+ expect(@video.setSpeed).toHaveBeenCalledWith '0.75', false
+
+ it 'tell video caption that the speed has changed', ->
+ expect(@player.caption.currentSpeed).toEqual '0.75'
+
+ describe 'when the video is playing', ->
+ beforeEach ->
+ @player.player.getPlayerState.andReturn YT.PlayerState.PLAYING
+ @player.onSpeedChange {}, '0.75'
+
+ it 'load the video', ->
+ expect(@player.player.loadVideoById).toHaveBeenCalledWith 'slowerSpeedYoutubeId', '80.000'
+
+ it 'trigger updatePlayTime event', ->
+ expect(@player.updatePlayTime).toHaveBeenCalledWith '80.000'
+
+ describe 'when the video is not playing', ->
+ beforeEach ->
+ @player.player.getPlayerState.andReturn YT.PlayerState.PAUSED
+ @player.onSpeedChange {}, '0.75'
+
+ it 'cue the video', ->
+ expect(@player.player.cueVideoById).toHaveBeenCalledWith 'slowerSpeedYoutubeId', '80.000'
+
+ it 'trigger updatePlayTime event', ->
+ expect(@player.updatePlayTime).toHaveBeenCalledWith '80.000'
+
+ describe 'onVolumeChange', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ @player = new VideoPlayerAlpha video: @video
+ @player.onVolumeChange undefined, 60
+
+ it 'set the volume on player', ->
+ expect(@player.player.setVolume).toHaveBeenCalledWith 60
+
+ describe 'update', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ @player = new VideoPlayerAlpha video: @video
+ spyOn @player, 'updatePlayTime'
+
+ describe 'when the current time is unavailable from the player', ->
+ beforeEach ->
+ @player.player.getCurrentTime.andReturn undefined
+ @player.update()
+
+ it 'does not trigger updatePlayTime event', ->
+ expect(@player.updatePlayTime).not.toHaveBeenCalled()
+
+ describe 'when the current time is available from the player', ->
+ beforeEach ->
+ @player.player.getCurrentTime.andReturn 60
+ @player.update()
+
+ it 'trigger updatePlayTime event', ->
+ expect(@player.updatePlayTime).toHaveBeenCalledWith(60)
+
+ describe 'updatePlayTime', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ @player = new VideoPlayerAlpha video: @video
+ spyOn(@video, 'getDuration').andReturn 1800
+ @player.caption.updatePlayTime = jasmine.createSpy('VideoCaptionAlpha.updatePlayTime')
+ @player.progressSlider.updatePlayTime = jasmine.createSpy('VideoProgressSliderAlpha.updatePlayTime')
+ @player.updatePlayTime 60
+
+ it 'update the video playback time', ->
+ expect($('.vidtime')).toHaveHtml '1:00 / 30:00'
+
+ it 'update the playback time on caption', ->
+ expect(@player.caption.updatePlayTime).toHaveBeenCalledWith 60
+
+ it 'update the playback time on progress slider', ->
+ expect(@player.progressSlider.updatePlayTime).toHaveBeenCalledWith 60, 1800
+
+ describe 'toggleFullScreen', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ @player = new VideoPlayerAlpha video: @video
+ @player.caption.resize = jasmine.createSpy('VideoCaptionAlpha.resize')
+
+ describe 'when the video player is not full screen', ->
+ beforeEach ->
+ spyOn @video, 'log'
+ @player.el.removeClass 'fullscreen'
+ @player.toggleFullScreen(jQuery.Event("click"))
+
+ it 'log the fullscreen event', ->
+ expect(@video.log).toHaveBeenCalledWith 'fullscreen',
+ currentTime: @player.currentTime
+
+ it 'replace the full screen button tooltip', ->
+ expect($('.add-fullscreen')).toHaveAttr 'title', 'Exit fill browser'
+
+ it 'add the fullscreen class', ->
+ expect(@player.el).toHaveClass 'fullscreen'
+
+ it 'tell VideoCaption to resize', ->
+ expect(@player.caption.resize).toHaveBeenCalled()
+
+ describe 'when the video player already full screen', ->
+ beforeEach ->
+ spyOn @video, 'log'
+ @player.el.addClass 'fullscreen'
+ @player.toggleFullScreen(jQuery.Event("click"))
+
+ it 'log the not_fullscreen event', ->
+ expect(@video.log).toHaveBeenCalledWith 'not_fullscreen',
+ currentTime: @player.currentTime
+
+ it 'replace the full screen button tooltip', ->
+ expect($('.add-fullscreen')).toHaveAttr 'title', 'Fill browser'
+
+ it 'remove exit full screen button', ->
+ expect(@player.el).not.toContain 'a.exit'
+
+ it 'remove the fullscreen class', ->
+ expect(@player.el).not.toHaveClass 'fullscreen'
+
+ it 'tell VideoCaption to resize', ->
+ expect(@player.caption.resize).toHaveBeenCalled()
+
+ describe 'play', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ @player = new VideoPlayerAlpha video: @video
+
+ describe 'when the player is not ready', ->
+ beforeEach ->
+ @player.player.playVideo = undefined
+ @player.play()
+
+ it 'does nothing', ->
+ expect(@player.player.playVideo).toBeUndefined()
+
+ describe 'when the player is ready', ->
+ beforeEach ->
+ @player.player.playVideo.andReturn true
+ @player.play()
+
+ it 'delegate to the Youtube player', ->
+ expect(@player.player.playVideo).toHaveBeenCalled()
+
+ describe 'isPlaying', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ @player = new VideoPlayerAlpha video: @video
+
+ describe 'when the video is playing', ->
+ beforeEach ->
+ @player.player.getPlayerState.andReturn YT.PlayerState.PLAYING
+
+ it 'return true', ->
+ expect(@player.isPlaying()).toBeTruthy()
+
+ describe 'when the video is not playing', ->
+ beforeEach ->
+ @player.player.getPlayerState.andReturn YT.PlayerState.PAUSED
+
+ it 'return false', ->
+ expect(@player.isPlaying()).toBeFalsy()
+
+ describe 'pause', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ @player = new VideoPlayerAlpha video: @video
+ @player.pause()
+
+ it 'delegate to the Youtube player', ->
+ expect(@player.player.pauseVideo).toHaveBeenCalled()
+
+ describe 'duration', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ @player = new VideoPlayerAlpha video: @video
+ spyOn @video, 'getDuration'
+ @player.duration()
+
+ it 'delegate to the video', ->
+ expect(@video.getDuration).toHaveBeenCalled()
+
+ describe 'currentSpeed', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ @player = new VideoPlayerAlpha video: @video
+ @video.speed = '3.0'
+
+ it 'delegate to the video', ->
+ expect(@player.currentSpeed()).toEqual '3.0'
+
+ describe 'volume', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @, [], false
+ $('.video').append $('')
+ @player = new VideoPlayerAlpha video: @video
+ @player.player.getVolume.andReturn 42
+
+ describe 'without value', ->
+ it 'return current volume', ->
+ expect(@player.volume()).toEqual 42
+
+ describe 'with value', ->
+ it 'set player volume', ->
+ @player.volume(60)
+ expect(@player.player.setVolume).toHaveBeenCalledWith(60)
diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_progress_slider_spec.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_progress_slider_spec.coffee
new file mode 100644
index 0000000000..dd787aefbb
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_progress_slider_spec.coffee
@@ -0,0 +1,165 @@
+describe 'VideoProgressSliderAlpha', ->
+ beforeEach ->
+ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
+
+ describe 'constructor', ->
+ describe 'on a non-touch based device', ->
+ beforeEach ->
+ spyOn($.fn, 'slider').andCallThrough()
+ @player = jasmine.stubVideoPlayerAlpha @
+ @progressSlider = @player.progressSlider
+
+ it 'build the slider', ->
+ expect(@progressSlider.slider).toBe '.slider'
+ expect($.fn.slider).toHaveBeenCalledWith
+ range: 'min'
+ change: @progressSlider.onChange
+ slide: @progressSlider.onSlide
+ stop: @progressSlider.onStop
+
+ it 'build the seek handle', ->
+ expect(@progressSlider.handle).toBe '.slider .ui-slider-handle'
+ expect($.fn.qtip).toHaveBeenCalledWith
+ content: "0:00"
+ position:
+ my: 'bottom center'
+ at: 'top center'
+ container: @progressSlider.handle
+ hide:
+ delay: 700
+ style:
+ classes: 'ui-tooltip-slider'
+ widget: true
+
+ describe 'on a touch-based device', ->
+ beforeEach ->
+ window.onTouchBasedDevice.andReturn true
+ spyOn($.fn, 'slider').andCallThrough()
+ @player = jasmine.stubVideoPlayerAlpha @
+ @progressSlider = @player.progressSlider
+
+ it 'does not build the slider', ->
+ expect(@progressSlider.slider).toBeUndefined
+ expect($.fn.slider).not.toHaveBeenCalled()
+
+ describe 'play', ->
+ beforeEach ->
+ spyOn(VideoProgressSliderAlpha.prototype, 'buildSlider').andCallThrough()
+ @player = jasmine.stubVideoPlayerAlpha @
+ @progressSlider = @player.progressSlider
+
+ describe 'when the slider was already built', ->
+
+ beforeEach ->
+ @progressSlider.play()
+
+ it 'does not build the slider', ->
+ expect(@progressSlider.buildSlider.calls.length).toEqual 1
+
+ describe 'when the slider was not already built', ->
+ beforeEach ->
+ spyOn($.fn, 'slider').andCallThrough()
+ @progressSlider.slider = null
+ @progressSlider.play()
+
+ it 'build the slider', ->
+ expect(@progressSlider.slider).toBe '.slider'
+ expect($.fn.slider).toHaveBeenCalledWith
+ range: 'min'
+ change: @progressSlider.onChange
+ slide: @progressSlider.onSlide
+ stop: @progressSlider.onStop
+
+ it 'build the seek handle', ->
+ expect(@progressSlider.handle).toBe '.ui-slider-handle'
+ expect($.fn.qtip).toHaveBeenCalledWith
+ content: "0:00"
+ position:
+ my: 'bottom center'
+ at: 'top center'
+ container: @progressSlider.handle
+ hide:
+ delay: 700
+ style:
+ classes: 'ui-tooltip-slider'
+ widget: true
+
+ describe 'updatePlayTime', ->
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ @progressSlider = @player.progressSlider
+
+ describe 'when frozen', ->
+ beforeEach ->
+ spyOn($.fn, 'slider').andCallThrough()
+ @progressSlider.frozen = true
+ @progressSlider.updatePlayTime 20, 120
+
+ it 'does not update the slider', ->
+ expect($.fn.slider).not.toHaveBeenCalled()
+
+ describe 'when not frozen', ->
+ beforeEach ->
+ spyOn($.fn, 'slider').andCallThrough()
+ @progressSlider.frozen = false
+ @progressSlider.updatePlayTime 20, 120
+
+ it 'update the max value of the slider', ->
+ expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 120
+
+ it 'update current value of the slider', ->
+ expect($.fn.slider).toHaveBeenCalledWith 'value', 20
+
+ describe 'onSlide', ->
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ @progressSlider = @player.progressSlider
+ spyOnEvent @progressSlider, 'slide_seek'
+ @progressSlider.onSlide {}, value: 20
+
+ it 'freeze the slider', ->
+ expect(@progressSlider.frozen).toBeTruthy()
+
+ it 'update the tooltip', ->
+ expect($.fn.qtip).toHaveBeenCalled()
+
+ it 'trigger seek event', ->
+ expect('slide_seek').toHaveBeenTriggeredOn @progressSlider
+ expect(@player.currentTime).toEqual 20
+
+ describe 'onChange', ->
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ @progressSlider = @player.progressSlider
+ @progressSlider.onChange {}, value: 20
+
+ it 'update the tooltip', ->
+ expect($.fn.qtip).toHaveBeenCalled()
+
+ describe 'onStop', ->
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ @progressSlider = @player.progressSlider
+ spyOnEvent @progressSlider, 'slide_seek'
+ @progressSlider.onStop {}, value: 20
+
+ it 'freeze the slider', ->
+ expect(@progressSlider.frozen).toBeTruthy()
+
+ it 'trigger seek event', ->
+ expect('slide_seek').toHaveBeenTriggeredOn @progressSlider
+ expect(@player.currentTime).toEqual 20
+
+ it 'set timeout to unfreeze the slider', ->
+ expect(window.setTimeout).toHaveBeenCalledWith jasmine.any(Function), 200
+ window.setTimeout.mostRecentCall.args[0]()
+ expect(@progressSlider.frozen).toBeFalsy()
+
+ describe 'updateTooltip', ->
+ beforeEach ->
+ @player = jasmine.stubVideoPlayerAlpha @
+ @progressSlider = @player.progressSlider
+ @progressSlider.updateTooltip 90
+
+ it 'set the tooltip value', ->
+ expect($.fn.qtip).toHaveBeenCalledWith 'option', 'content.text', '1:30'
diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_speed_control_spec.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_speed_control_spec.coffee
new file mode 100644
index 0000000000..ca4bfe815a
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_speed_control_spec.coffee
@@ -0,0 +1,91 @@
+describe 'VideoSpeedControlAlpha', ->
+ beforeEach ->
+ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
+ jasmine.stubVideoPlayerAlpha @
+ $('.speeds').remove()
+
+ describe 'constructor', ->
+ describe 'always', ->
+ beforeEach ->
+ @speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
+
+ it 'add the video speed control to player', ->
+ secondaryControls = $('.secondary-controls')
+ li = secondaryControls.find('.video_speeds li')
+ expect(secondaryControls).toContain '.speeds'
+ expect(secondaryControls).toContain '.video_speeds'
+ expect(secondaryControls.find('p.active').text()).toBe '1.0x'
+ expect(li.filter('.active')).toHaveData 'speed', @speedControl.currentSpeed
+ expect(li.length).toBe @speedControl.speeds.length
+ $.each li.toArray().reverse(), (index, link) =>
+ expect($(link)).toHaveData 'speed', @speedControl.speeds[index]
+ expect($(link).find('a').text()).toBe @speedControl.speeds[index] + 'x'
+
+ it 'bind to change video speed link', ->
+ expect($('.video_speeds a')).toHandleWith 'click', @speedControl.changeVideoSpeed
+
+ describe 'when running on touch based device', ->
+ beforeEach ->
+ window.onTouchBasedDevice.andReturn true
+ $('.speeds').removeClass 'open'
+ @speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
+
+ it 'open the speed toggle on click', ->
+ $('.speeds').click()
+ expect($('.speeds')).toHaveClass 'open'
+ $('.speeds').click()
+ expect($('.speeds')).not.toHaveClass 'open'
+
+ describe 'when running on non-touch based device', ->
+ beforeEach ->
+ $('.speeds').removeClass 'open'
+ @speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
+
+ it 'open the speed toggle on hover', ->
+ $('.speeds').mouseenter()
+ expect($('.speeds')).toHaveClass 'open'
+ $('.speeds').mouseleave()
+ expect($('.speeds')).not.toHaveClass 'open'
+
+ it 'close the speed toggle on mouse out', ->
+ $('.speeds').mouseenter().mouseleave()
+ expect($('.speeds')).not.toHaveClass 'open'
+
+ it 'close the speed toggle on click', ->
+ $('.speeds').mouseenter().click()
+ expect($('.speeds')).not.toHaveClass 'open'
+
+ describe 'changeVideoSpeed', ->
+ beforeEach ->
+ @speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
+ @video.setSpeed '1.0'
+
+ describe 'when new speed is the same', ->
+ beforeEach ->
+ spyOnEvent @speedControl, 'speedChange'
+ $('li[data-speed="1.0"] a').click()
+
+ it 'does not trigger speedChange event', ->
+ expect('speedChange').not.toHaveBeenTriggeredOn @speedControl
+
+ describe 'when new speed is not the same', ->
+ beforeEach ->
+ @newSpeed = null
+ $(@speedControl).bind 'speedChange', (event, newSpeed) => @newSpeed = newSpeed
+ spyOnEvent @speedControl, 'speedChange'
+ $('li[data-speed="0.75"] a').click()
+
+ it 'trigger speedChange event', ->
+ expect('speedChange').toHaveBeenTriggeredOn @speedControl
+ expect(@newSpeed).toEqual 0.75
+
+ describe 'onSpeedChange', ->
+ beforeEach ->
+ @speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
+ $('li[data-speed="1.0"] a').addClass 'active'
+ @speedControl.setSpeed '0.75'
+
+ it 'set the new speed as active', ->
+ 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'
diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_volume_control_spec.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_volume_control_spec.coffee
new file mode 100644
index 0000000000..4bb9f1cbf8
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_volume_control_spec.coffee
@@ -0,0 +1,94 @@
+describe 'VideoVolumeControlAlpha', ->
+ beforeEach ->
+ jasmine.stubVideoPlayerAlpha @
+ $('.volume').remove()
+
+ describe 'constructor', ->
+ beforeEach ->
+ spyOn($.fn, 'slider')
+ @volumeControl = new VideoVolumeControlAlpha el: $('.secondary-controls')
+
+ it 'initialize currentVolume to 100', ->
+ expect(@volumeControl.currentVolume).toEqual 100
+
+ it 'render the volume control', ->
+ expect($('.secondary-controls').html()).toContain """
+
+
+
+
+
+
+ """
+
+ it 'create the slider', ->
+ expect($.fn.slider).toHaveBeenCalledWith
+ orientation: "vertical"
+ range: "min"
+ min: 0
+ max: 100
+ value: 100
+ change: @volumeControl.onChange
+ slide: @volumeControl.onChange
+
+ it 'bind the volume control', ->
+ expect($('.volume>a')).toHandleWith 'click', @volumeControl.toggleMute
+
+ expect($('.volume')).not.toHaveClass 'open'
+ $('.volume').mouseenter()
+ expect($('.volume')).toHaveClass 'open'
+ $('.volume').mouseleave()
+ expect($('.volume')).not.toHaveClass 'open'
+
+ describe 'onChange', ->
+ beforeEach ->
+ spyOnEvent @volumeControl, 'volumeChange'
+ @newVolume = undefined
+ @volumeControl = new VideoVolumeControlAlpha el: $('.secondary-controls')
+ $(@volumeControl).bind 'volumeChange', (event, volume) => @newVolume = volume
+
+ describe 'when the new volume is more than 0', ->
+ beforeEach ->
+ @volumeControl.onChange undefined, value: 60
+
+ it 'set the player volume', ->
+ expect(@newVolume).toEqual 60
+
+ it 'remote muted class', ->
+ expect($('.volume')).not.toHaveClass 'muted'
+
+ describe 'when the new volume is 0', ->
+ beforeEach ->
+ @volumeControl.onChange undefined, value: 0
+
+ it 'set the player volume', ->
+ expect(@newVolume).toEqual 0
+
+ it 'add muted class', ->
+ expect($('.volume')).toHaveClass 'muted'
+
+ describe 'toggleMute', ->
+ beforeEach ->
+ @newVolume = undefined
+ @volumeControl = new VideoVolumeControlAlpha el: $('.secondary-controls')
+ $(@volumeControl).bind 'volumeChange', (event, volume) => @newVolume = volume
+
+ describe 'when the current volume is more than 0', ->
+ beforeEach ->
+ @volumeControl.currentVolume = 60
+ @volumeControl.toggleMute()
+
+ it 'save the previous volume', ->
+ expect(@volumeControl.previousVolume).toEqual 60
+
+ it 'set the player volume', ->
+ expect(@newVolume).toEqual 0
+
+ describe 'when the current volume is 0', ->
+ beforeEach ->
+ @volumeControl.currentVolume = 0
+ @volumeControl.previousVolume = 60
+ @volumeControl.toggleMute()
+
+ it 'set the player volume to previous volume', ->
+ expect(@newVolume).toEqual 60
diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display_spec.coffee
new file mode 100644
index 0000000000..3715c3d813
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display_spec.coffee
@@ -0,0 +1,286 @@
+describe 'VideoAlpha', ->
+ metadata =
+ slowerSpeedYoutubeId:
+ id: @slowerSpeedYoutubeId
+ duration: 300
+ normalSpeedYoutubeId:
+ id: @normalSpeedYoutubeId
+ duration: 200
+
+ beforeEach ->
+ jasmine.stubRequests()
+ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
+ @videosDefinition = '0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
+ @slowerSpeedYoutubeId = 'slowerSpeedYoutubeId'
+ @normalSpeedYoutubeId = 'normalSpeedYoutubeId'
+
+ afterEach ->
+ window.OldVideoPlayerAlpha = undefined
+ window.onYouTubePlayerAPIReady = undefined
+ window.onHTML5PlayerAPIReady = undefined
+
+ describe 'constructor', ->
+ describe 'YT', ->
+ beforeEach ->
+ loadFixtures 'videoalpha.html'
+ @stubVideoPlayerAlpha = jasmine.createSpy('VideoPlayerAlpha')
+ $.cookie.andReturn '0.75'
+
+ describe 'by default', ->
+ beforeEach ->
+ spyOn(window.VideoAlpha.prototype, 'fetchMetadata').andCallFake ->
+ @metadata = metadata
+ @video = new VideoAlpha '#example', @videosDefinition
+
+ it 'check videoType', ->
+ expect(@video.videoType).toEqual('youtube')
+
+ it 'reset the current video player', ->
+ expect(window.OldVideoPlayerAlpha).toBeUndefined()
+
+ it 'set the elements', ->
+ expect(@video.el).toBe '#video_id'
+
+ it 'parse the videos', ->
+ expect(@video.videos).toEqual
+ '0.75': @slowerSpeedYoutubeId
+ '1.0': @normalSpeedYoutubeId
+
+ it 'fetch the video metadata', ->
+ expect(@video.fetchMetadata).toHaveBeenCalled
+ expect(@video.metadata).toEqual metadata
+
+ it 'parse available video speeds', ->
+ expect(@video.speeds).toEqual ['0.75', '1.0']
+
+ it 'set current video speed via cookie', ->
+ expect(@video.speed).toEqual '0.75'
+
+ it 'store a reference for this video player in the element', ->
+ expect($('.video').data('video')).toEqual @video
+
+ describe 'when the Youtube API is already available', ->
+ beforeEach ->
+ @originalYT = window.YT
+ window.YT = { Player: true }
+ spyOn(window, 'VideoPlayerAlpha').andReturn(@stubVideoPlayerAlpha)
+ @video = new VideoAlpha '#example', @videosDefinition
+
+ afterEach ->
+ window.YT = @originalYT
+
+ it 'create the Video Player', ->
+ expect(window.VideoPlayerAlpha).toHaveBeenCalledWith(video: @video)
+ expect(@video.player).toEqual @stubVideoPlayerAlpha
+
+ describe 'when the Youtube API is not ready', ->
+ beforeEach ->
+ @originalYT = window.YT
+ window.YT = {}
+ @video = new VideoAlpha '#example', @videosDefinition
+
+ afterEach ->
+ window.YT = @originalYT
+
+ it 'set the callback on the window object', ->
+ expect(window.onYouTubePlayerAPIReady).toEqual jasmine.any(Function)
+
+ describe 'when the Youtube API becoming ready', ->
+ beforeEach ->
+ @originalYT = window.YT
+ window.YT = {}
+ spyOn(window, 'VideoPlayerAlpha').andReturn(@stubVideoPlayerAlpha)
+ @video = new VideoAlpha '#example', @videosDefinition
+ window.onYouTubePlayerAPIReady()
+
+ afterEach ->
+ window.YT = @originalYT
+
+ it 'create the Video Player for all video elements', ->
+ expect(window.VideoPlayerAlpha).toHaveBeenCalledWith(video: @video)
+ expect(@video.player).toEqual @stubVideoPlayerAlpha
+
+ describe 'HTML5', ->
+ beforeEach ->
+ loadFixtures 'videoalpha_html5.html'
+ @stubVideoPlayerAlpha = jasmine.createSpy('VideoPlayerAlpha')
+ $.cookie.andReturn '0.75'
+
+ describe 'by default', ->
+ beforeEach ->
+ @originalHTML5 = window.HTML5Video.Player
+ window.HTML5Video.Player = undefined
+ @video = new VideoAlpha '#example', @videosDefinition
+
+ afterEach ->
+ window.HTML5Video.Player = @originalHTML5
+
+ it 'check videoType', ->
+ expect(@video.videoType).toEqual('html5')
+
+ it 'reset the current video player', ->
+ expect(window.OldVideoPlayerAlpha).toBeUndefined()
+
+ it 'set the elements', ->
+ expect(@video.el).toBe '#video_id'
+
+ it 'parse the videos if subtitles exist', ->
+ sub = 'test_name_of_the_subtitles'
+ expect(@video.videos).toEqual
+ '0.75': sub
+ '1.0': sub
+ '1.25': sub
+ '1.5': sub
+
+ it 'parse the videos if subtitles doesn\'t exist', ->
+ $('#example').find('.video').data('sub', '')
+ @video = new VideoAlpha '#example', @videosDefinition
+ sub = ''
+ expect(@video.videos).toEqual
+ '0.75': sub
+ '1.0': sub
+ '1.25': sub
+ '1.5': sub
+
+ it 'parse Html5 sources', ->
+ html5Sources =
+ mp4: 'test.mp4'
+ webm: 'test.webm'
+ ogg: 'test.ogv'
+ expect(@video.html5Sources).toEqual html5Sources
+
+ it 'parse available video speeds', ->
+ speeds = jasmine.stubbedHtml5Speeds
+ expect(@video.speeds).toEqual speeds
+
+ it 'set current video speed via cookie', ->
+ expect(@video.speed).toEqual '0.75'
+
+ it 'store a reference for this video player in the element', ->
+ expect($('.video').data('video')).toEqual @video
+
+ describe 'when the HTML5 API is already available', ->
+ beforeEach ->
+ @originalHTML5Video = window.HTML5Video
+ window.HTML5Video = { Player: true }
+ spyOn(window, 'VideoPlayerAlpha').andReturn(@stubVideoPlayerAlpha)
+ @video = new VideoAlpha '#example', @videosDefinition
+
+ afterEach ->
+ window.HTML5Video = @originalHTML5Video
+
+ it 'create the Video Player', ->
+ expect(window.VideoPlayerAlpha).toHaveBeenCalledWith(video: @video)
+ expect(@video.player).toEqual @stubVideoPlayerAlpha
+
+ describe 'when the HTML5 API is not ready', ->
+ beforeEach ->
+ @originalHTML5Video = window.HTML5Video
+ window.HTML5Video = {}
+ @video = new VideoAlpha '#example', @videosDefinition
+
+ afterEach ->
+ window.HTML5Video = @originalHTML5Video
+
+ it 'set the callback on the window object', ->
+ expect(window.onHTML5PlayerAPIReady).toEqual jasmine.any(Function)
+
+ describe 'when the HTML5 API becoming ready', ->
+ beforeEach ->
+ @originalHTML5Video = window.HTML5Video
+ window.HTML5Video = {}
+ spyOn(window, 'VideoPlayerAlpha').andReturn(@stubVideoPlayerAlpha)
+ @video = new VideoAlpha '#example', @videosDefinition
+ window.onHTML5PlayerAPIReady()
+
+ afterEach ->
+ window.HTML5Video = @originalHTML5Video
+
+ it 'create the Video Player for all video elements', ->
+ expect(window.VideoPlayerAlpha).toHaveBeenCalledWith(video: @video)
+ expect(@video.player).toEqual @stubVideoPlayerAlpha
+
+ describe 'youtubeId', ->
+ beforeEach ->
+ loadFixtures 'videoalpha.html'
+ $.cookie.andReturn '1.0'
+ @video = new VideoAlpha '#example', @videosDefinition
+
+ describe 'with speed', ->
+ it 'return the video id for given speed', ->
+ expect(@video.youtubeId('0.75')).toEqual @slowerSpeedYoutubeId
+ expect(@video.youtubeId('1.0')).toEqual @normalSpeedYoutubeId
+
+ describe 'without speed', ->
+ it 'return the video id for current speed', ->
+ expect(@video.youtubeId()).toEqual @normalSpeedYoutubeId
+
+ describe 'setSpeed', ->
+ describe 'YT', ->
+ beforeEach ->
+ loadFixtures 'videoalpha.html'
+ @video = new VideoAlpha '#example', @videosDefinition
+
+ describe 'when new speed is available', ->
+ beforeEach ->
+ @video.setSpeed '0.75'
+
+ it 'set new speed', ->
+ expect(@video.speed).toEqual '0.75'
+
+ it 'save setting for new speed', ->
+ expect($.cookie).toHaveBeenCalledWith 'video_speed', '0.75', expires: 3650, path: '/'
+
+ describe 'when new speed is not available', ->
+ beforeEach ->
+ @video.setSpeed '1.75'
+
+ it 'set speed to 1.0x', ->
+ expect(@video.speed).toEqual '1.0'
+
+ describe 'HTML5', ->
+ beforeEach ->
+ loadFixtures 'videoalpha_html5.html'
+ @video = new VideoAlpha '#example', @videosDefinition
+
+ describe 'when new speed is available', ->
+ beforeEach ->
+ @video.setSpeed '0.75'
+
+ it 'set new speed', ->
+ expect(@video.speed).toEqual '0.75'
+
+ it 'save setting for new speed', ->
+ expect($.cookie).toHaveBeenCalledWith 'video_speed', '0.75', expires: 3650, path: '/'
+
+ describe 'when new speed is not available', ->
+ beforeEach ->
+ @video.setSpeed '1.75'
+
+ it 'set speed to 1.0x', ->
+ expect(@video.speed).toEqual '1.0'
+
+ describe 'getDuration', ->
+ beforeEach ->
+ loadFixtures 'videoalpha.html'
+ @video = new VideoAlpha '#example', @videosDefinition
+
+ it 'return duration for current video', ->
+ expect(@video.getDuration()).toEqual 200
+
+ describe 'log', ->
+ beforeEach ->
+ loadFixtures 'videoalpha.html'
+ @video = new VideoAlpha '#example', @videosDefinition
+ spyOn Logger, 'log'
+ @video.log 'someEvent', {
+ currentTime: 25,
+ speed: '1.0'
+ }
+
+ it 'call the logger with valid extra parameters', ->
+ expect(Logger.log).toHaveBeenCalledWith 'someEvent',
+ id: 'id'
+ code: @normalSpeedYoutubeId
+ currentTime: 25
+ speed: '1.0'
diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee
index ff61a9a459..317979bb4d 100644
--- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee
+++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee
@@ -37,7 +37,7 @@ class @VideoCaptionAlpha extends SubviewAlpha
@loaded = true
if onTouchBasedDevice()
- $('.subtitles li').html "Caption will be displayed when you start playing the video."
+ $('.subtitles').html "
Caption will be displayed when you start playing the video.
"
else
@renderCaption()
diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee
index 31dd115fa6..7019386e04 100644
--- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee
+++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee
@@ -6,7 +6,7 @@ class @VideoPlayerAlpha extends SubviewAlpha
# we must pause the player (stop setInterval() method).
if (window.OldVideoPlayerAlpha) and (window.OldVideoPlayerAlpha.onPause)
window.OldVideoPlayerAlpha.onPause()
- window.OldVideoPlayerAlpha = this
+ window.OldVideoPlayerAlpha = @
if @video.videoType is 'youtube'
@PlayerState = YT.PlayerState
@@ -29,7 +29,7 @@ class @VideoPlayerAlpha extends SubviewAlpha
$(@progressSlider).bind('slide_seek', @onSeek)
if @volumeControl
$(@volumeControl).bind('volumeChange', @onVolumeChange)
- $(document).keyup @bindExitFullScreen
+ $(document.documentElement).keyup @bindExitFullScreen
@$('.add-fullscreen').click @toggleFullScreen
@addToolTip() unless onTouchBasedDevice()
@@ -114,7 +114,7 @@ class @VideoPlayerAlpha extends SubviewAlpha
@video.log 'load_video'
if @video.videoType is 'html5'
@player.setPlaybackRate @video.speed
- if not onTouchBasedDevice() and $('.video:first').data('autoplay') is 'True'
+ if not onTouchBasedDevice() and $('.video:first').data('autoplay') isnt 'False'
$('.video-load-complete:first').data('video').player.play()
onStateChange: (event) =>
@@ -308,7 +308,7 @@ class @VideoPlayerAlpha extends SubviewAlpha
@player.pauseVideo() if @player.pauseVideo
duration: ->
- duration = @player.getDuration()
+ duration = @player.getDuration() if @player.getDuration
if isFinite(duration) is false
duration = @video.getDuration()
duration
diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee
index e9ed9923b0..5197c4938f 100644
--- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee
+++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee
@@ -12,7 +12,7 @@ class @VideoProgressSliderAlpha extends SubviewAlpha
@buildHandle()
buildHandle: ->
- @handle = @$('.slider .ui-slider-handle')
+ @handle = @$('.ui-slider-handle')
@handle.qtip
content: "#{Time.format(@slider.slider('value'))}"
position:
@@ -43,7 +43,7 @@ class @VideoProgressSliderAlpha extends SubviewAlpha
onStop: (event, ui) =>
@frozen = true
- $(@).trigger('seek', ui.value)
+ $(@).trigger('slide_seek', ui.value)
setTimeout (=> @frozen = false), 200
updateTooltip: (value)->
diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml
index dba8bbd0b4..1c25b272a3 100644
--- a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml
+++ b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml
@@ -1,6 +1,6 @@
---
metadata:
- display_name: Video Alpha 1
+ display_name: Video Alpha
version: 1
data: |
diff --git a/common/lib/xmodule/xmodule/tests/test_fields.py b/common/lib/xmodule/xmodule/tests/test_fields.py
index 884f218d6d..f0eb082dcf 100644
--- a/common/lib/xmodule/xmodule/tests/test_fields.py
+++ b/common/lib/xmodule/xmodule/tests/test_fields.py
@@ -3,6 +3,8 @@ import datetime
import unittest
from django.utils.timezone import UTC
from xmodule.fields import Date, Timedelta
+from xmodule.timeinfo import TimeInfo
+import time
class DateTest(unittest.TestCase):
@@ -52,6 +54,18 @@ class DateTest(unittest.TestCase):
self.assertEqual(
datetime.datetime(current.year, 12, 4, 16, 30, tzinfo=UTC()),
DateTest.date.from_json("December 4 16:30"))
+ self.assertIsNone(DateTest.date.from_json("12 12:00"))
+
+ def test_non_std_from_json(self):
+ """
+ Test the non-standard args being passed to from_json
+ """
+ now = datetime.datetime.now(UTC())
+ delta = now - datetime.datetime.fromtimestamp(0, UTC())
+ self.assertEqual(DateTest.date.from_json(delta.total_seconds() * 1000),
+ now)
+ yesterday = datetime.datetime.now(UTC()) - datetime.timedelta(days=-1)
+ self.assertEqual(DateTest.date.from_json(yesterday), yesterday)
def test_to_json(self):
'''
@@ -90,3 +104,12 @@ class TimedeltaTest(unittest.TestCase):
'1 days 46799 seconds',
TimedeltaTest.delta.to_json(datetime.timedelta(days=1, hours=12, minutes=59, seconds=59))
)
+
+
+class TimeInfoTest(unittest.TestCase):
+ def test_time_info(self):
+ due_date = datetime.datetime(2000, 4, 14, 10, tzinfo=UTC())
+ grace_pd_string = '1 day 12 hours 59 minutes 59 seconds'
+ timeinfo = TimeInfo(due_date, grace_pd_string)
+ self.assertEqual(timeinfo.close_date,
+ due_date + Timedelta().from_json(grace_pd_string))
diff --git a/common/lib/xmodule/xmodule/tests/test_logic.py b/common/lib/xmodule/xmodule/tests/test_logic.py
index ff17f88dfc..6fb331b3cf 100644
--- a/common/lib/xmodule/xmodule/tests/test_logic.py
+++ b/common/lib/xmodule/xmodule/tests/test_logic.py
@@ -1,15 +1,14 @@
# -*- coding: utf-8 -*-
+# pylint: disable=W0232
"""Test for Xmodule functional logic."""
import json
import unittest
-from lxml import etree
-
from xmodule.poll_module import PollDescriptor
from xmodule.conditional_module import ConditionalDescriptor
from xmodule.word_cloud_module import WordCloudDescriptor
-from xmodule.videoalpha_module import VideoAlphaDescriptor
+from xmodule.tests import test_system
class PostData:
"""Class which emulate postdata."""
@@ -17,6 +16,7 @@ class PostData:
self.dict_data = dict_data
def getlist(self, key):
+ """Get data by key from `self.dict_data`."""
return self.dict_data.get(key)
@@ -27,9 +27,10 @@ class LogicTest(unittest.TestCase):
def setUp(self):
class EmptyClass:
+ """Empty object."""
pass
- self.system = None
+ self.system = test_system()
self.descriptor = EmptyClass()
self.xmodule_class = self.descriptor_class.module_class
@@ -40,10 +41,12 @@ class LogicTest(unittest.TestCase):
)
def ajax_request(self, dispatch, get):
+ """Call Xmodule.handle_ajax."""
return json.loads(self.xmodule.handle_ajax(dispatch, get))
class PollModuleTest(LogicTest):
+ """Logic tests for Poll Xmodule."""
descriptor_class = PollDescriptor
raw_model_data = {
'poll_answers': {'Yes': 1, 'Dont_know': 0, 'No': 0},
@@ -69,6 +72,7 @@ class PollModuleTest(LogicTest):
class ConditionalModuleTest(LogicTest):
+ """Logic tests for Conditional Xmodule."""
descriptor_class = ConditionalDescriptor
def test_ajax_request(self):
@@ -83,6 +87,7 @@ class ConditionalModuleTest(LogicTest):
class WordCloudModuleTest(LogicTest):
+ """Logic tests for Word Cloud Xmodule."""
descriptor_class = WordCloudDescriptor
raw_model_data = {
'all_words': {'cat': 10, 'dog': 5, 'mom': 1, 'dad': 2},
@@ -91,8 +96,6 @@ class WordCloudModuleTest(LogicTest):
}
def test_bad_ajax_request(self):
-
- # TODO: move top global test. Formalize all our Xmodule errors.
response = self.ajax_request('bad_dispatch', {})
self.assertDictEqual(response, {
'status': 'fail',
@@ -118,34 +121,6 @@ class WordCloudModuleTest(LogicTest):
{'text': 'cat', 'size': 12, 'percent': 54.0}]
)
- self.assertEqual(100.0, sum(i['percent'] for i in response['top_words']) )
-
-
-class VideoAlphaModuleTest(LogicTest):
- descriptor_class = VideoAlphaDescriptor
-
- raw_model_data = {
- 'data': ''
- }
-
- def test_get_timeframe_no_parameters(self):
- xmltree = etree.fromstring('test')
- output = self.xmodule._get_timeframe(xmltree)
- self.assertEqual(output, ('', ''))
-
- def test_get_timeframe_with_one_parameter(self):
- xmltree = etree.fromstring(
- 'test'
- )
- output = self.xmodule._get_timeframe(xmltree)
- self.assertEqual(output, (247, ''))
-
- def test_get_timeframe_with_two_parameters(self):
- xmltree = etree.fromstring(
- '''test'''
- )
- output = self.xmodule._get_timeframe(xmltree)
- self.assertEqual(output, (247, 47079))
+ self.assertEqual(
+ 100.0,
+ sum(i['percent'] for i in response['top_words']))
diff --git a/common/lib/xmodule/xmodule/timeinfo.py b/common/lib/xmodule/xmodule/timeinfo.py
index 9a63c0477d..8f4d99506a 100644
--- a/common/lib/xmodule/xmodule/timeinfo.py
+++ b/common/lib/xmodule/xmodule/timeinfo.py
@@ -1,6 +1,5 @@
-from .timeparse import parse_timedelta
-
import logging
+from xmodule.fields import Timedelta
log = logging.getLogger(__name__)
class TimeInfo(object):
@@ -14,6 +13,7 @@ class TimeInfo(object):
self.close_date - the real due date
"""
+ _delta_standin = Timedelta()
def __init__(self, due_date, grace_period_string):
if due_date is not None:
self.display_due_date = due_date
@@ -23,7 +23,7 @@ class TimeInfo(object):
if grace_period_string is not None and self.display_due_date:
try:
- self.grace_period = parse_timedelta(grace_period_string)
+ self.grace_period = TimeInfo._delta_standin.from_json(grace_period_string)
self.close_date = self.display_due_date + self.grace_period
except:
log.error("Error parsing the grace period {0}".format(grace_period_string))
diff --git a/common/lib/xmodule/xmodule/timeparse.py b/common/lib/xmodule/xmodule/timeparse.py
deleted file mode 100644
index b189262761..0000000000
--- a/common/lib/xmodule/xmodule/timeparse.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""
-Helper functions for handling time in the format we like.
-"""
-import re
-from datetime import timedelta, datetime
-
-TIME_FORMAT = "%Y-%m-%dT%H:%M"
-
-TIMEDELTA_REGEX = re.compile(r'^((?P\d+?) day(?:s?))?(\s)?((?P\d+?) hour(?:s?))?(\s)?((?P\d+?) minute(?:s)?)?(\s)?((?P\d+?) second(?:s)?)?$')
-
-def parse_time(time_str):
- """
- Takes a time string in TIME_FORMAT
-
- Returns it as a time_struct.
-
- Raises ValueError if the string is not in the right format.
- """
- return datetime.strptime(time_str, TIME_FORMAT)
-
-
-def stringify_time(dt):
- """
- Convert a datetime struct to a string
- """
- return dt.isoformat()
-
-def parse_timedelta(time_str):
- """
- time_str: A string with the following components:
- day[s] (optional)
- hour[s] (optional)
- minute[s] (optional)
- second[s] (optional)
-
- Returns a datetime.timedelta parsed from the string
- """
- parts = TIMEDELTA_REGEX.match(time_str)
- if not parts:
- return
- parts = parts.groupdict()
- time_params = {}
- for (name, param) in parts.iteritems():
- if param:
- time_params[name] = int(param)
- return timedelta(**time_params)
diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py
index 43de021799..a64e094a58 100644
--- a/common/lib/xmodule/xmodule/videoalpha_module.py
+++ b/common/lib/xmodule/xmodule/videoalpha_module.py
@@ -1,3 +1,15 @@
+# pylint: disable=W0223
+"""VideoAlpha is ungraded Xmodule for support video content.
+It's new improved video module, which support additional feature:
+
+- Can play non-YouTube video sources via in-browser HTML5 video player.
+- YouTube defaults to HTML5 mode from the start.
+- Speed changes in both YouTube and non-YouTube videos happen via
+in-browser HTML5 video method (when in HTML5 mode).
+- Navigational subtitles can be disabled altogether via an attribute
+in XML.
+"""
+
import json
import logging
@@ -21,6 +33,7 @@ log = logging.getLogger(__name__)
class VideoAlphaFields(object):
+ """Fields for `VideoAlphaModule` and `VideoAlphaDescriptor`."""
data = String(help="XML data for the problem", scope=Scope.content)
position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
display_name = String(help="Display name for this module", scope=Scope.settings)
@@ -68,7 +81,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
'ogv': self._get_source(xmltree, ['ogv']),
}
self.track = self._get_track(xmltree)
- self.start_time, self.end_time = self._get_timeframe(xmltree)
+ self.start_time, self.end_time = self.get_timeframe(xmltree)
def _get_source(self, xmltree, exts=None):
"""Find the first valid source, which ends with one of `exts`."""
@@ -77,7 +90,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
return self._get_first_external(xmltree, 'source', condition)
def _get_track(self, xmltree):
- # find the first valid track
+ """Find the first valid track."""
return self._get_first_external(xmltree, 'track')
def _get_first_external(self, xmltree, tag, condition=bool):
@@ -93,39 +106,33 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
break
return result
- def _get_timeframe(self, xmltree):
+ def get_timeframe(self, xmltree):
""" Converts 'start_time' and 'end_time' parameters in video tag to seconds.
If there are no parameters, returns empty string. """
- def parse_time(s):
+ def parse_time(str_time):
"""Converts s in '12:34:45' format to seconds. If s is
None, returns empty string"""
- if s is None:
+ if str_time is None:
return ''
else:
- x = time.strptime(s, '%H:%M:%S')
+ obj_time = time.strptime(str_time, '%H:%M:%S')
return datetime.timedelta(
- hours=x.tm_hour,
- minutes=x.tm_min,
- seconds=x.tm_sec
+ hours=obj_time.tm_hour,
+ minutes=obj_time.tm_min,
+ seconds=obj_time.tm_sec
).total_seconds()
return parse_time(xmltree.get('start_time')), parse_time(xmltree.get('end_time'))
def handle_ajax(self, dispatch, get):
- """Handle ajax calls to this video.
- TODO (vshnayder): This is not being called right now, so the
- position is not being saved.
- """
+ """This is not being called right now and we raise 404 error."""
log.debug(u"GET {0}".format(get))
log.debug(u"DISPATCH {0}".format(dispatch))
- if dispatch == 'goto_position':
- self.position = int(float(get['position']))
- log.info(u"NEW POSITION {0}".format(self.position))
- return json.dumps({'success': True})
raise Http404()
def get_instance_state(self):
+ """Return information about state (position)."""
return json.dumps({'position': self.position})
def get_html(self):
@@ -143,7 +150,8 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
'sources': self.sources,
'track': self.track,
'display_name': self.display_name_with_default,
- # TODO (cpennington): This won't work when we move to data that isn't on the filesystem
+ # This won't work when we move to data that
+ # isn't on the filesystem
'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': caption_asset_path,
'show_captions': self.show_captions,
@@ -154,5 +162,6 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
class VideoAlphaDescriptor(VideoAlphaFields, RawDescriptor):
+ """Descriptor for `VideoAlphaModule`."""
module_class = VideoAlphaModule
template_dir_name = "videoalpha"
diff --git a/lms/djangoapps/courseware/features/navigation.feature b/lms/djangoapps/courseware/features/navigation.feature
new file mode 100644
index 0000000000..8fd8b54c1a
--- /dev/null
+++ b/lms/djangoapps/courseware/features/navigation.feature
@@ -0,0 +1,25 @@
+Feature: Navigate Course
+ As a student in an edX course
+ In order to view the course properly
+ I want to be able to navigate through the content
+
+ Scenario: I can navigate to a section
+ Given I am viewing a course with multiple sections
+ When I click on section "2"
+ Then I should see the content of section "2"
+
+ Scenario: I can navigate to subsections
+ Given I am viewing a section with multiple subsections
+ When I click on subsection "2"
+ Then I should see the content of subsection "2"
+
+ Scenario: I can navigate to sequences
+ Given I am viewing a section with multiple sequences
+ When I click on sequence "2"
+ Then I should see the content of sequence "2"
+
+ Scenario: I can go back to where I was after I log out and back in
+ Given I am viewing a course with multiple sections
+ When I click on section "2"
+ And I return later
+ Then I should see that I was most recently in section "2"
diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py
new file mode 100644
index 0000000000..edd748e46f
--- /dev/null
+++ b/lms/djangoapps/courseware/features/navigation.py
@@ -0,0 +1,186 @@
+#pylint: disable=C0111
+#pylint: disable=W0621
+
+from lettuce import world, step
+from django.contrib.auth.models import User
+from lettuce.django import django_url
+from student.models import CourseEnrollment
+from common import course_id, course_location
+from problems_setup import PROBLEM_DICT
+
+TEST_COURSE_ORG = 'edx'
+TEST_COURSE_NAME = 'Test Course'
+TEST_SECTION_NAME = 'Test Section'
+TEST_SUBSECTION_NAME = 'Test Subsection'
+
+
+@step(u'I am viewing a course with multiple sections')
+def view_course_multiple_sections(step):
+ create_course()
+ # Add a section to the course to contain problems
+ section1 = world.ItemFactory.create(parent_location=course_location('model_course'),
+ display_name=section_name(1))
+
+ # Add a section to the course to contain problems
+ section2 = world.ItemFactory.create(parent_location=course_location('model_course'),
+ display_name=section_name(2))
+
+ place1 = world.ItemFactory.create(parent_location=section1.location,
+ template='i4x://edx/templates/sequential/Empty',
+ display_name=subsection_name(1))
+
+ place2 = world.ItemFactory.create(parent_location=section2.location,
+ template='i4x://edx/templates/sequential/Empty',
+ display_name=subsection_name(2))
+
+ add_problem_to_course_section('model_course', 'multiple choice', place1.location)
+ add_problem_to_course_section('model_course', 'drop down', place2.location)
+
+ create_user_and_visit_course()
+
+
+@step(u'I am viewing a section with multiple subsections')
+def view_course_multiple_subsections(step):
+ create_course()
+
+ # Add a section to the course to contain problems
+ section1 = world.ItemFactory.create(parent_location=course_location('model_course'),
+ display_name=section_name(1))
+
+ place1 = world.ItemFactory.create(parent_location=section1.location,
+ template='i4x://edx/templates/sequential/Empty',
+ display_name=subsection_name(1))
+
+ place2 = world.ItemFactory.create(parent_location=section1.location,
+ display_name=subsection_name(2))
+
+ add_problem_to_course_section('model_course', 'multiple choice', place1.location)
+ add_problem_to_course_section('model_course', 'drop down', place2.location)
+
+ create_user_and_visit_course()
+
+
+@step(u'I am viewing a section with multiple sequences')
+def view_course_multiple_sequences(step):
+ create_course()
+ # Add a section to the course to contain problems
+ section1 = world.ItemFactory.create(parent_location=course_location('model_course'),
+ display_name=section_name(1))
+
+ place1 = world.ItemFactory.create(parent_location=section1.location,
+ template='i4x://edx/templates/sequential/Empty',
+ display_name=subsection_name(1))
+
+ add_problem_to_course_section('model_course', 'multiple choice', place1.location)
+ add_problem_to_course_section('model_course', 'drop down', place1.location)
+
+ create_user_and_visit_course()
+
+
+@step(u'I click on section "([^"]*)"$')
+def click_on_section(step, section):
+ section_css = 'h3[tabindex="-1"]'
+ world.css_click(section_css)
+
+ subid = "ui-accordion-accordion-panel-" + str(int(section) - 1)
+ subsection_css = 'ul[id="%s"]> li > a' % subid
+ #for some reason needed to do it in two steps
+ world.css_find(subsection_css).click()
+
+
+@step(u'I click on subsection "([^"]*)"$')
+def click_on_subsection(step, subsection):
+ subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]> li > a'
+ world.css_find(subsection_css)[int(subsection) - 1].click()
+
+
+@step(u'I click on sequence "([^"]*)"$')
+def click_on_sequence(step, sequence):
+ sequence_css = 'a[data-element="%s"]' % sequence
+ world.css_click(sequence_css)
+
+
+@step(u'I should see the content of (?:sub)?section "([^"]*)"$')
+def see_section_content(step, section):
+ if section == "2":
+ text = 'The correct answer is Option 2'
+ elif section == "1":
+ text = 'The correct answer is Choice 3'
+ step.given('I should see "' + text + '" somewhere on the page')
+
+
+@step(u'I should see the content of sequence "([^"]*)"$')
+def see_sequence_content(step, sequence):
+ step.given('I should see the content of section "2"')
+
+
+@step(u'I return later')
+def return_to_course(step):
+ step.given('I visit the homepage')
+ world.click_link("View Course")
+ world.click_link("Courseware")
+
+
+@step(u'I should see that I was most recently in section "([^"]*)"$')
+def see_recent_section(step, section):
+ step.given('I should see "You were most recently in %s" somewhere on the page' % subsection_name(int(section)))
+
+#####################
+# HELPERS
+#####################
+
+
+def section_name(section):
+ return TEST_SECTION_NAME + str(section)
+
+
+def subsection_name(section):
+ return TEST_SUBSECTION_NAME + str(section)
+
+
+def create_course():
+ world.clear_courses()
+
+ world.CourseFactory.create(org=TEST_COURSE_ORG,
+ number="model_course",
+ display_name=TEST_COURSE_NAME)
+
+
+def create_user_and_visit_course():
+ world.create_user('robot')
+ u = User.objects.get(username='robot')
+
+ CourseEnrollment.objects.get_or_create(user=u, course_id=course_id("model_course"))
+
+ world.log_in('robot', 'test')
+ chapter_name = (TEST_SECTION_NAME + "1").replace(" ", "_")
+ section_name = (TEST_SUBSECTION_NAME + "1").replace(" ", "_")
+ url = django_url('/courses/edx/model_course/Test_Course/courseware/%s/%s' %
+ (chapter_name, section_name))
+
+ world.browser.visit(url)
+
+
+def add_problem_to_course_section(course, problem_type, parent_location, extraMeta=None):
+ '''
+ Add a problem to the course we have created using factories.
+ '''
+
+ assert(problem_type in PROBLEM_DICT)
+
+ # Generate the problem XML using capa.tests.response_xml_factory
+ factory_dict = PROBLEM_DICT[problem_type]
+ problem_xml = factory_dict['factory'].build_xml(**factory_dict['kwargs'])
+ metadata = {'rerandomize': 'always'} if not 'metadata' in factory_dict else factory_dict['metadata']
+ if extraMeta:
+ metadata = dict(metadata, **extraMeta)
+
+ # Create a problem item using our generated XML
+ # We set rerandomize=always in the metadata so that the "Reset" button
+ # will appear.
+ template_name = "i4x://edx/templates/problem/Blank_Common_Problem"
+ world.ItemFactory.create(parent_location=parent_location,
+ template=template_name,
+ display_name=str(problem_type),
+ data=problem_xml,
+ metadata=metadata)
diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py
index cc53bf735a..1cb403018c 100644
--- a/lms/djangoapps/courseware/tests/__init__.py
+++ b/lms/djangoapps/courseware/tests/__init__.py
@@ -25,8 +25,8 @@ class BaseTestXmodule(ModuleStoreTestCase):
"""Base class for testing Xmodules with mongo store.
This class prepares course and users for tests:
- 1. create test course
- 2. create, enrol and login users for this course
+ 1. create test course;
+ 2. create, enrol and login users for this course;
Any xmodule should overwrite only next parameters for test:
1. TEMPLATE_NAME
diff --git a/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py b/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py
new file mode 100644
index 0000000000..a6bff60acf
--- /dev/null
+++ b/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+"""Video xmodule tests in mongo."""
+
+from . import BaseTestXmodule
+from .test_videoalpha_xml import SOURCE_XML
+from django.conf import settings
+
+
+class TestVideo(BaseTestXmodule):
+ """Integration tests: web client + mongo."""
+
+ TEMPLATE_NAME = "i4x://edx/templates/videoalpha/Video_Alpha"
+ DATA = SOURCE_XML
+ MODEL_DATA = {
+ 'data': DATA
+ }
+
+ def test_handle_ajax_dispatch(self):
+ responses = {
+ user.username: self.clients[user.username].post(
+ self.get_url('whatever'),
+ {},
+ HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ for user in self.users
+ }
+
+ self.assertEqual(
+ set([
+ response.status_code
+ for _, response in responses.items()
+ ]).pop(),
+ 404)
+
+ def test_videoalpha_constructor(self):
+ """Make sure that all parameters extracted correclty from xml"""
+
+ # `get_html` return only context, cause we
+ # overwrite `system.render_template`
+ context = self.item_module.get_html()
+ expected_context = {
+ 'data_dir': getattr(self, 'data_dir', None),
+ 'caption_asset_path': '/c4x/MITx/999/asset/subs_',
+ 'show_captions': self.item_module.show_captions,
+ 'display_name': self.item_module.display_name_with_default,
+ 'end': self.item_module.end_time,
+ 'id': self.item_module.location.html_id(),
+ 'sources': self.item_module.sources,
+ 'start': self.item_module.start_time,
+ 'sub': self.item_module.sub,
+ 'track': self.item_module.track,
+ 'youtube_streams': self.item_module.youtube_streams,
+ 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
+ }
+ self.assertDictEqual(context, expected_context)
diff --git a/lms/djangoapps/courseware/tests/test_videoalpha_xml.py b/lms/djangoapps/courseware/tests/test_videoalpha_xml.py
new file mode 100644
index 0000000000..44e0a7811a
--- /dev/null
+++ b/lms/djangoapps/courseware/tests/test_videoalpha_xml.py
@@ -0,0 +1,129 @@
+# -*- coding: utf-8 -*-
+"""Test for VideoAlpha Xmodule functional logic.
+These tests data readed from xml, not from mongo.
+
+We have a ModuleStoreTestCase class defined in
+common/lib/xmodule/xmodule/modulestore/tests/django_utils.py.
+You can search for usages of this in the cms and lms tests for examples.
+You use this so that it will do things like point the modulestore
+setting to mongo, flush the contentstore before and after, load the
+templates, etc.
+You can then use the CourseFactory and XModuleItemFactory as defined in
+common/lib/xmodule/xmodule/modulestore/tests/factories.py to create the
+course, section, subsection, unit, etc.
+"""
+
+import json
+import unittest
+from mock import Mock
+from lxml import etree
+
+from django.conf import settings
+
+from xmodule.videoalpha_module import VideoAlphaDescriptor, VideoAlphaModule
+from xmodule.modulestore import Location
+from xmodule.tests import test_system
+from xmodule.tests.test_logic import LogicTest
+
+
+SOURCE_XML = """
+
+
+
+
+
+"""
+
+
+class VideoAlphaFactory(object):
+ """A helper class to create videoalpha modules with various parameters
+ for testing.
+ """
+
+ # tag that uses youtube videos
+ sample_problem_xml_youtube = SOURCE_XML
+
+ @staticmethod
+ def create():
+ """Method return VideoAlpha Xmodule instance."""
+ location = Location(["i4x", "edX", "videoalpha", "default",
+ "SampleProblem1"])
+ model_data = {'data': VideoAlphaFactory.sample_problem_xml_youtube}
+
+ descriptor = Mock(weight="1")
+
+ system = test_system()
+ system.render_template = lambda template, context: context
+ VideoAlphaModule.location = location
+ module = VideoAlphaModule(system, descriptor, model_data)
+
+ return module
+
+
+class VideoAlphaModuleTest(LogicTest):
+ """Tests for logic of VideoAlpha Xmodule."""
+
+ descriptor_class = VideoAlphaDescriptor
+
+ raw_model_data = {
+ 'data': ''
+ }
+
+ def test_get_timeframe_no_parameters(self):
+ xmltree = etree.fromstring('test')
+ output = self.xmodule.get_timeframe(xmltree)
+ self.assertEqual(output, ('', ''))
+
+ def test_get_timeframe_with_one_parameter(self):
+ xmltree = etree.fromstring(
+ 'test'
+ )
+ output = self.xmodule.get_timeframe(xmltree)
+ self.assertEqual(output, (247, ''))
+
+ def test_get_timeframe_with_two_parameters(self):
+ xmltree = etree.fromstring(
+ '''test'''
+ )
+ output = self.xmodule.get_timeframe(xmltree)
+ self.assertEqual(output, (247, 47079))
+
+
+class VideoAlphaModuleUnitTest(unittest.TestCase):
+ """Unit tests for VideoAlpha Xmodule."""
+
+ def test_videoalpha_constructor(self):
+ """Make sure that all parameters extracted correclty from xml"""
+ module = VideoAlphaFactory.create()
+
+ # `get_html` return only context, cause we
+ # overwrite `system.render_template`
+ context = module.get_html()
+ expected_context = {
+ 'caption_asset_path': '/static/subs/',
+ 'sub': module.sub,
+ 'data_dir': getattr(self, 'data_dir', None),
+ 'display_name': module.display_name_with_default,
+ 'end': module.end_time,
+ 'start': module.start_time,
+ 'id': module.location.html_id(),
+ 'show_captions': module.show_captions,
+ 'sources': module.sources,
+ 'youtube_streams': module.youtube_streams,
+ 'track': module.track,
+ 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
+ }
+ self.assertDictEqual(context, expected_context)
+
+ self.assertDictEqual(
+ json.loads(module.get_instance_state()),
+ {'position': 0})
diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py
index 6e9f6c1f71..496c834950 100644
--- a/lms/djangoapps/django_comment_client/utils.py
+++ b/lms/djangoapps/django_comment_client/utils.py
@@ -1,9 +1,9 @@
+import pytz
from collections import defaultdict
import logging
import urllib
from datetime import datetime
-from courseware.module_render import get_module
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.db import connection
@@ -169,7 +169,9 @@ def initialize_discussion_info(course):
category = " / ".join([x.strip() for x in category.split("/")])
last_category = category.split("/")[-1]
discussion_id_map[id] = {"location": module.location, "title": last_category + " / " + title}
- unexpanded_category_map[category].append({"title": title, "id": id, "sort_key": sort_key, "start_date": module.lms.start})
+ #Handle case where module.lms.start is None
+ entry_start_date = module.lms.start if module.lms.start else datetime.max.replace(tzinfo=pytz.UTC)
+ unexpanded_category_map[category].append({"title": title, "id": id, "sort_key": sort_key, "start_date": entry_start_date})
category_map = {"entries": defaultdict(dict), "subcategories": defaultdict(dict)}
for category_path, entries in unexpanded_category_map.items():
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index b9d3f58e8f..c8c49c2b1e 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -179,7 +179,7 @@ with open(ENV_ROOT / CONFIG_PREFIX + "auth.json") as auth_file:
AUTH_TOKENS = json.load(auth_file)
############### Mixed Related(Secure/Not-Secure) Items ##########
-# If segment.io key specified, load it and turn on segment IO if the feature flag is set
+# If Segment.io key specified, load it and enable Segment.io if the feature flag is set
SEGMENT_IO_LMS_KEY = AUTH_TOKENS.get('SEGMENT_IO_LMS_KEY')
if SEGMENT_IO_LMS_KEY:
MITX_FEATURES['SEGMENT_IO_LMS'] = ENV_TOKENS.get('SEGMENT_IO_LMS', False)
diff --git a/test_root/data/videoalpha/gizmo.mp4 b/test_root/data/videoalpha/gizmo.mp4
new file mode 100644
index 0000000000..1fc478842f
Binary files /dev/null and b/test_root/data/videoalpha/gizmo.mp4 differ
diff --git a/test_root/data/videoalpha/gizmo.ogv b/test_root/data/videoalpha/gizmo.ogv
new file mode 100644
index 0000000000..2c4a447f1f
Binary files /dev/null and b/test_root/data/videoalpha/gizmo.ogv differ
diff --git a/test_root/data/videoalpha/gizmo.webm b/test_root/data/videoalpha/gizmo.webm
new file mode 100644
index 0000000000..95d5031a86
Binary files /dev/null and b/test_root/data/videoalpha/gizmo.webm differ