diff --git a/common/lib/xmodule/xmodule/js/spec/.gitignore b/common/lib/xmodule/xmodule/js/spec/.gitignore index 951c538ae2..9d0c021751 100644 --- a/common/lib/xmodule/xmodule/js/spec/.gitignore +++ b/common/lib/xmodule/xmodule/js/spec/.gitignore @@ -6,3 +6,4 @@ # Tests for Time are written in pure JavaScript. !time_spec.js !collapsible_spec.js +!xmodule_spec.js diff --git a/common/lib/xmodule/xmodule/js/spec/xmodule_spec.js b/common/lib/xmodule/xmodule/js/spec/xmodule_spec.js new file mode 100644 index 0000000000..0ae76b7215 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/xmodule_spec.js @@ -0,0 +1,236 @@ +(function () { + 'use strict'; + + describe('XBlockToXModuleShim', function () { + describe('definition', function () { + it('XBlockToXModuleShim is defined, and is a function', function () { + expect($.isFunction(XBlockToXModuleShim)).toBe(true); + }); + }); + + describe('implementation', function () { + var el, + videoModule = { + 'module': 'video_module' + }, + editCallback, + displayCallback, + removeNone, + removeVideo; + + beforeEach(function () { + el = $('
'); + + if (window.None) { + spyOn(window, 'None'); + removeNone = false; + } else { + window.None = jasmine.createSpy('None'); + removeNone = true; + } + + if (window.Video) { + spyOn(window, 'Video'); + removeVideo = false; + } else { + window.Video = jasmine.createSpy('Video'); + removeVideo = true; + } + window.Video.andReturn(videoModule); + + editCallback = jasmine.createSpy('editCallback'); + $(document).on('XModule.loaded.edit', editCallback); + spyOnEvent($(document), 'XModule.loaded.edit'); + + displayCallback = jasmine.createSpy('displayCallback'); + $(document).on('XModule.loaded.display', displayCallback); + spyOnEvent($(document), 'XModule.loaded.display'); + }); + + afterEach(function () { + el = null; + + if (removeNone) { + window.None = undefined; + } + if (removeVideo) { + window.Video = undefined; + } + }); + + it('if element module is of type None, nothing happens', function () { + el.data('type', 'None'); + + expect(XBlockToXModuleShim(null, el)).toBeUndefined(); + expect(window.None).not.toHaveBeenCalled(); + }); + + it('if element module is of type Video, Video module constructor is called', function () { + el.data('type', 'Video'); + + expect(XBlockToXModuleShim(null, el)).toEqual(videoModule); + expect(window.Video).toHaveBeenCalled(); + + expect('XModule.loaded.edit').not.toHaveBeenTriggeredOn(document); + expect('XModule.loaded.display').not.toHaveBeenTriggeredOn(document); + }); + + it('if element has class "xmodule_edit"', function () { + el.data('type', 'Video') + .addClass('xmodule_edit'); + XBlockToXModuleShim(null, el); + expect('XModule.loaded.edit').toHaveBeenTriggeredOn($(document)); + expect(editCallback).toHaveBeenCalledWith(jasmine.any($.Event), el, videoModule); + expect('XModule.loaded.display').not.toHaveBeenTriggeredOn($(document)); + }); + + it('if element has class "xmodule_display"', function () { + el.data('type', 'Video') + .addClass('xmodule_display'); + XBlockToXModuleShim(null, el); + expect('XModule.loaded.edit').not.toHaveBeenTriggeredOn($(document)); + expect('XModule.loaded.display').toHaveBeenTriggeredOn($(document)); + expect(displayCallback).toHaveBeenCalledWith(jasmine.any($.Event), el, videoModule); + }); + + it('if element has classes "xmodule_edit", and "xmodule_display"', function () { + el.data('type', 'Video') + .addClass('xmodule_edit') + .addClass('xmodule_display'); + XBlockToXModuleShim(null, el); + expect('XModule.loaded.edit').toHaveBeenTriggeredOn($(document)); + expect('XModule.loaded.display').toHaveBeenTriggeredOn($(document)); + }); + + it('element is of an unknown Module type, console.error() is called if it is defined', function () { + var oldConsole = window.console; + + if (window.console && window.console.error) { + spyOn(window.console, 'error'); + } else { + window.console = jasmine.createSpy('console.error'); + } + + el.data('type', 'UnknownModule'); + expect(XBlockToXModuleShim(null, el)).toBeUndefined(); + + expect(console.error).toHaveBeenCalledWith( + 'Unable to load UnknownModule: window[moduleType] is not a constructor' + ); + + window.console = oldConsole; + }); + + it('element is of an unknown Module type, JavaScript throws if console.error() is not defined', function () { + var oldConsole = window.console, + testFunction = function () { + return XBlockToXModuleShim(null, el); + }; + + + if (window.console) { + window.console = undefined; + } + + el.data('type', 'UnknownModule'); + expect(testFunction).toThrow(); + + window.console = oldConsole; + }); + }); + }); + + describe('XModule.Descriptor', function () { + describe('definition', function () { + it('XModule is defined, and is a plain object', function () { + expect($.isPlainObject(XModule)).toBe(true); + }); + + it('XModule.Descriptor is defined, and is a function', function () { + expect($.isFunction(XModule.Descriptor)).toBe(true); + }); + + it('XModule.Descriptor has a complete prototype', function () { + expect($.isFunction(XModule.Descriptor.prototype.onUpdate)).toBe(true); + expect($.isFunction(XModule.Descriptor.prototype.update)).toBe(true); + expect($.isFunction(XModule.Descriptor.prototype.save)).toBe(true); + }); + }); + + describe('implementation', function () { + var el, obj, callback, length; + + // This is a dummy callback. + callback = function () { + var x = 1; + + return x + 1; + }; + + beforeEach(function () { + el = 'dummy object'; + obj = new XModule.Descriptor(el); + + spyOn(obj, 'save').andCallThrough(); + }); + + afterEach(function () { + el = null; + obj = null; + + length = undefined; + }); + + it('Descriptor is a proper constructor function', function () { + expect(obj.hasOwnProperty('element')).toBe(true); + expect(obj.element).toBe(el); + + expect(obj.hasOwnProperty('update')).toBe(true); + }); + + it('Descriptor.onUpdate called for the first time', function () { + expect(obj.hasOwnProperty('callbacks')).toBe(false); + obj.onUpdate(callback); + expect(obj.hasOwnProperty('callbacks')).toBe(true); + expect($.isArray(obj.callbacks)).toBe(true); + + length = obj.callbacks.length; + expect(length).toBe(1); + expect(obj.callbacks[length - 1]).toBe(callback); + }); + + it('Descriptor.onUpdate called for Nth time', function () { + // In this test it doesn't matter what obj.callbacks + // consists of. + obj.callbacks = ['test1', 'test2', 'test3']; + + obj.onUpdate(callback); + + length = obj.callbacks.length; + expect(length).toBe(4); + expect(obj.callbacks[length - 1]).toBe(callback); + }); + + it('Descriptor.save returns a blank object', function () { + // NOTE: In the future the implementation of .save() + // method may change! + expect(obj.save()).toEqual({}); + }); + + it('Descriptor.update triggers all callbacks with whatever .save() returns', function () { + var callback1 = jasmine.createSpy('callback1'), + callback2 = jasmine.createSpy('callback2'), + testValue = 'test 123'; + + obj.onUpdate(callback1); + obj.onUpdate(callback2); + + obj.save.andReturn(testValue); + obj.update(); + + expect(callback1).toHaveBeenCalledWith(testValue); + expect(callback2).toHaveBeenCalledWith(testValue); + }); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/src/.gitignore b/common/lib/xmodule/xmodule/js/src/.gitignore index 5a20f017dc..0d20de51a2 100644 --- a/common/lib/xmodule/xmodule/js/src/.gitignore +++ b/common/lib/xmodule/xmodule/js/src/.gitignore @@ -12,3 +12,4 @@ # Converted to JS from CoffeeScript. !time.js !collapsible.js +!xmodule.js diff --git a/common/lib/xmodule/xmodule/js/src/xmodule.coffee b/common/lib/xmodule/xmodule/js/src/xmodule.coffee deleted file mode 100644 index acaac65641..0000000000 --- a/common/lib/xmodule/xmodule/js/src/xmodule.coffee +++ /dev/null @@ -1,66 +0,0 @@ -@XModule = {} - -@XBlockToXModuleShim = (runtime, element) -> - ### - Load a single module (either an edit module or a display module) - from the supplied element, which should have a data-type attribute - specifying the class to load - ### - moduleType = $(element).data('type') - if moduleType == 'None' - return - - try - module = new window[moduleType](element) - if $(element).hasClass('xmodule_edit') - $(document).trigger('XModule.loaded.edit', [element, module]) - - if $(element).hasClass('xmodule_display') - $(document).trigger('XModule.loaded.display', [element, module]) - - return module - - catch error - if window.console and console.log - console.error "Unable to load #{moduleType}: #{error.message}" - else - throw error - - -class @XModule.Descriptor - - ### - Register a callback method to be called when the state of this - descriptor is updated. The callback will be passed the results - of calling the save method on this descriptor. - ### - onUpdate: (callback) -> - if ! @callbacks? - @callbacks = [] - - @callbacks.push(callback) - - ### - Notify registered callbacks that the state of this descriptor has changed - ### - update: => - data = @save() - callback(data) for callback in @callbacks - - ### - Bind the module to an element. This may be called multiple times, - if the element content has changed and so the module needs to be rebound - - @method: constructor - @param {html element} the .xmodule_edit section containing all of the descriptor content - ### - constructor: (@element) -> return - - ### - Return the current state of the descriptor (to be written to the module store) - - @method: save - @returns {object} An object containing children and data attributes (both optional). - The contents of the attributes will be saved to the server - ### - save: -> return {} diff --git a/common/lib/xmodule/xmodule/js/src/xmodule.js b/common/lib/xmodule/xmodule/js/src/xmodule.js new file mode 100644 index 0000000000..7676f33da5 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/xmodule.js @@ -0,0 +1,97 @@ +(function () { + 'use strict'; + + var XModule = {}; + + XModule.Descriptor = (function () { + /* + * Bind the module to an element. This may be called multiple times, + * if the element content has changed and so the module needs to be rebound + * + * @method: constructor + * @param {html element} the .xmodule_edit section containing all of the descriptor content + */ + var Descriptor = function (element) { + this.element = element; + this.update = _.bind(this.update, this); + }; + + /* + * Register a callback method to be called when the state of this + * descriptor is updated. The callback will be passed the results + * of calling the save method on this descriptor. + */ + Descriptor.prototype.onUpdate = function (callback) { + if (!this.callbacks) { + this.callbacks = []; + } + + this.callbacks.push(callback); + }; + + /* + * Notify registered callbacks that the state of this descriptor has changed + */ + Descriptor.prototype.update = function () { + var data, callbacks, i, length; + + data = this.save(); + callbacks = this.callbacks; + length = callbacks.length; + + $.each(callbacks, function (index, callback) { + callback(data); + }); + }; + + /* + * Return the current state of the descriptor (to be written to the module store) + * + * @method: save + * @returns {object} An object containing children and data attributes (both optional). + * The contents of the attributes will be saved to the server + */ + Descriptor.prototype.save = function () { + return {}; + }; + + return Descriptor; + }()); + + this.XBlockToXModuleShim = function (runtime, element) { + /* + * Load a single module (either an edit module or a display module) + * from the supplied element, which should have a data-type attribute + * specifying the class to load + */ + var moduleType = $(element).data('type'), + module; + + if (moduleType === 'None') { + return; + } + + try { + module = new window[moduleType](element); + + if ($(element).hasClass('xmodule_edit')) { + $(document).trigger('XModule.loaded.edit', [element, module]); + } + + if ($(element).hasClass('xmodule_display')) { + $(document).trigger('XModule.loaded.display', [element, module]); + } + + return module; + } catch (error) { + console.error('Unable to load ' + moduleType + ': ' + error.message); + } + }; + + // Export this module. We do it at the end when everything is ready + // because some RequireJS scripts require this module. If + // `window.XModule` appears as defined before this file has a chance + // to execute fully, then there is a chance that RequireJS will execute + // some script prematurely. + this.XModule = XModule; +}).call(this); diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 1e3bebcc96..3845422e80 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -64,10 +64,12 @@ class HTMLSnippet(object): # this means we need to make sure that all xmodules include this dependency which had been previously implicitly # fulfilled in a different area of code coffee = cls.js.setdefault('coffee', []) - fragment = resource_string(__name__, 'js/src/xmodule.coffee') + js = cls.js.setdefault('js', []) - if fragment not in coffee: - coffee.insert(0, fragment) + fragment = resource_string(__name__, 'js/src/xmodule.js') + + if fragment not in js: + js.insert(0, fragment) return cls.js