From 64ccae699fb18819651c13f535944f9824c38081 Mon Sep 17 00:00:00 2001 From: Zach Shipley Date: Tue, 1 Jul 2014 15:27:43 -0500 Subject: [PATCH] Issue #124: Use ES6 Promises instead of lavaca/util/Promise Using [this Promise API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise): - Updated all functions which were directly constructing lavaca/util/Promise instances to construct Promise instances instead - Updated all functions calling a `.then`, `.always`, `.success`, or `.error` method to only use ES6-compliant `.then` or `.catch` methods. - Updated unit tests I did skip updating lavaca/ui/Form and its unit tests... I'm somewhat sure it will be removed in issue #121. *Side effect*: ES6-ifying `mvc/View#render` into "sequential-looking" `.then` chains resulted in removing two methods: - `mvc/View#renderTemplate` because it boiled down to one statement: `template.render(model)` - `mvc/View#bindRenderEvents` because it de-duplicated adding 'rendersuccess' and 'rendererror' onto the end of `render()` and `renderPageView()` at the expense of code flow readability imo. As an aside, I suspect we might be able to dedupe the entire `render()` and `renderPageView()` methods. *Side effect*: lavaca/util/Map: synchronous AJAX is incompatible with ES6 Promises because they force asynchronicity. Change lavaca/util/Map to return a (non-ES6) jQuery Promise instead. --- .jshintrc | 4 +- Gruntfile.js | 5 +- bower.json | 1 + src/mvc/Application.js | 104 +++++------ src/mvc/Collection.js | 9 +- src/mvc/Controller.js | 33 ++-- src/mvc/Model.js | 69 ++++--- src/mvc/Route.js | 16 +- src/mvc/Router.js | 46 ++--- src/mvc/View.js | 161 +++++++---------- src/mvc/ViewManager.js | 171 +++++++++--------- src/net/Connectivity.js | 35 ++-- src/ui/DustTemplate.js | 51 +++--- src/ui/Form.js | 11 +- src/ui/Template.js | 6 +- src/util/Map.js | 5 +- src/util/Promise.js | 225 ----------------------- test/unit/.jshintrc | 1 + test/unit/mvc/Controller.js | 47 +++-- test/unit/mvc/Model.js | 33 ++-- test/unit/mvc/Router.js | 24 ++- test/unit/mvc/View.js | 340 ++++++++++++++++++++--------------- test/unit/mvc/ViewManager.js | 201 ++++++++++----------- test/unit/ui/DustTemplate.js | 270 +++++++++++++++++----------- test/unit/util/Promise.js | 109 ----------- 25 files changed, 847 insertions(+), 1130 deletions(-) delete mode 100755 src/util/Promise.js delete mode 100755 test/unit/util/Promise.js diff --git a/.jshintrc b/.jshintrc index ec7d9d19..2bb5bedd 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,9 +1,11 @@ { "globals": { - "define": false + "define": false, + "Promise": false }, "node": true, + "browser": true, "devel": true, "sub": true, diff --git a/Gruntfile.js b/Gruntfile.js index 8b92cedd..66ec4aa4 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -7,7 +7,10 @@ module.exports = function(grunt) { jasmine: { all: { // PhantomJS is not fully ES5-compatible; shim it - src: 'src/components/es5-shim/es5-shim.js', + src: [ + 'src/components/es5-shim/es5-shim.js', + 'src/components/es6-shim/es6-shim.js' + ], options: { specs: 'test/unit/**/*.js', template: require('grunt-template-jasmine-requirejs'), diff --git a/bower.json b/bower.json index 2092c8db..d54b9b34 100644 --- a/bower.json +++ b/bower.json @@ -21,6 +21,7 @@ "require-dust": "git://github.com/georgehenderson/require-dust.git#master", "mout": "~0.7.1", "es5-shim": "~2.1.0", + "es6-shim": "0.13.0", "requirejs ": "~2.1.8", "iscroll ": "~5.0.5", "hammerjs": "1.0.5" diff --git a/src/mvc/Application.js b/src/mvc/Application.js index 963ebfbc..91968b9d 100755 --- a/src/mvc/Application.js +++ b/src/mvc/Application.js @@ -9,7 +9,6 @@ define(function(require) { Connectivity = require('lavaca/net/Connectivity'), Template = require('lavaca/ui/Template'), Config = require('lavaca/util/Config'), - Promise = require('lavaca/util/Promise'), Translation = require('lavaca/util/Translation'); function _stopEvent(e) { @@ -27,8 +26,8 @@ define(function(require) { function _isExternal(url) { var match = url.match(/^([^:\/?#]+:)?(?:\/\/([^\/?#]*))?([^?#]+)?(\?[^#]*)?(#.*)?/); - if (typeof match[1] === 'string' - && match[1].length > 0 + if (typeof match[1] === 'string' + && match[1].length > 0 && match[1].toLowerCase() !== location.protocol) { return true; } @@ -115,48 +114,49 @@ define(function(require) { * @param {Event} e The event object */ onTapLink: function(e) { -      var link = $(e.currentTarget), -          defaultPrevented = e.isDefaultPrevented(), -          url = link.attr('href') || link.attr('data-href'), -          rel = link.attr('rel'), -          target = link.attr('target'), -          isExternal = link.is('[data-external]') || _isExternal(url), + var link = $(e.currentTarget), + defaultPrevented = e.isDefaultPrevented(), + url = link.attr('href') || link.attr('data-href'), + rel = link.attr('rel'), + target = link.attr('target'), + isExternal = link.is('[data-external]') || _isExternal(url), metaKey = e.type === 'tap' ? (e.gesture.srcEvent.ctrlKey || e.gesture.srcEvent.metaKey) : (e.ctrlKey || e.metaKey); if (metaKey) { target = metaKey ? '_blank' : (target ? target : '_self'); } -      if (!defaultPrevented) { + if (!defaultPrevented) { if (Device.isCordova() && target) { e.preventDefault(); window.open(url, target || '_blank'); } else if (isExternal || target) { window.open(url, target); -          return true; -        } else { -          e.preventDefault(); -          if (rel === 'back') { -            History.back(); -          } else if (rel === 'force-back' && url) { + return true; + } else { + e.preventDefault(); + if (rel === 'back') { + History.back(); + } else if (rel === 'force-back' && url) { History.isRoutingBack = true; - this.router.exec(url, null, null).always(function() { + var _always = function() { History.isRoutingBack = false; - }); + }; + this.router.exec(url, null, null).then(_always, _always); } else if (rel === 'cancel') { -            this.viewManager.dismiss(e.currentTarget); -          } else if (url) { -            url = url.replace(/^\/?#/, ''); -            this.router.exec(url).error(this.onInvalidRoute); -          } -        } -      } -    }, + this.viewManager.dismiss(e.currentTarget); + } else if (url) { + url = url.replace(/^\/?#/, ''); + this.router.exec(url).catch(this.onInvalidRoute); + } + } + } + }, /** * Makes an AJAX request if the user is online. If the user is offline, the returned * promise will be rejected with the string argument "offline". (Alias for [[Lavaca.net.Connectivity]].ajax) * @method ajax * * @param {Object} opts jQuery-style AJAX options - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ ajax: function() { return Connectivity.ajax.apply(Connectivity, arguments); @@ -167,12 +167,9 @@ define(function(require) { * * @param {Object} args Data of any type from a resolved promise returned by Application.beforeInit(). Defaults to null. * - * @return {Lavaca.util.Promise} A promise that resolves when the application is ready for use + * @return {Promise} A promise that resolves when the application is ready for use */ init: function(args) { - var promise = new Promise(this), - _cbPromise, - lastly; Template.init(); /** * View manager used to transition between UI states @@ -191,32 +188,24 @@ define(function(require) { */ this.router = router.setViewManager(this.viewManager); - - lastly = function() { - this.router.startHistory(); - if (!this.router.hasNavigated) { - promise.when( - this.router.exec(this.initialHashRoute || this.initRoute, this.initState, this.initParams) - ); - if (this.initState) { - History.replace(this.initState.state, this.initState.title, this.initState.url); - } - } else { - promise.resolve(); - } - }.bind(this); - this.bindLinkHandler(); - if (this._callback) { - _cbPromise = this._callback(args); - _cbPromise instanceof Promise ? _cbPromise.then(lastly, promise.rejector()) : lastly(); - } else { - lastly(); - } - return promise.then(function() { - this.trigger('ready'); - }); + return Promise.resolve() + .then(function() { + return this._callback(args); + }.bind(this)) + .then(function() { + this.router.startHistory(); + if (!this.router.hasNavigated) { + if (this.initState) { + History.replace(this.initState.state, this.initState.title, this.initState.url); + } + return this.router.exec(this.initialHashRoute || this.initRoute, this.initState, this.initParams); + } + }.bind(this)) + .then(function() { + this.trigger('ready'); + }.bind(this)); }, /** * Binds a global link handler @@ -250,11 +239,10 @@ define(function(require) { * * @param {Lavaca.util.Config} Config cache that's been initialized * - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ beforeInit: function(Config) { - var promise = new Promise(); - return promise.resolve(null); + return Promise.resolve(null); } }); diff --git a/src/mvc/Collection.js b/src/mvc/Collection.js index 0fd2b5b7..1131f32b 100755 --- a/src/mvc/Collection.js +++ b/src/mvc/Collection.js @@ -3,7 +3,6 @@ define(function(require) { var Model = require('lavaca/mvc/Model'), Connectivity = require('lavaca/net/Connectivity'), ArrayUtils = require('lavaca/util/ArrayUtils'), - Promise = require('lavaca/util/Promise'), clone = require('mout/lang/deepClone'), merge = require('mout/object/merge'); @@ -188,7 +187,7 @@ define(function(require) { * @return {Boolean} false if no items were able to be added, true otherwise. */ //@event addItem - + insert: function(insertIndex, item /*, item1, item2, item3...*/) { var result = false, idAttribute = this.TModel.prototype.idAttribute, @@ -647,7 +646,7 @@ define(function(require) { * @method saveToServer * * @param {String} url The URL to which to post the data - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ saveToServer: function(url) { return this.save(function(model, changedAttributes, attributes) { @@ -659,13 +658,13 @@ define(function(require) { changedAttributes[this.idAttribute] = id; data = changedAttributes; } - return (new Promise(this)).when(Connectivity.ajax({ + return Connectivity.ajax({ url: url, cache: false, type: 'POST', data: data, dataType: 'json' - })); + }); }); }, /** diff --git a/src/mvc/Controller.js b/src/mvc/Controller.js index e1f7666a..7231a466 100755 --- a/src/mvc/Controller.js +++ b/src/mvc/Controller.js @@ -3,7 +3,6 @@ define(function(require) { var Connectivity = require('lavaca/net/Connectivity'), History = require('lavaca/net/History'), Disposable = require('lavaca/util/Disposable'), - Promise = require('lavaca/util/Promise'), StringUtils = require('lavaca/util/StringUtils'), Translation = require('lavaca/util/Translation'); @@ -43,7 +42,7 @@ define(function(require) { * * @param {String} action The name of the controller method to call * @param {Object} params Key-value arguments to pass to the action - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ /** * Executes an action on this controller @@ -51,27 +50,21 @@ define(function(require) { * @param {String} action The name of the controller method to call * @param {Object} params Key-value arguments to pass to the action * @param {Object} state A history record object - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ exec: function(action, params, state) { this.params = params; this.state = state; - var promise = new Promise(this), - model, - result; + var model; if (state) { model = state.state; - promise.success(function() { - document.title = state.title; - }); - } - result = this[action](params, model); - if (result instanceof Promise) { - promise.when(result); - } else { - promise.resolve(); } - return promise; + return Promise.resolve(this[action](params, model)) + .then(function() { + if (state) { + document.title = state.title; + } + }); }, /** * Loads a view @@ -81,10 +74,10 @@ define(function(require) { * @param {Function} TView The type of view to load (should derive from [[Lavaca.mvc.View]]) * @param {Object} model The data object to pass to the view * @param {Number} layer The integer indicating what UI layer the view sits on - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ view: function(cacheKey, TView, model, layer) { - return Promise.when(this, this.viewManager.load(cacheKey, TView, model, layer)); + return this.viewManager.load(cacheKey, TView, model, layer); }, /** * Adds a state to the browser history @@ -119,7 +112,7 @@ define(function(require) { * @method redirect * * @param {String} str The URL string - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise * */ /** @@ -127,7 +120,7 @@ define(function(require) { * @method redirect * @param {String} str The URL string * @param {Array} args Format arguments to insert into the URL - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ redirect: function(str, args) { return this.router.unlock().exec(this.url(str, args || [])); diff --git a/src/mvc/Model.js b/src/mvc/Model.js index 41676029..72605862 100755 --- a/src/mvc/Model.js +++ b/src/mvc/Model.js @@ -4,7 +4,6 @@ define(function(require) { Connectivity = require('lavaca/net/Connectivity'), ArrayUtils = require('lavaca/util/ArrayUtils'), Cache = require('lavaca/util/Cache'), - Promise = require('lavaca/util/Promise'), clone = require('mout/lang/deepClone'), merge = require('mout/object/merge'), Config = require('lavaca/util/Config'); @@ -421,14 +420,14 @@ define(function(require) { * @method fetch * * @param {String} url The URL from which to load the data - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ /** * Loads the data for this model from the server and only apply to this model attributes (Note: Does not clear the model first) * @method fetch * * @param {Object} options jQuery AJAX settings. If url property is missing, fetch() will try to use the url property on this model - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ /** * Loads the data for this model from the server and only apply to this model attributes (Note: Does not clear the model first) @@ -436,7 +435,7 @@ define(function(require) { * * @param {String} url The URL from which to load the data * @param {Object} options jQuery AJAX settings - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ fetch: function(url, options) { if (typeof url === 'object') { @@ -446,9 +445,8 @@ define(function(require) { options.url = url; } options.url = this.getApiURL(options.url || this.url); - return Promise.when(this, Connectivity.ajax(options)) - .success(this.onFetchSuccess) - .error(this.onFetchError); + return Connectivity.ajax(options) + .then(this.onFetchSuccess.bind(this), this.onFetchError.bind(this)); }, /** * Converts a relative path to an absolute api url based on environment config 'apiRoot' @@ -475,7 +473,7 @@ define(function(require) { * @param {Function} callback A function callback(model, changedAttributes, attributes) * that returns either a promise or a truthy value * indicating whether the operation failed or succeeded - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ /** * Saves the model @@ -485,50 +483,47 @@ define(function(require) { * that returns either a promise or a truthy value * indicating whether the operation failed or succeeded * @param {Object} thisp The context for the callback - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ //* @event saveSuccess //* @event saveError save: function(callback, thisp) { - var promise = new Promise(this), - attributes = this.toObject(), + var attributes = this.toObject(), changedAttributes = {}, i = -1, - attribute, - result; + attribute; + while (!!(attribute = this.unsavedAttributes[++i])) { changedAttributes[attribute] = attributes[attribute]; } - promise - .success(function(value) { - var idAttribute = this.idAttribute; - if (this.isNew() && value[idAttribute] !== UNDEFINED) { - this.set(idAttribute, value[idAttribute]); - } - this.unsavedAttributes = []; - this.trigger('saveSuccess', {response: value}); - }) - .error(function(value) { - this.trigger('saveError', {response: value}); - }); - result = callback.call(thisp || this, this, changedAttributes, attributes); - if (result instanceof Promise) { - promise.when(result); - } else if (result) { - promise.resolve(result); - } else { - promise.reject(result); - } - return promise; + + return Promise.resolve() + .then(function() { + return callback.call(thisp || this, this, changedAttributes, attributes); + }.bind(this)) + .then( + function(value) { + var idAttribute = this.idAttribute; + if (this.isNew() && value[idAttribute] !== UNDEFINED) { + this.set(idAttribute, value[idAttribute]); + } + this.unsavedAttributes = []; + this.trigger('saveSuccess', {response: value}); + return value; + }.bind(this), + function(value) { + this.trigger('saveError', {response: value}); + }.bind(this) + ); }, /** * Saves the model to the server via POST * @method saveToServer * * @param {String} url The URL to which to post the data - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ saveToServer: function(url) { return this.save(function(model, changedAttributes, attributes) { @@ -540,13 +535,13 @@ define(function(require) { changedAttributes[this.idAttribute] = id; data = changedAttributes; } - return Promise.when(this, Connectivity.ajax({ + return Connectivity.ajax({ url: url, cache: false, type: 'POST', data: data, dataType: 'json' - })); + }); }); }, /** diff --git a/src/mvc/Route.js b/src/mvc/Route.js index c99a2920..91917749 100755 --- a/src/mvc/Route.js +++ b/src/mvc/Route.js @@ -152,7 +152,7 @@ define(function(require) { * @param {String} url The URL that supplies parameters to this route * @param {Lavaca.mvc.Router} router The router used by the application * @param {Lavaca.mvc.ViewManager} viewManager The view manager used by the application - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ /** * Executes this route's controller action see if work @@ -162,7 +162,7 @@ define(function(require) { * @param {Lavaca.mvc.Router} router The router used by the application * @param {Lavaca.mvc.ViewManager} viewManager The view manager used by the application * @param {Object} state A history record object - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ /** * Executes this route's controller action see if work @@ -173,17 +173,13 @@ define(function(require) { * @param {Lavaca.mvc.ViewManager} viewManager The view manager used by the application * @param {Object} state A history record object * @param {Object} params Additional parameters to pass to the controller action - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ exec: function(url, router, viewManager, state, params) { var controller = new this.TController(router, viewManager), - urlParams = this.parse(url), - promise = controller.exec(this.action, merge(urlParams, params), state); - function dispose() { - setTimeout(this.dispose(),0); - } - promise.then(dispose, dispose); - return promise; + urlParams = this.parse(url); + return controller.exec(this.action, merge(urlParams, params), state) + .then(this.dispose.bind(this), this.dispose.bind(this)); } }); diff --git a/src/mvc/Router.js b/src/mvc/Router.js index 5ad70151..8dfc7ce7 100755 --- a/src/mvc/Router.js +++ b/src/mvc/Router.js @@ -2,8 +2,7 @@ define(function(require) { var Route = require('./Route'), History = require('lavaca/net/History'), - Disposable = require('lavaca/util/Disposable'), - Promise = require('lavaca/util/Promise'); + Disposable = require('lavaca/util/Disposable'); /** * @class lavaca.mvc.Router @@ -46,9 +45,10 @@ define(function(require) { this.onpopstate = function(e) { if (this.hasNavigated) { History.isRoutingBack = e.direction === 'back'; - this.exec(e.url, e).always(function() { + var _always = function() { History.isRoutingBack = false; - }); + }; + this.exec(e.url, e).then(_always, _always); } }; History.on('popstate', this.onpopstate, this); @@ -116,7 +116,7 @@ define(function(require) { * @method exec * * @param {String} url The URL - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ /** * Executes the action for a given URL @@ -124,7 +124,7 @@ define(function(require) { * * @param {String} url The URL * @param {Object} state A history record object - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ /** * Executes the action for a given URL @@ -133,37 +133,43 @@ define(function(require) { * @param {String} url The URL * @param {Object} state A history record object * @param {Object} params Additional parameters to pass to the route - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ exec: function(url, state, params) { if (this.locked) { - return (new Promise(this)).reject('locked'); + return Promise.reject('locked'); } else { this.locked = true; } + if (!url) { url = '/'; } if (url.indexOf('http') === 0) { url = url.replace(/^http(s?):\/\/.+?/, ''); } + var i = -1, - route, - promise = new Promise(this); - promise.always(function() { - this.unlock(); - }); - if (!this.hasNavigated) { - promise.success(function() { - this.hasNavigated = true; - }); - } + route; + while (!!(route = this.routes[++i])) { if (route.matches(url)) { - return promise.when(route.exec(url, this, this.viewManager, state, params)); + break; } } - return promise.reject(url, state); + + if (!route) { + return Promise.reject([url, state]); + } + + return Promise.resolve() + .then(function() { + return route.exec(url, this, this.viewManager, state, params); + }.bind(this)) + .then(function() { + this.hasNavigated = true; + }.bind(this)) + .then(this.unlock.bind(this), this.unlock.bind(this)); }, /** * Unlocks the router so that it can be used again diff --git a/src/mvc/View.js b/src/mvc/View.js index 05e370e5..aeb1604a 100755 --- a/src/mvc/View.js +++ b/src/mvc/View.js @@ -5,7 +5,6 @@ define(function(require) { Model = require('lavaca/mvc/Model'), Template = require('lavaca/ui/Template'), Cache = require('lavaca/util/Cache'), - Promise = require('lavaca/util/Promise'), ArrayUtils = require('lavaca/util/ArrayUtils'), uuid = require('lavaca/util/uuid'); @@ -173,36 +172,6 @@ define(function(require) { */ viewType: null, - bindRenderEvents: function(renderPromise) { - var promise = new Promise(this); - /* - * Fires when html from template has rendered - * @event rendersuccess - */ - promise - .success(function(html) { - this.trigger('rendersuccess', {html: html}); - renderPromise.resolve(); - }) - /** - * Fired when there was an error during rendering process - * @event rendererror - */ - .error(function(err) { - this.trigger('rendererror', {err: err}); - renderPromise.reject(); - }); - - return promise; - }, - - renderTemplate: function(template, promise, model) { - return template - .render(model) - .success(promise.resolver()) - .error(promise.rejector()); - }, - getRenderModel: function() { var model = this.model; return model instanceof Model ? model.toObject() : model; @@ -214,21 +183,26 @@ define(function(require) { * @return {lavaca.util.Promise} A promise */ render: function() { - var self = this, - renderPromise = new Promise(this), - promise = this.bindRenderEvents(renderPromise), - template = Template.get(this.template), + var template = Template.get(this.template), model = this.getRenderModel(); - this.renderTemplate(template, promise, model) - .then(function() { - if (self.className){ - self.el.addClass(self.className); - } - }); - - return renderPromise; + if (this.className){ + this.el.addClass(this.className); + } + return Promise.resolve() + .then(function() { + return template.render(model); + }) + .then( + function(html) { + this.trigger('rendersuccess', {html: html}); + }.bind(this), + function(err) { + this.trigger('rendererror', {err: err}); + throw err; + }.bind(this) + ); }, /** * Renders the view using its template and model @@ -237,9 +211,7 @@ define(function(require) { * @return {lavaca.util.Promise} A promise */ renderPageView: function() { - var renderPromise = new Promise(this), - promise = this.bindRenderEvents(renderPromise), - template = Template.get(this.template), + var template = Template.get(this.template), model = this.getRenderModel(); if (this.el) { @@ -254,10 +226,19 @@ define(function(require) { this.shell.addClass(this.className); } - this.renderTemplate(template, promise, model); - - return renderPromise; - + return Promise.resolve() + .then(function() { + return template.render(model); + }) + .then( + function(html) { + this.trigger('rendersuccess', {html: html}); + }.bind(this), + function(err) { + this.trigger('rendererror', {err: err}); + throw err; + }.bind(this) + ); }, /** @@ -327,13 +308,13 @@ define(function(require) { */ redraw: function(selector, model) { var self = this, - templateRenderPromise = new Promise(this), - redrawPromise = new Promise(this), template = Template.get(this.template), replaceAll; + if (!template) { - return redrawPromise.reject(); + return Promise.reject(); } + if (typeof selector === 'object' || selector instanceof Model) { model = selector; replaceAll = true; @@ -358,20 +339,24 @@ define(function(require) { self.applyChildViewEvents(); self.trigger('redrawsuccess'); } - templateRenderPromise - .success(function(html) { + + return Promise.resolve() + .then(function() { + return template.render(model); + }) + .then(function(html) { if (replaceAll) { this.disposeChildViews(this.el); this.disposeWidgets(this.el); this.el.html(html); processMaps(); - redrawPromise.resolve(html); - return; + return html; } + if(selector) { var $newEl = $('
' + html + '
').find(selector), - $oldEl = this.el.find(selector); - if($newEl.length === $oldEl.length) { + $oldEl = this.el.find(selector); + if ($newEl.length === $oldEl.length) { $oldEl.each(function(index) { var $el = $(this); self.disposeChildViews($el); @@ -379,20 +364,14 @@ define(function(require) { $el.replaceWith($newEl.eq(index)).remove(); }); processMaps(); - redrawPromise.resolve(html); + return html; } else { - redrawPromise.reject('Count of items matching selector is not the same in the original html and in the newly rendered html.'); + throw 'Count of items matching selector is not the same in the original html and in the newly rendered html.'; } - } else { - redrawPromise.resolve(html); } - }) - .error(redrawPromise.rejector()); - template - .render(model) - .success(templateRenderPromise.resolver()) - .error(templateRenderPromise.rejector()); - return redrawPromise; + + return html; + }.bind(this)); }, /** @@ -890,28 +869,18 @@ define(function(require) { * @return {lavaca.util.Promise} A promise */ enter: function(container) { - var promise = new Promise(this), - renderPromise; - container = $(container); + var renderPromise; if (!this.hasRendered) { - renderPromise = this - .render() - .error(promise.rejector()); - } - this.insertInto(container); - if (renderPromise) { - promise.when(renderPromise); - } else { - setTimeout(promise.resolver().bind(this),0); + renderPromise = this.render(); } - promise.then(function() { - /** - * Fired when there was an error during rendering process - * @event rendererror - */ - this.trigger('enter'); - }); - return promise; + return Promise.resolve() + .then(renderPromise) + .then(function() { + return this.insertInto( $(container) ); + }.bind(this)) + .then(function() { + this.trigger('enter'); + }.bind(this)); }, /** * Executes when the user navigates away from this view @@ -922,17 +891,9 @@ define(function(require) { * @return {lavaca.util.Promise} A promise */ exit: function() { - var promise = new Promise(this); this.shell.detach(); - setTimeout(promise.resolver(),0); - promise.then(function() { - /** - * Fired when there was an error during rendering process - * @event rendererror - */ - this.trigger('exit'); - }); - return promise; + this.trigger('exit'); + return Promise.resolve(); } }); diff --git a/src/mvc/ViewManager.js b/src/mvc/ViewManager.js index 1e504871..a5ac6014 100755 --- a/src/mvc/ViewManager.js +++ b/src/mvc/ViewManager.js @@ -5,8 +5,9 @@ define(function(require) { ArrayUtils = require('lavaca/util/ArrayUtils'), Cache = require('lavaca/util/Cache'), Disposable = require('lavaca/util/Disposable'), - Promise = require('lavaca/util/Promise'), - merge = require('mout/object/merge'); + merge = require('mout/object/merge'), + contains = require('mout/array/contains'), + difference = require('mout/array/difference'); /** * Manager responsible for drawing views @@ -81,7 +82,7 @@ define(function(require) { * @param {Function} TPageView The type of view to load (should derive from [[Lavaca.mvc.View]]) * @param {Object} model The views model * @param {Number} layer The index of the layer on which the view will sit - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ /** * Loads a view @@ -91,36 +92,31 @@ define(function(require) { * @param {Function} TPageView The type of view to load (should derive from [[Lavaca.mvc.View]]) * @param {Object} model The views model * @param {Object} params Parameters to be mapped to the view - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ load: function(cacheKey, TPageView, model, params) { if (this.locked) { - return (new Promise(this)).reject('locked'); + return Promise.reject('locked'); } else { this.locked = true; } params = params || {}; - var self = this, - layer = TPageView.prototype.layer || 0, - pageView = this.pageViews.get(cacheKey), - promise = new Promise(this), - enterPromise = new Promise(promise), - renderPromise = null, - exitPromise = null; - promise.always(function() { - this.locked = false; - }); + var layer = TPageView.prototype.layer || 0, + pageView = this.pageViews.get(cacheKey); + if (typeof params === 'number') { layer = params; } else if (params.layer) { layer = params.layer; } + + var shouldRender = false; if (!pageView) { pageView = new TPageView(null, model, layer); if (typeof params === 'object') { merge(pageView, params); } - renderPromise = pageView.renderPageView(); + shouldRender = true; if (cacheKey !== null) { this.pageViews.set(cacheKey, pageView); pageView.cacheKey = cacheKey; @@ -130,96 +126,96 @@ define(function(require) { merge(pageView, params); } } - function lastly() { - self.enteringPageViews = [pageView]; - promise.success(function() { - setTimeout(function() { - self.enteringPageViews = []; - }.bind(this)); - }); - self.beforeEnterExit(layer - 1, pageView).then(function(){ - exitPromise = self.dismissLayersAbove(layer - 1, pageView); - if (self.layers[layer] !== pageView) { - enterPromise - .when(pageView.enter(self.el, self.exitingPageViews), exitPromise) - .then(promise.resolve); - self.layers[layer] = pageView; - } else { - promise.when(exitPromise); + + return Promise.resolve() + .then(function() { + if (shouldRender) { + return pageView.renderPageView(); } - }); - } - if (renderPromise) { - renderPromise.then(lastly, promise.rejector()); - } else { - lastly(); - } - return promise; + }) + .then(function() { + return this.beforeEnterExit(layer-1, pageView); + }.bind(this)) + .then(function() { + this.enteringPageViews = [pageView]; + return Promise.all([ + (function() { + if (this.layers[layer] !== pageView) { + return pageView.enter(this.el, this.exitingPageViews); + } + }.bind(this))(), + (function() { + return this.dismissLayersAbove(layer-1, pageView); + }.bind(this))() + ]); + }.bind(this)) + .then(function() { + this.locked = false; + this.enteringPageViews = []; + this.layers[layer] = pageView; + }.bind(this)); }, /** - * Execute beforeEnter or beforeExit for each layer. Both functions + * Execute beforeEnter or beforeExit for each layer. Both functions * beforeEnter and beforeExit must return promises. * @method beforeEnterExit * * @param {Number} index The index above which is to be cleared - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ /** - * Execute beforeEnter or beforeExit for each layer. Both functions - * beforeEnter and beforeExit must return promises. + * Execute beforeEnter or beforeExit for each layer. Both functions + * beforeEnter and beforeExit must return promises. * @method beforeEnterExit * * @param {Number} index The index above which is to be cleared * @param {Lavaca.mvc.View} enteringView A view that will be entering - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ beforeEnterExit: function(index, enteringView) { - var promise = new Promise(this), - i, + var i, layer, - promiseArray = []; + list = []; if (enteringView && typeof enteringView.beforeEnter === 'function') { - promiseArray.push(enteringView.beforeEnter()); + list.push(enteringView.beforeEnter()); } for (i = this.layers.length - 1; i > index; i--) { if ((layer = this.layers[i]) && (!enteringView || enteringView !== layer)) { (function(layer) { if (typeof layer.beforeExit === 'function') { - promiseArray.push(layer.beforeExit()); + list.push(layer.beforeExit()); } }).call(this, layer); } } - if (promiseArray.length === 0) { - promise.resolve(); - } else { - promise.when.apply(promise, promiseArray); - } - return promise; + return Promise.all(list); }, /** * Removes all views on a layer * @method dismiss * * @param {Number} index The index of the layer to remove + * @return {Promise} A promise */ /** * Removes all views on a layer * @method dismiss * * @param {jQuery} el An element on the layer to remove (or the layer itself) + * @return {Promise} A promise */ /** * Removes all views on a layer * @method dismiss * * @param {Lavaca.mvc.View} view The view on the layer to remove + * @return {Promise} A promise */ dismiss: function(layer) { if (typeof layer === 'number') { - this.dismissLayersAbove(layer - 1); + return this.dismissLayersAbove(layer - 1); } else if (layer instanceof View) { - this.dismiss(layer.layer); + return this.dismiss(layer.layer); } else { layer = $(layer); var index = layer.attr('data-layer-index'); @@ -228,7 +224,7 @@ define(function(require) { index = layer.attr('data-layer-index'); } if (index !== null) { - this.dismiss(Number(index)); + return this.dismiss(Number(index)); } } }, @@ -237,7 +233,7 @@ define(function(require) { * @method dismissLayersAbove * * @param {Number} index The index above which to clear - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ /** * Removes all layers above a given index @@ -245,36 +241,37 @@ define(function(require) { * * @param {Number} index The index above which to clear * @param {Lavaca.mvc.View} exceptForView A view that should not be dismissed - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ dismissLayersAbove: function(index, exceptForView) { - var promise = new Promise(this), - dismissedLayers = false, - i, - layer; - for (i = this.layers.length - 1; i > index; i--) { - if ((layer = this.layers[i]) && (!exceptForView || exceptForView !== layer)) { - (function(layer) { - this.exitingPageViews.push(layer); - promise - .when(layer.exit(this.el, this.enteringPageViews)) - .success(function() { - setTimeout(function() { - ArrayUtils.remove(this.exitingPageViews, layer); - if (!layer.cacheKey || (exceptForView && exceptForView.cacheKey === layer.cacheKey)) { - layer.dispose(); - } - }.bind(this)); - }); - this.layers[i] = null; - }).call(this, layer); - dismissedLayers = true; + var toDismiss = this.layers.slice(index+1) + .filter(function(layer) { + return (layer && (!exceptForView || exceptForView !== layer)); + }); + + this.layers = this.layers.map(function(layer) { + if (contains(toDismiss, layer)) { + return null; } - } - if (!dismissedLayers) { - promise.resolve(); - } - return promise; + return layer; + }); + + var promises = toDismiss + .map(function(layer) { + return Promise.resolve() + .then(function() { + this.exitingPageViews.push(layer); + return layer.exit(this.el, this.enteringPageViews); + }.bind(this)) + .then(function() { + ArrayUtils.remove(this.exitingPageViews, layer); + if (!layer.cacheKey || (exceptForView && exceptForView.cacheKey === layer.cacheKey)) { + layer.dispose(); + } + }.bind(this)); + }.bind(this)); + + return Promise.all(promises); }, /** * Empties the view cache diff --git a/src/net/Connectivity.js b/src/net/Connectivity.js index 25a21b7f..9590b6d6 100755 --- a/src/net/Connectivity.js +++ b/src/net/Connectivity.js @@ -1,7 +1,6 @@ define(function(require) { var $ = require('$'), - Promise = require('lavaca/util/Promise'), resolve = require('lavaca/util/resolve'); /** @@ -53,31 +52,19 @@ define(function(require) { * @static * * @param {Object} opts jQuery-style AJAX options - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ Connectivity.ajax = function(opts) { - var promise = new Promise(), - origSuccess = opts.success, - origError = opts.error; - opts.success = function() { - if (origSuccess) { - origSuccess.apply(this, arguments); - } - promise.resolve.apply(promise, arguments); - }; - opts.error = function() { - if (origError) { - origError.apply(this, arguments); - } - promise.reject.apply(promise, arguments); - }; - if (Connectivity.isOffline() && !_isLocalUrl(opts.url)) { - promise.reject(_offlineErrorCode); - } else { - $.ajax(opts); - } - promise.error(_onAjaxError); - return promise; + return Promise.resolve() + .then(function() { + if (Connectivity.isOffline() && !_isLocalUrl(opts.url)) { + throw _offlineErrorCode; + } + }) + .then(function() { + return $.ajax(opts); + }) + .catch(_onAjaxError); }; /** diff --git a/src/ui/DustTemplate.js b/src/ui/DustTemplate.js index 96a7e9c2..92cc4af8 100755 --- a/src/ui/DustTemplate.js +++ b/src/ui/DustTemplate.js @@ -3,7 +3,6 @@ define(function(require) { var dust = require('dust'), Template = require('lavaca/ui/Template'), Config = require('lavaca/util/Config'), - Promise = require('lavaca/util/Promise'), StringUtils = require('lavaca/util/StringUtils'), Translation = require('lavaca/util/Translation'); require('dust-helpers'); @@ -94,7 +93,7 @@ define(function(require) { } return chunk.write(StringUtils.format.apply(this, args)); }, - /** + /** * Helper function, exposed in dust templates, that uses * [[Lavaca.ui.Template]] to include other templates. Accessed as: * @@ -119,15 +118,25 @@ define(function(require) { var name = dust.helpers.tap(params.name, chunk, context), result; - // Note that this only works because - // dust renders are synchronous so - // the .then() is called before this - // helper function returns - Template - .render(name, context.stack.head) - .then(function(html) { - result = html; - }); + // dust is the "asynchronous" template language... which doesn't allow its + // helpers to be asynchronous. I'm duplicating code from + // DustTemplate#render to avoid the Promise wrapper (which per the + // es6 spec must operate on a different turn of the event loop for each `.then`). + // The dust.render callback is also on a different turn of the event loop via + // setTimeout(0), but all calls within the same turn of the event + // loop will be in sequence (effectively synchronous) on the next turn. + var template = Template.get(name); + if (!template.code && template.src) { + template.load(template.src); + } + if (template.code && !template.compiled) { + template.compile(); + template.compiled = true; + } + dust.render(name, context.stack.head, function(err, html) { + result = html; + }); + return chunk.write(result); }, /** @@ -208,10 +217,9 @@ define(function(require) { * @method render * * @param {Object} model The data model to provide to the template - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ render: function(model) { - var promise = new Promise(this); if (!this.code && this.src) { this.load(this.src); } @@ -219,14 +227,15 @@ define(function(require) { this.compile(); this.compiled = true; } - dust.render(this.name, model, function(err, html) { - if (err) { - promise.reject(err); - } else { - promise.resolve(html); - } - }); - return promise; + return new Promise(function(resolve, reject) { + dust.render(this.name, model, function(err, html) { + if (err) { + reject(err); + } else { + resolve(html); + } + }); + }.bind(this)); }, /** * Makes this template ready for disposals diff --git a/src/ui/Form.js b/src/ui/Form.js index 3e09e719..1ab4ee02 100755 --- a/src/ui/Form.js +++ b/src/ui/Form.js @@ -1,8 +1,7 @@ define(function(require) { var $ = require('$'), - Widget = require('lavaca/ui/Widget'), - Promise = require('lavaca/util/Promise'); + Widget = require('lavaca/ui/Widget'); function _required(value) { return value ? null : 'error_required'; @@ -361,20 +360,20 @@ define(function(require) { /** * Checks the entire form to see if it's in a valid state * @method validate - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ /** * Checks the entire form to see if it's in a valid state * @method validate * @param {Function} succcess A callback to execute when the form is valid * @param {Function} error A callback to execute when the form is invalid - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ /** * Checks the entire form to see if it's in a valid state * @method validate * @param {jQuery} input An input to check - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ /** * Checks the entire form to see if it's in a valid state @@ -382,7 +381,7 @@ define(function(require) { * @param {Function} succcess A callback to execute when the input is valid * @param {Function} error A callback to execute when the input is invalid * @param {jQuery} input An input to check - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ validate: function(success, error, input) { if (success && typeof success !== 'function') { diff --git a/src/ui/Template.js b/src/ui/Template.js index 24c1bfa1..79cfb7f8 100755 --- a/src/ui/Template.js +++ b/src/ui/Template.js @@ -17,7 +17,7 @@ define(function(require) { * @method render * * @param {Object} model The data model to provide to the template - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ render: function() { throw 'Abstract'; @@ -50,7 +50,7 @@ define(function(require) { * @static */ /** - * + * * Scans the document for all templates with registered types and * prepares template objects from them * @method init @@ -97,7 +97,7 @@ define(function(require) { * * @param {String} name The name of the template * @param {Object} model The data model to provide to the template - * @return {Lavaca.util.Promise} A promise + * @return {Promise} A promise */ Template.render = function(name, model) { var template = Template.get(name); diff --git a/src/util/Map.js b/src/util/Map.js index 5e297a76..1c606154 100755 --- a/src/util/Map.js +++ b/src/util/Map.js @@ -2,8 +2,7 @@ define(function(require) { var $ = require('$'), Cache = require('./Cache'), - Disposable = require('./Disposable'), - Connectivity = require('lavaca/net/Connectivity'); + Disposable = require('./Disposable'); function _absolute(url) { if (url && url.indexOf('http') !== 0) { @@ -118,7 +117,7 @@ define(function(require) { */ load: function(url) { var self = this; - Connectivity.ajax({ + $.ajax({ async: false, url: url, success: function(resp) { diff --git a/src/util/Promise.js b/src/util/Promise.js deleted file mode 100755 index 9340d83c..00000000 --- a/src/util/Promise.js +++ /dev/null @@ -1,225 +0,0 @@ -define(function(require) { - - var extend = require('./extend'); - - /** - * Utility type for asynchronous programming - * @class lavaca.util.Promise - * - * @constructor - * - * @param {Object} thisp What the "this" keyword resolves to in callbacks - */ - var Promise = extend(function(thisp) { - /** - * What the "this" keyword resolves to in callbacks - * @property {Object} thisp - * @default null - */ - this.thisp = thisp; - /** - * Pending handlers for the success event - * @property {Array} resolvedQueue - * @default [] - */ - this.resolvedQueue = []; - /** - * Pending handlers for the error event - * @property {Array} rejectedQueue - * @default [] - */ - this.rejectedQueue = []; - }, { - /** - * Flag indicating that the promise completed successfully - * @property {Boolean} succeeded - * @default false - */ - succeeded: false, - /** - * Flag indicating that the promise failed to complete - * @property {Boolean} failed - * @default false - */ - failed: false, - /** - * Queues a callback to be executed when the promise succeeds - * @method success - * - * @param {Function} callback The callback to execute - * @return {Lavaca.util.Promise} This promise (for chaining) - */ - success: function(callback) { - if (callback) { - if (this.succeeded) { - callback.apply(this.thisp, this.resolveArgs); - } else { - this.resolvedQueue.push(callback); - } - } - return this; - }, - /** - * Queues a callback to be executed when the promise fails - * @method error - * - * @param {Function} callback The callback to execute - * @return {Lavaca.util.Promise} This promise (for chaining) - */ - error: function(callback) { - if (callback) { - if (this.failed) { - callback.apply(this.thisp, this.rejectArgs); - } else { - this.rejectedQueue.push(callback); - } - } - return this; - }, - /** - * Queues a callback to be executed when the promise is either rejected or resolved - * @method always - * - * @param {Function} callback The callback to execute - * @return {Lavaca.util.Promise} This promise (for chaining) - */ - always: function(callback) { - return this.then(callback, callback); - }, - /** - * Queues up callbacks after the promise is completed - * @method then - * - * @param {Function} resolved A callback to execute when the operation succeeds - * @param {Function} rejected A callback to execute when the operation fails - * @return {Lavaca.util.Promise} This promise (for chaining) - */ - then: function(resolved, rejected) { - return this - .success(resolved) - .error(rejected); - }, - /** - * Resolves the promise successfully - * @method resolve - * - * @params {Object} value Values to pass to the queued success callbacks - * @return {Lavaca.util.Promise} This promise (for chaining) - */ - resolve: function(/* value1, value2, valueN */) { - if (!this.succeeded && !this.failed) { - this.succeeded = true; - this.resolveArgs = [].slice.call(arguments, 0); - var i = -1, - callback; - while (!!(callback = this.resolvedQueue[++i])) { - callback.apply(this.thisp, this.resolveArgs); - } - } - return this; - }, - /** - * Resolves the promise as a failure - * @method reject - * - * @params {String} err Failure messages - * @return {Lavaca.util.Promise} This promise (for chaining) - */ - reject: function(/* err1, err2, errN */) { - if (!this.succeeded && !this.failed) { - this.failed = true; - this.rejectArgs = [].slice.call(arguments, 0); - var i = -1, - callback; - while (!!(callback = this.rejectedQueue[++i])) { - callback.apply(this.thisp, this.rejectArgs); - } - } - return this; - }, - /** - * Queues this promise to be resolved only after several other promises - * have been successfully resolved, or immediately rejected when one - * of those promises is rejected - * @method when - * - * @params {Lavaca.util.Promise} promise One or more other promises - * @return {Lavaca.util.Promise} This promise (for chaining) - */ - when: function(/* promise1, promise2, promiseN */) { - var self = this, - values = [], - i = -1, - pendingPromiseCount = arguments.length, - promise; - while (!!(promise = arguments[++i])) { - (function(index) { - promise - .success(function(v) { - values[index] = v; - if (--pendingPromiseCount < 1) { - self.resolve.apply(self, values); - } - }) - .error(function() { - self.reject.apply(self, arguments); - }); - })(i); - } - promise = null; - return this; - }, - /** - * Produces a callback that resolves the promise with any number of arguments - * @method resolver - * @return {Function} The callback - */ - resolver: function() { - var self = this; - return function() { - self.resolve.apply(self, arguments); - }; - }, - /** - * Produces a callback that rejects the promise with any number of arguments - * @method rejector - * - * @return {Function} The callback - */ - rejector: function() { - var self = this; - return function() { - self.reject.apply(self, arguments); - }; - } - }); - /** - * - * Creates a promise to be resolved only after several other promises - * have been successfully resolved, or immediately rejected when one - * of those promises is rejected - * @method when - * @static - * @params {Lavaca.util.Promise} promise One or more other promises - * @return {Lavaca.util.Promise} The new promise - */ - /** - * Creates a promise to be resolved only after several other promises - * have been successfully resolved, or immediately rejected when one - * of those promises is rejected - * @method when - * @static - * @param {Object} thisp The execution context of the promise - * @params {Lavaca.util.Promise} promise One or more other promises - * @return {Lavaca.util.Promise} The new promise - */ - Promise.when = function(thisp/*, promise1, promise2, promiseN */) { - var thispIsPromise = thisp instanceof Promise, - promise = new Promise(thispIsPromise ? window : thisp), - args = [].slice.call(arguments, thispIsPromise ? 0 : 1); - return promise.when.apply(promise, args); - }; - - return Promise; - -}); diff --git a/test/unit/.jshintrc b/test/unit/.jshintrc index ea895e54..48327279 100755 --- a/test/unit/.jshintrc +++ b/test/unit/.jshintrc @@ -1,6 +1,7 @@ { "globals": { "define": false, + "Promise": false, "describe": false, "expect": false, "beforeEach": false, diff --git a/test/unit/mvc/Controller.js b/test/unit/mvc/Controller.js index 054dfcc5..6c88f8db 100755 --- a/test/unit/mvc/Controller.js +++ b/test/unit/mvc/Controller.js @@ -39,12 +39,18 @@ define(function(require) { it('can execute an action on itself', function() { var controller = new testController(router, viewManager), params = {one: 1, two: 2}, - promise = controller.exec('foo', params); - promise.success(function() { - expect(ob.foo).toHaveBeenCalled(); - expect(ob.foo.calls[0].args[0].one).toBe(1); - expect(ob.foo.calls[0].args[0].two).toBe(2); + done = false; + runs(function() { + controller.exec('foo', params).then(function() { + expect(ob.foo).toHaveBeenCalled(); + expect(ob.foo.calls[0].args[0].one).toBe(1); + expect(ob.foo.calls[0].args[0].two).toBe(2); + done = true; + }); }); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); describe('can load a view', function() { var noop = { @@ -63,20 +69,18 @@ define(function(require) { myPageView = View.extend({ template: 'hello-world', }), - promise, + done = false, response; runs(function() { - promise = controller.view('myView', myPageView); - }); - waitsFor(function() { - promise.success(function() { - response = this.viewManager.pageViews.get('myView').hasRendered; + controller.view('myView', myPageView).then(function() { + response = viewManager.pageViews.get('myView').hasRendered; + expect(response).toBe(true); + done = true; }); - return response; - }, 'a view to be rendered', 300); - runs(function() { - expect(response).toBe(true); }); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); }); it('can add a state to the browser history', function() { @@ -100,11 +104,16 @@ define(function(require) { describe('can redirect user to another route', function() { it('directly', function() { var controller = new testController(router, viewManager), - promise; - promise = controller.redirect('/foo'); - promise.success(function() { - expect(ob.foo).toHaveBeenCalled(); + done = false; + runs(function() { + controller.redirect('/foo').then(function() { + expect(ob.foo).toHaveBeenCalled(); + done = true; + }); }); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); }); }); diff --git a/test/unit/mvc/Model.js b/test/unit/mvc/Model.js index 9d08588a..974224a8 100755 --- a/test/unit/mvc/Model.js +++ b/test/unit/mvc/Model.js @@ -6,13 +6,13 @@ define(function(require) { var testModel; beforeEach(function() { testModel = new Model(); - }); + }); afterEach(function() { testModel.clear(); }); it('can be initialized', function() { testModel = new Model(); - var type = typeof testModel; + var type = typeof testModel; expect(type).toEqual(typeof new Model()); }); it('can be initialized with a hash of attributes', function() { @@ -41,7 +41,7 @@ define(function(require) { expect(testModel.get('myAttribute')).toEqual(true); expect(testModel.get('email')).toBeNull(); }); - + describe('Saving and IDs', function() { it('should not have an ID if it has not been saved', function() { expect(testModel.get('id')).toBeNull(); @@ -61,22 +61,27 @@ define(function(require) { expect(myModel.id()).toEqual('Hello, World!'); }); it('should get an ID on save', function() { - var promise; + var done = false; testModel.apply({ foo: 'bar', email: 'test@lavaca.com' }); - promise = testModel.save(function(model) { - expect(model.unsavedAttributes.length).toEqual(2); - model.set('email', null); - model.set('id', 1); - return model; - }); - promise.success(function(model) { - expect(model.unsavedAttributes.length).toEqual(0); - expect(model.get('id')).toEqual(1); - expect(model.isNew()).toEqual(false); + runs(function() { + testModel.save(function(model) { + expect(model.unsavedAttributes.length).toEqual(2); + model.set('email', null); + model.set('id', 1); + return model; + }).then(function(model) { + expect(model.unsavedAttributes.length).toEqual(0); + expect(model.get('id')).toEqual(1); + expect(model.isNew()).toEqual(false); + done = true; + }); }); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); }); diff --git a/test/unit/mvc/Router.js b/test/unit/mvc/Router.js index 2f9bbd84..8979ad69 100755 --- a/test/unit/mvc/Router.js +++ b/test/unit/mvc/Router.js @@ -28,16 +28,22 @@ define(function(require) { expect(router.routes.length).toBe(3); }); it('can exec routes that delegate to a controller', function() { - var promise, - testController = Controller.extend(ob); - router.add('/foo/{param}', testController, 'foo', {}); - promise = router.exec('/foo/bar', null, {one: 1}); - promise.success(function() { - expect(ob.foo.calls[0].args[0]).toEqual(jasmine.any(Object)); - expect(ob.foo.calls[0].args[0].param).toEqual('bar'); - expect(ob.foo.calls[0].args[0].one).toEqual(1); - expect(ob.foo.calls[0].args[1]).toBeUndefined(); + var testController = Controller.extend(ob), + done = false; + + runs(function() { + router.add('/foo/{param}', testController, 'foo', {}); + router.exec('/foo/bar', null, {one: 1}).then(function() { + expect(ob.foo.calls[0].args[0]).toEqual(jasmine.any(Object)); + expect(ob.foo.calls[0].args[0].param).toEqual('bar'); + expect(ob.foo.calls[0].args[0].one).toEqual(1); + expect(ob.foo.calls[0].args[1]).toBeUndefined(); + done = true; + }); }); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); }); diff --git a/test/unit/mvc/View.js b/test/unit/mvc/View.js index 638bf51d..b11039d5 100755 --- a/test/unit/mvc/View.js +++ b/test/unit/mvc/View.js @@ -104,186 +104,238 @@ define(function(require) { }); }); it('can be rendered', function() { - var promise; - $('body').append(''); - Template.init(); + var done = false; + runs(function() { + $('body').append(''); + Template.init(); - testView = new View(el, model); - testView.template = 'model-tmpl'; - promise = testView.render(); - promise.success(function() { - expect(testView.hasRendered).toEqual(true); - expect($(testView.el).length).toBe(1); - expect($(testView.el).html()).toBe('

