From f60af340ea8cd2b3015a028b6b618304c6176083 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Thu, 5 Jul 2012 18:44:56 -0400 Subject: [PATCH] Add Jasmine suite to CMS --- cms/envs/common.py | 14 ++ cms/static/coffee/files.json | 8 + cms/static/js/jasmine-jquery.js | 315 ++++++++++++++++++++++++++++++++ cms/templates/jasmine/base.html | 66 +++++++ cms/urls.py | 11 +- 5 files changed, 412 insertions(+), 2 deletions(-) create mode 100644 cms/static/coffee/files.json create mode 100644 cms/static/js/jasmine-jquery.js create mode 100644 cms/templates/jasmine/base.html diff --git a/cms/envs/common.py b/cms/envs/common.py index 472a59d295..1d036d761a 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -58,6 +58,10 @@ MAKO_TEMPLATES['main'] = [ COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates' ] +TEMPLATE_DIRS = ( + PROJECT_ROOT / "templates", +) + MITX_ROOT_URL = '' TEMPLATE_CONTEXT_PROCESSORS = ( @@ -68,6 +72,9 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.csrf', # necessary for csrf protection ) +################################# Jasmine ################################### +JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' + ################################# Middleware ################################### # List of finder classes that know how to find static files in # various locations. @@ -190,6 +197,10 @@ PIPELINE_JS = { 'module-js': { 'source_filenames': module_js_sources, 'output_filename': 'js/modules.js', + }, + 'spec': { + 'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/spec/**/*.coffee')], + 'output_filename': 'js/spec.js' } } @@ -233,4 +244,7 @@ INSTALLED_APPS = ( # For asset pipelining 'pipeline', 'staticfiles', + + # For testing + 'django_jasmine', ) diff --git a/cms/static/coffee/files.json b/cms/static/coffee/files.json new file mode 100644 index 0000000000..d3a414edb4 --- /dev/null +++ b/cms/static/coffee/files.json @@ -0,0 +1,8 @@ +{ + "js_files": [ + "/static/js/jquery.min.js", + "/static/js/json2.js", + "/static/js/underscore-min.js", + "/static/js/backbone-min.js" + ] +} diff --git a/cms/static/js/jasmine-jquery.js b/cms/static/js/jasmine-jquery.js new file mode 100644 index 0000000000..d516eb1080 --- /dev/null +++ b/cms/static/js/jasmine-jquery.js @@ -0,0 +1,315 @@ +var readFixtures = function() { + return jasmine.getFixtures().proxyCallTo_('read', arguments); +}; + +var preloadFixtures = function() { + jasmine.getFixtures().proxyCallTo_('preload', arguments); +}; + +var loadFixtures = function() { + jasmine.getFixtures().proxyCallTo_('load', arguments); +}; + +var setFixtures = function(html) { + jasmine.getFixtures().set(html); +}; + +var sandbox = function(attributes) { + return jasmine.getFixtures().sandbox(attributes); +}; + +var spyOnEvent = function(selector, eventName) { + jasmine.JQuery.events.spyOn(selector, eventName); +}; + +jasmine.getFixtures = function() { + return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures(); +}; + +jasmine.Fixtures = function() { + this.containerId = 'jasmine-fixtures'; + this.fixturesCache_ = {}; + this.fixturesPath = 'spec/javascripts/fixtures'; +}; + +jasmine.Fixtures.prototype.set = function(html) { + this.cleanUp(); + this.createContainer_(html); +}; + +jasmine.Fixtures.prototype.preload = function() { + this.read.apply(this, arguments); +}; + +jasmine.Fixtures.prototype.load = function() { + this.cleanUp(); + this.createContainer_(this.read.apply(this, arguments)); +}; + +jasmine.Fixtures.prototype.read = function() { + var htmlChunks = []; + + var fixtureUrls = arguments; + for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { + htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex])); + } + + return htmlChunks.join(''); +}; + +jasmine.Fixtures.prototype.clearCache = function() { + this.fixturesCache_ = {}; +}; + +jasmine.Fixtures.prototype.cleanUp = function() { + jQuery('#' + this.containerId).remove(); +}; + +jasmine.Fixtures.prototype.sandbox = function(attributes) { + var attributesToSet = attributes || {}; + return jQuery('
').attr(attributesToSet); +}; + +jasmine.Fixtures.prototype.createContainer_ = function(html) { + var container; + if(html instanceof jQuery) { + container = jQuery('
'); + container.html(html); + } else { + container = '
' + html + '
' + } + jQuery('body').append(container); +}; + +jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) { + if (typeof this.fixturesCache_[url] == 'undefined') { + this.loadFixtureIntoCache_(url); + } + return this.fixturesCache_[url]; +}; + +jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) { + var url = this.makeFixtureUrl_(relativeUrl); + var request = new XMLHttpRequest(); + request.open("GET", url + "?" + new Date().getTime(), false); + request.send(null); + this.fixturesCache_[relativeUrl] = request.responseText; +}; + +jasmine.Fixtures.prototype.makeFixtureUrl_ = function(relativeUrl){ + return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl; +}; + +jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) { + return this[methodName].apply(this, passedArguments); +}; + + +jasmine.JQuery = function() {}; + +jasmine.JQuery.browserTagCaseIndependentHtml = function(html) { + return jQuery('
').append(html).html(); +}; + +jasmine.JQuery.elementToString = function(element) { + var sample = $(element).get()[0] + if (sample == undefined || sample.cloneNode) + return jQuery('
').append($(element).clone()).html(); + else + return element.toString(); +}; + +jasmine.JQuery.matchersClass = {}; + +(function(namespace) { + var data = { + spiedEvents: {}, + handlers: [] + }; + + namespace.events = { + spyOn: function(selector, eventName) { + var handler = function(e) { + data.spiedEvents[[selector, eventName]] = e; + }; + jQuery(selector).bind(eventName, handler); + data.handlers.push(handler); + }, + + wasTriggered: function(selector, eventName) { + return !!(data.spiedEvents[[selector, eventName]]); + }, + + wasPrevented: function(selector, eventName) { + return data.spiedEvents[[selector, eventName]].isDefaultPrevented(); + }, + + cleanUp: function() { + data.spiedEvents = {}; + data.handlers = []; + } + } +})(jasmine.JQuery); + +(function(){ + var jQueryMatchers = { + toHaveClass: function(className) { + return this.actual.hasClass(className); + }, + + toBeVisible: function() { + return this.actual.is(':visible'); + }, + + toBeHidden: function() { + return this.actual.is(':hidden'); + }, + + toBeSelected: function() { + return this.actual.is(':selected'); + }, + + toBeChecked: function() { + return this.actual.is(':checked'); + }, + + toBeEmpty: function() { + return this.actual.is(':empty'); + }, + + toExist: function() { + return $(document).find(this.actual).length; + }, + + toHaveAttr: function(attributeName, expectedAttributeValue) { + return hasProperty(this.actual.attr(attributeName), expectedAttributeValue); + }, + + toHaveProp: function(propertyName, expectedPropertyValue) { + return hasProperty(this.actual.prop(propertyName), expectedPropertyValue); + }, + + toHaveId: function(id) { + return this.actual.attr('id') == id; + }, + + toHaveHtml: function(html) { + return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html); + }, + + toHaveText: function(text) { + var trimmedText = $.trim(this.actual.text()); + if (text && jQuery.isFunction(text.test)) { + return text.test(trimmedText); + } else { + return trimmedText == text; + } + }, + + toHaveValue: function(value) { + return this.actual.val() == value; + }, + + toHaveData: function(key, expectedValue) { + return hasProperty(this.actual.data(key), expectedValue); + }, + + toBe: function(selector) { + return this.actual.is(selector); + }, + + toContain: function(selector) { + return this.actual.find(selector).length; + }, + + toBeDisabled: function(selector){ + return this.actual.is(':disabled'); + }, + + toBeFocused: function(selector) { + return this.actual.is(':focus'); + }, + + // tests the existence of a specific event binding + toHandle: function(eventName) { + var events = this.actual.data("events"); + return events && events[eventName].length > 0; + }, + + // tests the existence of a specific event binding + handler + toHandleWith: function(eventName, eventHandler) { + var stack = this.actual.data("events")[eventName]; + var i; + for (i = 0; i < stack.length; i++) { + if (stack[i].handler == eventHandler) { + return true; + } + } + return false; + } + }; + + var hasProperty = function(actualValue, expectedValue) { + if (expectedValue === undefined) { + return actualValue !== undefined; + } + return actualValue == expectedValue; + }; + + var bindMatcher = function(methodName) { + var builtInMatcher = jasmine.Matchers.prototype[methodName]; + + jasmine.JQuery.matchersClass[methodName] = function() { + if (this.actual + && (this.actual instanceof jQuery + || jasmine.isDomNode(this.actual))) { + this.actual = $(this.actual); + var result = jQueryMatchers[methodName].apply(this, arguments) + if (this.actual.get && !$.isWindow(this.actual.get()[0])) + this.actual = jasmine.JQuery.elementToString(this.actual) + return result; + } + + if (builtInMatcher) { + return builtInMatcher.apply(this, arguments); + } + + return false; + }; + }; + + for(var methodName in jQueryMatchers) { + bindMatcher(methodName); + } +})(); + +beforeEach(function() { + this.addMatchers(jasmine.JQuery.matchersClass); + this.addMatchers({ + toHaveBeenTriggeredOn: function(selector) { + this.message = function() { + return [ + "Expected event " + this.actual + " to have been triggered on " + selector, + "Expected event " + this.actual + " not to have been triggered on " + selector + ]; + }; + return jasmine.JQuery.events.wasTriggered($(selector), this.actual); + } + }); + this.addMatchers({ + toHaveBeenPreventedOn: function(selector) { + this.message = function() { + return [ + "Expected event " + this.actual + " to have been prevented on " + selector, + "Expected event " + this.actual + " not to have been prevented on " + selector + ]; + }; + return jasmine.JQuery.events.wasPrevented(selector, this.actual); + } + }); +}); + +afterEach(function() { + jasmine.getFixtures().cleanUp(); + jasmine.JQuery.events.cleanUp(); +}); diff --git a/cms/templates/jasmine/base.html b/cms/templates/jasmine/base.html new file mode 100644 index 0000000000..610beda824 --- /dev/null +++ b/cms/templates/jasmine/base.html @@ -0,0 +1,66 @@ + + + + + Jasmine Spec Runner + + {% load staticfiles %} + + + {# core files #} + + + + + {# source files #} + {% for url in suite.js_files %} + + {% endfor %} + + {% load compressed %} + {# static files #} + {% compressed_js 'main' %} + + {# spec files #} + {% compressed_js 'spec' %} + + + + +

Jasmine Spec Runner

+ + + + + diff --git a/cms/urls.py b/cms/urls.py index 9d827c3fe3..d522ee23a1 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -1,12 +1,19 @@ -from django.conf.urls.defaults import patterns, url +from django.conf import settings +from django.conf.urls.defaults import patterns, include, url # Uncomment the next two lines to enable the admin: # from django.contrib import admin # admin.autodiscover() -urlpatterns = patterns('', +urlpatterns = ('', url(r'^$', 'contentstore.views.index', name='index'), url(r'^edit_item$', 'contentstore.views.edit_item', name='edit_item'), url(r'^save_item$', 'contentstore.views.save_item', name='save_item'), url(r'^temp_force_export$', 'contentstore.views.temp_force_export') ) + +if settings.DEBUG: + ## Jasmine + urlpatterns=urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),) + +urlpatterns = patterns(*urlpatterns)