From 1ac6e1230434e8c8de7c3ebb4f0079c30ed45cbc Mon Sep 17 00:00:00 2001 From: Slater-Victoroff Date: Wed, 5 Jun 2013 10:22:18 -0400 Subject: [PATCH 01/16] Pyes working, considering switch to raw requests, phonetic and fuzzy search both working --- common/djangoapps/search/__init__.py | 0 common/djangoapps/search/index.py | 82 ++++++++++++++++++++++++++ common/djangoapps/search/mapping.json | 24 ++++++++ common/djangoapps/search/views.py | 83 +++++++++++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 common/djangoapps/search/__init__.py create mode 100644 common/djangoapps/search/index.py create mode 100644 common/djangoapps/search/mapping.json create mode 100644 common/djangoapps/search/views.py diff --git a/common/djangoapps/search/__init__.py b/common/djangoapps/search/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/search/index.py b/common/djangoapps/search/index.py new file mode 100644 index 0000000000..7012def8cb --- /dev/null +++ b/common/djangoapps/search/index.py @@ -0,0 +1,82 @@ +import os +import os.path as pt +import json +import re +import string + +from pyes import * +import nltk.stem.snowball as snowball +import fuzzy + + +def grab_transcripts(sjson_directory): + """Returns referenes to all of the files contained within a subs directory""" + all_children = [child for child in os.listdir(sjson_directory)] + all_transcripts = [child for child in all_children if pt.isfile(pt.join(sjson_directory, child))] + # . is not a valid character for a youtube id, so it can be reliably used to pick up the start + # of the file extension + uuids = [transcript_id[:transcript_id.find(".")] for transcript_id in all_transcripts] + parsed_transcripts = [open(pt.join(sjson_directory, transcript)).read() for transcript in all_transcripts] + return zip([clean_transcript(transcript) for transcript in parsed_transcripts], uuids) + + +def clean_transcript(transcript_string): + """Tries to parse and clean a raw transcript. Errors for invalid sjson""" + transcript_list = filter(None, json.loads(transcript_string)['text']) + relevant_text = " ".join([phrase.encode('utf-8').strip() for phrase in transcript_list]) + relevant_text = relevant_text.lower().translate(None, string.punctuation) + cleanedText = re.sub('\n', " ", relevant_text) + return cleanedText + + +def phonetic_transcript(clean_transcript, stemmer): + return " ".join([phoneticize(word, stemmer) for word in clean_transcript.split(" ")]) + + +def phoneticize(word, stemmer): + encode = lambda word: word.decode('utf-8').encode('ascii', 'ignore') + phonetic = lambda word: fuzzy.nysiis(stemmer.stem(encode(word))) + return phonetic(word) + + +def initialize_transcripts(database, mapping): + database.indices.create_index("transcript-index") + + +def index_course(database, sjson_directory, course_name, mapping): + stemmer = snowball.EnglishStemmer() + database.put_mapping(course_name, {'properties': mapping}, "transcript-index") + all_transcripts = grab_transcripts(sjson_directory) + video_counter = 0 + for transcript_tuple in all_transcripts: + data_map = {"searchable_text": transcript_tuple[0], "uuid": transcript_tuple[1]} + data_map['phonetic_text'] = phonetic_transcript(transcript_tuple[0], stemmer) + database.index(data_map, "transcript-index", course_name) + video_counter += 1 + database.indices.refresh("transcript-index") + + +def fuzzy_search(database, query, course_name): + search_query = FuzzyLikeThisFieldQuery("searchable_text", query) + return database.search(query=search_query, indices="transcript-index") + + +def phonetic_search(database, query, course_name): + stemmer = snowball.EnglishStemmer() + search_query = TextQuery("phonetic_text", phoneticize(query, stemmer)) + return database.search(query=search_query, indices="transcript-index") + + +data_directory = '/Users/climatologist/edx_all/data/content-mit-6002x/static/subs/' +mapping_directory = 'mapping.json' +database = ES('127.0.0.1:9200') +mapping = json.loads(open(mapping_directory, 'rb').read()) + +#initialize_transcripts(database, mapping) +#index_course(database, data_directory, "test-course", mapping) +fuzzy_results = fuzzy_search(database, "gaussian", "test-course") +phonetic_results = phonetic_search(database, "gaussian", "test-course") +for r in fuzzy_results: + print "Fuzzy: " + r['uuid'] +for r in phonetic_results: + print "Phonetic: " + r['uuid'] diff --git a/common/djangoapps/search/mapping.json b/common/djangoapps/search/mapping.json new file mode 100644 index 0000000000..8d4deade6d --- /dev/null +++ b/common/djangoapps/search/mapping.json @@ -0,0 +1,24 @@ +{ + + "searchable_text": { + "boost": 1.0, + "index": "analyzed", + "store": "yes", + "type": "string", + "term_vector": "with_positions_offsets" + }, + + "phonetic_text": { + "boost": 1.0, + "index": "analyzed", + "store": "yes", + "type": "string", + "term_vector": "with_positions_offsets" + }, + + "uuid": { + "index": "not_analyzed", + "store": "yes", + "type": "string" + } +} \ No newline at end of file diff --git a/common/djangoapps/search/views.py b/common/djangoapps/search/views.py new file mode 100644 index 0000000000..f15dc9266b --- /dev/null +++ b/common/djangoapps/search/views.py @@ -0,0 +1,83 @@ +from django.http import HttpResponse +from django.template.loader import get_template +from django.template import Context +from django.contrib.auth.models import User +from django.contrib.staticfiles import finders +from courseware.courses import get_courses +from courseware.model_data import ModelDataCache +from courseware.module_render import get_module_for_descriptor + +from courseware.views import registered_for_course +#import logging +import lxml +import re +import posixpath +import urllib +from os import listdir +from os.path import isfile +from os.path import join + + +def test(request): + user = User.objects.prefetch_related("groups").get(id=request.user.id) + request.user = user + + course_list = get_courses(user, request.META.get('HTTP_HOST')) + + all_modules = [get_module(request, user, course) for course in course_list if registered_for_course(course, user)] + child_modules = [] + for module in all_modules: + child_modules.extend(module.get_children()) + bottom_modules = [] + for module in child_modules: + bottom_modules.extend(module.get_children()) + asset_divs = get_asset_div(convert_to_valid_html(bottom_modules[2].get_html())) + strings = [get_transcript_directory(lxml.html.tostring(div)) for div in asset_divs] + search_template = get_template('search.html') + html = search_template.render(Context({'course_list': strings})) + return HttpResponse(html) + + +def get_children(course): + """Returns the children of a given course""" + attributes = [child.location for child in course._child_instances] + return attributes + + +def convert_to_valid_html(html): + replacement = {"<": "<", ">": ">", """: "\"", "'": "'"} + for i, j in replacement.iteritems(): + html = html.replace(i, j) + return html + + +def get_asset_div(html_page): + return lxml.html.find_class(html_page, "video") + + +def get_module(request, user, course): + model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course.id, user, course, depth=2) + course_module = get_module_for_descriptor(user, request, course, model_data_cache, course.id) + return course_module + + +def get_youtube_code(module_html): + youtube_snippet = re.sub(r'(.*?)(1\.0:)(.*?)(,1\.25)(.*)', r'\3', module_html) + sliced_youtube_code = youtube_snippet[:youtube_snippet.find('\n')] + return sliced_youtube_code + + +def get_transcript_directory(module_html): + directory_snippet = re.sub(r'(.*?)(data-caption-asset-path=\")(.*?)(\">.*)', r'\3', module_html) + sliced_directory = directory_snippet[:directory_snippet.find('\n')] + return resolve_to_absolute_path(sliced_directory) + + +def resolve_to_absolute_path(transcript_directory): + normalized_path = posixpath.normpath(urllib.unquote(transcript_directory)).lstrip('/') + return all_transcript_files(normalized_path) + + +def all_transcript_files(normalized_path): + files = [transcript for transcript in listdir(normalized_path) if isfile(join(normalized_path, transcript))] + return files From 1f284c56c161c3ccffab3ddefacd2034f528c03e Mon Sep 17 00:00:00 2001 From: Jay Zoldak Date: Wed, 5 Jun 2013 18:15:16 -0400 Subject: [PATCH 02/16] Skip a test that is causing intermittent failures due to the way it is overriding urls --- common/djangoapps/mitxmako/tests.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/djangoapps/mitxmako/tests.py b/common/djangoapps/mitxmako/tests.py index 21866eb9b5..f419daa681 100644 --- a/common/djangoapps/mitxmako/tests.py +++ b/common/djangoapps/mitxmako/tests.py @@ -4,13 +4,15 @@ from django.core.urlresolvers import reverse from django.conf import settings from mitxmako.shortcuts import marketing_link from mock import patch - +from nose.plugins.skip import SkipTest class ShortcutsTests(TestCase): """ Test the mitxmako shortcuts file """ - + # TODO: fix this test. It is causing intermittent test failures on + # subsequent tests due to the way urls are loaded + raise SkipTest() @override_settings(MKTG_URLS={'ROOT': 'dummy-root', 'ABOUT': '/about-us'}) @override_settings(MKTG_URL_LINK_MAP={'ABOUT': 'login'}) def test_marketing_link(self): From 3b165da17047b13c696ee319fa8798d20f8cff04 Mon Sep 17 00:00:00 2001 From: Slater-Victoroff Date: Thu, 6 Jun 2013 08:56:16 -0400 Subject: [PATCH 03/16] Replaced troublesome pyes integration with direct calls made to elasticsearch rest api --- common/djangoapps/search/analyzer.json | 36 +++++++ common/djangoapps/search/es_requests.py | 104 ++++++++++++++++++++ common/djangoapps/search/mapping.json | 17 +--- common/djangoapps/search/protectedWords.txt | 15 +++ common/djangoapps/search/settings.json | 8 ++ 5 files changed, 165 insertions(+), 15 deletions(-) create mode 100644 common/djangoapps/search/analyzer.json create mode 100644 common/djangoapps/search/es_requests.py create mode 100644 common/djangoapps/search/protectedWords.txt create mode 100644 common/djangoapps/search/settings.json diff --git a/common/djangoapps/search/analyzer.json b/common/djangoapps/search/analyzer.json new file mode 100644 index 0000000000..2221d7ad19 --- /dev/null +++ b/common/djangoapps/search/analyzer.json @@ -0,0 +1,36 @@ +{ +"analyzer": { + + "transcript_analyzer": { + "type": "custom", + "tokenizer": "standard", + "filter": ["protected", "asciifolding", "custom_word_delimiter", "lowercase", "custom_stemmer", "shingle"], + "char_filter": ["custom_mapping"] + } +}, + +"filter" : { + + "custom_word_delimiter":{ + "type": "word_delimiter", + "preserve_original": "true" + }, + + "custom_stemmer": { + "type": "stemmer", + "name": "english" + }, + + "protected": { + "type": "keyword_marker", + "keywords_path": "protectedWords.txt" + } +}, + +"char_filter": { + "custom_mapping": { + "type": "mapping", + "mappings": ["\n=>-"] + } +} +} \ No newline at end of file diff --git a/common/djangoapps/search/es_requests.py b/common/djangoapps/search/es_requests.py new file mode 100644 index 0000000000..7c4a81faf5 --- /dev/null +++ b/common/djangoapps/search/es_requests.py @@ -0,0 +1,104 @@ +import requests +import json + + +class ElasticDatabase: + + def __init__(self, url, index_settings_file, *args): + """ + Will initialize elastic search object with any indices specified by args + + specifically the url should be something of the form `http://localhost:9200` + importantly do not include a slash at the end of the url name. + + args should be a list of dictionaries, each dictionary specifying a JSON mapping + to be used for a specific type. + + Example Dictionary: + {"index": "transcript", "type": "6-002x", "mapping": + { + "properties" : { + "searchable_text": { + "type": "string", + "store": "yes", + "index": "analyzed" + } + } + } + } + + Eventually we will support different configuration files for different indices, but + since this is only indexing transcripts right now it seems excessive""" + + self.url = url + self.args = args + self.index_settings = open(index_settings_file, 'rb').read() + + def parse_args(self): + for mapping in self.args: + try: + json_mapping = json.loads(mapping) + except ValueError: + print "Badly formed JSON args, please check your mappings file" + break + + try: + index = json_mapping['index'] + type_ = json_mapping['type'] + mapping = json_mapping['mapping'] + self.setup_index(index) + self.setup_type(index, type_, mapping) + except KeyError: + print "Could not find needed keys. Keys found: " + print mapping.keys() + continue + + def setup_type(self, index, type_, json_mapping): + """ + json_mapping should be a dictionary starting at the properties level of a mapping. + + The type level will be added, so if you include it things will break. The purpose of this + is to encourage loose coupling between types and mappings for better code + """ + + full_url = "/".join([self.url, index, type_, "_mapping"]) + json_put_body = {type_: json_mapping} + requests.put(full_url, data=json_put_body) + + def has_index(self, index): + """Checks to see if a given index exists in the database returns existance boolean, + + If this returns something other than a 200 or a 404 something is wrong and so we error""" + full_url = "/".join([self.url, index]) + status = requests.head(full_url).status_code + if status == 200: + return True + if status == 404: + return False + else: + print "Got an unexpected reponse code: " + str(status) + raise + + def setup_index(self, index): + """Creates a new elasticsearch index, returns the response it gets""" + full_url = "/".join(self.url, index) + "/" + return requests.put(full_url, data=self.index_settings) + + def index_data(self, index, type_, id_, data): + """Data should be passed in as a dictionary, assumes it matches the given mapping""" + full_url = "/".join([self.url, index, type_, id_]) + response = requests.put(full_url, json.dumps(data)) + return json.loads(response)['ok'] + + def get_index_settings(self, index): + """Returns the current settings of """ + full_url = "/".join([self.url, index, "_settings"]) + return json.loads(requests.get(full_url)._content) + + def get_type_mapping(self, index, type_): + full_url = "/".join([self.url, index, type_, "_mapping"]) + return json.loads(requests.get(full_url)._content) + + def index_data(self, index, type_, id_, json_data): + full_url = "/".join([self.url, index, type_, id_]) + requests.put(full_url, data=json_data) diff --git a/common/djangoapps/search/mapping.json b/common/djangoapps/search/mapping.json index 8d4deade6d..8c4ac045ac 100644 --- a/common/djangoapps/search/mapping.json +++ b/common/djangoapps/search/mapping.json @@ -5,20 +5,7 @@ "index": "analyzed", "store": "yes", "type": "string", - "term_vector": "with_positions_offsets" - }, - - "phonetic_text": { - "boost": 1.0, - "index": "analyzed", - "store": "yes", - "type": "string", - "term_vector": "with_positions_offsets" - }, - - "uuid": { - "index": "not_analyzed", - "store": "yes", - "type": "string" + "term_vector": "with_positions_offsets", + "analyzer": "transcript_analyzer" } } \ No newline at end of file diff --git a/common/djangoapps/search/protectedWords.txt b/common/djangoapps/search/protectedWords.txt new file mode 100644 index 0000000000..5f3fce5203 --- /dev/null +++ b/common/djangoapps/search/protectedWords.txt @@ -0,0 +1,15 @@ +"gauss", +"stokes", +"navier", +"einstein", +"goddard", +"oppenheimer", +"bloch", +"hawkings", +"newton", +"bohr", +"darwin", +"planck", +"rontgen", +"tesla", +"franklin" \ No newline at end of file diff --git a/common/djangoapps/search/settings.json b/common/djangoapps/search/settings.json new file mode 100644 index 0000000000..929ec092c9 --- /dev/null +++ b/common/djangoapps/search/settings.json @@ -0,0 +1,8 @@ +{ + "settings": { + "index": { + "number_of_replicas": 2, + "number_of_shards": 3 + } + } +} \ No newline at end of file From 7eb18fe01991654aee2fdca68ea3916bc51cb0ee Mon Sep 17 00:00:00 2001 From: Anton Stupak Date: Mon, 27 May 2013 11:37:01 +0300 Subject: [PATCH 04/16] adds test files for video --- .../xmodule/xmodule/js/fixtures/video.html | 27 +++-- .../lib/xmodule/xmodule/js/spec/helper.coffee | 10 +- .../video/display/video_caption_spec.coffee | 82 ++++++++----- .../video/display/video_control_spec.coffee | 47 ++++---- .../video/display/video_player_spec.coffee | 53 ++++++--- .../display/video_progress_slider_spec.coffee | 108 ++++++++++-------- .../display/video_speed_control_spec.coffee | 7 +- .../display/video_volume_control_spec.coffee | 3 +- .../xmodule/js/spec/video/display_spec.coffee | 48 ++++---- .../js/src/video/display/video_caption.coffee | 2 +- .../display/video_progress_slider.coffee | 2 +- 11 files changed, 219 insertions(+), 170 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/fixtures/video.html b/common/lib/xmodule/xmodule/js/fixtures/video.html index 15404a89d1..e86a24dc5c 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video.html @@ -1,12 +1,21 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
\ 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 fbc89f7bd9..5cf75366d8 100644 --- a/common/lib/xmodule/xmodule/js/spec/helper.coffee +++ b/common/lib/xmodule/xmodule/js/spec/helper.coffee @@ -28,7 +28,7 @@ jasmine.stubRequests = -> spyOn($, 'ajax').andCallFake (settings) -> if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/ settings.success data: jasmine.stubbedMetadata[match[1]] - else if match = settings.url.match /static\/subs\/(.+)\.srt\.sjson/ + else if match = settings.url.match /static(\/.*)?\/subs\/(.+)\.srt\.sjson/ settings.success jasmine.stubbedCaption else if settings.url.match /.+\/problem_get$/ settings.success html: readFixtures('problem_content.html') @@ -47,19 +47,15 @@ jasmine.stubYoutubePlayer = -> jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) -> enableParts = [enableParts] unless $.isArray(enableParts) - suite = context.suite currentPartName = suite.description while suite = suite.parentSuite enableParts.push currentPartName - for part in ['VideoCaption', 'VideoSpeedControl', 'VideoVolumeControl', 'VideoProgressSlider'] - unless $.inArray(part, enableParts) >= 0 - spyOn window, part - loadFixtures 'video.html' jasmine.stubRequests() YT.Player = undefined - context.video = new Video 'example', '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId' + videosDefinition = '0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId' + context.video = new Video '#example', videosDefinition jasmine.stubYoutubePlayer() if createPlayer return new VideoPlayer(video: context.video) diff --git a/common/lib/xmodule/xmodule/js/spec/video/display/video_caption_spec.coffee b/common/lib/xmodule/xmodule/js/spec/video/display/video_caption_spec.coffee index 90e026e57e..ddf6be18db 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/display/video_caption_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/video/display/video_caption_spec.coffee @@ -1,23 +1,25 @@ -# TODO: figure out why failing -xdescribe 'VideoCaption', -> +describe 'VideoCaption', -> + beforeEach -> - jasmine.stubVideoPlayer @ - $('.subtitles').remove() + spyOn(VideoCaption.prototype, 'fetchCaption').andCallThrough() + spyOn($, 'ajaxWithPrefix').andCallThrough() + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false afterEach -> YT.Player = undefined $.fn.scrollTo.reset() + $('.subtitles').remove() describe 'constructor', -> - beforeEach -> - spyOn($, 'getWithPrefix').andCallThrough() describe 'always', -> + beforeEach -> - @caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' + @player = jasmine.stubVideoPlayer @ + @caption = @player.caption it 'set the youtube id', -> - expect(@caption.youtubeId).toEqual 'def456' + expect(@caption.youtubeId).toEqual 'normalSpeedYoutubeId' it 'create the caption element', -> expect($('.video')).toContain 'ol.subtitles' @@ -26,7 +28,12 @@ xdescribe 'VideoCaption', -> expect($('.video')).toContain 'a.hide-subtitles' it 'fetch the caption', -> - expect($.getWithPrefix).toHaveBeenCalledWith @caption.captionURL(), jasmine.any(Function) + 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 @@ -42,9 +49,10 @@ xdescribe 'VideoCaption', -> expect($('.subtitles')).toHandleWith 'DOMMouseScroll', @caption.onMovement describe 'when on a non touch-based device', -> + beforeEach -> - spyOn(window, 'onTouchBasedDevice').andReturn false - @caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' + @player = jasmine.stubVideoPlayer @ + @caption = @player.caption it 'render the caption', -> expect($('.subtitles').html()).toMatch new RegExp(''' @@ -66,9 +74,11 @@ xdescribe 'VideoCaption', -> expect(@caption.rendered).toBeTruthy() describe 'when on a touch-based device', -> + beforeEach -> - spyOn(window, 'onTouchBasedDevice').andReturn true - @caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' + window.onTouchBasedDevice.andReturn true + @player = jasmine.stubVideoPlayer @ + @caption = @player.caption it 'show explaination message', -> expect($('.subtitles li')).toHaveHtml "Caption will be displayed when you start playing the video." @@ -77,12 +87,15 @@ xdescribe 'VideoCaption', -> expect(@caption.rendered).toBeFalsy() describe 'mouse movement', -> + beforeEach -> - spyOn(window, 'setTimeout').andReturn 100 + @player = jasmine.stubVideoPlayer @ + @caption = @player.caption + window.setTimeout.andReturn(100) spyOn window, 'clearTimeout' - @caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' describe 'when cursor is outside of the caption box', -> + beforeEach -> $(window).trigger jQuery.Event 'mousemove' @@ -90,6 +103,7 @@ xdescribe 'VideoCaption', -> expect(@caption.frozen).toBeFalsy() describe 'when cursor is in the caption box', -> + beforeEach -> $('.subtitles').trigger jQuery.Event 'mouseenter' @@ -143,8 +157,10 @@ xdescribe 'VideoCaption', -> expect($.fn.scrollTo).not.toHaveBeenCalled() describe 'search', -> + beforeEach -> - @caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' + @player = jasmine.stubVideoPlayer @ + @caption = @player.caption it 'return a correct caption index', -> expect(@caption.search(0)).toEqual 0 @@ -157,8 +173,9 @@ xdescribe 'VideoCaption', -> describe 'play', -> describe 'when the caption was not rendered', -> beforeEach -> - spyOn(window, 'onTouchBasedDevice').andReturn true - @caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' + window.onTouchBasedDevice.andReturn true + @player = jasmine.stubVideoPlayer @ + @caption = @player.caption @caption.play() it 'render the caption', -> @@ -185,7 +202,8 @@ xdescribe 'VideoCaption', -> describe 'pause', -> beforeEach -> - @caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' + @player = jasmine.stubVideoPlayer @ + @caption = @player.caption @caption.playing = true @caption.pause() @@ -193,8 +211,10 @@ xdescribe 'VideoCaption', -> expect(@caption.playing).toBeFalsy() describe 'updatePlayTime', -> + beforeEach -> - @caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' + @player = jasmine.stubVideoPlayer @ + @caption = @player.caption describe 'when the video speed is 1.0x', -> beforeEach -> @@ -240,13 +260,15 @@ xdescribe 'VideoCaption', -> expect($('.subtitles li[data-index=1]')).toHaveClass 'current' describe 'resize', -> + beforeEach -> - @caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' + @player = jasmine.stubVideoPlayer @ + @caption = @player.caption $('.subtitles li[data-index=1]').addClass 'current' @caption.resize() it 'set the height of caption container', -> - expect(parseInt($('.subtitles').css('maxHeight'))).toEqual $('.video-wrapper').height() + expect(parseInt($('.subtitles').css('maxHeight'))).toBeCloseTo $('.video-wrapper').height(), 5 it 'set the height of caption spacing', -> expect(parseInt($('.subtitles .spacing:first').css('height'))).toEqual( @@ -258,8 +280,10 @@ xdescribe 'VideoCaption', -> expect($.fn.scrollTo).toHaveBeenCalled() describe 'scrollCaption', -> + beforeEach -> - @caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' + @player = jasmine.stubVideoPlayer @ + @caption = @player.caption describe 'when frozen', -> beforeEach -> @@ -291,15 +315,17 @@ xdescribe 'VideoCaption', -> offset: - ($('.video-wrapper').height() / 2 - $('.subtitles .current:first').height() / 2) describe 'seekPlayer', -> + beforeEach -> - @caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' + @player = jasmine.stubVideoPlayer @ + @caption = @player.caption @time = null $(@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"]').click() + $('.subtitles li[data-start="30000"]').trigger('click') it 'trigger seek event with the correct time', -> expect(@time).toEqual 30.000 @@ -307,14 +333,15 @@ xdescribe 'VideoCaption', -> describe 'when the video speed is not 1.0x', -> beforeEach -> @caption.currentSpeed = '0.75' - $('.subtitles li[data-start="30000"]').click() + $('.subtitles li[data-start="30000"]').trigger('click') it 'trigger seek event with the correct time', -> expect(@time).toEqual 40.000 describe 'toggle', -> beforeEach -> - @caption = new VideoCaption el: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' + @player = jasmine.stubVideoPlayer @ + @caption = @player.caption $('.subtitles li[data-index=1]').addClass 'current' describe 'when the caption is visible', -> @@ -325,7 +352,6 @@ xdescribe 'VideoCaption', -> it 'hide the caption', -> expect(@caption.el).toHaveClass 'closed' - describe 'when the caption is hidden', -> beforeEach -> @caption.el.addClass 'closed' diff --git a/common/lib/xmodule/xmodule/js/spec/video/display/video_control_spec.coffee b/common/lib/xmodule/xmodule/js/spec/video/display/video_control_spec.coffee index 7603d5777f..e15b0c856a 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/display/video_control_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/video/display/video_control_spec.coffee @@ -1,53 +1,44 @@ -# TODO: figure out why failing -xdescribe 'VideoControl', -> +describe 'VideoControl', -> beforeEach -> - jasmine.stubVideoPlayer @ + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false + loadFixtures 'video.html' $('.video-controls').html '' describe 'constructor', -> + it 'render the video controls', -> - new VideoControl(el: $('.video-controls')) - expect($('.video-controls').html()).toContain ''' -
-
-
    -
  • Play
  • -
  • -
    0:00 / 0:00
    -
  • -
