diff --git a/Makefile b/Makefile index 18ae6b5..190d5ba 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,9 @@ test-once: test-dev: NODE_PATH=$(NODE_PATH) $(GRUNT) test_dev --pattern=$(pattern) +test-serve: + NODE_PATH=$(NODE_PATH) $(GRUNT) test_serve --pattern=$(pattern) + test-ci: NODE_PATH=$(NODE_PATH) $(GRUNT) test_ci diff --git a/bower.json b/bower.json index a416f80..baae9b5 100644 --- a/bower.json +++ b/bower.json @@ -11,10 +11,14 @@ "lodash": "2.4.1", "marked": "0.3.2", "react": "0.10.0", - "requirejs-text": "2.0.12" + "requirejs-text": "2.0.12", + "patternslib": "master" }, "devDependencies": { "expect": "0.3.1", "sinonjs": "1.10.2" + }, + "resolutions": { + "jquery": "1.11.1" } } diff --git a/js/config.js b/js/config.js index c91bdcf..516d53a 100644 --- a/js/config.js +++ b/js/config.js @@ -18,16 +18,25 @@ 'jquery': 'bower_components/jquery/dist/jquery', 'marked': 'bower_components/marked/lib/marked', 'mockup-docs': 'js/docs/app', + 'mockup-docs-navigation': 'js/docs/navigation', 'mockup-docs-page': 'js/docs/page', 'mockup-docs-pattern': 'js/docs/pattern', 'mockup-docs-view': 'js/docs/view', - 'mockup-docs-navigation': 'js/docs/navigation', + 'mockup-parser': 'js/parser', 'mockup-patterns-base': 'js/pattern', - 'mockup-registry': 'js/registry', 'react': 'bower_components/react/react', 'sinon': 'bower_components/sinonjs/sinon', 'text': 'bower_components/requirejs-text/text', - 'underscore': 'bower_components/lodash/dist/lodash.underscore' + 'underscore': 'bower_components/lodash/dist/lodash.underscore', + + // Patternslib + "pat-compat": "bower_components/patternslib/src/core/compat", + "pat-jquery-ext": "bower_components/patternslib/src/core/jquery-ext", + "pat-logger": "bower_components/patternslib/src/core/logger", + "pat-parser": "bower_components/patternslib/src/core/parser", + "pat-registry": "bower_components/patternslib/src/core/registry", + "pat-utils": "bower_components/patternslib/src/core/utils", + "logging": "bower_components/logging/src/logging" }, shim: { 'backbone': {exports: 'window.Backbone', deps: ['underscore', 'jquery']}, diff --git a/js/docs/pattern.js b/js/docs/pattern.js index ca445af..6e79cca 100644 --- a/js/docs/pattern.js +++ b/js/docs/pattern.js @@ -2,7 +2,7 @@ define([ 'underscore', 'marked', 'react', - 'mockup-registry' + 'pat-registry' ], function(_, marked, React, Registry) { 'use strict'; diff --git a/js/grunt.js b/js/grunt.js index efe11dd..d0f1bc6 100644 --- a/js/grunt.js +++ b/js/grunt.js @@ -261,7 +261,7 @@ extend(true, this.gruntConfig, customGruntConfig || {}); /* - * TODO: add description + * Register different test runners */ var bundles = []; for (var name in this.bundles) { @@ -271,6 +271,7 @@ grunt.registerTask('test', [ 'jshint', 'karma:test' ]); grunt.registerTask('test_once', [ 'jshint', 'karma:testOnce' ]); grunt.registerTask('test_dev', [ 'karma:testDev' ]); + grunt.registerTask('test_serve', [ 'karma:testServe' ]); grunt.registerTask('test_ci', [ 'jshint', 'karma:testCI'].concat(bundles)); /* @@ -281,7 +282,7 @@ karma: { options: { basePath: './', - frameworks: [], + frameworks: ['mocha', 'chai'], files: this.files, preprocessors: { 'js/**/*.js': 'coverage' }, reporters: ['dots', 'progress', 'coverage', 'spec'], @@ -295,6 +296,7 @@ captureTimeout: 60000, plugins: [ 'karma-mocha', + 'karma-chai', 'karma-coverage', 'karma-requirejs', 'karma-sauce-launcher', @@ -318,8 +320,18 @@ reporters: ['dots', 'progress'], plugins: [ 'karma-mocha', + 'karma-chai', 'karma-requirejs', - 'karma-chrome-launcher', + 'karma-chrome-launcher' + ] + }, + testServe: { + preprocessors: {}, + reporters: ['dots', 'progress'], + plugins: [ + 'karma-mocha', + 'karma-chai', + 'karma-requirejs' ] }, testCI: { @@ -376,10 +388,7 @@ grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-karma'); grunt.loadNpmTasks('grunt-sed'); - } }; - module.exports = MockupGrunt; - })(); diff --git a/js/parser.js b/js/parser.js new file mode 100644 index 0000000..581d588 --- /dev/null +++ b/js/parser.js @@ -0,0 +1,42 @@ +define([ + 'jquery' +], function($) { + 'use strict'; + + var parser = { + getOptions: function getOptions($el, patternName, options) { + /* This is the Mockup parser. It parses a DOM element for pattern + * configuration options. + */ + options = options || {}; + // get options from parent element first, stop if element tag name is 'body' + if ($el.length !== 0 && !$.nodeName($el[0], 'body')) { + options = getOptions($el.parent(), patternName, options); + } + // collect all options from element + var elOptions = {}; + if ($el.length !== 0) { + elOptions = $el.data('pat-' + patternName); + if (elOptions) { + // parse options if string + if (typeof(elOptions) === 'string') { + var tmpOptions = {}; + $.each(elOptions.split(';'), function(i, item) { + item = item.split(':'); + item.reverse(); + var key = item.pop(); + key = key.replace(/^\s+|\s+$/g, ''); // trim + item.reverse(); + var value = item.join(':'); + value = value.replace(/^\s+|\s+$/g, ''); // trim + tmpOptions[key] = value; + }); + elOptions = tmpOptions; + } + } + } + return $.extend(true, {}, options, elOptions); + } + }; + return parser; +}); diff --git a/js/pattern.js b/js/pattern.js index 849a644..b55fff7 100644 --- a/js/pattern.js +++ b/js/pattern.js @@ -1,26 +1,44 @@ /* Base Pattern */ - define([ 'jquery', - 'mockup-registry' -], function($, Registry) { + 'pat-registry', + 'mockup-parser', + "pat-logger" +], function($, Registry, mockupParser, logger) { 'use strict'; + var log = logger.getLogger("Mockup Base"); + + var initMockup = function initMockup($el, options, trigger) { + var name = this.prototype.name; + var log = logger.getLogger("pat." + name); + var pattern = $el.data('pattern-' + name); + if (pattern === undefined && Registry.patterns[name]) { + try { + pattern = new Registry.patterns[name]($el, mockupParser.getOptions($el, name, options)); + } catch (e) { + log.error('Failed while initializing "' + name + '" pattern.'); + } + $el.data('pattern-' + name, pattern); + } + return pattern; + }; // Base Pattern var Base = function($el, options) { this.$el = $el; this.options = $.extend(true, {}, this.defaults || {}, options || {}); this.init(); - this.trigger('init'); + this.emit('init'); }; + Base.prototype = { constructor: Base, on: function(eventName, eventCallback) { this.$el.on(eventName + '.' + this.name + '.patterns', eventCallback); }, - trigger: function(eventName, args) { + emit: function(eventName, args) { // args should be a list if (args === undefined) { args = []; @@ -28,28 +46,57 @@ define([ this.$el.trigger(eventName + '.' + this.name + '.patterns', args); } }; - Base.extend = function(NewPattern) { - var Base = this, Constructor; - if (NewPattern && NewPattern.hasOwnProperty('constructor')) { - Constructor = NewPattern.constructor; + Base.extend = function(patternProps) { + /* Helper function to correctly set up the prototype chain for new patterns. + */ + var parent = this; + var child; + + // Check that the required configuration properties are given. + if (!patternProps) { + throw new Error("Pattern configuration properties required when calling Base.extend"); + } + + // The constructor function for the new subclass is either defined by you + // (the "constructor" property in your `extend` definition), or defaulted + // by us to simply call the parent's constructor. + if (patternProps.hasOwnProperty('constructor')) { + child = patternProps.constructor; } else { - Constructor = function() { Base.apply(this, arguments); }; // TODO: arguments from where + child = function() { parent.apply(this, arguments); }; } - var Surrogate = function() { this.constructor = Constructor; }; - Surrogate.prototype = Base.prototype; - Constructor.prototype = new Surrogate(); - Constructor.extend = Base.extend; + // Allow patterns to be extended indefinitely + child.extend = Base.extend; - $.extend(true, Constructor.prototype, NewPattern); + // Static properties required by the Patternslib registry + child.init = initMockup; + child.jquery_plugin = true; + child.trigger = patternProps.trigger; - Constructor.__super__ = Base.prototype; // TODO: needed? + // Set the prototype chain to inherit from `parent`, without calling + // `parent`'s constructor function. + var Surrogate = function() { this.constructor = child; }; + Surrogate.prototype = parent.prototype; + child.prototype = new Surrogate(); - Registry.register(Constructor); + // Add pattern's configuration properties (instance properties) to the subclass, + $.extend(true, child.prototype, patternProps); - return Constructor; - }; + // Set a convenience property in case the parent's prototype is needed + // later. + child.__super__ = parent.prototype; + // Register the pattern in the Patternslib registry. + if (!patternProps.name) { + log.warn("This mockup pattern without a name attribute will not be registered!"); + } else if (!patternProps.trigger) { + log.warn("The mockup pattern '"+patternProps.name+"' does not have a trigger attribute, it will not be registered."); + } else { + Registry.register(child, patternProps.name); + } + return child; + }; return Base; }); diff --git a/package.json b/package.json index 345bd51..78b6132 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "homepage": "http://plone.github.io/mockup", "devDependencies": { "bower": "~1.3.1", + "chai": "^1.10.0", "coveralls": "~2.10.0", "extend": "~1.2.1", "grunt": "~0.4.4", @@ -18,6 +19,7 @@ "grunt-karma": "~0.8.2", "grunt-sed": "~0.1.1", "karma": "~0.12.1", + "karma-chai": "^0.1.0", "karma-chrome-launcher": "~0.1.2", "karma-coverage": "~0.2.1", "karma-junit-reporter": "~0.2.1", diff --git a/tests/parser-test.js b/tests/parser-test.js new file mode 100644 index 0000000..8ecca90 --- /dev/null +++ b/tests/parser-test.js @@ -0,0 +1,33 @@ +// Tests for the Mockup parser +define([ + 'expect', + 'sinon', + 'jquery', + 'mockup-parser', +], function(expect, sinon, $, parser) { + 'use strict'; + window.mocha.setup('bdd'); + describe('The Mockup parser', function () { + it("can read pattern configuraion options from the DOM", function() { + var $el = $('' + + '
' + + '
' + + '
'); + + var options = parser.getOptions( + $('.pat-example', $el), + 'example', + { name3: 'value3'} + ); + expect(options.name1).to.equal('value1'); + expect(options.name2).to.equal('something'); + expect(options.name3).to.equal('value3'); + expect(options['some-thing-name4']).to.equal('value4'); + expect(options['some-stuff']).to.equal('value5'); + }); + }); +}); diff --git a/tests/pattern-test.js b/tests/pattern-test.js index 0c0ad72..be84a45 100644 --- a/tests/pattern-test.js +++ b/tests/pattern-test.js @@ -1,30 +1,46 @@ -// Tests for Base Pattern - +// Tests for the Mockup Base Pattern define([ 'expect', + 'sinon', 'jquery', - 'mockup-registry', + 'pat-registry', 'mockup-patterns-base' -], function(expect, $, Registry, Base) { +], function(expect, sinon, $, Registry, Base) { 'use strict'; - window.mocha.setup('bdd'); - describe('Base', function () { + describe('The Mockup Base Pattern', function () { beforeEach(function() { - this._patterns = $.extend({}, Registry.patterns); - }); + this.jqueryPatterns = {}; + $.each(Registry.patterns, $.proxy(function(patternName) { + this.jqueryPatterns[Registry.patterns[patternName].prototype.jqueryPlugin] = + $.fn[Registry.patterns[patternName].prototype.jqueryPlugin]; + $.fn[Registry.patterns[patternName].prototype.jqueryPlugin] = undefined; + }, this)); + this._patterns = Registry.patterns; + Registry.patterns = {}; + }); afterEach(function() { + var jqueryPlugin; + $.each(Registry.patterns, function(patternName) { + $.fn[Registry.patterns[patternName].prototype.jqueryPlugin] = undefined; + }); Registry.patterns = this._patterns; + $.each(Registry.patterns, $.proxy(function(patternName) { + jqueryPlugin = Registry.patterns[patternName].prototype.jqueryPlugin; + $.fn[jqueryPlugin] = this.jqueryPatterns[jqueryPlugin]; + }, this)); }); it('can be extended and used in similar way as classes', function(done) { var Tmp = Base.extend({ + name: 'example', + trigger: 'pat-example', some: 'thing', init: function() { - expect(this.$el).to.equal('element'); + expect(this.$el.hasClass('pat-example')).to.equal(true); expect(this.options).to.have.keys(['option']); this.extra(); }, @@ -33,12 +49,71 @@ define([ done(); } }); - var tmp = new Tmp('element', {option: 'value'}); + var tmp = new Tmp($('
'), {option: 'value'}); + }); + + it('will automatically register a pattern in the Patternslib registry when extended', function() { + var registerSpy = sinon.spy(); + var originalRegister = Registry.register; + Registry.register = function (pattern, name) { + registerSpy(); + originalRegister(pattern, name); + }; + var NewPattern = Base.extend({ + name: 'example', + trigger: '.pat-example' + }); + expect(NewPattern.trigger).to.be.equal('.pat-example'); + expect(Object.keys(Registry.patterns).length).to.be.equal(1); + expect(Object.keys(Registry.patterns)[0]).to.be.equal('example'); + expect(registerSpy.called).to.be.equal(true); + Registry.register = originalRegister; + }); + + it('will not automatically register a pattern without a "name" attribute', function() { + var registerSpy = sinon.spy(); + var originalRegister = Registry.register; + Registry.register = function (pattern, name) { + registerSpy(); + originalRegister(pattern, name); + }; + var NewPattern = Base.extend({trigger: '.pat-example'}); + expect(registerSpy.called).to.be.equal(false); + Registry.register = originalRegister; + }); + + it('will not automatically register a pattern without a "trigger" attribute', function() { + var registerSpy = sinon.spy(); + var originalRegister = Registry.register; + Registry.register = function (pattern, name) { + registerSpy(); + originalRegister(pattern, name); + }; + var NewPattern = Base.extend({name: 'example'}); + expect(registerSpy.called).to.be.equal(false); + Registry.register = originalRegister; + }); + + it('will instantiate new instances of a pattern when the DOM is scanned', function(done) { + var NewPattern = Base.extend({ + name: 'example', + trigger: '.pat-example', + init: function() { + expect(this.$el.attr('class')).to.be.equal('pat-example'); + done(); + } + }); + Registry.scan($('
')); + }); + + it('requires that patterns that extend it provide an object of properties', function() { + expect(Base.extend.bind(Base, {})).should.assert("Pattern configuration properties required when calling Base.extend"); }); it('can be extended multiple times', function(done) { var Tmp1 = Base.extend({ - some: 'thing1', + name: 'thing', + trigger: 'pat-thing', something: 'else', init: function() { expect(this.some).to.equal('thing3'); @@ -47,6 +122,8 @@ define([ } }); var Tmp2 = Tmp1.extend({ + name: 'thing', + trigger: 'pat-thing', some: 'thing2', init: function() { expect(this.some).to.equal('thing3'); @@ -55,6 +132,8 @@ define([ } }); var Tmp3 = Tmp2.extend({ + name: 'thing', + trigger: 'pat-thing', some: 'thing3', init: function() { expect(this.some).to.equal('thing3'); @@ -65,25 +144,16 @@ define([ var tmp3 = new Tmp3('element', {option: 'value'}); }); - it('can also extend with already existing constructors', function(done) { - var Tmp1 = function() { - expect(1).to.be(1); - done(); - }; - var Tmp2 = function() {}; - Tmp2.constructor = Tmp1; - new Base.extend(Tmp2)('element'); - }); - - it('has on/trigger helpers to prefix events', function(done) { + it('has on/emit helpers to prefix events', function(done) { var Tmp = Base.extend({ name: 'tmp', + trigger: '.pat-tmp', init: function() { this.on('something', function(e, arg1) { expect(arg1).to.be('yaay!'); done(); }); - this.trigger('somethingelse', ['yaay!']); + this.emit('somethingelse', ['yaay!']); } }); var tmp = new Tmp( @@ -92,7 +162,5 @@ define([ done(); })); }); - }); - }); diff --git a/tests/registry-test.js b/tests/registry-test.js deleted file mode 100644 index 7594d8f..0000000 --- a/tests/registry-test.js +++ /dev/null @@ -1,215 +0,0 @@ -// Tests for Registry - -define([ - 'expect', - 'sinon', - 'jquery', - 'mockup-registry' -], function(expect, sinon, $, Registry) { - 'use strict'; - - window.mocha.setup('bdd'); - - describe('Registry', function () { - beforeEach(function() { - var self = this; - self.warnMsg = ''; - self._warn = Registry.warn; - Registry.warn = function(msg) { - self.warnMsg = msg; - }; - Registry.error = function(msg) { - self.warnMsg = msg; - }; - self.jqueryPatterns = {}; - $.each(Registry.patterns, function(patternName) { - self.jqueryPatterns[Registry.patterns[patternName].prototype.jqueryPlugin] = $.fn[Registry.patterns[patternName].prototype.jqueryPlugin]; - $.fn[Registry.patterns[patternName].prototype.jqueryPlugin] = undefined; - }); - self._patterns = Registry.patterns; - Registry.patterns = {}; - }); - afterEach(function() { - var self = this, jqueryPlugin; - self.warnMsg = ''; - Registry.warn = self._warn; - $.each(Registry.patterns, function(patternName) { - $.fn[Registry.patterns[patternName].prototype.jqueryPlugin] = undefined; - }); - Registry.patterns = self._patterns; - $.each(Registry.patterns, function(patternName) { - jqueryPlugin = Registry.patterns[patternName].prototype.jqueryPlugin; - $.fn[jqueryPlugin] = self.jqueryPatterns[jqueryPlugin]; - }); - }); - - it('skip initializing pattern if not in registry', function() { - var $el = $('
'); - Registry.init($el, 'example', {}); - expect($el.data('example')).to.be.equal(undefined); - }); - - it('failing pattern initialization', function() { - window.DEBUG = false; - Registry.patterns.example = function($el, options) { - throw new Error('some random error'); - }; - Registry.init($('
'), 'example', {}); - expect(this.warnMsg).to.be.equal('Failed while initializing "example" pattern.'); - window.DEBUG = true; - }); - - it('pattern initialization', function() { - var $pattern = $('
'); - Registry.patterns.example = function($el, options) { - this.example = 'works'; - }; - Registry.init($pattern, 'example', {}); - expect($pattern.data('pattern-example').example).to.be.equal('works'); - }); - - it('pattern wont get initialized twice', function() { - var $pattern = $('
'); - Registry.patterns.example = function($pattern, options) { - this.example = 'works'; - }; - var pattern1 = Registry.init($pattern, 'example', {}); - pattern1.example2 = 'works'; - var pattern2 = Registry.init($pattern, 'example', {}); - expect(pattern1).to.be.equal(pattern2); - expect(pattern2.example2).to.be.equal('works'); - expect(pattern1.example2).to.be.equal('works'); - }); - - it('scan for pattern', function() { - var $dom = $('' + - '
' + - '
' + - '
'); - Registry.patterns.example = function($el, options) { - this.example = 'works'; - }; - Registry.scan($dom); - expect($dom.data('pattern-example').example).to.be.equal('works'); - expect($dom.find('div').data('pattern-example').example).to.be.equal('works'); - }); - - it('scan for pattern among other class names on element', function() { - var $el = $('
'); - Registry.patterns.example = function($el, options) { - this.example = 'works'; - }; - Registry.scan($el); - expect($el.data('pattern-example').example).to.be.equal('works'); - }); - - it('trigger event on completed scan', function () { - var spy = sinon.spy(); - // register spy as event listener - $(document).on('scan-completed.registry.mockup-core', spy); - var $el = $('
'); - Registry.scan($el); - expect(spy.called).to.be(true); // spy must be called - }); - - it('try register a pattern without name', function() { - Registry.register(function($el, options) {}); - expect(this.warnMsg).to.be.equal('Pattern didn\'t specified a name.'); - }); - - it('register a pattern', function() { - var ExamplePattern = function($el, options) { }; - ExamplePattern.prototype.name = 'example'; - Registry.register(ExamplePattern); - expect(Registry.patterns.example).to.be.equal(ExamplePattern); - expect($.fn.patternExample).to.not.equal(undefined); - }); - - it('custom jquery plugin name for pattern', function() { - var ExamplePattern = function($el, options) { }; - ExamplePattern.prototype.name = 'example'; - ExamplePattern.prototype.jqueryPlugin = 'example'; - Registry.register(ExamplePattern); - expect($.fn.patternExample).to.be.equal(undefined); - expect($.fn.example).to.not.equal(undefined); - }); - - it('jquery plugin with custom options', function() { - var ExamplePattern = function($el, options) { - this.options = options; - }; - ExamplePattern.prototype.name = 'example'; - - Registry.register(ExamplePattern); - - var self = this, - $el = $('
'); - - $el.patternExample({option1: 'value1'}); - - expect($el.data('pattern-example').options.option1).to.be.equal('value1'); - }); - - it('call methods via jquery plugin', function() { - var ExamplePattern = function($el, options) { }, value = ''; - ExamplePattern.prototype.name = 'example'; - ExamplePattern.prototype.method = function() { value = 'method'; }; - ExamplePattern.prototype.methodWithOptions = function(options) { - if (options.optionA && options.optionA === 'valueA') { - value = 'methodWithOptions'; - } - }; - ExamplePattern.prototype._methodPrivate = function() { }; - Registry.register(ExamplePattern); - - var self = this, $el = $('
'); - - value = ''; - $el.patternExample('method_non_existing'); - expect(value).to.be.equal(''); - expect(self.warnMsg).to.be.equal('Method "method_non_existing" does not exists.'); - - value = ''; - $el.patternExample('method'); - expect(value).to.be.equal('method'); - - value = ''; - $el.patternExample('methodWithOptions', {optionA: 'valueB'}); - expect(value).to.be.equal(''); - - value = ''; - $el.patternExample('methodWithOptions', {optionA: 'valueA'}); - expect(value).to.be.equal('methodWithOptions'); - - value = ''; - $el.patternExample('_methodPrivate'); - expect(value).to.be.equal(''); - expect(self.warnMsg).to.be.equal('Method "_methodPrivate" is private.'); - }); - - it('read options from dom tree', function() { - var $el = $('' + - '
' + - '
' + - '
'); - - var options = Registry.getOptions( - $('.pat-example', $el), - 'example', - { name3: 'value3'} - ); - - expect(options.name1).to.equal('value1'); - expect(options.name2).to.equal('something'); - expect(options.name3).to.equal('value3'); - expect(options['some-thing-name4']).to.equal('value4'); - expect(options['some-stuff']).to.equal('value5'); - }); - - }); - -});