Hello World

Color is blue.

'); + testView = new View(el, model); + testView.template = 'model-tmpl'; + testView.render().then(function() { + expect(testView.hasRendered).toEqual(true); + expect($(testView.el).length).toBe(1); + expect($(testView.el).html()).toBe('

Hello World

Color is blue.

'); + done = true; + }); }); - + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); it('can redraw whole view', function() { - var promise; - $('body').append(''); - Template.init(); + var done = false; + runs(function() { + $('body').append(''); + Template.init(); - testView = new View(el, model); - testView.template = 'model-tmpl'; - promise = testView.render(); - promise.success(function() { - expect(testView.hasRendered).toEqual(true); - expect($(testView.el).length).toBe(1); - expect($(testView.el).html()).toBe('

Hello World

Color is blue.

'); - model.set('color', 'red'); - testView.redraw(); - expect($(testView.el).html()).toBe('

Hello World

Color is red.

'); + testView = new View(el, model); + testView.template = 'model-tmpl'; + testView.render().then(function() { + expect(testView.hasRendered).toEqual(true); + expect($(testView.el).length).toBe(1); + expect($(testView.el).html()).toBe('

Hello World

Color is blue.

'); + model.set('color', 'red'); + return testView.redraw(); + }).then(function() { + expect($(testView.el).html()).toBe('