- -
- ''' + @control = new window.VideoControl(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 VideoControl(el: $('.video-controls')) - expect($('.video_control')).toHandleWith 'click', control.togglePlayback + @control = new window.VideoControl(el: $('.video-controls')) + expect($('.video_control')).toHandleWith 'click', @control.togglePlayback describe 'when on a touch based device', -> beforeEach -> - spyOn(window, 'onTouchBasedDevice').andReturn true + window.onTouchBasedDevice.andReturn true + @control = new window.VideoControl(el: $('.video-controls')) it 'does not add the play class to video control', -> - new VideoControl(el: $('.video-controls')) expect($('.video_control')).not.toHaveClass 'play' expect($('.video_control')).not.toHaveHtml 'Play' describe 'when on a non-touch based device', -> + beforeEach -> - spyOn(window, 'onTouchBasedDevice').andReturn false + @control = new window.VideoControl(el: $('.video-controls')) it 'add the play class to video control', -> - new VideoControl(el: $('.video-controls')) expect($('.video_control')).toHaveClass 'play' expect($('.video_control')).toHaveHtml 'Play' describe 'play', -> + beforeEach -> - @control = new VideoControl(el: $('.video-controls')) + @control = new window.VideoControl(el: $('.video-controls')) @control.play() it 'switch playback button to play state', -> @@ -56,8 +47,9 @@ xdescribe 'VideoControl', -> expect($('.video_control')).toHaveHtml 'Pause' describe 'pause', -> + beforeEach -> - @control = new VideoControl(el: $('.video-controls')) + @control = new window.VideoControl(el: $('.video-controls')) @control.pause() it 'switch playback button to pause state', -> @@ -66,8 +58,9 @@ xdescribe 'VideoControl', -> expect($('.video_control')).toHaveHtml 'Play' describe 'togglePlayback', -> + beforeEach -> - @control = new VideoControl(el: $('.video-controls')) + @control = new window.VideoControl(el: $('.video-controls')) describe 'when the control does not have play or pause class', -> beforeEach -> diff --git a/common/lib/xmodule/xmodule/js/spec/video/display/video_player_spec.coffee b/common/lib/xmodule/xmodule/js/spec/video/display/video_player_spec.coffee index b6c562c88a..0fa2c6f515 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/display/video_player_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/video/display/video_player_spec.coffee @@ -1,6 +1,9 @@ -# TODO: figure out why failing -xdescribe 'VideoPlayer', -> +describe 'VideoPlayer', -> beforeEach -> + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false + # It tries to call methods of VideoProgressSlider on Spy + for part in ['VideoCaption', 'VideoSpeedControl', 'VideoVolumeControl', 'VideoProgressSlider', 'VideoControl'] + spyOn(window[part].prototype, 'initialize').andCallThrough() jasmine.stubVideoPlayer @, [], false afterEach -> @@ -8,7 +11,6 @@ xdescribe 'VideoPlayer', -> describe 'constructor', -> beforeEach -> - spyOn window, 'VideoControl' spyOn YT, 'Player' $.fn.qtip.andCallFake -> $(this).data('qtip', true) @@ -22,32 +24,47 @@ xdescribe 'VideoPlayer', -> expect(@player.currentTime).toEqual 0 it 'set the element', -> - expect(@player.el).toBe '#video_example' + expect(@player.el).toHaveId 'video_id' it 'create video control', -> - expect(window.VideoControl).toHaveBeenCalledWith el: $('.video-controls', @player.el) + expect(window.VideoControl.prototype.initialize).toHaveBeenCalled() + expect(@player.control).toBeDefined() + expect(@player.control.el).toBe $('.video-controls', @player.el) it 'create video caption', -> - expect(window.VideoCaption).toHaveBeenCalledWith el: @player.el, youtubeId: 'normalSpeedYoutubeId', currentSpeed: '1.0' + expect(window.VideoCaption.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.VideoSpeedControl).toHaveBeenCalledWith el: $('.secondary-controls', @player.el), speeds: ['0.75', '1.0'], currentSpeed: '1.0' + expect(window.VideoSpeedControl.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.VideoProgressSlider).toHaveBeenCalledWith el: $('.slider', @player.el) + expect(window.VideoSpeedControl.prototype.initialize).toHaveBeenCalled() + expect(@player.progressSlider).toBeDefined() + expect(@player.progressSlider.el).toBe $('.slider', @player.el) it 'create Youtube player', -> - expect(YT.Player).toHaveBeenCalledWith('example', { + expect(YT.Player).toHaveBeenCalledWith('id', { playerVars: controls: 0 wmode: 'transparent' rel: 0 showinfo: 0 enablejsapi: 1 + modestbranding: 1 videoId: 'normalSpeedYoutubeId' events: onReady: @player.onReady onStateChange: @player.onStateChange + onPlaybackQualityChange: @player.onPlaybackQualityChange }) it 'bind to video control play event', -> @@ -76,7 +93,6 @@ xdescribe 'VideoPlayer', -> describe 'when not on a touch based device', -> beforeEach -> - spyOn(window, 'onTouchBasedDevice').andReturn false $('.add-fullscreen, .hide-subtitles').removeData 'qtip' @player = new VideoPlayer video: @video @@ -85,11 +101,13 @@ xdescribe 'VideoPlayer', -> expect($('.hide-subtitles')).toHaveData 'qtip' it 'create video volume control', -> - expect(window.VideoVolumeControl).toHaveBeenCalledWith el: $('.secondary-controls', @player.el) + expect(window.VideoVolumeControl.prototype.initialize).toHaveBeenCalled() + expect(@player.volumeControl).toBeDefined() + expect(@player.volumeControl.el).toBe $('.secondary-controls', @player.el) describe 'when on a touch based device', -> beforeEach -> - spyOn(window, 'onTouchBasedDevice').andReturn true + window.onTouchBasedDevice.andReturn true $('.add-fullscreen, .hide-subtitles').removeData 'qtip' @player = new VideoPlayer video: @video @@ -98,7 +116,8 @@ xdescribe 'VideoPlayer', -> expect($('.hide-subtitles')).not.toHaveData 'qtip' it 'does not create video volume control', -> - expect(window.VideoVolumeControl).not.toHaveBeenCalled() + expect(window.VideoVolumeControl.prototype.initialize).not.toHaveBeenCalled() + expect(@player.volumeControl).not.toBeDefined() describe 'onReady', -> beforeEach -> @@ -110,7 +129,6 @@ xdescribe 'VideoPlayer', -> describe 'when not on a touch based device', -> beforeEach -> - spyOn(window, 'onTouchBasedDevice').andReturn false spyOn @player, 'play' @player.onReady() @@ -119,7 +137,7 @@ xdescribe 'VideoPlayer', -> describe 'when on a touch based device', -> beforeEach -> - spyOn(window, 'onTouchBasedDevice').andReturn true + window.onTouchBasedDevice.andReturn true spyOn @player, 'play' @player.onReady() @@ -347,9 +365,6 @@ xdescribe 'VideoPlayer', -> it 'replace the full screen button tooltip', -> expect($('.add-fullscreen')).toHaveAttr 'title', 'Exit fill browser' - it 'add a new exit from fullscreen button', -> - expect(@player.el).toContain 'a.exit' - it 'add the fullscreen class', -> expect(@player.el).toHaveClass 'fullscreen' @@ -438,7 +453,7 @@ xdescribe 'VideoPlayer', -> describe 'volume', -> beforeEach -> - @player = new VideoPlayer @video + @player = new VideoPlayer video: @video @player.player.getVolume.andReturn 42 describe 'without value', -> diff --git a/common/lib/xmodule/xmodule/js/spec/video/display/video_progress_slider_spec.coffee b/common/lib/xmodule/xmodule/js/spec/video/display/video_progress_slider_spec.coffee index 99b675b1d7..bf6dada93b 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/display/video_progress_slider_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/video/display/video_progress_slider_spec.coffee @@ -1,31 +1,30 @@ -# TODO: figure out why failing -xdescribe 'VideoProgressSlider', -> +describe 'VideoProgressSlider', -> beforeEach -> - jasmine.stubVideoPlayer @ + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false describe 'constructor', -> describe 'on a non-touch based device', -> beforeEach -> spyOn($.fn, 'slider').andCallThrough() - spyOn(window, 'onTouchBasedDevice').andReturn false - @slider = new VideoProgressSlider el: $('.slider') + @player = jasmine.stubVideoPlayer @ + @progressSlider = @player.progressSlider it 'build the slider', -> - expect(@slider.slider).toBe '.slider' + expect(@progressSlider.slider).toBe '.slider' expect($.fn.slider).toHaveBeenCalledWith range: 'min' - change: @slider.onChange - slide: @slider.onSlide - stop: @slider.onStop + change: @progressSlider.onChange + slide: @progressSlider.onSlide + stop: @progressSlider.onStop it 'build the seek handle', -> - expect(@slider.handle).toBe '.slider .ui-slider-handle' + expect(@progressSlider.handle).toBe '.slider .ui-slider-handle' expect($.fn.qtip).toHaveBeenCalledWith content: "0:00" position: my: 'bottom center' at: 'top center' - container: @slider.handle + container: @progressSlider.handle hide: delay: 700 style: @@ -34,47 +33,51 @@ xdescribe 'VideoProgressSlider', -> describe 'on a touch-based device', -> beforeEach -> + window.onTouchBasedDevice.andReturn true spyOn($.fn, 'slider').andCallThrough() - spyOn(window, 'onTouchBasedDevice').andReturn true - @slider = new VideoProgressSlider el: $('.slider') + @player = jasmine.stubVideoPlayer @ + @progressSlider = @player.progressSlider it 'does not build the slider', -> - expect(@slider.slider).toBeUndefined + expect(@progressSlider.slider).toBeUndefined expect($.fn.slider).not.toHaveBeenCalled() describe 'play', -> beforeEach -> - @slider = new VideoProgressSlider el: $('.slider') - spyOn($.fn, 'slider').andCallThrough() + spyOn(VideoProgressSlider.prototype, 'buildSlider').andCallThrough() + @player = jasmine.stubVideoPlayer @ + @progressSlider = @player.progressSlider describe 'when the slider was already built', -> + beforeEach -> - @slider.play() + @progressSlider.play() it 'does not build the slider', -> - expect($.fn.slider).not.toHaveBeenCalled + expect(@progressSlider.buildSlider.calls.length).toEqual 1 describe 'when the slider was not already built', -> beforeEach -> - @slider.slider = null - @slider.play() + spyOn($.fn, 'slider').andCallThrough() + @progressSlider.slider = null + @progressSlider.play() it 'build the slider', -> - expect(@slider.slider).toBe '.slider' + expect(@progressSlider.slider).toBe '.slider' expect($.fn.slider).toHaveBeenCalledWith range: 'min' - change: @slider.onChange - slide: @slider.onSlide - stop: @slider.onStop + change: @progressSlider.onChange + slide: @progressSlider.onSlide + stop: @progressSlider.onStop it 'build the seek handle', -> - expect(@slider.handle).toBe '.ui-slider-handle' + expect(@progressSlider.handle).toBe '.ui-slider-handle' expect($.fn.qtip).toHaveBeenCalledWith content: "0:00" position: my: 'bottom center' at: 'top center' - container: @slider.handle + container: @progressSlider.handle hide: delay: 700 style: @@ -83,21 +86,23 @@ xdescribe 'VideoProgressSlider', -> describe 'updatePlayTime', -> beforeEach -> - @slider = new VideoProgressSlider el: $('.slider') - spyOn($.fn, 'slider').andCallThrough() + @player = jasmine.stubVideoPlayer @ + @progressSlider = @player.progressSlider describe 'when frozen', -> beforeEach -> - @slider.frozen = true - @slider.updatePlayTime 20, 120 + 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 -> - @slider.frozen = false - @slider.updatePlayTime 20, 120 + 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 @@ -107,55 +112,58 @@ xdescribe 'VideoProgressSlider', -> describe 'onSlide', -> beforeEach -> - @slider = new VideoProgressSlider el: $('.slider') + @player = jasmine.stubVideoPlayer @ + @progressSlider = @player.progressSlider @time = null - $(@slider).bind 'seek', (event, time) => @time = time - spyOnEvent @slider, 'seek' - @slider.onSlide {}, value: 20 + $(@progressSlider).bind 'seek', (event, time) => @time = time + spyOnEvent @progressSlider, 'seek' + @progressSlider.onSlide {}, value: 20 it 'freeze the slider', -> - expect(@slider.frozen).toBeTruthy() + expect(@progressSlider.frozen).toBeTruthy() it 'update the tooltip', -> expect($.fn.qtip).toHaveBeenCalled() it 'trigger seek event', -> - expect('seek').toHaveBeenTriggeredOn @slider + expect('seek').toHaveBeenTriggeredOn @progressSlider expect(@time).toEqual 20 describe 'onChange', -> beforeEach -> - @slider = new VideoProgressSlider el: $('.slider') - @slider.onChange {}, value: 20 + @player = jasmine.stubVideoPlayer @ + @progressSlider = @player.progressSlider + @progressSlider.onChange {}, value: 20 it 'update the tooltip', -> expect($.fn.qtip).toHaveBeenCalled() describe 'onStop', -> beforeEach -> - @slider = new VideoProgressSlider el: $('.slider') + @player = jasmine.stubVideoPlayer @ + @progressSlider = @player.progressSlider @time = null - $(@slider).bind 'seek', (event, time) => @time = time - spyOnEvent @slider, 'seek' - spyOn(window, 'setTimeout') - @slider.onStop {}, value: 20 + $(@progressSlider).bind 'seek', (event, time) => @time = time + spyOnEvent @progressSlider, 'seek' + @progressSlider.onStop {}, value: 20 it 'freeze the slider', -> - expect(@slider.frozen).toBeTruthy() + expect(@progressSlider.frozen).toBeTruthy() it 'trigger seek event', -> - expect('seek').toHaveBeenTriggeredOn @slider + expect('seek').toHaveBeenTriggeredOn @progressSlider expect(@time).toEqual 20 it 'set timeout to unfreeze the slider', -> expect(window.setTimeout).toHaveBeenCalledWith jasmine.any(Function), 200 window.setTimeout.mostRecentCall.args[0]() - expect(@slider.frozen).toBeFalsy() + expect(@progressSlider.frozen).toBeFalsy() describe 'updateTooltip', -> beforeEach -> - @slider = new VideoProgressSlider el: $('.slider') - @slider.updateTooltip 90 + @player = jasmine.stubVideoPlayer @ + @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/video/display/video_speed_control_spec.coffee b/common/lib/xmodule/xmodule/js/spec/video/display/video_speed_control_spec.coffee index a7af239094..ac321b8e97 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/display/video_speed_control_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/video/display/video_speed_control_spec.coffee @@ -1,6 +1,6 @@ -# TODO: figure out why failing -xdescribe 'VideoSpeedControl', -> +describe 'VideoSpeedControl', -> beforeEach -> + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false jasmine.stubVideoPlayer @ $('.speeds').remove() @@ -25,7 +25,7 @@ xdescribe 'VideoSpeedControl', -> describe 'when running on touch based device', -> beforeEach -> - spyOn(window, 'onTouchBasedDevice').andReturn true + window.onTouchBasedDevice.andReturn true $('.speeds').removeClass 'open' @speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' @@ -37,7 +37,6 @@ xdescribe 'VideoSpeedControl', -> describe 'when running on non-touch based device', -> beforeEach -> - spyOn(window, 'onTouchBasedDevice').andReturn false $('.speeds').removeClass 'open' @speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' diff --git a/common/lib/xmodule/xmodule/js/spec/video/display/video_volume_control_spec.coffee b/common/lib/xmodule/xmodule/js/spec/video/display/video_volume_control_spec.coffee index 41ac5dd3e4..a2b14afa55 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/display/video_volume_control_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/video/display/video_volume_control_spec.coffee @@ -1,5 +1,4 @@ -# TODO: figure out why failing -xdescribe 'VideoVolumeControl', -> +describe 'VideoVolumeControl', -> beforeEach -> jasmine.stubVideoPlayer @ $('.volume').remove() diff --git a/common/lib/xmodule/xmodule/js/spec/video/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/video/display_spec.coffee index ac90310519..a83fa3905c 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/display_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/video/display_spec.coffee @@ -1,12 +1,20 @@ -# TODO: figure out why failing -xdescribe 'Video', -> +describe 'Video', -> + metadata = undefined + beforeEach -> loadFixtures 'video.html' jasmine.stubRequests() - @videosDefinition = '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId' + @videosDefinition = '0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId' @slowerSpeedYoutubeId = 'slowerSpeedYoutubeId' @normalSpeedYoutubeId = 'normalSpeedYoutubeId' + metadata = + slowerSpeedYoutubeId: + id: @slowerSpeedYoutubeId + duration: 300 + normalSpeedYoutubeId: + id: @normalSpeedYoutubeId + duration: 200 afterEach -> window.player = undefined @@ -16,17 +24,18 @@ xdescribe 'Video', -> beforeEach -> @stubVideoPlayer = jasmine.createSpy('VideoPlayer') $.cookie.andReturn '0.75' - window.player = 100 + window.player = undefined describe 'by default', -> beforeEach -> - @video = new Video 'example', @videosDefinition - + spyOn(window.Video.prototype, 'fetchMetadata').andCallFake -> + @metadata = metadata + @video = new Video '#example', @videosDefinition it 'reset the current video player', -> expect(window.player).toBeNull() it 'set the elements', -> - expect(@video.el).toBe '#video_example' + expect(@video.el).toBe '#video_id' it 'parse the videos', -> expect(@video.videos).toEqual @@ -34,13 +43,8 @@ xdescribe 'Video', -> '1.0': @normalSpeedYoutubeId it 'fetch the video metadata', -> - expect(@video.metadata).toEqual - slowerSpeedYoutubeId: - id: @slowerSpeedYoutubeId - duration: 300 - normalSpeedYoutubeId: - id: @normalSpeedYoutubeId - duration: 200 + expect(@video.fetchMetadata).toHaveBeenCalled + expect(@video.metadata).toEqual metadata it 'parse available video speeds', -> expect(@video.speeds).toEqual ['0.75', '1.0'] @@ -56,7 +60,7 @@ xdescribe 'Video', -> @originalYT = window.YT window.YT = { Player: true } spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer) - @video = new Video 'example', @videosDefinition + @video = new Video '#example', @videosDefinition afterEach -> window.YT = @originalYT @@ -69,7 +73,7 @@ xdescribe 'Video', -> beforeEach -> @originalYT = window.YT window.YT = {} - @video = new Video 'example', @videosDefinition + @video = new Video '#example', @videosDefinition afterEach -> window.YT = @originalYT @@ -82,7 +86,7 @@ xdescribe 'Video', -> @originalYT = window.YT window.YT = {} spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer) - @video = new Video 'example', @videosDefinition + @video = new Video '#example', @videosDefinition window.onYouTubePlayerAPIReady() afterEach -> @@ -95,7 +99,7 @@ xdescribe 'Video', -> describe 'youtubeId', -> beforeEach -> $.cookie.andReturn '1.0' - @video = new Video 'example', @videosDefinition + @video = new Video '#example', @videosDefinition describe 'with speed', -> it 'return the video id for given speed', -> @@ -108,7 +112,7 @@ xdescribe 'Video', -> describe 'setSpeed', -> beforeEach -> - @video = new Video 'example', @videosDefinition + @video = new Video '#example', @videosDefinition describe 'when new speed is available', -> beforeEach -> @@ -129,14 +133,14 @@ xdescribe 'Video', -> describe 'getDuration', -> beforeEach -> - @video = new Video 'example', @videosDefinition + @video = new Video '#example', @videosDefinition it 'return duration for current video', -> expect(@video.getDuration()).toEqual 200 describe 'log', -> beforeEach -> - @video = new Video 'example', @videosDefinition + @video = new Video '#example', @videosDefinition @video.setSpeed '1.0' spyOn Logger, 'log' @video.player = { currentTime: 25 } @@ -144,7 +148,7 @@ xdescribe 'Video', -> it 'call the logger with valid parameters', -> expect(Logger.log).toHaveBeenCalledWith 'someEvent', - id: 'example' + id: 'id' code: @normalSpeedYoutubeId currentTime: 25 speed: '1.0' diff --git a/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee index bf3ec1e102..c72067b0dc 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display/video_caption.coffee @@ -37,7 +37,7 @@ class @VideoCaption extends Subview @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/video/display/video_progress_slider.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_progress_slider.coffee index 874756cb71..ef2f38698b 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display/video_progress_slider.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display/video_progress_slider.coffee @@ -11,7 +11,7 @@ class @VideoProgressSlider extends Subview @buildHandle() buildHandle: -> - @handle = @$('.slider .ui-slider-handle') + @handle = @$('.ui-slider-handle') @handle.qtip content: "#{Time.format(@slider.slider('value'))}" position: From 8b06916eb6e6d15723d1fe66695a2537e1b46a51 Mon Sep 17 00:00:00 2001 From: Anto Stupak Date: Wed, 29 May 2013 15:53:00 +0300 Subject: [PATCH 05/16] Fix tests for firefox --- .../video/display/video_caption_spec.coffee | 31 +++++++++---------- .../video/display/video_player_spec.coffee | 2 +- .../display/video_speed_control_spec.coffee | 19 ++++++------ .../js/src/video/display/video_player.coffee | 2 +- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/spec/video/display/video_caption_spec.coffee b/common/lib/xmodule/xmodule/js/spec/video/display/video_caption_spec.coffee index ddf6be18db..8c63751f07 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/display/video_caption_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/video/display/video_caption_spec.coffee @@ -55,12 +55,11 @@ describe 'VideoCaption', -> @caption = @player.caption it 'render the caption', -> - expect($('.subtitles').html()).toMatch new RegExp(''' -
  • Caption at 0
  • -
  • Caption at 10000
  • -
  • Caption at 20000
  • -
  • Caption at 30000
  • - '''.replace(/\n/g, '')) + 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' @@ -179,12 +178,11 @@ describe 'VideoCaption', -> @caption.play() it 'render the caption', -> - expect($('.subtitles').html()).toMatch new RegExp( - '''
  • Caption at 0
  • ''' + - '''
  • Caption at 10000
  • ''' + - '''
  • Caption at 20000
  • ''' + - '''
  • Caption at 30000
  • ''' - ) + 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' @@ -268,13 +266,12 @@ describe 'VideoCaption', -> @caption.resize() it 'set the height of caption container', -> - expect(parseInt($('.subtitles').css('maxHeight'))).toBeCloseTo $('.video-wrapper').height(), 5 + expect(parseInt($('.subtitles').css('maxHeight'))).toBeCloseTo $('.video-wrapper').height(), 2 it 'set the height of caption spacing', -> - expect(parseInt($('.subtitles .spacing:first').css('height'))).toEqual( - $('.video-wrapper').height() / 2 - $('.subtitles li:not(.spacing):first').height() / 2) - expect(parseInt($('.subtitles .spacing:last').css('height'))).toEqual( - $('.video-wrapper').height() / 2 - $('.subtitles li:not(.spacing):last').height() / 2) + expect(Math.abs(parseInt($('.subtitles .spacing:first').css('height')) - @caption.topSpacingHeight())).toBeLessThan 1 + expect(Math.abs(parseInt($('.subtitles .spacing:last').css('height')) - @caption.bottomSpacingHeight())).toBeLessThan 1 + it 'scroll caption to new position', -> expect($.fn.scrollTo).toHaveBeenCalled() diff --git a/common/lib/xmodule/xmodule/js/spec/video/display/video_player_spec.coffee b/common/lib/xmodule/xmodule/js/spec/video/display/video_player_spec.coffee index 0fa2c6f515..dab8c0815a 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/display/video_player_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/video/display/video_player_spec.coffee @@ -86,7 +86,7 @@ describe 'VideoPlayer', -> expect($(@player.volumeControl)).toHandleWith 'volumeChange', @player.onVolumeChange it 'bind to key press', -> - expect($(document)).toHandleWith 'keyup', @player.bindExitFullScreen + expect($(document.documentElement)).toHandleWith 'keyup', @player.bindExitFullScreen it 'bind to fullscreen switching button', -> expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen diff --git a/common/lib/xmodule/xmodule/js/spec/video/display/video_speed_control_spec.coffee b/common/lib/xmodule/xmodule/js/spec/video/display/video_speed_control_spec.coffee index ac321b8e97..687f90e030 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/display/video_speed_control_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/video/display/video_speed_control_spec.coffee @@ -10,15 +10,16 @@ describe 'VideoSpeedControl', -> @speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' it 'add the video speed control to player', -> - expect($('.secondary-controls').html()).toContain ''' - - ''' + 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 diff --git a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee index 561ca07c8a..73ff3512e2 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee @@ -15,7 +15,7 @@ class @VideoPlayer extends Subview $(@progressSlider).bind('seek', @onSeek) if @volumeControl $(@volumeControl).bind('volumeChange', @onVolumeChange) - $(document).keyup @bindExitFullScreen + $(document.documentElement).keyup @bindExitFullScreen @$('.add-fullscreen').click @toggleFullScreen @addToolTip() unless onTouchBasedDevice() From b25710346f44c3e0bdbc69928eba39fc8abc3106 Mon Sep 17 00:00:00 2001 From: Vasyl Nakvasiuk Date: Thu, 30 May 2013 11:22:20 +0300 Subject: [PATCH 06/16] add python video tests --- lms/djangoapps/courseware/tests/__init__.py | 97 +++++++++++++ .../courseware/tests/test_video_mongo.py | 49 +++++++ .../courseware/tests/test_video_xml.py | 137 ++++++++++++++++++ 3 files changed, 283 insertions(+) create mode 100644 lms/djangoapps/courseware/tests/test_video_mongo.py create mode 100644 lms/djangoapps/courseware/tests/test_video_xml.py diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py index e69de29bb2..dd3c4dc2b3 100644 --- a/lms/djangoapps/courseware/tests/__init__.py +++ b/lms/djangoapps/courseware/tests/__init__.py @@ -0,0 +1,97 @@ +""" +integration tests for xmodule + +Contains: + + 1. BaseTestXmodule class provides course and users + for testing Xmodules with mongo store. +""" + +from django.test.utils import override_settings +from django.core.urlresolvers import reverse +from django.test.client import Client + +from student.tests.factories import UserFactory, CourseEnrollmentFactory +from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +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 + + Any xmodule should overwrite only next parameters for test: + 1. TEMPLATE_NAME + 2. DATA + 3. COURSE_DATA and USER_COUNT if needed + + This class should not contain any tests, because TEMPLATE_NAME + should be defined in child class. + """ + USER_COUNT = 2 + COURSE_DATA = {} + + # Data from YAML common/lib/xmodule/xmodule/templates/NAME/default.yaml + TEMPLATE_NAME = "" + DATA = {} + + def setUp(self): + + self.course = CourseFactory.create(data=self.COURSE_DATA) + + # Turn off cache. + modulestore().request_cache = None + modulestore().metadata_inheritance_cache_subsystem = None + + chapter = ItemFactory.create( + parent_location=self.course.location, + template="i4x://edx/templates/sequential/Empty", + ) + section = ItemFactory.create( + parent_location=chapter.location, + template="i4x://edx/templates/sequential/Empty" + ) + + # username = robot{0}, password = 'test' + self.users = [ + UserFactory.create(username='robot%d' % i, email='robot+test+%d@edx.org' % i) + for i in range(self.USER_COUNT) + ] + + for user in self.users: + CourseEnrollmentFactory.create(user=user, course_id=self.course.id) + + item = ItemFactory.create( + parent_location=section.location, + template=self.TEMPLATE_NAME, + data=self.DATA + ) + self.item_url = Location(item.location).url() + + # login all users for acces to Xmodule + self.clients = {user.username: Client() for user in self.users} + self.login_statuses = [ + self.clients[user.username].login( + username=user.username, password='test') + for user in self.users + ] + + self.assertTrue(all(self.login_statuses)) + + def get_url(self, dispatch): + """Return word cloud url with dispatch.""" + return reverse( + 'modx_dispatch', + args=(self.course.id, self.item_url, dispatch) + ) + + def tearDown(self): + for user in self.users: + user.delete() diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py new file mode 100644 index 0000000000..f979ae2686 --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +"""Video xmodule tests in mongo.""" + +import json + +from . import BaseTestXmodule + + +class TestVideo(BaseTestXmodule): + """Integration tests: web client + mongo.""" + + TEMPLATE_NAME = "i4x://edx/templates/video/default" + DATA = '