From af7b006a485d9b2f8c3f2b01a5462567f950c556 Mon Sep 17 00:00:00 2001 From: Aleksei Androsov Date: Wed, 17 Sep 2014 17:59:59 +0400 Subject: [PATCH] =?UTF-8?q?[ns.Update]=20=D0=9F=D0=B5=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=80=D0=BE=D1=81=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B5=D0=B9=20=D0=B2=20=D1=81=D0=BB=D1=83=D1=87=D0=B0=D0=B5=20?= =?UTF-8?q?=D0=B8=D1=85=20=D0=BD=D0=B5=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B8=20#464?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ns.update.js | 68 ++++++++++++++++++++++++-- test/spec/ns.update.edge-cases.js | 81 +++++++++++++++++++++++++++++++ test/tests.yate | 1 + yate/noscript.yate | 6 ++- 4 files changed, 152 insertions(+), 4 deletions(-) diff --git a/src/ns.update.js b/src/ns.update.js index 45aa376c..62c14c03 100644 --- a/src/ns.update.js +++ b/src/ns.update.js @@ -42,6 +42,13 @@ this.promise = new Vow.Promise(); + /** + * Количество перезапросов из-за невалидных моделей. + * @type {number} + * @private + */ + this._restartCount = 1; + options = options || {}; /** @@ -85,6 +92,13 @@ */ ns.Update.prototype._EVENTS_ORDER = ['ns-view-hide', 'ns-view-htmldestroy', 'ns-view-htmlinit', 'ns-view-async', 'ns-view-show', 'ns-view-touch']; + /** + * Лимит на перезапрос моделей. + * @type {number} + * @constant + */ + ns.Update.prototype.RESTART_LIMIT = 2; + /** * Регистрирует указанное событие, добавляя к нему признаки ns.update * @private @@ -147,7 +161,7 @@ }; if (ns.Update.handleError(error, this)) { - return [].concat(err.invalid, err.valid); + return Vow.resolve([].concat(err.invalid, err.valid)); } else { return Vow.reject(error); @@ -201,6 +215,12 @@ this.log('collected incomplete views', views); var models = views2models(views.sync); + /** + * Массив моделей, которые должны быть валидны перед отрисовкой. + * @private + */ + this._models = models; + this.log('collected needed models', models); this.switchTimer('collectModels', 'requestSyncModels'); @@ -328,11 +348,53 @@ * потому что у видов будет уже другое состояние, если что-то поменяется между generateHTML и insertNodes * @private */ - ns.Update.prototype._updateDOM = function() { + ns.Update.prototype._startUpdateDOM = function() { if (this._expired()) { return this._rejectWithStatus(this.STATUS.EXPIRED); } + // Проверяем валидность моделей перед стартом. + // Из-за асинхронности модели могут оказаться невалидными. + var models = { + valid: [], + invalid: [] + }; + var modelHasErrors = false; + for (var i = 0, j = this._models.length; i < j; i++) { + var model = this._models[i]; + if (model.isValid()) { + models.valid.push(model); + + } else { + models.invalid.push(model); + if (model.status === model.STATUS.ERROR) { + modelHasErrors = true; + break; + } + } + } + + // если все хорошо, то переходим к обновлению DOM + if (models.invalid.length === 0) { + return this._updateDOM(); + } + + // если в моделях есть ошибки или мы превысили лимит перезапусков, то фейлимся + if (modelHasErrors || this._restartCount >= this.RESTART_LIMIT) { + // имитируем ошибку "Невалидные модели" + return this._onRequestModelsError(models) + // если ns.Update.handleError решит, что все ок, то запускаем обновление DOM + .then(this._updateDOM, null, this); + + } else { + // если модели невалидные, но в них нет ошибок, то пробуем перезапуститься + this._restartCount++; + return this._requestModels(this._models) + .then(this._startUpdateDOM, null, this); + } + }; + + ns.Update.prototype._updateDOM = function() { var html = this._renderUpdateTree(); var node = ns.html2node(html || ''); return this._insertNodes(node); @@ -409,7 +471,7 @@ // начинаем цепочку с промиса, чтобы ловить ошибки в том числе и из _requestAllModels Vow.invoke(this._requestAllModels.bind(this)) .then(saveAsyncPromises, null, this) - .then(this._updateDOM, null, this) + .then(this._startUpdateDOM, null, this) .then(fulfillWithAsyncPromises, this._reject, this); return this.promise; diff --git a/test/spec/ns.update.edge-cases.js b/test/spec/ns.update.edge-cases.js index 6d360560..72445dc1 100644 --- a/test/spec/ns.update.edge-cases.js +++ b/test/spec/ns.update.edge-cases.js @@ -110,4 +110,85 @@ describe('ns.Update. Синтетические случаи', function() { expect(ns.request.models).to.have.callCount(0); }); }); + + describe('Инвалидация моделей в момент запроса приводит к перезапросу моделей', function() { + + beforeEach(function() { + this.sinon.server.autoRespond = true; + this.sinon.server.respond(function(xhr) { + xhr.respond( + 200, + {"Content-Type": "application/json"}, + JSON.stringify({ + models: [ + { data: true } + ] + }) + ); + }); + + + ns.layout.define('page', { + 'view1': { + 'view2': {} + } + }); + + ns.Model.define('model1'); + ns.Model.define('model2'); + + ns.View.define('view1', { models: ['model1'] }); + ns.View.define('view2', { models: ['model2'] }); + + ns.Model.get('model1').setData({}); + + // имитируем, что приходим в _updateDOM с невалидными моделями + var that = this; + this.restartLimit = 1; + var restartCount = 0; + ns.Update.prototype.__stubStartUpdateDOM = ns.Update.prototype._startUpdateDOM; + this.sinon.stub(ns.Update.prototype, '_startUpdateDOM', function() { + if (restartCount < that.restartLimit) { + ns.Model.get('model1').invalidate({}); + restartCount++; + } + + return this.__stubStartUpdateDOM(); + }); + + this.view = ns.View.create('view1'); + this.layout = ns.layout.page('page'); + }); + + afterEach(function() { + delete ns.Update.prototype.__stubStartUpdateDOM; + }); + + it('должен перезапросить и отрисовать виды', function() { + return new ns.Update(this.view, this.layout).render(); + }); + + it('должен завершить update с ошибкой при превышении лимита', function() { + this.restartLimit = 2; + return new ns.Update(this.view, this.layout) + .render() + .then(function() { + throw new Error('fulfilled'); + }, function(err) { + expect(err.error).to.be.equal(ns.U.STATUS.MODELS); + }); + }); + + it('должен отрисовать виды с ошибкой при превышении лимита, если разрешил handleError', function() { + this.restartLimit = 2; + this.sinon.stub(ns.Update, 'handleError').returns(true); + + return new ns.Update(this.view, this.layout) + .render() + .then(function() { + expect(this.view.node.outerHTML).to.contain('test ns-view-error-content'); + }, null, this); + }); + + }); }); diff --git a/test/tests.yate b/test/tests.yate index 0d5e0f8f..9f59d692 100644 --- a/test/tests.yate +++ b/test/tests.yate @@ -20,6 +20,7 @@ match .* ns-view-async-content { match .* ns-view-error-content { "test ns-view-error-content" + apply . ns-view-content } // Это шаблон для проверки того, что yate сбилжен для тестов. diff --git a/yate/noscript.yate b/yate/noscript.yate index c8b8f812..102815cf 100644 --- a/yate/noscript.yate +++ b/yate/noscript.yate @@ -114,4 +114,8 @@ match .* ns-view-async-content {} // Часть обязательных моделей не удалось получить или для них вернулась ошибка // @public -match .* ns-view-error-content {} +match .* ns-view-error-content { + // Надо все равно отрисовать дерево, + // чтобы не нарваться на ошибку "Can't find node for " + apply . ns-view-content +}