Hello World

Color is red.

'); + done = true; + }).catch(function(e) { + debugger; + }); + $('script[data-name="model-tmpl"]').remove(); }); - $('script[data-name="model-tmpl"]').remove(); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); it('can redraw whole view using a custom model', function() { - var promise, - otherModel = new Model({color: 'orange', primary: false}); - $('body').append(''); - Template.init(); + var done = false; + runs(function() { + var otherModel = new Model({color: 'orange', primary: false}); + $('body').append(''); + Template.init(); - testView = new View(el, model); - testView.template = 'model-tmpl'; - promise = testView.render(); - promise.success(function() { - expect(testView.hasRendered).toEqual(true); - expect($(testView.el).length).toBe(1); - expect($(testView.el).html()).toBe('

Color is blue.

It is primary

'); - testView.redraw(otherModel); - expect($(testView.el).html()).toBe('

Color is orange.

It is not primary

'); + testView = new View(el, model); + testView.template = 'model-tmpl'; + testView.render().then(function() { + expect(testView.hasRendered).toEqual(true); + expect($(testView.el).length).toBe(1); + expect($(testView.el).html()).toBe('

Color is blue.

It is primary

'); + return testView.redraw(otherModel); + }).then(function() { + expect($(testView.el).html()).toBe('

Color is orange.

It is not primary

'); + done = true; + }); + $('script[data-name="model-tmpl"]').remove(); }); - $('script[data-name="model-tmpl"]').remove(); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); it('can redraw part of a based on a selector', function() { - var promise; - $('body').append(''); - Template.init(); + var done = false; + runs(function() { + $('body').append(''); + Template.init(); - testView = new View(el, model); - testView.template = 'model-tmpl'; - promise = testView.render(); - promise.success(function() { - expect(testView.hasRendered).toEqual(true); - expect($(testView.el).length).toBe(1); - expect($(testView.el).html()).toBe('

Color is blue.

It is primary

'); - model.set('color', 'orange'); - model.set('primary', false); - testView.redraw('p.redraw'); - expect($(testView.el).html()).toBe('

Color is orange.

It is primary

'); + testView = new View(el, model); + testView.template = 'model-tmpl'; + testView.render().then(function() { + expect(testView.hasRendered).toEqual(true); + expect($(testView.el).length).toBe(1); + expect($(testView.el).html()).toBe('

Color is blue.

It is primary

'); + model.set('color', 'orange'); + model.set('primary', false); + return testView.redraw('p.redraw'); + }).then(function() { + expect($(testView.el).html()).toBe('

Color is orange.

It is primary

'); + done = true; + }); + $('script[data-name="model-tmpl"]').remove(); }); - $('script[data-name="model-tmpl"]').remove(); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); it('can redraw part of a based on a selector with a custom model', function() { - var promise, - otherModel = new Model({color: 'orange', primary: false}); - $('body').append(''); - Template.init(); + var done = false; + runs(function() { + var otherModel = new Model({color: 'orange', primary: false}); + $('body').append(''); + Template.init(); - testView = new View(el, model); - testView.template = 'model-tmpl'; - promise = testView.render(); - promise.success(function() { - expect(testView.hasRendered).toEqual(true); - expect($(testView.el).length).toBe(1); - expect($(testView.el).html()).toBe('

Color is blue.

It is primary

'); - testView.redraw('p.redraw', otherModel); - expect($(testView.el).html()).toBe('

Color is orange.

It is primary

'); + testView = new View(el, model); + testView.template = 'model-tmpl'; + testView.render().then(function() { + expect(testView.hasRendered).toEqual(true); + expect($(testView.el).length).toBe(1); + expect($(testView.el).html()).toBe('

Color is blue.

It is primary

'); + return testView.redraw('p.redraw', otherModel); + }).then(function() { + expect($(testView.el).html()).toBe('

Color is orange.

It is primary

'); + done = true; + }); + $('script[data-name="model-tmpl"]').remove(); }); - $('script[data-name="model-tmpl"]').remove(); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); it('can re-render without redrawing', function() { - var promise; - $('body').append(''); - Template.init(); + var done = false; + runs(function() { + $('body').append(''); + Template.init(); - testView = new View(el, model); - testView.template = 'model-tmpl'; - promise = testView.render(); - promise.success(function() { - expect(testView.hasRendered).toEqual(true); - expect($(testView.el).length).toBe(1); - expect($(testView.el).html()).toBe('

Color is blue.

It is primary

'); - model.set('color', 'orange'); - model.set('primary', false); - testView.redraw(false) - .then(function(html) { - expect($(testView.el).html()).toBe('

Color is blue.

It is primary

'); - expect(html).toBe('

Color is orange.

It is not primary

'); - }); + testView = new View(el, model); + testView.template = 'model-tmpl'; + testView.render().then(function() { + expect(testView.hasRendered).toEqual(true); + expect($(testView.el).length).toBe(1); + expect($(testView.el).html()).toBe('

Color is blue.

It is primary

'); + model.set('color', 'orange'); + model.set('primary', false); + return testView.redraw(false); + }).then(function(html) { + expect($(testView.el).html()).toBe('

Color is blue.

It is primary

'); + expect(html).toBe('

Color is orange.

It is not primary

'); + done = true; + }); + $('script[data-name="model-tmpl"]').remove(); }); - $('script[data-name="model-tmpl"]').remove(); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); it('can re-render using a custom model without redrawing', function() { - var promise, - otherModel = new Model({color: 'orange', primary: false}); - $('body').append(''); - Template.init(); + var done = false; + runs(function() { + var otherModel = new Model({color: 'orange', primary: false}); + $('body').append(''); + Template.init(); - testView = new View(el, model); - testView.template = 'model-tmpl'; - promise = testView.render(); - promise.success(function() { - expect(testView.hasRendered).toEqual(true); - expect($(testView.el).length).toBe(1); - expect($(testView.el).html()).toBe('

Color is blue.

It is primary

'); - testView.redraw(false, otherModel) - .then(function(html) { - expect($(testView.el).html()).toBe('

Color is blue.

It is primary

'); - expect(html).toBe('

Color is orange.

It is not primary

'); - }); + testView = new View(el, model); + testView.template = 'model-tmpl'; + testView.render().then(function() { + expect(testView.hasRendered).toEqual(true); + expect($(testView.el).length).toBe(1); + expect($(testView.el).html()).toBe('

Color is blue.

It is primary

'); + return testView.redraw(false, otherModel); + }).then(function(html) { + expect($(testView.el).html()).toBe('

Color is blue.

It is primary

'); + expect(html).toBe('

Color is orange.

It is not primary

'); + done = true; + }); + $('script[data-name="model-tmpl"]').remove(); }); - $('script[data-name="model-tmpl"]').remove(); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); it('can map a widget', function() { - $('body').append(''); - Template.init(); + var done = false; + runs(function() { + $('body').append(''); + Template.init(); - var MyWidget = Widget.extend(function MyWidget() { - Widget.apply(this, arguments); - this.testProp = 'abc'; - }); + var MyWidget = Widget.extend(function MyWidget() { + Widget.apply(this, arguments); + this.testProp = 'abc'; + }); - testView = new View(el, new Model()); - testView.template = 'widget-tmpl'; - testView.mapWidget('.widget', MyWidget); - testView.render().success(function() { - expect(testView.widgets.get('widget').testProp).toEqual('abc'); + testView = new View(el, new Model()); + testView.template = 'widget-tmpl'; + testView.mapWidget('.widget', MyWidget); + testView.render().then(function() { + expect(testView.widgets.get('widget').testProp).toEqual('abc'); + done = true; + }); + $('script[data-name="model-tmpl"]').remove(); }); - $('script[data-name="model-tmpl"]').remove(); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); it('can map a widget with custom arguments', function() { - $('body').append(''); - Template.init(); + var done = false; + runs(function() { + $('body').append(''); + Template.init(); - var MyWidget = Widget.extend(function MyWidget(el, testProp) { - Widget.apply(this, arguments); - this.testProp = testProp; - }); - var MyOtherWidget = Widget.extend(function MyOtherWidget(el, testStr, testInt) { - Widget.apply(this, arguments); - this.testStr = testStr; - this.testInt = testInt; - }); + var MyWidget = Widget.extend(function MyWidget(el, testProp) { + Widget.apply(this, arguments); + this.testProp = testProp; + }); + var MyOtherWidget = Widget.extend(function MyOtherWidget(el, testStr, testInt) { + Widget.apply(this, arguments); + this.testStr = testStr; + this.testInt = testInt; + }); - testView = new View(el, new Model()); - testView.template = 'widget-tmpl'; - testView.mapWidget({ - '.widget': { - TWidget: MyWidget, - args: 'xyz' - }, - '.other-widget': { - TWidget: MyOtherWidget, - args: ['qwert', 12345] - } - }); - testView.render().success(function() { - expect(testView.widgets.get('widget').testProp).toEqual('xyz'); - expect(testView.widgets.get('other-widget').testStr).toEqual('qwert'); - expect(testView.widgets.get('other-widget').testInt).toEqual(12345); - }); + testView = new View(el, new Model()); + testView.template = 'widget-tmpl'; + testView.mapWidget({ + '.widget': { + TWidget: MyWidget, + args: 'xyz' + }, + '.other-widget': { + TWidget: MyOtherWidget, + args: ['qwert', 12345] + } + }); + testView.render().then(function() { + expect(testView.widgets.get('widget').testProp).toEqual('xyz'); + expect(testView.widgets.get('other-widget').testStr).toEqual('qwert'); + expect(testView.widgets.get('other-widget').testInt).toEqual(12345); + done = true; + }); - $('script[data-name="model-tmpl"]').remove(); + $('script[data-name="model-tmpl"]').remove(); + }); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); }); diff --git a/test/unit/mvc/ViewManager.js b/test/unit/mvc/ViewManager.js index 609ca606..6e7c249b 100755 --- a/test/unit/mvc/ViewManager.js +++ b/test/unit/mvc/ViewManager.js @@ -24,161 +24,150 @@ define(function(require) { var myPageView = View.extend(function(){View.apply(this, arguments);},{ template: 'hello-world' }), - promise, - response; + done = false; runs(function() { - promise = viewManager.load('myView', myPageView); + Promise.resolve() + .then(function() { + return viewManager.load('myView', myPageView); + }) + .then(function() { + var response = viewManager.pageViews.get('myView').hasRendered; + expect(response).toBe(true); + done = true; + }); }); waitsFor(function() { - promise.success(function() { - response = viewManager.pageViews.get('myView').hasRendered; - }); - return response; - }, 'a view to be rendered', 300); - runs(function() { - expect(response).toBe(true); - }); + return !!done; + }, 'promises to resolve', 100); }); describe('can remove', function() { it('a view on a layer and all views above', function() { var myPageView = View.extend(function(){View.apply(this, arguments);},{ template: 'hello-world', }), - promise, - secondP, - response; + done = false; runs(function() { - promise = viewManager.load('myView', myPageView); + Promise.resolve() + .then(function() { + return viewManager.load('myView', myPageView); + }) + .then(function() { + return viewManager.load('anotherView', myPageView, null, 1); + }) + .then(function() { + expect($('#view-root').children().length).toBe(2); + return viewManager.dismiss(0); + }) + .then(function() { + expect($('#view-root').children().length).toBe(0); + done = true; + }); }); waitsFor(function() { - promise.success(function() { - response = true; - }); - return response; - }, 'a view to be rendered', 300); - runs(function() { - secondP = viewManager.load('anotherView', myPageView, null, 1); - }); - waitsFor(function() { - secondP.success(function() { - response = true; - }); - return response; - }, 'a view to be rendered', 300); - runs(function() { - expect($('#view-root').children().length).toBe(2); - viewManager.dismiss(0); - expect($('#view-root').children().length).toBe(0); - }); + return !!done; + }, 'promises to resolve', 100); }); it('a view on layer without removing views below', function() { var myPageView = View.extend(function(){View.apply(this, arguments);},{ template: 'hello-world', }), - promise, - secondP, - response; - - runs(function() { - promise = viewManager.load('myView', myPageView); - }); - waitsFor(function() { - promise.success(function() { - response = true; - }); - return response; - }, 'a view to be rendered', 300); + done = false; runs(function() { - secondP = viewManager.load('anotherView', myPageView, null, 1); + Promise.resolve() + .then(function() { + return viewManager.load('myView', myPageView); + }) + .then(function() { + return viewManager.load('anotherView', myPageView, null, 1); + }) + .then(function() { + expect($('#view-root').children().length).toBe(2); + return viewManager.dismiss(1); + }) + .then(function() { + expect($('#view-root').children().length).toBe(1); + done = true; + }); }); waitsFor(function() { - secondP.success(function() { - response = true; - }); - return response; - }, 'a view to be rendered', 300); - runs(function() { - expect($('#view-root').children().length).toBe(2); - viewManager.dismiss(1); - expect($('#view-root').children().length).toBe(1); - }); + return !!done; + }, 'promises to resolve', 100); }); it('a layer by an el', function() { var myPageView = View.extend(function(){View.apply(this, arguments);},{ template: 'hello-world', className: 'test-view', }), - promise, - response; + done = false; runs(function() { - promise = viewManager.load('myView', myPageView); + Promise.resolve() + .then(function() { + return viewManager.load('myView', myPageView); + }) + .then(function() { + return viewManager.dismiss('.test-view'); + }) + .then(function() { + expect($('#view-root').children().length).toBe(0); + done = true; + }); }); waitsFor(function() { - promise.success(function() { - response = viewManager.pageViews.get('myView').hasRendered; - }); - return response; - }, 'a view to be rendered', 300); - runs(function() { - viewManager.dismiss('.test-view'); - expect($('#view-root').children().length).toBe(0); - }); + return !!done; + }, 'promises to resolve', 100); }); it('a layer relative to view object in the cache', function() { var myPageView = View.extend(function(){View.apply(this, arguments);},{ template: 'hello-world', className: 'test-view', }), - promise, - response; + done = false; runs(function() { - promise = viewManager.load('myView', myPageView); + Promise.resolve() + .then(function() { + return viewManager.load('myView', myPageView); + }) + .then(function() { + return viewManager.dismiss(viewManager.pageViews.get('myView')); + }) + .then(function() { + expect($('#view-root').children().length).toBe(0); + done = true; + }); }); waitsFor(function() { - promise.success(function() { - response = viewManager.pageViews.get('myView').hasRendered; - }); - return response; - }, 'a view to be rendered', 300); - runs(function() { - viewManager.dismiss(viewManager.pageViews.get('myView')); - expect($('#view-root').children().length).toBe(0); - }); + return !!done; + }, 'promises to resolve', 100); }); }); it('can empty the view cache', function() { var myPageView = View.extend(function(){View.apply(this, arguments);},{ template: 'hello-world', }), - promise, - secondP, - response; + done = false; runs(function() { - promise = viewManager.load('myView', myPageView); - }); - waitsFor(function() { - promise.success(function() { - response = true; - }); - return response; - }, 'a view to be rendered', 300); - runs(function() { - secondP = viewManager.load('anotherView', myPageView, null, 1); + Promise.resolve() + .then(function() { + return viewManager.load('myView', myPageView); + }) + .then(function() { + return viewManager.load('anotherView', myPageView, null, 1); + }) + .then(function() { + return viewManager.dismiss(1); + }) + .then(function() { + viewManager.flush(); + expect(viewManager.pageViews).toEqual(new Cache()); + expect(viewManager.layers[0].cacheKey).toEqual('myView'); + done = true; + }); }); waitsFor(function() { - secondP.success(function() { - response = true; - }); - return response; - }, 'a view to be rendered', 300); - runs(function() { - viewManager.dismiss(1); - viewManager.flush(); - expect(viewManager.pageViews).toEqual(new Cache()); - expect(viewManager.layers[0].cacheKey).toEqual('myView'); - }); + return !!done; + }, 'promises to resolve', 100); }); }); diff --git a/test/unit/ui/DustTemplate.js b/test/unit/ui/DustTemplate.js index 7aa22972..430b93fe 100755 --- a/test/unit/ui/DustTemplate.js +++ b/test/unit/ui/DustTemplate.js @@ -37,126 +37,180 @@ define(function(require) { Translation.dispose(); }); it('can render a basic template from a string', function() { - var source = '{#names}{.}{~n}{/names}', - context = { names: ['Moe', 'Larry', 'Curly'] }; - - template = new DustTemplate('tmpl', null, source); - template.render(context) - .success(function(html) { - expect(html).toEqual('Moe\nLarry\nCurly\n'); - }) - .error(noop.error); - - expect(noop.error).not.toHaveBeenCalled(); + var done = false; + runs(function() { + var source = '{#names}{.}{~n}{/names}', + context = { names: ['Moe', 'Larry', 'Curly'] }; + + template = new DustTemplate('tmpl', null, source); + template.render(context) + .then(function(html) { + expect(html).toEqual('Moe\nLarry\nCurly\n'); + }, noop.error) + .then(function() { + expect(noop.error).not.toHaveBeenCalled(); + done = true; + }); + }); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); it('can render a basic template from an inline script', function() { - var source = '{#names}{.}{~n}{/names}', - context = { names: ['Moe', 'Larry', 'Curly'] }, - template = _newTemplate(source); - - template.render(context) - .success(function(html) { - expect(html).toEqual('Moe\nLarry\nCurly\n'); - }) - .error(noop.error); - - expect(noop.error).not.toHaveBeenCalled(); + var done = false; + runs(function() { + var source = '{#names}{.}{~n}{/names}', + context = { names: ['Moe', 'Larry', 'Curly'] }, + template = _newTemplate(source); + + template.render(context) + .then(function(html) { + expect(html).toEqual('Moe\nLarry\nCurly\n'); + }, noop.error) + .then(function() { + expect(noop.error).not.toHaveBeenCalled(); + done = true; + }); + }); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); it('can use a LinkedIn helper', function() { - var source = '{@eq key="{val}" value="foo"}equal{/eq}', - context = { val: 'foo' }, - template = _newTemplate(source); - - template.render(context) - .success(function(html) { - expect(html).toEqual('equal'); - }) - .error(noop.error); - - expect(noop.error).not.toHaveBeenCalled(); + var done = false; + runs(function() { + var source = '{@eq key="{val}" value="foo"}equal{/eq}', + context = { val: 'foo' }, + template = _newTemplate(source); + + template.render(context) + .then(function(html) { + expect(html).toEqual('equal'); + }, noop.error) + .then(function() { + expect(noop.error).not.toHaveBeenCalled(); + done = true; + }); + }); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); it('can use a translation', function() { - var source = '{@msg key="test-value"/}', - context = {}, - template; - - _newTranslation('{"test-value": "hello world"}'); - template = _newTemplate(source); - - template.render(context) - .success(function(html) { - expect(html).toEqual('hello world'); - }) - .error(noop.error); - - expect(noop.error).not.toHaveBeenCalled(); + var done = false; + runs(function() { + var source = '{@msg key="test-value"/}', + context = {}, + template; + + _newTranslation('{"test-value": "hello world"}'); + template = _newTemplate(source); + + template.render(context) + .then(function(html) { + expect(html).toEqual('hello world'); + }, noop.error) + .then(function() { + expect(noop.error).not.toHaveBeenCalled(); + done = true; + }); + }); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); it('can use an include', function() { - _newTemplate('

{title}

', 'titleTmpl'); - var parentSource = '

{name}

{@include name="titleTmpl"/}', - context = {name: 'Larry', title: 'Developer'}, - parentTemplate = _newTemplate(parentSource); - - parentTemplate.render(context) - .success(function(html) { - expect(html).toEqual('

Larry

Developer

', 'titleTmpl'); - }) - .error(noop.error); - - expect(noop.error).not.toHaveBeenCalled(); + var done = false; + runs(function() { + _newTemplate('

{title}

', 'titleTmpl'); + var parentSource = '

{name}

{@include name="titleTmpl"/}', + context = {name: 'Larry', title: 'Developer'}, + parentTemplate = _newTemplate(parentSource); + + parentTemplate.render(context) + .then(function(html) { + expect(html).toEqual('

Larry

Developer

', 'titleTmpl'); + }, noop.error) + .then(function() { + expect(noop.error).not.toHaveBeenCalled(); + done = true; + }); + }); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); it('can reference a variable from a config file', function() { - var source = '

{@config key="test_key" environment="test-environment" /}

', - context = {}, - template = _newTemplate(source); - - $('body').append(''); - Config.init(); - - template.render(context) - .success(function(html) { - expect(html).toEqual('

test value

'); - }) - .error(noop.error); - - expect(noop.error).not.toHaveBeenCalled(); - Config.dispose(); - $('#temp-config-script').remove(); + var done = false; + runs(function() { + var source = '

{@config key="test_key" environment="test-environment" /}

', + context = {}, + template = _newTemplate(source); + + $('body').append(''); + Config.init(); + + template.render(context) + .then(function(html) { + expect(html).toEqual('

test value

'); + }, noop.error) + .then(function() { + expect(noop.error).not.toHaveBeenCalled(); + Config.dispose(); + $('#temp-config-script').remove(); + done = true; + }); + }); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); it('can selectively render content based on current config environment', function() { - var source = '

{@config only="test-local"}Yes{:else}No{/config}

' + - '

{@config only="test-staging"}Yes{:else}No{/config}

' + - '

{@config only="test-production"}Yes{:else}No{/config}

' + - '

{@config not="test-local"}Yes{:else}No{/config}

' + - '

{@config not="test-staging"}Yes{:else}No{/config}

' + - '

{@config not="test-production"}Yes{:else}No{/config}

', - context = {}, - template = _newTemplate(source); - - $('body').append(''); - $('body').append(''); - $('body').append(''); - Config.init(); - - // Test first environment - Config.setDefault('test-local'); - template.render(context) - .success(function(html) { - expect(html).toEqual('

Yes

No

No

No

Yes

Yes

'); - }) - .error(noop.error); - - // Test second environment - Config.setDefault('test-production'); - template.render(context) - .success(function(html) { - expect(html).toEqual('

No

No

Yes

Yes

Yes

No

'); - }) - .error(noop.error); - - expect(noop.error).not.toHaveBeenCalled(); - Config.dispose(); - $('script.test-configs').remove(); + var done = false; + runs(function() { + var source = '

{@config only="test-local"}Yes{:else}No{/config}

' + + '

{@config only="test-staging"}Yes{:else}No{/config}

' + + '

{@config only="test-production"}Yes{:else}No{/config}

' + + '

{@config not="test-local"}Yes{:else}No{/config}

' + + '

{@config not="test-staging"}Yes{:else}No{/config}

' + + '

{@config not="test-production"}Yes{:else}No{/config}

', + context = {}, + template = _newTemplate(source); + + $('body').append(''); + $('body').append(''); + $('body').append(''); + Config.init(); + + // Test first environment + Config.setDefault('test-local'); + Promise.resolve() + .then(function() { + return template.render(context); + }) + .then(function(html) { + expect(html).toEqual('

Yes

No

No

No

Yes

Yes

'); + }, noop.error) + .then(function() { + // Test second environment + Config.setDefault('test-production'); + }) + .then(function() { + return template.render(context); + }) + .then(function(html) { + expect(html).toEqual('

No

No

Yes

Yes

Yes

No

'); + }, noop.error) + .then(function() { + expect(noop.error).not.toHaveBeenCalled(); + Config.dispose(); + $('script.test-configs').remove(); + done = true; + }); + }); + waitsFor(function() { + return !!done; + }, 'promises to resolve', 100); }); }); diff --git a/test/unit/util/Promise.js b/test/unit/util/Promise.js deleted file mode 100755 index f85f2327..00000000 --- a/test/unit/util/Promise.js +++ /dev/null @@ -1,109 +0,0 @@ -define(function(require) { - - var Promise = require('lavaca/util/Promise'); - - var promise, - noop; - - describe('A Promise', function() { - beforeEach(function() { - promise = new Promise(); - noop = { - success: function() { }, - error: function() { }, - always: function() { } - }; - spyOn(noop, 'success'); - spyOn(noop, 'error'); - spyOn(noop, 'always'); - }); - it('can be resolved calling success(), always() and not error()', function() { - promise - .success(noop.success) - .error(noop.error) - .always(noop.always) - .resolve(); - expect(noop.success).toHaveBeenCalled(); - expect(noop.error).not.toHaveBeenCalled(); - expect(noop.always).toHaveBeenCalled(); - }); - it('can be rejected calling error(), always() and not success()', function() { - promise - .success(noop.success) - .error(noop.error) - .always(noop.always) - .reject(); - expect(noop.success).not.toHaveBeenCalled(); - expect(noop.error).toHaveBeenCalled(); - expect(noop.always).toHaveBeenCalled(); - }); - it('can queue and trigger multiple succeess callbacks', function() { - promise - .success(noop.success) - .success(noop.success) - .error(noop.error) - .always(noop.always) - .then(noop.success, noop.error) - .resolve(); - expect(noop.success.callCount).toBe(3); - expect(noop.error.callCount).toBe(0); - expect(noop.always.callCount).toBe(1); - }); - it('can queue and trigger multiple error callbacks', function() { - promise - .success(noop.success) - .error(noop.error) - .error(noop.error) - .always(noop.always) - .then(noop.success, noop.error) - .reject(); - expect(noop.success.callCount).toBe(0); - expect(noop.error.callCount).toBe(3); - expect(noop.always.callCount).toBe(1); - }); - it('can queue and trigger callbacks "when" all promises are resolved', function() { - var promise2 = new Promise(); - Promise.when(promise, promise2) - .success(noop.success) - .error(noop.error) - .always(noop.always); - promise.resolve(); - promise2.resolve(); - expect(noop.success).toHaveBeenCalled(); - expect(noop.error).not.toHaveBeenCalled(); - expect(noop.always).toHaveBeenCalled(); - }); - it('can queue and trigger callbacks "when" a single promise is rejected', function() { - var promise2 = new Promise(); - Promise.when(promise, promise2) - .success(noop.success) - .error(noop.error) - .always(noop.always); - promise.reject(); - expect(noop.success).not.toHaveBeenCalled(); - expect(noop.error).toHaveBeenCalled(); - expect(noop.always).toHaveBeenCalled(); - }); - it('can be resolved with a resolver', function() { - promise - .success(noop.success) - .error(noop.error) - .always(noop.always); - (promise.resolver())(); - expect(noop.success).toHaveBeenCalled(); - expect(noop.error).not.toHaveBeenCalled(); - expect(noop.always).toHaveBeenCalled(); - }); - it('can be rejected with a rejector', function() { - promise - .success(noop.success) - .error(noop.error) - .always(noop.always); - (promise.rejector())(); - expect(noop.success).not.toHaveBeenCalled(); - expect(noop.error).toHaveBeenCalled(); - expect(noop.always).toHaveBeenCalled(); - }); - }); - -});