diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js index 4690f15d801..6c4fb9c912a 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_action.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js @@ -105,7 +105,7 @@ var StatementAction = AbstractAction.extend({ var self = this; _.each(this.widgets, function(widget) { widget.destroy(); - }) + }); this.widgets = []; this.model.reload() .then(function() { @@ -122,23 +122,26 @@ var StatementAction = AbstractAction.extend({ */ start: function () { var self = this; + var args = arguments; + var sup = this._super; - this.renderer.prependTo(self.$('.o_form_sheet')); - this._renderLines(); + return this.renderer.prependTo(self.$('.o_form_sheet')).then(function() { + return self._renderLines().then(function() { + // No more lines to reconcile, trigger the rainbowman. + var initialState = self.renderer._initialState; + if(initialState.valuenow === initialState.valuemax){ + initialState.context = self.model.getContext(); + self.renderer.showRainbowMan(initialState); + }else{ + // Create a notification if some lines has been reconciled automatically. + if(initialState.valuenow > 0) + self.renderer._renderNotifications(self.model.statement.notifications); + self._openFirstLine(); + } - // No more lines to reconcile, trigger the rainbowman. - var initialState = this.renderer._initialState; - if(initialState.valuenow === initialState.valuemax){ - initialState.context = this.model.getContext(); - this.renderer.showRainbowMan(initialState); - }else{ - // Create a notification if some lines has been reconciled automatically. - if(initialState.valuenow > 0) - this.renderer._renderNotifications(this.model.statement.notifications); - this._openFirstLine(); - } - - return this._super.apply(this, arguments); + return sup.apply(self, args); + }); + }); }, /** @@ -176,7 +179,7 @@ var StatementAction = AbstractAction.extend({ _loadMore: function(qty) { var self = this; return this.model.loadMore(qty).then(function () { - self._renderLines(); + return self._renderLines(); }); }, /** @@ -192,7 +195,9 @@ var StatementAction = AbstractAction.extend({ }))[0]; if (handle) { var line = this.model.getLine(handle); - this.model.changeMode(handle, 'match').always(function () { + this.model.changeMode(handle, 'match').then(function () { + self._getWidget(handle).update(line); + }).guardedCatch(function(){ self._getWidget(handle).update(line); }); } @@ -206,11 +211,12 @@ var StatementAction = AbstractAction.extend({ _renderLines: function () { var self = this; var linesToDisplay = this.model.getStatementLines(); + var linePromises = []; _.each(linesToDisplay, function (line, handle) { var widget = new self.config.LineRenderer(self, self.model, line); widget.handle = handle; self.widgets.push(widget); - widget.appendTo(self.$('.o_reconciliation_lines')); + linePromises.push(widget.appendTo(self.$('.o_reconciliation_lines'))); }); if (this.model.hasMoreLines() === false) { this.renderer.hideLoadMoreButton(true); @@ -218,6 +224,7 @@ var StatementAction = AbstractAction.extend({ else { this.renderer.hideLoadMoreButton(false); } + return Promise.all(linePromises); }, //-------------------------------------------------------------------------- @@ -237,7 +244,7 @@ var StatementAction = AbstractAction.extend({ var handle = event.target.handle; var line = this.model.getLine(handle); var mode = line.mode; - this.model[_.str.camelize(event.name)](handle, event.data.data).always(function () { + this.model[_.str.camelize(event.name)](handle, event.data.data).then(function () { self._getWidget(handle).update(line); if (mode === 'inactive' && line.mode !== 'inactive') { _.each(self.model.lines, function (line, _handle) { @@ -320,7 +327,7 @@ var StatementAction = AbstractAction.extend({ }, /** * call 'validate' model method then destroy the - * validated lines and update the action renderer with the new status bar + * validated lines and update the action renderer with the new status bar * values and notifications then open the first available line * * @private @@ -329,8 +336,7 @@ var StatementAction = AbstractAction.extend({ _onValidate: function (event) { var self = this; var handle = event.target.handle; - var method = 'validate'; - this.model[method](handle).then(function (result) { + this.model.validate(handle).then(function (result) { self.renderer.update({ 'valuenow': self.model.valuenow, 'valuemax': self.model.valuemax, @@ -345,7 +351,7 @@ var StatementAction = AbstractAction.extend({ self.widgets.splice(index, 1); }); // Get number of widget and if less than constant and if there are more to laod, load until constant - if (self.widgets.length < self.model.defaultDisplayQty + if (self.widgets.length < self.model.defaultDisplayQty && self.model.valuemax - self.model.valuenow >= self.model.defaultDisplayQty) { var toLoad = self.model.defaultDisplayQty - self.widgets.length; self._loadMore(toLoad); diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 6496b2c9036..11dbd804420 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -125,7 +125,7 @@ var StatementModel = BasicModel.extend({ * * @param {string} handle * @param {number} mv_line_id - * @returns {Deferred} + * @returns {Promise} */ addProposition: function (handle, mv_line_id) { var self = this; @@ -138,19 +138,19 @@ var StatementModel = BasicModel.extend({ if(!line.st_line.partner_id && line.reconciliation_proposition && line.reconciliation_proposition.length == 1 && prop.partner_id && line.type === undefined){ return this.changePartner(handle, {'id': prop.partner_id, 'display_name': prop.partner_name}, true) - .then(function (result) { - return $.when(self._computeLine(line), self._performMoveLine(handle)); + .then(function () { + return Promise.all([self._computeLine(line), self._performMoveLine(handle)]); }); } - return $.when(this._computeLine(line), this._performMoveLine(handle)); + return Promise.all([this._computeLine(line), this._performMoveLine(handle)]); }, /** * change the filter for the target line and fetch the new matched lines * * @param {string} handle * @param {string} filter - * @returns {Deferred} + * @returns {Promise} */ changeFilter: function (handle, filter) { var line = this.getLine(handle); @@ -171,7 +171,7 @@ var StatementModel = BasicModel.extend({ * * @param {string} handle * @param {'inactive' | 'match' | 'create'} mode - * @returns {Deferred} + * @returns {Promise} */ changeMode: function (handle, mode) { var line = this.getLine(handle); @@ -189,13 +189,13 @@ var StatementModel = BasicModel.extend({ if (line.mode === 'create') { return this.createProposition(handle); } - return $.when(); + return Promise.resolve(); }, /** * call 'write' method on the 'account.bank.statement' * * @param {string} name - * @returns {Deferred} + * @returns {Promise} */ changeName: function (name) { return this._rpc({ @@ -209,7 +209,7 @@ var StatementModel = BasicModel.extend({ * * @param {string} handle * @param {number} offset - * @returns {Deferred} + * @returns {Promise} */ changeOffset: function (handle, offset) { this.getLine(handle).offset += (offset > 0 ? 1 : -1) * this.limitMoveLines; @@ -223,14 +223,14 @@ var StatementModel = BasicModel.extend({ * @param {Object} partner * @param {string} partner.display_name * @param {number} partner.id - * @returns {Deferred} + * @returns {Promise} */ changePartner: function (handle, partner, preserveMode) { var self = this; var line = this.getLine(handle); line.st_line.partner_id = partner && partner.id; line.st_line.partner_name = partner && partner.display_name || ''; - return $.when(partner && this._changePartner(handle, partner.id)) + return Promise.resolve(partner && this._changePartner(handle, partner.id)) .then(function() { if(line.st_line.partner_id){ _.each(line.reconciliation_proposition, function(prop){ @@ -238,7 +238,7 @@ var StatementModel = BasicModel.extend({ line.reconciliation_proposition = []; return false; } - }) + }); } return self._computeLine(line); }) @@ -249,12 +249,12 @@ var StatementModel = BasicModel.extend({ }) .then(function () { return line.mode === 'create' ? self.createProposition(handle) : false; - }) + }); }, /** * close the statement - * @returns {Deferred} resolves to the res_id of the closed statements + * @returns {Promise} resolves to the res_id of the closed statements */ closeStatement: function () { var self = this; @@ -272,14 +272,14 @@ var StatementModel = BasicModel.extend({ * then open the first available line * * @param {string} handle - * @returns {Deferred} + * @returns {Promise} */ createProposition: function (handle) { var line = this.getLine(handle); var prop = _.filter(line.reconciliation_proposition, '__focus'); var last = prop[prop.length-1]; if (last && !this._isValid(last)) { - return $.Deferred().resolve(false); + return Promise.resolve(false); } prop = this._formatQuickCreate(line); @@ -301,7 +301,7 @@ var StatementModel = BasicModel.extend({ */ getStatementLines: function () { var self = this; - var linesToDisplay = _.pick(this.lines, function(value, key, object) { + var linesToDisplay = _.pick(this.lines, function(value, key, object) { if (value.visible === true && self.alreadyDisplayed.indexOf(key) === -1) { self.alreadyDisplayed.push(key); return object; @@ -315,7 +315,6 @@ var StatementModel = BasicModel.extend({ * @returns {boolean} true if load more button needs to be displayed */ hasMoreLines: function () { - var self = this; var notDisplayed = _.filter(this.lines, function(line) { return !line.visible; }); if (notDisplayed.length > 0) { return true; @@ -341,23 +340,23 @@ var StatementModel = BasicModel.extend({ * * @param {Object} context * @param {number[]} context.statement_line_ids - * @returns {Deferred} + * @returns {Promise} */ load: function (context) { var self = this; this.statement_line_ids = context.statement_line_ids; if (!this.statement_line_ids) { - return $.when(); + return Promise.resolve(); } this.context = context; return self.reload(); - + }, /** * Load more bank statement line * * @param {integer} qty quantity to load - * @returns {Deferred} + * @returns {Promise} */ loadMore: function(qty) { if (qty === undefined) { @@ -370,10 +369,10 @@ var StatementModel = BasicModel.extend({ }, /** * RPC method to load informations on lines - * + * * @param {Array} ids ids of bank statement line passed to rpc call * @param {Array} excluded_ids list of move_line ids that needs to be excluded from search - * @returns {Deferred} + * @returns {Promise} */ loadData: function(ids, excluded_ids) { var self = this; @@ -393,7 +392,8 @@ var StatementModel = BasicModel.extend({ self.alreadyDisplayed = []; self.lines = {}; self.pagerIndex = 0; - self.search_str = $('.reconciliation_search_input').val() + // FIXME: model should not be tied to the DOM ! + self.search_str = $('.reconciliation_search_input').val(); var def_statement = this._rpc({ model: 'account.reconciliation.widget', method: 'get_bank_statement_data', @@ -446,7 +446,7 @@ var StatementModel = BasicModel.extend({ self.accounts = _.object(_.pluck(accounts, 'id'), _.pluck(accounts, 'code')); }); var def_taxes = self._loadTaxes(); - return $.when(def_statement, def_reconcileModel, def_account, def_taxes).then(function () { + return Promise.all([def_statement, def_reconcileModel, def_account, def_taxes]).then(function () { _.each(self.lines, function (line) { line.reconcileModels = self.reconcileModels; }); @@ -469,21 +469,21 @@ var StatementModel = BasicModel.extend({ self.taxes[tax.id] = { price_include: tax.price_include, amount_type: tax.amount_type, - } - }) + }; + }); }); }, /** * Add lines into the propositions from the reconcile model * Can add 2 lines, and each with its taxes. The second line become editable * in the create mode. - * + * * @see 'updateProposition' method for more informations about the * 'amount_type' * * @param {string} handle * @param {integer} reconcileModelId - * @returns {Deferred} + * @returns {Promise} */ quickCreateProposition: function (handle, reconcileModelId) { var line = this.getLine(handle); @@ -493,12 +493,12 @@ var StatementModel = BasicModel.extend({ var focus = this._formatQuickCreate(line, _.pick(reconcileModel, fields)); focus.reconcileModelId = reconcileModelId; - if (!line.reconciliation_proposition.every(function(prop) {return prop.to_check == focus.to_check})) { + if (!line.reconciliation_proposition.every(function(prop) {return prop.to_check == focus.to_check;})) { new CrashManager().show_warning({data: { exception_type: _t("Incorrect Operation"), message: _t("You cannot mix items with and without the 'To Check' checkbox ticked.") }}); - return $.when(); + return Promise.resolve(); } line.reconciliation_proposition.push(focus); @@ -520,7 +520,7 @@ var StatementModel = BasicModel.extend({ * * @param {string} handle * @param {number} id (move line id) - * @returns {Deferred} + * @returns {Promise} */ removeProposition: function (handle, id) { var self = this; @@ -541,15 +541,15 @@ var StatementModel = BasicModel.extend({ line.mode = (id || line.mode !== "create") && isNaN(id) && !this.avoidCreate ? 'create' : 'match'; defs.push(this._computeLine(line)); if (line.mode === 'create') { - return $.when.apply($, defs).then(function () { + return Promise.all(defs).then(function () { return self.createProposition(handle); }); } else if (line.mode === 'match') { - return $.when.apply($, defs).then(function () { + return Promise.all(defs).then(function () { return self._performMoveLine(handle); }); } - return $.when.apply($, defs); + return Promise.all(defs); }, getPartialReconcileAmount: function(handle, data) { var line = this.getLine(handle); @@ -562,7 +562,7 @@ var StatementModel = BasicModel.extend({ return Math.abs(amount); } if (partial <= Math.abs(prop.amount) && partial >= 0) { - return partial + return partial; } return Math.abs(amount); } @@ -571,7 +571,7 @@ var StatementModel = BasicModel.extend({ * Force the partial reconciliation to display the reconciliate button. * * @param {string} handle - * @returns {Deferred} + * @returns {Promise} */ partialReconcile: function(handle, data) { var line = this.getLine(handle); @@ -607,14 +607,14 @@ var StatementModel = BasicModel.extend({ * Change the value of the editable proposition line or create a new one. * * If the editable line comes from a reconcile model with 2 lines - * and their 'amount_type' is "percent" + * and their 'amount_type' is "percent" * and their total equals 100% (this doesn't take into account the taxes * who can be included or not) * Then the total is recomputed to have 100%. * * @param {string} handle * @param {*} values - * @returns {Deferred} + * @returns {Promise} */ updateProposition: function (handle, values) { var self = this; @@ -624,13 +624,14 @@ var StatementModel = BasicModel.extend({ prop = this._formatQuickCreate(line); line.reconciliation_proposition.push(prop); } - if (!line.reconciliation_proposition.slice(0,-1).every(function(prop) {return prop.to_check == values.to_check})) { + if (!line.reconciliation_proposition.slice(0,-1).every(function(prop) {return prop.to_check == values.to_check;})) { new CrashManager().show_warning({data: { exception_type: _t("Incorrect Operation"), message: _t("You cannot mix items with and without the 'To Check' checkbox ticked.") }}); + // FIXME: model should not be tied to the DOM ! $('.create_to_check input').click(); - return $.when(); + return Promise.resolve(); } _.each(values, function (value, fieldName) { if (fieldName === 'analytic_tag_ids') { @@ -682,7 +683,7 @@ var StatementModel = BasicModel.extend({ * Update the number of validated lines * * @param {(string|string[])} handle - * @returns {Deferred} resolved with an object who contains + * @returns {Promise} resolved with an object who contains * 'handles' key */ validate: function (handle) { @@ -699,9 +700,11 @@ var StatementModel = BasicModel.extend({ } var ids = []; var values = []; + var handlesPromises = []; _.each(handles, function (handle) { var line = self.getLine(handle); var props = _.filter(line.reconciliation_proposition, function (prop) {return !prop.invalid;}); + var computeLinePromise; if (props.length === 0) { // Usability: if user has not choosen any lines and click validate, it has the same behavior // as creating a write-off of the same amount. @@ -710,61 +713,65 @@ var StatementModel = BasicModel.extend({ })); // update balance of line otherwise it won't be to zero and another line will be added line.reconciliation_proposition.push(props[0]); - self._computeLine(line); + computeLinePromise = self._computeLine(line); } ids.push(line.id); - var values_dict = { - "partner_id": line.st_line.partner_id, - "counterpart_aml_dicts": _.map(_.filter(props, function (prop) { - return !isNaN(prop.id) && !prop.already_paid; - }), self._formatToProcessReconciliation.bind(self, line)), - "payment_aml_ids": _.pluck(_.filter(props, function (prop) { - return !isNaN(prop.id) && prop.already_paid; - }), 'id'), - "new_aml_dicts": _.map(_.filter(props, function (prop) { - return isNaN(prop.id) && prop.display; - }), self._formatToProcessReconciliation.bind(self, line)), - }; - line.reconciliation_proposition.some(function(prop) { - if (prop.to_check) { - values_dict['to_check'] = true; - return true; - } - }) - if (line.reconciliation_proposition[0].to_check) { - values_dict['to_check'] = true; - } - - // If the lines are not fully balanced, create an unreconciled amount. - // line.st_line.currency_id is never false here because its equivalent to - // statement_line.currency_id or statement_line.journal_id.currency_id or statement_line.journal_id.company_id.currency_id (Python-side). - // see: get_statement_line_for_reconciliation_widget method in account/models/account_bank_statement.py for more details - var currency = session.get_currency(line.st_line.currency_id); - var balance = line.balance.amount; - if (!utils.float_is_zero(balance, currency.digits[1])) { - var unreconciled_amount_dict = { - 'account_id': line.st_line.open_balance_account_id, - 'credit': balance > 0 ? balance : 0, - 'debit': balance < 0 ? -balance : 0, - 'name': line.st_line.name + ' : ' + _t("Open balance"), + handlesPromises.push(Promise.resolve(computeLinePromise).then(function() { + var values_dict = { + "partner_id": line.st_line.partner_id, + "counterpart_aml_dicts": _.map(_.filter(props, function (prop) { + return !isNaN(prop.id) && !prop.already_paid; + }), self._formatToProcessReconciliation.bind(self, line)), + "payment_aml_ids": _.pluck(_.filter(props, function (prop) { + return !isNaN(prop.id) && prop.already_paid; + }), 'id'), + "new_aml_dicts": _.map(_.filter(props, function (prop) { + return isNaN(prop.id) && prop.display; + }), self._formatToProcessReconciliation.bind(self, line)), }; - values_dict['new_aml_dicts'].push(unreconciled_amount_dict); - } - values.push(values_dict); - line.reconciled = true; - self.valuenow++; + line.reconciliation_proposition.some(function(prop) { + if (prop.to_check) { + values_dict['to_check'] = true; + return true; + } + }); + if (line.reconciliation_proposition[0].to_check) { + values_dict['to_check'] = true; + } + + // If the lines are not fully balanced, create an unreconciled amount. + // line.st_line.currency_id is never false here because its equivalent to + // statement_line.currency_id or statement_line.journal_id.currency_id or statement_line.journal_id.company_id.currency_id (Python-side). + // see: get_statement_line_for_reconciliation_widget method in account/models/account_bank_statement.py for more details + var currency = session.get_currency(line.st_line.currency_id); + var balance = line.balance.amount; + if (!utils.float_is_zero(balance, currency.digits[1])) { + var unreconciled_amount_dict = { + 'account_id': line.st_line.open_balance_account_id, + 'credit': balance > 0 ? balance : 0, + 'debit': balance < 0 ? -balance : 0, + 'name': line.st_line.name + ' : ' + _t("Open balance"), + }; + values_dict['new_aml_dicts'].push(unreconciled_amount_dict); + } + values.push(values_dict); + line.reconciled = true; + self.valuenow++; + })); }); - return this._rpc({ - model: 'account.reconciliation.widget', - method: 'process_bank_statement_line', - args: [ids, values], - context: self.context, - }) - .then(this._validatePostProcess.bind(this)) - .then(function () { - return {handles: handles}; - }); + return Promise.all(handlesPromises).then(function() { + return self._rpc({ + model: 'account.reconciliation.widget', + method: 'process_bank_statement_line', + args: [ids, values], + context: self.context, + }) + .then(self._validatePostProcess.bind(self)) + .then(function () { + return {handles: handles}; + }); + }); }, //-------------------------------------------------------------------------- @@ -779,10 +786,6 @@ var StatementModel = BasicModel.extend({ * @param {Object} prop */ _addProposition: function (line, prop) { - function checkAccountType (r) { - return !isNaN(r.id) && r.account_type !== prop.account_type; - } - line.reconciliation_proposition.push(prop); }, /** @@ -793,7 +796,7 @@ var StatementModel = BasicModel.extend({ * * @private * @param {string} handle - * @returns {Deferred} + * @returns {Promise} */ _blurProposition: function (handle) { var line = this.getLine(handle); @@ -811,7 +814,7 @@ var StatementModel = BasicModel.extend({ * @private * @param {string} handle * @param {integer} partner_id - * @returns {Deferred} + * @returns {Promise} */ _changePartner: function (handle, partner_id) { var self = this; @@ -833,7 +836,7 @@ var StatementModel = BasicModel.extend({ * * @private * @param {Object} line - * @returns {Deferred} + * @returns {Promise} */ _computeLine: function (line) { //balance_type @@ -886,7 +889,7 @@ var StatementModel = BasicModel.extend({ '__focus': false }); - prop.tax_exigible = tax.tax_exigibility === 'on_payment' ? true : undefined + prop.tax_exigible = tax.tax_exigibility === 'on_payment' ? true : undefined; prop.amount = tax.base; prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions); prop.invalid = !self._isValid(prop); @@ -904,7 +907,7 @@ var StatementModel = BasicModel.extend({ } }); - return $.when.apply($, tax_defs).then(function () { + return Promise.all(tax_defs).then(function () { _.each(reconciliation_proposition, function (prop) { prop.__tax_to_recompute = false; }); @@ -946,7 +949,7 @@ var StatementModel = BasicModel.extend({ }); }, /** - * + * * * @private * @param {string} handle @@ -1004,7 +1007,7 @@ var StatementModel = BasicModel.extend({ * * @private * @param {Object[]} lines - * @returns {Deferred} + * @returns {Promise} */ _formatLine: function (lines) { var self = this; @@ -1028,7 +1031,7 @@ var StatementModel = BasicModel.extend({ if(!line.st_line.partner_id && line.reconciliation_proposition.length > 0){ var hasDifferentPartners = function(prop){ return !prop.partner_id || prop.partner_id != line.reconciliation_proposition[0].partner_id; - } + }; if(!_.any(line.reconciliation_proposition, hasDifferentPartners)){ return self.changePartner(line.handle, { @@ -1059,7 +1062,7 @@ var StatementModel = BasicModel.extend({ }) ); }); - return $.when.apply($, defs); + return Promise.all(defs); }, /** * Format the server value then compute the line @@ -1069,7 +1072,7 @@ var StatementModel = BasicModel.extend({ * @private * @param {string} handle * @param {Object[]} mv_lines - * @returns {Deferred} + * @returns {Promise} */ _formatMoveLine: function (handle, mv_lines) { var self = this; @@ -1170,7 +1173,7 @@ var StatementModel = BasicModel.extend({ if (parseInt(prop['id'])) { excludedIds.push(prop['id']); } - }) + }); } }); return excludedIds; @@ -1202,7 +1205,7 @@ var StatementModel = BasicModel.extend({ * * @private * @param {string} handle - * @returns {Deferred} + * @returns {Promise} */ _performMoveLine: function (handle) { var line = this.getLine(handle); @@ -1263,7 +1266,7 @@ var StatementModel = BasicModel.extend({ if (prop.analytic_account_id) result.analytic_account_id = prop.analytic_account_id.id; if (prop.tax_id && !prop.is_tax) result.tax_ids = [[4, prop.tax_id.id, null]]; if (prop.tax_id && prop.is_tax) result.tax_line_id = prop.tax_id.id; - if (prop.reconcileModelId) result.reconcile_model_id = prop.reconcileModelId + if (prop.reconcileModelId) result.reconcile_model_id = prop.reconcileModelId; return result; }, /** @@ -1275,7 +1278,7 @@ var StatementModel = BasicModel.extend({ * @returns {Deferred} */ _validatePostProcess: function (data) { - return $.when(); + return Promise.resolve(); }, }); @@ -1312,7 +1315,7 @@ var ManualModel = StatementModel.extend({ * @param {integer[]} [context.company_ids] * @param {integer[]} [context.partner_ids] used for 'customers' and * 'suppliers' mode - * @returns {Deferred} + * @returns {Promise} */ load: function (context) { var self = this; @@ -1335,7 +1338,7 @@ var ManualModel = StatementModel.extend({ }); var domainReconcile = []; - var company_ids = context && context.company_ids || [session.company_id] + var company_ids = context && context.company_ids || [session.company_id]; if (company_ids) { domainReconcile.push(['company_id', 'in', company_ids]); } @@ -1350,7 +1353,7 @@ var ManualModel = StatementModel.extend({ var def_taxes = this._loadTaxes(); - return $.when(def_reconcileModel, def_account, def_taxes).then(function () { + return Promise.all([def_reconcileModel, def_account, def_taxes]).then(function () { switch(context.mode) { case 'customers': case 'suppliers': @@ -1399,7 +1402,7 @@ var ManualModel = StatementModel.extend({ }) .then(function (result) { // Flatten the result - self.manualLines = [].concat(result.accounts, result.customers, result.suppliers) + self.manualLines = [].concat(result.accounts, result.customers, result.suppliers); self.valuenow = 0; self.valuemax = Object.keys(self.manualLines).length; var lines = self.manualLines.splice(0, self.defaultDisplayQty); @@ -1413,7 +1416,7 @@ var ManualModel = StatementModel.extend({ * Load more partners/accounts * * @param {integer} qty quantity to load - * @returns {Deferred} + * @returns {Promise} */ loadMore: function(qty) { if (qty === undefined) { @@ -1427,22 +1430,22 @@ var ManualModel = StatementModel.extend({ * Method to load informations on lines * * @param {Array} lines manualLines to load - * @returns {Deferred} + * @returns {Promise} */ loadData: function(lines) { var self = this; var defs = []; _.each(lines, function (l) { - defs.push(self._formatLine(l.mode, l)) + defs.push(self._formatLine(l.mode, l)); }); - return $.when.apply($, defs); + return Promise.all(defs); }, /** * Mark the account or the partner as reconciled * * @param {(string|string[])} handle - * @returns {Deferred} resolved with the handle array + * @returns {Promise} resolved with the handle array */ validate: function (handle) { var self = this; @@ -1457,7 +1460,7 @@ var ManualModel = StatementModel.extend({ }); } - var def = $.when(); + var def = Promise.resolve(); var process_reconciliations = []; var reconciled = []; _.each(handles, function (handle) { @@ -1520,7 +1523,7 @@ var ManualModel = StatementModel.extend({ } })); }); - return $.when.apply($, defs).then(function() { + return Promise.all(defs).then(function () { if (partner_ids.length) { self._rpc({ model: 'res.partner', @@ -1539,11 +1542,11 @@ var ManualModel = StatementModel.extend({ /** * override change the balance type to display or not the reconcile button - * + * * @override * @private * @param {Object} line - * @returns {Deferred} + * @returns {Promise} */ _computeLine: function (line) { return this._super(line).then(function () { @@ -1568,7 +1571,7 @@ var ManualModel = StatementModel.extend({ * @private * @param {'customers' | 'suppliers' | 'accounts'} type * @param {Object} data - * @returns {Deferred} + * @returns {Promise} */ _formatLine: function (type, data) { var line = this.lines[_.uniqueId('rline')] = _.extend(data, { @@ -1591,7 +1594,7 @@ var ManualModel = StatementModel.extend({ }, /** * override to add journal_id - * + * * @override * @private * @param {Object} line @@ -1619,12 +1622,11 @@ var ManualModel = StatementModel.extend({ * @returns {Object} */ _formatQuickCreate: function (line, values) { - var self = this; // Add journal to created line if (values && values.journal_id === undefined && line && line.createForm && line.createForm.journal_id) { values.journal_id = line.createForm.journal_id; } - return this._super(line, values) + return this._super(line, values); }, /** * @override @@ -1650,7 +1652,7 @@ var ManualModel = StatementModel.extend({ * @override * @private * @param {string} handle - * @returns {Deferred} + * @returns {Promise} */ _performMoveLine: function (handle) { var line = this.getLine(handle); @@ -1675,7 +1677,7 @@ var ManualModel = StatementModel.extend({ }) .then(this._formatMoveLine.bind(this, handle)); }, - + _formatToProcessReconciliation: function (line, prop) { var result = this._super(line, prop); result['date'] = prop.date; diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index 1543615a491..c5e25dc5c1a 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -69,7 +69,7 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, { if (this.model.context && this.model.context.args && this.model.context.args.search) { this.$('.reconciliation_search_input').val(self.model.context.args.search); } - return $.when.apply($, defs); + return Promise.all(defs); }, /** * @override @@ -322,7 +322,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { 'toggle': 'popover' }); var def2 = this._super.apply(this, arguments); - return $.when(def1, def2); + return Promise.all([def1, def2]); }, //-------------------------------------------------------------------------- @@ -419,41 +419,44 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { // create form if (state.createForm) { + var createPromise; if (!this.fields.account_id) { - this._renderCreate(state); + createPromise = this._renderCreate(state); } - var data = this.model.get(this.handleCreateRecord).data; - this.model.notifyChanges(this.handleCreateRecord, state.createForm).then(function () { - // FIXME can't it directly written REPLACE_WITH ids=state.createForm.analytic_tag_ids - self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'REPLACE_WITH', ids: []}}).then(function (){ - var defs = []; - _.each(state.createForm.analytic_tag_ids, function (tag) { - defs.push(self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'ADD_M2M', ids: tag}})); - }); - $.when.apply($, defs).then(function () { - var record = self.model.get(self.handleCreateRecord); - _.each(self.fields, function (field, fieldName) { - if (self._avoidFieldUpdate[fieldName]) return; - if (fieldName === "partner_id") return; - if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) { - field.reset(record); - } - if (fieldName === 'tax_id') { - if (!state.createForm[fieldName] || state.createForm[fieldName].amount_type === "group") { - $('.create_force_tax_included').addClass('d-none'); + Promise.resolve(createPromise).then(function(){ + var data = self.model.get(self.handleCreateRecord).data; + self.model.notifyChanges(self.handleCreateRecord, state.createForm).then(function () { + // FIXME can't it directly written REPLACE_WITH ids=state.createForm.analytic_tag_ids + self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'REPLACE_WITH', ids: []}}).then(function (){ + var defs = []; + _.each(state.createForm.analytic_tag_ids, function (tag) { + defs.push(self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'ADD_M2M', ids: tag}})); + }); + Promise.all(defs).then(function () { + var record = self.model.get(self.handleCreateRecord); + _.each(self.fields, function (field, fieldName) { + if (self._avoidFieldUpdate[fieldName]) return; + if (fieldName === "partner_id") return; + if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) { + field.reset(record); } - else { - $('.create_force_tax_included').removeClass('d-none'); + if (fieldName === 'tax_id') { + if (!state.createForm[fieldName] || state.createForm[fieldName].amount_type === "group") { + $('.create_force_tax_included').addClass('d-none'); + } + else { + $('.create_force_tax_included').removeClass('d-none'); + } } - } + }); }); }); }); + if(state.createForm.tax_id){ + // Set the 'Tax Include' field editable or not depending of the 'price_include' value. + self.$('.create_force_tax_included input').attr('disabled', state.createForm.tax_id.price_include); + } }); - if(state.createForm.tax_id){ - // Set the 'Tax Include' field editable or not depending of the 'price_include' value. - this.$('.create_force_tax_included input').attr('disabled', state.createForm.tax_id.price_include); - } } this.$('.create .add_line').toggle(!!state.balance.amount_currency); }, @@ -511,10 +514,11 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { * * @private * @param {object} state - statement line + * @returns {Promise} */ _renderCreate: function (state) { var self = this; - this.model.makeRecord('account.bank.statement.line', [{ + return this.model.makeRecord('account.bank.statement.line', [{ relation: 'account.account', type: 'many2one', name: 'account_id', @@ -583,7 +587,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { self.fields.amount = new basic_fields.FieldFloat(self, 'amount', record, {mode: 'edit'}); - + self.fields.date = new basic_fields.FieldDate(self, 'date', record, {mode: 'edit'}); @@ -597,13 +601,13 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { self.fields.tax_id.appendTo($create.find('.create_tax_id .o_td_field')); self.fields.analytic_account_id.appendTo($create.find('.create_analytic_account_id .o_td_field')); self.fields.analytic_tag_ids.appendTo($create.find('.create_analytic_tag_ids .o_td_field')); - self.fields.force_tax_included.appendTo($create.find('.create_force_tax_included .o_td_field')) + self.fields.force_tax_included.appendTo($create.find('.create_force_tax_included .o_td_field')); self.fields.label.appendTo($create.find('.create_label .o_td_field')) .then(addRequiredStyle.bind(self, self.fields.label)); self.fields.amount.appendTo($create.find('.create_amount .o_td_field')) .then(addRequiredStyle.bind(self, self.fields.amount)); - self.fields.date.appendTo($create.find('.create_date .o_td_field')) - self.fields.to_check.appendTo($create.find('.create_to_check .o_td_field')) + self.fields.date.appendTo($create.find('.create_date .o_td_field')); + self.fields.to_check.appendTo($create.find('.create_to_check .o_td_field')); self.$('.create').append($create); function addRequiredStyle(widget) { @@ -628,7 +632,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { res_model: 'account.reconcile.model', views: [[false, 'form']], target: 'current' - }, + }, { on_reverse_breadcrumb: function() {self.trigger_up('reload');}, }); @@ -836,11 +840,11 @@ var ManualLineRenderer = LineRenderer.extend({ * @override * @param {string} handle * @param {number} proposition id (move line id) - * @returns {Deferred} + * @returns {Promise} */ removeProposition: function (handle, id) { if (!id) { - return $.when(); + return Promise.resolve(); } return this._super(handle, id); }, @@ -879,7 +883,7 @@ var ManualLineRenderer = LineRenderer.extend({ defs.push(def); } - return $.when.apply($, defs).then(function () { + return Promise.all(defs).then(function () { if (!self.fields.title_account_id) { return self.fields.partner_id.prependTo(self.$('.accounting_view thead td:eq(1) span:first')); } else { @@ -909,10 +913,11 @@ var ManualLineRenderer = LineRenderer.extend({ * @override */ _renderCreate: function (state) { - this._super(state); + var parentPromise = this._super(state); this.$('.create .create_journal_id').show(); - this.$('.create .create_date').removeClass('d-none') + this.$('.create .create_date').removeClass('d-none'); this.$('.create .create_journal_id .o_input').addClass('o_required_modifier'); + return parentPromise; }, }); diff --git a/addons/account/static/tests/account_payment_field_tests.js b/addons/account/static/tests/account_payment_field_tests.js index 1bbeaf6d352..bf846c91e14 100644 --- a/addons/account/static/tests/account_payment_field_tests.js +++ b/addons/account/static/tests/account_payment_field_tests.js @@ -25,10 +25,10 @@ QUnit.module('account', { }, function () { QUnit.module('Reconciliation'); - QUnit.test('Reconciliation form field', function (assert) { + QUnit.test('Reconciliation form field', async function (assert) { assert.expect(5); - var form = createView({ + var form = await createView({ View: FormView, model: 'account.invoice', data: this.data, @@ -40,11 +40,11 @@ QUnit.module('account', { mockRPC: function (route, args) { if (args.method === 'remove_move_reconcile') { assert.deepEqual(args.args, [22,{"invoice_id":1}], "should call remove_move_reconcile {warning: required focus}"); - return $.when(); + return Promise.resolve(); } if (args.method === 'assign_outstanding_credit') { assert.deepEqual(args.args, [4, 20], "should call assign_outstanding_credit {warning: required focus}"); - return $.when(); + return Promise.resolve(); } return this._super.apply(this, arguments); }, diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index fce0c44411d..053d18c1659 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -45,7 +45,7 @@ var db = { {id: 99, display_name: "partner 99", image: 'ZZZ', customer: true}, ], mark_as_reconciled: function () { - return $.when(); + return Promise.resolve(); }, }, 'account.account': { @@ -72,7 +72,7 @@ var db = { {id: 504, code: 504, name: "504 Account", company_id: 1}, ], mark_as_reconciled: function () { - return $.when(); + return Promise.resolve(); }, }, 'account.tax': { @@ -93,7 +93,7 @@ var db = { var tax = _.find(db['account.tax'].records, {'id': args[0][0]}); var amount = args[1]; var tax_base = tax.price_include ? amount*100/(100+tax.amount) : amount; - return $.when({ + return Promise.resolve({ "base": amount, "taxes": [{ 'id': tax.id, @@ -193,7 +193,7 @@ var db = { 'account.reconciliation.widget': { fields: {}, auto_reconcile: function () { - return $.when(Datas.used.auto_reconciliation); + return Promise.resolve(Datas.used.auto_reconciliation); }, process_bank_statement_line: function (args) { var datas = args[1]; @@ -206,7 +206,7 @@ var db = { return ids.indexOf(mv_line.id) === -1; }); } - return $.when(); + return Promise.resolve(); }, get_move_lines_for_bank_statement_line: function (args) { var partner_id = args.splice(1, 1)[0]; @@ -224,7 +224,7 @@ var db = { return line; }) .slice(0, options.params.limitMoveLines); - return $.when(lines); + return Promise.resolve(lines); }, get_bank_statement_line_data: function (args) { var ids = args[0]; @@ -233,12 +233,12 @@ var db = { value_max: ids.length, lines: _.filter(Datas.used.data_widget, function (w) {return _.contains(ids, w.st_line.id);}) }; - return $.when(results); + return Promise.resolve(results); }, get_bank_statement_data: function () { var results = Datas.used.data_preprocess; results.lines = _.filter(Datas.used.data_widget, function (w) {return _.contains(results.st_lines_ids, w.st_line.id);}); - return $.when(results); + return Promise.resolve(results); }, get_move_lines_for_manual_reconciliation: function (args) { var excluded_ids = args.splice(2, 1)[0]; @@ -255,14 +255,14 @@ var db = { return line; }) .slice(0, options.params.limitMoveLines); - return $.when(lines); + return Promise.resolve(lines); }, get_all_data_for_manual_reconciliation: function (args) { var key = JSON.stringify(args); if (!Datas.used.data_for_manual_reconciliation_widget[key]) { throw new Error("Unknown parameters for get_all_data_for_manual_reconciliation: '"+ key + "'"); } - return $.when(Datas.used.data_for_manual_reconciliation_widget[key]); + return Promise.resolve(Datas.used.data_for_manual_reconciliation_widget[key]); }, process_move_lines: function (args) { var datas = args[0]; @@ -274,7 +274,7 @@ var db = { }); } } - return $.when(); + return Promise.resolve(); }, } }; @@ -595,15 +595,15 @@ QUnit.module('account', { }, function () { QUnit.module('Reconciliation'); - QUnit.test('Reconciliation basic rendering', function (assert) { + QUnit.test('Reconciliation basic rendering', async function (assert) { assert.expect(11); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); testUtils.mock.addMockEnvironment(clientAction, { 'data': this.params.data, }); - clientAction.appendTo($('#qunit-fixture')); - + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); var widget = clientAction.widgets[0]; assert.hasClass(widget.$el,'o_reconciliation_line', "should instance of widget reconciliation"); @@ -617,18 +617,18 @@ QUnit.module('account', { assert.containsN(clientAction, '[data-mode="inactive"]', 3, "should be as 'inactive' mode by default"); assert.strictEqual(widget.$el.data('mode'), 'match', "the first one should automatically switch to match mode"); - widget.$('.accounting_view thead td:first').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view thead td:first')); assert.strictEqual(widget.$el.data('mode'), 'inactive', "should switch to 'inactive' mode"); - widget.$('.accounting_view tfoot td:first').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view tfoot td:first')); assert.strictEqual(widget.$el.data('mode'), 'create', "should switch to 'create' mode"); - widget.$('.accounting_view tfoot td:first').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view tfoot td:first')); assert.strictEqual(widget.$el.data('mode'), 'match', "should switch to 'match' mode"); clientAction.destroy(); }); - QUnit.test('Reconciliation fields', function (assert) { + QUnit.test('Reconciliation fields', async function (assert) { assert.expect(6); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); @@ -644,13 +644,14 @@ QUnit.module('account', { } }, }); - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); var widget = clientAction.widgets[0]; assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Agrolait", "the partner many2one should display agrolait"); assert.strictEqual(clientAction.widgets[2].$('.o_input_dropdown input').val(), "Camptocamp", "the partner many2one should display Camptocamp"); - widget.$('.accounting_view tfoot td:first').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view tfoot td:first')); assert.containsN(widget, '.create input.o_input', 8, "create panel should contain 8 fields (account_id, tax_id, journal_id, analytic_account_id, analytic_tag_ids, label, amount, date)"); assert.containsN(widget, '.create .create_account_id .o_required_modifier, .create .create_label .o_required_modifier, .create .create_amount .o_required_modifier', 3, @@ -663,7 +664,7 @@ QUnit.module('account', { clientAction.destroy(); }); - QUnit.test('Reconciliation basic data', function (assert) { + QUnit.test('Reconciliation basic data', async function (assert) { assert.expect(17); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); @@ -680,7 +681,8 @@ QUnit.module('account', { }, }); - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); var widget = clientAction.widgets[0]; assert.containsN(widget, '.mv_line', 2, "should display 2 account move lines"); @@ -689,7 +691,7 @@ QUnit.module('account', { "should display 4 account move lines who contains the account_code, due_date, label and the credit"); assert.strictEqual(widget.$('.mv_line .cell_right:contains(".")').length, 2, "should display only the credit account move lines (hide the debit)"); - clientAction.widgets[1].$('.accounting_view thead td:first').trigger('click'); + await testUtils.dom.click(clientAction.widgets[1].$('.accounting_view thead td:first')); assert.containsN(clientAction.widgets[1], '.mv_line', 5, "should display 5 account move lines"); assert.strictEqual(clientAction.widgets[1].$('.mv_line .cell_right:contains(".")').length, 3, "should display only the credit account move lines (hide the debit)"); assert.containsN(clientAction.widgets[1], '.mv_line.already_reconciled', 3, "should display 3 already reconciled account move lines"); @@ -701,7 +703,7 @@ QUnit.module('account', { // load more assert.ok(clientAction.widgets[1].$('.match div.load-more a:visible').length, "should display the 'load more' button"); assert.equal(clientAction.widgets[1].$('.match div.load-more span').text(), 3, "should display 3 items remaining"); - clientAction.widgets[1].$('.match div.load-more a').trigger('click'); + await testUtils.dom.click(clientAction.widgets[1].$('.match div.load-more a')); assert.containsN(clientAction.widgets[1], '.mv_line', 8, "should load 3 more records"); assert.notOk(clientAction.widgets[1].$('.match div.load-more a:visible').length, "should not display the 'load more' button anymore"); @@ -709,14 +711,14 @@ QUnit.module('account', { assert.ok(clientAction.widgets[1].$('caption .text-danger:visible').length, "should display the 'Select a partner or choose a counterpart' message"); assert.ok(clientAction.widgets[2].$('caption button.btn-primary:visible').length, "should display the 'Reconcile' button"); - clientAction.widgets[3].$('.accounting_view thead td:first').trigger('click'); + await testUtils.dom.click(clientAction.widgets[3].$('.accounting_view thead td:first')); assert.strictEqual(clientAction.widgets[3].$el.data('mode'), 'create', "should switch to 'create' mode instead 'match' mode when 'match' mode is empty"); // open the first line - widget.$('.accounting_view thead td:first').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view thead td:first')); // select propositions - widget.$('.match .cell_account_code:first').trigger('click'); - widget.$('.match .cell_account_code:first').trigger('click'); + await testUtils.dom.click( widget.$('.match .cell_account_code:first')); + await testUtils.dom.click(widget.$('.match .cell_account_code:first')); testUtils.mock.intercept(clientAction, 'call_service', function (event) { assert.deepEqual(event.data.args[1].args, @@ -736,18 +738,18 @@ QUnit.module('account', { }], payment_aml_ids: [], new_aml_dicts: []}]], "Should call process_bank_statement_line with ids"); - var def = $.Deferred(); + var def = testUtils.makeTestPromise(); def.abort = function () {}; event.data.callback(def); }); // click on reconcile button - widget.$('button.o_reconcile:not(:hidden)').trigger('click'); + await testUtils.dom.click( widget.$('button.o_reconcile:not(:hidden)')); clientAction.destroy(); }); - QUnit.test('Reconciliation validate without proposition', function (assert) { + QUnit.test('Reconciliation validate without proposition', async function (assert) { assert.expect(1); // Test added to prevent this issue happening again: https://github.com/odoo/odoo/commit/3549688b21eb65e16b9c3f2b6462eb8d8b52cd47 var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); @@ -763,8 +765,8 @@ QUnit.module('account', { } }, }); - - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); var widget = clientAction.widgets[0]; // Ensure that when we validate a line without any selection, it is the same @@ -781,17 +783,18 @@ QUnit.module('account', { analytic_tag_ids: [[6, null, []]] }]}]], "Should call process_bank_statement_line with ids"); - var def = $.Deferred(); + var def = testUtils.makeTestPromise(); def.abort = function () {}; event.data.callback(def); }); // click on validate button - widget.$('button.o_validate:not(:hidden)').trigger('click'); + await testUtils.dom.click(widget.$('button.o_validate:not(:hidden)')); + clientAction.destroy(); }); - QUnit.test('Reconciliation validate with proposition', function (assert) { + QUnit.test('Reconciliation validate with proposition', async function (assert) { assert.expect(1); // Test added to check this functionality: https://github.com/odoo/odoo/commit/2f3b469dee6f18cbccce1cdf2a81cfe57960c533 var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); @@ -808,14 +811,16 @@ QUnit.module('account', { }, }); - clientAction.appendTo($('#qunit-fixture')); - + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); var widget = clientAction.widgets[0]; // Add a line as proposition // open the first line - widget.$('.accounting_view thead td:first').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view thead td:first'),{allowInvisible:true}); + await testUtils.nextTick(); // select propositions - widget.$('.match .cell_account_code:first').trigger('click'); + await testUtils.dom.click(widget.$('.match .cell_account_code:first'),{allowInvisible:true}); + await testUtils.nextTick(); // Ensure that when we validate a line with propositions and that there is a remaining balance // We also create a line which is the open balance. @@ -837,17 +842,17 @@ QUnit.module('account', { name: 'SAJ/2014/002 and SAJ/2014/003 : Open balance' }]}]], "Should call process_bank_statement_line with ids"); - var def = $.Deferred(); + var def = testUtils.makeTestPromise(); def.abort = function () {}; event.data.callback(def); }); // click on validate button - widget.$('button.o_validate:not(:hidden)').trigger('click'); + await testUtils.dom.click(widget.$('button.o_validate:not(:hidden)')); clientAction.destroy(); }); - QUnit.test('Reconciliation partial', function (assert) { + QUnit.test('Reconciliation partial [REQUIRE FOCUS]', async function (assert) { assert.expect(9); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); @@ -887,40 +892,40 @@ QUnit.module('account', { }, }); - clientAction.appendTo($('#qunit-fixture')); - + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); var widget = clientAction.widgets[0]; - widget.$('.match .cell_account_code:first').trigger('click'); + await testUtils.dom.click(widget.$('.match .cell_account_code:first')); assert.equal( widget.$('.cell_right .edit_amount').length, 1, "should display the edition pencil"); widget = clientAction.widgets[1]; - widget.$('.accounting_view thead td:first').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view thead td:first')); assert.strictEqual(widget.$('.match .mv_line[data-line-id=114] .cell_account_code:first()').length, 1, 'Should have line') - widget.$('.match .mv_line[data-line-id=114] .cell_account_code').trigger('click'); + await testUtils.dom.click(widget.$('.match .mv_line[data-line-id=114] .cell_account_code')); assert.equal( widget.$('.accounting_view tbody .cell_left .edit_amount').length, 1, "should display the edition pencil"); // The partner has been set automatically, remove it. - widget.$('.o_input_dropdown input').trigger('click'); - widget.$('.o_input_dropdown input').val('').trigger('keyup').trigger('blur'); + await testUtils.dom.click(widget.$('.o_input_dropdown input')); + await testUtils.fields.editAndTrigger(widget.$('.o_input_dropdown input'),'',['keyup','blur']); assert.strictEqual(widget.$('button.btn-primary:visible').length, 0, "should display the reconcile model buttons"); assert.strictEqual(widget.$('.text-danger:visible').length, 1, "should display counterpart alert"); - widget.$('.accounting_view .cell_left .edit_amount').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view .cell_left .edit_amount')); assert.strictEqual(widget.$('.accounting_view .cell_left .edit_amount_input:not(.d-none)').length, 1, "should display the input field to edit amount"); // Edit amount - widget.$('.accounting_view .cell_left .edit_amount_input:not(.d-none)').val('32.58').trigger('blur'); + await testUtils.fields.editAndTrigger(widget.$('.accounting_view .cell_left .edit_amount_input:not(.d-none)'),'32.58',['change','blur']); assert.strictEqual(widget.$('.accounting_view .cell_left .line_amount').text().replace(/[\n\r\s]+/g, ' '), " $ 10000.00 $ 32.58 ", "should display previous amount and new amount") assert.strictEqual(widget.$('button.btn-primary:visible').length, 1, "should display the reconcile button"); - widget.$('button.btn-primary:visible').trigger('click'); + await testUtils.dom.click(widget.$('button.btn-primary:visible')); clientAction.destroy(); }); - QUnit.test('Reconciliation title', function (assert) { + QUnit.test('Reconciliation title', async function (assert) { assert.expect(6); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); @@ -928,13 +933,13 @@ QUnit.module('account', { 'data': this.params.data, }); - clientAction.appendTo($('#qunit-fixture')); - + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); assert.strictEqual(clientAction.$('h1.statement_name:visible').text(), "BNK/2014/001", "Should display the statement name"); - clientAction.$('h1.statement_name').trigger('click'); + await testUtils.dom.click(clientAction.$('h1.statement_name')); assert.containsNone(clientAction, 'h1.statement_name:visible', "Should hide the statement name title to edit the content"); assert.containsOnce(clientAction, 'h1.statement_name_edition:visible', "Should show the edition field of statement name"); - testUtils.fields.editAndTrigger(clientAction.$('h1.statement_name_edition input'), 'BNK/2014/001-BB', ['input', 'blur']); + await testUtils.fields.editAndTrigger(clientAction.$('h1.statement_name_edition input'), 'BNK/2014/001-BB', ['input', 'blur']); assert.containsNone(clientAction, 'h1.statement_name_edition:visible', "Should hide the edition field of statement name"); assert.containsOnce(clientAction, 'h1.statement_name:visible', "Should show the statement name"); assert.strictEqual(clientAction.$('h1.statement_name:visible').text(), "BNK/2014/001-BB", "Should update the statement name"); @@ -942,7 +947,7 @@ QUnit.module('account', { clientAction.destroy(); }); - QUnit.test('Reconciliation currencies', function (assert) { + QUnit.test('Reconciliation currencies', async function (assert) { assert.expect(2); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); @@ -961,13 +966,14 @@ QUnit.module('account', { grouping: [3,0], } }); - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); var widget = clientAction.widgets[0]; assert.strictEqual(clientAction.$('.accounting_view tfoot .cell_right, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s]+/g, ' '), "$ 1,175.00$ 32.58$ 2,000.00", "should display the different amounts with the currency"); - widget.$('.accounting_view thead .mv_line td:first').trigger('click'); + // await testUtils.dom.click(widget.$('.accounting_view thead .mv_line td:first')); assert.strictEqual(clientAction.$('.accounting_view tbody').text().replace(/[\n\r\s]+/g, ' '), " 101200 2017-02-07 INV/2017/0012 $ 650.00 ", "should display the created reconciliation line with the currency"); @@ -975,7 +981,7 @@ QUnit.module('account', { clientAction.destroy(); }); - QUnit.test('Reconciliation change partner', function (assert) { + QUnit.test('Reconciliation change partner', async function (assert) { assert.expect(17); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); @@ -990,51 +996,57 @@ QUnit.module('account', { }, }); - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); var widget = clientAction.widgets[0]; assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Agrolait", "the partner many2one should display agrolait"); assert.containsN(widget, '.match table tr', 2, "agrolait should have 2 propositions for reconciliation"); // Adding the two propositions // This is in order to try that after changing partner the propositions are emptied - widget.$('.match .cell_account_code:first').trigger('click'); - widget.$('.match .cell_account_code:first').trigger('click'); + await testUtils.dom.click(widget.$('.match .cell_account_code:first')); + await testUtils.dom.click(widget.$('.match .cell_account_code:first')); assert.containsN(widget, '.accounting_view tbody tr', 2, "Both proposition should be selected"); // Similate changing partner to one that does not have propositions to see if create mode is open after - widget.$('.o_input_dropdown input').trigger('click'); + await testUtils.dom.click(widget.$('.o_input_dropdown input')); $('.ui-autocomplete .ui-menu-item a:contains(partner 1)').trigger('mouseenter').trigger('click'); + await testUtils.nextTick(); clientAction._onAction({target: widget, name: 'change_partner', data: {data: {display_name: 'partner 1', id: 1}}, stopped: false}); + await testUtils.nextTick(); assert.strictEqual(widget.$('.o_input_dropdown input').val(), "partner 1", "the partner many2one should display partner 1"); assert.containsNone(widget, '.match table tr.mv_line', "partner 1 should have 0 propositions for reconciliation"); assert.strictEqual(widget.$el.data('mode'), 'create', "widget should be in create mode"); // Simulate changing partner - widget.$('.o_input_dropdown input').trigger('click'); + await testUtils.dom.clickFirst(widget.$('.o_input_dropdown input')); $('.ui-autocomplete .ui-menu-item a:contains(Camptocamp)').trigger('mouseenter').trigger('click'); clientAction._onAction({target: widget, name: 'change_partner', data: {data: {display_name: 'Camptocamp', id: 12}}, stopped: false}); + await testUtils.nextTick(); assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Camptocamp", "the partner many2one should display Camptocamp"); assert.containsN(widget, '.match table tr.mv_line', 3, "camptocamp should have 3 propositions for reconciliation"); // Simulate changing partner with SelectCreateDialog widget = clientAction.widgets[1]; assert.strictEqual($('.modal').length, 0, "shouldn't have any opened modal"); - widget.$('.o_input_dropdown input').trigger('click'); + await testUtils.dom.click(widget.$('.o_input_dropdown input')); $('.ui-autocomplete .ui-menu-item a:contains(Search More):eq(1)').trigger('mouseenter').trigger('click'); + await testUtils.nextTick(); assert.strictEqual($('.modal').length, 1, "should open a SelectCreateDialog"); - testUtils.dom.click($('.modal table.o_list_view td:contains(Camptocamp)')); + await testUtils.dom.click($('.modal table.o_list_view td:contains(Camptocamp)')); assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Camptocamp", "the partner many2one should display Camptocamp"); widget = clientAction.widgets[2]; - widget.$('.accounting_view thead td:first').trigger('click'); - widget.$('.accounting_view .mv_line .cell_label').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view thead td:first')); + await testUtils.dom.click(widget.$('.accounting_view .mv_line .cell_label')); assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Camptocamp", "the partner many2one should display agrolait"); assert.containsN(widget, '.match table tr', 3, "Camptocamp should have 3 propositions for reconciliation"); assert.notOk(widget.$('.match div.load-more a:visible').length, "should not display the load more button"); // Simulate remove partner - widget.$('.o_input_dropdown input').trigger('click'); - widget.$('.o_input_dropdown input').val('').trigger('keyup').trigger('blur'); + await testUtils.dom.click(widget.$('.o_input_dropdown input')); + await testUtils.fields.editAndTrigger(widget.$('.o_input_dropdown input'),'',['keyup','blur']); + assert.strictEqual(widget.$('.o_input_dropdown input').val(), "", "the partner many2one should be empty"); assert.containsN(widget, '.match table tr.mv_line', 5, "should have 5 propositions for reconciliation if partner is false"); assert.ok(widget.$('.match div.load-more a:visible').length, "should display the load more button"); @@ -1042,7 +1054,7 @@ QUnit.module('account', { clientAction.destroy(); }); - QUnit.test('Reconciliation create line', function (assert) { + QUnit.test('Reconciliation create line', async function (assert) { assert.expect(23); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); @@ -1059,7 +1071,8 @@ QUnit.module('account', { } }, }); - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); assert.strictEqual(clientAction.$('.accounting_view tfoot .cell_right, .accounting_view tfoot .cell_left').text().replace(/[$, ]+/g, ''), " 1175.00 32.58 2000.00", "should display the open balance values"); @@ -1067,9 +1080,10 @@ QUnit.module('account', { assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), 'Open balance', "should display 'Open Balance' line with the rest to reconcile"); - widget.$('.accounting_view tfoot td:first').trigger('click'); - widget.$('.create .create_account_id input').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view tfoot td:first')); + await testUtils.dom.click(widget.$('.create .create_account_id input')); $('.ui-autocomplete .ui-menu-item a:contains(101200 Account Receivable)').trigger('mouseenter').trigger('click'); + await testUtils.nextTick(); assert.notOk(widget.$('.accounting_view tfoot .cell_label').text(), "should not display 'Open Balance' line because the rest to reconcile is null"); assert.containsOnce(widget, '.accounting_view tbody tr', "should have only the created reconcile line"); @@ -1082,16 +1096,16 @@ QUnit.module('account', { '{"type":"ir.actions.act_window","res_model":"account.reconcile.model","views":[[false,"form"]],"target":"current"}', "should open the reconcile model form view"); }); - widget.$('.create .reconcile_model_create').trigger('click'); + await testUtils.dom.click(widget.$('.create .reconcile_model_create'),{allowInvisible:true}); testUtils.mock.intercept(clientAction, 'do_action', function (event) { assert.strictEqual(JSON.stringify(event.data.action), '{"type":"ir.actions.act_window","res_model":"account.reconcile.model","views":[[false,"list"],[false,"form"]],"view_type":"list","view_mode":"list","target":"current"}', "should open the reconcile model list view"); }); - widget.$('.create .reconcile_model_edit').trigger('click'); + await testUtils.dom.click(widget.$('.create .reconcile_model_edit'),{allowInvisible:true}); - testUtils.fields.editInput(widget.$('.create .create_amount input'), '1100.00'); + await testUtils.fields.editInput(widget.$('.create .create_amount input'), '1100.00'); assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().trim().replace(/[\n\r\s\u00a0]+/g, ' '), "$ 1100.00", "should display the value 1100.00 in right column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$ 75.00", "should display 'Open Balance' line because the rest to reconcile is 75.00"); @@ -1100,11 +1114,12 @@ QUnit.module('account', { "the new line should be update the amout"); assert.ok(widget.$('caption button.btn-secondary:visible').length, "should display the 'validate' button"); - widget.$('.create .add_line').trigger('click').trigger('click'); - testUtils.fields.editInput(widget.$('.create .create_amount input'), '-100'); - widget.$('.create .create_account_id input').trigger('click'); + await testUtils.dom.click(widget.$('.create .add_line'),{allowInvisible:true}); + await testUtils.fields.editInput(widget.$('.create .create_amount input'), '-100'); + await testUtils.dom.click(widget.$('.create .create_account_id input')); $('.ui-autocomplete .ui-menu-item a:contains(101000 Current Assets)').trigger('mouseenter').trigger('click'); - testUtils.fields.editInput(widget.$('.create .create_label input'), 'test0'); + await testUtils.nextTick(); + await testUtils.fields.editInput(widget.$('.create .create_label input'), 'test0'); assert.strictEqual(widget.$('.accounting_view tbody .cell_left:last').text().trim().replace(/[\n\r\s\u00a0]+/g, ' '), "$ 100.00", "should display the value 100.00 in left column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); @@ -1113,15 +1128,16 @@ QUnit.module('account', { assert.strictEqual(widget.$('.accounting_view tbody tr:eq(1)').text().replace(/[\n\r\s$,]+/g, ' '), " 101000 New test0 100.00 ", "the new line should have the selected account, name and amout"); - widget.$('.accounting_view tfoot td:first').trigger('click'); - widget.$('.accounting_view tfoot td:first').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view tfoot td:first')); + await testUtils.dom.click(widget.$('.accounting_view tfoot td:first')); assert.strictEqual(widget.$('.create .create_amount input').val(), "175.00", "should have '175.00' as default amount value"); - testUtils.fields.editInput(widget.$('.create .create_amount input'), '200'); + await testUtils.fields.editInput(widget.$('.create .create_amount input'), '200'); widget.$('.create .create_account_id input').trigger('click'); $('.ui-autocomplete .ui-menu-item a:contains(101000 Current Assets)').trigger('mouseenter').trigger('click'); - testUtils.fields.editInput(widget.$('.create .create_label input'), 'test1'); + await testUtils.nextTick(); + await testUtils.fields.editInput(widget.$('.create .create_label input'), 'test1'); assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text().trim().replace(/[\n\r\s\u00a0]+/g, ' '), "$ 200.00", "should display the value 200.00 in left column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open balance'"); @@ -1131,11 +1147,11 @@ QUnit.module('account', { clientAction.destroy(); }); - QUnit.test('Reconciliation create line (many2one test)', function (assert) { + QUnit.test('Reconciliation create line (many2one test)', async function (assert) { assert.expect(5); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); - var def = $.Deferred(); + var def = testUtils.makeTestPromise(); testUtils.mock.addMockEnvironment(clientAction, { data: this.params.data, @@ -1160,33 +1176,37 @@ QUnit.module('account', { }, }); - clientAction.prependTo($('#qunit-fixture')); + await clientAction.prependTo($('#qunit-fixture')); + await testUtils.nextTick(); var widget = clientAction.widgets[0]; // open the first line in write-off mode - widget.$('.accounting_view tfoot td:first').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view tfoot td:first')); // select an account with the many2one (drop down) - widget.$('.create .create_account_id input').trigger('click'); + await testUtils.dom.click(widget.$('.create .create_account_id input')); $('.ui-autocomplete .ui-menu-item a:contains(101200)').trigger('mouseenter').trigger('click'); + await testUtils.nextTick(); assert.strictEqual(widget.$('.create .create_account_id input').val(), "101200 Account Receivable", "Display the selected account"); assert.strictEqual(widget.$('tbody:first .cell_account_code').text(), "101200", "Display the code of the selected account"); // use the many2one select dialog to change the account - widget.$('.create .create_account_id input').trigger('click'); + await testUtils.dom.click(widget.$('.create .create_account_id input')); $('.ui-autocomplete .ui-menu-item a:contains(Search)').trigger('mouseenter').trigger('click'); + await testUtils.nextTick(); // select the account who does not appear in the drop drown - testUtils.dom.click($('.modal tr.o_data_row:contains(502)')); + await testUtils.dom.click($('.modal tr.o_data_row:contains(502)')); assert.strictEqual(widget.$('.create .create_account_id input').val(), "101200 Account Receivable", "Selected account does not change"); // wait the name_get to render the changes def.resolve(); + await testUtils.nextTick(); assert.strictEqual(widget.$('.create .create_account_id input').val(), "502 Account", "Display the selected account"); assert.strictEqual(widget.$('tbody:first .cell_account_code').text(), "502", "Display the code of the selected account"); clientAction.destroy(); }); - QUnit.test('Reconciliation create line with taxes', function (assert) { + QUnit.test('Reconciliation create line with taxes', async function (assert) { assert.expect(13); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); @@ -1203,23 +1223,26 @@ QUnit.module('account', { } }, }); - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); var widget = clientAction.widgets[0]; - widget.$('.accounting_view tfoot td:first').trigger('click'); - widget.$('.create .create_account_id input').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view tfoot td:first')); + await testUtils.dom.click(widget.$('.create .create_account_id input')); $('.ui-autocomplete .ui-menu-item a:contains(101000 Current Assets)').trigger('mouseenter').trigger('click'); - testUtils.fields.editInput(widget.$('.create .create_label input'), 'test1'); - testUtils.fields.editInput(widget.$('.create .create_amount input'), '1100'); + await testUtils.nextTick(); + await testUtils.fields.editInput(widget.$('.create .create_label input'), 'test1'); + await testUtils.fields.editInput(widget.$('.create .create_amount input'), '1100'); assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text().trim().replace(/[\n\r\s\u00a0]+/g, ' '), "$ 1100.00", "should display the value 1100.00 in left column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column"); assert.containsOnce(widget, '.accounting_view tbody tr', "should have 1 created reconcile lines"); - widget.$('.create .create_tax_id input').trigger('click'); + await testUtils.dom.click(widget.$('.create .create_tax_id input')); $('.ui-autocomplete .ui-menu-item a:contains(10.00%)').trigger('mouseenter').trigger('click'); + await testUtils.nextTick(); assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().trim().replace(/[\n\r\s\u00a0]+/g, ' '), "$ 1000.00 $ 100.00", "should have 2 created reconcile lines with right column values"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); @@ -1227,8 +1250,9 @@ QUnit.module('account', { assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "", "should display 'Open Balance' without any value in left column"); assert.containsN(widget, '.accounting_view tbody tr', 2, "should have 2 created reconcile lines"); - widget.$('.create .create_tax_id input').trigger('click'); + await testUtils.dom.click(widget.$('.create .create_tax_id input')); $('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').trigger('click'); + await testUtils.nextTick(); assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().trim().replace(/[\n\r\s\u00a0]+/g, ' '), "$ 1100.00 $ 220.00", "should have 2 created reconcile lines with right column values"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open balance'"); @@ -1238,7 +1262,7 @@ QUnit.module('account', { clientAction.destroy(); }); - QUnit.test('Reconciliation create line from reconciliation model', function (assert) { + QUnit.test('Reconciliation create line from reconciliation model', async function (assert) { assert.expect(6); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); @@ -1246,32 +1270,33 @@ QUnit.module('account', { testUtils.mock.addMockEnvironment(clientAction, { data: this.params.data, }); - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); var widget = clientAction.widgets[0]; - widget.$('.accounting_view tfoot td:first').trigger('click'); - widget.$('.create .quick_add button:contains(ATOS)').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view tfoot td:first')); + await testUtils.dom.click(widget.$('.create .quick_add button:contains(ATOS)')); assert.strictEqual(widget.$('.accounting_view tbody .cell_label, .accounting_view tbody .cell_right').text().replace(/[\n\r\s$,]+/g, ' '), - " ATOS Banque 1145.62 Tax 20.00% 229.12 ATOS Frais 26.71 Tax 10.00% include 2.67 ", "should display 4 lines"); + " ATOS Banque 1145.62 ATOS Frais 26.71 Tax 20.00% 229.12 Tax 10.00% include 2.67 ", "should display 4 lines"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '), "Open balance229.12", "should display the 'Open balance' line with value in left column"); - testUtils.fields.editInput(widget.$('.create .create_amount input'), '100'); + await testUtils.fields.editAndTrigger(widget.$('.create .create_amount input'), '100',['input']); assert.strictEqual(widget.$('.accounting_view tbody').text().replace(/[\n\r\s$,]+/g, ' '), - " 101120 New ATOS Banque 1075.00 101120 New Tax 20.00% 215.00 101130 New ATOS Frais 90.91 101300 New Tax 10.00% include 9.09 ", + " 101120 New ATOS Banque 1075.00 101130 New ATOS Frais 90.91 101120 New Tax 20.00% 215.00 101300 New Tax 10.00% include 9.09 ", "should update the value of the 4 lines (because the line must have 100% of the value)"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '), "Open balance215.00", "should change the 'Open balance' line because the 20.00% tax is not an include tax"); - widget.$('.accounting_view tbody .cell_account_code:first').trigger('click'); - widget.$('.accounting_view tbody .cell_label:first').trigger('click'); + await testUtils.dom.click(widget.$('.accounting_view tbody .cell_account_code:first')); + await testUtils.dom.click(widget.$('.accounting_view tbody .cell_label:first')); assert.strictEqual(widget.$('.accounting_view tbody').text().replace(/[\n\r\s$,]+/g, ' '), "", "should removed every line"); - widget.$('.create .quick_add button:contains(Double)').trigger('click'); + await testUtils.dom.click(widget.$('.create .quick_add button:contains(Double)')); assert.strictEqual(widget.$('.accounting_view tbody').text().replace(/[\n\r\s$,]+/g, ' '), " 101120 New Double Banque 1145.62 101130 New Double Frais 29.38 ", @@ -1280,7 +1305,7 @@ QUnit.module('account', { clientAction.destroy(); }); - QUnit.test('Reconciliation manual', function (assert) { + QUnit.test('Reconciliation manual', async function (assert) { assert.expect(13); var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options); @@ -1289,7 +1314,8 @@ QUnit.module('account', { data: this.params.data, session: this.params.session, }); - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); assert.strictEqual(clientAction.$('.accounting_view:first thead').text().replace(/[\n\r\s]+/g, ' '), " 101000 Current AssetsLast Reconciliation: 2017-02-16 101000 ", @@ -1298,7 +1324,7 @@ QUnit.module('account', { assert.containsN(clientAction, '.accounting_view:first tbody tr', 2, "should have 2 propositions"); assert.containsOnce(clientAction, '.accounting_view:first .o_reconcile:visible', "should display the reconcile button"); - clientAction.$('.accounting_view:first .o_reconcile:visible').trigger('click'); + await testUtils.dom.click(clientAction.$('.accounting_view:first .o_reconcile:visible')); assert.strictEqual(clientAction.$('.accounting_view:first thead').text().replace(/[\n\r\s]+/g, ' '), " Agrolait 101200 ", @@ -1311,19 +1337,19 @@ QUnit.module('account', { assert.containsOnce(clientAction, '.accounting_view:first .o_no_valid:visible', "should display the skip button"); - clientAction.$('.accounting_view:eq(1) thead td:first').trigger('click'); - clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="21"] .cell_label').trigger('click'); - clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="22"] .cell_label').trigger('click'); + await testUtils.dom.click(clientAction.$('.accounting_view:eq(1) thead td:first')); + await testUtils.dom.click(clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="21"] .cell_label')); + await testUtils.dom.click(clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="22"] .cell_label')); assert.strictEqual(clientAction.$('.o_reconciliation_line:eq(1) tfoot tr').length, 0, "should not display the 'Write-off' line because the balance is null in Euro"); assert.strictEqual(clientAction.$('.o_reconciliation_line:eq(1) .o_reconcile:visible').length, 1, "should display 'Reconcile' button in green"); - clientAction.$('.o_reconciliation_line:eq(1) .o_reconcile:visible').trigger('click'); + await testUtils.dom.click(clientAction.$('.o_reconciliation_line:eq(1) .o_reconcile:visible')); assert.containsOnce(clientAction, '.o_reconciliation_line[data-mode!="inactive"]', "should have only one line open"); - clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="23"] .cell_label').trigger('click'); - clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="24"] .cell_label').trigger('click'); + await testUtils.dom.click(clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="23"] .cell_label')); + await testUtils.dom.click(clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="24"] .cell_label')); assert.strictEqual(clientAction.$('.o_reconciliation_line:eq(1) tfoot tr').length, 1, "should display the 'Write-off' line because the balance is not null in Euro"); assert.strictEqual(clientAction.$('.o_reconciliation_line:eq(1) .o_validate:visible').length, 1, "should display 'Reconcile' button"); @@ -1331,7 +1357,7 @@ QUnit.module('account', { clientAction.destroy(); }); - QUnit.test('Reconciliation: Payment < inv1 + inv2(partial)', function (assert) { + QUnit.test('Reconciliation: Payment < inv1 + inv2(partial)', async function (assert) { assert.expect(3); /* @@ -1388,34 +1414,35 @@ QUnit.module('account', { } }, }); - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); // The first reconciliation "line" is where it happens var widget = clientAction.widgets[0]; // Add first invoice to reconcile fully - widget.$('.match .cell_account_code:first').trigger('click'); + await testUtils.dom.click(widget.$('.match .cell_account_code:first')); assert.ok( widget.$('.cell_right .edit_amount').length, "should display the pencil to edit amount"); // Add second invoice to reconcile partially - widget.$('.match .cell_account_code:first').trigger('click'); + await testUtils.dom.click( widget.$('.match .cell_account_code:first')); // Edit amount on last invoice - testUtils.dom.click(widget.$('.edit_amount:last()')); - widget.$('.edit_amount_input:last()').val('525').trigger('blur'); + await testUtils.dom.click(widget.$('.edit_amount:last()')); + await testUtils.fields.editAndTrigger(widget.$('.edit_amount_input:last()'),'525',['blur']); var $buttonReconcile = widget.$('button.o_reconcile:not(hidden)'); assert.equal($buttonReconcile.length, 1, 'The reconcile button must be visible'); - testUtils.dom.click($buttonReconcile); + await testUtils.dom.click($buttonReconcile); clientAction.destroy(); }); - QUnit.test('Reconciliation: payment and 2 partials', function (assert) { + QUnit.test('Reconciliation: payment and 2 partials', async function (assert) { assert.expect(6); /* @@ -1486,19 +1513,20 @@ QUnit.module('account', { } }, }); - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); // The first reconciliation "line" is where it happens var widget = clientAction.widgets[0]; // Add first invoice // There should be the opportunity to reconcile partially - widget.$('.match .cell_account_code:first').trigger('click'); + await testUtils.dom.click(widget.$('.match .cell_account_code:first')); assert.ok(widget.$('.cell_right .edit_amount').length, "should display the pencil to edit amount"); // Add second invoice - widget.$('.match .cell_account_code:first').trigger('click'); + await testUtils.dom.click(widget.$('.match .cell_account_code:first')); assert.ok(widget.$('.cell_right .edit_amount').length, "should display the pencil to edit amount"); @@ -1510,20 +1538,20 @@ QUnit.module('account', { assert.equal(writeOffCreate.find('input[name=amount]').val(), -1225, 'The right amount should be proposed for the write-off'); - testUtils.dom.click(writeOffCreate.find('.create_account_id input.ui-autocomplete-input')); - testUtils.dom.click($('ul.ui-autocomplete li a:first')); + await testUtils.dom.click(writeOffCreate.find('.create_account_id input.ui-autocomplete-input')); + await testUtils.dom.click($('ul.ui-autocomplete li a:first')); var $buttonReconcile = widget.$('button.o_reconcile:not(hidden)'); assert.equal($buttonReconcile.length, 1, 'The reconcile button must be visible'); - testUtils.dom.click($buttonReconcile); + await testUtils.dom.click($buttonReconcile); clientAction.destroy(); }); - QUnit.test('Reconciliation: partial payment of 2 invoices with one payment', function (assert) { + QUnit.test('Reconciliation: partial payment of 2 invoices with one payment [REQUIRE FOCUS]', async function (assert) { assert.expect(4); /* @@ -1549,7 +1577,7 @@ QUnit.module('account', { amount_str: "$ 1200.00", debit: 1200.0, account_code: "101200", ref: "", already_paid: false}; var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); - testUtils.addMockEnvironment(clientAction, { + testUtils.mock.addMockEnvironment(clientAction, { data: this.params.data, mockRPC: function (route, args) { if (args.method === 'process_bank_statement_line') { @@ -1588,39 +1616,40 @@ QUnit.module('account', { } }, }); - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); // The first reconciliation "line" is where it happens var widget = clientAction.widgets[0]; // Add first invoice // There should be the opportunity to reconcile partially - widget.$('.match .cell_account_code:first').trigger('click'); + await testUtils.dom.click(widget.$('.match .cell_account_code:first')); assert.ok(widget.$('.cell_right .edit_amount').length, "should display the pencil to edit amount"); // Add second invoice - widget.$('.match .cell_account_code:first').trigger('click'); + await testUtils.dom.click(widget.$('.match .cell_account_code:first')); assert.ok(widget.$('.cell_right .edit_amount').length, "should display the pencil to edit amount"); // Edit invoice first amount - widget.$('.edit_amount:first()').trigger('click'); - widget.$('.edit_amount_input:first()').val('500').trigger('blur'); + await testUtils.dom.click(widget.$('.edit_amount:first()')); + await testUtils.fields.editAndTrigger(widget.$('.edit_amount_input:first()'),'500',['blur']); // Edit invoice second amount var $buttonReconcile = widget.$('button.o_reconcile:not(hidden)'); - widget.$('.edit_amount:last()').trigger('click'); - widget.$('.edit_amount_input:last()').val('675').trigger('blur'); + await testUtils.dom.click(widget.$('.edit_amount:last()')); + await testUtils.fields.editAndTrigger(widget.$('.edit_amount_input:last()'),'675',['blur']); assert.equal($buttonReconcile.length, 1, 'The reconcile button must be visible'); - $buttonReconcile.click(); + await testUtils.dom.click($buttonReconcile,{allowInvisible:true}); clientAction.destroy(); }); - QUnit.test('Manual Reconciliation: remove a prop to attain balance and reconcile', function (assert) { + QUnit.test('Manual Reconciliation: remove a prop to attain balance and reconcile', async function (assert) { assert.expect(5); // tweak the data to fit our needs @@ -1659,18 +1688,18 @@ QUnit.module('account', { }, }); - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); // The first reconciliation "line" is where it happens var widget = clientAction.widgets[0]; // Add first prop - widget.$('.match .cell_account_code:first').trigger('click'); + await testUtils.dom.click(widget.$('.match .cell_account_code:first')); assert.equal( widget.$('.cell_right .edit_amount').length, 0, "should not display the pencil to edit amount"); // Add second prop - widget.$('.match .cell_account_code:first').trigger('click'); - + await testUtils.dom.click(widget.$('.match .cell_account_code:first')); // Check that a create form is here var writeOffCreate = widget.$('div.create'); @@ -1681,18 +1710,18 @@ QUnit.module('account', { 'The right amount should be proposed for the write-off'); // remove the first line, the other two will balance one another - testUtils.dom.click(widget.$('tr[data-line-id="999"] td:first')); + await testUtils.dom.click(widget.$('tr[data-line-id="999"] td:first')); var $buttonReconcile = widget.$('button.o_reconcile:not(hidden)'); assert.equal($buttonReconcile.length, 1, 'The reconcile button must be visible'); - testUtils.dom.click($buttonReconcile); + await testUtils.dom.click($buttonReconcile); clientAction.destroy(); }); - QUnit.test('Manual Reconciliation: No lines for account', function (assert) { + QUnit.test('Manual Reconciliation: No lines for account', async function (assert) { assert.expect(2); var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options); @@ -1709,7 +1738,8 @@ QUnit.module('account', { }, }); - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); // The second reconciliation "line" is where it happens var widget = clientAction.widgets[1]; @@ -1719,7 +1749,7 @@ QUnit.module('account', { assert.notOk('data-line-id' in emptyLine.getAttributes(), 'Empty line should be empty'); - testUtils.dom.click(emptyLine.find('td:first')); + await testUtils.dom.click(emptyLine.find('td:first')); // Check that a create form is here var writeOffCreate = widget.$('div.create .create_account_id'); @@ -1730,12 +1760,12 @@ QUnit.module('account', { clientAction.destroy(); }); - QUnit.test('Tax on account receivable', function(assert){ + QUnit.test('Tax on account receivable', async function(assert){ assert.expect(21); this.params.data_for_manual_reconciliation_widget['[[],null]'].accounts = []; var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options); - testUtils.addMockEnvironment(clientAction, { + testUtils.mock.addMockEnvironment(clientAction, { data: this.params.data, session: {}, mockRPC: function(route, args) { @@ -1745,21 +1775,21 @@ QUnit.module('account', { // to get the available values for the droplists. case 'account.account': assert.step("Account"); - return $.when( + return Promise.resolve( _.map(this.data[args.model].records, function (record) { return [record.id, record.name]; }) ); case 'account.tax': assert.step("Tax"); - return $.when( + return Promise.resolve( _.map(this.data[args.model].records, function (record) { return [record.id, record.display_name]; }) ); case 'account.journal': assert.step("Journal"); - return $.when( + return Promise.resolve( _.map(this.data[args.model].records, function (record) { return [record.id, record.display_name]; }) @@ -1790,41 +1820,40 @@ QUnit.module('account', { return this._super.apply(this, arguments); } }); - - clientAction.appendTo($('#qunit-fixture')); + + await clientAction.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); var widget = clientAction.widgets[0]; // Select invoice of 1k$, payment of 1k$ and payment of 180$ var $tableToReconcile = widget.$('.match'); - _.each([6, 19, 21], function(id) { - $tableToReconcile.find('tr.mv_line[data-line-id='+id+']:first td:first-child').click(); + var defs = _.map([6, 19, 21], function(id) { + return testUtils.dom.click($tableToReconcile.find('tr.mv_line[data-line-id='+id+']:first td:first-child')); }); - + await Promise.all(defs); assert.verifySteps([], "No rpc done"); // Store the money in excess to the "account receivable" account with 20% taxes - widget.$("table tfoot tr td:first").click(); + await testUtils.dom.click(widget.$("table tfoot tr td:first")); var $reconcileForm = widget.$(".create"); - $reconcileForm.find('.create_account_id input').click(); - $('.ui-autocomplete .ui-menu-item a:contains(101200 Account Receivable)') - .trigger('mouseover') - .trigger('click'); + await testUtils.dom.click($reconcileForm.find('.create_account_id input')); + $('.ui-autocomplete .ui-menu-item a:contains(101200 Account Receivable)').trigger('mouseover').trigger('click'); + await testUtils.nextTick(); assert.verifySteps(["Account"], "Account rpc done"); - $reconcileForm.find('.create_tax_id input').click(); - $('.ui-autocomplete .ui-menu-item a:contains(Tax 20.00%)') - .trigger('mouseover') - .trigger('click'); - assert.verifySteps(["Account", "Tax"], "Tax rpc done"); + await testUtils.dom.click($reconcileForm.find('.create_tax_id input')); + $('.ui-autocomplete .ui-menu-item a:contains(Tax 20.00%)').trigger('mouseover').trigger('click'); + await testUtils.nextTick(); + assert.verifySteps(["Tax"], "Tax rpc done"); + + await testUtils.dom.click($reconcileForm.find('.create_journal_id input'),{allowInvisible:true}); + $('.ui-autocomplete .ui-menu-item a:contains(company 1 journal)').trigger('mouseover').trigger('click'); + await testUtils.nextTick(); + await testUtils.fields.editAndTrigger($reconcileForm.find('.create_label input'),'dummy text','input'); + await testUtils.dom.click($reconcileForm.find('.create_label input')); + assert.verifySteps(["Journal"], "Journal rpc done"); - $reconcileForm.find('.create_journal_id input').click(); - $('.ui-autocomplete .ui-menu-item a:contains(company 1 journal)') - .trigger('mouseover') - .trigger('click'); - $reconcileForm.find('.create_label input').val('dummy text').trigger('input'); - assert.verifySteps(["Account", "Tax", "Journal"], "Journal rpc done"); - // Verify the two (gift + tax) lines were added to the list var $newLines = widget.$('tr.mv_line[data-line-id^=createLine]'); var idx = ($($($newLines[0]).find("td")[3]).text().trim() === "dummy text") ? 0 : 1; @@ -1854,13 +1883,13 @@ QUnit.module('account', { "Tax line has the correct right amount"); // Reconcile - widget.$("button.o_reconcile.btn.btn-primary:first").click(); + await testUtils.dom.click(widget.$("button.o_reconcile.btn.btn-primary:first")); assert.ok(true, "No error in reconciliation"); clientAction.destroy(); }); - - QUnit.test('Reconcile temporarily and ask to check', function(assert){ + + QUnit.test('Reconcile temporarily and ask to check', async function(assert){ assert.expect(4); this.params.options.context['to_check'] = true; var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); @@ -1877,22 +1906,24 @@ QUnit.module('account', { } }, }); - clientAction.appendTo($('#qunit-fixture')); + await clientAction.appendTo($('#qunit-fixture')); + // await clientAction.appendTo(document.body); // for debug purpose var widget = clientAction.widgets[0]; // Add a line as proposition // open the first line - widget.$('.accounting_view tfoot td.cell_label').click() + await testUtils.dom.click(widget.$('.accounting_view tfoot td.cell_label')); var $reconcileForm = widget.$(".create"); - $reconcileForm.find('.create_account_id input').val('499001 Suspense Account').keydown().keyup() - $reconcileForm.find('.create_account_id input').click(); + $reconcileForm.find('.create_account_id input').val('499001 Suspense Account').keydown().keyup(); + await testUtils.dom.click($reconcileForm.find('.create_account_id input')); $('.ui-autocomplete .ui-menu-item a:contains(499001 Suspense Account)') .trigger('mouseover') .trigger('click'); - + await testUtils.nextTick(); + assert.equal($("button.o_validate.btn.btn-secondary.text-warning:first").length, 0, "should not display reconcile button in orange"); - $reconcileForm.find('.create_to_check input').click() + await testUtils.dom.click($reconcileForm.find('.create_to_check input')); assert.equal($("button.o_validate.btn.btn-secondary.text-warning:first").length, 1, "should display reconcile button in orange"); testUtils.mock.intercept(clientAction, 'call_service', function (event) { @@ -1909,12 +1940,12 @@ QUnit.module('account', { to_check: true, }]], "Should call process_bank_statement_line with to_check set to true"); - var def = $.Deferred(); + var def = testUtils.makeTestPromise(); def.abort = function () {}; event.data.callback(def); }); - widget.$("button.o_reconcile.btn.btn-primary:first").click(); + await testUtils.dom.click(widget.$("button.o_reconcile.btn.btn-primary:first"), {allowInvisible: true}); assert.ok(true, "No error in reconciliation"); clientAction.destroy(); diff --git a/addons/account/static/tests/section_and_note_tests.js b/addons/account/static/tests/section_and_note_tests.js index c40790fd9f7..0cc19043674 100644 --- a/addons/account/static/tests/section_and_note_tests.js +++ b/addons/account/static/tests/section_and_note_tests.js @@ -46,9 +46,9 @@ QUnit.module('section_and_note', { }; }, }, function () { - QUnit.test('correct display of section and note fields', function (assert) { + QUnit.test('correct display of section and note fields', async function (assert) { assert.expect(4); - var form = createView({ + var form = await createView({ View: FormView, model: 'invoice', data: this.data, @@ -76,18 +76,18 @@ QUnit.module('section_and_note', { "should have a section class"); // enter edit mode - testUtils.form.clickEdit(form); + await testUtils.form.clickEdit(form); // editing line should be textarea $tr0 = form.$('tr.o_data_row:eq(0)'); - testUtils.dom.click($tr0.find('td.o_data_cell')); - assert.strictEqual($tr0.find('td.o_data_cell textarea[name="name"]').length, 1, + await testUtils.dom.click($tr0.find('td.o_data_cell')); + assert.containsOnce($tr0, 'td.o_data_cell textarea[name="name"]', "editing line should be textarea"); // editing section should be input $tr1 = form.$('tr.o_data_row:eq(1)'); - testUtils.dom.click($tr1.find('td.o_data_cell')); - assert.strictEqual($tr1.find('td.o_data_cell input[name="name"]').length, 1, + await testUtils.dom.click($tr1.find('td.o_data_cell')); + assert.containsOnce($tr1, 'td.o_data_cell input[name="name"]', "editing section should be input"); form.destroy(); diff --git a/addons/auth_password_policy/static/src/js/change_password.js b/addons/auth_password_policy/static/src/js/change_password.js index 1a6e7faaafc..0a40f4bd39d 100644 --- a/addons/auth_password_policy/static/src/js/change_password.js +++ b/addons/auth_password_policy/static/src/js/change_password.js @@ -18,16 +18,16 @@ ChangePassword.include({ }).then(function (p) { _this._meter = new Meter(_this, new policy.Policy(p), policy.recommendations); }); - return $.when( + return Promise.all([ this._super.apply(this, arguments), getPolicy - ); + ]); }, start: function () { - return $.when( + return Promise.all([ this._meter.insertAfter(this.$('input[name=new_password]')), this._super() - ); + ]); } }) }); diff --git a/addons/auth_password_policy/static/src/js/password_field.js b/addons/auth_password_policy/static/src/js/password_field.js index fa8cfd20ff2..f0aa2c4420e 100644 --- a/addons/auth_password_policy/static/src/js/password_field.js +++ b/addons/auth_password_policy/static/src/js/password_field.js @@ -28,10 +28,10 @@ var PasswordField = fields.InputField.extend({ }).then(function (p) { _this._meter = new Meter(_this, new policy.Policy(p), policy.recommendations); }); - return $.when( + return Promise.all([ this._super.apply(this, arguments), getPolicy - ); + ]); }, /** * Add a next to the input (TODO: move to template?) @@ -42,7 +42,7 @@ var PasswordField = fields.InputField.extend({ _renderEdit: function () { var _this = this; var meter = this._meter; - return $.when(this._super.apply(this, arguments)).then(function () { + return Promise.resolve(this._super.apply(this, arguments)).then(function () { return meter._widgetRenderAndInsert(function (t) { // insertAfter doesn't work and appendTo means the meter is // ignored (as this.$el is an input[type=password]) diff --git a/addons/barcodes/static/src/js/barcode_events.js b/addons/barcodes/static/src/js/barcode_events.js index bb28a8e1085..22c23ece8c3 100644 --- a/addons/barcodes/static/src/js/barcode_events.js +++ b/addons/barcodes/static/src/js/barcode_events.js @@ -289,9 +289,9 @@ var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, { }, stop: function(){ - $('body').unbind("keypress", this.__handler); - $('body').unbind("keydown", this.__keydown_handler); - $('body').unbind('keyup', this.__keyup_handler); + $('body').off("keypress", this.__handler); + $('body').off("keydown", this.__keydown_handler); + $('body').off('keyup', this.__keyup_handler); }, }); diff --git a/addons/barcodes/static/src/js/barcode_field.js b/addons/barcodes/static/src/js/barcode_field.js index d8a7a1c853e..b5786f8a161 100644 --- a/addons/barcodes/static/src/js/barcode_field.js +++ b/addons/barcodes/static/src/js/barcode_field.js @@ -13,7 +13,7 @@ var FieldFloatScannable = basicFields.FieldFloat.extend({ // The barcode_events component intercepts keypresses and releases them when it // appears they are not part of a barcode. But since released keypresses don't // trigger native behaviour (like characters input), we must simulate it. - 'keypress': '_onKeypress', + keypress: '_onKeypress', }), //-------------------------------------------------------------------------- @@ -24,9 +24,9 @@ var FieldFloatScannable = basicFields.FieldFloat.extend({ * @override * @private */ - _renderEdit: function() { + _renderEdit: function () { var self = this; - $.when(this._super()).then(function () { + return Promise.resolve(this._super()).then(function () { self.$input.data('enableBarcode', true); }); }, diff --git a/addons/barcodes/static/src/js/barcode_form_view.js b/addons/barcodes/static/src/js/barcode_form_view.js index 15e27498f94..08463f2b997 100644 --- a/addons/barcodes/static/src/js/barcode_form_view.js +++ b/addons/barcodes/static/src/js/barcode_form_view.js @@ -56,13 +56,13 @@ FormController.include({ * @private * @param {string} barcode sent by the scanner (string generate from keypress series) * @param {Object} activeBarcode: options sent by the field who use barcode features - * @returns {Deferred} + * @returns {Promise} */ _barcodeAddX2MQuantity: function (barcode, activeBarcode) { if (this.mode === 'readonly') { this.do_warn(_t('Error: Document not editable'), _t('To modify this document, please first start edition.')); - return new $.Deferred().reject(); + return Promise.reject(); } var record = this.model.get(this.handle); @@ -166,7 +166,7 @@ FormController.include({ * @param {Object} current record * @param {string} barcode sent by the scanner (string generate from keypress series) * @param {Object} activeBarcode: options sent by the field who use barcode features - * @returns {Deferred} + * @returns {Promise} */ _barcodeSelectedCandidate: function (candidate, record, barcode, activeBarcode, quantity) { var changes = {}; @@ -198,7 +198,7 @@ FormController.include({ * @param {Object} current record * @param {string} barcode sent by the scanner (string generate from keypress series) * @param {Object} activeBarcode: options sent by the field who use barcode features - * @returns {Deferred} + * @returns {Promise} */ _barcodeWithoutCandidate: function (record, barcode, activeBarcode) { var changes = {}; @@ -267,26 +267,25 @@ FormController.include({ * @param {string|function} method defined by the commands options * @param {string} barcode sent by the scanner (string generate from keypress series) * @param {Object} activeBarcode: options sent by the field who use barcode features - * @returns {Deferred} + * @returns {Promise} */ _barcodeActiveScanned: function (method, barcode, activeBarcode) { var self = this; var methodDef; - var def = new $.Deferred(); - if (typeof method === 'string') { - methodDef = this[method](barcode, activeBarcode); - } else { - methodDef = method.call(this, barcode, activeBarcode); - } - methodDef - .done(function () { - var record = self.model.get(self.handle); - var candidate = self._getBarCodeRecord(record, barcode, activeBarcode); - activeBarcode.candidate = candidate; - }) - .always(function () { - def.resolve(); - }); + var def = new Promise(function (resolve, reject) { + if (typeof method === 'string') { + methodDef = self[method](barcode, activeBarcode); + } else { + methodDef = method.call(self, barcode, activeBarcode); + } + methodDef + .then(function () { + var record = self.model.get(self.handle); + var candidate = self._getBarCodeRecord(record, barcode, activeBarcode); + activeBarcode.candidate = candidate; + }) + .then(resolve, resolve); + }); return def; }, /** @@ -296,7 +295,7 @@ FormController.include({ * @private * @param {string} barcode sent by the scanner (string generate from keypress series) * @param {DOM Object} target - * @returns {Deferred} + * @returns {Promise} */ _barcodeScanned: function (barcode, target) { var self = this; @@ -325,7 +324,7 @@ FormController.include({ if (prefixed && !hasCommand) { self.do_warn(_t('Error: Barcode command is undefined'), barcode); } - return self.alive($.when.apply($, defs)).then(function () { + return self.alive(Promise.all(defs)).then(function () { if (!prefixed) { // remember the barcode scanned for the quantity listener self.current_barcode = barcode; @@ -421,12 +420,12 @@ FormRenderer.include({ */ _barcodeButtonHandler: function ($button, node) { var commands = {}; - commands.barcode = function () {return $.when();}; + commands.barcode = function () {return Promise.resolve();}; commands['O-BTN.' + node.attrs.barcode_trigger] = function () { if (!$button.hasClass('o_invisible_modifier')) { $button.click(); } - return $.when(); + return Promise.resolve(); }; var name = node.attrs.name; if (node.attrs.string) { diff --git a/addons/barcodes/static/src/js/barcode_parser.js b/addons/barcodes/static/src/js/barcode_parser.js index cf357a438b2..3077f650fae 100644 --- a/addons/barcodes/static/src/js/barcode_parser.js +++ b/addons/barcodes/static/src/js/barcode_parser.js @@ -22,7 +22,7 @@ var BarcodeParser = Class.extend({ return; } var id = this.nomenclature_id[0]; - rpc.query({ + return rpc.query({ model: 'barcode.nomenclature', method: 'read', args: [[id], ['name','rule_ids','upc_ean_conv']], @@ -80,7 +80,7 @@ var BarcodeParser = Class.extend({ var total = sum1 + 3 * sum2; return Number((10 - total % 10) % 10); }, - + // returns true if the ean is a valid EAN barcode number by checking the control digit. // ean must be a string @@ -185,10 +185,10 @@ var BarcodeParser = Class.extend({ return match; }, - + // attempts to interpret a barcode (string encoding a barcode Code-128) // it will return an object containing various information about the barcode. - // most importantly : + // most importantly : // - code : the barcode // - type : the type of the barcode (e.g. alias, unit product, weighted product...) // @@ -198,7 +198,7 @@ var BarcodeParser = Class.extend({ parse_barcode: function(barcode){ var parsed_result = { encoding: '', - type:'error', + type:'error', code:barcode, base_code: barcode, value: 0, @@ -213,7 +213,7 @@ var BarcodeParser = Class.extend({ var rule = rules[i]; var cur_barcode = barcode; - if ( rule.encoding === 'ean13' && + if ( rule.encoding === 'ean13' && this.check_encoding(barcode,'upca') && this.nomenclature.upc_ean_conv in {'upc2ean':'','always':''} ){ cur_barcode = '0' + cur_barcode; diff --git a/addons/barcodes/static/tests/barcode_tests.js b/addons/barcodes/static/tests/barcode_tests.js index 58cf13925b7..47a53402b14 100644 --- a/addons/barcodes/static/tests/barcode_tests.js +++ b/addons/barcodes/static/tests/barcode_tests.js @@ -51,10 +51,10 @@ QUnit.module('Barcodes', { } }); -QUnit.test('Button with barcode_trigger', function (assert) { +QUnit.test('Button with barcode_trigger', async function (assert) { assert.expect(2); - var form = createView({ + var form = await createView({ View: FormView, model: 'product', data: this.data, @@ -84,16 +84,16 @@ QUnit.test('Button with barcode_trigger', function (assert) { _.each(['O','-','B','T','N','.','d','o','i','t','Enter'], triggerKeypressEvent); // O-BTN.dothat (should not call execute_action as the button isn't visible) _.each(['O','-','B','T','N','.','d','o','t','h','a','t','Enter'], triggerKeypressEvent); - + await testUtils.nextTick(); assert.verifySteps([], "no warning should be displayed"); form.destroy(); }); -QUnit.test('edit, save and cancel buttons', function (assert) { +QUnit.test('edit, save and cancel buttons', async function (assert) { assert.expect(6); - var form = createView({ + var form = await createView({ View: FormView, model: 'product', data: this.data, @@ -109,33 +109,37 @@ QUnit.test('edit, save and cancel buttons', function (assert) { // O-CMD.EDIT _.each(["O","-","C","M","D",".","E","D","I","T","Enter"], triggerKeypressEvent); + await testUtils.nextTick(); assert.containsOnce(form, ".o_form_editable", "should have switched to 'edit' mode"); // dummy change to check that it actually saves - testUtils.fields.editInput(form.$('.o_field_widget'), 'test'); + await testUtils.fields.editInput(form.$('.o_field_widget'), 'test'); // O-CMD.SAVE _.each(["O","-","C","M","D",".","S","A","V","E","Enter"], triggerKeypressEvent); + await testUtils.nextTick(); assert.containsOnce(form, ".o_form_readonly", "should have switched to 'readonly' mode"); assert.verifySteps(['save'], 'should have saved'); // O-CMD.EDIT _.each(["O","-","C","M","D",".","E","D","I","T","Enter"], triggerKeypressEvent); + await testUtils.nextTick(); // dummy change to check that it correctly discards - testUtils.fields.editInput(form.$('.o_field_widget'), 'test'); + await testUtils.fields.editInput(form.$('.o_field_widget'), 'test'); // O-CMD.CANCEL _.each(["O","-","C","M","D",".","D","I","S","C","A","R","D","Enter"], triggerKeypressEvent); + await testUtils.nextTick(); assert.containsOnce(form, ".o_form_readonly", "should have switched to 'readonly' mode"); - assert.verifySteps(['save'], 'should not have saved'); + assert.verifySteps([], 'should not have saved'); form.destroy(); }); -QUnit.test('pager buttons', function (assert) { +QUnit.test('pager buttons', async function (assert) { assert.expect(5); - var form = createView({ + var form = await createView({ View: FormView, model: 'product', data: this.data, @@ -150,21 +154,25 @@ QUnit.test('pager buttons', function (assert) { assert.strictEqual(form.$('.o_field_widget').text(), 'Large Cabinet'); // O-CMD.PAGER-NEXT _.each(["O","-","C","M","D",".","N","E","X","T","Enter"], triggerKeypressEvent); + await testUtils.nextTick(); assert.strictEqual(form.$('.o_field_widget').text(), 'Cabinet with Doors'); // O-CMD.PAGER-PREV _.each(["O","-","C","M","D",".","P","R","E","V","Enter"], triggerKeypressEvent); + await testUtils.nextTick(); assert.strictEqual(form.$('.o_field_widget').text(), 'Large Cabinet'); // O-CMD.PAGER-LAST _.each(["O","-","C","M","D",".","P","A","G","E","R","-","L","A","S","T","Enter"], triggerKeypressEvent); + await testUtils.nextTick(); assert.strictEqual(form.$('.o_field_widget').text(), 'Cabinet with Doors'); // O-CMD.PAGER-FIRST _.each(["O","-","C","M","D",".","P","A","G","E","R","-","F","I","R","S","T","Enter"], triggerKeypressEvent); + await testUtils.nextTick(); assert.strictEqual(form.$('.o_field_widget').text(), 'Large Cabinet'); form.destroy(); }); -QUnit.test('do no update form twice after a command barcode scanned', function (assert) { +QUnit.test('do no update form twice after a command barcode scanned', async function (assert) { assert.expect(7); var delay = barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms; @@ -176,7 +184,7 @@ QUnit.test('do no update form twice after a command barcode scanned', function ( }, }); - var form = createView({ + var form = await createView({ View: FormView, model: 'product', data: this.data, @@ -201,21 +209,23 @@ QUnit.test('do no update form twice after a command barcode scanned', function ( // switch to next record _.each(["O","-","C","M","D",".","N","E","X","T","Enter"], triggerKeypressEvent); + await testUtils.nextTick(); // a first update is done to reload the data (thus followed by a read), but // update shouldn't be called afterwards - assert.verifySteps(['read', 'update', 'read']); + assert.verifySteps(['update', 'read']); _.each(['5','4','3','9','8','2','6','7','1','2','5','2','Enter'], triggerKeypressEvent); + await testUtils.nextTick(); // a real barcode has been scanned -> an update should be requested (with // option reload='false', so it isn't followed by a read) - assert.verifySteps(['read', 'update', 'read', 'update']); + assert.verifySteps(['update']); form.destroy(); barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay; testUtils.mock.unpatch(FormController); }); -QUnit.test('widget field_float_scannable', function (assert) { +QUnit.test('widget field_float_scannable', async function (assert) { var done = assert.async(); assert.expect(11); @@ -227,7 +237,7 @@ QUnit.test('widget field_float_scannable', function (assert) { int_field: function () {}, }; - var form = createView({ + var form = await createView({ View: FormView, model: 'product', data: this.data, @@ -250,7 +260,7 @@ QUnit.test('widget field_float_scannable', function (assert) { assert.strictEqual(form.$('.o_field_widget[name=int_field]').text(), '4', "should display the correct value in readonly"); - testUtils.form.clickEdit(form); + await testUtils.form.clickEdit(form); assert.strictEqual(form.$('.o_field_widget[name=int_field]').val(), '4', "should display the correct value in edit"); @@ -260,13 +270,15 @@ QUnit.test('widget field_float_scannable', function (assert) { assert.strictEqual(form.$('.o_field_widget[name=int_field]').get(0), document.activeElement, "int field should be focused"); form.$('.o_field_widget[name=int_field]').trigger({type: 'keypress', which: 50, keyCode: 50}); // 2 + await testUtils.nextTick(); assert.strictEqual(form.$('.o_field_widget[name=int_field]').get(0), document.activeElement, "int field should still be focused"); form.$('.o_field_widget[name=int_field]').trigger({type: 'keypress', which: 54, keyCode: 54}); // 6 + await testUtils.nextTick(); assert.strictEqual(form.$('.o_field_widget[name=int_field]').get(0), document.activeElement, "int field should still be focused"); - setTimeout(function () { + setTimeout(async function () { assert.strictEqual(form.$('.o_field_widget[name=int_field]').val(), '426', "should display the correct value in edit"); assert.strictEqual(form.$('.o_field_widget[name=int_field]').get(0), document.activeElement, @@ -275,6 +287,7 @@ QUnit.test('widget field_float_scannable', function (assert) { assert.verifySteps([], 'should not have done any onchange RPC'); form.$('.o_field_widget[name=int_field]').trigger('change'); // should trigger the onchange + await testUtils.nextTick(); assert.verifySteps(['onchange'], 'should have done the onchange RPC'); @@ -284,7 +297,7 @@ QUnit.test('widget field_float_scannable', function (assert) { }); }); -QUnit.test('widget barcode_handler', function (assert) { +QUnit.test('widget barcode_handler', async function (assert) { assert.expect(4); var delay = barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms; @@ -299,7 +312,7 @@ QUnit.test('widget barcode_handler', function (assert) { }, }; - var form = createView({ + var form = await createView({ View: FormView, model: 'product', data: this.data, @@ -324,6 +337,7 @@ QUnit.test('widget barcode_handler', function (assert) { "initial value should be correct"); _.each(['5','4','3','9','8','2','6','7','1','2','5','2','Enter'], triggerKeypressEvent); + await testUtils.nextTick(); assert.strictEqual(form.$('.o_field_widget[name=int_field]').val(), '1', "value should have been incremented"); @@ -333,7 +347,7 @@ QUnit.test('widget barcode_handler', function (assert) { barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay; }); -QUnit.test('specification of widget barcode_handler', function (assert) { +QUnit.test('specification of widget barcode_handler', async function (assert) { assert.expect(5); var delay = barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms; @@ -356,7 +370,7 @@ QUnit.test('specification of widget barcode_handler', function (assert) { }); fieldRegistry.add('test_barcode_handler', TestBarcodeHandler); - var form = createView({ + var form = await createView({ View: FormView, model: 'order', data: this.data, @@ -392,25 +406,28 @@ QUnit.test('specification of widget barcode_handler', function (assert) { // scan twice product 1 _.each(['1','2','3','4','5','6','7','8','9','0','Enter'], triggerKeypressEvent); + await testUtils.nextTick(); assert.strictEqual(form.$('.o_data_row:first .o_data_cell:nth(1)').text(), '1', "quantity of line one should have been incremented"); _.each(['1','2','3','4','5','6','7','8','9','0','Enter'], triggerKeypressEvent); + await testUtils.nextTick(); assert.strictEqual(form.$('.o_data_row:first .o_data_cell:nth(1)').text(), '2', "quantity of line one should have been incremented"); // scan once product 2 _.each(['0','9','8','7','6','5','4','3','2','1','Enter'], triggerKeypressEvent); + await testUtils.nextTick(); assert.strictEqual(form.$('.o_data_row:nth(1) .o_data_cell:nth(1)').text(), '1', "quantity of line one should have been incremented"); - testUtils.form.clickSave(form); + await testUtils.form.clickSave(form); form.destroy(); barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay; delete fieldRegistry.map.test_barcode_handler; }); -QUnit.test('specification of widget barcode_handler with keypress and notifyChange', function (assert) { +QUnit.test('specification of widget barcode_handler with keypress and notifyChange', async function (assert) { assert.expect(6); var done = assert.async(); @@ -440,7 +457,7 @@ QUnit.test('specification of widget barcode_handler with keypress and notifyChan }); fieldRegistry.add('test_barcode_handler', TestBarcodeHandler); - var form = createView({ + var form = await createView({ View: FormView, model: 'order', data: this.data, @@ -464,20 +481,24 @@ QUnit.test('specification of widget barcode_handler with keypress and notifyChan }, }); _.each(['1','2','3','4','5','6','7','8','9','0','Enter'], triggerKeypressEvent); - // Quantity listener should open a dialog. + await testUtils.nextTick(); + // Quantity listener should open a dialog. triggerKeypressEvent('5'); + await testUtils.nextTick(); - setTimeout(function () { + setTimeout(async function () { var keycode = $.ui.keyCode.ENTER; assert.strictEqual($('.modal .modal-body').length, 1, 'should open a modal with a quantity as input'); assert.strictEqual($('.modal .modal-body .o_set_qty_input').val(), '5', 'the quantity by default in the modal shoud be 5'); $('.modal .modal-body .o_set_qty_input').val('7'); + await testUtils.nextTick(); $('.modal .modal-body .o_set_qty_input').trigger($.Event('keypress', {which: keycode, keyCode: keycode})); + await testUtils.nextTick(); assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(1)').text(), '7', - "quantity checked should be 7"); + "quantity checked should be 7"); assert.verifySteps(['read', 'read']); @@ -487,12 +508,12 @@ QUnit.test('specification of widget barcode_handler with keypress and notifyChan done(); }); }); -QUnit.test('barcode_scanned only trigger error for active view', function (assert) { +QUnit.test('barcode_scanned only trigger error for active view', async function (assert) { assert.expect(2); this.data.order_line.fields._barcode_scanned = {string: 'Barcode scanned', type: 'char'}; - var form = createView({ + var form = await createView({ View: FormView, model: 'order', data: this.data, @@ -526,7 +547,7 @@ QUnit.test('barcode_scanned only trigger error for active view', function (asser }, }); - testUtils.dom.click(form.$('.o_data_row:first')); + await testUtils.dom.click(form.$('.o_data_row:first')); // We do not trigger on the body since modal and // form view are both inside it. @@ -540,6 +561,7 @@ QUnit.test('barcode_scanned only trigger error for active view', function (asser return $('.modal').trigger($.Event('keypress', {which: keycode, keyCode: keycode})); } _.each(['O','-','B','T','N','.','c','a','n','c','e','l','Enter'], modalTriggerKeypressEvent); + await testUtils.nextTick(); assert.verifySteps(['warning'], "only one event should be triggered"); form.destroy(); }); diff --git a/addons/base_import/static/src/js/import_action.js b/addons/base_import/static/src/js/import_action.js index 3d983d4b3d6..6a0d6fee4d8 100644 --- a/addons/base_import/static/src/js/import_action.js +++ b/addons/base_import/static/src/js/import_action.js @@ -152,7 +152,7 @@ var DataImport = AbstractAction.extend({ }).then(function (result) { self.importTemplates = result; }); - return $.when(this._super.apply(this, arguments), def); + return Promise.all([this._super.apply(this, arguments), def]); }, start: function () { var self = this; @@ -162,9 +162,9 @@ var DataImport = AbstractAction.extend({ this.setup_float_format_picker(); this.setup_date_format_picker(); - return $.when( + return Promise.all([ this._super(), - self.create_model().done(function (id) { + self.create_model().then(function (id) { self.id = id; self.$('input[name=import_id]').val(id); @@ -173,8 +173,8 @@ var DataImport = AbstractAction.extend({ cp_content: {$buttons: self.$buttons}, }; self.updateControlPanel(status); - }) - ); + }), + ]); }, create_model: function() { return this._rpc({ @@ -346,7 +346,7 @@ var DataImport = AbstractAction.extend({ method: 'parse_preview', args: [this.id, this.import_options()], kwargs: {context: session.user_context}, - }).done(function (result) { + }).then(function (result) { var signal = result.error ? 'preview_failed' : 'preview_succeeded'; self[signal](result); }); @@ -567,7 +567,9 @@ var DataImport = AbstractAction.extend({ method: 'do', args: [this.id, fields, columns, this.import_options()], kwargs : kwargs, - }).then(null, function (error, event) { + }).then(null, function (reason) { + var error = reason.message; + var event = reason.event; // In case of unexpected exception, convert // "JSON-RPC error" to an import failure, and // prevent default handling (warning dialog) @@ -588,7 +590,7 @@ var DataImport = AbstractAction.extend({ || error.message; } - return $.when({'messages': [{ + return Promise.resolve({'messages': [{ type: 'error', record: false, message: msg, @@ -596,12 +598,14 @@ var DataImport = AbstractAction.extend({ }) ; }, onvalidate: function () { - return this.call_import({ dryrun: true, tracking_disable: true }) - .done(this.proxy('validated')); + var prom = this.call_import({ dryrun: true, tracking_disable: true }); + prom.then(this.proxy('validated')); + return prom; }, onimport: function () { var self = this; - return this.call_import({ dryrun: false }).done(function (results) { + var prom = this.call_import({ dryrun: false }); + prom.then(function (results) { var message = results.messages; if (!_.any(message, function (message) { return message.type === 'error'; })) { @@ -610,6 +614,7 @@ var DataImport = AbstractAction.extend({ } self['import_failed'](results); }); + return prom; }, onimported: function (event, from, to, results) { this.do_notify(_t("Import completed"), _.str.sprintf(_t("%d records were successfully imported"), results.ids.length)); diff --git a/addons/base_import/static/tests/import_buttons_mobile_tests.js b/addons/base_import/static/tests/import_buttons_mobile_tests.js index ac3410b9369..49c8960069c 100644 --- a/addons/base_import/static/tests/import_buttons_mobile_tests.js +++ b/addons/base_import/static/tests/import_buttons_mobile_tests.js @@ -19,10 +19,10 @@ QUnit.module('Base Import Mobile Tests', { } }); -QUnit.test('import button should be hidden in list on mobile screens', function (assert) { +QUnit.test('import button should be hidden in list on mobile screens', async function (assert) { assert.expect(1); - var list = createView({ + var list = await createView({ View: ListView, model: 'foo', data: this.data, @@ -34,10 +34,10 @@ QUnit.test('import button should be hidden in list on mobile screens', function list.destroy(); }); -QUnit.test('import button should be hidden in kanban on mobile screens', function (assert) { +QUnit.test('import button should be hidden in kanban on mobile screens', async function (assert) { assert.expect(1); - var kanban = createView({ + var kanban = await createView({ View: KanbanView, model: 'foo', data: this.data, diff --git a/addons/base_import/static/tests/import_buttons_tests.js b/addons/base_import/static/tests/import_buttons_tests.js index f231b551fa2..40bd162dd2f 100644 --- a/addons/base_import/static/tests/import_buttons_tests.js +++ b/addons/base_import/static/tests/import_buttons_tests.js @@ -22,31 +22,31 @@ QUnit.module('Base Import Tests', { } }); -QUnit.test('add import button in list', function(assert) { +QUnit.test('add import button in list', async function(assert) { assert.expect(2); - var list = createView({ + var list = await createView({ View: ListView, model: 'foo', data: this.data, arch: '', }); - assert.ok(list.$buttons.find('.o_button_import:contains(Import)').is(':visible'), + assert.isVisible(list.$buttons.find('.o_button_import:contains(Import)'), "should have a visible Import button"); testUtils.mock.intercept(list, 'do_action', function() { assert.ok(true, "should have triggered a do_action"); }); - testUtils.dom.click(list.$buttons.find('.o_button_import:contains(Import)')); + await testUtils.dom.click(list.$buttons.find('.o_button_import:contains(Import)')); list.destroy(); }); -QUnit.test('list without import button', function(assert) { +QUnit.test('list without import button', async function(assert) { assert.expect(1); - var list = createView({ + var list = await createView({ View: ListView, model: 'foo', data: this.data, @@ -56,14 +56,14 @@ QUnit.test('list without import button', function(assert) { } }); - assert.ok(!list.$buttons.find('.o_button_import').length, "should not have an Import button"); + assert.containsNone(list.$buttons, '.o_button_import', 'should not have an Import button'); list.destroy(); }); -QUnit.test('add import button in kanban', function(assert) { +QUnit.test('add import button in kanban', async function(assert) { assert.expect(2); - var kanban = createView({ + var kanban = await createView({ View: KanbanView, model: 'foo', data: this.data, @@ -74,21 +74,21 @@ QUnit.test('add import button in kanban', function(assert) { '', }); - assert.ok(kanban.$buttons.find('.o_button_import:contains(Import)').is(':visible'), + assert.isVisible(kanban.$buttons.find('.o_button_import:contains(Import)'), "should have a visible Import button"); testUtils.mock.intercept(kanban, 'do_action', function() { assert.ok(true, "should have triggered a do_action"); }); - testUtils.dom.click(kanban.$buttons.find('.o_button_import:contains(Import)')); + await testUtils.dom.click(kanban.$buttons.find('.o_button_import:contains(Import)')); kanban.destroy(); }); -QUnit.test('kanban without import button', function(assert) { +QUnit.test('kanban without import button', async function(assert) { assert.expect(1); - var kanban = createView({ + var kanban = await createView({ View: KanbanView, model: 'foo', data: this.data, @@ -102,7 +102,7 @@ QUnit.test('kanban without import button', function(assert) { } }); - assert.ok(!kanban.$buttons.find('.o_button_import').length, "should not have an Import button"); + assert.containsNone(kanban.$buttons, '.o_button_import', "should not have an Import button"); kanban.destroy(); }); diff --git a/addons/board/static/src/js/add_to_board_menu.js b/addons/board/static/src/js/add_to_board_menu.js index fb6b469d5ab..1b3ebc3fc5b 100644 --- a/addons/board/static/src/js/add_to_board_menu.js +++ b/addons/board/static/src/js/add_to_board_menu.js @@ -62,7 +62,7 @@ var AddToBoardMenu = Widget.extend({ * information. * * @private - * @returns {Deferred} + * @returns {Promise} */ _addToBoard: function () { var self = this; diff --git a/addons/board/static/src/js/board_view.js b/addons/board/static/src/js/board_view.js index bcfc892213d..953f5d73101 100644 --- a/addons/board/static/src/js/board_view.js +++ b/addons/board/static/src/js/board_view.js @@ -55,7 +55,7 @@ var BoardController = FormController.extend({ /** * Actually save a dashboard * - * @returns {Deferred} + * @returns {Promise} */ _saveDashboard: function () { var board = this.renderer.getBoard(); @@ -233,7 +233,7 @@ var BoardRenderer = FormRenderer.extend({ * @param {Object} params.context * @param {any[]} params.domain * @param {string} params.viewType - * @returns {Deferred} + * @returns {Promise} */ _createController: function (params) { var self = this; @@ -244,7 +244,7 @@ var BoardRenderer = FormRenderer.extend({ .then(function (action) { if (!action) { // the action does not exist anymore - return $.when(); + return Promise.resolve(); } var evalContext = new Context(params.context).eval(); if (evalContext.group_by && evalContext.group_by.length === 0) { diff --git a/addons/board/static/tests/dashboard_tests.js b/addons/board/static/tests/dashboard_tests.js index 19e3ec8c6bd..08ddf9d88e0 100644 --- a/addons/board/static/tests/dashboard_tests.js +++ b/addons/board/static/tests/dashboard_tests.js @@ -45,13 +45,13 @@ QUnit.module('Dashboard', { }], }, }; - } + }, }); -QUnit.test('dashboard basic rendering', function (assert) { +QUnit.test('dashboard basic rendering', async function (assert) { assert.expect(4); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -64,7 +64,7 @@ QUnit.test('dashboard basic rendering', function (assert) { form.destroy(); - form = createView({ + form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -84,10 +84,10 @@ QUnit.test('dashboard basic rendering', function (assert) { form.destroy(); }); -QUnit.test('display the no content helper', function (assert) { +QUnit.test('display the no content helper', async function (assert) { assert.expect(1); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -108,10 +108,10 @@ QUnit.test('display the no content helper', function (assert) { form.destroy(); }); -QUnit.test('basic functionality, with one sub action', function (assert) { +QUnit.test('basic functionality, with one sub action', async function (assert) { assert.expect(25); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -125,7 +125,7 @@ QUnit.test('basic functionality, with one sub action', function (assert) { mockRPC: function (route, args) { if (route === '/web/action/load') { assert.step('load action'); - return $.when({ + return Promise.resolve({ res_model: 'partner', views: [[4, 'list']], }); @@ -135,7 +135,7 @@ QUnit.test('basic functionality, with one sub action', function (assert) { } if (route === '/web/view/edit_custom') { assert.step('edit custom'); - return $.when(true); + return Promise.resolve(true); } return this._super.apply(this, arguments); }, @@ -158,24 +158,24 @@ QUnit.test('basic functionality, with one sub action', function (assert) { assert.ok(form.$('.oe_content').is(':visible'), "content is visible"); - testUtils.dom.click(form.$('.oe_fold')); + await testUtils.dom.click(form.$('.oe_fold')); assert.notOk(form.$('.oe_content').is(':visible'), "content is no longer visible"); - testUtils.dom.click(form.$('.oe_fold')); + await testUtils.dom.click(form.$('.oe_fold')); assert.ok(form.$('.oe_content').is(':visible'), "content is visible again"); assert.verifySteps(['load action', 'edit custom', 'edit custom']); assert.strictEqual($('.modal').length, 0, "should have no modal open"); - testUtils.dom.click(form.$('button.oe_dashboard_link_change_layout')); + await testUtils.dom.click(form.$('button.oe_dashboard_link_change_layout')); assert.strictEqual($('.modal').length, 1, "should have opened a modal"); assert.strictEqual($('.modal li[data-layout="2-1"] i.oe_dashboard_selected_layout').length, 1, "should mark currently selected layout"); - testUtils.dom.click($('.modal .oe_dashboard_layout_selector li[data-layout="1-1"]')); + await testUtils.dom.click($('.modal .oe_dashboard_layout_selector li[data-layout="1-1"]')); assert.strictEqual($('.modal').length, 0, "should have no modal open"); assert.containsOnce(form, 'table.oe_dashboard[data-layout="1-1"]', @@ -183,24 +183,24 @@ QUnit.test('basic functionality, with one sub action', function (assert) { assert.containsOnce(form, '.oe_action', "should have one displayed action"); - testUtils.dom.click(form.$('span.oe_close')); + await testUtils.dom.click(form.$('span.oe_close')); assert.strictEqual($('.modal').length, 1, "should have opened a modal"); // confirm the close operation - testUtils.dom.click($('.modal button.btn-primary')); + await testUtils.dom.click($('.modal button.btn-primary')); assert.strictEqual($('.modal').length, 0, "should have no modal open"); assert.containsNone(form, '.oe_action', "should have no displayed action"); - assert.verifySteps(['load action', 'edit custom', 'edit custom', 'edit custom', 'edit custom']); + assert.verifySteps(['edit custom', 'edit custom']); form.destroy(); }); -QUnit.test('views in the dashboard do not have a control panel', function (assert) { +QUnit.test('views in the dashboard do not have a control panel', async function (assert) { assert.expect(2); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -213,7 +213,7 @@ QUnit.test('views in the dashboard do not have a control panel', function (asser '', mockRPC: function (route) { if (route === '/web/action/load') { - return $.when({ + return Promise.resolve({ res_model: 'partner', views: [[4, 'list'], [5, 'form']], }); @@ -232,7 +232,7 @@ QUnit.test('views in the dashboard do not have a control panel', function (asser form.destroy(); }); -QUnit.test('can render an action without view_mode attribute', function (assert) { +QUnit.test('can render an action without view_mode attribute', async function (assert) { // The view_mode attribute is automatically set to the 'action' nodes when // the action is added to the dashboard using the 'Add to dashboard' button // in the searchview. However, other dashboard views can be written by hand @@ -242,7 +242,7 @@ QUnit.test('can render an action without view_mode attribute', function (assert) // first view of the action can be used, by default. assert.expect(3); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -259,10 +259,10 @@ QUnit.test('can render an action without view_mode attribute', function (assert) }, mockRPC: function (route, args) { if (route === '/board/static/src/img/layout_1-1-1.png') { - return $.when(); + return Promise.resolve(); } if (route === '/web/action/load') { - return $.when({ + return Promise.resolve({ context: '{"b": 2}', domain: '[["foo", "=", "yop"]]', res_model: 'partner', @@ -287,12 +287,12 @@ QUnit.test('can render an action without view_mode attribute', function (assert) form.destroy(); }); -QUnit.test('can sort a sub list', function (assert) { +QUnit.test('can sort a sub list', async function (assert) { assert.expect(2); this.data.partner.fields.foo.sortable = true; - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -305,7 +305,7 @@ QUnit.test('can sort a sub list', function (assert) { '', mockRPC: function (route) { if (route === '/web/action/load') { - return $.when({ + return Promise.resolve({ res_model: 'partner', views: [[4, 'list']], }); @@ -321,17 +321,17 @@ QUnit.test('can sort a sub list', function (assert) { assert.strictEqual($('tr.o_data_row').text(), 'yoplalalaabc', "should have correct initial data"); - testUtils.dom.click(form.$('th.o_column_sortable:contains(Foo)')); + await testUtils.dom.click(form.$('th.o_column_sortable:contains(Foo)')); assert.strictEqual($('tr.o_data_row').text(), 'abclalalayop', "data should have been sorted"); form.destroy(); }); -QUnit.test('can open a record', function (assert) { +QUnit.test('can open a record', async function (assert) { assert.expect(1); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -344,7 +344,7 @@ QUnit.test('can open a record', function (assert) { '', mockRPC: function (route) { if (route === '/web/action/load') { - return $.when({ + return Promise.resolve({ res_model: 'partner', views: [[4, 'list']], }); @@ -367,14 +367,14 @@ QUnit.test('can open a record', function (assert) { }, }); - testUtils.dom.click(form.$('tr.o_data_row td:contains(yop)')); + await testUtils.dom.click(form.$('tr.o_data_row td:contains(yop)')); form.destroy(); }); -QUnit.test('can open record using action form view', function (assert) { +QUnit.test('can open record using action form view', async function (assert) { assert.expect(1); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -387,7 +387,7 @@ QUnit.test('can open record using action form view', function (assert) { '', mockRPC: function (route) { if (route === '/web/action/load') { - return $.when({ + return Promise.resolve({ res_model: 'partner', views: [[4, 'list'], [5, 'form']], }); @@ -412,14 +412,14 @@ QUnit.test('can open record using action form view', function (assert) { }, }); - testUtils.dom.click(form.$('tr.o_data_row td:contains(yop)')); + await testUtils.dom.click(form.$('tr.o_data_row td:contains(yop)')); form.destroy(); }); -QUnit.test('can drag and drop a view', function (assert) { - assert.expect(4); +QUnit.test('can drag and drop a view', async function (assert) { + assert.expect(5); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -432,14 +432,14 @@ QUnit.test('can drag and drop a view', function (assert) { '', mockRPC: function (route) { if (route === '/web/action/load') { - return $.when({ + return Promise.resolve({ res_model: 'partner', views: [[4, 'list']], }); } if (route === '/web/view/edit_custom') { assert.step('edit custom'); - return $.when(true); + return Promise.resolve(true); } return this._super.apply(this, arguments); }, @@ -452,20 +452,21 @@ QUnit.test('can drag and drop a view', function (assert) { assert.containsOnce(form, 'td.index_0 .oe_action', "initial action is in column 0"); - testUtils.dom.dragAndDrop(form.$('.oe_dashboard_column.index_0 .oe_header'), + await testUtils.dom.dragAndDrop(form.$('.oe_dashboard_column.index_0 .oe_header'), form.$('.oe_dashboard_column.index_1')); assert.containsNone(form, 'td.index_0 .oe_action', "initial action is not in column 0"); assert.containsOnce(form, 'td.index_1 .oe_action', "initial action is in in column 1"); + assert.verifySteps(['edit custom']); form.destroy(); }); -QUnit.test('twice the same action in a dashboard', function (assert) { +QUnit.test('twice the same action in a dashboard', async function (assert) { assert.expect(2); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -479,14 +480,14 @@ QUnit.test('twice the same action in a dashboard', function (assert) { '', mockRPC: function (route) { if (route === '/web/action/load') { - return $.when({ + return Promise.resolve({ res_model: 'partner', views: [[4, 'list'],[5, 'kanban']], }); } if (route === '/web/view/edit_custom') { assert.step('edit custom'); - return $.when(true); + return Promise.resolve(true); } return this._super.apply(this, arguments); }, @@ -510,10 +511,10 @@ QUnit.test('twice the same action in a dashboard', function (assert) { form.destroy(); }); -QUnit.test('non-existing action in a dashboard', function (assert) { +QUnit.test('non-existing action in a dashboard', async function (assert) { assert.expect(1); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -531,11 +532,11 @@ QUnit.test('non-existing action in a dashboard', function (assert) { }, mockRPC: function (route) { if (route === '/board/static/src/img/layout_1-1-1.png') { - return $.when(); + return Promise.resolve(); } if (route === '/web/action/load') { // server answer if the action doesn't exist anymore - return $.when(false); + return Promise.resolve(false); } return this._super.apply(this, arguments); }, @@ -547,10 +548,10 @@ QUnit.test('non-existing action in a dashboard', function (assert) { form.destroy(); }); -QUnit.test('clicking on a kanban\'s button should trigger the action', function (assert) { +QUnit.test('clicking on a kanban\'s button should trigger the action', async function (assert) { assert.expect(2); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -582,24 +583,24 @@ QUnit.test('clicking on a kanban\'s button should trigger the action', function mockRPC: function (route) { if (route === '/board/static/src/img/layout_1-1-1.png') { - return $.when(); + return Promise.resolve(); } if (route === '/web/action/load') { - return $.when({res_model: 'partner', view_mode: 'kanban', views: [[false, 'kanban']]}); + return Promise.resolve({res_model: 'partner', view_mode: 'kanban', views: [[false, 'kanban']]}); } if (route === '/web/dataset/search_read') { - return $.when({records: [{foo: 'aqualung'}]}); + return Promise.resolve({records: [{foo: 'aqualung'}]}); } return this._super.apply(this, arguments); } }); - testUtils.dom.click(form.$('.o_kanban_test').find('button:first')); + await testUtils.dom.click(form.$('.o_kanban_test').find('button:first')); form.destroy(); }); -QUnit.test('subviews are aware of attach in or detach from the DOM', function (assert) { +QUnit.test('subviews are aware of attach in or detach from the DOM', async function (assert) { assert.expect(2); // patch list renderer `on_attach_callback` for the test only @@ -609,7 +610,7 @@ QUnit.test('subviews are aware of attach in or detach from the DOM', function (a } }); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -622,7 +623,7 @@ QUnit.test('subviews are aware of attach in or detach from the DOM', function (a '', mockRPC: function (route) { if (route === '/web/action/load') { - return $.when({ + return Promise.resolve({ res_model: 'partner', views: [[4, 'list']], }); @@ -643,7 +644,7 @@ QUnit.test('subviews are aware of attach in or detach from the DOM', function (a form.destroy(); }); -QUnit.test('dashboard intercepts custom events triggered by sub controllers', function (assert) { +QUnit.test('dashboard intercepts custom events triggered by sub controllers', async function (assert) { assert.expect(1); // we patch the ListController to force it to trigger the custom events that @@ -654,7 +655,7 @@ QUnit.test('dashboard intercepts custom events triggered by sub controllers', fu }, }); - var board = createView({ + var board = await createView({ View: BoardView, model: 'board', data: this.data, @@ -667,7 +668,7 @@ QUnit.test('dashboard intercepts custom events triggered by sub controllers', fu '', mockRPC: function (route) { if (route === '/web/action/load') { - return $.when({res_model: 'partner', views: [[false, 'list']]}); + return Promise.resolve({res_model: 'partner', views: [[false, 'list']]}); } return this._super.apply(this, arguments); }, @@ -685,10 +686,10 @@ QUnit.test('dashboard intercepts custom events triggered by sub controllers', fu board.destroy(); }); -QUnit.test('save actions to dashboard', function (assert) { +QUnit.test('save actions to dashboard', async function (assert) { assert.expect(3); - var actionManager = createActionManager({ + var actionManager = await createActionManager({ data: this.data, archs: { 'partner,false,list': '', @@ -700,13 +701,13 @@ QUnit.test('save actions to dashboard', function (assert) { "should save the correct action"); assert.strictEqual(args.view_mode, 'list', "should save the correct view type"); - return $.when(true); + return Promise.resolve(true); } return this._super.apply(this, arguments); }, }); - actionManager.doAction({ + await actionManager.doAction({ id: 1, res_model: 'partner', type: 'ir.actions.act_window', @@ -717,19 +718,19 @@ QUnit.test('save actions to dashboard', function (assert) { "should display the list view"); // add this action to dashboard - testUtils.dom.click($('.o_search_options .o_dropdown button:contains(Favorites)')); - testUtils.dom.click($('.o_add_to_board.o_menu_header')); + await testUtils.dom.click($('.o_search_options .o_dropdown button:contains(Favorites)')); + await testUtils.dom.click($('.o_add_to_board.o_menu_header')); testUtils.fields.editInput($('input.o_add_to_board_input'), 'a name'); - testUtils.dom.click($('.o_add_to_board_confirm_button')); + await testUtils.dom.click($('.o_add_to_board_confirm_button')); actionManager.destroy(); }); -QUnit.test('save two searches to dashboard', function (assert) { +QUnit.test('save two searches to dashboard', async function (assert) { // the second search saved should not be influenced by the first assert.expect(2); - var actionManager = createActionManager({ + var actionManager = await createActionManager({ data: this.data, archs: { 'partner,false,list': '', @@ -747,13 +748,13 @@ QUnit.test('save two searches to dashboard', function (assert) { } filter_count += 1; - return $.when(true); + return Promise.resolve(true); } return this._super.apply(this, arguments); }, }); - actionManager.doAction({ + await actionManager.doAction({ id: 1, res_model: 'partner', type: 'ir.actions.act_window', @@ -762,33 +763,33 @@ QUnit.test('save two searches to dashboard', function (assert) { var filter_count = 0; // Add a first filter - testUtils.dom.click(actionManager.$('.o_filters_menu_button')); - testUtils.dom.click(actionManager.$('.o_add_custom_filter')); + await testUtils.dom.click(actionManager.$('.o_filters_menu_button')); + await testUtils.dom.click(actionManager.$('.o_add_custom_filter')); actionManager.$('.o_searchview_extended_prop_value .o_input').val('a'); - testUtils.dom.click(actionManager.$('.o_apply_filter')); + await testUtils.dom.click(actionManager.$('.o_apply_filter')); // Add it to dashboard - testUtils.dom.click(actionManager.$('.o_favorites_menu_button')); - testUtils.dom.click(actionManager.$('.o_add_to_board')); - testUtils.dom.click(actionManager.$('.o_add_to_board_confirm_button')); + await testUtils.dom.click(actionManager.$('.o_favorites_menu_button')); + await testUtils.dom.click(actionManager.$('.o_add_to_board')); + await testUtils.dom.click(actionManager.$('.o_add_to_board_confirm_button')); // Remove it - testUtils.dom.click(actionManager.$('.o_facet_remove')); + await testUtils.dom.click(actionManager.$('.o_facet_remove')); // Add the second filter - testUtils.dom.click(actionManager.$('.o_filters_menu_button')); - testUtils.dom.click(actionManager.$('.o_add_custom_filter')); + await testUtils.dom.click(actionManager.$('.o_filters_menu_button')); + await testUtils.dom.click(actionManager.$('.o_add_custom_filter')); actionManager.$('.o_searchview_extended_prop_value .o_input').val('b'); - testUtils.dom.click(actionManager.$('.o_apply_filter')); + await testUtils.dom.click(actionManager.$('.o_apply_filter')); // Add it to dashboard - testUtils.dom.click(actionManager.$('.o_favorites_menu_button')); - testUtils.dom.click(actionManager.$('.o_add_to_board')); - testUtils.dom.click(actionManager.$('.o_add_to_board_confirm_button')); + await testUtils.dom.click(actionManager.$('.o_favorites_menu_button')); + await testUtils.dom.click(actionManager.$('.o_add_to_board')); + await testUtils.dom.click(actionManager.$('.o_add_to_board_confirm_button')); actionManager.destroy(); }); -QUnit.test("Views should be loaded in the user's language", function (assert) { +QUnit.test("Views should be loaded in the user's language", async function (assert) { assert.expect(2); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -810,7 +811,7 @@ QUnit.test("Views should be loaded in the user's language", function (assert) { 'The data should be loaded with the correct context'); } if (route === '/web/action/load') { - return $.when({ + return Promise.resolve({ res_model: 'partner', views: [[4, 'list']], }); @@ -826,9 +827,9 @@ QUnit.test("Views should be loaded in the user's language", function (assert) { form.destroy(); }); -QUnit.test("Dashboard should use correct groupby", function (assert) { +QUnit.test("Dashboard should use correct groupby", async function (assert) { assert.expect(1); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -845,7 +846,7 @@ QUnit.test("Dashboard should use correct groupby", function (assert) { 'user defined groupby should have precedence on action groupby'); } if (route === '/web/action/load') { - return $.when({ + return Promise.resolve({ res_model: 'partner', context: { group_by: 'some_field', @@ -864,10 +865,10 @@ QUnit.test("Dashboard should use correct groupby", function (assert) { form.destroy(); }); -QUnit.test('click on a cell of pivot view inside dashboard', function (assert) { +QUnit.test('click on a cell of pivot view inside dashboard', async function (assert) { assert.expect(3); - var form = createView({ + var form = await createView({ View: BoardView, model: 'board', data: this.data, @@ -880,7 +881,7 @@ QUnit.test('click on a cell of pivot view inside dashboard', function (assert) { '', mockRPC: function (route) { if (route === '/web/action/load') { - return $.when({ + return Promise.resolve({ res_model: 'partner', views: [[4, 'pivot']], }); @@ -899,7 +900,7 @@ QUnit.test('click on a cell of pivot view inside dashboard', function (assert) { assert.verifySteps([]); - testUtils.dom.click(form.$('.o_pivot .o_pivot_cell_value')); + await testUtils.dom.click(form.$('.o_pivot .o_pivot_cell_value')); assert.verifySteps(['do action']); diff --git a/addons/bus/static/src/js/longpolling_bus.js b/addons/bus/static/src/js/longpolling_bus.js index 83f2b0676ef..118e13251ae 100644 --- a/addons/bus/static/src/js/longpolling_bus.js +++ b/addons/bus/static/src/js/longpolling_bus.js @@ -187,11 +187,11 @@ var LongpollingBus = Bus.extend(ServicesMixin, { self._pollRpc = false; self._onPoll(result); self._poll(); - }, function (error, ev) { + }).guardedCatch(function (result) { self._pollRpc = false; // no error popup if request is interrupted or fails for any reason - ev.preventDefault(); - if (error && error.message === "XmlHttpRequestError abort") { + result.event.preventDefault(); + if (result.message && result.message.message === "XmlHttpRequestError abort") { self._poll(); } else { // random delay to avoid massive longpolling diff --git a/addons/bus/static/tests/bus_tests.js b/addons/bus/static/tests/bus_tests.js index e58fd22e8a8..551a145bda9 100644 --- a/addons/bus/static/tests/bus_tests.js +++ b/addons/bus/static/tests/bus_tests.js @@ -21,10 +21,10 @@ QUnit.module('Bus', { LocalStorageServiceMock = AbstractStorageService.extend({storage: new RamStorage()}); }, }, function () { - QUnit.test('notifications received from the longpolling channel', function (assert) { + QUnit.test('notifications received from the longpolling channel', async function (assert) { assert.expect(6); - var pollDeferred = $.Deferred(); + var pollPromise = testUtils.makeTestPromise(); var parent = new Widget(); testUtils.mock.addMockEnvironment(parent, { @@ -35,49 +35,52 @@ QUnit.module('Bus', { }, mockRPC: function (route, args) { if (route === '/longpolling/poll') { - assert.step([route, args.channels.join(',')]); + assert.step(route + ' - ' + args.channels.join(',')); - pollDeferred = $.Deferred(); - pollDeferred.abort = (function () { + pollPromise = testUtils.makeTestPromise(); + pollPromise.abort = (function () { this.reject({message: "XmlHttpRequestError abort"}, $.Event()); - }).bind(pollDeferred); - return pollDeferred; + }).bind(pollPromise); + return pollPromise; } return this._super.apply(this, arguments); } }); var widget = new Widget(parent); - widget.appendTo($('#qunit-fixture')); + await widget.appendTo($('#qunit-fixture')); widget.call('bus_service', 'onNotification', this, function (notifications) { - assert.step(['notification', notifications]); + assert.step('notification - ' + notifications.toString()); }); widget.call('bus_service', 'addChannel', 'lambda'); - pollDeferred.resolve([{ + pollPromise.resolve([{ id: 1, channel: 'lambda', message: 'beta', }]); - pollDeferred.resolve([{ + await testUtils.nextTick(); + + pollPromise.resolve([{ id: 2, channel: 'lambda', message: 'epsilon', }]); + await testUtils.nextTick(); assert.verifySteps([ - ["/longpolling/poll", "lambda"], - ["notification", [["lambda","beta"]]], - ["/longpolling/poll", "lambda"], - ["notification", [["lambda","epsilon"]]], - ["/longpolling/poll", "lambda"] + '/longpolling/poll - lambda', + 'notification - lambda,beta', + '/longpolling/poll - lambda', + 'notification - lambda,epsilon', + '/longpolling/poll - lambda', ]); parent.destroy(); }); - QUnit.test('provide notification ID of 0 by default', function (assert) { + QUnit.test('provide notification ID of 0 by default', async function (assert) { // This test is important in order to ensure that we provide the correct // sentinel value 0 when we are not aware of the last notification ID // that we have received. We cannot provide an ID of -1, otherwise it @@ -96,7 +99,7 @@ QUnit.module('Bus', { }, }); - var pollDeferred = $.Deferred(); + var pollPromise = testUtils.makeTestPromise(); var parent = new Widget(); testUtils.mock.addMockEnvironment(parent, { data: {}, @@ -110,18 +113,18 @@ QUnit.module('Bus', { assert.strictEqual(args.last, 0, "provided last notification ID should be 0"); - pollDeferred = $.Deferred(); - pollDeferred.abort = (function () { + pollPromise = testUtils.makeTestPromise(); + pollPromise.abort = (function () { this.reject({message: "XmlHttpRequestError abort"}, $.Event()); - }).bind(pollDeferred); - return pollDeferred; + }).bind(pollPromise); + return pollPromise; } return this._super.apply(this, arguments); } }); var widget = new Widget(parent); - widget.appendTo($('#qunit-fixture')); + await widget.appendTo($('#qunit-fixture')); // trigger longpolling poll RPC widget.call('bus_service', 'addChannel', 'lambda'); @@ -131,13 +134,12 @@ QUnit.module('Bus', { parent.destroy(); }); - QUnit.test('cross tab bus share message from a channel', function (assert) { - var done = assert.async(); + QUnit.test('cross tab bus share message from a channel', async function (assert) { assert.expect(5); // master - var pollDeferredMaster = $.Deferred(); + var pollPromiseMaster = testUtils.makeTestPromise(); var parentMaster = new Widget(); testUtils.mock.addMockEnvironment(parentMaster, { @@ -148,79 +150,74 @@ QUnit.module('Bus', { }, mockRPC: function (route, args) { if (route === '/longpolling/poll') { - assert.step(['master', route, args.channels.join(',')]); + assert.step('master' + ' - ' + route + ' - ' + args.channels.join(',')); - pollDeferredMaster = $.Deferred(); - pollDeferredMaster.abort = (function () { + pollPromiseMaster = testUtils.makeTestPromise(); + pollPromiseMaster.abort = (function () { this.reject({message: "XmlHttpRequestError abort"}, $.Event()); - }).bind(pollDeferredMaster); - return pollDeferredMaster; + }).bind(pollPromiseMaster); + return pollPromiseMaster; } return this._super.apply(this, arguments); } }); var master = new Widget(parentMaster); - master.appendTo($('#qunit-fixture')); + await master.appendTo($('#qunit-fixture')); master.call('bus_service', 'onNotification', master, function (notifications) { - assert.step(['master', 'notification', notifications]); + assert.step('master - notification - ' + notifications.toString()); }); master.call('bus_service', 'addChannel', 'lambda'); // slave - - setTimeout(function () { - var parentSlave = new Widget(); - testUtils.mock.addMockEnvironment(parentSlave, { - data: {}, - services: { - bus_service: BusService, - local_storage: LocalStorageServiceMock, - }, - mockRPC: function (route, args) { - if (route === '/longpolling/poll') { - throw new Error("Can not use the longpolling of the slave client"); - } - return this._super.apply(this, arguments); + await testUtils.nextTick(); + var parentSlave = new Widget(); + testUtils.mock.addMockEnvironment(parentSlave, { + data: {}, + services: { + bus_service: BusService, + local_storage: LocalStorageServiceMock, + }, + mockRPC: function (route, args) { + if (route === '/longpolling/poll') { + throw new Error("Can not use the longpolling of the slave client"); } - }); + return this._super.apply(this, arguments); + } + }); - var slave = new Widget(parentSlave); - slave.appendTo($('#qunit-fixture')); + var slave = new Widget(parentSlave); + await slave.appendTo($('#qunit-fixture')); - slave.call('bus_service', 'onNotification', slave, function (notifications) { - assert.step(['slave', 'notification', notifications]); - }); - slave.call('bus_service', 'addChannel', 'lambda'); + slave.call('bus_service', 'onNotification', slave, function (notifications) { + assert.step('slave - notification - ' + notifications.toString()); + }); + slave.call('bus_service', 'addChannel', 'lambda'); - pollDeferredMaster.resolve([{ - id: 1, - channel: 'lambda', - message: 'beta', - }]); + pollPromiseMaster.resolve([{ + id: 1, + channel: 'lambda', + message: 'beta', + }]); + await testUtils.nextTick(); - assert.verifySteps([ - ["master", "/longpolling/poll", "lambda"], - ["master", "notification", [["lambda", "beta"]]], - ["slave", "notification", [["lambda", "beta"]]], - ["master", "/longpolling/poll", "lambda"], - ]); + assert.verifySteps([ + 'master - /longpolling/poll - lambda', + 'master - notification - lambda,beta', + 'slave - notification - lambda,beta', + 'master - /longpolling/poll - lambda', + ]); - parentMaster.destroy(); - parentSlave.destroy(); - - done(); - - }, 3); + parentMaster.destroy(); + parentSlave.destroy(); }); - QUnit.test('cross tab bus elect new master on master unload', function (assert) { - var done = assert.async(); + QUnit.test('cross tab bus elect new master on master unload', async function (assert) { assert.expect(8); // master - var pollDeferredMaster = $.Deferred(); + var pollPromiseMaster = testUtils.makeTestPromise(); var parentMaster = new Widget(); testUtils.mock.addMockEnvironment(parentMaster, { @@ -231,89 +228,87 @@ QUnit.module('Bus', { }, mockRPC: function (route, args) { if (route === '/longpolling/poll') { - assert.step(['master', route, args.channels.join(',')]); + assert.step('master - ' + route + ' - ' + args.channels.join(',')); - pollDeferredMaster = $.Deferred(); - pollDeferredMaster.abort = (function () { + pollPromiseMaster = testUtils.makeTestPromise(); + pollPromiseMaster.abort = (function () { this.reject({message: "XmlHttpRequestError abort"}, $.Event()); - }).bind(pollDeferredMaster); - return pollDeferredMaster; + }).bind(pollPromiseMaster); + return pollPromiseMaster; } return this._super.apply(this, arguments); } }); var master = new Widget(parentMaster); - master.appendTo($('#qunit-fixture')); + await master.appendTo($('#qunit-fixture')); master.call('bus_service', 'onNotification', master, function (notifications) { - assert.step(['master', 'notification', notifications]); + assert.step('master - notification - ' + notifications.toString()); }); master.call('bus_service', 'addChannel', 'lambda'); // slave - setTimeout(function () { - var parentSlave = new Widget(); - var pollDeferredSlave = $.Deferred(); - testUtils.mock.addMockEnvironment(parentSlave, { - data: {}, - services: { - bus_service: BusService, - local_storage: LocalStorageServiceMock, - }, - mockRPC: function (route, args) { - if (route === '/longpolling/poll') { - assert.step(['slave', route, args.channels.join(',')]); + await testUtils.nextTick(); + var parentSlave = new Widget(); + var pollPromiseSlave = testUtils.makeTestPromise(); + testUtils.mock.addMockEnvironment(parentSlave, { + data: {}, + services: { + bus_service: BusService, + local_storage: LocalStorageServiceMock, + }, + mockRPC: function (route, args) { + if (route === '/longpolling/poll') { + assert.step('slave - ' + route + ' - ' + args.channels.join(',')); - pollDeferredSlave = $.Deferred(); - pollDeferredSlave.abort = (function () { - this.reject({message: "XmlHttpRequestError abort"}, $.Event()); - }).bind(pollDeferredSlave); - return pollDeferredSlave; - } - return this._super.apply(this, arguments); + pollPromiseSlave = testUtils.makeTestPromise(); + pollPromiseSlave.abort = (function () { + this.reject({message: "XmlHttpRequestError abort"}, $.Event()); + }).bind(pollPromiseSlave); + return pollPromiseSlave; } - }); + return this._super.apply(this, arguments); + } + }); - var slave = new Widget(parentSlave); - slave.appendTo($('#qunit-fixture')); + var slave = new Widget(parentSlave); + await slave.appendTo($('#qunit-fixture')); - slave.call('bus_service', 'onNotification', slave, function (notifications) { - assert.step(['slave', 'notification', notifications]); - }); - slave.call('bus_service', 'addChannel', 'lambda'); + slave.call('bus_service', 'onNotification', slave, function (notifications) { + assert.step('slave - notification - ' + notifications.toString()); + }); + slave.call('bus_service', 'addChannel', 'lambda'); - pollDeferredMaster.resolve([{ - id: 1, - channel: 'lambda', - message: 'beta', - }]); + pollPromiseMaster.resolve([{ + id: 1, + channel: 'lambda', + message: 'beta', + }]); + await testUtils.nextTick(); - // simulate unloading master - master.call('bus_service', '_onUnload'); + // simulate unloading master + master.call('bus_service', '_onUnload'); - pollDeferredSlave.resolve([{ - id: 2, - channel: 'lambda', - message: 'gamma', - }]); + pollPromiseSlave.resolve([{ + id: 2, + channel: 'lambda', + message: 'gamma', + }]); + await testUtils.nextTick(); - assert.verifySteps([ - ["master", "/longpolling/poll", "lambda"], - ["master", "notification", [["lambda", "beta"]]], - ["slave", "notification", [["lambda", "beta"]]], - ["master", "/longpolling/poll", "lambda"], - ["slave", "/longpolling/poll", "lambda"], - ["slave", "notification", [["lambda", "gamma"]]], - ["slave", "/longpolling/poll", "lambda"], - ]); + assert.verifySteps([ + 'master - /longpolling/poll - lambda', + 'master - notification - lambda,beta', + 'slave - notification - lambda,beta', + 'master - /longpolling/poll - lambda', + 'slave - /longpolling/poll - lambda', + 'slave - notification - lambda,gamma', + 'slave - /longpolling/poll - lambda', + ]); - parentMaster.destroy(); - parentSlave.destroy(); - - done(); - - }, 3); + parentMaster.destroy(); + parentSlave.destroy(); }); - -});}); +}); +}); diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js index 10ecc60cdeb..c9f7b14babc 100644 --- a/addons/calendar/static/src/js/base_calendar.js +++ b/addons/calendar/static/src/js/base_calendar.js @@ -40,10 +40,8 @@ var CalendarNotification = Notification.extend({ }, 'click .link2showed': function() { - var self = this; - this._rpc({route: '/calendar/notify_ack'}).always(function() { - self.destroy(); - }); + this._rpc({route: '/calendar/notify_ack'}) + .then(this.destroy.bind(this), this.destroy.bind(this)); }, }); }, @@ -84,8 +82,10 @@ WebClient.include({ }, get_next_calendar_notif: function() { session.rpc("/calendar/notify", {}, {shadow: true}) - .done(this.display_calendar_notif.bind(this)) - .fail(function(err, ev) { + .then(this.display_calendar_notif.bind(this)) + .guardedCatch(function(reason) { // + var err = reason.message; + var ev = reason.event; if(err.code === -32098) { // Prevent the CrashManager to display an error // in case of an xhr error not due to a server error @@ -114,7 +114,7 @@ BasicModel.include({ * @private * @param {Object} record * @param {string} fieldName - * @returns {Deferred} + * @returns {Promise} */ _fetchSpecialAttendeeStatus: function (record, fieldName) { var context = record.getContext({fieldName: fieldName}); diff --git a/addons/calendar/static/tests/calendar_tests.js b/addons/calendar/static/tests/calendar_tests.js index fd00ed3074c..3eea3dff08b 100644 --- a/addons/calendar/static/tests/calendar_tests.js +++ b/addons/calendar/static/tests/calendar_tests.js @@ -33,10 +33,10 @@ QUnit.module('calendar', { }; }, }, function () { - QUnit.test("many2manyattendee widget: basic rendering", function (assert) { + QUnit.test("many2manyattendee widget: basic rendering", async function (assert) { assert.expect(9); - var form = createView({ + var form = await createView({ View: FormView, model: 'event', data: this.data, @@ -53,7 +53,7 @@ QUnit.module('calendar', { "the partner ids should be passed as argument"); assert.strictEqual(args.args[1], 14, "the event id should be passed as argument"); - return $.when([ + return Promise.resolve([ [1, "Jesus", "accepted", 0], [2, "Mahomet", "needsAction", 0], ]); diff --git a/addons/calendar/static/tests/systray_activity_menu_tests.js b/addons/calendar/static/tests/systray_activity_menu_tests.js index ac98f5751c9..d53b25105fe 100644 --- a/addons/calendar/static/tests/systray_activity_menu_tests.js +++ b/addons/calendar/static/tests/systray_activity_menu_tests.js @@ -37,7 +37,7 @@ QUnit.module('ActivityMenu', { } }); -QUnit.test('activity menu widget:today meetings', function (assert) { +QUnit.test('activity menu widget:today meetings', async function (assert) { assert.expect(8); var self = this; var activityMenu = new ActivityMenu(); @@ -45,13 +45,13 @@ QUnit.test('activity menu widget:today meetings', function (assert) { services: this.services, mockRPC: function (route, args) { if (args.method === 'systray_get_activities') { - return $.when(self.data['calendar.event']['records']); + return Promise.resolve(self.data['calendar.event']['records']); } return this._super(route, args); }, }); - activityMenu.appendTo($('#qunit-fixture')); + await activityMenu.appendTo($('#qunit-fixture')); assert.hasClass(activityMenu.$el,'o_mail_systray_item', 'should be the instance of widget'); @@ -59,13 +59,13 @@ QUnit.test('activity menu widget:today meetings', function (assert) { assert.ok(true, "should have triggered a do_action"); assert.strictEqual(event.data.action.res_id, activityMenu.$('.o_meeting_filter').eq(0).data('res_id'), 'should open particular meeting form view'); }); - testUtils.dom.click(activityMenu.$('.dropdown-toggle')); - testUtils.dom.click(activityMenu.$('.o_meeting_filter').eq(0)); + await testUtils.dom.click(activityMenu.$('.dropdown-toggle')); + await testUtils.dom.click(activityMenu.$('.o_meeting_filter').eq(0)); testUtils.mock.intercept(activityMenu, 'do_action', function (event) { assert.strictEqual(event.data.action, "calendar.action_calendar_event", 'should open meeting calendar view in day mode'); }); - testUtils.dom.click(activityMenu.$('.o_mail_preview')); + await testUtils.dom.click(activityMenu.$('.o_mail_preview')); assert.ok(activityMenu.$('.o_meeting_filter'), "should be a meeting"); assert.containsN(activityMenu, '.o_meeting_filter', 2, 'there should be 2 meetings'); diff --git a/addons/crm/static/tests/sales_team_dashboard_tests.js b/addons/crm/static/tests/sales_team_dashboard_tests.js index c28cf8f4157..7cb352a30ae 100644 --- a/addons/crm/static/tests/sales_team_dashboard_tests.js +++ b/addons/crm/static/tests/sales_team_dashboard_tests.js @@ -52,7 +52,7 @@ QUnit.test('dashboard basic rendering', function (assert) { mockRPC: function (route, args) { if (args.method === 'retrieve_sales_dashboard') { assert.ok(true, "should call /retrieve_sales_dashboard"); - return $.when(dashboard_data); + return Promise.resolve(dashboard_data); } return this._super(route, args); }, @@ -79,12 +79,12 @@ QUnit.test('dashboard set a new target', function (assert) { if (args.method === 'retrieve_sales_dashboard') { // should be called twice: for the first rendering, and after the target update assert.ok(true, "should call /retrieve_sales_dashboard"); - return $.when(dashboard_data); + return Promise.resolve(dashboard_data); } if (args.method === 'modify_target_sales_dashboard') { assert.ok(true, "should call /modify_target_sales_dashboard"); dashboard_data[args.args[0]].target = args.args[1]; - return $.when(); + return Promise.resolve(); } return this._super(route, args); }, @@ -117,7 +117,7 @@ QUnit.test('dashboard: click on a button to execute an action', function (assert '', mockRPC: function (route, args) { if (args.method === 'retrieve_sales_dashboard') { - return $.when(dashboard_data); + return Promise.resolve(dashboard_data); } return this._super(route, args); }, @@ -151,7 +151,7 @@ QUnit.test('dashboard should be displayed even if there is no content', function domain: [['id', '=', 239]], // no record will match this domain mockRPC: function (route, args) { if (args.method === 'retrieve_sales_dashboard') { - return $.when(dashboardData); + return Promise.resolve(dashboardData); } return this._super(route, args); }, diff --git a/addons/crm_iap_lead/static/src/js/leads_tree_generate_leads.js b/addons/crm_iap_lead/static/src/js/leads_tree_generate_leads.js index 27059c28893..e60d8deb9a9 100644 --- a/addons/crm_iap_lead/static/src/js/leads_tree_generate_leads.js +++ b/addons/crm_iap_lead/static/src/js/leads_tree_generate_leads.js @@ -34,7 +34,7 @@ odoo.define('crm.leads.tree', function (require) { self.buttons_template = 'LeadMiningRequestListView.buttons'; } }); - return $.when(this._super.apply(this, arguments), ready); + return Promise.all([this._super.apply(this, arguments), ready]); }, renderButtons: function () { this._super.apply(this, arguments); @@ -57,7 +57,7 @@ odoo.define('crm.leads.tree', function (require) { self.buttons_template = 'LeadMiningRequestKanbanView.buttons'; } }); - return $.when(this._super.apply(this, arguments), ready); + return Promise.all([this._super.apply(this, arguments), ready]); }, renderButtons: function () { this._super.apply(this, arguments); diff --git a/addons/google_calendar/static/src/js/google_calendar.js b/addons/google_calendar/static/src/js/google_calendar.js index 2759d44f985..ad4714f05f0 100644 --- a/addons/google_calendar/static/src/js/google_calendar.js +++ b/addons/google_calendar/static/src/js/google_calendar.js @@ -74,7 +74,7 @@ CalendarController.include({ local_context: context, }, }) - .done(function(o) { + .then(function(o) { if (o.status === "OK") { Dialog.alert(self, _t("All events have been disconnected from your previous account. You can now restart the synchronization"), { title: _t('Event disconnection success'), @@ -89,9 +89,7 @@ CalendarController.include({ title: _t('Accounts'), }); } - }).always(function () { - event.data.on_always(); - }); + }).then(event.data.on_always, event.data.on_always); } }); diff --git a/addons/google_calendar/static/tests/google_calendar_tests.js b/addons/google_calendar/static/tests/google_calendar_tests.js index 704452771be..76aaa65411b 100644 --- a/addons/google_calendar/static/tests/google_calendar_tests.js +++ b/addons/google_calendar/static/tests/google_calendar_tests.js @@ -4,7 +4,7 @@ odoo.define('google_calendar.calendar_tests', function (require) { var CalendarView = require('web.CalendarView'); var testUtils = require('web.test_utils'); -var createAsyncView = testUtils.createAsyncView; +var createCalendarView = testUtils.createCalendarView; var initialDate = new Date("2016-12-12T08:00:00Z"); @@ -27,11 +27,11 @@ QUnit.module('Google Calendar', { type: {string: "type", type: "integer"}, }, records: [ - {id: 5, user_id: 4, partner_id: 4, name: "event 1", start: "2016-12-13 15:55:05", stop: "2016-12-15 18:55:05", allday: false, partner_ids: [], type: 2}, - {id: 6, user_id: 4, partner_id: 4, name: "event 2", start: "2016-12-18 08:00:00", stop: "2016-12-18 09:00:00", allday: false, partner_ids: [], type: 3} + {id: 5, user_id: 4, partner_id: 4, name: "event 1", start: "2016-12-13 15:55:05", stop: "2016-12-15 18:55:05", allday: false, partner_ids: [], type: 2}, + {id: 6, user_id: 4, partner_id: 4, name: "event 2", start: "2016-12-18 08:00:00", stop: "2016-12-18 09:00:00", allday: false, partner_ids: [], type: 3} ], check_access_rights: function () { - return $.when(true); + return Promise.resolve(true); } }, user: { @@ -69,11 +69,10 @@ QUnit.module('Google Calendar', { } }, function () { - QUnit.test('sync google calendar', function (assert) { + QUnit.test('sync google calendar', async function (assert) { assert.expect(6); - var done = assert.async(); - createAsyncView({ + var calendar = await createCalendarView({ View: CalendarView, model: 'calendar.event', data: this.data, @@ -93,30 +92,27 @@ QUnit.module('Google Calendar', { this.data['calendar.event'].records.push( {id: 7, user_id: 4, partner_id: 4, name: "event from google calendar", start: "2016-12-28 15:55:05", stop: "2016-12-29 18:55:05", allday: false, partner_ids: [], type: 2} ); - return $.when({status: 'need_refresh'}); + return Promise.resolve({status: 'need_refresh'}); } else if (route === '/web/dataset/call_kw/calendar.event/search_read') { assert.step(route); } return this._super.apply(this, arguments); }, - }).then(function (calendar) { - assert.containsN(calendar, '.fc-event', 2, "should display 2 events on the month"); - - var $sidebar = calendar.$('.o_calendar_sidebar'); - - testUtils.dom.click(calendar.$('.o_google_sync_button')); - - assert.verifySteps([ - '/web/dataset/call_kw/calendar.event/search_read', - '/google_calendar/sync_data', - '/web/dataset/call_kw/calendar.event/search_read', - ], 'should do a search_read before and after the call to sync_data'); - - assert.containsN(calendar, '.fc-event', 3, "should now display 3 events on the month"); - - calendar.destroy(); - done(); }); + + assert.containsN(calendar, '.fc-event', 2, "should display 2 events on the month"); + + await testUtils.dom.click(calendar.$('.o_google_sync_button')); + + assert.verifySteps([ + '/web/dataset/call_kw/calendar.event/search_read', + '/google_calendar/sync_data', + '/web/dataset/call_kw/calendar.event/search_read', + ], 'should do a search_read before and after the call to sync_data'); + + assert.containsN(calendar, '.fc-event', 3, "should now display 3 events on the month"); + + calendar.destroy(); }); }); }); diff --git a/addons/google_drive/static/src/js/gdrive.js b/addons/google_drive/static/src/js/gdrive.js index 5855e056161..7ed0487e84d 100644 --- a/addons/google_drive/static/src/js/gdrive.js +++ b/addons/google_drive/static/src/js/gdrive.js @@ -19,7 +19,7 @@ Sidebar.include({ if (this.options.viewType === "form") { def = this._addGoogleDocItems(this.env.model, this.env.activeIds[0]); } - return $.when(def).then(this._super.bind(this)); + return Promise.resolve(def).then(this._super.bind(this)); }, //-------------------------------------------------------------------------- @@ -30,12 +30,12 @@ Sidebar.include({ * @private * @param {string} model * @param {integer} resID - * @returns {Deferred} + * @returns {Promise} */ _addGoogleDocItems: function (model, resID) { var self = this; if (!resID) { - return $.when(); + return Promise.resolve(); } var gdoc_item = _.indexOf(_.pluck(self.items.other, 'classname'), 'oe_share_gdoc'); if (gdoc_item !== -1) { diff --git a/addons/google_drive/static/tests/gdrive_test.js b/addons/google_drive/static/tests/gdrive_test.js index 1e96911de09..158330376bd 100644 --- a/addons/google_drive/static/tests/gdrive_test.js +++ b/addons/google_drive/static/tests/gdrive_test.js @@ -15,7 +15,7 @@ var createView = testUtils.createView; var _addGoogleDocItemsOriginal = GoogleDriveSideBar.prototype._addGoogleDocItems; var _addGoogleDocItemsMocked = function (model, resID) { - return $.when(); + return Promise.resolve(); }; GoogleDriveSideBar.prototype._addGoogleDocItems = _addGoogleDocItemsMocked; @@ -67,10 +67,10 @@ QUnit.module('gdrive_integration', { }, function () { QUnit.module('Google Drive Sidebar'); - QUnit.test('rendering of the google drive attachments in Sidebar', function (assert) { + QUnit.test('rendering of the google drive attachments in Sidebar', async function (assert) { assert.expect(3); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -83,10 +83,10 @@ QUnit.module('gdrive_integration', { if (route === '/web/dataset/call_kw/google.drive.config/get_google_drive_config') { assert.deepEqual(args.args, ['partner', 1], 'The route to get google drive config should have been called'); - return $.when([{id: 27, name: 'Cyberdyne Systems'}]); + return Promise.resolve([{id: 27, name: 'Cyberdyne Systems'}]); } if (route === '/web/dataset/call_kw/google.drive.config/search_read'){ - return $.when([{google_drive_resource_id: "T1000", + return Promise.resolve([{google_drive_resource_id: "T1000", google_drive_client_id: "cyberdyne.org", id: 1}]); } @@ -94,7 +94,7 @@ QUnit.module('gdrive_integration', { assert.deepEqual(args.args, [27, 1, 'T1000'], 'The route to get the Google url should have been called'); // We don't return anything useful, otherwise it will open a new tab - return $.when(); + return Promise.resolve(); } return this._super.apply(this, arguments); }, @@ -106,18 +106,17 @@ QUnit.module('gdrive_integration', { 'The button to the google action should be present'); // click on gdrive sidebar item - testUtils.dom.click(form.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); - testUtils.dom.click($googleAction); + await testUtils.dom.click(form.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); + await testUtils.dom.click($googleAction); form.destroy(); }); - QUnit.test('click on the google drive attachments after switching records', function (assert) { + QUnit.test('click on the google drive attachments after switching records', async function (assert) { assert.expect(3); - var self = this; var currentID; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -134,10 +133,10 @@ QUnit.module('gdrive_integration', { if (route === '/web/dataset/call_kw/google.drive.config/get_google_drive_config') { assert.deepEqual(args.args, ['partner', 1], 'The route to get google drive config should have been called'); - return $.when([{id: 27, name: 'Cyberdyne Systems'}]); + return Promise.resolve([{id: 27, name: 'Cyberdyne Systems'}]); } if (route === '/web/dataset/call_kw/google.drive.config/search_read'){ - return $.when([{google_drive_resource_id: "T1000", + return Promise.resolve([{google_drive_resource_id: "T1000", google_drive_client_id: "cyberdyne.org", id: 1}]); } @@ -145,20 +144,20 @@ QUnit.module('gdrive_integration', { assert.deepEqual(args.args, [27, currentID, 'T1000'], 'The route to get the Google url should have been called'); // We don't return anything useful, otherwise it will open a new tab - return $.when(); + return Promise.resolve(); } return this._super.apply(this, arguments); }, }); currentID = 1; - testUtils.dom.click(form.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); - testUtils.dom.click(form.sidebar.$('.oe_share_gdoc')); + await testUtils.dom.click(form.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); + await testUtils.dom.click(form.sidebar.$('.oe_share_gdoc')); - testUtils.dom.click(form.pager.$('.o_pager_next')); + await testUtils.dom.click(form.pager.$('.o_pager_next')); currentID = 2; - testUtils.dom.click(form.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); - testUtils.dom.click(form.sidebar.$('.oe_share_gdoc')); + await testUtils.dom.click(form.sidebar.$('.o_dropdown_toggler_btn:contains(Action)')); + await testUtils.dom.click(form.sidebar.$('.oe_share_gdoc')); form.destroy(); }); diff --git a/addons/google_spreadsheet/static/src/js/add_to_google_spreadsheet_menu.js b/addons/google_spreadsheet/static/src/js/add_to_google_spreadsheet_menu.js index e1390f7844d..d0b608f4f36 100644 --- a/addons/google_spreadsheet/static/src/js/add_to_google_spreadsheet_menu.js +++ b/addons/google_spreadsheet/static/src/js/add_to_google_spreadsheet_menu.js @@ -64,7 +64,7 @@ var AddToGoogleSpreadsheetMenu = Widget.extend({ var ds = new data.DataSet(this, 'google.drive.config'); ds.call('set_spreadsheet', [modelName, Domain.prototype.arrayToString(domain), groupBys, list_view_id]) - .done(function (res) { + .then(function (res) { if (res.url){ window.open(res.url, '_blank'); } diff --git a/addons/hr/static/src/js/chat.js b/addons/hr/static/src/js/chat.js index 2f39851de67..233b9898e08 100644 --- a/addons/hr/static/src/js/chat.js +++ b/addons/hr/static/src/js/chat.js @@ -54,7 +54,7 @@ odoo.define('hr.employee_chat', function (require) { dmChat = self.call('mail_service', 'getChannel', dmChatId); dmChat.detach(); }); - $.when(def); + Promise.resolve(def); } }, }); diff --git a/addons/hr_attendance/static/src/js/kiosk_mode.js b/addons/hr_attendance/static/src/js/kiosk_mode.js index cbb84282eff..5370874a1b9 100644 --- a/addons/hr_attendance/static/src/js/kiosk_mode.js +++ b/addons/hr_attendance/static/src/js/kiosk_mode.js @@ -31,7 +31,7 @@ var KioskMode = AbstractAction.extend({ }); // Make a RPC call every day to keep the session alive self._interval = window.setInterval(this._callServer.bind(this), (60*60*1000*24)); - return $.when(def, this._super.apply(this, arguments)); + return Promise.all([def, this._super.apply(this, arguments)]); }, _onBarcodeScanned: function(barcode) { diff --git a/addons/hr_attendance/static/src/js/my_attendances.js b/addons/hr_attendance/static/src/js/my_attendances.js index b79469596af..5bf9884875d 100644 --- a/addons/hr_attendance/static/src/js/my_attendances.js +++ b/addons/hr_attendance/static/src/js/my_attendances.js @@ -25,7 +25,7 @@ var MyAttendances = AbstractAction.extend({ self.employee = res.length && res[0]; }); - return $.when(def, this._super.apply(this, arguments)); + return Promise.all([def, this._super.apply(this, arguments)]); }, update_attendance: function () { diff --git a/addons/hr_attendance/static/tests/hr_attendance_tests.js b/addons/hr_attendance/static/tests/hr_attendance_tests.js index a2bc6d6a6b9..a1cf6955791 100644 --- a/addons/hr_attendance/static/tests/hr_attendance_tests.js +++ b/addons/hr_attendance/static/tests/hr_attendance_tests.js @@ -53,7 +53,7 @@ QUnit.module('HR Attendance', { }, function () { QUnit.module('My attendances (client action)'); - QUnit.test('simple rendering', function (assert) { + QUnit.test('simple rendering', async function (assert) { assert.expect(1); var $target = $('#qunit-fixture'); @@ -64,7 +64,7 @@ QUnit.module('HR Attendance', { uid: 1, }, }); - clientAction.appendTo($target); + await clientAction.appendTo($target); assert.strictEqual(clientAction.$('.o_hr_attendance_kiosk_mode h1').text(), 'Employee A', "should have rendered the client action without crashing"); @@ -72,7 +72,7 @@ QUnit.module('HR Attendance', { clientAction.destroy(); }); - QUnit.test('Attendance Kiosk Mode Test', function (assert) { + QUnit.test('Attendance Kiosk Mode Test', async function (assert) { assert.expect(2); var $target = $('#qunit-fixture'); @@ -89,12 +89,12 @@ QUnit.module('HR Attendance', { if (args.method === 'attendance_scan' && args.model === 'hr.employee') { rpcCount++; - return $.when(self.data['hr.employee'].records[0]); + return Promise.resolve(self.data['hr.employee'].records[0]); } return this._super(route, args); }, }); - clientAction.appendTo($target); + await clientAction.appendTo($target); core.bus.trigger('barcode_scanned', 1); core.bus.trigger('barcode_scanned', 1); assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.'); @@ -105,7 +105,7 @@ QUnit.module('HR Attendance', { clientAction.destroy(); }); - QUnit.test('Attendance Greeting Message Test', function (assert) { + QUnit.test('Attendance Greeting Message Test', async function (assert) { assert.expect(10); var $target = $('#qunit-fixture'); @@ -113,7 +113,7 @@ QUnit.module('HR Attendance', { var rpcCount = 0; var clientActions = []; - function createGreetingMessage (target, barcode){ + async function createGreetingMessage (target, barcode){ var action = { attendance: { check_in: "2018-09-20 13:41:13", @@ -138,18 +138,19 @@ QUnit.module('HR Attendance', { as functional flow. */ createGreetingMessage (target, args.args[0]); - return $.when({action: action}); + return Promise.resolve({action: action}); } return this._super(route, args); }, }); - clientAction.appendTo(target); + await clientAction.appendTo(target); clientActions.push(clientAction); } // init - mock coming from kiosk - createGreetingMessage ($target, 1); + await createGreetingMessage ($target, 1); + await testUtils.nextMicrotaskTick(); assert.strictEqual(clientActions.length, 1, 'Number of clientAction must = 1.'); core.bus.trigger('barcode_scanned', 1); @@ -161,15 +162,19 @@ QUnit.module('HR Attendance', { assert.strictEqual(rpcCount, 0, 'RPC call should not have been done.'); core.bus.trigger('barcode_scanned', 2); + await testUtils.nextTick(); assert.strictEqual(clientActions.length, 2, 'Number of clientActions must = 2.'); assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.'); core.bus.trigger('barcode_scanned', 2); + await testUtils.nextMicrotaskTick(); assert.strictEqual(clientActions.length, 2, 'Number of clientActions must = 2.'); assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.'); core.bus.trigger('barcode_scanned', 1); + await testUtils.nextTick(); assert.strictEqual(clientActions.length, 3, 'Number of clientActions must = 3.'); core.bus.trigger('barcode_scanned', 1); + await testUtils.nextMicrotaskTick(); assert.strictEqual(clientActions.length, 3, 'Number of clientActions must = 3.'); assert.strictEqual(rpcCount, 2, 'RPC call should have been done only twice.'); diff --git a/addons/hr_org_chart/static/src/js/hr_org_chart.js b/addons/hr_org_chart/static/src/js/hr_org_chart.js index 39565570c84..c4f200d1ea9 100644 --- a/addons/hr_org_chart/static/src/js/hr_org_chart.js +++ b/addons/hr_org_chart/static/src/js/hr_org_chart.js @@ -34,7 +34,7 @@ var FieldOrgChart = AbstractField.extend({ * * @private * @param {integer} employee_id - * @returns {Deferred} + * @returns {Promise} */ _getOrgData: function (employee_id) { var self = this; @@ -52,7 +52,7 @@ var FieldOrgChart = AbstractField.extend({ * * @private * @param {integer} employee_id - * @returns {Deferred} + * @returns {Promise} */ _getSubordinatesData: function (employee_id, type) { return this.dm.add(this._rpc({ @@ -134,7 +134,7 @@ var FieldOrgChart = AbstractField.extend({ * * @private * @param {MouseEvent} event - * @returns {Deferred} action loaded + * @returns {Promise} action loaded */ _onEmployeeRedirect: function (event) { event.preventDefault(); @@ -154,7 +154,7 @@ var FieldOrgChart = AbstractField.extend({ * * @private * @param {MouseEvent} event - * @returns {Deferred} action loaded + * @returns {Promise} action loaded */ _onEmployeeSubRedirect: function (event) { event.preventDefault(); diff --git a/addons/hr_org_chart/static/tests/hr_org_chart_tests.js b/addons/hr_org_chart/static/tests/hr_org_chart_tests.js index d84cf293ae6..e536f0b587b 100644 --- a/addons/hr_org_chart/static/tests/hr_org_chart_tests.js +++ b/addons/hr_org_chart/static/tests/hr_org_chart_tests.js @@ -21,10 +21,10 @@ QUnit.module('hr_org_chart', { }; }, }, function () { - QUnit.test("hr org chart: empty render", function (assert) { + QUnit.test("hr org chart: empty render", async function (assert) { assert.expect(2); - var form = createView({ + var form = await createView({ View: FormView, model: 'hr_employee', data: this.data, @@ -36,7 +36,7 @@ QUnit.module('hr_org_chart', { mockRPC: function (route, args) { if (route === '/hr/get_org_chart') { assert.ok('employee_id' in args, "it should have 'employee_id' as argument"); - return $.when({ + return Promise.resolve({ children: [], managers: [], managers_more: false, @@ -45,13 +45,14 @@ QUnit.module('hr_org_chart', { return this._super(route, args); } }); - assert.strictEqual(form.$('[name="child_ids"]').children().length, 1, "the chart should have 1 child"); + assert.strictEqual(form.$('[name="child_ids"]').children().length, 1, + "the chart should have 1 child"); form.destroy(); }); - QUnit.test("hr org chart: basic render", function (assert) { + QUnit.test("hr org chart: basic render", async function (assert) { assert.expect(3); - var form = createView({ + var form = await createView({ View: FormView, model: 'hr_employee', data: this.data, @@ -69,7 +70,7 @@ QUnit.module('hr_org_chart', { mockRPC: function (route, args) { if (route === '/hr/get_org_chart') { assert.ok('employee_id' in args, "it should have 'employee_id' as argument"); - return $.when({ + return Promise.resolve({ children: [{ direct_sub_count: 0, indirect_sub_count: 0, @@ -101,10 +102,10 @@ QUnit.module('hr_org_chart', { "the current employee should only be displayed once in the chart"); form.destroy(); }); - QUnit.test("hr org chart: basic manager render", function (assert) { + QUnit.test("hr org chart: basic manager render", async function (assert) { assert.expect(4); - var form = createView({ + var form = await createView({ View: FormView, model: 'hr_employee', data: this.data, @@ -122,7 +123,7 @@ QUnit.module('hr_org_chart', { mockRPC: function (route, args) { if (route === '/hr/get_org_chart') { assert.ok('employee_id' in args, "should have 'employee_id' as argument"); - return $.when({ + return Promise.resolve({ children: [{ direct_sub_count: 0, indirect_sub_count: 0, diff --git a/addons/hr_skills/static/src/js/resume_widget.js b/addons/hr_skills/static/src/js/resume_widget.js index cf683146fed..4249fca884b 100644 --- a/addons/hr_skills/static/src/js/resume_widget.js +++ b/addons/hr_skills/static/src/js/resume_widget.js @@ -153,7 +153,7 @@ var ResumeLineRenderer = AbstractGroupedOne2ManyRenderer.extend({ _render: function () { var self = this; - this._super().then(function () { + return this._super().then(function () { // Allow to sort records self.$el.find('.o_list_view').sortable({ axis: 'y', diff --git a/addons/hr_skills/static/tests/widget_tests.js b/addons/hr_skills/static/tests/widget_tests.js index 69c52b1aa8a..3bee4c4ee29 100644 --- a/addons/hr_skills/static/tests/widget_tests.js +++ b/addons/hr_skills/static/tests/widget_tests.js @@ -131,9 +131,9 @@ odoo.define('hr_skills.field_one_to_many_group_tests', function (require) { }; } }, function () { - QUnit.test('resumé one2many field group by field render', function (assert) { + QUnit.test('resumé one2many field group by field render', async function (assert) { assert.expect(16); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -179,9 +179,9 @@ odoo.define('hr_skills.field_one_to_many_group_tests', function (require) { form.destroy(); }); - QUnit.test('resumé one2many field group by field create', function (assert) { + QUnit.test('resumé one2many field group by field create', async function (assert) { assert.expect(5); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -221,22 +221,22 @@ odoo.define('hr_skills.field_one_to_many_group_tests', function (require) { }, }); - testUtils.form.clickEdit(form); - testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')[0]); + await testUtils.form.clickEdit(form); + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')[0]); // Fill line form (type should be set from the add button context) - testUtils.fields.editInput($('input[name="name"]'), 'new line'); - testUtils.fields.editInput($('textarea[name="description"]'), 'new description'); - testUtils.fields.editSelect($('input[name="date_start"]'), '2025-01-01'); - testUtils.fields.editSelect($('input[name="date_end"]'), '2030-01-01'); - testUtils.modal.clickButton('Save & Close'); + await testUtils.fields.editInput($('input[name="name"]'), 'new line'); + await testUtils.fields.editInput($('textarea[name="description"]'), 'new description'); + await testUtils.fields.editSelect($('input[name="date_start"]'), '2025-01-01'); + await testUtils.fields.editSelect($('input[name="date_end"]'), '2030-01-01'); + await testUtils.modal.clickButton('Save & Close'); - testUtils.form.clickSave(form); + await testUtils.form.clickSave(form); form.destroy(); }); - QUnit.test('resumé one2many field group by field delete', function (assert) { + QUnit.test('resumé one2many field group by field delete', async function (assert) { assert.expect(2); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -274,16 +274,16 @@ odoo.define('hr_skills.field_one_to_many_group_tests', function (require) { }, }); - testUtils.form.clickEdit(form); - testUtils.dom.click(form.$('.o_list_record_remove')[0]); + await testUtils.form.clickEdit(form); + await testUtils.dom.click(form.$('.o_list_record_remove')[0]); - testUtils.form.clickSave(form); + await testUtils.form.clickSave(form); form.destroy(); }); - QUnit.test('skills one2many field group by field render', function (assert) { + QUnit.test('skills one2many field group by field render', async function (assert) { assert.expect(13); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index 964e9ef5d3e..f8f8b0ea9d1 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -79,7 +79,7 @@ var LivechatButton = Widget.extend({ ready = session.rpc('/im_livechat/init', {channel_id: this.options.channel_id}) .then(function (result) { if (!result.available_for_me) { - return $.Deferred().reject(); + return Promise.reject(); } self._rule = result.rule; }); @@ -209,7 +209,7 @@ var LivechatButton = Widget.extend({ this._openingChat = true; clearTimeout(this._autoPopupTimeout); if (cookie) { - def = $.when(JSON.parse(cookie)); + def = Promise.resolve(JSON.parse(cookie)); } else { this._messages = []; // re-initialize messages cache def = session.rpc('/im_livechat/get_session', { @@ -235,7 +235,9 @@ var LivechatButton = Widget.extend({ utils.set_cookie('im_livechat_session', JSON.stringify(self._livechat.toData()), 60*60); utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60*60); } - }).always(function () { + }).then(function () { + self._openingChat = false; + }).guardedCatch(function() { self._openingChat = false; }); }, 200, true), @@ -270,7 +272,7 @@ var LivechatButton = Widget.extend({ /** * @private * @param {Object} message - * @return {$.Deferred} + * @return {Promise} */ _sendMessage: function (message) { var self = this; @@ -337,8 +339,8 @@ var LivechatButton = Widget.extend({ ev.stopPropagation(); var self = this; var messageData = ev.data.messageData; - this._sendMessage(messageData).fail(function (error, e) { - e.preventDefault(); + this._sendMessage(messageData).guardedCatch(function (reason) { + reason.event.preventDefault(); return self._sendMessage(messageData); // try again just in case }); }, diff --git a/addons/im_livechat/static/src/js/models/website_livechat.js b/addons/im_livechat/static/src/js/models/website_livechat.js index 46652f25713..cf5bc36c7b6 100644 --- a/addons/im_livechat/static/src/js/models/website_livechat.js +++ b/addons/im_livechat/static/src/js/models/website_livechat.js @@ -130,7 +130,7 @@ var WebsiteLivechat = AbstractThread.extend(ThreadTypingMixin, { * @private * @param {Object} params * @param {boolean} params.typing - * @returns {$.Promise} + * @returns {Promise} */ _notifyMyselfTyping: function (params) { return session.rpc('/im_livechat/notify_typing', { diff --git a/addons/im_livechat/static/tests/website_livechat_window_tests.js b/addons/im_livechat/static/tests/website_livechat_window_tests.js index 4b8aa6635a8..9eab3d0be06 100644 --- a/addons/im_livechat/static/tests/website_livechat_window_tests.js +++ b/addons/im_livechat/static/tests/website_livechat_window_tests.js @@ -17,7 +17,7 @@ var Widget = require('web.Widget'); QUnit.module('im_livechat', {}, function () { QUnit.module('Website Livechat Window', {}); -QUnit.test('basic rendering', function (assert) { +QUnit.test('basic rendering', async function (assert) { assert.expect(3); var parent = new Widget(); @@ -46,7 +46,7 @@ QUnit.test('basic rendering', function (assert) { var chatWindow = new ChatWindow(parent, livechat); testUtils.mock.addMockEnvironment(parent, {}); - chatWindow.appendTo($('#qunit-fixture')); + await chatWindow.appendTo($('#qunit-fixture')); chatWindow.render(); assert.containsOnce(chatWindow, '.o_thread_window_header', diff --git a/addons/im_support/static/src/js/mail_manager.js b/addons/im_support/static/src/js/mail_manager.js index 6a44e87ed8d..3672af2491f 100644 --- a/addons/im_support/static/src/js/mail_manager.js +++ b/addons/im_support/static/src/js/mail_manager.js @@ -62,7 +62,7 @@ MailManager.include({ * Overrides to filter out the Support channel from the previews. * * @override - * @returns {$.Promise} list of valid objects for mail.Preview + * @returns {Promise} list of valid objects for mail.Preview * template */ getChannelPreviews: function () { @@ -97,7 +97,7 @@ MailManager.include({ * * @param {string} [channelState='open'] state of the Support * channel (see CHANNEL_STATES for accepted values) - * @returns {Deferred} + * @returns {Promise} */ startSupportLivechat: function (channelState) { var self = this; @@ -130,15 +130,15 @@ MailManager.include({ self.supportChannelUUID = channel.uuid; // add the channel to the MailManager - self._addChannel(_.extend(channel, { + return self._addChannel(_.extend(channel, { id: channel.uuid, is_minimized: _.contains(['open', 'folded'], channelState), state: channelState, - })); - - // display an automatic message in the channel - var supportChannel = self.getChannel(channel.uuid); - supportChannel.addDefaultMessage(); + })).then(function () { + // display an automatic message in the channel + var supportChannel = self.getChannel(channel.uuid); + supportChannel.addDefaultMessage(); + }); } else { // the channel has already been added to the MailManager, so // simply re-open it @@ -149,7 +149,7 @@ MailManager.include({ channel.fold(channelState === 'folded'); } } - }).fail(function () { + }).guardedCatch(function () { self.do_warn(_t("The Support server can't be reached.")); }); }, diff --git a/addons/im_support/static/src/js/support_channel.js b/addons/im_support/static/src/js/support_channel.js index 3147cc55a94..8a3fae912f9 100644 --- a/addons/im_support/static/src/js/support_channel.js +++ b/addons/im_support/static/src/js/support_channel.js @@ -249,7 +249,7 @@ var SupportChannel = SearchableThread.extend({ * * @override * @private - * @return {$.Promise} + * @return {Promise} */ _postMessage: function (data) { // ensure that the poll is active before posting the message diff --git a/addons/im_support/static/src/js/support_message.js b/addons/im_support/static/src/js/support_message.js index bf21bd2624f..7a4df85ca3a 100644 --- a/addons/im_support/static/src/js/support_message.js +++ b/addons/im_support/static/src/js/support_message.js @@ -51,10 +51,10 @@ var SupportMessage = Message.extend({ * channel (which are records on the Support database). * * @override - * @returns {$.Promise} + * @returns {Promise} */ toggleStarStatus: function () { - return $.when(); + return Promise.resolve(); }, }); diff --git a/addons/im_support/static/tests/helpers/test_utils.js b/addons/im_support/static/tests/helpers/test_utils.js index e0a5bdfd73a..dece9e2c361 100644 --- a/addons/im_support/static/tests/helpers/test_utils.js +++ b/addons/im_support/static/tests/helpers/test_utils.js @@ -29,7 +29,7 @@ function addMockSupportEnvironment(widget, params) { var originalRPC = supportSession.rpc; var defaultMockSupportRPC = function (route, args) { if (route === '/odoo_im_support/get_support_channel') { - return $.when({ + return Promise.resolve({ available: true, channel_type: 'livechat', public: 'private', @@ -37,13 +37,13 @@ function addMockSupportEnvironment(widget, params) { }); } if (route === '/odoo_im_support/fetch_messages') { // fetching history - return $.when([]); + return Promise.resolve([]); } if (route === '/odoo_im_support/chat_post') { - return $.when(); + return Promise.resolve(); } if (route === '/longpolling/support_poll') { - return $.Deferred(); + return Promise.resolve(); } }; supportSession.rpc = function (route, args) { diff --git a/addons/im_support/static/tests/systray_no_support_tests.js b/addons/im_support/static/tests/systray_no_support_tests.js index f835c0e55b7..80e3d443597 100644 --- a/addons/im_support/static/tests/systray_no_support_tests.js +++ b/addons/im_support/static/tests/systray_no_support_tests.js @@ -32,7 +32,7 @@ QUnit.module('systray', { }, }); -QUnit.test('messaging menu does not display the Support channel if not available', function (assert) { +QUnit.test('messaging menu does not display the Support channel if not available', async function (assert) { // the Support channel should only be displayed if a support_token and a support_origin are // specified in the session, which is not the case for this test assert.expect(1); @@ -42,7 +42,7 @@ QUnit.test('messaging menu does not display the Support channel if not available data: this.data, services: this.services, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsNone(messagingMenu, '.o_mail_systray_dropdown_bottom .o_mail_preview[data-preview-id=SupportChannel]', "should not display the Support channel"); diff --git a/addons/im_support/static/tests/systray_tests.js b/addons/im_support/static/tests/systray_tests.js index 9f50ce1d29c..9df4c3b2bd8 100644 --- a/addons/im_support/static/tests/systray_tests.js +++ b/addons/im_support/static/tests/systray_tests.js @@ -29,7 +29,7 @@ QUnit.module('systray', { }, }); -QUnit.test('messaging menu displays the Support channel', function (assert) { +QUnit.test('messaging menu displays the Support channel', async function (assert) { assert.expect(1); var messagingMenu = new MessagingMenu(); @@ -38,15 +38,16 @@ QUnit.test('messaging menu displays the Support channel', function (assert) { services: this.services, session: this.supportParams, }); - messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); + await messagingMenu.appendTo($('#qunit-fixture')); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_mail_systray_dropdown_bottom .o_mail_preview[data-preview-id=SupportChannel]', "should display the Support channel"); messagingMenu.destroy(); }); -QUnit.test('clicking on Support channel: channel not available', function (assert) { +QUnit.test('clicking on Support channel: channel not available', async function (assert) { assert.expect(9); var messagingMenu = new MessagingMenu(); @@ -71,12 +72,13 @@ QUnit.test('clicking on Support channel: channel not available', function (asser services: this.services, session: this.supportParams, }); - messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); + await messagingMenu.appendTo($('#qunit-fixture')); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_mail_systray_dropdown_bottom .o_mail_preview[data-preview-id=SupportChannel]'); - testUtils.dom.click(messagingMenu.$('.o_mail_preview[data-preview-id=SupportChannel]')); + await testUtils.dom.click(messagingMenu.$('.o_mail_preview[data-preview-id=SupportChannel]')); assert.strictEqual($('.o_thread_window').length, 1, "should have open a chat window"); assert.strictEqual($('.o_thread_window .o_thread_window_title').text().trim(), 'Support (offline)', @@ -94,7 +96,7 @@ QUnit.test('clicking on Support channel: channel not available', function (asser messagingMenu.destroy(); }); -QUnit.test('clicking on Support channel: channel available', function (assert) { +QUnit.test('clicking on Support channel: channel available', async function (assert) { assert.expect(9); var messagingMenu = new MessagingMenu(); @@ -115,12 +117,13 @@ QUnit.test('clicking on Support channel: channel available', function (assert) { services: this.services, session: this.supportParams, }); - messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); + await messagingMenu.appendTo($('#qunit-fixture')); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_mail_systray_dropdown_bottom .o_mail_preview[data-preview-id=SupportChannel]'); - testUtils.dom.click(messagingMenu.$('.o_mail_preview[data-preview-id=SupportChannel]')); + await testUtils.dom.click(messagingMenu.$('.o_mail_preview[data-preview-id=SupportChannel]')); assert.strictEqual($('.o_thread_window').length, 1, "should have open a chat window"); assert.strictEqual($('.o_thread_window .o_thread_window_title').text().trim(), 'Support', @@ -138,7 +141,7 @@ QUnit.test('clicking on Support channel: channel available', function (assert) { messagingMenu.destroy(); }); -QUnit.test('post messages in Support channel', function (assert) { +QUnit.test('post messages in Support channel', async function (assert) { assert.expect(8); var messagingMenu = new MessagingMenu(); @@ -159,18 +162,18 @@ QUnit.test('post messages in Support channel', function (assert) { services: this.services, session: this.supportParams, }); - messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); + await messagingMenu.appendTo($('#qunit-fixture')); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_mail_systray_dropdown_bottom .o_mail_preview[data-preview-id=SupportChannel]'); - testUtils.dom.click(messagingMenu.$('.o_mail_preview[data-preview-id=SupportChannel]')); + await testUtils.dom.click(messagingMenu.$('.o_mail_preview[data-preview-id=SupportChannel]')); assert.strictEqual($('.o_thread_window .o_composer_input').length, 1, "should have a composer"); - $('.o_thread_window .o_composer_input .o_input') - .val('some message') - .trigger($.Event('keydown', {which: $.ui.keyCode.ENTER})); + await testUtils.fields.editInput($('.o_thread_window .o_composer_input .o_input'), 'some message'); + await testUtils.fields.triggerKeydown($('.o_thread_window .o_composer_input .o_input'), 'enter'); assert.verifySteps([ '/mail/init_messaging', @@ -183,7 +186,7 @@ QUnit.test('post messages in Support channel', function (assert) { messagingMenu.destroy(); }); -QUnit.test('fold Support channel', function (assert) { +QUnit.test('fold Support channel', async function (assert) { assert.expect(11); var messagingMenu = new MessagingMenu(); @@ -204,23 +207,24 @@ QUnit.test('fold Support channel', function (assert) { services: this.services, session: this.supportParams, }); + await testUtils.nextTick(); testUtils.mock.intercept(messagingMenu, 'call_service', function (ev) { if (ev.data.service === 'local_storage') { assert.step('LocalStorage: ' + ev.data.method + ' ' + ev.data.args); } }, true); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_mail_systray_dropdown_bottom .o_mail_preview[data-preview-id=SupportChannel]'); - testUtils.dom.click(messagingMenu.$('.o_mail_preview[data-preview-id=SupportChannel]')); + await testUtils.dom.click(messagingMenu.$('.o_mail_preview[data-preview-id=SupportChannel]')); assert.strictEqual($('.o_thread_window').length, 1, "should have open a chat window"); // fold, re-open and close channel - testUtils.dom.click($('.o_thread_window .o_thread_window_title')); - testUtils.dom.click($('.o_thread_window .o_thread_window_title')); + await testUtils.dom.click($('.o_thread_window .o_thread_window_title')); + await testUtils.dom.click($('.o_thread_window .o_thread_window_title')); testUtils.dom.click($('.o_thread_window .o_thread_window_close')); assert.verifySteps([ @@ -237,7 +241,7 @@ QUnit.test('fold Support channel', function (assert) { messagingMenu.destroy(); }); -QUnit.test('restore Support channel if necessary', function (assert) { +QUnit.test('restore Support channel if necessary', async function (assert) { assert.expect(5); var messagingMenu = new MessagingMenu(); @@ -259,21 +263,22 @@ QUnit.test('restore Support channel if necessary', function (assert) { services: this.services, session: this.supportParams, }); - messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); + await messagingMenu.appendTo($('#qunit-fixture')); assert.strictEqual($('.o_thread_window').length, 1, "should have open a chat window"); assert.verifySteps([ - '/mail/init_messaging', 'cors: /odoo_im_support/get_support_channel', + '/mail/init_messaging', 'cors: /odoo_im_support/fetch_messages', ]); messagingMenu.destroy(); }); -QUnit.test('receive messages in the Support channel', function (assert) { +QUnit.test('receive messages in the Support channel', async function (assert) { assert.expect(10); var supportChannelID; @@ -297,7 +302,8 @@ QUnit.test('receive messages in the Support channel', function (assert) { services: this.services, session: this.supportParams, }); - messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); + await messagingMenu.appendTo($('#qunit-fixture')); assert.strictEqual($('.o_thread_window').length, 1, "should have open a chat window"); @@ -312,6 +318,7 @@ QUnit.test('receive messages in the Support channel', function (assert) { }; var notification = [[false, 'mail.channel', 1], data]; messagingMenu.call('support_bus_service', 'trigger', 'notification', [notification]); + await testUtils.nextTick(); assert.strictEqual($('.o_thread_window .o_thread_message').length, 1, "there should be a new message in the thread"); @@ -323,8 +330,8 @@ QUnit.test('receive messages in the Support channel', function (assert) { 'A message', "message is correct"); assert.verifySteps([ - '/mail/init_messaging', 'cors: /odoo_im_support/get_support_channel', + '/mail/init_messaging', 'cors: /odoo_im_support/fetch_messages', ]); diff --git a/addons/lunch/static/src/js/lunch_kanban_controller.js b/addons/lunch/static/src/js/lunch_kanban_controller.js index 8055f876d2e..e1025251b53 100644 --- a/addons/lunch/static/src/js/lunch_kanban_controller.js +++ b/addons/lunch/static/src/js/lunch_kanban_controller.js @@ -103,7 +103,7 @@ var LunchKanbanController = KanbanController.extend({ self.$('.o_lunch_kanban').prepend(self.widget.$el); }); }); - return $.when(def, this._super.apply(self, arguments)); + return Promise.all([def, this._super.apply(self, arguments)]); }, /** * Override to add the location domain (coming from the lunchKanbanWidget) diff --git a/addons/lunch/static/src/js/lunch_kanban_model.js b/addons/lunch/static/src/js/lunch_kanban_model.js index 0474d905e06..ddd11f9a9be 100644 --- a/addons/lunch/static/src/js/lunch_kanban_model.js +++ b/addons/lunch/static/src/js/lunch_kanban_model.js @@ -76,7 +76,7 @@ var LunchKanbanModel = KanbanModel.extend({ }, _updateLocation: function (locationId) { this.locationId = locationId; - return $.when(); + return Promise.resolve(); }, _updateUser: function (userId) { var self = this; diff --git a/addons/lunch/static/src/js/lunch_kanban_widget.js b/addons/lunch/static/src/js/lunch_kanban_widget.js index 3cd3230faa1..216c62b482c 100644 --- a/addons/lunch/static/src/js/lunch_kanban_widget.js +++ b/addons/lunch/static/src/js/lunch_kanban_widget.js @@ -2,12 +2,12 @@ odoo.define('lunch.LunchKanbanWidget', function (require) { "use strict"; var core = require('web.core'); -var field_registry = require('web.field_registry'); +var relationalFields = require('web.relational_fields'); var session = require('web.session'); var Widget = require('web.Widget'); var _t = core._t; -var FieldMany2One = field_registry.get('many2one'); +var FieldMany2One = relationalFields.FieldMany2One; var LunchMany2One = FieldMany2One.extend({ diff --git a/addons/lunch/static/tests/test_lunch_kanban.js b/addons/lunch/static/tests/test_lunch_kanban.js index 38d27fafda7..ef22057d3b2 100644 --- a/addons/lunch/static/tests/test_lunch_kanban.js +++ b/addons/lunch/static/tests/test_lunch_kanban.js @@ -23,10 +23,10 @@ QUnit.module('Views', { }, function () { QUnit.module('LunchKanbanView'); - QUnit.test('Simple rendering of LunchKanbanView', function (assert) { + QUnit.test('Simple rendering of LunchKanbanView', async function (assert) { assert.expect(7); - var lunchKanban = createView({ + var lunchKanban = await createView({ View: LunchKanbanView, model: 'product', data: this.data, @@ -36,7 +36,7 @@ QUnit.module('Views', { '', mockRPC: function(route, args) { if (route === '/lunch/infos') { - return $.when({ + return Promise.resolve({ order: 1, wallet: 20, username: 'Marc Demo', @@ -56,11 +56,11 @@ QUnit.module('Views', { user_location: [1, 'hello'], }); } else if (route === '/lunch/user_location_get') { - return $.when(1); + return Promise.resolve(1); } else if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') { - return $.when(); + return Promise.resolve(); } else if (route.startsWith('data:image/png;base64,')) { - return $.when(); + return Promise.resolve(); } return this._super(route, args); }, @@ -88,12 +88,12 @@ QUnit.module('Views', { lunchKanban.destroy(); }); - QUnit.test('User interactions', function (assert) { + QUnit.test('User interactions', async function (assert) { assert.expect(9); var state = 'new'; - var lunchKanban = createView({ + var lunchKanban = await createView({ View: LunchKanbanView, model: 'product', data: this.data, @@ -103,7 +103,7 @@ QUnit.module('Views', { '', mockRPC: function(route, args) { if (route === '/lunch/infos') { - return $.when({ + return Promise.resolve({ order: 1, wallet: 20, username: 'Marc Demo', @@ -124,35 +124,35 @@ QUnit.module('Views', { user_location: [1, 'hello'], }); } else if (route === '/lunch/payment_message') { - return $.when({message: 'Hello'}); + return Promise.resolve({message: 'Hello'}); } else if (route === '/lunch/pay') { - return $.when(true); + return Promise.resolve(true); } else if (route === '/lunch/user_location_get') { - return $.when(1); + return Promise.resolve(1); } else if (args.method === 'update_quantity') { - assert.step(args.args); - return $.when(); + assert.step(JSON.stringify(args.args)); + return Promise.resolve(); } else if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') { - return $.when(); + return Promise.resolve(); } else if (route.startsWith('data:image/png;base64,')) { - return $.when(); + return Promise.resolve(); } return this._super(route, args); }, }); - lunchKanban.$('.o_add_product').click(); - lunchKanban.$('.o_remove_product').click(); + await testUtils.dom.click(lunchKanban.$('.o_add_product')); + await testUtils.dom.click(lunchKanban.$('.o_remove_product')); state = 'ordered'; - lunchKanban.$('.o_lunch_widget_order_button').click(); + await testUtils.dom.click(lunchKanban.$('.o_lunch_widget_order_button')); // state is shown assert.strictEqual(lunchKanban.$('.o_lunch_ordered').length, 1, 'state should be shown as ordered'); // buttons assert.strictEqual(lunchKanban.$('.o_remove_product').length, 1, 'button to remove product should be shown'); assert.strictEqual(lunchKanban.$('.o_add_product').length, 1, 'button to add product should be shown'); state = 'confirmed'; - lunchKanban.reload(); + await lunchKanban.reload(); // state is updated assert.strictEqual(lunchKanban.$('.o_lunch_confirmed').length, 1, 'state should be shown as confirmed'); // Buttons not shown anymore @@ -161,17 +161,17 @@ QUnit.module('Views', { assert.verifySteps([ - [[1], 1], - [[1], -1], + JSON.stringify([[1], 1]), + JSON.stringify([[1], -1]), ]); lunchKanban.destroy(); }); - QUnit.test('Manager interactions', function (assert) { + QUnit.test('Manager interactions', async function (assert) { assert.expect(1); - var lunchKanban = createView({ + var lunchKanban = await createView({ View: LunchKanbanView, model: 'product', data: this.data, @@ -181,7 +181,7 @@ QUnit.module('Views', { '', mockRPC: function(route, args) { if (route === '/lunch/infos') { - return $.when({ + return Promise.resolve({ order: 1, wallet: 20, username: 'Marc Demo', @@ -202,11 +202,11 @@ QUnit.module('Views', { user_location: [1, 'hello'], }); } else if (route === '/lunch/user_location_get') { - return $.when(1); + return Promise.resolve(1); } else if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') { - return $.when(); + return Promise.resolve(); } else if (route.startsWith('data:image/png;base64,')) { - return $.when(); + return Promise.resolve(); } return this._super(route, args); }, diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js index 68ff4b0b86f..607f1b8c553 100644 --- a/addons/mail/static/src/js/activity.js +++ b/addons/mail/static/src/js/activity.js @@ -25,11 +25,11 @@ var _t = core._t; * * @param {Widget} self a widget instance that can perform RPCs * @param {Array} ids the ids of activities to read - * @return {Deferred} resolved with the activities + * @return {Promise} resolved with the activities */ function _readActivities(self, ids) { if (!ids.length) { - return $.when([]); + return Promise.resolve([]); } var context = self.getSession().user_context; if (self.record && !_.isEmpty(self.record.getContext())) { @@ -64,7 +64,7 @@ BasicModel.include({ * @private * @param {Object} record - an element from the localData * @param {string} fieldName - * @return {Deferred} resolved with the activities + * @return {Promise} resolved with the activities */ _fetchSpecialActivity: function (record, fieldName) { var localID = (record._changes && fieldName in record._changes) ? @@ -144,7 +144,7 @@ var BasicActivity = AbstractField.extend({ /** * @param {integer} previousActivityTypeID - * @return {$.Promise} + * @return {Promise} */ scheduleActivity: function () { var callback = this._reload.bind(this, { activity: true, thread: true }); @@ -213,7 +213,7 @@ var BasicActivity = AbstractField.extend({ * @param {integer} id * @param {integer} previousActivityTypeID * @param {function} callback - * @return {$.Deferred} + * @return {Promise} */ _openActivityForm: function (id, callback) { var action = { @@ -237,7 +237,7 @@ var BasicActivity = AbstractField.extend({ * @param {integer} activityID * @param {string} feedback * @param {integer[]} attachmentIds - * @return {$.Promise} + * @return {Promise} */ _sendActivityFeedback: function (activityID, feedback, attachmentIds) { return this._rpc({ @@ -303,7 +303,7 @@ var BasicActivity = AbstractField.extend({ /** * @private * @param {MouseEvent} ev - * @returns {$.Promise} + * @returns {Promise} */ _onEditActivity: function (ev) { ev.preventDefault(); @@ -449,7 +449,7 @@ var BasicActivity = AbstractField.extend({ /** * @private * @param {MouseEvent} ev - * @returns {$.Deferred} + * @returns {Promise} */ _onPreviewMailTemplate: function (ev) { ev.stopPropagation(); @@ -477,7 +477,7 @@ var BasicActivity = AbstractField.extend({ /** * @private * @param {MouseEvent} ev - * @returns {$.Promise} + * @returns {Promise} */ _onSendMailTemplate: function (ev) { ev.stopPropagation(); @@ -493,7 +493,7 @@ var BasicActivity = AbstractField.extend({ /** * @private * @param {MouseEvent} ev - * @returns {$.Deferred} + * @returns {Promise} */ _onScheduleActivity: function (ev) { ev.preventDefault(); @@ -504,7 +504,7 @@ var BasicActivity = AbstractField.extend({ * @private * @param {MouseEvent} ev * @param {Object} options - * @returns {$.Promise} + * @returns {Promise} */ _onUnlinkActivity: function (ev, options) { ev.preventDefault(); diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index 72971fbeee9..397ae2c8a16 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -103,7 +103,7 @@ var Chatter = Widget.extend({ })); // start and append the widgets var fieldDefs = _.invoke(this.fields, 'appendTo', $('
')); - var def = this._dp.add($.when.apply($, fieldDefs)); + var def = this._dp.add(Promise.all(fieldDefs)); this._render(def).then(this._updateMentionSuggestions.bind(this)); return this._super.apply(this, arguments); @@ -153,7 +153,7 @@ var Chatter = Widget.extend({ fieldsToReset = this.fields; } var fieldDefs = _.invoke(fieldsToReset, 'reset', record); - var def = this._dp.add($.when.apply($, fieldDefs)); + var def = this._dp.add(Promise.all(fieldDefs)); this._render(def).then(function () { self.$el.height('auto'); self._updateMentionSuggestions(); @@ -203,17 +203,18 @@ var Chatter = Widget.extend({ * Discard changes on the record. * * @private - * @returns {$.Deferred} resolved if successfully discarding changes on + * @returns {Promise} resolved if successfully discarding changes on * the record, rejected otherwise */ _discardChanges: function () { - var def = $.Deferred(); - this.trigger_up('discard_changes', { - recordID: this.record.id, - onSuccess: def.resolve.bind(def), - onFailure: def.reject.bind(def), + var self = this; + return new Promise(function (resolve, reject) { + self.trigger_up('discard_changes', { + recordID: self.record.id, + onSuccess: resolve, + onFailure: reject, + }); }); - return def; }, /** * Discard changes on the record if the message will reload the record @@ -221,14 +222,14 @@ var Chatter = Widget.extend({ * * @private * @param {Object} messageData - * @return {$.Deferred} resolved if no reload or proceed to discard the + * @return {Promise} resolved if no reload or proceed to discard the * changes on the record, rejected otherwise */ _discardOnReload: function (messageData) { if (this._reloadAfterPost(messageData)) { return this._discardChanges(); } - return $.when(); + return Promise.resolve(); }, /** * @private @@ -323,7 +324,7 @@ var Chatter = Widget.extend({ self._reloadAttachmentBox(); self.trigger_up('reload', {fieldNames: ['message_attachment_count'], keepChanges: true}); } - }).fail(function () { + }).guardedCatch(function () { self._enableComposer(); }); }); @@ -375,9 +376,9 @@ var Chatter = Widget.extend({ _render: function (def) { // the rendering of the chatter is aynchronous: relational data of its fields needs to be // fetched (in some case, it might be synchronous as they hold an internal cache). - // this function takes a deferred as argument, which is resolved once all fields have + // this function takes a promise as argument, which is resolved once all fields have // fetched their data - // this function appends the fields where they should be once the given deferred is resolved + // this function appends the fields where they should be once the given promise is resolved // and if it takes more than 500ms, displays a spinner to indicate that it is loading var self = this; @@ -385,6 +386,11 @@ var Chatter = Widget.extend({ concurrency.rejectAfter(concurrency.delay(500), def).then(function () { $spinner.appendTo(self.$el); }); + var always = function () { + // disable widgets in create mode, otherwise enable + self._isCreateMode ? self._disableChatter() : self._enableChatter(); + $spinner.remove(); + }; return def.then(function () { if (self.fields.activity) { @@ -400,11 +406,7 @@ var Chatter = Widget.extend({ if (self.fields.thread) { self.fields.thread.$el.appendTo(self.$el); } - }).always(function () { - // disable widgets in create mode, otherwise enable - self._isCreateMode ? self._disableChatter() : self._enableChatter(); - $spinner.remove(); - }); + }).then(always).guardedCatch(always); }, /** * @private @@ -517,7 +519,7 @@ var Chatter = Widget.extend({ if (!this._areAttachmentsLoaded) { def = this._fetchAttachments(); } - $.when(def).then(this._openAttachmentBox.bind(this)); + Promise.resolve(def).then(this._openAttachmentBox.bind(this)); } }, /** @@ -534,10 +536,10 @@ var Chatter = Widget.extend({ _onOpenComposerMessage: function () { var self = this; if (!this.suggested_partners_def) { - this.suggested_partners_def = $.Deferred(); - var method = 'message_get_suggested_recipients'; - var args = [[this.context.default_res_id], this.context]; - this._rpc({model: this.record.model, method: method, args: args}) + this.suggested_partners_def = new Promise(function (resolve, reject) { + var method = 'message_get_suggested_recipients'; + var args = [[self.context.default_res_id], self.context]; + self._rpc({model: self.record.model, method: method, args: args}) .then(function (result) { if (!self.suggested_partners_def) { return; // widget has been reset (e.g. we just switched to another record) @@ -555,8 +557,9 @@ var Chatter = Widget.extend({ reason: recipient[2], }); }); - self.suggested_partners_def.resolve(suggested_partners); + resolve(suggested_partners); }); + }); } this.suggested_partners_def.then(function (suggested_partners) { self._openComposer({ isLog: false, suggested_partners: suggested_partners }); diff --git a/addons/mail/static/src/js/composers/basic_composer.js b/addons/mail/static/src/js/composers/basic_composer.js index 9def912612c..72936152ae5 100644 --- a/addons/mail/static/src/js/composers/basic_composer.js +++ b/addons/mail/static/src/js/composers/basic_composer.js @@ -128,7 +128,7 @@ var BasicComposer = Widget.extend({ .on('update_typing_partners', this, this._onUpdateTypingPartners); // Mention - this._mentionManager.prependTo(this.$('.o_composer')); + var prependPromise = this._mentionManager.prependTo(this.$('.o_composer')); // Drag-Drop files // Allowing body to detect dragenter and dragleave for display @@ -137,7 +137,7 @@ var BasicComposer = Widget.extend({ $body.on('dragleave.' + this._dropZoneNS, this._onBodyFileDragLeave.bind(this)); $body.on("dragover." + this._dropZoneNS, this._onBodyFileDragover.bind(this)); $body.on("drop." + this._dropZoneNS, this._onBodyFileDrop.bind(this)); - return this._super(); + return Promise.all([this._super(), prependPromise]); }, destroy: function () { $("body").off('dragleave.' + this._dropZoneNS); @@ -236,7 +236,7 @@ var BasicComposer = Widget.extend({ * displayed to the user. If none of them match, then it will fetch for more * partner suggestions (@see _mentionFetchPartners). * - * @param {$.Deferred} prefetchedPartners list of list of + * @param {Promise} prefetchedPartners list of list of * prefetched partners. */ mentionSetPrefetchedPartners: function (prefetchedPartners) { @@ -299,20 +299,21 @@ var BasicComposer = Widget.extend({ /** * @private * @param {string} search - * @returns {$.Deferred} + * @returns {Promise} */ _mentionGetCannedResponses: function (search) { var self = this; - var def = $.Deferred(); - clearTimeout(this._cannedTimeout); - this._cannedTimeout = setTimeout(function () { - var cannedResponses = self.call('mail_service', 'getCannedResponses'); - var matches = fuzzy.filter(utils.unaccent(search), _.pluck(cannedResponses, 'source')); - var indexes = _.pluck(matches.slice(0, self.options.mentionFetchLimit), 'index'); - def.resolve(_.map(indexes, function (index) { - return cannedResponses[index]; - })); - }, 500); + var def = new Promise(function (resolve, reject) { + clearTimeout(self._cannedTimeout); + self._cannedTimeout = setTimeout(function () { + var cannedResponses = self.call('mail_service', 'getCannedResponses'); + var matches = fuzzy.filter(utils.unaccent(search), _.pluck(cannedResponses, 'source')); + var indexes = _.pluck(matches.slice(0, self.options.mentionFetchLimit), 'index'); + resolve(_.map(indexes, function (index) { + return cannedResponses[index]; + })); + }, 500); + }); return def; }, /** @@ -346,7 +347,7 @@ var BasicComposer = Widget.extend({ */ _mentionFetchPartners: function (search) { var self = this; - return $.when(this._mentionPrefetchedPartners).then(function (prefetchedPartners) { + return Promise.resolve(this._mentionPrefetchedPartners).then(function (prefetchedPartners) { // filter prefetched partners with the given search string var suggestions = []; var limit = self.options.mentionFetchLimit; @@ -371,7 +372,7 @@ var BasicComposer = Widget.extend({ { limit: limit, search: search } ); } - return $.when(suggestions).then(function (suggestions) { + return Promise.resolve(suggestions).then(function (suggestions) { //add im_status on suggestions _.each(suggestions, function (suggestionsSet) { _.each(suggestionsSet, function (suggestion) { @@ -387,27 +388,27 @@ var BasicComposer = Widget.extend({ * @param {string} model * @param {string} method * @param {Object} kwargs - * @return {$.Deferred} + * @return {Promise} */ _mentionFetchThrottled: function (model, method, kwargs) { var self = this; // Delays the execution of the RPC to prevent unnecessary RPCs when the user is still typing - var def = $.Deferred(); - clearTimeout(this.mentionFetchTimer); - this.mentionFetchTimer = setTimeout(function () { - return self._rpc({model: model, method: method, kwargs: kwargs}) + return new Promise(function (resolve, reject) { + clearTimeout(self.mentionFetchTimer); + self.mentionFetchTimer = setTimeout(function () { + return self._rpc({model: model, method: method, kwargs: kwargs}) .then(function (results) { - def.resolve(results); + resolve(results); }); - }, this.MENTION_THROTTLE); - return def; + }, self.MENTION_THROTTLE); + }); }, /** * @private - * @returns {$.Deferred} + * @returns {Promise} */ _preprocessMessage: function () { - // Return a deferred as this function is extended with asynchronous + // Return a promise as this function is extended with asynchronous // behavior for the chatter composer //Removing unwanted extra spaces from message @@ -420,7 +421,7 @@ var BasicComposer = Widget.extend({ var commands = this.options.commandsEnabled ? this._mentionManager.getListenerSelection('/') : []; - return $.when({ + return Promise.resolve({ content: this._mentionManager.generateLinks(value), attachment_ids: _.pluck(this.get('attachment_ids'), 'id'), partner_ids: _.uniq(_.pluck(this._mentionManager.getListenerSelection('@'), 'id')), diff --git a/addons/mail/static/src/js/composers/chatter_composer.js b/addons/mail/static/src/js/composers/chatter_composer.js index 777467e6eb8..8aaaf08dd6d 100644 --- a/addons/mail/static/src/js/composers/chatter_composer.js +++ b/addons/mail/static/src/js/composers/chatter_composer.js @@ -59,109 +59,112 @@ var ChatterComposer = BasicComposer.extend({ * @private * @param {Array} checkedSuggestedPartners list of 'recipient' partners to * complete informations or validate - * @returns {Deferred} resolved with the list of checked suggested partners + * @returns {Promise} resolved with the list of checked suggested partners * (real partner) **/ _checkSuggestedPartners: function (checkedSuggestedPartners) { var self = this; - var checkDone = $.Deferred(); - - var recipients = _.filter(checkedSuggestedPartners, function (recipient) { - return recipient.checked; - }); - var recipientsToFind = _.filter(recipients, function (recipient) { - return (! recipient.partner_id); - }); - var namesToFind = _.pluck(recipientsToFind, 'full_name'); - var recipientsToCheck = _.filter(recipients, function (recipient) { - return (recipient.partner_id && ! recipient.email_address); - }); - var recipientIDs = _.pluck(_.filter(recipients, function (recipient) { - return recipient.partner_id && recipient.email_address; - }), 'partner_id'); - - var namesToRemove = []; - var recipientIDsToRemove = []; - - // have unknown names - // -> call message_get_partner_info_from_emails to try to find - // partner_id - var def; - if (namesToFind.length > 0) { - def = this._rpc({ - model: this._model, - method: 'message_partner_info_from_emails', - args: [[this.context.default_res_id], namesToFind], - }); - } - - // for unknown names + incomplete partners - // -> open popup - cancel = remove from recipients - $.when(def).pipe(function (result) { - result = result || []; - var emailDefs = []; - var recipientPopups = result.concat(recipientsToCheck); - - _.each(recipientPopups, function (partnerInfo) { - var deferred = $.Deferred(); - emailDefs.push(deferred); - - var partnerName = partnerInfo.full_name; - var partnerID = partnerInfo.partner_id; - var parsedEmail = mailUtils.parseEmail(partnerName); - - var dialog = new viewDialogs.FormViewDialog(self, { - res_model: 'res.partner', - res_id: partnerID, - context: { - active_model: self._model, - active_id: self.context.default_res_id, - force_email: true, - ref: 'compound_context', - default_name: parsedEmail[0], - default_email: parsedEmail[1], - }, - title: _t("Please complete customer's informations"), - disable_multiple_selection: true, - }).open(); - dialog.on('closed', self, function () { - deferred.resolve(); - }); - dialog.opened().then(function () { - dialog.form_view.on('on_button_cancel', self, function () { - namesToRemove.push(partnerName); - if (partnerID) { - recipientIDsToRemove.push(partnerID); - } - }); - }); + var checkDone = new Promise(function (resolve, reject) { + var recipients = _.filter(checkedSuggestedPartners, function (recipient) { + return recipient.checked; }); - $.when.apply($, emailDefs).then(function () { - var newNamesToFind = _.difference(namesToFind, namesToRemove); - var def; - if (newNamesToFind.length > 0) { - def = self._rpc({ - model: self._model, - method: 'message_partner_info_from_emails', - args: [[self.context.default_res_id], newNamesToFind, true], + var recipientsToFind = _.filter(recipients, function (recipient) { + return (! recipient.partner_id); + }); + var namesToFind = _.pluck(recipientsToFind, 'full_name'); + var recipientsToCheck = _.filter(recipients, function (recipient) { + return (recipient.partner_id && ! recipient.email_address); + }); + var recipientIDs = _.pluck(_.filter(recipients, function (recipient) { + return recipient.partner_id && recipient.email_address; + }), 'partner_id'); + + var namesToRemove = []; + var recipientIDsToRemove = []; + + // have unknown names + // -> call message_get_partner_info_from_emails to try to find + // partner_id + var def; + if (namesToFind.length > 0) { + def = self._rpc({ + model: self._model, + method: 'message_partner_info_from_emails', + args: [[self.context.default_res_id], namesToFind], + }); + } + + // for unknown names + incomplete partners + // -> open popup - cancel = remove from recipients + Promise.resolve(def).then(function (result) { + result = result || []; + var emailDefs = []; + var recipientPopups = result.concat(recipientsToCheck); + + _.each(recipientPopups, function (partnerInfo) { + var prom = new Promise(function(innerResolve, innerReject) { + var partnerName = partnerInfo.full_name; + var partnerID = partnerInfo.partner_id; + var parsedEmail = mailUtils.parseEmail(partnerName); + + var dialog = new viewDialogs.FormViewDialog(self, { + res_model: 'res.partner', + res_id: partnerID, + context: { + active_model: self._model, + active_id: self.context.default_res_id, + force_email: true, + ref: 'compound_context', + default_name: parsedEmail[0], + default_email: parsedEmail[1], + }, + title: _t("Please complete customer's informations"), + disable_multiple_selection: true, + }).open(); + dialog.on('closed', self, function () { + innerResolve(); + }); + dialog.opened().then(function () { + dialog.form_view.on('on_button_cancel', self, function () { + namesToRemove.push(partnerName); + if (partnerID) { + recipientIDsToRemove.push(partnerID); + } + }); }); - } - $.when(def).pipe(function (result) { - result = result || []; - var recipientPopups = result.concat(recipientsToCheck); - _.each(recipientPopups, function (partnerInfo) { - if ( - partnerInfo.partner_id && - _.indexOf(partnerInfo.partner_id, recipientIDsToRemove) === -1 - ) { - recipientIDs.push(partnerInfo.partner_id); - } }); - }).pipe(function () { - checkDone.resolve(recipientIDs); + + emailDefs.push(prom); + + }); + Promise.all(emailDefs).then(function () { + var newNamesToFind = _.difference(namesToFind, namesToRemove); + var def; + if (newNamesToFind.length > 0) { + def = self._rpc({ + model: self._model, + method: 'message_partner_info_from_emails', + args: [[self.context.default_res_id], newNamesToFind, true], + }); + } + Promise.resolve(def).then(function (result) { + result = result || []; + var recipientPopups = result.concat(recipientsToCheck); + _.each(recipientPopups, function (partnerInfo) { + if ( + partnerInfo.partner_id && + _.indexOf(partnerInfo.partner_id, recipientIDsToRemove) === -1 + ) { + recipientIDs.push(partnerInfo.partner_id); + } + }); + }).then(function () { + resolve(recipientIDs); + }); }); }); }); + return checkDone; }, /** @@ -187,41 +190,40 @@ var ChatterComposer = BasicComposer.extend({ /** * @override * @private - * @returns {$.Deferred} + * @returns {Promise} */ _preprocessMessage: function () { var self = this; - var def = $.Deferred(); - this._super().then(function (message) { - message = _.extend(message, { - subtype: 'mail.mt_comment', - message_type: 'comment', - context: _.defaults({}, self.context, session.user_context), - }); - - // Subtype - if (self.options.isLog) { - message.subtype = 'mail.mt_note'; - } - - // Partner_ids - if (!self.options.isLog) { - var checkedSuggestedPartners = self._getCheckedSuggestedPartners(); - self._checkSuggestedPartners(checkedSuggestedPartners).done(function (partnerIDs) { - message.partner_ids = (message.partner_ids || []).concat(partnerIDs); - // update context - message.context = _.defaults({}, message.context, { - mail_post_autofollow: true, - }); - def.resolve(message); + return new Promise(function (resolve, reject) { + self._super().then(function (message) { + message = _.extend(message, { + subtype: 'mail.mt_comment', + message_type: 'comment', + context: _.defaults({}, self.context, session.user_context), }); - } else { - def.resolve(message); - } + // Subtype + if (self.options.isLog) { + message.subtype = 'mail.mt_note'; + } + + // Partner_ids + if (!self.options.isLog) { + var checkedSuggestedPartners = self._getCheckedSuggestedPartners(); + self._checkSuggestedPartners(checkedSuggestedPartners).then(function (partnerIDs) { + message.partner_ids = (message.partner_ids || []).concat(partnerIDs); + // update context + message.context = _.defaults({}, message.context, { + mail_post_autofollow: true, + }); + resolve(message); + }); + } else { + resolve(message); + } + + }); }); - - return def; }, /** * @override @@ -244,20 +246,20 @@ var ChatterComposer = BasicComposer.extend({ } var self = this; - var recipientDoneDef = $.Deferred(); - - // any operation on the full-composer will reload the record, so - // warn the user that any unsaved changes on the record will be lost. - this.trigger_up('discard_record_changes', { - proceed: function () { - if (self.options.isLog) { - recipientDoneDef.resolve([]); - } else { - var checkedSuggestedPartners = self._getCheckedSuggestedPartners(); - self._checkSuggestedPartners(checkedSuggestedPartners) - .then(recipientDoneDef.resolve.bind(recipientDoneDef)); - } - }, + var recipientDoneDef = new Promise(function (resolve, reject) { + // any operation on the full-composer will reload the record, so + // warn the user that any unsaved changes on the record will be lost. + self.trigger_up('discard_record_changes', { + proceed: function () { + if (self.options.isLog) { + resolve([]); + } else { + var checkedSuggestedPartners = self._getCheckedSuggestedPartners(); + self._checkSuggestedPartners(checkedSuggestedPartners) + .then(resolve); + } + }, + }); }); recipientDoneDef.then(function (partnerIDs) { diff --git a/addons/mail/static/src/js/composers/extended_composer.js b/addons/mail/static/src/js/composers/extended_composer.js index 917b71fa880..1acfba1058c 100644 --- a/addons/mail/static/src/js/composers/extended_composer.js +++ b/addons/mail/static/src/js/composers/extended_composer.js @@ -102,7 +102,7 @@ var ExtendedComposer = BasicComposer.extend({ /** * @override * @private - * @returns {$.Deferred} + * @returns {Promise} */ _preprocessMessage: function () { var self = this; diff --git a/addons/mail/static/src/js/composers/mention_manager.js b/addons/mail/static/src/js/composers/mention_manager.js index 6cbaab5002a..7ad8a36b39a 100644 --- a/addons/mail/static/src/js/composers/mention_manager.js +++ b/addons/mail/static/src/js/composers/mention_manager.js @@ -114,7 +114,7 @@ var MentionManager = Widget.extend({ if (this._activeListener) { var mentionWord = this._mentionWord; - $.when(this._activeListener.fetchCallback(mentionWord)) + Promise.resolve(this._activeListener.fetchCallback(mentionWord)) .then(function (suggestions) { if (mentionWord === self._mentionWord) { // update suggestions only if mentionWord didn't change diff --git a/addons/mail/static/src/js/discuss.js b/addons/mail/static/src/js/discuss.js index 0e3b66dd720..4e88434f86d 100644 --- a/addons/mail/static/src/js/discuss.js +++ b/addons/mail/static/src/js/discuss.js @@ -82,7 +82,7 @@ var PartnerInviteDialog = Dialog.extend({ /** * @private - * @returns {$.Promise} + * @returns {Promise} */ _addChannel: function () { var self = this; @@ -297,7 +297,7 @@ var Discuss = AbstractAction.extend({ * @override */ willStart: function () { - return $.when(this._super(), this.call('mail_service', 'isReady')); + return Promise.all([this._super(), this.call('mail_service', 'isReady')]); }, /** * @override @@ -331,7 +331,7 @@ var Discuss = AbstractAction.extend({ this._extendedComposer.appendTo(this.$('.o_mail_discuss_content'))); defs.push(this._super.apply(this, arguments)); - return $.when.apply($, defs) + return Promise.all(defs) .then(function () { return self._setThread(self._defaultThreadID); }) @@ -474,7 +474,7 @@ var Discuss = AbstractAction.extend({ }, /** * @private - * @returns {$.Promise} + * @returns {Promise} */ _fetchAndRenderThread: function () { var self = this; @@ -567,7 +567,7 @@ var Discuss = AbstractAction.extend({ * loaded when scrolling to the top, so they can't be loaded if there is no * scrollbar) * - * @returns {Deferred} resolved when there are enough messages to fill the + * @returns {Promise} resolved when there are enough messages to fill the * screen, or when there is no more message to fetch */ _loadEnoughMessages: function () { @@ -583,7 +583,7 @@ var Discuss = AbstractAction.extend({ * Load more messages for the current thread * * @private - * @returns {$.Promise} + * @returns {Promise} */ _loadMoreMessages: function () { var self = this; @@ -647,7 +647,7 @@ var Discuss = AbstractAction.extend({ autoFocus: true, source: function (request, response) { self._lastSearchVal = _.escape(request.term); - self._searchChannel(self._lastSearchVal).done(function (result){ + self._searchChannel(self._lastSearchVal).then(function (result){ result.push({ label: _.str.sprintf( '' + _t("Create %s (Public)") + '', @@ -689,7 +689,7 @@ var Discuss = AbstractAction.extend({ autoFocus: true, source: function (request, response) { self._lastSearchVal = _.escape(request.term); - self.call('mail_service', 'searchPartner', self._lastSearchVal, 10).done(response); + self.call('mail_service', 'searchPartner', self._lastSearchVal, 10).then(response); }, select: function (ev, ui) { var partnerID = ui.item.id; @@ -819,7 +819,7 @@ var Discuss = AbstractAction.extend({ * Renders, binds events and appends a thread widget. * * @private - * @returns {Deferred} + * @returns {Promise} */ _renderThread: function () { this._threadWidget = new ThreadWidget(this, { @@ -877,7 +877,7 @@ var Discuss = AbstractAction.extend({ /** * @private * @param {string} searchVal - * @returns {$.Promise} + * @returns {Promise} */ _searchChannel: function (searchVal){ return this._rpc({ @@ -915,7 +915,7 @@ var Discuss = AbstractAction.extend({ this._extendedComposer.do_show(); this._threadWidget.scrollToMessage({ - msgID: messageID, + messageID: messageID, duration: 200, onlyIfNecessary: true }); @@ -927,7 +927,7 @@ var Discuss = AbstractAction.extend({ * * @private * @param {integer} threadID a thread with such ID - * @returns {$.Promise} + * @returns {Promise} */ _setThread: function (threadID) { var self = this; @@ -1483,9 +1483,6 @@ var Discuss = AbstractAction.extend({ } else { self._threadWidget.scrollToBottom(); } - }) - .fail(function () { - // todo: display notifications }); }, /** diff --git a/addons/mail/static/src/js/discuss_mobile.js b/addons/mail/static/src/js/discuss_mobile.js index 549e05b78fd..617700c20c1 100644 --- a/addons/mail/static/src/js/discuss_mobile.js +++ b/addons/mail/static/src/js/discuss_mobile.js @@ -116,7 +116,7 @@ Discuss.include({ this._thread = thread; if (thread.getType() !== 'mailbox') { this.call('mail_service', 'openThreadWindow', threadID); - return $.when(); + return Promise.resolve(); } else { return this._super.apply(this, arguments); } @@ -179,7 +179,7 @@ Discuss.include({ }); def = this.call('mail_service', 'getChannelPreviews', channels); } - return $.when(def).then(function (previews) { + return Promise.resolve(def).then(function (previews) { // update content if (inMailbox) { if (!previouslyInInbox) { diff --git a/addons/mail/static/src/js/document_viewer.js b/addons/mail/static/src/js/document_viewer.js index 9affc3f41cf..77f33d152c0 100644 --- a/addons/mail/static/src/js/document_viewer.js +++ b/addons/mail/static/src/js/document_viewer.js @@ -76,7 +76,7 @@ var DocumentViewer = Widget.extend({ start: function () { this.$el.modal('show'); this.$el.on('hidden.bs.modal', _.bind(this._onDestroy, this)); - this.$('.o_viewer_img').load(_.bind(this._onImageLoaded, this)); + this.$('.o_viewer_img').on("load", _.bind(this._onImageLoaded, this)); this.$('[data-toggle="tooltip"]').tooltip({delay: 0}); return this._super.apply(this, arguments); }, @@ -132,7 +132,7 @@ var DocumentViewer = Widget.extend({ this.$('.o_viewer_content').html(QWeb.render('DocumentViewer.Content', { widget: this })); - this.$('.o_viewer_img').load(_.bind(this._onImageLoaded, this)); + this.$('.o_viewer_img').on("load", _.bind(this._onImageLoaded, this)); this.$('[data-toggle="tooltip"]').tooltip({delay: 0}); this._reset(); }, diff --git a/addons/mail/static/src/js/followers.js b/addons/mail/static/src/js/followers.js index 0a760516607..a8475f43219 100644 --- a/addons/mail/static/src/js/followers.js +++ b/addons/mail/static/src/js/followers.js @@ -55,14 +55,14 @@ var Followers = AbstractField.extend({ // note: the rendering of this widget is asynchronous as it needs to // fetch the details of the followers, but it performs a first rendering // synchronously (_displayGeneric), and updates its rendering once it - // has fetched the required data, so this function doesn't return a deferred + // has fetched the required data, so this function doesn't return a promise // as we don't want to wait to the data to be loaded to display the widget var self = this; var fetch_def = this.dp.add(this._readFollowers()); concurrency.rejectAfter(concurrency.delay(0), fetch_def).then(this._displayGeneric.bind(this)); - fetch_def.then(function () { + return fetch_def.then(function () { self._displayButtons(); self._displayFollowers(self.followers); if (self.subtypes) { // current user is follower @@ -203,7 +203,7 @@ var Followers = AbstractField.extend({ params: { follower_ids: missing_ids, res_model: this.model } }); } - return $.when(def).then(function (results) { + return Promise.resolve(def).then(function (results) { if (results) { self.followers = _.uniq(results.followers.concat(self.followers), 'id'); if (results.subtypes) { //read_followers will return False if current user is not in the list @@ -241,27 +241,27 @@ var Followers = AbstractField.extend({ */ _unfollow: function (ids) { var self = this; - var def = $.Deferred(); - var text = _t("Warning! \n If you remove a follower, he won't be notified of any email or discussion on this document.\n Do you really want to remove this follower ?"); - Dialog.confirm(this, text, { - confirm_callback: function () { - var args = [ - [self.res_id], - ids.partner_ids, - ids.channel_ids, - {}, // FIXME - ]; - self._rpc({ + return new Promise(function (resolve, reject) { + var text = _t("Warning! \n If you remove a follower, he won't be notified of any email or discussion on this document.\n Do you really want to remove this follower ?"); + Dialog.confirm(this, text, { + confirm_callback: function () { + var args = [ + [self.res_id], + ids.partner_ids, + ids.channel_ids, + {}, // FIXME + ]; + self._rpc({ model: self.model, method: 'message_unsubscribe', args: args }) .then(self._reload.bind(self)); - def.resolve(); - }, - cancel_callback: def.reject.bind(def), + resolve(); + }, + cancel_callback: reject, + }); }); - return def; }, _updateSubscription: function (event, followerID, isChannel) { var ids = {}; @@ -290,7 +290,7 @@ var Followers = AbstractField.extend({ // If no more subtype followed, unsubscribe the follower if (!checklist.length) { - this._unfollow(ids).fail(function () { + this._unfollow(ids).guardedCatch(function () { $(event.currentTarget).find('input').addBack('input').prop('checked', true); }); } else { diff --git a/addons/mail/static/src/js/form_renderer.js b/addons/mail/static/src/js/form_renderer.js index 91d2c2033c4..e7b767c0b2f 100644 --- a/addons/mail/static/src/js/form_renderer.js +++ b/addons/mail/static/src/js/form_renderer.js @@ -50,18 +50,24 @@ FormRenderer.include({ * @private */ _renderNode: function (node) { + var self = this; if (node.tag === 'div' && node.attrs.class === 'oe_chatter') { if (!this.chatter) { this.chatter = new Chatter(this, this.state, this.mailFields, { isEditable: this.activeActions.edit, viewType: 'form', }); - this.chatter.appendTo($('
')); - this._handleAttributes(this.chatter.$el, node); + + var $temporaryParentDiv = $('
'); + this.defs.push(this.chatter.appendTo($temporaryParentDiv).then(function () { + self.chatter.$el.unwrap(); + self._handleAttributes(self.chatter.$el, node); + })); + return $temporaryParentDiv; } else { this.chatter.update(this.state); + return this.chatter.$el; } - return this.chatter.$el; } else { return this._super.apply(this, arguments); } diff --git a/addons/mail/static/src/js/many2many_tags_email.js b/addons/mail/static/src/js/many2many_tags_email.js index e66412a7a2c..6648ef50423 100644 --- a/addons/mail/static/src/js/many2many_tags_email.js +++ b/addons/mail/static/src/js/many2many_tags_email.js @@ -20,7 +20,7 @@ BasicModel.include({ * @private * @param {Object} record - an element from the localData * @param {string} fieldName - * @return {Deferred} the deferred is resolved with the + * @return {Promise} the promise is resolved with the * invalidPartnerIds */ _setInvalidMany2ManyTagsEmail: function (record, fieldName) { @@ -42,10 +42,10 @@ BasicModel.include({ var changes = {operation: 'DELETE', ids: _.pluck(invalidPartnerIds, 'id')}; def = this._applyX2ManyChange(record, fieldName, changes); } - return $.when(def).then(function () { - return $.when({ + return Promise.resolve(def).then(function () { + return { invalidPartnerIds: _.pluck(invalidPartnerIds, 'res_id'), - }); + }; }); }, }); @@ -65,7 +65,7 @@ var FieldMany2ManyTagsEmail = M2MTags.extend({ * Open a popup for each invalid partners (without email) to fill the email. * * @private - * @returns {Deferred} + * @returns {Promise} */ _checkEmailPopup: function () { var self = this; @@ -75,25 +75,25 @@ var FieldMany2ManyTagsEmail = M2MTags.extend({ // propose the user to correct invalid partners _.each(this.record.specialData[this.name].invalidPartnerIds, function (resID) { - var def = $.Deferred(); - popupDefs.push(def); - - var pop = new form_common.FormViewDialog(self, { - res_model: self.field.relation, - res_id: resID, - context: self.record.context, - title: "", - on_saved: function (record) { - if (record.data.email) { - validPartners.push(record.res_id); - } - }, - }).open(); - pop.on('closed', self, function () { - def.resolve(); + var def = new Promise(function (resolve, reject) { + var pop = new form_common.FormViewDialog(self, { + res_model: self.field.relation, + res_id: resID, + context: self.record.context, + title: "", + on_saved: function (record) { + if (record.data.email) { + validPartners.push(record.res_id); + } + }, + }).open(); + pop.on('closed', self, function () { + resolve(); + }); }); + popupDefs.push(def); }); - return $.when.apply($, popupDefs).then(function() { + return Promise.all(popupDefs).then(function() { // All popups have been processed for the given ids // It is now time to set the final value with valid partners ids. validPartners = _.uniq(validPartners); @@ -117,14 +117,14 @@ var FieldMany2ManyTagsEmail = M2MTags.extend({ */ _render: function () { var self = this; - var def = $.Deferred(); var _super = this._super.bind(this); - if (this.record.specialData[this.name].invalidPartnerIds.length) { - def = this._checkEmailPopup(); - } else { - def.resolve(); - } - return def.then(function () { + return new Promise(function (resolve, reject) { + if (self.record.specialData[self.name].invalidPartnerIds.length) { + resolve(self._checkEmailPopup()); + } else { + resolve(); + } + }).then(function () { return _super.apply(self, arguments); }); }, diff --git a/addons/mail/static/src/js/models/messages/message.js b/addons/mail/static/src/js/models/messages/message.js index a414a11874b..77e4120da29 100644 --- a/addons/mail/static/src/js/models/messages/message.js +++ b/addons/mail/static/src/js/models/messages/message.js @@ -511,7 +511,7 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin * @see {mail.Manager.Notification} for the receipt of 'toggle_star' * notification after this rpc. * - * @return {$.Promise} + * @return {Promise} */ toggleStarStatus: function () { return this._rpc({ diff --git a/addons/mail/static/src/js/models/threads/abstract_thread.js b/addons/mail/static/src/js/models/threads/abstract_thread.js index a515c4d2f8a..192d0e58c93 100644 --- a/addons/mail/static/src/js/models/threads/abstract_thread.js +++ b/addons/mail/static/src/js/models/threads/abstract_thread.js @@ -150,18 +150,18 @@ var AbstractThread = Class.extend(Mixins.EventDispatcherMixin, { * Mark the thread as read, which resets the unread counter to 0. This is * only performed if the unread counter is not 0. * - * @returns {$.Promise} + * @returns {Promise} */ markAsRead: function () { if (this._unreadCounter > 0) { return this._markAsRead(); } - return $.when(); + return Promise.resolve(); }, /** * Post a message on this thread * - * @returns {$.Promise} resolved with the message object to be sent to the + * @returns {Promise} resolved with the message object to be sent to the * server */ postMessage: function () { @@ -200,22 +200,22 @@ var AbstractThread = Class.extend(Mixins.EventDispatcherMixin, { * Mark the thread as read * * @private - * @returns {$.Promise} + * @returns {Promise} */ _markAsRead: function () { this.resetUnreadCounter(); - return $.when(); + return Promise.resolve(); }, /** * Post a message on this thread * * @abstract * @private - * @returns {$.Promise} resolved with the message object to be sent to the + * @returns {Promise} resolved with the message object to be sent to the * server */ _postMessage: function () { - return $.when(); + return Promise.resolve(); }, /** * Warn views (e.g. discuss app, thread window, etc.) to update visually diff --git a/addons/mail/static/src/js/models/threads/channel.js b/addons/mail/static/src/js/models/threads/channel.js index 9f0f37264db..9e9f3ac6a39 100644 --- a/addons/mail/static/src/js/models/threads/channel.js +++ b/addons/mail/static/src/js/models/threads/channel.js @@ -70,7 +70,7 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, { this._isMyselfModerator = data.is_moderator; this._lastMessageDate = undefined; this._members = data.members || []; - // Deferred that is resolved on fetched members of this channel. + // Promise that is resolved on fetched members of this channel. this._membersDef = undefined; // number of messages that are 'needaction', which is equivalent to the // number of messages in this channel that are in inbox. @@ -129,19 +129,21 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, { * @pverride */ detach: function () { - this._super.apply(this, arguments); - this._rpc({ - model: 'mail.channel', - method: 'channel_minimize', - args: [this.getUUID(), true], - }, { - shadow: true, + var self = this; + return this._super.apply(this, arguments).then(function () { + self._rpc({ + model: 'mail.channel', + method: 'channel_minimize', + args: [self.getUUID(), true], + }, { + shadow: true, + }); }); }, /** * Force fetch members of the channel * - * @returns {$.Promise} resolved with list of channel listeners + * @returns {Promise} resolved with list of channel listeners */ forceFetchMembers: function () { this._membersDef = undefined; @@ -186,7 +188,7 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, { /** * Get listeners of a channel * - * @returns {$.Promise>} resolved with list of list of + * @returns {Promise} resolved with list of list of * channel listeners. */ getMentionPartnerSuggestions: function () { @@ -302,7 +304,7 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, { * Unsubscribes from channel * * @abstract - * @returns {$.Promise} resolve when unsubscribed + * @returns {Promise} resolve when unsubscribed */ unsubscribe: function () {}, /** @@ -381,19 +383,19 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, { * * @override * @private - * @returns {$.Promise} resolved when message has been marked as read + * @returns {Promise} resolved when message has been marked as read */ _markAsRead: function () { var superDef = this._super.apply(this, arguments); var seenDef = this._notifySeen(); - return $.when(superDef, seenDef); + return Promise.all([superDef, seenDef]); }, /** * @override {mail.model.ThreadTypingMixin} * @private * @param {Object} params * @param {boolean} params.typing - * @returns {$.Promise} + * @returns {Promise} */ _notifyMyselfTyping: function (params) { return this._rpc({ @@ -424,7 +426,7 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, { * @override * @private * @param {Object} data data related to the new message - * @returns {$.Promise} resolved when the message has been sent to + * @returns {Promise} resolved when the message has been sent to * the server, with the object message that has been sent to the server. */ _postMessage: function (data) { diff --git a/addons/mail/static/src/js/models/threads/document_thread.js b/addons/mail/static/src/js/models/threads/document_thread.js index 6c15b39d25e..08e441610ee 100644 --- a/addons/mail/static/src/js/models/threads/document_thread.js +++ b/addons/mail/static/src/js/models/threads/document_thread.js @@ -92,7 +92,7 @@ var DocumentThread = Thread.extend({ * @param {Object} options * @param {boolean} [options.forceFetch] if true, fetch anyway, as user * clicked on 'load more'. - * @returns {$.Promise} + * @returns {Promise} */ fetchMessages: function (options) { var self = this; @@ -217,14 +217,14 @@ var DocumentThread = Thread.extend({ * Get most up to date messageIDs * * @private - * @returns {$.Promise} resolved when message IDs have been fetched and set + * @returns {Promise} resolved when message IDs have been fetched and set * in the model */ _fetchMessageIDs: function () { var self = this; var resID = this.getDocumentID(); if (!resID || !this._mustFetchMessageIDs) { - return $.when(); + return Promise.resolve(); } return this._rpc({ model: this.getDocumentModel(), @@ -239,7 +239,7 @@ var DocumentThread = Thread.extend({ * @private * @param {Object} options * @param {boolean} [options.forceFetch] - * @returns {$.Promise} resolved when messages have been fetched + document + * @returns {Promise} resolved when messages have been fetched + document * thread has updated messages */ _fetchMessages: function (options) { @@ -273,7 +273,7 @@ var DocumentThread = Thread.extend({ }); }); } else { - return $.when(); + return Promise.resolve(); } }); @@ -284,7 +284,7 @@ var DocumentThread = Thread.extend({ * * @override * @private - * @returns {$.Promise} resolved when messages have been marked as read on + * @returns {Promise} resolved when messages have been marked as read on * the server. */ _markAsRead: function () { @@ -299,7 +299,7 @@ var DocumentThread = Thread.extend({ * @override * @private * @param {Object} data data related to the new message - * @return {$.Promise} resolved when the message has been sent to + * @return {Promise} resolved when the message has been sent to * the server, with the object message that has been sent to the server. */ _postMessage: function (data) { diff --git a/addons/mail/static/src/js/models/threads/livechat.js b/addons/mail/static/src/js/models/threads/livechat.js index 45cab5aef10..c3b4a5e2b4d 100644 --- a/addons/mail/static/src/js/models/threads/livechat.js +++ b/addons/mail/static/src/js/models/threads/livechat.js @@ -34,7 +34,7 @@ var Livechat = TwoUserChannel.extend({ * display of a user that is typing. * * @override - * @returns {$.Promise>} resolved with list of list of + * @returns {Promise} resolved with list of list of * livechat members. */ getMentionPartnerSuggestions: function () { diff --git a/addons/mail/static/src/js/models/threads/mailbox.js b/addons/mail/static/src/js/models/threads/mailbox.js index 263072f575e..bedd6af27e1 100644 --- a/addons/mail/static/src/js/models/threads/mailbox.js +++ b/addons/mail/static/src/js/models/threads/mailbox.js @@ -59,7 +59,7 @@ var Mailbox = SearchableThread.extend({ * channel, only keep the lastest message of this document/channel in the * previews. * - * @returns {$.Promise} + * @returns {Promise} */ getMessagePreviews: function () { var self = this; @@ -119,7 +119,7 @@ var Mailbox = SearchableThread.extend({ * makes only sense for 'Inbox'. * * @param {Array} domain - * @return {$.Promise} resolved when all messages have been marked as read + * @return {Promise} resolved when all messages have been marked as read * on the server */ markAllMessagesAsRead: function (domain) { @@ -133,7 +133,7 @@ var Mailbox = SearchableThread.extend({ }, }); } - return $.when(); + return Promise.resolve(); }, //-------------------------------------------------------------------------- diff --git a/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js b/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js index f995b4e09b3..7d97538dbb9 100644 --- a/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js +++ b/addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js @@ -239,11 +239,11 @@ var ThreadTypingMixin = { * @private * @param {Object} params * @param {boolean} params.typing whether we are typing something or not - * @returns {$.Promise} resolved if the server is notified, rejected + * @returns {Promise} resolved if the server is notified, rejected * otherwise */ _notifyMyselfTyping: function (params) { - return $.when(); + return Promise.resolve(); }, /** * Warn views that the list of users that are currently typing on this diff --git a/addons/mail/static/src/js/models/threads/multi_user_channel.js b/addons/mail/static/src/js/models/threads/multi_user_channel.js index bac417c361d..3dab9337502 100644 --- a/addons/mail/static/src/js/models/threads/multi_user_channel.js +++ b/addons/mail/static/src/js/models/threads/multi_user_channel.js @@ -56,7 +56,7 @@ var MultiUserChannel = Channel.extend({ * Unsubscribes from channel * * @override - * @returns {$.Promise} resolve when unsubscribed + * @returns {Promise} resolve when unsubscribed */ unsubscribe: function () { return this._rpc({ diff --git a/addons/mail/static/src/js/models/threads/searchable_thread.js b/addons/mail/static/src/js/models/threads/searchable_thread.js index 8ddff5457af..511bc6348c8 100644 --- a/addons/mail/static/src/js/models/threads/searchable_thread.js +++ b/addons/mail/static/src/js/models/threads/searchable_thread.js @@ -49,7 +49,7 @@ var SearchableThread = Thread.extend({ } var cache = this._getCache(domain); if (cache.loaded) { - return $.when(cache.messages); + return Promise.all(cache.messages); } else { return this._fetchMessages(domain); } @@ -159,7 +159,7 @@ var SearchableThread = Thread.extend({ * @private * @param {Array} [pDomain] filter on the messages of the channel * @param {boolean} [loadMore] Whether it should load more message - * @return {$.Promise} resolved with list of messages + * @return {Promise} resolved with list of messages */ _fetchMessages: function (pDomain, loadMore) { var self = this; diff --git a/addons/mail/static/src/js/models/threads/thread.js b/addons/mail/static/src/js/models/threads/thread.js index 8c622779bb3..8f4e8956d27 100644 --- a/addons/mail/static/src/js/models/threads/thread.js +++ b/addons/mail/static/src/js/models/threads/thread.js @@ -58,12 +58,13 @@ var Thread = AbstractThread.extend(ServicesMixin, { * of this thread. Otherwise unfold it while detaching it. * @param {boolean} [options.passively=false] if set, if the thread window * will be created passively. + * @returns {Promise} */ detach: function (options) { options = options || {}; this._detached = true; this._folded = options.keepFoldState ? this._folded : false; - this._warnUpdatedWindowState({ + return this._warnUpdatedWindowState({ passively: options.passively, }); }, @@ -79,10 +80,10 @@ var Thread = AbstractThread.extend(ServicesMixin, { * `this.LIMIT` number of messages at a time. * * @abstract - * @returns {$.Promise} + * @returns {Promise} */ fetchMessages: function () { - return $.when([]); + return Promise.resolve([]); }, /** * Updates the folded state of the thread. Must be overriden to reflect @@ -110,10 +111,10 @@ var Thread = AbstractThread.extend(ServicesMixin, { * By default, a thread has not listener. * * @abstract - * @returns {$.Promise>} + * @returns {Promise>} */ getMentionPartnerSuggestions: function () { - return $.when([]); + return Promise.resolve([]); }, /** * Returns the information required to render the preview of this channel. @@ -287,10 +288,10 @@ var Thread = AbstractThread.extend(ServicesMixin, { /** * @abstract * @private - * @return {$.Promise} + * @return {Promise} */ _fetchMessages: function () { - return $.when(); + return Promise.resolve(); }, /** * Replace character representations of emojis by their unicode @@ -330,7 +331,7 @@ var Thread = AbstractThread.extend(ServicesMixin, { * @abstract * @private * @param {Object} data - * @returns {$.Promise} resolved with the message object to be sent + * @returns {Promise} resolved with the message object to be sent * to the server */ _postMessage: function (data) { @@ -355,7 +356,7 @@ var Thread = AbstractThread.extend(ServicesMixin, { messageData.subject = data.subject; } return this._super.apply(this, arguments).then(function () { - return $.when(messageData); + return messageData; }); }, /** @@ -374,10 +375,11 @@ var Thread = AbstractThread.extend(ServicesMixin, { * @private * @param {Object} [options={}] * @param {boolean} [options.passively=false] + * @returns {Promise} */ _warnUpdatedWindowState: function (options) { options = options || {}; - this.call('mail_service', 'updateThreadWindow', this.getID(), options); + return this.call('mail_service', 'updateThreadWindow', this.getID(), options); }, }); diff --git a/addons/mail/static/src/js/models/threads/two_user_channel.js b/addons/mail/static/src/js/models/threads/two_user_channel.js index 204fd95eb82..54a4145bd13 100644 --- a/addons/mail/static/src/js/models/threads/two_user_channel.js +++ b/addons/mail/static/src/js/models/threads/two_user_channel.js @@ -15,7 +15,7 @@ var TwoUserChannel = Channel.extend({ * Unpin from two-user thread * * @override - * @returns {$.Promise} resolve when unsubscribed + * @returns {Promise} resolve when unsubscribed */ unsubscribe: function () { return this._rpc({ diff --git a/addons/mail/static/src/js/services/mail_document_thread_manager.js b/addons/mail/static/src/js/services/mail_document_thread_manager.js index a98ae317c9d..f1d9eefb44a 100644 --- a/addons/mail/static/src/js/services/mail_document_thread_manager.js +++ b/addons/mail/static/src/js/services/mail_document_thread_manager.js @@ -182,7 +182,7 @@ MailManager.include({ resModel: resModel, }); } - this._super.apply(this, arguments); + return this._super.apply(this, arguments); }, /** * Updates the thread windows related to document threads. diff --git a/addons/mail/static/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js index 5eb588d24e7..27a7183ec88 100644 --- a/addons/mail/static/src/js/services/mail_manager.js +++ b/addons/mail/static/src/js/services/mail_manager.js @@ -80,13 +80,14 @@ var MailManager = AbstractService.extend({ * the unread_counter of channel. * @param {boolean} [options.silent] whether it should inform in the mailBus * of the newly created message. - * @returns {mail.model.Message} message object + * @returns {Promise} */ addMessage: function (data, options) { options = options || {}; var message = this.getMessage(data.id); + var prom = Promise.resolve(); if (!message) { - message = this._addNewMessage(data, options); + prom = this._addNewMessage(data, options); } else { if (data.moderation_status === 'accepted') { message.setModerationStatus('accepted', { @@ -97,7 +98,7 @@ var MailManager = AbstractService.extend({ this._addMessageToThreads(message, options); } } - return message; + return prom; }, /** * Creates a channel, can be either a true channel or a DM chat based on @@ -105,7 +106,7 @@ var MailManager = AbstractService.extend({ * * @param {integer|string} name id of partner (in case of dm) or name * @param {string} type ['dm_chat', 'public', 'private'] - * @returns {$.Promise} resolved with ID of the newly created + * @returns {Promise} resolved with ID of the newly created * channel */ createChannel: function (name, type) { @@ -150,7 +151,7 @@ var MailManager = AbstractService.extend({ * Returns the content that will be shown in the mail navbar dropdown * * @param {mail.model.Channel[]} channels - * @returns {$.Promise} resolved with list of channel previews + * @returns {Promise} resolved with list of channel previews */ getChannelPreviews: function (channels) { var self = this; @@ -227,23 +228,22 @@ var MailManager = AbstractService.extend({ * * @param {string} [filter=undefined] specify 'chat' or 'channels' to only * get previews for that type of threads - * @returns {$.Promise} resolved with list of objects that have a + * @returns {Promise} resolved with list of objects that have a * valid format for rendering a messaging menu preview items. */ getSystrayPreviews: function (filter) { var self = this; - var defs = []; var channelDef = this._getSystrayChannelPreviews(filter); var inboxDef = this._getSystrayInboxPreviews(filter); var failureDef = this._getSystrayMailFailurePreviews(filter); - defs = defs.concat([channelDef, inboxDef, failureDef]); - return $.when.apply($, defs) - .then(function (previewsChannel, previewsInbox, previewsFailure) { + + return Promise.all([channelDef, inboxDef, failureDef]) + .then(function (result) { //previewsChannel, previewsInbox, previewsFailure // order: failures > inbox > channel, each group must be sorted - previewsChannel = self._sortPreviews(previewsChannel); - previewsInbox = self._sortPreviews(previewsInbox); - previewsFailure = self._sortPreviews(previewsFailure); + var previewsChannel = self._sortPreviews(result[0]); + var previewsInbox = self._sortPreviews(result[1]); + var previewsFailure = self._sortPreviews(result[2]); return _.union(previewsFailure, previewsInbox, previewsChannel); }); }, @@ -295,7 +295,7 @@ var MailManager = AbstractService.extend({ * This is the case when it has fetched the initial state from the server, * by means of the route 'mail/init_messaging' * - * @returns {$.Promise} + * @returns {Promise} */ isReady: function () { return this._isReady; @@ -306,24 +306,24 @@ var MailManager = AbstractService.extend({ * * @param {integer} channelID * @param {Object} [options] options to be passed on channel add - * @returns {$.Promise} resolved with channelID joined + * @returns {Promise} resolved with channelID joined */ joinChannel: function (channelID, options) { - var def; + var prom; var channel = this.getChannel(channelID); if (channel) { // channel already joined - def = $.when(channelID); + prom = Promise.resolve(channelID); } else { - def = this._joinAndAddChannel(channelID, options); + prom = this._joinAndAddChannel(channelID, options); } - return def; + return prom; }, /** * Mark messages as read * * @param {Array} messageIDs list of messages IDs - * @returns {$.Promise} resolved when messages have been marked as read on + * @returns {Promise} resolved when messages have been marked as read on * the server. */ markMessagesAsRead: function (messageIDs) { @@ -341,7 +341,7 @@ var MailManager = AbstractService.extend({ args: [ids], }); } else { - return $.when(); + return Promise.resolve(); } }, /** @@ -396,19 +396,19 @@ var MailManager = AbstractService.extend({ * * @param {string} searchVal * @param {integer} limit max number of found partners in the response - * @returns {$.Promise} list of found partners (matching + * @returns {Promise} list of found partners (matching * 'searchVal') */ searchPartner: function (searchVal, limit) { - var def = $.Deferred(); + var self = this; var partners = this._searchPartnerPrefetch(searchVal, limit); - - if (!partners.length) { - def = this._searchPartnerFetch(searchVal, limit); - } else { - def = $.when(partners); - } - return def.then(function (partners) { + return new Promise(function (resolve, reject) { + if (!partners.length) { + resolve(self._searchPartnerFetch(searchVal, limit)); + } else { + resolve(partners); + } + }).then(function (partners) { var suggestions = _.map(partners, function (partner) { return { id: partner.id, @@ -422,7 +422,7 @@ var MailManager = AbstractService.extend({ /** * Unstars all messages from all channels * - * @returns {$.Promise} + * @returns {Promise} */ unstarAll: function () { return this._rpc({ @@ -448,11 +448,13 @@ var MailManager = AbstractService.extend({ * @param {string} [data.state] e.g. 'open', 'folded' * @param {Object|integer} [options=undefined] * @param {boolean} [options.silent=false] - * @returns {integer} the ID of the newly or already existing channel + * @returns {Promise} resolves with the ID of the newly or already + * existing channel */ _addChannel: function (data, options) { options = typeof options === 'object' ? options : {}; var channel = this.getChannel(data.id); + var proms = []; if (!channel) { channel = this._makeChannel(data, options); if (channel.getType() === 'dm_chat') { @@ -462,14 +464,16 @@ var MailManager = AbstractService.extend({ if (data.last_message) { // channel_info in mobile, necessary for showing channel // preview in mobile - this.addMessage(data.last_message); + proms.push(this.addMessage(data.last_message)); } this._sortThreads(); if (!options.silent) { - this._mailBus.trigger('new_channel', channel); + this._mailBus.trigger('new_channel', channel, proms); } } - return channel.getID(); + return Promise.all(proms).then(function () { + return channel.getID(); + }); }, /** * Add a new mailbox @@ -516,21 +520,23 @@ var MailManager = AbstractService.extend({ * @private * @param {Object} data * @param {Object} options - * @returns {mail.model.Message} + * @return {Promise} */ _addNewMessage: function (data, options) { + var self = this; var message = this._makeMessage(data); // Keep the array ordered by ID when inserting the new message var index = _.sortedIndex(this._messages, message, function (msg) { return msg.getID(); }); this._messages.splice(index, 0, message); - this._addNewMessagePostprocessThread(message, options); - this._addMessageToThreads(message, options); - if (!options.silent) { - this._mailBus.trigger('new_message', message); - } - return message; + return this._addNewMessagePostprocessThread(message, options).then(function () { + self._addMessageToThreads(message, options); + if (!options.silent) { + self._mailBus.trigger('new_message', message); + } + return message; + }); }, /** * For newly added message, postprocess threads linked to this message @@ -541,9 +547,11 @@ var MailManager = AbstractService.extend({ * @param {Array} [options.domain] * @param {boolean} [options.incrementUnread] * @param {boolean} [options.showNotification] + * @returns {Promise} */ _addNewMessagePostprocessThread: function (message, options) { var self = this; + var proms = []; _.each(message.getThreadIDs(), function (threadID) { var thread = self.getThread(threadID); if (thread) { @@ -553,6 +561,7 @@ var MailManager = AbstractService.extend({ !message.isSystemNotification() ) { if (thread.isTwoUserThread() && options.showNotification) { + var prom = Promise.resolve(); if ( !self._isDiscussOpen() && !config.device.isMobile && @@ -560,17 +569,21 @@ var MailManager = AbstractService.extend({ ) { // automatically open thread window // while keeping it unread - thread.detach({ passively: true }); - } - var query = { isVisible: false }; - self._mailBus.trigger('is_thread_bottom_visible', thread, query); - if (!self.call('bus_service', 'isOdooFocused') || !query.isVisible) { - self._notifyIncomingMessage(message); + prom = thread.detach({ passively: true }); } + proms.push(prom); + prom.then(function () { + var query = { isVisible: false }; + self._mailBus.trigger('is_thread_bottom_visible', thread, query); + if (!self.call('bus_service', 'isOdooFocused') || !query.isVisible) { + self._notifyIncomingMessage(message); + } + }); } } } }); + return Promise.all(proms); }, /** * Create a Channel (other than a DM) @@ -578,7 +591,7 @@ var MailManager = AbstractService.extend({ * @private * @param {string} name * @param {string} type - * @returns {$.Promise} ID of the created channel + * @returns {Promise} ID of the created channel */ _createChannel: function (name, type) { var context = _.extend({ isMobile: config.device.isMobile }, session.user_context); @@ -595,7 +608,7 @@ var MailManager = AbstractService.extend({ * * @private * @param {string} name - * @returns {$.Promise} ID of the created channel + * @returns {Promise} ID of the created channel */ _createDMChat: function (name) { var context = _.extend({ isMobile: config.device.isMobile }, session.user_context); @@ -610,7 +623,7 @@ var MailManager = AbstractService.extend({ /** * @private * @param {integer[]} channelIDs - * @returns {$.Promise} resolved with list of channel preview + * @returns {Promise} resolved with list of channel preview */ _fetchChannelPreviews: function (channelIDs) { return this._rpc({ @@ -652,7 +665,7 @@ var MailManager = AbstractService.extend({ * * @private * @param {mail.model.Channel[]} channels - * @returns {$.Deferred} resolved with list of channel previews + * @returns {Promise} resolved with list of channel previews */ _getChannelPreviews: function (channels) { var self = this; @@ -667,19 +680,22 @@ var MailManager = AbstractService.extend({ }); fetchDef = this._fetchChannelPreviews(ids); } else { - fetchDef = $.when(); + fetchDef = Promise.resolve(); } return fetchDef.then(function (fetchedPreviews) { // add fetched messages + var proms = []; _.each(fetchedPreviews, function (fetchedPreview) { if (fetchedPreview.last_message) { - self.addMessage(fetchedPreview.last_message); + proms.push(self.addMessage(fetchedPreview.last_message)); } // mark the channel as previewed, so that we do not need to // fetch preview again var channel = self.getChannel(fetchedPreview.id); channel.markAsPreviewed(); }); + return Promise.all(proms); + }).then(function () { return _.map(channels, function (channel) { return channel.getPreview(); }); @@ -694,7 +710,7 @@ var MailManager = AbstractService.extend({ * the documentID is omitted for a model preview, whereas it is set for a * preview of a failure related to a document. * - * @returns {$.Promise} resolved with previews of mail failures + * @returns {Promise} resolved with previews of mail failures */ _getMailFailurePreviews: function () { // items = list of objects: @@ -754,7 +770,7 @@ var MailManager = AbstractService.extend({ * @private * @param {string|undefined} [filter] the filter to apply on channel * previews in systray messaging menu - * @returns {$.Promise} resolved with object valid for template + * @returns {Promise} resolved with object valid for template * mail.Preview on channel previews, based on selected filter */ _getSystrayChannelPreviews: function (filter) { @@ -796,7 +812,7 @@ var MailManager = AbstractService.extend({ * * @private * @param {string|undefined} [filter] - * @returns {$.Deferred} resolved with valid objects for template + * @returns {Promise} resolved with valid objects for template * mail.Preview on inbox message previews */ _getSystrayInboxPreviews: function (filter) { @@ -806,7 +822,7 @@ var MailManager = AbstractService.extend({ var inbox = this.getMailbox('inbox'); return inbox.getMessagePreviews(); } else { - return $.when([]); + return Promise.resolve([]); } }, /** @@ -820,7 +836,7 @@ var MailManager = AbstractService.extend({ if (filter === 'mailbox_inbox' || !filter) { return this._getMailFailurePreviews(); } else { - return $.when([]); + return Promise.resolve([]); } }, /** @@ -863,7 +879,7 @@ var MailManager = AbstractService.extend({ * @private * @param {integer|string} channelID * @param {Object} options options passed on channel add - * @returns {$.Promise} channelID of joined and added channel + * @returns {Promise} channelID of joined and added channel */ _joinAndAddChannel: function (channelID, options) { var self = this; @@ -941,7 +957,7 @@ var MailManager = AbstractService.extend({ this._toNotifyChannelFetchedIDs = []; if (_.isEmpty(channelIDs)) { // no channel to notify fetched - return $.when(); + return Promise.resolve(); } return this._rpc({ model: 'mail.channel', @@ -1098,7 +1114,7 @@ var MailManager = AbstractService.extend({ * @private * @param {string} searchVal * @param {integer} limit - * @returns {$.Promise} fetched partners matching 'searchVal' + * @returns {Promise} fetched partners matching 'searchVal' */ _searchPartnerFetch: function (searchVal, limit) { return this._rpc({ @@ -1194,12 +1210,17 @@ var MailManager = AbstractService.extend({ * (e.g. list of public channel data are stored in 'channel_channel') * @param {Object[]} [data.channel_slots[i] list of data of channel of type * `i` + * @returns {Promise} */ _updateChannelsFromServer: function (data) { var self = this; + var proms = []; _.each(data.channel_slots, function (channels) { - _.each(channels, self._addChannel.bind(self)); + _.each(channels, function (channel) { + proms.push(self._addChannel(channel)); + }); }); + return Promise.all(proms); }, /** * Update commands from mail data fetched from the server @@ -1221,11 +1242,12 @@ var MailManager = AbstractService.extend({ * @param {Array} result.mention_partner_suggestions list of * suggestions. * @param {integer} result.menu_id the menu ID of discuss app + * @returns {Promise} */ _updateInternalStateFromServer: function (result) { // commands are needed for channel instantiation this._updateCommandsFromServer(result); - this._updateChannelsFromServer(result); + var prom = this._updateChannelsFromServer(result); this._updateModerationSettingsFromServer(result); this._updateMailboxesFromServer(result); this._updateMailFailuresFromServer(result); @@ -1233,6 +1255,8 @@ var MailManager = AbstractService.extend({ this._mentionPartnerSuggestions = result.mention_partner_suggestions; this._discussMenuID = result.menu_id; + + return prom; }, /** * Update the mailboxes with mail data fetched from server, namely 'Inbox', diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js index fb3527b2b90..75754f375d8 100644 --- a/addons/mail/static/src/js/services/mail_notification_manager.js +++ b/addons/mail/static/src/js/services/mail_notification_manager.js @@ -101,12 +101,12 @@ MailManager.include({ channelAlreadyInCache = !!this.getChannel(messageData.channel_ids[0]); def = this.joinChannel(messageData.channel_ids[0], { autoswitch: false }); } else { - def = $.when(); + def = Promise.resolve(); } def.then(function () { // don't increment unread if channel wasn't in cache yet as // its unread counter has just been fetched - self.addMessage(messageData, { + return self.addMessage(messageData, { showNotification: true, incrementUnread: channelAlreadyInCache }); @@ -179,18 +179,19 @@ MailManager.include({ _handleNeedactionNotification: function (messageData) { var self = this; var inbox = this.getMailbox('inbox'); - var message = this.addMessage(messageData, { + this.addMessage(messageData, { incrementUnread: true, showNotification: true, + }).then(function (message) { + inbox.incrementMailboxCounter(); + _.each(message.getThreadIDs(), function (threadID) { + var channel = self.getChannel(threadID); + if (channel) { + channel.incrementNeedactionCounter(); + } + }); + self._mailBus.trigger('update_needaction', inbox.getMailboxCounter()); }); - inbox.incrementMailboxCounter(); - _.each(message.getThreadIDs(), function (threadID) { - var channel = self.getChannel(threadID); - if (channel) { - channel.incrementNeedactionCounter(); - } - }); - this._mailBus.trigger('update_needaction', inbox.getMailboxCounter()); }, /** * Called when an activity record has been updated on the server @@ -381,8 +382,10 @@ MailManager.include({ * @param {Object} data.message data of message */ _handlePartnerMessageModeratorNotification: function (data) { - this.addMessage(data.message); - this._mailBus.trigger('update_moderation_counter'); + var self = this; + this.addMessage(data.message).then(function () { + self._mailBus.trigger('update_moderation_counter'); + }); }, /** * On receiving a notification that is specific to a user diff --git a/addons/mail/static/src/js/services/mail_status_manager.js b/addons/mail/static/src/js/services/mail_status_manager.js index 5ca151d47bf..4a2df2af94f 100644 --- a/addons/mail/static/src/js/services/mail_status_manager.js +++ b/addons/mail/static/src/js/services/mail_status_manager.js @@ -81,7 +81,7 @@ MailManager.include({ var self = this; var partnerIDs = data.partnerIDs; if (_.isEmpty(partnerIDs)) { - return $.when(); + return Promise.resolve(); } return this._rpc({ model: 'res.partner', diff --git a/addons/mail/static/src/js/services/mail_window_manager.js b/addons/mail/static/src/js/services/mail_window_manager.js index 4e481d6892a..083b46c9ff2 100644 --- a/addons/mail/static/src/js/services/mail_window_manager.js +++ b/addons/mail/static/src/js/services/mail_window_manager.js @@ -94,6 +94,7 @@ MailManager.include({ * is not open yet, and do nothing otherwise. * @param {boolean} [options.keepFoldState=false] if set to true, keep the * fold state of the thread + * @returns {Promise} */ openThreadWindow: function (threadID, options) { var self = this; @@ -108,10 +109,10 @@ MailManager.include({ return; } var threadWindow = this._getThreadWindow(threadID); - var def = $.when(); + var prom = Promise.resolve(); if (!threadWindow) { thread.isCreatingWindow = true; - def = thread.fetchMessages().then(function () { + prom = thread.fetchMessages().then(function () { threadWindow = self._makeNewThreadWindow(thread, options); self._placeNewThreadWindow(threadWindow, options.passively); return threadWindow.appendTo($(self.THREAD_WINDOW_APPENDTO)); @@ -126,12 +127,12 @@ MailManager.include({ ) { thread.markAsRead(); } - }).fail(function () { + thread.isCreatingWindow = false; + }).guardedCatch(function () { // thread window could not be open, which may happen due to // access error while fetching messages to the document. // abort opening the thread window in this case. thread.close(); - }).always(function () { thread.isCreatingWindow = false; }); } else if (!options.passively) { @@ -139,7 +140,7 @@ MailManager.include({ this._makeThreadWindowVisible(threadWindow); } } - def.then(function () { + return prom.then(function () { threadWindow.updateVisualFoldState(); }); }, @@ -155,14 +156,16 @@ MailManager.include({ */ updateThreadWindow: function (threadID, options) { var thread = this.getThread(threadID); + var prom = Promise.resolve(); if (thread) { if (thread.isDetached()) { _.extend(options, { keepFoldState: true }); - this.openThreadWindow(threadID, options); + prom = this.openThreadWindow(threadID, options); } else { this._closeThreadWindow(threadID); } } + return prom; }, //-------------------------------------------------------------------------- @@ -429,7 +432,7 @@ MailManager.include({ * * @private * @param {integer} partnerID - * @returns {$.Promise} resolved with ID of the DM chat + * @returns {Promise} resolved with ID of the DM chat */ _openAndDetachDMChat: function (partnerID) { return this._rpc({ @@ -647,10 +650,16 @@ MailManager.include({ * * @private * @param {mail.model.Channel} channel + * @param {Promise[]} proms used to synchronize async operations on the + * channel (like the rendering of its window) */ - _onNewChannel: function (channel) { + _onNewChannel: function (channel, proms) { if (channel.isDetached()) { - this.openThreadWindow(channel.getID(), { keepFoldState: true, passively: true }); + var prom = this.openThreadWindow(channel.getID(), { + keepFoldState: true, + passively: true, + }); + proms.push(prom); } else { this._closeThreadWindow(channel.getID()); } diff --git a/addons/mail/static/src/js/systray/systray_activity_menu.js b/addons/mail/static/src/js/systray/systray_activity_menu.js index 593cac91506..fae95a78c2f 100644 --- a/addons/mail/static/src/js/systray/systray_activity_menu.js +++ b/addons/mail/static/src/js/systray/systray_activity_menu.js @@ -20,7 +20,7 @@ var ActivityMenu = Widget.extend({ 'show.bs.dropdown': '_onActivityMenuShow', }, willStart: function () { - return $.when(this.call('mail_service', 'isReady')); + return Promise.resolve(this.call('mail_service', 'isReady')); }, start: function () { this._$activitiesPreview = this.$('.o_mail_systray_dropdown_items'); diff --git a/addons/mail/static/src/js/systray/systray_messaging_menu.js b/addons/mail/static/src/js/systray/systray_messaging_menu.js index df35de47174..4dacae435d4 100644 --- a/addons/mail/static/src/js/systray/systray_messaging_menu.js +++ b/addons/mail/static/src/js/systray/systray_messaging_menu.js @@ -30,7 +30,7 @@ var MessagingMenu = Widget.extend({ * @override */ willStart: function () { - return $.when(this._super.apply(this, arguments), this.call('mail_service', 'isReady')); + return Promise.all([this._super.apply(this, arguments), this.call('mail_service', 'isReady')]); }, /** * @override @@ -112,7 +112,7 @@ var MessagingMenu = Widget.extend({ }, /** * @private - * @returns {$.Promise} resolved with list of previews that are + * @returns {Promise} resolved with list of previews that are * compatible with the 'mail.Preview' template. */ _getPreviews: function () { diff --git a/addons/mail/static/src/js/thread_field.js b/addons/mail/static/src/js/thread_field.js index a3807802ad7..ab8023b06ad 100644 --- a/addons/mail/static/src/js/thread_field.js +++ b/addons/mail/static/src/js/thread_field.js @@ -57,7 +57,7 @@ var ThreadField = AbstractField.extend({ var def1 = this._threadWidget.appendTo(this.$el); var def2 = this._super.apply(this, arguments); - return this.alive($.when(def1, def2)).then(function () { + return this.alive(Promise.all([def1, def2])).then(function () { // unwrap the thread to remove an unnecessary level on div self.setElement(self._threadWidget.$el); var mailBus = self.call('mail_service', 'getMailBus'); @@ -86,7 +86,7 @@ var ThreadField = AbstractField.extend({ /** * @param {Object} message * @param {integer[]} message.partner_ids - * @return {$.Promise} + * @return {Promise} */ postMessage: function (message) { var self = this; @@ -95,9 +95,9 @@ var ThreadField = AbstractField.extend({ if (message.partner_ids.length) { self.trigger_up('reload_mail_fields', { followers: true }); } - }) - .fail(function () { + }).guardedCatch(function () { self.do_notify(_t("Sending Error"), _t("Your message has not been sent.")); + return Promise.reject(); }); }, @@ -109,7 +109,7 @@ var ThreadField = AbstractField.extend({ * @private * @param {Object} [options] * @param {boolean} [options.forceFetch] - * @return {$.Deferred} + * @return {Promise} */ _fetchAndRenderThread: function (options) { var self = this; @@ -117,7 +117,7 @@ var ThreadField = AbstractField.extend({ var thread = new CreateModeDocumentThread(); options = { isCreateMode: true }; self._threadWidget.render(thread, options); - return $.when(); + return Promise.resolve(); } else { var fetchDef = this.dp.add(this._documentThread.fetchMessages(options)); return fetchDef.then(function () { @@ -134,7 +134,7 @@ var ThreadField = AbstractField.extend({ /** * @override * @private - * @returns {$.Deferred} + * @returns {Promise} */ _render: function () { return this._fetchAndRenderThread(); diff --git a/addons/mail/static/src/js/thread_widget.js b/addons/mail/static/src/js/thread_widget.js index cbac2c3b417..56945533946 100644 --- a/addons/mail/static/src/js/thread_widget.js +++ b/addons/mail/static/src/js/thread_widget.js @@ -248,16 +248,16 @@ var ThreadWidget = Widget.extend({ */ removeMessageAndRender: function (messageID, thread, options) { var self = this; - var done = $.Deferred(); - this.$('.o_thread_message[data-message-id="' + messageID + '"]') + return new Promise(function (resolve, reject) { + self.$('.o_thread_message[data-message-id="' + messageID + '"]') .fadeOut({ done: function () { self.render(thread, options); - done.resolve(); + resolve(); }, duration: 200, }); - return done; + }); }, /** * Scroll to the bottom of the thread @@ -273,7 +273,7 @@ var ThreadWidget = Widget.extend({ * @param {boolean} [options.onlyIfNecessary] */ scrollToMessage: function (options) { - var $target = this.$('.o_thread_message[data-message-id="' + options.msgID + '"]'); + var $target = this.$('.o_thread_message[data-message-id="' + options.messageID + '"]'); if (options.onlyIfNecessary) { var delta = $target.parent().height() - $target.height(); var offset = delta < 0 ? diff --git a/addons/mail/static/src/js/thread_windows/abstract_thread_window.js b/addons/mail/static/src/js/thread_windows/abstract_thread_window.js index 300bfb11a3e..7a38d337f24 100644 --- a/addons/mail/static/src/js/thread_windows/abstract_thread_window.js +++ b/addons/mail/static/src/js/thread_windows/abstract_thread_window.js @@ -83,6 +83,8 @@ var AbstractThreadWindow = Widget.extend({ displayStars: this.options.displayStars, }); + // animate the (un)folding of thread windows + this.$el.css({transition: 'height ' + this.FOLD_ANIMATION_DURATION + 'ms linear'}); if (this.isFolded()) { this.$el.css('height', this.HEIGHT_FOLDED); } else if (this.options.autofocus) { @@ -95,7 +97,7 @@ var AbstractThreadWindow = Widget.extend({ var def = this._threadWidget.replace(this.$('.o_thread_window_content')).then(function () { self._threadWidget.$el.on('scroll', self, self._debouncedOnScroll); }); - return $.when(this._super(), def); + return Promise.all([this._super(), def]); }, /** * @override @@ -319,24 +321,14 @@ var AbstractThreadWindow = Widget.extend({ this._focusInput(); } } - this._animateFold(); + var height = this.isFolded() ? this.HEIGHT_FOLDED : this.HEIGHT_OPEN; + this.$el.css({height: height}); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- - /** - * Called when there is a change of the fold state of the thread window. - * This method animates the change of fold state of this thread window. - * - * @private - */ - _animateFold: function () { - this.$el.animate({ - height: this.isFolded() ? this.HEIGHT_FOLDED : this.HEIGHT_OPEN - }, this.FOLD_ANIMATION_DURATION); - }, /** * Set the focus on the composer of the thread window. This operation is * ignored in mobile context. diff --git a/addons/mail/static/src/js/thread_windows/thread_window.js b/addons/mail/static/src/js/thread_windows/thread_window.js index e5496e0bc3c..9999014a08d 100644 --- a/addons/mail/static/src/js/thread_windows/thread_window.js +++ b/addons/mail/static/src/js/thread_windows/thread_window.js @@ -83,7 +83,7 @@ var ThreadWindow = AbstractThreadWindow.extend({ } this._updateOutOfOfficeReadMoreLessButton(); - return $.when(superDef, composerDef); + return Promise.all([superDef, composerDef]); }, //-------------------------------------------------------------------------- @@ -241,7 +241,7 @@ var ThreadWindow = AbstractThreadWindow.extend({ autoFocus: true, source: function (request, response) { self.call('mail_service', 'searchPartner', request.term, 10) - .done(response); + .then(response); }, select: function (event, ui) { // remember partner ID so that we can replace this window diff --git a/addons/mail/static/src/js/views/activity/activity_model.js b/addons/mail/static/src/js/views/activity/activity_model.js index 738cdb55ca1..f5a2feee988 100644 --- a/addons/mail/static/src/js/views/activity/activity_model.js +++ b/addons/mail/static/src/js/views/activity/activity_model.js @@ -19,7 +19,7 @@ var ActivityModel = AbstractModel.extend({ * @override * @param {Object} params * @param {Array[]} params.domain - * @returns {Deferred} + * @returns {Promise} */ load: function (params) { this.domain = params.domain; @@ -31,7 +31,7 @@ var ActivityModel = AbstractModel.extend({ * @param {any} handle * @param {Object} params * @param {Array[]} params.domain - * @returns {Deferred} + * @returns {Promise} */ reload: function (handle, params) { if (params && 'domain' in params) { @@ -48,7 +48,7 @@ var ActivityModel = AbstractModel.extend({ * Fetch activity data. * * @private - * @returns {Deferred} + * @returns {Promise} */ _fetchData: function () { var self = this; diff --git a/addons/mail/static/src/js/views/activity/activity_renderer.js b/addons/mail/static/src/js/views/activity/activity_renderer.js index d139a4e84cd..75eb25367e4 100644 --- a/addons/mail/static/src/js/views/activity/activity_renderer.js +++ b/addons/mail/static/src/js/views/activity/activity_renderer.js @@ -5,8 +5,8 @@ var AbstractRenderer = require('web.AbstractRenderer'); var core = require('web.core'); var field_registry = require('web.field_registry'); -var QWeb = core.qweb; var _t = core._t; +var QWeb = core.qweb; var ActivityRenderer = AbstractRenderer.extend({ className: 'o_activity_view', @@ -125,21 +125,22 @@ var ActivityRenderer = AbstractRenderer.extend({ }; var KanbanActivity = field_registry.get('kanban_activity'); var widget = new KanbanActivity(self, "activity_ids", record, {}); - widget.appendTo($td); - // replace clock by closest deadline - var $date = $('
'); - var formated_date = moment(activity_group.o_closest_deadline).format('ll'); - var current_year = (new Date()).getFullYear(); - if (formated_date.endsWith(current_year)) { // Dummy logic to remove year (only if current year), we will maybe need to improve it - formated_date = formated_date.slice(0, -4); - formated_date = formated_date.replace(/( |,)*$/g, ""); - } - $date - .text(formated_date) - .addClass('o_closest_deadline'); - $td.find('a') - .empty() - .append($date); + widget.appendTo($td).then(function() { + // replace clock by closest deadline + var $date = $('
'); + var formated_date = moment(activity_group.o_closest_deadline).format('ll'); + var current_year = (new Date()).getFullYear(); + if (formated_date.endsWith(current_year)) { // Dummy logic to remove year (only if current year), we will maybe need to improve it + formated_date = formated_date.slice(0, -4); + formated_date = formated_date.replace(/( |,)*$/g, ""); + } + $date + .text(formated_date) + .addClass('o_closest_deadline'); + $td.find('a') + .empty() + .append($date); + }); return $td; }); var $tr = $('', {class: 'o_data_row'}) diff --git a/addons/mail/static/tests/activity_tests.js b/addons/mail/static/tests/activity_tests.js index 19f4f6eaf5d..4e3e509936c 100644 --- a/addons/mail/static/tests/activity_tests.js +++ b/addons/mail/static/tests/activity_tests.js @@ -162,19 +162,19 @@ QUnit.module('activity view', { } }); -QUnit.test('activity view: simple activity rendering', function (assert) { +QUnit.test('activity view: simple activity rendering', async function (assert) { assert.expect(7); - var activity = createView({ + var activity = await createView({ View: ActivityView, model: 'task', data: this.data, arch: '', mockRPC: function(route, args) { if (args.method === 'get_activity_data') { - return $.when(ACTIVITY_DATA); + return Promise.resolve(ACTIVITY_DATA); } return this._super(route, args); - } + }, }); assert.containsOnce(activity, 'table', @@ -194,16 +194,16 @@ QUnit.test('activity view: simple activity rendering', function (assert) { activity.destroy(); }); -QUnit.test('activity view: no content rendering', function (assert) { +QUnit.test('activity view: no content rendering', async function (assert) { assert.expect(2); - var activity = createView({ + var activity = await createView({ View: ActivityView, model: 'task', data: this.data, arch: '', mockRPC: function (route, args) { if (args.method === 'get_activity_data') { - return $.when({ activity_types: [] }); + return Promise.resolve({ activity_types: [] }); } return this._super(route, args); }, @@ -218,20 +218,20 @@ QUnit.test('activity view: no content rendering', function (assert) { activity.destroy(); }); -QUnit.test('activity view: batch send mail on activity', function (assert) { +QUnit.test('activity view: batch send mail on activity', async function (assert) { assert.expect(6); - var activity = createView({ + var activity = await createView({ View: ActivityView, model: 'task', data: this.data, arch: '', mockRPC: function(route, args) { if (args.method === 'activity_send_mail'){ - assert.step(args.args); - return $.when(); + assert.step(JSON.stringify(args.args)); + return Promise.resolve(); } if (args.method === 'get_activity_data') { - return $.when(ACTIVITY_DATA); + return Promise.resolve(ACTIVITY_DATA); } return this._super(route, args); }, @@ -250,16 +250,16 @@ QUnit.test('activity view: batch send mail on activity', function (assert) { testUtils.dom.click(activity.$('table thead tr:first th:nth-child(2) span:nth-child(2) i.fa-ellipsis-v')); testUtils.dom.click(activity.$('table thead tr:first th:nth-child(2) span:nth-child(2) .dropdown-menu.show .o_send_mail_template:nth-child(2):contains(Task: Reception Acknowledgment)')); assert.verifySteps([ - [[13, 30], 9], //send mail template 9 on tasl 13 and 30 - [[13, 30], 8] //send mail template 8 on tasl 13 and 30 + '[[13,30],9]', // send mail template 9 on tasks 13 and 30 + '[[13,30],8]', // send mail template 8 on tasks 13 and 30 ]); activity.destroy(); }); -QUnit.test('activity view: activity widget', function (assert) { +QUnit.test('activity view: activity widget', async function (assert) { assert.expect(16); - var activity = createView({ + var activity = await createView({ View: ActivityView, model: 'task', data: this.data, @@ -268,16 +268,16 @@ QUnit.test('activity view: activity widget', function (assert) { if (args.method === 'activity_send_mail'){ assert.deepEqual([[30],8],args.args, "Should send template 8 on record 30"); assert.step('activity_send_mail'); - return $.when(); + return Promise.resolve(); } if (args.method === 'action_feedback_schedule_next'){ assert.deepEqual([[3]],args.args, "Should execute action_feedback_schedule_next on activity 3 only "); assert.equal(args.kwargs.feedback, "feedback2"); assert.step('action_feedback_schedule_next'); - return $.when({serverGeneratedAction: true}); + return Promise.resolve({serverGeneratedAction: true}); } if (args.method === 'get_activity_data') { - return $.when(ACTIVITY_DATA); + return Promise.resolve(ACTIVITY_DATA); } return this._super(route, args); }, @@ -305,12 +305,12 @@ QUnit.test('activity view: activity widget', function (assert) { assert.step("Unexpected action"); } }, - } + }, }); var today = activity.$('table tbody tr:first td:nth-child(2).today'); var dropdown = today.find('.dropdown-menu.o_activity'); - testUtils.dom.click(today.find('.o_closest_deadline')); + await testUtils.dom.click(today.find('.o_closest_deadline')); assert.hasClass(dropdown,'show', "dropdown should be displayed"); assert.ok(dropdown.find('.o_activity_color_today:contains(Today)').length, "Title should be today"); assert.ok(dropdown.find('.o_activity_title_entry[data-activity-id="2"]:first div:contains(template8)').length, @@ -318,20 +318,20 @@ QUnit.test('activity view: activity widget', function (assert) { assert.ok(dropdown.find('.o_activity_title_entry[data-activity-id="2"]:eq(1) div:contains(template9)').length, "template9 should be available"); - testUtils.dom.click(dropdown.find('.o_activity_title_entry[data-activity-id="2"]:first .o_activity_template_preview')); - testUtils.dom.click(dropdown.find('.o_activity_title_entry[data-activity-id="2"]:first .o_activity_template_send')); + await testUtils.dom.click(dropdown.find('.o_activity_title_entry[data-activity-id="2"]:first .o_activity_template_preview')); + await testUtils.dom.click(dropdown.find('.o_activity_title_entry[data-activity-id="2"]:first .o_activity_template_send')); var overdue = activity.$('table tbody tr:first td:nth-child(3).overdue'); - testUtils.dom.click(overdue.find('.o_closest_deadline')); + await testUtils.dom.click(overdue.find('.o_closest_deadline')); dropdown = overdue.find('.dropdown-menu.o_activity'); assert.notOk(dropdown.find('.o_activity_title div div div:first span').length, "No template should be available"); - testUtils.dom.click(dropdown.find('.o_schedule_activity')); - testUtils.dom.click(overdue.find('.o_closest_deadline')); - testUtils.dom.click(dropdown.find('.o_mark_as_done')); + await testUtils.dom.click(dropdown.find('.o_schedule_activity')); + await testUtils.dom.click(overdue.find('.o_closest_deadline')); + await testUtils.dom.click(dropdown.find('.o_mark_as_done')); dropdown.find('#activity_feedback').val("feedback2"); - testUtils.dom.click(dropdown.find('.o_activity_popover_done_next')); + await testUtils.dom.click(dropdown.find('.o_activity_popover_done_next')); assert.verifySteps([ "do_action_compose", "activity_send_mail", @@ -342,10 +342,10 @@ QUnit.test('activity view: activity widget', function (assert) { activity.destroy(); }); -QUnit.test('activity view: no group_by_menu and no time_range_menu', function (assert) { +QUnit.test('activity view: no group_by_menu and no time_range_menu', async function (assert) { assert.expect(4); - var actionManager = createActionManager({ + var actionManager = await createActionManager({ actions: [{ id: 1, name: 'Task Action', @@ -365,13 +365,13 @@ QUnit.test('activity view: no group_by_menu and no time_range_menu', function (a if (args.method === 'get_activity_data') { assert.deepEqual(args.kwargs.context, {lang: 'zz_ZZ'}, 'The context should have been passed'); - return $.when(ACTIVITY_DATA); + return Promise.resolve(ACTIVITY_DATA); } return this._super(route, args); }, }); - actionManager.doAction(1); + await actionManager.doAction(1); assert.containsN(actionManager, '.o_search_options .o_dropdown button:visible', 2, "only two elements should be available in view search"); diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index 7aaa35ac4e3..08f5c01fc34 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -3,13 +3,11 @@ odoo.define('mail.chatter_tests', function (require) { var mailTestUtils = require('mail.testUtils'); -var concurrency = require('web.concurrency'); var core = require('web.core'); var FormView = require('web.FormView'); var KanbanView = require('web.KanbanView'); var testUtils = require('web.test_utils'); -var createAsyncView = testUtils.createAsyncView; var createView = testUtils.createView; var Activity = require('mail.Activity'); @@ -192,13 +190,12 @@ QUnit.module('Chatter', { } }); -QUnit.test('basic rendering', function (assert) { +QUnit.test('basic rendering', async function (assert) { assert.expect(9); var count = 0; var unwanted_read_count = 0; - - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -219,7 +216,7 @@ QUnit.test('basic rendering', function (assert) { } if (route === '/mail/read_followers') { count++; - return $.when({ + return Promise.resolve({ followers: [], subtypes: [], }); @@ -228,25 +225,24 @@ QUnit.test('basic rendering', function (assert) { }, res_id: 2, }); - - assert.ok(form.$('.o_mail_activity').length, "there should be an activity widget"); - assert.ok(form.$('.o_chatter_topbar .o_chatter_button_schedule_activity').length, - "there should be a 'Schedule an activity' button in the chatter's topbar"); - assert.ok(form.$('.o_chatter_topbar .o_followers').length, - "there should be a followers widget, moved inside the chatter's topbar"); - assert.ok(form.$('.o_chatter').length, "there should be a chatter widget"); - assert.ok(form.$('.o_mail_thread').length, "there should be a mail thread"); + assert.containsOnce(form, '.o_mail_activity', "there should be an activity widget"); + assert.containsOnce(form, '.o_chatter_topbar .o_chatter_button_schedule_activity', + "there should be a 'Schedule an activity' button in the chatter's topbar"); + assert.containsOnce(form, '.o_chatter_topbar .o_followers', + "there should be a followers widget, moved inside the chatter's topbar"); + assert.containsOnce(form, '.o_chatter', "there should be a chatter widget"); + assert.containsOnce(form, '.o_mail_thread'); assert.containsOnce(form, '.o_chatter_button_attachment', "should have one attachment button"); - assert.ok(!form.$('.o_chatter_topbar .o_chatter_button_log_note').length, + assert.containsNone(form, '.o_chatter_topbar .o_chatter_button_log_note', "log note button should not be available"); - testUtils.form.clickEdit(form); + await testUtils.form.clickEdit(form); assert.strictEqual(count, 0, "should have done no read_followers rpc as there are no followers"); assert.strictEqual(unwanted_read_count, 0, "followers should only be fetched with read_followers route"); form.destroy(); }); -QUnit.test('Activity Done keep feedback on blur', function (assert) { +QUnit.test('Activity Done keep feedback on blur', async function (assert) { assert.expect(3); var done = assert.async(); @@ -255,8 +251,8 @@ QUnit.test('Activity Done keep feedback on blur', function (assert) { ]; this.data.partner.records[0].activity_ids = [1]; - var shownDef = $.Deferred(); - var hiddenDef = $.Deferred(); + var shownDef = testUtils.makeTestPromise(); + var hiddenDef = testUtils.makeTestPromise(); testUtils.mock.patch(Activity, { _bindPopoverFocusout: function () { this._super.apply(this, arguments); @@ -264,7 +260,7 @@ QUnit.test('Activity Done keep feedback on blur', function (assert) { }, }); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -276,7 +272,6 @@ QUnit.test('Activity Done keep feedback on blur', function (assert) { '
' + '', }); - // sanity checks var $activityEl = form.$('.o_mail_activity[name=activity_ids]'); assert.strictEqual($activityEl.find('.o_thread_message').length, 1, @@ -288,18 +283,18 @@ QUnit.test('Activity Done keep feedback on blur', function (assert) { $popoverEl.on('hidden.bs.popover', hiddenDef.resolve.bind(hiddenDef)); // open popover - testUtils.dom.click($popoverEl); + await testUtils.dom.click($popoverEl); shownDef.then(function () { // write a feedback and focusout var $feedbackPopover = $($popoverEl.data('bs.popover').tip); $feedbackPopover.find('#activity_feedback').val('John Shaft').focusout(); - hiddenDef.then(function () { - shownDef = $.Deferred(); + hiddenDef.then(async function () { + shownDef = testUtils.makeTestPromise(); // re-open popover - testUtils.dom.click($popoverEl); + await testUtils.dom.click($popoverEl); shownDef.then(function () { var $feedbackPopover = $($popoverEl.data('bs.popover').tip); @@ -314,7 +309,7 @@ QUnit.test('Activity Done keep feedback on blur', function (assert) { }); }); -QUnit.test('Activity Done by uploading a file', function (assert) { +QUnit.test('Activity Done by uploading a file', async function (assert) { assert.expect(4); // simulate (shortcut) the upload and trigger the event when the @@ -348,7 +343,7 @@ QUnit.test('Activity Done by uploading a file', function (assert) { }]; this.data.partner.records[0].activity_ids = [1]; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -365,7 +360,7 @@ QUnit.test('Activity Done by uploading a file', function (assert) { var done_ids = args.args[0]; this.data.partner.records[0].activity_ids = _.difference(current_ids, done_ids); this.data.partner.records[0].activity_state = false; - return $.when(); + return Promise.resolve(); } return this._super.apply(this, arguments); }, @@ -379,7 +374,7 @@ QUnit.test('Activity Done by uploading a file', function (assert) { // click on "upload" button var $uploadBtn = $activity.find('.o_mark_as_done_upload_file'); - testUtils.dom.click($uploadBtn); + await testUtils.dom.click($uploadBtn); assert.containsNone($activity, '.o_thread_message', 'The only activity should be marked as done'); @@ -387,14 +382,14 @@ QUnit.test('Activity Done by uploading a file', function (assert) { form.destroy(); }); -QUnit.test('attachmentBox basic rendering', function (assert) { +QUnit.test('attachmentBox basic rendering', async function (assert) { assert.expect(19); this.data.partner.records.push({ id: 7, display_name: "attachment_test", }); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -424,7 +419,7 @@ QUnit.test('attachmentBox basic rendering', function (assert) { }); var $button = form.$('.o_chatter_button_attachment'); assert.strictEqual($button.length, 1, "should have one attachment button"); - testUtils.dom.click($button); + await testUtils.dom.click($button); assert.containsOnce(form, '.o_mail_chatter_attachments', "attachment widget should exist after a first click on the button"); assert.containsOnce(form, '.o_attachment_image', "there should be an image preview"); @@ -455,15 +450,15 @@ QUnit.test('attachmentBox basic rendering', function (assert) { "the download URL of name1 must be correct"); assert.hasAttrValue(form.$('.o_attachment_download').eq(1), 'href', '/web/content/2?download=true', "the download URL of name2 must be correct"); - testUtils.dom.click($button); + await testUtils.dom.click($button); assert.containsNone(form, '.o_mail_chatter_attachments') form.destroy(); }); -QUnit.test('chatter in create mode', function (assert) { +QUnit.test('chatter in create mode', async function (assert) { assert.expect(9); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -479,7 +474,7 @@ QUnit.test('chatter in create mode', function (assert) { res_id: 2, mockRPC: function (route, args) { if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") { - return $.when({2: []}); + return Promise.resolve({2: []}); } return this._super(route, args); }, @@ -489,7 +484,7 @@ QUnit.test('chatter in create mode', function (assert) { "chatter should be displayed"); // entering create mode - testUtils.form.clickCreate(form); + await testUtils.form.clickCreate(form); assert.hasClass(form.$el.find('.o_form_view'),'o_form_editable', "we should be in create mode"); assert.containsOnce(form, '.o_chatter', @@ -509,24 +504,24 @@ QUnit.test('chatter in create mode', function (assert) { "the content of the message should be 'Creating a new record...'"); // getting out of create mode by saving - testUtils.fields.editInput(form.$('.o_field_char'), 'coucou'); - testUtils.form.clickSave(form); + await testUtils.fields.editInput(form.$('.o_field_char'), 'coucou'); + await testUtils.form.clickSave(form); assert.containsOnce(form, '.o_chatter', "chatter should still be displayed after saving from create mode"); // check if chatter buttons still work - testUtils.dom.click(form.$('.o_chatter_button_new_message')); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); assert.containsOnce(form, '.o_thread_composer:visible', "chatter should be opened"); form.destroy(); }); -QUnit.test('chatter rendering inside the sheet', function (assert) { +QUnit.test('chatter rendering inside the sheet', async function (assert) { assert.expect(5); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -546,7 +541,7 @@ QUnit.test('chatter rendering inside the sheet', function (assert) { res_id: 2, mockRPC: function (route, args) { if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") { - return $.when({2: []}); + return Promise.resolve({2: []}); } return this._super(route, args); }, @@ -555,32 +550,32 @@ QUnit.test('chatter rendering inside the sheet', function (assert) { assert.containsOnce(form, '.o_chatter', "chatter should be displayed"); - testUtils.form.clickCreate(form); + await testUtils.form.clickCreate(form); assert.hasClass(form.$el.find('.o_form_view'),'o_form_editable', "we should be in create mode"); assert.containsOnce(form, '.o_chatter', "chatter should be displayed"); - testUtils.fields.editInput(form.$('.o_field_char'), 'coucou'); - testUtils.form.clickSave(form); + await testUtils.fields.editInput(form.$('.o_field_char'), 'coucou'); + await testUtils.form.clickSave(form); assert.containsOnce(form, '.o_chatter', "chatter should be displayed"); // check if chatter buttons still work - testUtils.dom.click(form.$('.o_chatter_button_new_message')); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); assert.containsOnce(form, '.o_thread_composer:visible', "chatter should be opened"); form.destroy(); }); -QUnit.test('kanban activity widget with no activity', function (assert) { +QUnit.test('kanban activity widget with no activity', async function (assert) { assert.expect(4); var rpcCount = 0; - var kanban = createView({ + var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, @@ -603,7 +598,7 @@ QUnit.test('kanban activity widget with no activity', function (assert) { assert.strictEqual(rpcCount, 1, '1 RPC (search_read) should have been done'); // click on the activity button - testUtils.dom.click($record.find('.o_activity_btn')); + await testUtils.dom.click($record.find('.o_activity_btn')); assert.strictEqual(rpcCount, 1, 'no RPC should have been done as there is no activity'); assert.strictEqual($record.find('.o_no_activity').length, 1, "should have no activity scheduled"); @@ -612,7 +607,7 @@ QUnit.test('kanban activity widget with no activity', function (assert) { kanban.destroy(); }); -QUnit.test('kanban activity widget with an activity', function (assert) { +QUnit.test('kanban activity widget with an activity', async function (assert) { assert.expect(12); this.data.partner.records[0].activity_ids = [1]; @@ -628,7 +623,7 @@ QUnit.test('kanban activity widget with an activity', function (assert) { activity_type_id: 1, }]; var rpcCount = 0; - var kanban = createView({ + var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, @@ -645,7 +640,7 @@ QUnit.test('kanban activity widget with an activity', function (assert) { var done_ids = args.args[0]; this.data.partner.records[0].activity_ids = _.difference(current_ids, done_ids); this.data.partner.records[0].activity_state = false; - return $.when(); + return Promise.resolve(); } return this._super(route, args); }, @@ -658,7 +653,7 @@ QUnit.test('kanban activity widget with an activity', function (assert) { assert.strictEqual(rpcCount, 1, '1 RPC (search_read) should have been done'); // click on the activity button - testUtils.dom.click($record.find('.o_activity_btn')); + await testUtils.dom.click($record.find('.o_activity_btn')); assert.strictEqual(rpcCount, 2, 'a read should have been done to fetch the activity details'); assert.strictEqual($record.find('.o_activity_title').length, 1, "should have an activity scheduled"); var label = $record.find('.o_activity_log .o_activity_color_today'); @@ -666,16 +661,16 @@ QUnit.test('kanban activity widget with an activity', function (assert) { assert.strictEqual(label.find('.badge-warning').text(), "1", "should display the correct count"); // click on the activity button to close the dropdown - testUtils.dom.click($record.find('.o_activity_btn')); + await testUtils.dom.click($record.find('.o_activity_btn')); assert.strictEqual(rpcCount, 2, 'no RPC should be done when closing the dropdown'); // click on the activity button to re-open dropdown - testUtils.dom.click($record.find('.o_activity_btn')); + await testUtils.dom.click($record.find('.o_activity_btn')); assert.strictEqual(rpcCount, 3, 'should have reloaded the activities'); // mark activity as done - testUtils.dom.click($record.find('.o_mark_as_done')); - testUtils.dom.click($record.find('.o_activity_popover_done')); + await testUtils.dom.click($record.find('.o_mark_as_done')); + await testUtils.dom.click($record.find('.o_activity_popover_done')); $record = kanban.$('.o_kanban_record').first(); // the record widget has been reset assert.strictEqual(rpcCount, 5, 'should have done an RPC to mark activity as done, and a read'); assert.ok($record.find('.o_mail_activity .o_activity_color_default:not(.o_activity_color_today)').length, @@ -687,7 +682,7 @@ QUnit.test('kanban activity widget with an activity', function (assert) { kanban.destroy(); }); -QUnit.test('kanban activity widget popover test', function (assert) { +QUnit.test('kanban activity widget popover test', async function (assert) { assert.expect(3); this.data.partner.records[0].activity_ids = [1]; @@ -703,7 +698,7 @@ QUnit.test('kanban activity widget popover test', function (assert) { activity_type_id: 1, }]; var rpcCount = 0; - var kanban = createView({ + var kanban = await createView({ View: KanbanView, model: 'partner', data: this.data, @@ -721,7 +716,7 @@ QUnit.test('kanban activity widget popover test', function (assert) { var done_ids = args.args[0]; this.data.partner.records[0].activity_ids = _.difference(current_ids, done_ids); this.data.partner.records[0].activity_state = false; - return $.when(); + return Promise.resolve(); } return this._super(route, args); }, @@ -729,25 +724,24 @@ QUnit.test('kanban activity widget popover test', function (assert) { var $record = kanban.$('.o_kanban_record').first(); - testUtils.dom.click($record.find('.o_activity_btn')); + await testUtils.dom.click($record.find('.o_activity_btn')); // Click on button and see popover no RPC call - testUtils.dom.click($record.find('.o_mark_as_done')); + await testUtils.dom.click($record.find('.o_mark_as_done')); assert.equal(rpcCount, 0, ""); // Click on discard no RPC call - testUtils.dom.click($record.find('.o_activity_popover_discard')); + await testUtils.dom.click($record.find('.o_activity_popover_discard')); assert.equal(rpcCount, 0, ""); // Click on button and then on done and schedule next // RPC call - testUtils.dom.click($record.find('.o_activity_popover_done_next')); + await testUtils.dom.click($record.find('.o_activity_popover_done_next')); assert.equal(rpcCount, 1, ""); kanban.destroy(); }); -QUnit.test('chatter: post, receive and star messages', function (assert) { - var done = assert.async(); - assert.expect(27); +QUnit.test('chatter: post, receive and star messages', async function (assert) { + assert.expect(26); this.data.partner.records[0].message_ids = [1]; this.data['mail.message'].records = [{ @@ -763,8 +757,8 @@ QUnit.test('chatter: post, receive and star messages', function (assert) { res_id: 2, }]; - var getSuggestionsDef = $.Deferred(); - var form = createView({ + var getSuggestionsDef = testUtils.makeTestPromise(); + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -780,11 +774,11 @@ QUnit.test('chatter: post, receive and star messages', function (assert) { res_id: 2, mockRPC: function (route, args) { if (args.method === 'message_get_suggested_recipients') { - return $.when({2: []}); + return Promise.resolve({2: []}); } if (args.method === 'get_mention_suggestions') { getSuggestionsDef.resolve(); - return $.when([[{email: "test@odoo.com", id: 1, name: "Test User"}], []]); + return Promise.resolve([[{email: "test@odoo.com", id: 1, name: "Test User"}], []]); } if (args.method === 'message_post') { var lastMessageData = _.max(this.data['mail.message'].records, function (messageData) { @@ -804,7 +798,7 @@ QUnit.test('chatter: post, receive and star messages', function (assert) { model: 'partner', res_id: 2, }); - return $.when(messageID); + return Promise.resolve(messageID); } if (args.method === 'toggle_message_starred') { assert.ok(_.contains(args.args[0], 2), @@ -823,7 +817,7 @@ QUnit.test('chatter: post, receive and star messages', function (assert) { }; var notification = [[false, 'res.partner'], data]; form.call('bus_service', 'trigger', 'notification', [notification]); - return $.when(); + return Promise.resolve(); } return this._super(route, args); }, @@ -841,10 +835,10 @@ QUnit.test('chatter: post, receive and star messages', function (assert) { "the message's author should be correct"); // send a message - testUtils.dom.click(form.$('.o_chatter_button_new_message')); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); assert.isVisible($('.oe_chatter .o_thread_composer'), "chatter should be opened"); form.$('.oe_chatter .o_composer_text_field:first()').val("My first message"); - testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); + await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); assert.isNotVisible($('.oe_chatter .o_thread_composer'), "chatter should be closed"); assert.containsN(form, '.o_thread_message', 2, "thread should contain two messages"); assert.ok(form.$('.o_thread_message:first().o_mail_discussion').length, @@ -855,10 +849,10 @@ QUnit.test('chatter: post, receive and star messages', function (assert) { "the message's author should be correct"); // log a note - testUtils.dom.click(form.$('.o_chatter_button_log_note')); + await testUtils.dom.click(form.$('.o_chatter_button_log_note')); assert.isVisible($('.oe_chatter .o_thread_composer'), "chatter should be opened"); form.$('.oe_chatter .o_composer_text_field:first()').val("My first note"); - testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); + await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); assert.isNotVisible($('.oe_chatter .o_thread_composer'), "chatter should be closed"); assert.containsN(form, '.o_thread_message', 3, "thread should contain three messages"); assert.ok(!form.$('.o_thread_message:first().o_mail_discussion').length, @@ -871,17 +865,17 @@ QUnit.test('chatter: post, receive and star messages', function (assert) { // star message 2 assert.ok(form.$('.o_thread_message[data-message-id=2] .o_thread_message_star.fa-star-o').length, "message 2 should not be starred"); - testUtils.dom.click(form.$('.o_thread_message[data-message-id=2] .o_thread_message_star')); + await testUtils.dom.click(form.$('.o_thread_message[data-message-id=2] .o_thread_message_star')); assert.ok(form.$('.o_thread_message[data-message-id=2] .o_thread_message_star.fa-star').length, "message 2 should be starred"); // unstar message 2 - testUtils.dom.click(form.$('.o_thread_message[data-message-id=2] .o_thread_message_star')); + await testUtils.dom.click(form.$('.o_thread_message[data-message-id=2] .o_thread_message_star')); assert.ok(form.$('.o_thread_message[data-message-id=2] .o_thread_message_star.fa-star-o').length, "message 2 should not be starred"); // very basic test of mention - testUtils.dom.click(form.$('.o_chatter_button_new_message')); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); var $input = form.$('.oe_chatter .o_composer_text_field:first()'); $input.val('@'); // the cursor position must be set for the mention manager to detect that we are mentionning @@ -889,30 +883,24 @@ QUnit.test('chatter: post, receive and star messages', function (assert) { $input[0].selectionEnd = 1; $input.trigger('keyup'); - assert.strictEqual(getSuggestionsDef.state(), "pending", - "the mention suggestion RPC should be throttled"); - - getSuggestionsDef - .then(concurrency.delay.bind(concurrency, 0)) - .then(function () { - assert.containsOnce(form, '.o_mention_proposition:visible', - "there should be one mention suggestion"); - assert.strictEqual(form.$('.o_mention_proposition').data('id'), 1, - "suggestion's id should be correct"); - assert.strictEqual(form.$('.o_mention_proposition .o_mention_name').text(), 'Test User', - "suggestion should be displayed correctly"); - assert.strictEqual(form.$('.o_mention_proposition .o_mention_info').text(), '(test@odoo.com)', - "suggestion should be displayed correctly"); - - //cleanup - form.destroy(); - done(); - }); + await testUtils.nextTick(); + await getSuggestionsDef; + await testUtils.nextTick(); + assert.containsOnce(form, '.o_mention_proposition:visible', + "there should be one mention suggestion"); + assert.strictEqual(form.$('.o_mention_proposition').data('id'), 1, + "suggestion's id should be correct"); + assert.strictEqual(form.$('.o_mention_proposition .o_mention_name').text(), 'Test User', + "suggestion should be displayed correctly"); + assert.strictEqual(form.$('.o_mention_proposition .o_mention_info').text(), '(test@odoo.com)', + "suggestion should be displayed correctly"); + //cleanup + form.destroy(); }); -QUnit.test('chatter: post a message disable the send button', function(assert) { +QUnit.test('chatter: post a message disable the send button', async function(assert) { assert.expect(3); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -929,15 +917,15 @@ QUnit.test('chatter: post a message disable the send button', function(assert) { session: {}, mockRPC: function (route, args) { if (args.method === 'message_get_suggested_recipients') { - return $.when({2: []}); + return Promise.resolve({2: []}); } if (args.method === 'message_post') { assert.ok(form.$('.o_composer_button_send').prop("disabled"), "Send button should be disabled when a message is being sent"); - return $.when(57923); + return Promise.resolve(57923); } if (args.method === 'message_format') { - return $.when([{ + return Promise.resolve([{ author_id: ["42", "Me"], model: 'partner', }]); @@ -946,20 +934,20 @@ QUnit.test('chatter: post a message disable the send button', function(assert) { }, }); - testUtils.dom.click(form.$('.o_chatter_button_new_message')); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); assert.notOk(form.$('.o_composer_button_send').prop('disabled'), "Send button should be enabled when posting a message"); form.$('.oe_chatter .o_composer_text_field:first()').val("My first message"); - testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); - testUtils.dom.click(form.$('.o_chatter_button_new_message')); + await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); assert.notOk(form.$('.o_composer_button_send').prop('disabled'), "Send button should be enabled when posting another message"); form.destroy(); }); -QUnit.test('chatter: post message failure keep message', function(assert) { +QUnit.test('chatter: post message failure keep message', async function(assert) { assert.expect(4); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -976,26 +964,27 @@ QUnit.test('chatter: post message failure keep message', function(assert) { session: {}, mockRPC: function (route, args) { if (args.method === 'message_get_suggested_recipients') { - return $.when({2: []}); + return Promise.resolve({2: []}); } if (args.method === 'message_post') { assert.ok(form.$('.o_composer_button_send').prop("disabled"), "Send button should be disabled when a message is being sent"); // simulate failure - return $.Deferred().reject(); + return Promise.reject(); + } if (args.method === 'message_format') { - return $.when([]); + return Promise.resolve([]); } return this._super(route, args); }, }); - testUtils.dom.click(form.$('.o_chatter_button_new_message')); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); assert.notOk(form.$('.o_composer_button_send').prop('disabled'), "Send button should be enabled initially"); - form.$('.oe_chatter .o_composer_text_field:first()').val("My first message"); - testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); + await testUtils.fields.editInput(form.$('.oe_chatter .o_composer_text_field:first()'), "My first message"); + await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); assert.strictEqual(form.$('.o_composer_text_field').val(), "My first message", "Should keep unsent message in the composer on failure"); assert.notOk(form.$('.o_composer_button_send').prop('disabled'), @@ -1003,10 +992,10 @@ QUnit.test('chatter: post message failure keep message', function(assert) { form.destroy(); }); -QUnit.test('chatter: receive notif when document is open', function (assert) { +QUnit.test('chatter: receive notif when document is open', async function (assert) { assert.expect(2); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1043,14 +1032,14 @@ QUnit.test('chatter: receive notif when document is open', function (assert) { this.data['mail.message'].records.push(needactionMessageData); var notification = [[false, 'mail.channel', 1], needactionMessageData]; form.call('bus_service', 'trigger', 'notification', [notification]); - + await testUtils.nextMicrotaskTick(); assert.strictEqual(thread.getUnreadCounter(), 1, "document thread should now have one unread message"); form.destroy(); }); -QUnit.test('chatter: access document with some notifs', function (assert) { +QUnit.test('chatter: access document with some notifs', async function (assert) { assert.expect(3); // simulate received needaction message on this document thread @@ -1067,7 +1056,7 @@ QUnit.test('chatter: access document with some notifs', function (assert) { this.data['mail.message'].records.push(needactionMessageData); this.data['partner'].records[0].message_ids = [5]; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1101,10 +1090,10 @@ QUnit.test('chatter: access document with some notifs', function (assert) { form.destroy(); }); -QUnit.test('chatter: post a message and switch in edit mode', function (assert) { +QUnit.test('chatter: post a message and switch in edit mode', async function (assert) { assert.expect(5); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1121,7 +1110,7 @@ QUnit.test('chatter: post a message and switch in edit mode', function (assert) session: {}, mockRPC: function (route, args) { if (args.method === 'message_get_suggested_recipients') { - return $.when({2: []}); + return Promise.resolve({2: []}); } if (args.method === 'message_post') { this.data['mail.message'].records.push({ @@ -1136,7 +1125,7 @@ QUnit.test('chatter: post a message and switch in edit mode', function (assert) model: 'partner', res_id: 2, }); - return $.when(42); + return Promise.resolve(42); } return this._super(route, args); @@ -1146,15 +1135,15 @@ QUnit.test('chatter: post a message and switch in edit mode', function (assert) assert.containsNone(form, '.o_thread_message', "thread should not contain messages"); // send a message - testUtils.dom.click(form.$('.o_chatter_button_new_message')); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); form.$('.oe_chatter .o_composer_text_field:first()').val("My first message"); - testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); + await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); assert.containsOnce(form, '.o_thread_message', "thread should contain a message"); assert.ok(form.$('.o_thread_message:first() .o_thread_message_core').text().indexOf('My first message') >= 0, "the message's body should be correct"); // switch in edit mode - testUtils.form.clickEdit(form); + await testUtils.form.clickEdit(form); assert.containsOnce(form, '.o_thread_message', "thread should contain a message"); assert.ok(form.$('.o_thread_message:first() .o_thread_message_core').text().indexOf('My first message') >= 0, "the message's body should be correct"); @@ -1162,13 +1151,13 @@ QUnit.test('chatter: post a message and switch in edit mode', function (assert) form.destroy(); }); -QUnit.test('chatter: discard changes on message post with post_refresh "always"', function (assert) { +QUnit.test('chatter: discard changes on message post with post_refresh "always"', async function (assert) { // After posting a message that always reloads the record, if the record // is dirty (= has some unsaved changes), we should warn the user that // these changes will be lost if he proceeds. assert.expect(2); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1186,7 +1175,7 @@ QUnit.test('chatter: discard changes on message post with post_refresh "always"' session: {}, mockRPC: function (route, args) { if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") { - return $.when({2: []}); + return Promise.resolve({2: []}); } return this._super(route, args); }, @@ -1196,12 +1185,12 @@ QUnit.test('chatter: discard changes on message post with post_refresh "always"' }); // Make record dirty - testUtils.fields.editInput(form.$('.o_form_sheet input'), 'trululu'); + await testUtils.fields.editInput(form.$('.o_form_sheet input'), 'trululu'); // Send a message - testUtils.dom.click(form.$('.o_chatter_button_new_message')); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); form.$('.oe_chatter .o_composer_text_field:first()').val("My first message"); - testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); + await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); var $modal = $('.modal-dialog'); assert.strictEqual($modal.length, 1, "should have a modal opened"); @@ -1212,14 +1201,14 @@ QUnit.test('chatter: discard changes on message post with post_refresh "always"' form.destroy(); }); -QUnit.test('chatter: discard changes on message post without post_refresh', function (assert) { +QUnit.test('chatter: discard changes on message post without post_refresh', async function (assert) { // After posting a message, if the record is dirty and there are no // post_refresh rule, it will not discard the changes on the record. assert.expect(2); var hasDiscardChanges = false; // set if `discard_changes` has been triggered up var messages = []; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1237,13 +1226,13 @@ QUnit.test('chatter: discard changes on message post without post_refresh', func session: {}, mockRPC: function (route, args) { if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") { - return $.when({2: []}); + return Promise.resolve({2: []}); } if (args.method === 'message_format') { var requested_msgs = _.filter(messages, function (msg) { return _.contains(args.args[0], msg.id); }); - return $.when(requested_msgs); + return Promise.resolve(requested_msgs); } if (args.method === 'message_post') { messages.push({ @@ -1258,7 +1247,7 @@ QUnit.test('chatter: discard changes on message post without post_refresh', func model: 'partner', res_id: 2, }); - return $.when(42); + return Promise.resolve(42); } return this._super(route, args); }, @@ -1273,12 +1262,12 @@ QUnit.test('chatter: discard changes on message post without post_refresh', func }); // Make record dirty - testUtils.fields.editInput(form.$('.o_form_sheet input'), 'trululu'); + await testUtils.fields.editInput(form.$('.o_form_sheet input'), 'trululu'); // Send a message - testUtils.dom.click(form.$('.o_chatter_button_new_message')); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); form.$('.oe_chatter .o_composer_text_field:first()').val("My first message"); - testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); + await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); var $modal = $('.modal-dialog'); assert.strictEqual($modal.length, 0, "should have no modal opened"); @@ -1287,18 +1276,17 @@ QUnit.test('chatter: discard changes on message post without post_refresh', func form.destroy(); }); -QUnit.test('chatter: discard changes on message post with post_refresh "recipients"', function (assert) { +QUnit.test('chatter: discard changes on message post with post_refresh "recipients"', async function (assert) { // After posting a message with mentions, the record will be reloaded, // as the rpc `message_post` may make changes on some fields of the record. // If the record is dirty (= has some unsaved changes), we should warn the // user that these changes will be lost if he proceeds. assert.expect(2); - var done = assert.async(); - var getSuggestionsDef = $.Deferred(); + var getSuggestionsDef = testUtils.makeTestPromise(); var messages = []; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1316,17 +1304,17 @@ QUnit.test('chatter: discard changes on message post with post_refresh "recipien session: {}, mockRPC: function (route, args) { if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") { - return $.when({2: [[42, "Me"]]}); + return Promise.resolve({2: [[42, "Me"]]}); } if (args.method === 'get_mention_suggestions') { getSuggestionsDef.resolve(); - return $.when([[{email: "me@odoo.com", id: 42, name: "Me"}], []]); + return Promise.resolve([[{email: "me@odoo.com", id: 42, name: "Me"}], []]); } if (args.method === 'message_format') { var requested_msgs = _.filter(messages, function (msg) { return _.contains(args.args[0], msg.id); }); - return $.when(requested_msgs); + return Promise.resolve(requested_msgs); } if (args.method === 'message_post') { messages.push({ @@ -1341,7 +1329,7 @@ QUnit.test('chatter: discard changes on message post with post_refresh "recipien model: 'partner', res_id: 2, }); - return $.when(42); + return Promise.resolve(42); } return this._super(route, args); }, @@ -1351,13 +1339,13 @@ QUnit.test('chatter: discard changes on message post with post_refresh "recipien }); // Make record dirty - testUtils.fields.editInput(form.$('.o_form_sheet input'), 'trululu'); + await testUtils.fields.editInput(form.$('.o_form_sheet input'), 'trululu'); // create a new message - testUtils.dom.click(form.$('.o_chatter_button_new_message')); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); // Add a user as mention - form.$('.oe_chatter .o_composer_text_field:first()').val("@"); + await testUtils.fields.editInput(form.$('.oe_chatter .o_composer_text_field:first()'), "@"); var $input = form.$('.oe_chatter .o_composer_text_field:first()'); $input.val('@'); @@ -1365,38 +1353,35 @@ QUnit.test('chatter: discard changes on message post with post_refresh "recipien $input[0].selectionStart = 1; $input[0].selectionEnd = 1; $input.trigger('keyup'); + await getSuggestionsDef; + await testUtils.nextTick(); + // click on mention + $input.trigger($.Event('keyup', {which: $.ui.keyCode.ENTER})); + await testUtils.nextTick(); - getSuggestionsDef - .then(concurrency.delay.bind(concurrency, 0)) - .then(function () { - // click on mention - $input.trigger($.Event('keyup', {which: $.ui.keyCode.ENTER})); + // untick recipient as follower (prompts a res.partner form otherwise) + form.$('input[type="checkbox"]').prop('checked', false); + await testUtils.nextTick(); + // send message + await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); - // untick recipient as follower (prompts a res.partner form otherwise) - form.$('input[type="checkbox"]').prop('checked', false); + var $modal = $('.modal-dialog'); + assert.strictEqual($modal.length, 1, "should have a modal opened"); + assert.strictEqual($modal.find('.modal-body').text(), + "The record has been modified, your changes will be discarded. Do you want to proceed?", + "should warn the user that any unsaved changes will be lost"); - // send message - testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); - - var $modal = $('.modal-dialog'); - assert.strictEqual($modal.length, 1, "should have a modal opened"); - assert.strictEqual($modal.find('.modal-body').text(), - "The record has been modified, your changes will be discarded. Do you want to proceed?", - "should warn the user that any unsaved changes will be lost"); - - form.destroy(); - done(); - }); + form.destroy(); }); -QUnit.test('chatter: discard changes on opening full-composer', function (assert) { +QUnit.test('chatter: discard changes on opening full-composer', async function (assert) { // When we open the full-composer, any following operations by the user // will reload the record (even closing the full-composer). Therefore, // we should warn the user when we open the full-composer if the record // is dirty (= has some unsaved changes). assert.expect(2); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1415,7 +1400,7 @@ QUnit.test('chatter: discard changes on opening full-composer', function (assert session: {}, mockRPC: function (route, args) { if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") { - return $.when({2: []}); + return Promise.resolve({2: []}); } return this._super(route, args); }, @@ -1425,11 +1410,11 @@ QUnit.test('chatter: discard changes on opening full-composer', function (assert }); // Make record dirty - testUtils.fields.editInput(form.$('.o_form_sheet input'), 'trululu'); + await testUtils.fields.editInput(form.$('.o_form_sheet input'), 'trululu'); // Open full-composer - testUtils.dom.click(form.$('.o_chatter_button_new_message')); - testUtils.dom.click(form.$('.o_composer_button_full_composer')); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); + await testUtils.dom.click(form.$('.o_composer_button_full_composer')); var $modal = $('.modal-dialog'); assert.strictEqual($modal.length, 1, "should have a modal opened"); @@ -1440,7 +1425,7 @@ QUnit.test('chatter: discard changes on opening full-composer', function (assert form.destroy(); }); -QUnit.test('chatter in x2many form view', function (assert) { +QUnit.test('chatter in x2many form view', async function (assert) { // the purpose of this test is to ensure that it doesn't crash when a x2many // record is opened in form view (thus in a dialog), and when there is a // chatter in the arch (typically, this may occur when the view used for the @@ -1453,7 +1438,7 @@ QUnit.test('chatter in x2many form view', function (assert) { }; this.data.partner.records[0].o2m = [2]; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1474,7 +1459,7 @@ QUnit.test('chatter in x2many form view', function (assert) { }, }); - testUtils.dom.click(form.$('.o_data_row:first')); + await testUtils.dom.click(form.$('.o_data_row:first')); assert.strictEqual($('.modal .o_form_view').length, 1, "should have open a form view in a modal"); @@ -1484,7 +1469,7 @@ QUnit.test('chatter in x2many form view', function (assert) { form.destroy(); }); -QUnit.test('chatter: Attachment viewer', function (assert) { +QUnit.test('chatter: Attachment viewer', async function (assert) { assert.expect(6); this.data.partner.records[0].message_ids = [1]; this.data['mail.message'].records = [{ @@ -1527,7 +1512,7 @@ QUnit.test('chatter: Attachment viewer', function (assert) { res_id: 2, }]; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1545,7 +1530,7 @@ QUnit.test('chatter: Attachment viewer', function (assert) { if (_.str.contains(route, '/mail/attachment/preview/') || _.str.contains(route, '/web/static/lib/pdfjs/web/viewer.html')){ var canvas = document.createElement('canvas'); - return $.when(canvas.toDataURL()); + return Promise.resolve(canvas.toDataURL()); } return this._super.apply(this, arguments); }, @@ -1555,29 +1540,29 @@ QUnit.test('chatter: Attachment viewer', function (assert) { assert.hasAttrValue(form.$('.o_thread_message .o_attachment a').first(), 'href', '/web/content/1?download=true', "image caption should have correct download link"); // click on first image attachement - testUtils.dom.click(form.$('.o_thread_message .o_attachment .o_image_box .o_image_overlay').first()); + await testUtils.dom.click(form.$('.o_thread_message .o_attachment .o_image_box .o_image_overlay').first()); assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[data-src="/web/image/1?unique=1&signature=999&model=ir.attachment"]').length, 1, "Modal popup should open with first image src"); // click on next button - testUtils.dom.click($('.modal .arrow.arrow-right.move_next span')); + await testUtils.dom.click($('.modal .arrow.arrow-right.move_next span')); assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[data-src="/web/image/2?unique=1&signature=999&model=ir.attachment"]').length, 1, "Modal popup should have now second image src"); assert.strictEqual($('.o_modal_fullscreen .o_viewer_toolbar .o_download_btn').length, 1, "Modal popup should have download button"); // close attachment popup - testUtils.dom.click($('.o_modal_fullscreen .o_viewer-header .o_close_btn')); + await testUtils.dom.click($('.o_modal_fullscreen .o_viewer-header .o_close_btn')); // click on pdf attachement - testUtils.dom.click(form.$('span:contains(Test PDF 1)')); + await testUtils.dom.click(form.$('span:contains(Test PDF 1)')); assert.strictEqual($('.o_modal_fullscreen iframe[data-src*="/web/content/4"]').length, 1, "Modal popup should open with the pdf preview"); // close attachment popup - testUtils.dom.click($('.o_modal_fullscreen .o_viewer-header .o_close_btn')); + await testUtils.dom.click($('.o_modal_fullscreen .o_viewer-header .o_close_btn')); form.destroy(); }); -QUnit.test('chatter: keep context when sending a message', function(assert) { +QUnit.test('chatter: keep context when sending a message', async function(assert) { assert.expect(1); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1596,7 +1581,7 @@ QUnit.test('chatter: keep context when sending a message', function(assert) { }, mockRPC: function (route, args) { if (args.method === 'message_get_suggested_recipients') { - return $.when({2: []}); + return Promise.resolve({2: []}); } if (args.method === 'message_post') { assert.deepEqual(args.kwargs.context, { @@ -1606,10 +1591,10 @@ QUnit.test('chatter: keep context when sending a message', function(assert) { mail_post_autofollow: true, }, "the context is incorrect"); - return $.when(57923); + return Promise.resolve(57923); } if (args.method === 'message_format') { - return $.when([{ + return Promise.resolve([{ author_id: [42, "Me"], model: 'partner', }]); @@ -1618,13 +1603,13 @@ QUnit.test('chatter: keep context when sending a message', function(assert) { }, }); - testUtils.dom.click(form.$('.o_chatter_button_new_message')); - testUtils.fields.editInput(form.$('.oe_chatter .o_composer_text_field:first()'), 'Pouet'); - testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); + await testUtils.fields.editInput(form.$('.oe_chatter .o_composer_text_field:first()'), 'Pouet'); + await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); form.destroy(); }); -QUnit.test('form activity widget: read RPCs', function (assert) { +QUnit.test('form activity widget: read RPCs', async function (assert) { // Records of model 'mail.activity' may be updated in business flows (e.g. // the date of a 'Meeting' activity is updated when the associated meeting // is dragged&dropped in the Calendar view). Because of that, the activities @@ -1645,7 +1630,7 @@ QUnit.test('form activity widget: read RPCs', function (assert) { }]; var nbReads = 0; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1670,15 +1655,15 @@ QUnit.test('form activity widget: read RPCs', function (assert) { assert.strictEqual(form.$('.o_mail_activity .o_thread_message .o_activity_date').text(), 'Today', "the activity should be today"); - testUtils.form.clickEdit(form); - testUtils.form.clickSave(form); + await testUtils.form.clickEdit(form); + await testUtils.form.clickSave(form); assert.strictEqual(nbReads, 1, "should not have re-read the activities"); // simulate a date change, and a reload of the form view var tomorrow = moment().add(1, 'day').format("YYYY-MM-DD"); this.data['mail.activity'].records[0].date_deadline = tomorrow; - form.reload(); + await form.reload(); assert.strictEqual(nbReads, 2, "should have re-read the activities"); assert.strictEqual(form.$('.o_mail_activity .o_thread_message .o_activity_date').text(), @@ -1687,10 +1672,10 @@ QUnit.test('form activity widget: read RPCs', function (assert) { form.destroy(); }); -QUnit.test('form activity widget on a new record', function (assert) { +QUnit.test('form activity widget on a new record', async function (assert) { assert.expect(0); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1711,7 +1696,7 @@ QUnit.test('form activity widget on a new record', function (assert) { form.destroy(); }); -QUnit.test('form activity widget with another x2many field in view', function (assert) { +QUnit.test('form activity widget with another x2many field in view', async function (assert) { assert.expect(1); this.data.partner.fields.m2m = {string: "M2M", type: 'many2many', relation: 'partner'}; @@ -1730,7 +1715,7 @@ QUnit.test('form activity widget with another x2many field in view', function (a activity_type_id: 2, }]; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1750,7 +1735,7 @@ QUnit.test('form activity widget with another x2many field in view', function (a form.destroy(); }); -QUnit.test('form activity widget: schedule next activity', function (assert) { +QUnit.test('form activity widget: schedule next activity', async function (assert) { assert.expect(4); this.data.partner.records[0].activity_ids = [1]; this.data.partner.records[0].activity_state = 'today'; @@ -1765,7 +1750,7 @@ QUnit.test('form activity widget: schedule next activity', function (assert) { activity_type_id: 2, }]; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1785,7 +1770,7 @@ QUnit.test('form activity widget: schedule next activity', function (assert) { assert.ok(_.isEqual(args.args[0], [1]), "should call 'action_feedback_schedule_next' for id 1"); assert.strictEqual(args.kwargs.feedback, 'everything is ok', "the feedback should be sent correctly"); - return $.when('test_result'); + return Promise.resolve('test_result'); } return this._super.apply(this, arguments); }, @@ -1797,16 +1782,16 @@ QUnit.test('form activity widget: schedule next activity', function (assert) { }, }); //Schedule next activity - testUtils.dom.click(form.$('.o_mail_activity .o_mark_as_done[data-activity-id=1]')); + await testUtils.dom.click(form.$('.o_mail_activity .o_mark_as_done[data-activity-id=1]')); assert.containsOnce(form, '.o_mail_activity_feedback.popover', "a feedback popover should be visible"); $('.o_mail_activity_feedback.popover textarea').val('everything is ok'); // write a feedback - testUtils.dom.click(form.$('.o_activity_popover_done_next')); + await testUtils.dom.click(form.$('.o_activity_popover_done_next')); form.destroy(); }); -QUnit.test('form activity widget: edit next activity', function (assert) { +QUnit.test('form activity widget: edit next activity', async function (assert) { assert.expect(3); var self = this; this.data.partner.records[0].activity_ids = [1]; @@ -1822,7 +1807,7 @@ QUnit.test('form activity widget: edit next activity', function (assert) { activity_type_id: 2, }]; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1866,13 +1851,13 @@ QUnit.test('form activity widget: edit next activity', function (assert) { }); assert.strictEqual(form.$('.o_mail_activity .o_mail_info strong:eq(1)').text(), " Type 2", "Initial type should be Type 2"); - testUtils.dom.click(form.$('.o_mail_activity .o_edit_activity[data-activity-id=1]')); + await testUtils.dom.click(form.$('.o_mail_activity .o_edit_activity[data-activity-id=1]')); assert.strictEqual(form.$('.o_mail_activity .o_mail_info strong:eq(1)').text(), " Type 1", "After edit type should be Type 1"); form.destroy(); }); -QUnit.test('form activity widget: clic mail template', function (assert) { +QUnit.test('form activity widget: clic mail template', async function (assert) { assert.expect(4); this.data.partner.records[0].activity_ids = [1]; this.data.partner.records[0].activity_state = 'today'; @@ -1886,7 +1871,7 @@ QUnit.test('form activity widget: clic mail template', function (assert) { activity_type_id: 2, }]; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1935,16 +1920,16 @@ QUnit.test('form activity widget: clic mail template', function (assert) { "we should have one activity"); assert.containsOnce(form, '.o_activity_template_preview', "Activity should contains one mail template"); - testUtils.dom.click(form.$('.o_activity_template_preview[data-template-id=100]')); + await testUtils.dom.click(form.$('.o_activity_template_preview[data-template-id=100]')); assert.containsOnce(form, '.o_mail_activity .o_thread_message', "activity should still be there"); form.destroy(); }); -QUnit.test('form activity widget: schedule activity does not discard changes', function (assert) { +QUnit.test('form activity widget: schedule activity does not discard changes', async function (assert) { assert.expect(1); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -1976,18 +1961,18 @@ QUnit.test('form activity widget: schedule activity does not discard changes', f }); // update value of foo field - testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'new value'); + await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'new value'); // schedule an activity (this triggers a do_action) - testUtils.dom.click(form.$('.o_chatter_button_schedule_activity')); + await testUtils.dom.click(form.$('.o_chatter_button_schedule_activity')); // save the record - testUtils.form.clickSave(form); + await testUtils.form.clickSave(form); form.destroy(); }); -QUnit.test('form activity widget: mark as done and remove', function (assert) { +QUnit.test('form activity widget: mark as done and remove', async function (assert) { assert.expect(15); var self = this; @@ -2015,7 +2000,7 @@ QUnit.test('form activity widget: mark as done and remove', function (assert) { activity_type_id: 1, }]; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -2072,7 +2057,7 @@ QUnit.test('form activity widget: mark as done and remove', function (assert) { "there should be two activities"); // remove activity 1 - testUtils.dom.click(form.$('.o_mail_activity .o_unlink_activity[data-activity-id=1]')); + await testUtils.dom.click(form.$('.o_mail_activity .o_unlink_activity[data-activity-id=1]')); assert.containsOnce(form, '.o_mail_activity .o_thread_message', "there should be one remaining activity"); assert.ok(!form.$('.o_mail_activity .o_unlink_activity[data-activity-id=1]').length, @@ -2081,11 +2066,11 @@ QUnit.test('form activity widget: mark as done and remove', function (assert) { // mark activity done assert.ok(!form.$('.o_mail_thread .o_thread_message').length, "there should be no chatter message"); - testUtils.dom.click(form.$('.o_mail_activity .o_mark_as_done[data-activity-id=2]')); + await testUtils.dom.click(form.$('.o_mail_activity .o_mark_as_done[data-activity-id=2]')); assert.containsOnce(form, '.o_mail_activity_feedback.popover', "a feedback popover should be visible"); $('.o_mail_activity_feedback.popover textarea').val('everything is ok'); // write a feedback - testUtils.dom.click(form.$('.o_activity_popover_done')); + await testUtils.dom.click(form.$('.o_activity_popover_done')); assert.containsNone(form, '.o_mail_activity_feedback.popover') assert.ok(!form.$('.o_mail_activity .o_thread_message').length, "there should be no more activity"); @@ -2096,7 +2081,7 @@ QUnit.test('form activity widget: mark as done and remove', function (assert) { form.destroy(); }); -QUnit.test('followers widget: follow/unfollow, edit subtypes', function (assert) { +QUnit.test('followers widget: follow/unfollow, edit subtypes', async function (assert) { assert.expect(24); var resID = 2; @@ -2108,7 +2093,7 @@ QUnit.test('followers widget: follow/unfollow, edit subtypes', function (assert) {id: 2, name: "Second subtype", followed: true}, {id: 3, name: "Third subtype", followed: false}, ]; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -2146,10 +2131,10 @@ QUnit.test('followers widget: follow/unfollow, edit subtypes', function (assert) res_model: 'partner', }); } - return $.when(true); + return Promise.resolve(true); } if (route === '/mail/read_followers') { - return $.when({ + return Promise.resolve({ followers: followers, subtypes: subtypes, // caution, subtype will only be returned if current user is in args follower list @@ -2160,7 +2145,7 @@ QUnit.test('followers widget: follow/unfollow, edit subtypes', function (assert) assert.ok(_.isEqual(args.args[1], [partnerID]), 'should call route for correct partner'); this.data.partner.records[0].message_follower_ids = []; followers = []; - return $.when(true); + return Promise.resolve(true); } if (route === '/web/dataset/call_kw/partner/read') { nbReads++; @@ -2182,7 +2167,7 @@ QUnit.test('followers widget: follow/unfollow, edit subtypes', function (assert) 'should display the "Follow" button'); // click to follow the document - testUtils.dom.click(form.$('.o_followers_follow_button')); + await testUtils.dom.click(form.$('.o_followers_follow_button')); assert.strictEqual(form.$('.o_followers_count').text(), "1", 'should have one follower'); assert.ok(form.$('.o_followers_follow_button.o_followers_following').length, 'should display the "Following/Unfollow" button'); @@ -2194,17 +2179,17 @@ QUnit.test('followers widget: follow/unfollow, edit subtypes', function (assert) 'subtype list should contain 3 subtypes'); assert.containsN(form, '.o_subtypes_list .o_subtype_checkbox:checked', 2, 'two subtypes should be checked by default'); - testUtils.dom.click(form.$('.o_subtypes_list .dropdown-toggle')); + await testUtils.dom.click(form.$('.o_subtypes_list .dropdown-toggle')); assert.ok(form.$('.o_subtypes_list.show').length, 'dropdown should be opened'); - testUtils.dom.click(form.$('.o_subtypes_list .o_subtype input[data-id=2]')); + await testUtils.dom.click(form.$('.o_subtypes_list .o_subtype input[data-id=2]')); assert.ok(form.$('.o_subtypes_list.show').length, 'dropdown should remain opened'); assert.ok(!form.$('.o_subtypes_list .o_subtype_checkbox[data-id=2]:checked').length, 'second subtype should now be unchecked'); // click to unfollow - testUtils.dom.click(form.$('.o_followers_follow_button')); + await testUtils.dom.click(form.$('.o_followers_follow_button')); assert.ok($('.modal').length, 'a confirm modal should be opened'); - testUtils.dom.click($('.modal .modal-footer .btn-primary')); + await testUtils.dom.click($('.modal .modal-footer .btn-primary')); assert.strictEqual(form.$('.o_followers_count').text(), "0", 'should have no followers'); assert.ok(form.$('.o_followers_follow_button.o_followers_notfollow').length, 'should display the "Follow" button'); @@ -2212,7 +2197,7 @@ QUnit.test('followers widget: follow/unfollow, edit subtypes', function (assert) form.destroy(); }); -QUnit.test('followers widget: do not display follower duplications', function (assert) { +QUnit.test('followers widget: do not display follower duplications', async function (assert) { assert.expect(2); this.data.partner.records[0].message_follower_ids = [1]; @@ -2225,7 +2210,7 @@ QUnit.test('followers widget: do not display follower duplications', function (a res_model: 'partner', }]; var def; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -2238,7 +2223,7 @@ QUnit.test('followers widget: do not display follower duplications', function (a '', mockRPC: function (route, args) { if (route === '/mail/read_followers') { - return $.when(def).then(function () { + return Promise.resolve(def).then(function () { return { followers: _.filter(followers, function (follower) { return _.contains(args.follower_ids, follower.id); @@ -2266,11 +2251,11 @@ QUnit.test('followers widget: do not display follower duplications', function (a // simulate concurrent calls to read_followers and check that those followers // are not added twice in the dropdown - def = $.Deferred(); + def = testUtils.makeTestPromise(); form.reload(); form.reload(); - def.resolve(); - + await def.resolve(); + await testUtils.nextTick(); assert.strictEqual(form.$('.o_followers_count').text(), '2', "should have 2 followers"); assert.containsN(form, '.o_followers_list .o_partner', 2, @@ -2279,12 +2264,12 @@ QUnit.test('followers widget: do not display follower duplications', function (a form.destroy(); }); -QUnit.test('does not render and crash when destroyed before chat system is ready', function (assert) { +QUnit.test('does not render and crash when destroyed before chat system is ready', async function (assert) { assert.expect(0); - var def = $.Deferred(); + var def = testUtils.makeTestPromise(); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -2302,7 +2287,7 @@ QUnit.test('does not render and crash when destroyed before chat system is ready res_id: 2, mockRPC: function (route, args) { if (args.method === 'message_format') { - return $.when([{ + return Promise.resolve([{ attachment_ids: [], body: "", date: "2016-12-20 09:35:40", @@ -2312,7 +2297,7 @@ QUnit.test('does not render and crash when destroyed before chat system is ready }]); } if (route === '/mail/read_followers') { - return $.when({ + return Promise.resolve({ followers: [], subtypes: [], }); @@ -2332,10 +2317,10 @@ QUnit.test('does not render and crash when destroyed before chat system is ready // itself. We simply make sure here that no crashes occur (since the form // view is destroyed, all rpcs will be dropped, and many other mechanisms // relying on events will not work, such as the chat bus) - def.resolve(); + await def.resolve(); }); -QUnit.test('chatter: do not duplicate messages on (un)star message', function (assert) { +QUnit.test('chatter: do not duplicate messages on (un)star message', async function (assert) { assert.expect(4); this.data.partner.records[0].message_ids = [1]; @@ -2352,7 +2337,7 @@ QUnit.test('chatter: do not duplicate messages on (un)star message', function (a res_id: 2, }]; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -2382,7 +2367,7 @@ QUnit.test('chatter: do not duplicate messages on (un)star message', function (a }; var notification = [[false, 'res.partner'], data]; form.call('bus_service', 'trigger', 'notification', [notification]); - return $.when(); + return Promise.resolve(); } return this._super(route, args); }, @@ -2395,12 +2380,12 @@ QUnit.test('chatter: do not duplicate messages on (un)star message', function (a "message should not be starred"); // star message - testUtils.dom.click(form.$('.o_thread_message .o_thread_message_star')); + await testUtils.dom.click(form.$('.o_thread_message .o_thread_message_star')); assert.containsOnce(form, '.o_thread_message', "there should still be a single message in the chatter after starring the message"); // unstar message - testUtils.dom.click(form.$('.o_thread_message .o_thread_message_star')); + await testUtils.dom.click(form.$('.o_thread_message .o_thread_message_star')); assert.containsOnce(form, '.o_thread_message', "there should still be a single message in the chatter after unstarring the message"); @@ -2408,7 +2393,7 @@ QUnit.test('chatter: do not duplicate messages on (un)star message', function (a form.destroy(); }); -QUnit.test('chatter: new messages on document without any "display_name"', function (assert) { +QUnit.test('chatter: new messages on document without any "display_name"', async function (assert) { assert.expect(5); this.data.partner.records[0].message_ids = [1]; @@ -2426,7 +2411,7 @@ QUnit.test('chatter: new messages on document without any "display_name"', funct res_id: 2, }]; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -2463,7 +2448,7 @@ QUnit.test('chatter: new messages on document without any "display_name"', funct }); this.data.partner.records[0].message_ids.push(2); - form.reload(); + await form.reload(); assert.containsN(form, '.o_thread_message', 2, "should have a two messages in the chatter after reload"); @@ -2476,7 +2461,7 @@ QUnit.test('chatter: new messages on document without any "display_name"', funct form.destroy(); }); -QUnit.test('chatter: suggested partner auto-follow on message post', function (assert) { +QUnit.test('chatter: suggested partner auto-follow on message post', async function (assert) { // need post_refresh 'recipient' to auto-follow suggested recipients // whose checkbox is checked. assert.expect(20); @@ -2507,7 +2492,7 @@ QUnit.test('chatter: suggested partner auto-follow on message post', function (a res_model: 'partner', }); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -2524,7 +2509,7 @@ QUnit.test('chatter: suggested partner auto-follow on message post', function (a res_id: 2, mockRPC: function (route, args) { if (args.method === 'message_get_suggested_recipients') { - return $.when({2: [ + return Promise.resolve({2: [ [ 8, 'DemoUser ', @@ -2567,10 +2552,10 @@ QUnit.test('chatter: suggested partner auto-follow on message post', function (a model: 'partner', res_id: 2, }); - return $.when(messageID); + return Promise.resolve(messageID); } if (route === '/mail/read_followers') { - return $.when({ + return Promise.resolve({ followers: followers, }); } @@ -2596,7 +2581,7 @@ QUnit.test('chatter: suggested partner auto-follow on message post', function (a "should have 'Admin' as follower"); // open composer - testUtils.dom.click(form.$('.o_chatter_button_new_message')); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); assert.isVisible($('.oe_chatter .o_thread_composer'), "chatter should be opened"); assert.strictEqual($('.o_composer_suggested_partners').length, 1, "should display suggested partners"); @@ -2613,7 +2598,7 @@ QUnit.test('chatter: suggested partner auto-follow on message post', function (a // send message form.$('.oe_chatter .o_composer_text_field:first()').val("My first message"); - testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); + await testUtils.dom.click(form.$('.oe_chatter .o_composer_button_send')); assert.strictEqual(form.$('.o_followers_count').text(), "2", "should have a two followers (widget counter)"); @@ -2630,7 +2615,7 @@ QUnit.test('chatter: suggested partner auto-follow on message post', function (a form.destroy(); }); -QUnit.test('chatter: mention prefetched partners (followers & employees)', function (assert) { +QUnit.test('chatter: mention prefetched partners (followers & employees)', async function (assert) { // Note: employees are in prefeteched partner for mentions in chatter when // the module hr is installed. assert.expect(10); @@ -2663,7 +2648,7 @@ QUnit.test('chatter: mention prefetched partners (followers & employees)', funct mention_partner_suggestions: [followerSuggestions.concat(nonFollowerSuggestions)], }; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -2680,7 +2665,7 @@ QUnit.test('chatter: mention prefetched partners (followers & employees)', funct res_id: 2, mockRPC: function (route, args) { if (route === '/mail/read_followers') { - return $.when({ + return Promise.resolve({ followers: [{ id: 10, name: 'FollowerUser1', @@ -2698,7 +2683,7 @@ QUnit.test('chatter: mention prefetched partners (followers & employees)', funct }); } if (args.method === 'message_get_suggested_recipients') { - return $.when({2: []}); + return Promise.resolve({2: []}); } if (args.method === 'get_mention_suggestions') { throw new Error('should not fetch partners for mentions'); @@ -2716,14 +2701,14 @@ QUnit.test('chatter: mention prefetched partners (followers & employees)', funct assert.strictEqual(form.$('.o_composer_mention_dropdown').length, 0, "should not show the mention suggestion dropdown"); - form.$('.o_chatter_button_new_message').click(); + await testUtils.dom.click(form.$('.o_chatter_button_new_message')); var $input = form.$('.oe_chatter .o_composer_text_field:first()'); $input.val('@'); // the cursor position must be set for the mention manager to detect that we are mentionning $input[0].selectionStart = 1; $input[0].selectionEnd = 1; $input.trigger('keyup'); - + await testUtils.nextTick(); assert.strictEqual(form.$('.o_composer_mention_dropdown').length, 1, "should show the mention suggestion dropdown"); @@ -2783,7 +2768,7 @@ QUnit.test('fieldmany2many tags email', function (assert) { this.data.partner.records[0].timmy = [12, 14]; // the modals need to be closed before the form view rendering - createAsyncView({ + createView({ View: FormView, model: 'partner', data: this.data, @@ -2799,7 +2784,7 @@ QUnit.test('fieldmany2many tags email', function (assert) { }, mockRPC: function (route, args) { if (args.method ==='read' && args.model === 'partner_type') { - assert.step(args.args[0]); + assert.step(JSON.stringify(args.args[0])); assert.deepEqual(args.args[1] , ['display_name', 'email'], "should read the email"); } return this._super.apply(this, arguments); @@ -2807,9 +2792,10 @@ QUnit.test('fieldmany2many tags email', function (assert) { archs: { 'partner_type,false,form': '
', }, - }).then(function (form) { + }).then(async function (form) { // should read it 3 times (1 with the form view, one with the form dialog and one after save) - assert.verifySteps([[12, 14], [14], [14]]); + assert.verifySteps(['[12,14]', '[14]', '[14]']); + await testUtils.nextTick(); assert.containsN(form, '.o_field_many2manytags[name="timmy"] .badge.o_tag_color_0', 2, "two tags should be present"); var firstTag = form.$('.o_field_many2manytags[name="timmy"] .badge.o_tag_color_0').first(); @@ -2820,25 +2806,27 @@ QUnit.test('fieldmany2many tags email', function (assert) { form.destroy(); done(); }); + testUtils.nextTick().then(function() { + assert.strictEqual($('.modal-body.o_act_window').length, 1, + "there should be one modal opened to edit the empty email"); + assert.strictEqual($('.modal-body.o_act_window input[name="display_name"]').val(), "silver", + "the opened modal should be a form view dialog with the partner_type 14"); + assert.strictEqual($('.modal-body.o_act_window input[name="email"]').length, 1, + "there should be an email field in the modal"); - assert.strictEqual($('.modal-body.o_act_window').length, 1, - "there should be one modal opened to edit the empty email"); - assert.strictEqual($('.modal-body.o_act_window input[name="display_name"]').val(), "silver", - "the opened modal should be a form view dialog with the partner_type 14"); - assert.strictEqual($('.modal-body.o_act_window input[name="email"]').length, 1, - "there should be an email field in the modal"); + // set the email and save the modal (will render the form view) + testUtils.fields.editInput($('.modal-body.o_act_window input[name="email"]'), 'coucou@petite.perruche'); + testUtils.dom.click($('.modal-footer .btn-primary')); + }); - // set the email and save the modal (will render the form view) - testUtils.fields.editInput($('.modal-body.o_act_window input[name="email"]'), 'coucou@petite.perruche'); - testUtils.dom.click($('.modal-footer .btn-primary')); }); -QUnit.test('fieldmany2many tags email (edition)', function (assert) { +QUnit.test('fieldmany2many tags email (edition)', async function (assert) { assert.expect(15); this.data.partner.records[0].timmy = [12]; - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -2854,7 +2842,7 @@ QUnit.test('fieldmany2many tags email (edition)', function (assert) { }, mockRPC: function (route, args) { if (args.method ==='read' && args.model === 'partner_type') { - assert.step(args.args[0]); + assert.step(JSON.stringify(args.args[0])); assert.deepEqual(args.args[1] , ['display_name', 'email'], "should read the email"); } return this._super.apply(this, arguments); @@ -2864,13 +2852,13 @@ QUnit.test('fieldmany2many tags email (edition)', function (assert) { }, }); - assert.verifySteps([[12]]); + assert.verifySteps(['[12]']); assert.containsOnce(form, '.o_field_many2manytags[name="timmy"] .badge.o_tag_color_0', "should contain one tag"); // add an other existing tag - testUtils.fields.many2one.clickOpenDropdown('timmy'); - testUtils.fields.many2one.clickHighlightedItem('timmy'); + await testUtils.fields.many2one.clickOpenDropdown('timmy'); + await testUtils.fields.many2one.clickHighlightedItem('timmy'); assert.strictEqual($('.modal-body.o_act_window').length, 1, "there should be one modal opened to edit the empty email"); @@ -2880,14 +2868,14 @@ QUnit.test('fieldmany2many tags email (edition)', function (assert) { "there should be an email field in the modal"); // set the email and save the modal (will rerender the form view) - testUtils.fields.editInput($('.modal-body.o_act_window input[name="email"]'), 'coucou@petite.perruche'); - testUtils.dom.click($('.modal-footer .btn-primary')); + await testUtils.fields.editInput($('.modal-body.o_act_window input[name="email"]'), 'coucou@petite.perruche'); + await testUtils.dom.click($('.modal-footer .btn-primary')); assert.containsN(form, '.o_field_many2manytags[name="timmy"] .badge.o_tag_color_0', 2, "should contain the second tag"); // should have read [14] three times: when opening the dropdown, when opening the modal, and // after the save - assert.verifySteps([[12], [14], [14], [14]]); + assert.verifySteps(['[14]', '[14]', '[14]']); form.destroy(); }); diff --git a/addons/mail/static/tests/discuss_mobile_tests.js b/addons/mail/static/tests/discuss_mobile_tests.js index 19e4c8bdd7f..84fd155522c 100644 --- a/addons/mail/static/tests/discuss_mobile_tests.js +++ b/addons/mail/static/tests/discuss_mobile_tests.js @@ -20,68 +20,64 @@ QUnit.module('Discuss in mobile', { }, }); -QUnit.test('mobile basic rendering', function (assert) { +QUnit.test('mobile basic rendering', async function (assert) { // This is a very basic first test for the client action. However, with // the chat_service, it is hard to override RPCs (for instance, the // /mail/client_action route is always called when the test suite is // launched), and we must wait for this RPC to be done before starting to // test the interface. This should be refactored to facilitate the testing. assert.expect(16); - var done = assert.async(); - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, - debug: true, - }).then(function (discuss) { - // test basic rendering in mobile - assert.containsOnce(discuss, '.o_mail_discuss_content .o_mail_no_content', - "should display the no content message"); - assert.containsOnce(discuss, '.o_mail_mobile_tabs', - "should have rendered the tabs"); - assert.hasClass(discuss.$('.o_mail_mobile_tab[data-type=mailbox_inbox]'),'active', - "should be in inbox tab"); - assert.containsOnce(discuss, '.o_mail_discuss_mobile_mailboxes_buttons:visible', - "inbox/starred buttons should be visible"); - assert.hasClass(discuss.$('.o_mail_discuss_mobile_mailboxes_buttons .o_mailbox_inbox_item[data-type=mailbox_inbox]'),'btn-primary', - "should be in inbox"); - assert.hasClass($('.o_mail_discuss_button_dm_chat'),'d-none', - "should have invisible button 'New Message'"); - assert.hasClass($('.o_mail_discuss_button_multi_user_channel'),'d-none', - "should have invisible button 'New Channel'"); - - // move to 'Chat' tab - testUtils.dom.click(discuss.$('.o_mail_mobile_tab[data-type=dm_chat]')); - assert.hasClass(discuss.$('.o_mail_mobile_tab[data-type=dm_chat]'),'active', - "should be in 'Chat' tab"); - assert.containsNone(discuss, '.o_mail_discuss_content .o_mail_no_content', - "should display the no content message"); - assert.strictEqual($('.o_mail_discuss_button_dm_chat').length, 1, - "should have a button to open DM chat in 'Chat' tab"); - assert.doesNotHaveClass($('.o_mail_discuss_button_dm_chat'), 'd-none', - "should be visible in 'Chat' tab"); - assert.hasClass($('.o_mail_discuss_button_multi_user_channel'),'d-none', - "should have invisible button 'New Channel' in 'Chat' tab"); - testUtils.dom.click($('.o_mail_discuss_button_dm_chat')); - assert.containsOnce(discuss, '.o_mail_add_thread input:visible', - "should display the input to add a channel"); - - // move to 'Channels' tab - testUtils.dom.click(discuss.$('.o_mail_mobile_tab[data-type=multi_user_channel]')); - assert.hasClass($('.o_mail_discuss_button_dm_chat'),'d-none', - "should have invisible button 'New Message' in 'Channels' tab"); - assert.doesNotHaveClass($('.o_mail_discuss_button_multi_user_channel'), 'd-none', - "should have visible button 'New Channel' in 'Channels' tab"); - testUtils.dom.click($('.o_mail_discuss_button_multi_user_channel')); - assert.containsOnce(discuss, '.o_mail_add_thread input:visible', - "should display the input to add a channel"); - - discuss.destroy(); - done(); }); + // test basic rendering in mobile + assert.containsOnce(discuss, '.o_mail_discuss_content .o_mail_no_content', + "should display the no content message"); + assert.containsOnce(discuss, '.o_mail_mobile_tabs', + "should have rendered the tabs"); + assert.hasClass(discuss.$('.o_mail_mobile_tab[data-type=mailbox_inbox]'),'active', + "should be in inbox tab"); + assert.containsOnce(discuss, '.o_mail_discuss_mobile_mailboxes_buttons:visible', + "inbox/starred buttons should be visible"); + assert.hasClass(discuss.$('.o_mail_discuss_mobile_mailboxes_buttons .o_mailbox_inbox_item[data-type=mailbox_inbox]'),'btn-primary', + "should be in inbox"); + assert.hasClass($('.o_mail_discuss_button_dm_chat'),'d-none', + "should have invisible button 'New Message'"); + assert.hasClass($('.o_mail_discuss_button_multi_user_channel'),'d-none', + "should have invisible button 'New Channel'"); + + // move to 'Chat' tab + await testUtils.dom.click(discuss.$('.o_mail_mobile_tab[data-type=dm_chat]')); + assert.hasClass(discuss.$('.o_mail_mobile_tab[data-type=dm_chat]'),'active', + "should be in 'Chat' tab"); + assert.containsNone(discuss, '.o_mail_discuss_content .o_mail_no_content', + "should display the no content message"); + assert.strictEqual($('.o_mail_discuss_button_dm_chat').length, 1, + "should have a button to open DM chat in 'Chat' tab"); + assert.doesNotHaveClass($('.o_mail_discuss_button_dm_chat'), 'd-none', + "should be visible in 'Chat' tab"); + assert.hasClass($('.o_mail_discuss_button_multi_user_channel'),'d-none', + "should have invisible button 'New Channel' in 'Chat' tab"); + await testUtils.dom.click($('.o_mail_discuss_button_dm_chat')); + assert.containsOnce(discuss, '.o_mail_add_thread input:visible', + "should display the input to add a channel"); + + // move to 'Channels' tab + await testUtils.dom.click(discuss.$('.o_mail_mobile_tab[data-type=multi_user_channel]')); + assert.hasClass($('.o_mail_discuss_button_dm_chat'),'d-none', + "should have invisible button 'New Message' in 'Channels' tab"); + assert.doesNotHaveClass($('.o_mail_discuss_button_multi_user_channel'), 'd-none', + "should have visible button 'New Channel' in 'Channels' tab"); + await testUtils.dom.click($('.o_mail_discuss_button_multi_user_channel') ); + assert.containsOnce(discuss, '.o_mail_add_thread input:visible', + "should display the input to add a channel"); + + discuss.destroy(); }); QUnit.test('on_{attach/detach}_callback', function (assert) { diff --git a/addons/mail/static/tests/discuss_moderation_tests.js b/addons/mail/static/tests/discuss_moderation_tests.js index 9c3807527ce..8b50aae0b53 100644 --- a/addons/mail/static/tests/discuss_moderation_tests.js +++ b/addons/mail/static/tests/discuss_moderation_tests.js @@ -145,7 +145,7 @@ QUnit.test('moderator: moderated channel with pending moderation message', funct data: this.data, services: this.services, }) - .then(function (discuss) { + .then(async function (discuss) { var $moderationBox = discuss.$( '.o_mail_discuss_item' + '[data-thread-id="mailbox_moderation"]'); @@ -156,7 +156,7 @@ QUnit.test('moderator: moderated channel with pending moderation message', funct "the mailbox counter of the moderation mailbox should display '1'"); // 1. go to moderation mailbox - testUtils.dom.click($moderationBox); + await testUtils.dom.click($moderationBox); var $message = discuss.$('.o_thread_message'); // check message assert.strictEqual($message.length, 1, @@ -210,7 +210,7 @@ QUnit.test('moderator: moderated channel with pending moderation message', funct 'the moderate button "Discard" should be invisible by default'); // click on message moderation checkbox - testUtils.dom.click($message.find('.moderation_checkbox')); + await testUtils.dom.click($message.find('.moderation_checkbox')); assert.ok($message.find('.moderation_checkbox').prop('checked'), "the moderation checkbox should become checked after click"); // check select all (disabled) / unselect all buttons (enabled) @@ -227,7 +227,7 @@ QUnit.test('moderator: moderated channel with pending moderation message', funct 'the moderate button "Discard" should become visible'); // 2. go to channel 'general' - testUtils.dom.click(discuss.$('.o_mail_discuss_item[data-thread-id="1"]')); + await testUtils.dom.click(discuss.$('.o_mail_discuss_item[data-thread-id="1"]')); $message = discuss.$('.o_thread_message'); // check correct message assert.strictEqual($message.length, 1, @@ -289,12 +289,12 @@ QUnit.test('moderator: accept pending moderation message', function (assert) { return this._super.apply(this, arguments); }, }) - .then(function (discuss) { + .then(async function (discuss) { // 1. go to moderation box var $moderationBox = discuss.$( '.o_mail_discuss_item' + '[data-thread-id="mailbox_moderation"]'); - testUtils.dom.click($moderationBox); + await testUtils.dom.click($moderationBox); // check there is a message to moderate var $message = discuss.$('.o_thread_message'); assert.strictEqual($message.length, 1, @@ -304,7 +304,7 @@ QUnit.test('moderator: accept pending moderation message', function (assert) { assert.strictEqual($message.find('.moderation_checkbox').length, 1, "the message should have a moderation checkbox"); // accept the message pending moderation - testUtils.dom.click(discuss.$('.o_thread_message_moderation[data-decision="accept"]')); + await testUtils.dom.click(discuss.$('.o_thread_message_moderation[data-decision="accept"]')); assert.verifySteps(['moderate']); // stop the fadeout animation and immediately remove the element @@ -313,7 +313,7 @@ QUnit.test('moderator: accept pending moderation message', function (assert) { "should now have no message displayed in moderation box"); // 2. go to channel 'general' - testUtils.dom.click(discuss.$('.o_mail_discuss_item[data-thread-id="1"]')); + await testUtils.dom.click(discuss.$('.o_mail_discuss_item[data-thread-id="1"]')); $message = discuss.$('.o_thread_message'); // check message is there and has no moderate checkbox assert.strictEqual($message.length, 1, @@ -379,12 +379,12 @@ QUnit.test('moderator: reject pending moderation message (reject with explanatio return this._super.apply(this, arguments); }, }) - .then(function (discuss) { + .then(async function (discuss) { // 1. go to moderation box var $moderationBox = discuss.$( '.o_mail_discuss_item' + '[data-thread-id="mailbox_moderation"]'); - testUtils.dom.click($moderationBox); + await testUtils.dom.click($moderationBox); // check there is a message to moderate var $message = discuss.$('.o_thread_message'); assert.strictEqual($message.length, 1, @@ -394,7 +394,7 @@ QUnit.test('moderator: reject pending moderation message (reject with explanatio assert.strictEqual($message.find('.moderation_checkbox').length, 1, "the message should have a moderation checkbox"); // reject the message pending moderation - testUtils.dom.click(discuss.$('.o_thread_message_moderation[data-decision="reject"]')); + await testUtils.dom.click(discuss.$('.o_thread_message_moderation[data-decision="reject"]')); // check reject dialog prompt assert.strictEqual($('.modal-dialog').length, 1, @@ -419,7 +419,7 @@ QUnit.test('moderator: reject pending moderation message (reject with explanatio "should have a send button on the reject dialog"); // send mesage - testUtils.dom.click($('.modal-footer button')); + await testUtils.dom.click($('.modal-footer button')); assert.verifySteps(['moderate']); // // stop the fadeout animation and immediately remove the element @@ -428,7 +428,7 @@ QUnit.test('moderator: reject pending moderation message (reject with explanatio "should now have no message displayed in moderation box"); // 2. go to channel 'general' - testUtils.dom.click(discuss.$('.o_mail_discuss_item[data-thread-id="1"]')); + await testUtils.dom.click(discuss.$('.o_mail_discuss_item[data-thread-id="1"]')); assert.containsNone(discuss, '.o_thread_message') discuss.destroy(); @@ -482,12 +482,12 @@ QUnit.test('moderator: discard pending moderation message (reject without explan return this._super.apply(this, arguments); }, }) - .then(function (discuss) { + .then(async function (discuss) { // 1. go to moderation box var $moderationBox = discuss.$( '.o_mail_discuss_item' + '[data-thread-id="mailbox_moderation"]'); - testUtils.dom.click($moderationBox); + await testUtils.dom.click($moderationBox); // check there is a message to moderate var $message = discuss.$('.o_thread_message'); assert.strictEqual($message.length, 1, @@ -497,7 +497,7 @@ QUnit.test('moderator: discard pending moderation message (reject without explan assert.strictEqual($message.find('.moderation_checkbox').length, 1, "the message should have a moderation checkbox"); // discard the message pending moderation - testUtils.dom.click(discuss.$('.o_thread_message_moderation[data-decision="discard"]')); + await testUtils.dom.click(discuss.$('.o_thread_message_moderation[data-decision="discard"]')); // check discard dialog prompt assert.strictEqual($('.modal-dialog').length, 1, @@ -513,7 +513,7 @@ QUnit.test('moderator: discard pending moderation message (reject without explan "should have a cancel button in the dialog for discard"); // discard mesage - testUtils.dom.click($('.modal-footer button.btn-primary')); + await testUtils.dom.click($('.modal-footer button.btn-primary')); assert.verifySteps(['moderate']); // stop the fadeout animation and immediately remove the element @@ -522,7 +522,7 @@ QUnit.test('moderator: discard pending moderation message (reject without explan "should now have no message displayed in moderation box"); // 2. go to channel 'general' - testUtils.dom.click(discuss.$('.o_mail_discuss_item[data-thread-id="1"]')); + await testUtils.dom.click(discuss.$('.o_mail_discuss_item[data-thread-id="1"]')); assert.containsNone(discuss, '.o_thread_message') discuss.destroy(); @@ -534,7 +534,7 @@ QUnit.test('author: send message in moderated channel', function (assert) { assert.expect(4); var done = assert.async(); - var messagePostDef = $.Deferred(); + var messagePostDef = testUtils.makeTestPromise(); this.data.initMessaging = { channel_slots: { @@ -554,7 +554,7 @@ QUnit.test('author: send message in moderated channel', function (assert) { params: {}, data: this.data, services: this.services, - mockRPC: function (route, args) { + mockRPC: async function (route, args) { if (args.method === 'message_post') { var message = { id: 100, @@ -572,9 +572,10 @@ QUnit.test('author: send message in moderated channel', function (assert) { }; var notification = [metaData, notifData]; objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]); + await testUtils.nextTick(); messagePostDef.resolve(); - return $.when(message.id); + return Promise.resolve(message.id); } return this._super.apply(this, arguments); }, @@ -582,14 +583,14 @@ QUnit.test('author: send message in moderated channel', function (assert) { partner_id: 2, }, }) - .then(function (discuss) { + .then(async function (discuss) { objectDiscuss = discuss; // go to channel 'general' - testUtils.dom.click(discuss.$('.o_mail_discuss_item[data-thread-id="1"]')); + await testUtils.dom.click(discuss.$('.o_mail_discuss_item[data-thread-id="1"]')); // post a message discuss.$('.o_composer_input textarea').first().val("some text"); - testUtils.dom.click(discuss.$('.o_composer_send button')); + await testUtils.dom.click(discuss.$('.o_composer_send button')); messagePostDef .then(function () { @@ -646,10 +647,10 @@ QUnit.test('author: sent message accepted in moderated channel', function (asser partner_id: 2, }, }) - .then(function (discuss) { + .then(async function (discuss) { // go to channel 'general' - testUtils.dom.click(discuss.$('.o_mail_discuss_item[data-thread-id="1"]')); + await testUtils.dom.click(discuss.$('.o_mail_discuss_item[data-thread-id="1"]')); // check message is pending var $message = discuss.$('.o_thread_message'); assert.strictEqual($message.length, 1, @@ -674,7 +675,7 @@ QUnit.test('author: sent message accepted in moderated channel', function (asser var metaData = [dbName, 'mail.channel']; var notification = [metaData, messageData]; discuss.call('bus_service', 'trigger', 'notification', [notification]); - + await testUtils.nextTick(); // check message is accepted $message = discuss.$('.o_thread_message'); assert.strictEqual($message.length, 1, @@ -727,10 +728,10 @@ QUnit.test('author: sent message rejected in moderated channel', function (asser partner_id: 2, }, }) - .then(function (discuss) { + .then(async function (discuss) { // go to channel 'general' - testUtils.dom.click(discuss.$('.o_mail_discuss_item[data-thread-id="1"]')); + await testUtils.dom.click(discuss.$('.o_mail_discuss_item[data-thread-id="1"]')); // check message is pending var $message = discuss.$('.o_thread_message'); assert.strictEqual($message.length, 1, @@ -751,7 +752,7 @@ QUnit.test('author: sent message rejected in moderated channel', function (asser var metaData = [dbName, 'res.partner']; var notification = [metaData, notifData]; discuss.call('bus_service', 'trigger', 'notification', [notification]); - + await testUtils.nextTick(); // // check no message assert.containsNone(discuss, '.o_thread_message', "message should be removed from channel after reject"); @@ -761,12 +762,11 @@ QUnit.test('author: sent message rejected in moderated channel', function (asser }); }); -QUnit.test('no crash when load-more fetching "accepted" message twice', function (assert) { +QUnit.test('no crash when load-more fetching "accepted" message twice', async function (assert) { // This tests requires discuss not loading more messages due to having less // messages to fetch than available height. This justifies we simply do not // patch FETCH_LIMIT to 1, as it would detect that more messages could fit // the empty space (it behaviour is linked to "auto load more"). - var done = assert.async(); assert.expect(2); var FETCH_LIMIT = Thread.prototype._FETCH_LIMIT; @@ -799,7 +799,7 @@ QUnit.test('no crash when load-more fetching "accepted" message twice', function }; var count = 0; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -811,33 +811,32 @@ QUnit.test('no crash when load-more fetching "accepted" message twice', function count++; if (count === 1) { // inbox message_fetch - return $.when([]); + return Promise.resolve([]); } // general message_fetch - return $.when(messageData); + return Promise.resolve(messageData); } return this._super.apply(this, arguments); }, - }) - .then(function (discuss) { - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.strictEqual($general.length, 1, - "should have the channel item with id 1"); - assert.strictEqual($general.attr('title'), 'general', - "should have the title 'general'"); - - // click on general - $general.click(); - - // simulate search - discuss.trigger_up('search', { - domains: [['author_id', '=', 100]], - }); - - discuss.destroy(); - done(); }); + + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.strictEqual($general.length, 1, + "should have the channel item with id 1"); + assert.strictEqual($general.attr('title'), 'general', + "should have the title 'general'"); + + // click on general + await testUtils.dom.click($general); + + // simulate search + discuss.trigger_up('search', { + domains: [['author_id', '=', 100]], + }); + await testUtils.nextTick(); + + discuss.destroy(); }); }); diff --git a/addons/mail/static/tests/discuss_seen_indicator_tests.js b/addons/mail/static/tests/discuss_seen_indicator_tests.js index 4db61a56ed1..46f5e0b5723 100644 --- a/addons/mail/static/tests/discuss_seen_indicator_tests.js +++ b/addons/mail/static/tests/discuss_seen_indicator_tests.js @@ -72,13 +72,13 @@ QUnit.module('Discuss (Seen Indicator)', { }; var notification = [[false, 'mail.channel', params.channelID], data]; params.widget.call('bus_service', 'trigger', 'notification', [notification]); + return testUtils.nextTick(); }; }, }); -QUnit.test('2 members: 1 myself message not received initially', function (assert) { +QUnit.test('2 members: 1 myself message not received initially', async function (assert) { assert.expect(2); - var done = assert.async(); var dmChat = { id: 1, @@ -123,30 +123,27 @@ QUnit.test('2 members: 1 myself message not received initially', function (asser res_id: 1, }]; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, session: { partner_id: 3 }, - }).then(function (discuss) { - // click on DM chat - var $dmChat = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($dmChat); - - assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); - - discuss.destroy(); - done(); }); + // click on general + var $dmChat = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + await testUtils.dom.click($dmChat); + + assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); + + discuss.destroy(); }); -QUnit.test('2 members: 1 myself message received by everyone initially', function (assert) { +QUnit.test('2 members: 1 myself message received by everyone initially', async function (assert) { assert.expect(4); - var done = assert.async(); var dmChat = { id: 1, @@ -191,35 +188,32 @@ QUnit.test('2 members: 1 myself message received by everyone initially', functio res_id: 1, }]; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, session: { partner_id: 3 }, - }).then(function (discuss) { - // click on DM chat - var $dmChat = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($dmChat); - var $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); - assert.isVisible($seenIcon, "should display seen icon on message"); - assert.containsOnce($seenIcon, '.fa-check', "should display a single check"); - - testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); - var $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); - assert.isVisible($seenIconContent, "should display content on seen icon mouseover"); - assert.strictEqual($seenIconContent.text().trim(), "Received by Everyone"); - - discuss.destroy(); - done(); }); + // click on DM chat + var $dmChat = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + await testUtils.dom.click($dmChat); + var $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); + assert.isVisible($seenIcon, "should display seen icon on message"); + assert.containsOnce($seenIcon, '.fa-check', "should display a single check"); + + await testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); + var $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); + assert.isVisible($seenIconContent, "should display content on seen icon mouseover"); + assert.strictEqual($seenIconContent.text().trim(), "Received by Everyone"); + + discuss.destroy(); }); -QUnit.test('2 members: 1 myself message seen by everyone initially', function (assert) { +QUnit.test('2 members: 1 myself message seen by everyone initially', async function (assert) { assert.expect(4); - var done = assert.async(); var dmChat = { id: 1, @@ -264,37 +258,33 @@ QUnit.test('2 members: 1 myself message seen by everyone initially', function (a res_id: 1, }]; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, session: { partner_id: 3 }, - }).then(function (discuss) { - // click on DM chat - var $dmChat = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($dmChat); - var $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); - assert.isVisible($seenIcon, "should display seen icon on message"); - assert.containsN($seenIcon, '.fa-check', 2, "should display 2 checks"); - - testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); - var $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); - assert.isVisible($seenIconContent, "should display content on seen icon mouseover"); - assert.strictEqual($seenIconContent.text().trim(), "Seen by Everyone"); - - discuss.destroy(); - done(); }); + // click on DM chat + var $dmChat = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + await testUtils.dom.click($dmChat); + var $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); + assert.isVisible($seenIcon, "should display seen icon on message"); + assert.containsN($seenIcon, '.fa-check', 2, "should display 2 checks"); + + await testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); + var $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); + assert.isVisible($seenIconContent, "should display content on seen icon mouseover"); + assert.strictEqual($seenIconContent.text().trim(), "Seen by Everyone"); + + discuss.destroy(); }); -QUnit.test('2 members: 1 myself message received by everyone (initially not received)', function (assert) { +QUnit.test('2 members: 1 myself message received by everyone (initially not received)', async function (assert) { assert.expect(6); - var done = assert.async(); - var self = this; var dmChat = { id: 1, channel_type: "chat", @@ -338,49 +328,45 @@ QUnit.test('2 members: 1 myself message received by everyone (initially not rece res_id: 1, }]; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, session: { partner_id: 3 }, - }).then(function (discuss) { - // click on DM chat - var $dmChat = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($dmChat); - - assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); - - self.simulate({ - action: 'received', - channelID: 1, - partnerID: 2, - lastMessageID: 100, - widget: discuss, - }); - - var $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); - assert.isVisible($seenIcon, "should display seen icon on message"); - assert.containsOnce($seenIcon, '.fa-check', "should display a single check"); - - testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); - var $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); - assert.isVisible($seenIconContent, "should display content on seen icon mouseover"); - assert.strictEqual($seenIconContent.text().trim(), "Received by Everyone"); - - discuss.destroy(); - done(); }); + // click on DM chat + var $dmChat = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + await testUtils.dom.click($dmChat); + + assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); + + await this.simulate({ + action: 'received', + channelID: 1, + partnerID: 2, + lastMessageID: 100, + widget: discuss, + }); + + var $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); + assert.isVisible($seenIcon, "should display seen icon on message"); + assert.containsOnce($seenIcon, '.fa-check', "should display a single check"); + + await testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); + var $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); + assert.isVisible($seenIconContent, "should display content on seen icon mouseover"); + assert.strictEqual($seenIconContent.text().trim(), "Received by Everyone"); + + discuss.destroy(); }); -QUnit.test('2 members: 1 myself message seen by everyone (initially not received)', function (assert) { +QUnit.test('2 members: 1 myself message seen by everyone (initially not received)', async function (assert) { assert.expect(6); - var done = assert.async(); - var self = this; var dmChat = { id: 1, channel_type: "chat", @@ -424,49 +410,45 @@ QUnit.test('2 members: 1 myself message seen by everyone (initially not received res_id: 1, }]; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, session: { partner_id: 3 }, - }).then(function (discuss) { - // click on DM chat - var $dmChat = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($dmChat); - - assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); - - self.simulate({ - action: 'seen', - channelID: 1, - partnerID: 2, - lastMessageID: 100, - widget: discuss, - }); - - var $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); - assert.isVisible($seenIcon, "should display seen icon on message"); - assert.containsN($seenIcon, '.fa-check', 2, "should display 2 checks"); - - testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); - var $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); - assert.isVisible($seenIconContent, "should display content on seen icon mouseover"); - assert.strictEqual($seenIconContent.text().trim(), "Seen by Everyone"); - - discuss.destroy(); - done(); }); + // click on DM chat + var $dmChat = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + await testUtils.dom.click($dmChat); + + assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); + + await this.simulate({ + action: 'seen', + channelID: 1, + partnerID: 2, + lastMessageID: 100, + widget: discuss, + }); + + var $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); + assert.isVisible($seenIcon, "should display seen icon on message"); + assert.containsN($seenIcon, '.fa-check', 2, "should display 2 checks"); + + await testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); + var $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); + assert.isVisible($seenIconContent, "should display content on seen icon mouseover"); + assert.strictEqual($seenIconContent.text().trim(), "Seen by Everyone"); + + discuss.destroy(); }); -QUnit.test('2 members: 1 other message seen by me (initially not received)', function (assert) { +QUnit.test('2 members: 1 other message seen by me (initially not received)', async function (assert) { assert.expect(3); - var done = assert.async(); - var self = this; var dmChat = { id: 1, channel_type: "chat", @@ -510,44 +492,41 @@ QUnit.test('2 members: 1 other message seen by me (initially not received)', fun res_id: 1, }]; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, session: { partner_id: 3 }, - }).then(function (discuss) { - // click on DM chat - var $dmChat = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($dmChat); - - assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); - - self.simulate({ - action: 'seen', - channelID: 1, - partnerID: 3, - lastMessageID: 100, - widget: discuss, - }); - - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); - - discuss.destroy(); - done(); }); + // click on DM chat + var $dmChat = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + await testUtils.dom.click($dmChat); + + assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); + + await this.simulate({ + action: 'seen', + channelID: 1, + partnerID: 3, + lastMessageID: 100, + widget: discuss, + }); + + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); + + discuss.destroy(); }); -QUnit.skip('3 members: 1 myself message received by some initially', function (assert) { +QUnit.skip('3 members: 1 myself message received by some initially', async function (assert) { // IMPORTANT: This test make sense if this feature is on multi user channels, // which was the case in case at some point. It has been disabled for // performance reasons. We are unsure if this will come back in the future, // so we keep to tests in 'skip' mode in the mean time. assert.expect(4); - var done = assert.async(); var members = [{ id: 2, @@ -601,7 +580,7 @@ QUnit.skip('3 members: 1 myself message received by some initially', function (a res_id: 1, }]; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -610,31 +589,29 @@ QUnit.skip('3 members: 1 myself message received by some initially', function (a session: { partner_id: 3 }, mockRPC: function (route, args) { if (args.method === 'channel_fetch_listeners') { - return $.when(members); + return Promise.resolve(members); } return this._super.apply(this, arguments); }, - }).then(function (discuss) { - // click on general - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($general); - var $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); - assert.isVisible($seenIcon, "should display seen icon on message"); - assert.containsOnce($seenIcon, '.fa-check', "should display a single check"); - - testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); - var $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); - assert.isVisible($seenIconContent, "should display content on seen icon mouseover"); - assert.strictEqual($seenIconContent.text().replace(/\s/g, ""), - "Receivedby:Someone(someone@example.com)"); - - discuss.destroy(); - done(); }); + // click on general + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + await testUtils.dom.click($general); + var $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); + assert.isVisible($seenIcon, "should display seen icon on message"); + assert.containsOnce($seenIcon, '.fa-check', "should display a single check"); + + await testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); + var $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); + assert.isVisible($seenIconContent, "should display content on seen icon mouseover"); + assert.strictEqual($seenIconContent.text().replace(/\s/g, ""), + "Receivedby:Someone(someone@example.com)"); + + discuss.destroy(); }); -QUnit.skip('several members: 1 myself message seen by everyone (initially not received)', function (assert) { +QUnit.skip('several members: 1 myself message seen by everyone (initially not received)', async function (assert) { // IMPORTANT: This test make sense if this feature is on multi user channels, // which was the case in case at some point. It has been disabled for // performance reasons. We are unsure if this will come back in the future, @@ -643,9 +620,7 @@ QUnit.skip('several members: 1 myself message seen by everyone (initially not re // seen icon and content should change for my messages based on other users // actions (received and/or seen the message) assert.expect(26); - var done = assert.async(); - var self = this; var members = [{ id: 2, name: "Someone", @@ -706,7 +681,7 @@ QUnit.skip('several members: 1 myself message seen by everyone (initially not re res_id: 1, }]; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -715,133 +690,131 @@ QUnit.skip('several members: 1 myself message seen by everyone (initially not re session: { partner_id: 3 }, mockRPC: function (route, args) { if (args.method === 'channel_fetch_listeners') { - return $.when(members); + return Promise.resolve(members); } return this._super.apply(this, arguments); }, - }).then(function (discuss) { - // click on general - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($general); - - assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); - - self.simulate({ - action: 'received', - channelID: 1, - partnerID: 2, - lastMessageID: 100, - widget: discuss, - }); - - var $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); - assert.isVisible($seenIcon, "should display seen icon on message"); - assert.containsOnce($seenIcon, '.fa-check', "should display a single check"); - - testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); - var $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); - assert.isVisible($seenIconContent, "should display content on seen icon mouseover"); - assert.strictEqual($seenIconContent.text().replace(/\s/g, ""), - "Receivedby:Someone(someone@example.com)"); - - self.simulate({ - action: 'received', - channelID: 1, - partnerID: 4, - lastMessageID: 100, - widget: discuss, - }); - - $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); - assert.isVisible($seenIcon, "should still display seen icon on message"); - assert.containsOnce($seenIcon, '.fa-check', "should still display a single check"); - - testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); - $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); - assert.isVisible($seenIconContent, "should still display content on seen icon mouseover"); - assert.strictEqual($seenIconContent.text().replace(/\s/g, ""), - "Receivedby:Someone(someone@example.com)Other(other@example.com)"); - - self.simulate({ - action: 'seen', - channelID: 1, - partnerID: 2, - lastMessageID: 100, - widget: discuss, - }); - - $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); - assert.isVisible($seenIcon, "should still display seen icon on message"); - assert.containsN($seenIcon, '.fa-check', 2, "should now display 2 checks"); - - testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); - $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); - assert.isVisible($seenIconContent, "should still display content on seen icon mouseover"); - assert.strictEqual($seenIconContent.text().replace(/\s/g, ""), - "Seenby:Someone(someone@example.com)Receivedby:Other(other@example.com)"); - - self.simulate({ - action: 'seen', - channelID: 1, - partnerID: 4, - lastMessageID: 100, - widget: discuss, - }); - - $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); - assert.isVisible($seenIcon, "should still display seen icon on message"); - assert.containsN($seenIcon, '.fa-check', 2, "should still display 2 checks"); - - testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); - $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); - assert.isVisible($seenIconContent, "should still display content on seen icon mouseover"); - assert.strictEqual($seenIconContent.text().replace(/\s/g, ""), - "Seenby:Someone(someone@example.com)Other(other@example.com)"); - - self.simulate({ - action: 'received', - channelID: 1, - partnerID: 5, - lastMessageID: 100, - widget: discuss, - }); - - $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); - assert.isVisible($seenIcon, "should still display seen icon on message"); - assert.containsN($seenIcon, '.fa-check', 2, "should still display 2 checks"); - - testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); - $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); - assert.isVisible($seenIconContent, "should still display content on seen icon mouseover"); - assert.strictEqual($seenIconContent.text().replace(/\s/g, ""), - "Seenby:Someone(someone@example.com)Other(other@example.com)ReceivedbyEveryone"); - - self.simulate({ - action: 'seen', - channelID: 1, - partnerID: 5, - lastMessageID: 100, - widget: discuss, - }); - - $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); - assert.isVisible($seenIcon, "should still display seen icon on message"); - assert.containsN($seenIcon, '.fa-check', 2, "should still display 2 checks"); - - testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); - $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); - assert.isVisible($seenIconContent, "should still display content on seen icon mouseover"); - assert.strictEqual($seenIconContent.text().trim(), - "Seen by Everyone"); - - discuss.destroy(); - done(); }); + // click on general + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + await testUtils.dom.click($general); + + assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); + + await this.simulate({ + action: 'received', + channelID: 1, + partnerID: 2, + lastMessageID: 100, + widget: discuss, + }); + + var $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); + assert.isVisible($seenIcon, "should display seen icon on message"); + assert.containsOnce($seenIcon, '.fa-check', "should display a single check"); + + await testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); + var $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); + assert.isVisible($seenIconContent, "should display content on seen icon mouseover"); + assert.strictEqual($seenIconContent.text().replace(/\s/g, ""), + "Receivedby:Someone(someone@example.com)"); + + await this.simulate({ + action: 'received', + channelID: 1, + partnerID: 4, + lastMessageID: 100, + widget: discuss, + }); + + $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); + assert.isVisible($seenIcon, "should still display seen icon on message"); + assert.containsOnce($seenIcon, '.fa-check', "should still display a single check"); + + await testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); + $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); + assert.isVisible($seenIconContent, "should still display content on seen icon mouseover"); + assert.strictEqual($seenIconContent.text().replace(/\s/g, ""), + "Receivedby:Someone(someone@example.com)Other(other@example.com)"); + + await this.simulate({ + action: 'seen', + channelID: 1, + partnerID: 2, + lastMessageID: 100, + widget: discuss, + }); + + $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); + assert.isVisible($seenIcon, "should still display seen icon on message"); + assert.containsN($seenIcon, '.fa-check', 2, "should now display 2 checks"); + + await testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); + $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); + assert.isVisible($seenIconContent, "should still display content on seen icon mouseover"); + assert.strictEqual($seenIconContent.text().replace(/\s/g, ""), + "Seenby:Someone(someone@example.com)Receivedby:Other(other@example.com)"); + + await this.simulate({ + action: 'seen', + channelID: 1, + partnerID: 4, + lastMessageID: 100, + widget: discuss, + }); + + $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); + assert.isVisible($seenIcon, "should still display seen icon on message"); + assert.containsN($seenIcon, '.fa-check', 2, "should still display 2 checks"); + + await testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); + $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); + assert.isVisible($seenIconContent, "should still display content on seen icon mouseover"); + assert.strictEqual($seenIconContent.text().replace(/\s/g, ""), + "Seenby:Someone(someone@example.com)Other(other@example.com)"); + + await this.simulate({ + action: 'received', + channelID: 1, + partnerID: 5, + lastMessageID: 100, + widget: discuss, + }); + + $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); + assert.isVisible($seenIcon, "should still display seen icon on message"); + assert.containsN($seenIcon, '.fa-check', 2, "should still display 2 checks"); + + await testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); + $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); + assert.isVisible($seenIconContent, "should still display content on seen icon mouseover"); + assert.strictEqual($seenIconContent.text().replace(/\s/g, ""), + "Seenby:Someone(someone@example.com)Other(other@example.com)ReceivedbyEveryone"); + + await this.simulate({ + action: 'seen', + channelID: 1, + partnerID: 5, + lastMessageID: 100, + widget: discuss, + }); + + $seenIcon = discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'); + assert.isVisible($seenIcon, "should still display seen icon on message"); + assert.containsN($seenIcon, '.fa-check', 2, "should still display 2 checks"); + + await testUtils.dom.triggerMouseEvent($seenIcon, 'mouseover'); + $seenIconContent = $('.o_mail_thread_message_seen_icon_content'); + assert.isVisible($seenIconContent, "should still display content on seen icon mouseover"); + assert.strictEqual($seenIconContent.text().trim(), + "Seen by Everyone"); + + discuss.destroy(); }); -QUnit.skip('several members: other message seen by everyone (initially not received)', function (assert) { +QUnit.skip('several members: other message seen by everyone (initially not received)', async function (assert) { // IMPORTANT: This test make sense if this feature is on multi user channels, // which was the case in case at some point. It has been disabled for // performance reasons. We are unsure if this will come back in the future, @@ -849,9 +822,7 @@ QUnit.skip('several members: other message seen by everyone (initially not recei // seen icon should never be visible for message of others assert.expect(9); - var done = assert.async(); - var self = this; var members = [{ id: 2, name: "Someone", @@ -912,7 +883,7 @@ QUnit.skip('several members: other message seen by everyone (initially not recei res_id: 1, }]; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -921,103 +892,99 @@ QUnit.skip('several members: other message seen by everyone (initially not recei session: { partner_id: 3 }, mockRPC: function (route, args) { if (args.method === 'channel_fetch_listeners') { - return $.when(members); + return Promise.resolve(members); } return this._super.apply(this, arguments); }, - }).then(function (discuss) { - // click on general - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($general); - - assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); - - self.simulate({ - action: 'received', - channelID: 1, - partnerID: 3, - lastMessageID: 100, - widget: discuss, - }); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'), - "should not display seen icon on message of other users"); - - self.simulate({ - action: 'seen', - channelID: 1, - partnerID: 3, - lastMessageID: 100, - widget: discuss, - }); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'), - "should not display seen icon on message of other users"); - - self.simulate({ - action: 'received', - channelID: 1, - partnerID: 4, - lastMessageID: 100, - widget: discuss, - }); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'), - "should not display seen icon on message of other users"); - - self.simulate({ - action: 'seen', - channelID: 1, - partnerID: 2, - lastMessageID: 100, - widget: discuss, - }); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'), - "should not display seen icon on message of other users"); - - self.simulate({ - action: 'seen', - channelID: 1, - partnerID: 4, - lastMessageID: 100, - widget: discuss, - }); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'), - "should not display seen icon on message of other users"); - - self.simulate({ - action: 'received', - channelID: 1, - partnerID: 5, - lastMessageID: 100, - widget: discuss, - }); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'), - "should not display seen icon on message of other users"); - - self.simulate({ - action: 'seen', - channelID: 1, - partnerID: 5, - lastMessageID: 100, - widget: discuss, - }); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'), - "should not display seen icon on message of other users"); - - discuss.destroy(); - done(); }); + // click on general + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + await testUtils.dom.click($general); + + assert.isVisible(discuss.$('.o_thread_message[data-message-id=100]')); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); + + await this.simulate({ + action: 'received', + channelID: 1, + partnerID: 3, + lastMessageID: 100, + widget: discuss, + }); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'), + "should not display seen icon on message of other users"); + + await this.simulate({ + action: 'seen', + channelID: 1, + partnerID: 3, + lastMessageID: 100, + widget: discuss, + }); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'), + "should not display seen icon on message of other users"); + + await this.simulate({ + action: 'received', + channelID: 1, + partnerID: 4, + lastMessageID: 100, + widget: discuss, + }); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'), + "should not display seen icon on message of other users"); + + await this.simulate({ + action: 'seen', + channelID: 1, + partnerID: 2, + lastMessageID: 100, + widget: discuss, + }); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'), + "should not display seen icon on message of other users"); + + await this.simulate({ + action: 'seen', + channelID: 1, + partnerID: 4, + lastMessageID: 100, + widget: discuss, + }); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'), + "should not display seen icon on message of other users"); + + await this.simulate({ + action: 'received', + channelID: 1, + partnerID: 5, + lastMessageID: 100, + widget: discuss, + }); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'), + "should not display seen icon on message of other users"); + + await this.simulate({ + action: 'seen', + channelID: 1, + partnerID: 5, + lastMessageID: 100, + widget: discuss, + }); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]'), + "should not display seen icon on message of other users"); + + discuss.destroy(); }); -QUnit.skip('several members: only show seen icons from last message seen by everyone', function (assert) { +QUnit.skip('several members: only show seen icons from last message seen by everyone', async function (assert) { // IMPORTANT: This test make sense if this feature is on multi user channels, // which was the case in case at some point. It has been disabled for // performance reasons. We are unsure if this will come back in the future, // so we keep to tests in 'skip' mode in the mean time. assert.expect(12); - var done = assert.async(); - var self = this; var members = [{ id: 2, name: "Someone", @@ -1092,7 +1059,7 @@ QUnit.skip('several members: only show seen icons from last message seen by ever res_id: 1, }]; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -1101,55 +1068,53 @@ QUnit.skip('several members: only show seen icons from last message seen by ever session: { partner_id: 3 }, mockRPC: function (route, args) { if (args.method === 'channel_fetch_listeners') { - return $.when(members); + return Promise.resolve(members); } return this._super.apply(this, arguments); }, - }).then(function (discuss) { - // click on general - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($general); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=98]')); - assert.isVisible(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=99]')); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); - - self.simulate({ - action: 'seen', - channelID: 1, - partnerID: 2, - lastMessageID: 100, - widget: discuss, - }); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=98]')); - assert.isVisible(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=99]')); - assert.isVisible(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); - - self.simulate({ - action: 'seen', - channelID: 1, - partnerID: 4, - lastMessageID: 100, - widget: discuss, - }); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=98]')); - assert.isVisible(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=99]')); - assert.isVisible(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); - - self.simulate({ - action: 'seen', - channelID: 1, - partnerID: 5, - lastMessageID: 100, - widget: discuss, - }); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=98]')); - assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=99]')); - assert.isVisible(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); - - discuss.destroy(); - done(); }); + // click on general + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + await testUtils.dom.click($general); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=98]')); + assert.isVisible(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=99]')); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); + + await this.simulate({ + action: 'seen', + channelID: 1, + partnerID: 2, + lastMessageID: 100, + widget: discuss, + }); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=98]')); + assert.isVisible(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=99]')); + assert.isVisible(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); + + await this.simulate({ + action: 'seen', + channelID: 1, + partnerID: 4, + lastMessageID: 100, + widget: discuss, + }); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=98]')); + assert.isVisible(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=99]')); + assert.isVisible(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); + + await this.simulate({ + action: 'seen', + channelID: 1, + partnerID: 5, + lastMessageID: 100, + widget: discuss, + }); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=98]')); + assert.containsNone(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=99]')); + assert.isVisible(discuss.$('.o_mail_thread_message_seen_icon[data-message-id=100]')); + + discuss.destroy(); }); }); diff --git a/addons/mail/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js index 02cb977b246..7dc8be4320c 100644 --- a/addons/mail/static/tests/discuss_tests.js +++ b/addons/mail/static/tests/discuss_tests.js @@ -83,73 +83,66 @@ QUnit.module('Discuss', { } }); -QUnit.test('basic rendering', function (assert) { +QUnit.test('basic rendering', async function (assert) { assert.expect(5); - var done = assert.async(); - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, - }) - .then(function (discuss) { - // test basic rendering - var $sidebar = discuss.$('.o_mail_discuss_sidebar'); - assert.strictEqual($sidebar.length, 1, - "should have rendered a sidebar"); - - assert.containsOnce(discuss, '.o_mail_discuss_content', - "should have rendered the content"); - assert.containsOnce(discuss, '.o_mail_no_content', - "should display no content message"); - - var $inbox = $sidebar.find('.o_mail_discuss_item[data-thread-id=mailbox_inbox]'); - assert.strictEqual($inbox.length, 1, - "should have the channel item 'mailbox_inbox' in the sidebar"); - - var $starred = $sidebar.find('.o_mail_discuss_item[data-thread-id=mailbox_starred]'); - assert.strictEqual($starred.length, 1, - "should have the channel item 'mailbox_starred' in the sidebar"); - discuss.destroy(); - done(); }); + + // test basic rendering + var $sidebar = discuss.$('.o_mail_discuss_sidebar'); + assert.strictEqual($sidebar.length, 1, + "should have rendered a sidebar"); + + assert.containsOnce(discuss, '.o_mail_discuss_content', + "should have rendered the content"); + assert.containsOnce(discuss, '.o_mail_no_content', + "should display no content message"); + + var $inbox = $sidebar.find('.o_mail_discuss_item[data-thread-id=mailbox_inbox]'); + assert.strictEqual($inbox.length, 1, + "should have the channel item 'mailbox_inbox' in the sidebar"); + + var $starred = $sidebar.find('.o_mail_discuss_item[data-thread-id=mailbox_starred]'); + assert.strictEqual($starred.length, 1, + "should have the channel item 'mailbox_starred' in the sidebar"); + discuss.destroy(); }); -QUnit.test('searchview options visibility', function (assert) { +QUnit.test('searchview options visibility', async function (assert) { assert.expect(5); - var done = assert.async(); - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, - }) - .then(function (discuss) { - assert.containsOnce(discuss, '.o_control_panel .o_search_options', - "should have search options"); - assert.hasClass(discuss.$('.o_control_panel .o_searchview_more.fa'), 'fa-search-minus', - "should have a button to toggle search options"); - - assert.isVisible(discuss.$('.o_control_panel .o_search_options'), - "search options should be visible by default"); - testUtils.dom.click(discuss.$('.o_control_panel .o_searchview_more.fa')); - assert.hasClass(discuss.$('.o_control_panel .o_searchview_more.fa'), 'fa-search-plus', - "should have a button to toggle search options"); - assert.isNotVisible(discuss.$('.o_control_panel .o_search_options'), - "search options should be hidden after clicking on search option toggler"); - - discuss.destroy(); - done(); }); + + assert.containsOnce(discuss, '.o_control_panel .o_search_options', + "should have search options"); + assert.hasClass(discuss.$('.o_control_panel .o_searchview_more.fa'), 'fa-search-minus', + "should have a button to toggle search options"); + + assert.isVisible(discuss.$('.o_control_panel .o_search_options'), + "search options should be visible by default"); + await testUtils.dom.click(discuss.$('.o_control_panel .o_searchview_more.fa')); + assert.hasClass(discuss.$('.o_control_panel .o_searchview_more.fa'), 'fa-search-plus', + "should have a button to toggle search options"); + assert.isNotVisible(discuss.$('.o_control_panel .o_search_options'), + "search options should be hidden after clicking on search option toggler"); + + discuss.destroy(); }); -QUnit.test('searchview filter messages', function (assert) { +QUnit.test('searchview filter messages', async function (assert) { assert.expect(10); - var done = assert.async(); this.data['mail.message'].records = [{ author_id: [5, 'Demo User'], @@ -165,7 +158,7 @@ QUnit.test('searchview filter messages', function (assert) { needaction_partner_ids: [3], }]; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -180,8 +173,7 @@ QUnit.test('searchview filter messages', function (assert) { '' + '', }, - }) - .then(function (discuss) { + }); assert.containsN(discuss, '.o_thread_message', 2, "there should be two messages in the inbox mailbox"); assert.strictEqual($('.o_searchview_input').length, 1, @@ -191,8 +183,9 @@ QUnit.test('searchview filter messages', function (assert) { // interact with searchview so that there is only once message $('.o_searchview_input').val("ab").trigger('keyup'); + await testUtils.nextTick(); $('.o_searchview_input').trigger($.Event('keydown', { which: $.ui.keyCode.ENTER })); - + await testUtils.nextTick(); assert.strictEqual($('.o_searchview_facet').length, 1, "the searchview should have a facet"); assert.strictEqual($('.o_facet_values').text().trim(), 'ab', @@ -201,9 +194,11 @@ QUnit.test('searchview filter messages', function (assert) { "there should be a single message after filter"); // interact with search view so that there are no matching messages - testUtils.dom.click($('.o_facet_remove')); + await testUtils.dom.click($('.o_facet_remove')); $('.o_searchview_input').val("abcd").trigger('keyup'); + await testUtils.nextTick(); $('.o_searchview_input').trigger($.Event('keydown', { which: $.ui.keyCode.ENTER })); + await testUtils.nextTick(); assert.strictEqual($('.o_searchview_facet').length, 1, "the searchview should have a facet"); @@ -216,17 +211,15 @@ QUnit.test('searchview filter messages', function (assert) { "should display that there are no matching messages"); discuss.destroy(); - done(); - }); + }); -QUnit.test('unescape channel name in the sidebar', function (assert) { +QUnit.test('unescape channel name in the sidebar', async function (assert) { // When the user creates a channel, the channel's name is escaped, this in // order to prevent XSS attacks. However, the user should see visually the // unescaped name of the channel. For instance, when the user creates a // channel named "R&D", he should see "R&D" and not "R&D". assert.expect(2); - var done = assert.async(); this.data.initMessaging = { channel_slots: { @@ -238,34 +231,31 @@ QUnit.test('unescape channel name in the sidebar', function (assert) { }, }; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, - }) - .then(function (discuss) { - var $sidebar = discuss.$('.o_mail_discuss_sidebar'); - - var $channel = $sidebar.find('.o_mail_discuss_item[data-thread-id=1]'); - assert.strictEqual($channel.length, 1, - "should have the channel item for channel 1 in the sidebar"); - assert.strictEqual($channel.find('.o_thread_name').text().replace(/\s/g, ''), - "#R&D", - "should have unescaped channel name in the sidebar"); - - discuss.destroy(); - done(); }); + + var $sidebar = discuss.$('.o_mail_discuss_sidebar'); + + var $channel = $sidebar.find('.o_mail_discuss_item[data-thread-id=1]'); + assert.strictEqual($channel.length, 1, + "should have the channel item for channel 1 in the sidebar"); + assert.strictEqual($channel.find('.o_thread_name').text().replace(/\s/g, ''), + "#R&D", + "should have unescaped channel name in the sidebar"); + + discuss.destroy(); }); -QUnit.test('@ mention in channel', function (assert) { +QUnit.test('@ mention in channel', async function (assert) { assert.expect(34); - var done = assert.async(); - var fetchListenersDef = $.Deferred(); - var receiveMessageDef = $.Deferred(); + var fetchListenersDef = testUtils.makeTestPromise(); + var receiveMessageDef = testUtils.makeTestPromise(); this.data.initMessaging = { channel_slots: { @@ -276,9 +266,8 @@ QUnit.test('@ mention in channel', function (assert) { }], }, }; - var objectDiscuss; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -287,7 +276,7 @@ QUnit.test('@ mention in channel', function (assert) { mockRPC: function (route, args) { if (args.method === 'channel_fetch_listeners') { fetchListenersDef.resolve(); - return $.when([ + return Promise.resolve([ {id: 1, name: 'Admin'}, {id: 2, name: 'TestUser'}, {id: 3, name: 'DemoUser'} @@ -302,142 +291,133 @@ QUnit.test('@ mention in channel', function (assert) { var notification = [[false, 'mail.channel', 1], data]; objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]); receiveMessageDef.resolve(); - return $.when(42); + return Promise.resolve(42); } return this._super.apply(this, arguments); }, - }) - .then(function (discuss) { - objectDiscuss = discuss; - - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.strictEqual($general.length, 1, - "should have the channel item with id 1"); - assert.hasAttrValue($general, 'title', 'general', - "should have the title 'general'"); - - // click on general - testUtils.dom.click($general); - var $input = discuss.$('textarea.o_composer_text_field').first(); - assert.ok($input.length, "should display a composer input"); - - // Note: focus is needed in order to trigger rpc 'channel_fetch_listeners' - $input.focus(); - $input.val("@"); - $input.trigger('keyup'); - - fetchListenersDef - .then(concurrency.delay.bind(concurrency, 0)) - .then(function () { - assert.containsOnce(discuss, '.dropup.o_composer_mention_dropdown.show', - "dropup menu for partner mentions should be open"); - - var $mentionPropositions = discuss.$('.o_mention_proposition'); - assert.strictEqual($mentionPropositions.length, 3, - "should display 3 partner mention propositions"); - - var $mention1 = $mentionPropositions.first(); - var $mention2 = $mentionPropositions.first().next(); - var $mention3 = $mentionPropositions.first().next().next(); - - // correct 1st mention proposition - assert.hasClass($mention1,'active', - "first partner mention should be active"); - assert.strictEqual($mention1.data('id'), 1, - "first partner mention should link to the correct partner id"); - assert.strictEqual($mention1.find('.o_mention_name').text(), "Admin", - "first partner mention should display the correct partner name"); - // correct 2nd mention proposition - assert.doesNotHaveClass($mention2, 'active', - "second partner mention should not be active"); - assert.strictEqual($mention2.data('id'), 2, - "second partner mention should link to the correct partner id"); - assert.strictEqual($mention2.find('.o_mention_name').text(), "TestUser", - "second partner mention should display the correct partner name"); - // correct 3rd mention proposition - assert.doesNotHaveClass($mention3, 'active', - "third partner mention should not be active"); - assert.strictEqual($mention3.data('id'), 3, - "third partner mention should link to the correct partner id"); - assert.strictEqual($mention3.find('.o_mention_name').text(), "DemoUser", - "third partner mention should display the correct partner name"); - - // check DOWN event - $input.trigger($.Event('keyup', {which: $.ui.keyCode.DOWN})); - assert.doesNotHaveClass($mention1, 'active', - "first partner mention should not be active"); - assert.hasClass($mention2,'active', - "second partner mention should be active"); - assert.doesNotHaveClass($mention3, 'active', - "third partner mention should not be active"); - - // check UP event - $input.trigger($.Event('keyup', {which: $.ui.keyCode.UP})); - assert.hasClass($mention1,'active', - "first partner mention should be active"); - assert.doesNotHaveClass($mention2, 'active', - "second partner mention should not be active"); - assert.doesNotHaveClass($mention3, 'active', - "third partner mention should not be active"); - - // check TAB event (full cycle, hence 3 TABs) - $input.trigger($.Event('keyup', {which: $.ui.keyCode.TAB})); - assert.doesNotHaveClass($mention1, 'active', - "first partner mention should not be active"); - assert.hasClass($mention2,'active', - "second partner mention should be active"); - assert.doesNotHaveClass($mention3, 'active', - "third partner mention should not be active"); - - $input.trigger($.Event('keyup', {which: $.ui.keyCode.TAB})); - assert.doesNotHaveClass($mention1, 'active', - "first partner mention should not be active"); - assert.doesNotHaveClass($mention2, 'active', - "second partner mention should not be active"); - assert.hasClass($mention3,'active', - "third partner mention should be active"); - - $input.trigger($.Event('keyup', {which: $.ui.keyCode.TAB})); - assert.hasClass($mention1,'active', - "first partner mention should be active"); - assert.doesNotHaveClass($mention2, 'active', - "second partner mention should not be active"); - assert.doesNotHaveClass($mention3, 'active', - "third partner mention should not be active"); - -// testUtils.dom.click( equivalent to $mentionPropositions.find('active')); - $input.trigger($.Event('keyup', {which: $.ui.keyCode.ENTER})); - assert.containsNone(discuss, '.o_mention_proposition', - "should not have any partner mention proposition after ENTER"); - assert.strictEqual($input.val().trim() , "@Admin", - "should have the correct mention link in the composer input"); - - // send message - $input.trigger($.Event('keydown', {which: $.ui.keyCode.ENTER})); - - receiveMessageDef - .then(concurrency.delay.bind(concurrency, 0)) - .then(function () { - assert.containsOnce(discuss, '.o_thread_message_content', - "should display one message with some content"); - assert.containsOnce(discuss, '.o_thread_message_content a', - "should contain a link in the message content"); - assert.strictEqual(discuss.$('.o_thread_message_content a').text(), - "@Admin", "should have correct mention link in the message content"); - - discuss.destroy(); - done(); - }); - }); }); + objectDiscuss = discuss; + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.strictEqual($general.length, 1, + "should have the channel item with id 1"); + assert.hasAttrValue($general, 'title', 'general', + "should have the title 'general'"); + + // click on general + await testUtils.dom.click($general); + var $input = discuss.$('textarea.o_composer_text_field').first(); + assert.ok($input.length, "should display a composer input"); + + // Note: focus is needed in order to trigger rpc 'channel_fetch_listeners' + $input.focus(); + $input.val("@"); + $input.trigger('keyup'); + + fetchListenersDef; + await testUtils.nextTick(); + assert.containsOnce(discuss, '.dropup.o_composer_mention_dropdown.show', + "dropup menu for partner mentions should be open"); + + var $mentionPropositions = discuss.$('.o_mention_proposition'); + assert.strictEqual($mentionPropositions.length, 3, + "should display 3 partner mention propositions"); + + var $mention1 = $mentionPropositions.first(); + var $mention2 = $mentionPropositions.first().next(); + var $mention3 = $mentionPropositions.first().next().next(); + + // correct 1st mention proposition + assert.hasClass($mention1,'active', + "first partner mention should be active"); + assert.strictEqual($mention1.data('id'), 1, + "first partner mention should link to the correct partner id"); + assert.strictEqual($mention1.find('.o_mention_name').text(), "Admin", + "first partner mention should display the correct partner name"); + // correct 2nd mention proposition + assert.doesNotHaveClass($mention2, 'active', + "second partner mention should not be active"); + assert.strictEqual($mention2.data('id'), 2, + "second partner mention should link to the correct partner id"); + assert.strictEqual($mention2.find('.o_mention_name').text(), "TestUser", + "second partner mention should display the correct partner name"); + // correct 3rd mention proposition + assert.doesNotHaveClass($mention3, 'active', + "third partner mention should not be active"); + assert.strictEqual($mention3.data('id'), 3, + "third partner mention should link to the correct partner id"); + assert.strictEqual($mention3.find('.o_mention_name').text(), "DemoUser", + "third partner mention should display the correct partner name"); + + // check DOWN event + await testUtils.fields.triggerKey('up', $input, 'down'); + assert.doesNotHaveClass($mention1, 'active', + "first partner mention should not be active"); + assert.hasClass($mention2,'active', + "second partner mention should be active"); + assert.doesNotHaveClass($mention3, 'active', + "third partner mention should not be active"); + + // check UP event + await testUtils.fields.triggerKey('up', $input, 'up'); + assert.hasClass($mention1,'active', + "first partner mention should be active"); + assert.doesNotHaveClass($mention2, 'active', + "second partner mention should not be active"); + assert.doesNotHaveClass($mention3, 'active', + "third partner mention should not be active"); + + // check TAB event (full cycle, hence 3 TABs) + await testUtils.fields.triggerKey('up', $input, 'tab'); + assert.doesNotHaveClass($mention1, 'active', + "first partner mention should not be active"); + assert.hasClass($mention2,'active', + "second partner mention should be active"); + assert.doesNotHaveClass($mention3, 'active', + "third partner mention should not be active"); + + await testUtils.fields.triggerKey('up', $input, 'tab'); + assert.doesNotHaveClass($mention1, 'active', + "first partner mention should not be active"); + assert.doesNotHaveClass($mention2, 'active', + "second partner mention should not be active"); + assert.hasClass($mention3,'active', + "third partner mention should be active"); + + await testUtils.fields.triggerKey('up', $input, 'tab'); + assert.hasClass($mention1,'active', + "first partner mention should be active"); + assert.doesNotHaveClass($mention2, 'active', + "second partner mention should not be active"); + assert.doesNotHaveClass($mention3, 'active', + "third partner mention should not be active"); + + await testUtils.fields.triggerKey('up', $input, 'enter'); + assert.containsNone(discuss, '.o_mention_proposition', + "should not have any partner mention proposition after ENTER"); + assert.strictEqual($input.val().trim() , "@Admin", + "should have the correct mention link in the composer input"); + + // send message + await testUtils.fields.triggerKeydown($input, 'enter'); + + await receiveMessageDef; + await testUtils.nextTick(); + + assert.containsOnce(discuss, '.o_thread_message_content', + "should display one message with some content"); + assert.containsOnce(discuss, '.o_thread_message_content a', + "should contain a link in the message content"); + assert.strictEqual(discuss.$('.o_thread_message_content a').text(), + "@Admin", "should have correct mention link in the message content"); + + discuss.destroy(); }); -QUnit.test('@ mention with special chars', function (assert) { +QUnit.test('@ mention with special chars', async function (assert) { assert.expect(11); - var done = assert.async(); - var fetchListenersDef = $.Deferred(); - var receiveMessageDef = $.Deferred(); + var fetchListenersDef = testUtils.makeTestPromise(); + var receiveMessageDef = testUtils.makeTestPromise(); this.data.initMessaging = { channel_slots: { channel_channel: [{ @@ -448,7 +428,7 @@ QUnit.test('@ mention with special chars', function (assert) { }, }; var objectDiscuss; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -457,7 +437,7 @@ QUnit.test('@ mention with special chars', function (assert) { mockRPC: function (route, args) { if (args.method === 'channel_fetch_listeners') { fetchListenersDef.resolve(); - return $.when([ + return Promise.resolve([ {id: 1, name: '\u0405pëciãlUser<&>"`\' \u30C4'}, ]); } @@ -473,70 +453,64 @@ QUnit.test('@ mention with special chars', function (assert) { var notification = [[false, 'mail.channel', 1], data]; objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]); receiveMessageDef.resolve(); - return $.when(42); + return Promise.resolve(42); } return this._super.apply(this, arguments); }, - }) - .then(function (discuss) { - objectDiscuss = discuss; - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - // click on general - $general.click(); - var $input = discuss.$('textarea.o_composer_text_field').first(); - assert.ok($input.length, "should display a composer input"); - // Note: focus is needed in order to trigger rpc 'channel_fetch_listeners' - $input.focus(); - $input.val("@"); - $input.trigger('keyup'); - fetchListenersDef - .then(concurrency.delay.bind(concurrency, 0)) - .then(function () { - var $mention = discuss.$('.o_mention_proposition'); - // correct mention proposition - assert.ok($mention.hasClass('active'), - "first partner mention should be active"); - assert.strictEqual($mention.data('id'), 1, - "first partner mention should link to the correct partner id"); - assert.strictEqual($mention.find('.o_mention_name').text(), '\u0405pëciãlUser<&>"`\' \u30C4', - "first partner mention should display the correct partner name"); - // equivalent to $mentionPropositions.find('active').click(); - $input.trigger($.Event('keyup', {which: $.ui.keyCode.ENTER})); - assert.strictEqual(discuss.$('.o_mention_proposition').length, 0, - "should not have any partner mention proposition after ENTER"); - assert.strictEqual($input.val().trim() , "@\u0405pëciãlUser<&>\"`'\u00A0\u30C4", - "should have the correct mention link in the composer input"); - // send message - $input.trigger($.Event('keydown', {which: $.ui.keyCode.ENTER})); - receiveMessageDef - .then(concurrency.delay.bind(concurrency, 0)) - .then(function () { - assert.strictEqual(discuss.$('.o_thread_message_content').length, 1, - "should display one message with some content"); - assert.strictEqual(discuss.$('.o_thread_message_content a').length, 1, - "should contain a link in the message content"); - assert.strictEqual(discuss.$('.o_thread_message_content a').text(), - "@\u0405pëciãlUser<&>\"`' \u30C4", - "should have correct mention link in the message content"); - $input.val("@"); - $input.trigger('keyup'); - var $mention = discuss.$('.o_mention_proposition'); - assert.strictEqual($mention.find('.o_mention_name').text(), - '\u0405pëciãlUser<&>"`\' \u30C4', - "first partner mention should still display the correct partner name"); - discuss.destroy(); - done(); - }); - }); }); + objectDiscuss = discuss; + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + // click on general + $general.click(); + await testUtils.nextTick(); + var $input = discuss.$('textarea.o_composer_text_field').first(); + assert.ok($input.length, "should display a composer input"); + // Note: focus is needed in order to trigger rpc 'channel_fetch_listeners' + $input.focus(); + $input.val("@"); + $input.trigger('keyup'); + await fetchListenersDef; + await testUtils.nextTick(); + var $mention = discuss.$('.o_mention_proposition'); + // correct mention proposition + assert.ok($mention.hasClass('active'), + "first partner mention should be active"); + assert.strictEqual($mention.data('id'), 1, + "first partner mention should link to the correct partner id"); + assert.strictEqual($mention.find('.o_mention_name').text(), '\u0405pëciãlUser<&>"`\' \u30C4', + "first partner mention should display the correct partner name"); + // equivalent to $mentionPropositions.find('active').click(); + await testUtils.fields.triggerKey('up', $input, 'enter'); + assert.strictEqual(discuss.$('.o_mention_proposition').length, 0, + "should not have any partner mention proposition after ENTER"); + assert.strictEqual($input.val().trim() , "@\u0405pëciãlUser<&>\"`'\u00A0\u30C4", + "should have the correct mention link in the composer input"); + // send message + await testUtils.fields.triggerKeydown($input, 'enter'); + await receiveMessageDef; + await testUtils.nextTick(); + assert.strictEqual(discuss.$('.o_thread_message_content').length, 1, + "should display one message with some content"); + assert.strictEqual(discuss.$('.o_thread_message_content a').length, 1, + "should contain a link in the message content"); + assert.strictEqual(discuss.$('.o_thread_message_content a').text(), + "@\u0405pëciãlUser<&>\"`' \u30C4", + "should have correct mention link in the message content"); + $input.val("@"); + $input.trigger('keyup'); + await testUtils.nextTick(); + var $mention = discuss.$('.o_mention_proposition'); + assert.strictEqual($mention.find('.o_mention_name').text(), + '\u0405pëciãlUser<&>"`\' \u30C4', + "first partner mention should still display the correct partner name"); + discuss.destroy(); }); -QUnit.test('@ mention in mailing channel', function (assert) { +QUnit.test('@ mention in mailing channel', async function (assert) { assert.expect(8); - var done = assert.async(); - var fetchListenersDef = $.Deferred(); + var fetchListenersDef = testUtils.makeTestPromise(); this.data.initMessaging = { channel_slots: { @@ -549,7 +523,7 @@ QUnit.test('@ mention in mailing channel', function (assert) { }, }; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -558,62 +532,58 @@ QUnit.test('@ mention in mailing channel', function (assert) { mockRPC: function (route, args) { if (args.method === 'channel_fetch_listeners') { fetchListenersDef.resolve(); - return $.when([ + return Promise.resolve([ {id: 1, name: 'Admin'}, ]); } return this._super.apply(this, arguments); }, - }) - .then(function (discuss) { - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - - // click on general - $general.click(); - // 1st composer: basic composer (hidden), 2nd composer: extended (shown) - var $input = discuss.$('textarea.o_composer_text_field').eq(1); - assert.ok($input.length, "should display a composer input"); - - // Simulate '@' typed by user with mocked Window.getSelection - // Note: focus is needed in order to trigger rpc 'channel_fetch_listeners' - $input.focus(); - $input.val("@"); - $input.trigger('keyup'); - - fetchListenersDef - .then(concurrency.delay.bind(concurrency, 0)) - .then(function () { - assert.strictEqual(discuss.$('.dropup.o_composer_mention_dropdown.show').length, 1, - "dropup menu for partner mentions should be open"); - - var $mention = discuss.$('.o_mention_proposition'); - assert.strictEqual($mention.length, 1, - "should display 1 partner mention proposition"); - - // correct mention proposition - assert.ok($mention.hasClass('active'), - "partner mention should be active"); - assert.strictEqual($mention.data('id'), 1, - "partner mention should link to the correct partner id"); - assert.strictEqual($mention.find('.o_mention_name').text(), "Admin", - "partner mention should display the correct partner name"); - - // equivalent to $mentionPropositions.find('active').click(); - $input.trigger($.Event('keyup', {which: $.ui.keyCode.ENTER})); - - assert.ok($input.is(':focus'), "composer body should have focus"); - assert.notOk(discuss.$('.o_composer_subject').is(':focus')); - - discuss.destroy(); - done(); - }); }); + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + + // click on general + $general.click(); + await testUtils.nextTick(); + // 1st composer: basic composer (hidden), 2nd composer: extended (shown) + var $input = discuss.$('textarea.o_composer_text_field').eq(1); + assert.ok($input.length, "should display a composer input"); + + // Simulate '@' typed by user with mocked Window.getSelection + // Note: focus is needed in order to trigger rpc 'channel_fetch_listeners' + $input.focus(); + $input.val("@"); + $input.trigger('keyup'); + + await fetchListenersDef; + await testUtils.nextTick(); + + assert.strictEqual(discuss.$('.dropup.o_composer_mention_dropdown.show').length, 1, + "dropup menu for partner mentions should be open"); + + var $mention = discuss.$('.o_mention_proposition'); + assert.strictEqual($mention.length, 1, + "should display 1 partner mention proposition"); + + // correct mention proposition + assert.ok($mention.hasClass('active'), + "partner mention should be active"); + assert.strictEqual($mention.data('id'), 1, + "partner mention should link to the correct partner id"); + assert.strictEqual($mention.find('.o_mention_name').text(), "Admin", + "partner mention should display the correct partner name"); + + // equivalent to $mentionPropositions.find('active').click(); + await testUtils.fields.triggerKey('up', $input, 'enter'); + + assert.ok($input.is(':focus'), "composer body should have focus"); + assert.notOk(discuss.$('.o_composer_subject').is(':focus')); + + discuss.destroy(); }); -QUnit.test('no crash focusout emoji button', function (assert) { +QUnit.test('no crash focusout emoji button', async function (assert) { assert.expect(3); - var done = assert.async(); this.data.initMessaging = { channel_slots: { @@ -625,40 +595,37 @@ QUnit.test('no crash focusout emoji button', function (assert) { }, }; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, }) - .then(function (discuss) { - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.strictEqual($general.length, 1, - "should have the channel item with id 1"); - assert.hasAttrValue($general, 'title', 'general', - "should have the title 'general'"); - // click on general - testUtils.dom.click($general); - discuss.$('.o_composer_button_emoji').focus(); - try { - discuss.$('.o_composer_button_emoji').focusout(); - assert.ok(true, "should not crash on focusout of emoji button"); - } finally { - discuss.destroy(); - done(); - } - }); + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.strictEqual($general.length, 1, + "should have the channel item with id 1"); + assert.hasAttrValue($general, 'title', 'general', + "should have the title 'general'"); + + // click on general + await testUtils.dom.click($general); + discuss.$('.o_composer_button_emoji').focus(); + try { + discuss.$('.o_composer_button_emoji').focusout(); + assert.ok(true, "should not crash on focusout of emoji button"); + } finally { + discuss.destroy(); + } }); -QUnit.test('older messages are loaded on scroll', function (assert) { +QUnit.test('older messages are loaded on scroll', async function (assert) { assert.expect(10); - var done = assert.async(); var fetchCount = 0; - var loadMoreDef = $.Deferred(); + var loadMoreDef = testUtils.makeTestPromise(); var messageData = []; for (var i = 0; i < 35; i++) { messageData.push({ @@ -681,7 +648,7 @@ QUnit.test('older messages are loaded on scroll', function (assert) { }; this.data['mail.message'].records = messageData; - createDiscuss({ + var discuss = await createDiscuss({ context: {}, data: this.data, params: {}, @@ -703,44 +670,39 @@ QUnit.test('older messages are loaded on scroll', function (assert) { } return this._super.apply(this, arguments); }, - }).then(function (discuss) { - - assert.verifySteps(['message_fetch'], - "should fetch messages once for needaction messages (Inbox)"); - - var $general = discuss.$('.o_mail_discuss_item[data-thread-id=1]'); - assert.strictEqual($general.length, 1, - "should have a channel item with id 1"); - - // switch to 'general' - testUtils.dom.click($general); - - assert.verifySteps(['message_fetch', 'message_fetch'], - "should fetch a second time for general channel messages (30 last messages)"); - - assert.containsN(discuss, '.o_thread_message', 30, - "should display the 30 messages"); - - // simulate a scroll to top to load more messages - discuss.$('.o_mail_thread').scrollTop(0); - - loadMoreDef - .then(concurrency.delay.bind(concurrency, 0)) - .then(function () { - assert.verifySteps(['message_fetch', 'message_fetch', 'message_fetch'], - "should fetch a third time for general channel messages (5 remaining messages)"); - assert.containsN(discuss, '.o_thread_message', 35, - "all messages should now be loaded"); - - discuss.destroy(); - done(); - }); }); + + assert.verifySteps(['message_fetch'], + "should fetch messages once for needaction messages (Inbox)"); + + var $general = discuss.$('.o_mail_discuss_item[data-thread-id=1]'); + assert.strictEqual($general.length, 1, + "should have a channel item with id 1"); + + // switch to 'general' + await testUtils.dom.click($general); + + assert.verifySteps(['message_fetch'], + "should fetch a second time for general channel messages (30 last messages)"); + + assert.containsN(discuss, '.o_thread_message', 30, + "should display the 30 messages"); + + // simulate a scroll to top to load more messages + discuss.$('.o_mail_thread').scrollTop(0); + + await loadMoreDef; + await testUtils.nextTick(); + assert.verifySteps(['message_fetch'], + "should fetch a third time for general channel messages (5 remaining messages)"); + assert.containsN(discuss, '.o_thread_message', 35, + "all messages should now be loaded"); + + discuss.destroy(); }); -QUnit.test('"Unstar all" button should reset the starred counter', function (assert) { +QUnit.test('"Unstar all" button should reset the starred counter', async function (assert) { assert.expect(2); - var done = assert.async(); var messageData = []; _.each(_.range(1, 41), function (num) { @@ -768,7 +730,7 @@ QUnit.test('"Unstar all" button should reset the starred counter', function (ass this.data['mail.message'].records = messageData; var objectDiscuss; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -783,37 +745,34 @@ QUnit.test('"Unstar all" button should reset the starred counter', function (ass }; var notification = [[false, 'res.partner'], data]; objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]); - return $.when(42); + return Promise.resolve(42); } return this._super.apply(this, arguments); }, session: {partner_id: 1}, }) - .then(function (discuss) { - objectDiscuss = discuss; - var $starred = discuss.$('.o_mail_discuss_sidebar').find('.o_mail_mailbox_title_starred'); - var $starredCounter = $('.o_mail_mailbox_title_starred > .o_mail_sidebar_needaction'); + objectDiscuss = discuss; - // Go to Starred channel - testUtils.dom.click($starred); - // Test Initial Value - assert.strictEqual($starredCounter.text().trim(), "40", "40 messages should be starred"); + var $starred = discuss.$('.o_mail_discuss_sidebar').find('.o_mail_mailbox_title_starred'); + var $starredCounter = $('.o_mail_mailbox_title_starred > .o_mail_sidebar_needaction'); - // Unstar all and wait 'update_starred' - testUtils.dom.click($('.o_control_panel .o_mail_discuss_button_unstar_all')); - $starredCounter = $('.o_mail_mailbox_title_starred > .o_mail_sidebar_needaction'); - assert.strictEqual($starredCounter.text().trim(), "0", - "All messages should be unstarred"); + // Go to Starred channel + await testUtils.dom.click($starred); + // Test Initial Value + assert.strictEqual($starredCounter.text().trim(), "40", "40 messages should be starred"); - discuss.destroy(); - done(); - }); + // Unstar all and wait 'update_starred' + await testUtils.dom.click($('.o_control_panel .o_mail_discuss_button_unstar_all')); + $starredCounter = $('.o_mail_mailbox_title_starred > .o_mail_sidebar_needaction'); + assert.strictEqual($starredCounter.text().trim(), "0", + "All messages should be unstarred"); + + discuss.destroy(); }); -QUnit.test('confirm dialog when administrator leave (not chat) channel', function (assert) { +QUnit.test('confirm dialog when administrator leave (not chat) channel', async function (assert) { assert.expect(2); - var done = assert.async(); this.data.initMessaging = { channel_slots: { @@ -826,7 +785,7 @@ QUnit.test('confirm dialog when administrator leave (not chat) channel', functio }, }; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -835,30 +794,27 @@ QUnit.test('confirm dialog when administrator leave (not chat) channel', functio session: { uid: 3, }, - }) - .then(function (discuss) { - // Unsubscribe on MyChannel as administrator - // The unpin icon is only shown when hovering on sidebar item. - // helpter dom.click needs the element to be visible, hence the - // hack on display of this icon. - discuss.$('.o_mail_partner_unpin').css('display', 'block'); - testUtils.dom.click(discuss.$('.o_mail_partner_unpin')); - - assert.strictEqual($('.modal-dialog').length, 1, - "should display a dialog"); - assert.strictEqual($('.modal-body').text(), - "You are the administrator of this channel. Are you sure you want to unsubscribe?", - "Warn user that he will be unsubscribed from channel as admin."); - discuss.destroy(); - done(); }); + + // Unsubscribe on MyChannel as administrator + // The unpin icon is only shown when hovering on sidebar item. + // helpter dom.click needs the element to be visible, hence the + // hack on display of this icon. + discuss.$('.o_mail_partner_unpin').css('display', 'block'); + await testUtils.dom.click(discuss.$('.o_mail_partner_unpin')); + + assert.strictEqual($('.modal-dialog').length, 1, + "should display a dialog"); + assert.strictEqual($('.modal-body').text(), + "You are the administrator of this channel. Are you sure you want to unsubscribe?", + "Warn user that he will be unsubscribed from channel as admin."); + discuss.destroy(); }); -QUnit.test('convert emoji sources to unicodes on message_post', function (assert) { +QUnit.test('convert emoji sources to unicodes on message_post', async function (assert) { assert.expect(2); - var done = assert.async(); - var receiveMessageDef = $.Deferred(); + var receiveMessageDef = testUtils.makeTestPromise(); this.data.initMessaging = { channel_slots: { @@ -871,7 +827,7 @@ QUnit.test('convert emoji sources to unicodes on message_post', function (assert }; var objectDiscuss; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -890,40 +846,34 @@ QUnit.test('convert emoji sources to unicodes on message_post', function (assert var notification = [[false, 'mail.channel', 1], data]; objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]); receiveMessageDef.resolve(); - return $.when(42); + return Promise.resolve(42); } return this._super.apply(this, arguments); }, }) - .then(function (discuss) { - objectDiscuss= discuss; - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); + objectDiscuss= discuss; - // click on general - testUtils.dom.click($general); - var $input = discuss.$('textarea.o_composer_text_field').first(); + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); - $input.focus(); - $input.val(":) x'D"); - $input.trigger($.Event('keydown', {which: $.ui.keyCode.ENTER})); + // click on general + await testUtils.dom.click($general); + var $input = discuss.$('textarea.o_composer_text_field').first(); - receiveMessageDef - .then(concurrency.delay.bind(concurrency, 0)) - .then(function () { + $input.focus(); + $input.val(":) x'D"); + await testUtils.fields.triggerKeydown($input, 'enter'); - assert.strictEqual(discuss.$('.o_thread_message_content').text().replace(/\s/g, ""), - "😊😂", - "New posted message should contain all emojis in their unicode representation"); - discuss.destroy(); - done(); - }); + await receiveMessageDef.then(function () { + assert.strictEqual(discuss.$('.o_thread_message_content').text().replace(/\s/g, ""), + "😊😂", + "New posted message should contain all emojis in their unicode representation"); + discuss.destroy(); }); }); -QUnit.test('mark all messages as read from Inbox', function (assert) { - var done = assert.async(); +QUnit.test('mark all messages as read from Inbox', async function (assert) { assert.expect(9); this.data['mail.message'].records = [{ @@ -944,10 +894,10 @@ QUnit.test('mark all messages as read from Inbox', function (assert) { needaction_inbox_counter: 2, }; - var markAllReadDef = $.Deferred(); + var markAllReadDef = testUtils.makeTestPromise(); var objectDiscuss; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -966,53 +916,50 @@ QUnit.test('mark all messages as read from Inbox', function (assert) { var notification = [[false, 'res.partner', 3], notificationData]; objectDiscuss.call('bus_service', 'trigger', 'notification', [notification]); markAllReadDef.resolve(); - return $.when(); + return Promise.resolve(); } return this._super.apply(this, arguments); }, }) - .then(function (discuss) { - objectDiscuss = discuss; - var $inbox = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_inbox"]'); - assert.strictEqual($inbox.length, 1, - "there should be an 'Inbox' item in Discuss sidebar"); - assert.strictEqual($inbox.find('.o_mail_sidebar_needaction').text().trim(), "2", - "the mailbox counter of 'Inbox' should be 2"); - assert.hasClass($inbox,'o_active', - "'Inbox' should be the currently active thread"); - assert.containsN(discuss, '.o_thread_message', 2, - "there should be 2 messages in inbox"); + objectDiscuss = discuss; - var $markAllReadButton = $('.o_mail_discuss_button_mark_all_read'); - assert.strictEqual($markAllReadButton.length, 1, - "there should be a 'Mark All As Read' button"); - assert.hasAttrValue($markAllReadButton, 'style', - 'display: inline-block;', - "the 'Mark All As Read' button should be visible"); - assert.notOk($markAllReadButton.prop('disabled'), - "the 'Mark All As Read' button should not be disabled"); + var $inbox = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_inbox"]'); + assert.strictEqual($inbox.length, 1, + "there should be an 'Inbox' item in Discuss sidebar"); + assert.strictEqual($inbox.find('.o_mail_sidebar_needaction').text().trim(), "2", + "the mailbox counter of 'Inbox' should be 2"); + assert.hasClass($inbox,'o_active', + "'Inbox' should be the currently active thread"); + assert.containsN(discuss, '.o_thread_message', 2, + "there should be 2 messages in inbox"); - testUtils.dom.click($markAllReadButton); + var $markAllReadButton = $('.o_mail_discuss_button_mark_all_read'); + assert.strictEqual($markAllReadButton.length, 1, + "there should be a 'Mark All As Read' button"); + assert.hasAttrValue($markAllReadButton, 'style', + 'display: inline-block;', + "the 'Mark All As Read' button should be visible"); + assert.notOk($markAllReadButton.prop('disabled'), + "the 'Mark All As Read' button should not be disabled"); - markAllReadDef.then(function () { - // immediately jump to end of the fadeout animation on messages - $inbox = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_inbox"]'); - discuss.$('.o_thread_message').stop(false, true); - assert.strictEqual($inbox.find('.o_mail_sidebar_needaction').text().trim(), "0", - "the mailbox counter of 'Inbox' should have reset to 0"); - assert.containsNone(discuss, '.o_thread_message', - "there should no message in inbox anymore"); + await testUtils.dom.click($markAllReadButton); - discuss.destroy(); - done(); - }); + markAllReadDef.then(function () { + // immediately jump to end of the fadeout animation on messages + $inbox = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_inbox"]'); + discuss.$('.o_thread_message').stop(false, true); + assert.strictEqual($inbox.find('.o_mail_sidebar_needaction').text().trim(), "0", + "the mailbox counter of 'Inbox' should have reset to 0"); + assert.containsNone(discuss, '.o_thread_message', + "there should no message in inbox anymore"); + + discuss.destroy(); }); }); -QUnit.test('drag and drop file in composer', function (assert) { +QUnit.test('drag and drop file in composer [REQUIRE NON-INCOGNITO WINDOW]', async function (assert) { assert.expect(8); - var done = assert.async(); this.data.initMessaging = { channel_slots: { @@ -1024,60 +971,57 @@ QUnit.test('drag and drop file in composer', function (assert) { }, }; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, }) - .then(function (discuss) { - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - // click on general - testUtils.dom.click($general); + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); - // first composer is active (basic), 2nd is hidden (extended) - var $composer = discuss.$('.o_thread_composer').first(); - assert.containsNone($composer, '.o_attachments_list', - "should not display any attachment on the composer initially"); - assert.containsOnce($composer, '.o_file_drop_zone_container', - "should have a dropzone to drag-and-drop files"); + // click on general + await testUtils.dom.click($general); - var $dropZoneContainer = $composer.find('.o_file_drop_zone_container'); - assert.isNotVisible($dropZoneContainer, - "dropzone should not be visible"); + // first composer is active (basic), 2nd is hidden (extended) + var $composer = discuss.$('.o_thread_composer').first(); + assert.containsNone($composer, '.o_attachments_list', + "should not display any attachment on the composer initially"); + assert.containsOnce($composer, '.o_file_drop_zone_container', + "should have a dropzone to drag-and-drop files"); - testUtils.file.createFile({ - name: 'text.txt', - content: 'hello, world', - contentType: 'text/plain', - }).then(function (file) { - testUtils.file.dragoverFile($dropZoneContainer, file); - assert.isVisible($dropZoneContainer, - "dropzone should be visible"); + var $dropZoneContainer = $composer.find('.o_file_drop_zone_container'); + assert.isNotVisible($dropZoneContainer, + "dropzone should not be visible"); - testUtils.file.dropFile($dropZoneContainer, file); - assert.containsOnce($composer, '.o_attachments_list', - "should display some attachments on the composer"); - assert.containsOnce($composer, '.o_attachment', - "should display one attachment on the composer"); - - var filename = $('.o_attachment').find('.caption').first().text().trim(); - assert.strictEqual(filename, 'text.txt', - "should display the correct filename"); - assert.hasClass($('.o_attachment_uploaded i'), 'fa-check', - "text file should have been uploaded"); - - discuss.destroy(); - done(); - }); + var file = await testUtils.file.createFile({ + name: 'text.txt', + content: 'hello, world', + contentType: 'text/plain', }); + + await testUtils.file.dragoverFile($dropZoneContainer, file); + assert.isVisible($dropZoneContainer, + "dropzone should be visible"); + + await testUtils.file.dropFile($dropZoneContainer, file); + assert.containsOnce($composer, '.o_attachments_list', + "should display some attachments on the composer"); + assert.containsOnce($composer, '.o_attachment', + "should display one attachment on the composer"); + + var filename = $('.o_attachment').find('.caption').first().text().trim(); + assert.strictEqual(filename, 'text.txt', + "should display the correct filename"); + assert.hasClass($('.o_attachment_uploaded i'), 'fa-check', + "text file should have been uploaded"); + + discuss.destroy(); }); -QUnit.test('reply to message from inbox', function (assert) { - var done = assert.async(); +QUnit.test('reply to message from inbox', async function (assert) { assert.expect(11); var self = this; @@ -1095,7 +1039,7 @@ QUnit.test('reply to message from inbox', function (assert) { needaction_inbox_counter: 1, }; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -1118,45 +1062,41 @@ QUnit.test('reply to message from inbox', function (assert) { model: 'some.document', record_name: 'SomeDocument', }); - return $.when(2); + return Promise.resolve(2); } return this._super.apply(this, arguments); }, }) - .then(function (discuss) { - assert.strictEqual(discuss.$('.o_mail_discuss_item.o_active').data('thread-id'), - 'mailbox_inbox', - "Inbox should be selected by default"); - assert.containsOnce(discuss, '.o_thread_message', - "should display a single message in inbox"); - assert.strictEqual(discuss.$('.o_thread_message').data('message-id'), 1, - "message should be linked to correct message"); - assert.containsOnce(discuss.$('.o_thread_message'), '.o_thread_message_reply', - "should display the reply icon for message linked to a document"); + assert.strictEqual(discuss.$('.o_mail_discuss_item.o_active').data('thread-id'), + 'mailbox_inbox', + "Inbox should be selected by default"); + assert.containsOnce(discuss, '.o_thread_message', + "should display a single message in inbox"); + assert.strictEqual(discuss.$('.o_thread_message').data('message-id'), 1, + "message should be linked to correct message"); + assert.containsOnce(discuss.$('.o_thread_message'), '.o_thread_message_reply', + "should display the reply icon for message linked to a document"); - testUtils.dom.click(discuss.$('.o_thread_message_reply')); - var $composer = discuss.$('.o_thread_composer_extended'); - assert.isVisible($composer, - "extended composer should become visible"); - assert.strictEqual($composer.find('.o_composer_subject input').val(), - 'Re: SomeDocument', - "composer should have copied document name as subject of message"); + await testUtils.dom.click(discuss.$('.o_thread_message_reply')); + var $composer = discuss.$('.o_thread_composer_extended'); + assert.isVisible($composer, + "extended composer should become visible"); + assert.strictEqual($composer.find('.o_composer_subject input').val(), + 'Re: SomeDocument', + "composer should have copied document name as subject of message"); - var $textarea = $composer.find('.o_composer_input textarea').first(); - testUtils.fields.editInput($textarea, 'someContent'); - assert.containsOnce($composer, '.o_composer_button_send', - "should have button to send reply message"); - testUtils.dom.click($composer.find('.o_composer_button_send')); + var $textarea = $composer.find('.o_composer_input textarea').first(); + await testUtils.fields.editInput($textarea, 'someContent'); + assert.containsOnce($composer, '.o_composer_button_send', + "should have button to send reply message"); + await testUtils.dom.click($composer.find('.o_composer_button_send')); - assert.verifySteps(['message_post']); + assert.verifySteps(['message_post']); - discuss.destroy(); - done(); - }); + discuss.destroy(); }); -QUnit.test('discard replying to message from inbox', function (assert) { - var done = assert.async(); +QUnit.test('discard replying to message from inbox', async function (assert) { assert.expect(4); var self = this; @@ -1174,7 +1114,7 @@ QUnit.test('discard replying to message from inbox', function (assert) { needaction_inbox_counter: 1, }; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -1197,34 +1137,31 @@ QUnit.test('discard replying to message from inbox', function (assert) { model: 'some.document', record_name: 'SomeDocument', }); - return $.when(2); + return Promise.resolve(2); } return this._super.apply(this, arguments); }, }) - .then(function (discuss) { - testUtils.dom.click(discuss.$('.o_thread_message_reply')); - assert.containsOnce(discuss, '.o_thread_selected_message', - "should have a message selected"); - var $composer = discuss.$('.o_thread_composer_extended'); - assert.containsOnce($composer, '.o_composer_button_discard', - "should have button to discard replying to message"); + await testUtils.dom.click(discuss.$('.o_thread_message_reply')); + assert.containsOnce(discuss, '.o_thread_selected_message', + "should have a message selected"); - testUtils.dom.click($composer.find('.o_composer_button_discard')); - assert.isNotVisible($composer, - "extended composer should become hidden on discard"); - assert.containsNone(discuss, '.o_thread_selected_message', - "should not longer have a message selected"); + var $composer = discuss.$('.o_thread_composer_extended'); + assert.containsOnce($composer, '.o_composer_button_discard', + "should have button to discard replying to message"); - discuss.destroy(); - done(); - }); + await testUtils.dom.click($composer.find('.o_composer_button_discard')); + assert.isNotVisible($composer, + "extended composer should become hidden on discard"); + assert.containsNone(discuss, '.o_thread_selected_message', + "should not longer have a message selected"); + + discuss.destroy(); }); -QUnit.test('no quick search channels in the sidebar with less than 20 channels', function (assert) { +QUnit.test('no quick search channels in the sidebar with less than 20 channels', async function (assert) { assert.expect(3); - var done = assert.async(); var channelsData = []; _.each(_.range(0, 19), function (num) { @@ -1241,32 +1178,29 @@ QUnit.test('no quick search channels in the sidebar with less than 20 channels', }, }; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, }) - .then(function (discuss) { - var $sidebar = discuss.$('.o_mail_discuss_sidebar'); - var $channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels'); - assert.strictEqual($channelsSidebar.length, 1, - "should show channels and dms in sidebar"); - assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 19, - "should show 19 channels in sidebar"); - assert.strictEqual(discuss.$('.o_discuss_sidebar_quick_search').length, 0, - "should not display a quick search"); + var $sidebar = discuss.$('.o_mail_discuss_sidebar'); - discuss.destroy(); - done(); - }); + var $channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels'); + assert.strictEqual($channelsSidebar.length, 1, + "should show channels and dms in sidebar"); + assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 19, + "should show 19 channels in sidebar"); + assert.strictEqual(discuss.$('.o_discuss_sidebar_quick_search').length, 0, + "should not display a quick search"); + + discuss.destroy(); }); -QUnit.test('quick search channels in the sidebar with more than 20 channels', function (assert) { +QUnit.test('quick search channels in the sidebar with more than 20 channels', async function (assert) { assert.expect(7); - var done = assert.async(); var channelsData = []; var channelANum = 15; @@ -1294,52 +1228,49 @@ QUnit.test('quick search channels in the sidebar with more than 20 channels', fu }, }; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, }) - .then(function (discuss) { - var $sidebar = discuss.$('.o_mail_discuss_sidebar'); - var $channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels'); - assert.strictEqual($channelsSidebar.length, 1, - "should show channels in sidebar"); - assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 20, - "should show 20 channels in sidebar"); - assert.strictEqual(discuss.$('.o_discuss_sidebar_quick_search').length, 1, - "should display a quick search"); + var $sidebar = discuss.$('.o_mail_discuss_sidebar'); - discuss.$('.o_discuss_sidebar_quick_search input').val('channelA').trigger('input'); - $channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels'); - assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 15, - "should now show 15 channels in sidebar"); + var $channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels'); + assert.strictEqual($channelsSidebar.length, 1, + "should show channels in sidebar"); + assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 20, + "should show 20 channels in sidebar"); + assert.strictEqual(discuss.$('.o_discuss_sidebar_quick_search').length, 1, + "should display a quick search"); - discuss.$('.o_discuss_sidebar_quick_search input').val('channelB').trigger('input'); + discuss.$('.o_discuss_sidebar_quick_search input').val('channelA').trigger('input'); + $channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels'); + assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 15, + "should now show 15 channels in sidebar"); - $channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels'); - assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 5, - "should now show 5 channels in sidebar"); + discuss.$('.o_discuss_sidebar_quick_search input').val('channelB').trigger('input'); - discuss.$('.o_discuss_sidebar_quick_search input').val('channelB4').trigger('input'); + $channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels'); + assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 5, + "should now show 5 channels in sidebar"); - $channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels'); - assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 1, - "should now show a single channel in sidebar"); - assert.strictEqual($channelsSidebar.find('.o_thread_name').text().replace(/\s/g, ''), - '#ChannelB4', - "should have searched the correct channel B4"); + discuss.$('.o_discuss_sidebar_quick_search input').val('channelB4').trigger('input'); - discuss.destroy(); - done(); - }); + $channelsSidebar = $sidebar.find('.o_mail_discuss_sidebar_channels'); + assert.strictEqual($channelsSidebar.find('.o_mail_discuss_item').length, 1, + "should now show a single channel in sidebar"); + assert.strictEqual($channelsSidebar.find('.o_thread_name').text().replace(/\s/g, ''), + '#ChannelB4', + "should have searched the correct channel B4"); + + discuss.destroy(); }); -QUnit.test('select emoji replaces cursor position', function (assert) { +QUnit.test('select emoji replaces cursor position', async function (assert) { assert.expect(5); - var done = assert.async(); this.data.initMessaging = { channel_slots: { @@ -1351,55 +1282,51 @@ QUnit.test('select emoji replaces cursor position', function (assert) { }, }; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, data: this.data, services: this.services, - }) - .then(function (discuss) { - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.strictEqual($general.length, 1, - "should have the channel item with id 1"); - assert.hasAttrValue($general, 'title', 'general', - "should have the title 'general'"); - - // click on general - testUtils.dom.click($general); - - // general uses basic composer, so this is the 1st composer - // (2nd composer is hidden) - var $composer = discuss.$('.o_composer_text_field').first(); - - testUtils.fields.editInput($composer, 'abcdefgh'); - - assert.strictEqual($composer.val(), "abcdefgh"); - - // cursor position: ab|cd|efgh (selecting c and d) - $composer[0].setSelectionRange(2, 4); - - testUtils.dom.click(discuss.$('.o_composer_button_emoji')); - testUtils.dom.triggerMouseEvent(discuss.$('.o_mail_emoji[data-emoji=":)"]'), 'mousedown'); - - assert.strictEqual($composer.val(), "ab :) efgh", - "should have inserted emoji "); - - testUtils.dom.click(discuss.$('.o_composer_button_emoji')); - testUtils.dom.triggerMouseEvent(discuss.$('.o_mail_emoji[data-emoji=":)"]'), 'mousedown'); - - assert.strictEqual($composer.val(), "ab :) :) efgh", - "should have inserted emoji after previously inserted emoji"); - - discuss.destroy(); - done(); }); + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.strictEqual($general.length, 1, + "should have the channel item with id 1"); + assert.hasAttrValue($general, 'title', 'general', + "should have the title 'general'"); + + // click on general + await testUtils.dom.click($general); + + // general uses basic composer, so this is the 1st composer + // (2nd composer is hidden) + var $composer = discuss.$('.o_composer_text_field').first(); + + await testUtils.fields.editInput($composer, 'abcdefgh'); + + assert.strictEqual($composer.val(), "abcdefgh"); + + // cursor position: ab|cd|efgh (selecting c and d) + $composer[0].setSelectionRange(2, 4); + + await testUtils.dom.click(discuss.$('.o_composer_button_emoji')); + await testUtils.dom.triggerMouseEvent(discuss.$('.o_mail_emoji[data-emoji=":)"]'), 'mousedown'); + + assert.strictEqual($composer.val(), "ab :) efgh", + "should have inserted emoji "); + + await testUtils.dom.click(discuss.$('.o_composer_button_emoji')); + await testUtils.dom.triggerMouseEvent(discuss.$('.o_mail_emoji[data-emoji=":)"]'), 'mousedown'); + + assert.strictEqual($composer.val(), "ab :) :) efgh", + "should have inserted emoji after previously inserted emoji"); + + discuss.destroy(); }); -QUnit.test('rename DM conversation', function (assert) { +QUnit.test('rename DM conversation', async function (assert) { assert.expect(11); - var done = assert.async(); this.data.initMessaging = { channel_slots: { @@ -1416,7 +1343,7 @@ QUnit.test('rename DM conversation', function (assert) { }, }; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -1428,41 +1355,38 @@ QUnit.test('rename DM conversation', function (assert) { assert.step(args.method); assert.strictEqual(args.args[0], 1); assert.strictEqual(args.kwargs.name, "Demo"); - return $.when("Demo"); + return Promise.resolve("Demo"); } return this._super.apply(this, arguments); }, - }) - .then(function (discuss) { - var $dm = discuss.$('.o_mail_discuss_item[data-thread-id=1]'); - assert.isVisible($dm, "should display DM in the discuss sidebar"); - assert.strictEqual($dm.find('.o_thread_name').text().trim(), "Demo User"); - - // The settings icon is only shown when hovering on sidebar item. - // helpter dom.click needs the element to be visible, hence the - // hack on display of this icon. - discuss.$('.o_mail_channel_settings').css('display', 'block'); - testUtils.dom.click(discuss.$('.o_mail_channel_settings')); - - assert.isVisible($('.modal-dialog')); - assert.strictEqual($('.modal-title').text(), "Rename conversation"); - assert.isVisible($('.o_mail_discuss_rename_channel_input')); - assert.strictEqual($('.o_mail_discuss_rename_channel_input').val(), "Demo User"); - - testUtils.fields.editInput($('.o_mail_discuss_rename_channel_input'), "Demo"); - testUtils.dom.click($('.o_mail_conversation_rename')); - $dm = discuss.$('.o_mail_discuss_item[data-thread-id=1]'); - assert.verifySteps(['channel_set_custom_name']); - assert.strictEqual($dm.find('.o_thread_name').text().trim(), "Demo"); - - discuss.destroy(); - done(); }); + + var $dm = discuss.$('.o_mail_discuss_item[data-thread-id=1]'); + assert.isVisible($dm, "should display DM in the discuss sidebar"); + assert.strictEqual($dm.find('.o_thread_name').text().trim(), "Demo User"); + + // The settings icon is only shown when hovering on sidebar item. + // helpter dom.click needs the element to be visible, hence the + // hack on display of this icon. + discuss.$('.o_mail_channel_settings').css('display', 'block'); + await testUtils.dom.click(discuss.$('.o_mail_channel_settings')); + + assert.isVisible($('.modal-dialog')); + assert.strictEqual($('.modal-title').text(), "Rename conversation"); + assert.isVisible($('.o_mail_discuss_rename_channel_input')); + assert.strictEqual($('.o_mail_discuss_rename_channel_input').val(), "Demo User"); + + await testUtils.fields.editInput($('.o_mail_discuss_rename_channel_input'), "Demo"); + await testUtils.dom.click($('.o_mail_conversation_rename')); + $dm = discuss.$('.o_mail_discuss_item[data-thread-id=1]'); + assert.verifySteps(['channel_set_custom_name']); + assert.strictEqual($dm.find('.o_thread_name').text().trim(), "Demo"); + + discuss.destroy(); }); -QUnit.test('custom-named DM conversation', function (assert) { +QUnit.test('custom-named DM conversation', async function (assert) { assert.expect(2); - var done = assert.async(); this.data.initMessaging = { channel_slots: { @@ -1480,7 +1404,7 @@ QUnit.test('custom-named DM conversation', function (assert) { }, }; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -1488,14 +1412,12 @@ QUnit.test('custom-named DM conversation', function (assert) { services: this.services, session: { partner_id: 3 }, }) - .then(function (discuss) { - var $dm = discuss.$('.o_mail_discuss_item[data-thread-id=1]'); - assert.isVisible($dm, "should display DM in the discuss sidebar"); - assert.strictEqual($dm.find('.o_thread_name').text().trim(), "My Buddy"); - discuss.destroy(); - done(); - }); + var $dm = discuss.$('.o_mail_discuss_item[data-thread-id=1]'); + assert.isVisible($dm, "should display DM in the discuss sidebar"); + assert.strictEqual($dm.find('.o_thread_name').text().trim(), "My Buddy"); + + discuss.destroy(); }); }); diff --git a/addons/mail/static/tests/discuss_typing_notification_tests.js b/addons/mail/static/tests/discuss_typing_notification_tests.js index 2d1eb679c65..d77d32b9f1f 100644 --- a/addons/mail/static/tests/discuss_typing_notification_tests.js +++ b/addons/mail/static/tests/discuss_typing_notification_tests.js @@ -153,15 +153,14 @@ QUnit.module('Discuss (Typing Notifications)', { } }); -QUnit.test('notify is typing', function (assert) { +QUnit.test('notify is typing', async function (assert) { assert.expect(11); - var done = assert.async(); // this is to track the step that we are testing in the `notify_typing` // mocked RPC var step = 1; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -188,44 +187,42 @@ QUnit.test('notify is typing', function (assert) { return this._super.apply(this, arguments); }, session: { partner_id: this.myPartnerID }, - }).then(function (discuss) { - - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.strictEqual($general.length, 1, - "should have the channel item with id 1"); - assert.hasAttrValue($general, 'title', 'general', - "should have the title 'general'"); - - // click on general - testUtils.dom.click($general); - - var $input = discuss.$('textarea.o_composer_text_field').first(); - assert.ok($input.length, "should display a composer input"); - - // STEP 1: current user is typing something - step = 1; - $input.focus(); - testUtils.fields.editInput($input, '1'); - - assert.verifySteps(['notify_typing']); - - // STEP 2: current user clears input - step = 2; - testUtils.fields.editInput($input, ''); - - assert.verifySteps(['notify_typing', 'notify_typing']); - - discuss.destroy(); - done(); }); + + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.strictEqual($general.length, 1, + "should have the channel item with id 1"); + assert.hasAttrValue($general, 'title', 'general', + "should have the title 'general'"); + + // click on general + await testUtils.dom.click($general); + + var $input = discuss.$('textarea.o_composer_text_field').first(); + assert.ok($input.length, "should display a composer input"); + + // STEP 1: current user is typing something + step = 1; + $input.focus(); + testUtils.fields.editInput($input, '1'); + + assert.verifySteps(['notify_typing']); + + // STEP 2: current user clears input + step = 2; + testUtils.fields.editInput($input, ''); + + assert.verifySteps(['notify_typing']); + + discuss.destroy(); }); -QUnit.test('receive is typing notification', function (assert) { +QUnit.test('receive is typing notification', async function (assert) { assert.expect(10); var self = this; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -235,7 +232,7 @@ QUnit.test('receive is typing notification', function (assert) { // when receiving an 'is typing' notification, fetch the list of // members of this channel if we haven't done yet. if (args.method === 'channel_fetch_listeners') { - return $.when([ + return Promise.resolve([ { id: self.myPartnerID, name: self.myName }, { id: 42, name: "Someone" }, ]); @@ -243,67 +240,68 @@ QUnit.test('receive is typing notification', function (assert) { return this._super.apply(this, arguments); }, session: { partner_id: this.myPartnerID }, - }).then(function (discuss) { - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.strictEqual($general.length, 1, - "should have the channel item with id 1"); - assert.hasAttrValue($general, 'title', 'general', - "should have the title 'general'"); - - // pick 1st composer (basic), not 2nd composer (extended, hidden) - var $composer = discuss.$('.o_thread_composer').first(); - - // click on general - testUtils.dom.click($general); - assert.containsNone($general, '.o_mail_thread_typing_icon', - "should no longer show someone is typing in sidebar of discuss"); - assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), "", - "should no longer show someone is typing on the composer of active thread"); - - self.simulateIsTyping({ - channelID: 1, - isTyping: true, - partnerID: 42, - widget: discuss, - }); - - $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.containsOnce($general, '.o_mail_thread_typing_icon', - "should have a thread typing icon next to general icon in the sidebar"); - assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', - "Someone is typing...", - "should show who is typing on hover on this thread typing icon"); - assert.containsOnce($composer, '.o_composer_thread_typing', - "should show typing info on the composer of active thread"); - assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), - "Someone is typing...", - "should show who is typing on the composer of active thread"); - - self.simulateIsTyping({ - channelID: 1, - isTyping: false, - partnerID: 42, - widget: discuss, - }); - - $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.containsNone($general, '.o_mail_thread_typing_icon', - "should no longer show someone is typing in sidebar of discuss"); - assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), "", - "should no longer show someone is typing on the composer of active thread"); - - discuss.destroy(); }); + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.strictEqual($general.length, 1, + "should have the channel item with id 1"); + assert.hasAttrValue($general, 'title', 'general', + "should have the title 'general'"); + + // pick 1st composer (basic), not 2nd composer (extended, hidden) + var $composer = discuss.$('.o_thread_composer').first(); + + // click on general + await testUtils.dom.click($general); + assert.containsNone($general, '.o_mail_thread_typing_icon', + "should no longer show someone is typing in sidebar of discuss"); + assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), "", + "should no longer show someone is typing on the composer of active thread"); + + self.simulateIsTyping({ + channelID: 1, + isTyping: true, + partnerID: 42, + widget: discuss, + }); + await testUtils.nextMicrotaskTick(); + + $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.containsOnce($general, '.o_mail_thread_typing_icon', + "should have a thread typing icon next to general icon in the sidebar"); + assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', + "Someone is typing...", + "should show who is typing on hover on this thread typing icon"); + assert.containsOnce($composer, '.o_composer_thread_typing', + "should show typing info on the composer of active thread"); + assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), + "Someone is typing...", + "should show who is typing on the composer of active thread"); + + self.simulateIsTyping({ + channelID: 1, + isTyping: false, + partnerID: 42, + widget: discuss, + }); + await testUtils.nextMicrotaskTick(); + + $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.containsNone($general, '.o_mail_thread_typing_icon', + "should no longer show someone is typing in sidebar of discuss"); + assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), "", + "should no longer show someone is typing on the composer of active thread"); + + discuss.destroy(); }); -QUnit.test('receive message of someone that was typing something', function (assert) { +QUnit.test('receive message of someone that was typing something', async function (assert) { assert.expect(6); var self = this; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -313,7 +311,7 @@ QUnit.test('receive message of someone that was typing something', function (ass // when receiving an 'is typing' notification, fetch the list of // members of this channel if we haven't done yet. if (args.method === 'channel_fetch_listeners') { - return $.when([ + return Promise.resolve([ { id: self.myPartnerID, name: self.myName }, { id: 42, name: "Someone" }, ]); @@ -321,62 +319,63 @@ QUnit.test('receive message of someone that was typing something', function (ass return this._super.apply(this, arguments); }, session: { partner_id: this.myPartnerID }, - }).then(function (discuss) { - // click on general channel - var $general = discuss.$('.o_mail_discuss_sidebar .o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($general); - - // pick 1st composer (basic), not 2nd composer (extended, hidden) - var $composer = discuss.$('.o_thread_composer').first(); - - self.simulateIsTyping({ - channelID: 1, - isTyping: true, - partnerID: 42, - widget: discuss, - }); - - $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.containsOnce($general, '.o_mail_thread_typing_icon', - "should have a thread typing icon next to general icon in the sidebar"); - assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', - "Someone is typing...", - "should show who is typing on hover on this thread typing icon"); - assert.containsOnce($composer, '.o_composer_thread_typing', - "should show typing info on the composer of active thread"); - assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), - "Someone is typing...", - "should show who is typing on the composer of active thread"); - - // Simulate receive message from the person typing something - var messageData = { - author_id: [42, 'Someone'], - body: "

test

", - channel_ids: [1], - id: 101, - model: 'mail.channel', - res_id: 1, - }; - var notification = [[false, 'mail.channel', 1], messageData]; - discuss.call('bus_service', 'trigger', 'notification', [notification]); - - $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.containsNone($general, '.o_mail_thread_typing_icon', - "should no longer show someone is typing in sidebar of discuss, after receiving message"); - assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), "", - "should no longer show someone is typing on the composer of active thread, after receiving message"); - - discuss.destroy(); }); + // click on general channel + var $general = discuss.$('.o_mail_discuss_sidebar .o_mail_discuss_item[data-thread-id=1]'); + await testUtils.dom.click($general); + + // pick 1st composer (basic), not 2nd composer (extended, hidden) + var $composer = discuss.$('.o_thread_composer').first(); + + self.simulateIsTyping({ + channelID: 1, + isTyping: true, + partnerID: 42, + widget: discuss, + }); + await testUtils.nextMicrotaskTick(); + + $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.containsOnce($general, '.o_mail_thread_typing_icon', + "should have a thread typing icon next to general icon in the sidebar"); + assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', + "Someone is typing...", + "should show who is typing on hover on this thread typing icon"); + assert.containsOnce($composer, '.o_composer_thread_typing', + "should show typing info on the composer of active thread"); + assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), + "Someone is typing...", + "should show who is typing on the composer of active thread"); + + // Simulate receive message from the person typing something + var messageData = { + author_id: [42, 'Someone'], + body: "

test

", + channel_ids: [1], + id: 101, + model: 'mail.channel', + res_id: 1, + }; + var notification = [[false, 'mail.channel', 1], messageData]; + await discuss.call('bus_service', 'trigger', 'notification', [notification]); + await testUtils.nextTick(); + + $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.containsNone($general, '.o_mail_thread_typing_icon', + "should no longer show someone is typing in sidebar of discuss, after receiving message"); + assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), "", + "should no longer show someone is typing on the composer of active thread, after receiving message"); + + discuss.destroy(); }); -QUnit.test('do not display myself as typing', function (assert) { +QUnit.test('do not display myself as typing', async function (assert) { assert.expect(2); var self = this; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -386,40 +385,39 @@ QUnit.test('do not display myself as typing', function (assert) { // when receiving an 'is typing' notification, fetch the list of // members of this channel if we haven't done yet. if (args.method === 'channel_fetch_listeners') { - return $.when([ + return Promise.resolve([ { id: self.myPartnerID, name: self.myName }, ]); } return this._super.apply(this, arguments); }, session: { partner_id: this.myPartnerID }, - }).then(function (discuss) { - // click on general channel - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($general); - // pick 1st composer (basic), not 2nd composer (extended, hidden) - var $composer = discuss.$('.o_thread_composer').first(); - - self.simulateIsTyping({ - channelID: 1, - isTyping: true, - partnerID: self.myPartnerID, - widget: discuss, - }); - - $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.containsNone($general, '.o_mail_thread_typing_icon', - "should not show current user is typing in sidebar of discuss"); - assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), "", - "should not show current user is typing on the composer of active thread"); - - discuss.destroy(); }); + // click on general channel + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + await testUtils.dom.click($general); + // pick 1st composer (basic), not 2nd composer (extended, hidden) + var $composer = discuss.$('.o_thread_composer').first(); + + self.simulateIsTyping({ + channelID: 1, + isTyping: true, + partnerID: self.myPartnerID, + widget: discuss, + }); + + $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.containsNone($general, '.o_mail_thread_typing_icon', + "should not show current user is typing in sidebar of discuss"); + assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), "", + "should not show current user is typing on the composer of active thread"); + + discuss.destroy(); }); -QUnit.test('several users typing something at the same time', function (assert) { +QUnit.test('several users typing something at the same time', async function (assert) { // The order to display several typing partners name is not random: // it shows at most 2 typing partners, which are the typing partners that // have been typing something for the longest time. @@ -428,7 +426,7 @@ QUnit.test('several users typing something at the same time', function (assert) assert.expect(10); var self = this; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -438,7 +436,7 @@ QUnit.test('several users typing something at the same time', function (assert) // when receiving an 'is typing' notification, fetch the list of // members of this channel if we haven't done yet. if (args.method === 'channel_fetch_listeners') { - return $.when([ + return Promise.resolve([ { id: self.myPartnerID, name: self.myName }, { id: 42, name: "Someone" }, { id: 43, name: "Anonymous" }, @@ -448,98 +446,101 @@ QUnit.test('several users typing something at the same time', function (assert) return this._super.apply(this, arguments); }, session: { partner_id: this.myPartnerID }, - }).then(function (discuss) { - // click on general channel - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - - // click on general - testUtils.dom.click($general); - - // pick 1st composer (basic), not 2nd composer (extended, hidden) - var $composer = discuss.$('.o_thread_composer').first(); - - self.simulateIsTyping({ - channelID: 1, - isTyping: true, - partnerID: 42, - widget: discuss, - }); - - $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.containsOnce($general, '.o_mail_thread_typing_icon', - "should have a thread typing icon next to general icon in the sidebar"); - assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', - "Someone is typing...", - "should show someone is typing on hover on this thread typing icon"); - assert.containsOnce($composer, '.o_composer_thread_typing', - "should show typing info on the composer of active thread"); - assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), - "Someone is typing...", - "should show someone is typing on the composer of active thread"); - - self.simulateIsTyping({ - channelID: 1, - isTyping: true, - partnerID: 43, - widget: discuss, - }); - - $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', - "Someone and Anonymous are typing...", - "should show someone and anonymous are simultaneously typing on hover on this thread typing icon"); - assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), - "Someone and Anonymous are typing...", - "should show someone and anonymous are simultaneously typing on the composer of active thread"); - - self.simulateIsTyping({ - channelID: 1, - isTyping: true, - partnerID: 44, - widget: discuss, - }); - - $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', - "Someone, Anonymous and more are typing...", - "should show 2 and more users are simultaneously typing on hover on this thread typing icon"); - assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), - "Someone, Anonymous and more are typing...", - "should show 2 and more users are simultaneously typing on the composer of active thread"); - - self.simulateIsTyping({ - channelID: 1, - isTyping: false, - partnerID: 42, - widget: discuss, - }); - - $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', - "Anonymous and Shy Guy are typing...", - "should no longer show than someone is typing on hover on this thread typing icon"); - assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), - "Anonymous and Shy Guy are typing...", - "should no longer show than someone is typing on the composer of active thread"); - - discuss.destroy(); }); + // click on general channel + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + + // click on general + await testUtils.dom.click($general); + + // pick 1st composer (basic), not 2nd composer (extended, hidden) + var $composer = discuss.$('.o_thread_composer').first(); + + self.simulateIsTyping({ + channelID: 1, + isTyping: true, + partnerID: 42, + widget: discuss, + }); + await testUtils.nextMicrotaskTick(); + + $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.containsOnce($general, '.o_mail_thread_typing_icon', + "should have a thread typing icon next to general icon in the sidebar"); + assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', + "Someone is typing...", + "should show someone is typing on hover on this thread typing icon"); + assert.containsOnce($composer, '.o_composer_thread_typing', + "should show typing info on the composer of active thread"); + assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), + "Someone is typing...", + "should show someone is typing on the composer of active thread"); + + self.simulateIsTyping({ + channelID: 1, + isTyping: true, + partnerID: 43, + widget: discuss, + }); + await testUtils.nextMicrotaskTick(); + + $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', + "Someone and Anonymous are typing...", + "should show someone and anonymous are simultaneously typing on hover on this thread typing icon"); + assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), + "Someone and Anonymous are typing...", + "should show someone and anonymous are simultaneously typing on the composer of active thread"); + + self.simulateIsTyping({ + channelID: 1, + isTyping: true, + partnerID: 44, + widget: discuss, + }); + await testUtils.nextMicrotaskTick(); + + $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', + "Someone, Anonymous and more are typing...", + "should show 2 and more users are simultaneously typing on hover on this thread typing icon"); + assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), + "Someone, Anonymous and more are typing...", + "should show 2 and more users are simultaneously typing on the composer of active thread"); + + self.simulateIsTyping({ + channelID: 1, + isTyping: false, + partnerID: 42, + widget: discuss, + }); + await testUtils.nextMicrotaskTick(); + + $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', + "Anonymous and Shy Guy are typing...", + "should no longer show than someone is typing on hover on this thread typing icon"); + assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), + "Anonymous and Shy Guy are typing...", + "should no longer show than someone is typing on the composer of active thread"); + + discuss.destroy(); }); -QUnit.test('long typing partner A and in-between short typing partner B', function (assert) { +QUnit.test('long typing partner A and in-between short typing partner B', async function (assert) { // Let's suppose that A is typing a very long message. If B types a short // and sends it, it should display that B is typing, remove B from display // when receiving the message, but still keep A in the typing notification. assert.expect(8); var self = this; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -549,7 +550,7 @@ QUnit.test('long typing partner A and in-between short typing partner B', functi // when receiving an 'is typing' notification, fetch the list of // members of this channel if we haven't done yet. if (args.method === 'channel_fetch_listeners') { - return $.when([ + return Promise.resolve([ { id: self.myPartnerID, name: self.myName }, { id: 42, name: "A" }, { id: 43, name: "B" }, @@ -558,76 +559,78 @@ QUnit.test('long typing partner A and in-between short typing partner B', functi return this._super.apply(this, arguments); }, session: { partner_id: this.myPartnerID }, - }).then(function (discuss) { - // click on general channel - var $general = discuss.$('.o_mail_discuss_sidebar .o_mail_discuss_item[data-thread-id=1]'); - testUtils.dom.click($general); - // pick 1st composer (basic), not 2nd composer (extended, hidden) - var $composer = discuss.$('.o_thread_composer').first(); - - self.simulateIsTyping({ - channelID: 1, - isTyping: true, - partnerID: 42, - widget: discuss, - }); - - $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.containsOnce($general, '.o_mail_thread_typing_icon', - "should have a thread typing icon next to general icon in the sidebar"); - assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', - "A is typing...", - "should show A is typing on hover on this thread typing icon"); - assert.containsOnce($composer, '.o_composer_thread_typing', - "should show typing info on the composer of active thread"); - assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), - "A is typing...", - "should show A is typing on the composer of active thread"); - - self.simulateIsTyping({ - channelID: 1, - isTyping: true, - partnerID: 43, - widget: discuss, - }); - - // B comes after A in the display, because A is oldest typing partner. - $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', - "A and B are typing...", - "should show A and B are typing on hover on this thread typing icon"); - assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), - "A and B are typing...", - "should show A and B are typing on the composer of active thread"); - - // Simulate receive message from B - var messageData = { - author_id: [43, 'Someone'], - body: "

test

", - channel_ids: [1], - id: 101, - model: 'mail.channel', - res_id: 1, - }; - var notification = [[false, 'mail.channel', 1], messageData]; - discuss.call('bus_service', 'trigger', 'notification', [notification]); - - $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', - "A is typing...", - "should show A is still typing on hover on this thread typing icon"); - assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), - "A is typing...", - "should show A is still typing on the composer of active thread"); - - discuss.destroy(); }); + // click on general channel + var $general = discuss.$('.o_mail_discuss_sidebar .o_mail_discuss_item[data-thread-id=1]'); + await testUtils.dom.click($general); + // pick 1st composer (basic), not 2nd composer (extended, hidden) + var $composer = discuss.$('.o_thread_composer').first(); + + self.simulateIsTyping({ + channelID: 1, + isTyping: true, + partnerID: 42, + widget: discuss, + }); + await testUtils.nextMicrotaskTick(); + + $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.containsOnce($general, '.o_mail_thread_typing_icon', + "should have a thread typing icon next to general icon in the sidebar"); + assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', + "A is typing...", + "should show A is typing on hover on this thread typing icon"); + assert.containsOnce($composer, '.o_composer_thread_typing', + "should show typing info on the composer of active thread"); + assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), + "A is typing...", + "should show A is typing on the composer of active thread"); + + self.simulateIsTyping({ + channelID: 1, + isTyping: true, + partnerID: 43, + widget: discuss, + }); + await testUtils.nextMicrotaskTick(); + + // B comes after A in the display, because A is oldest typing partner. + $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', + "A and B are typing...", + "should show A and B are typing on hover on this thread typing icon"); + assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), + "A and B are typing...", + "should show A and B are typing on the composer of active thread"); + + // Simulate receive message from B + var messageData = { + author_id: [43, 'Someone'], + body: "

test

", + channel_ids: [1], + id: 101, + model: 'mail.channel', + res_id: 1, + }; + var notification = [[false, 'mail.channel', 1], messageData]; + await discuss.call('bus_service', 'trigger', 'notification', [notification]); + await testUtils.nextTick(); + + $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + assert.hasAttrValue($general.find('.o_mail_thread_typing_icon'), 'title', + "A is typing...", + "should show A is still typing on hover on this thread typing icon"); + assert.strictEqual($composer.find('.o_composer_thread_typing').text().trim(), + "A is typing...", + "should show A is still typing on the composer of active thread"); + + discuss.destroy(); }); -QUnit.test('receive is typing notification from unselected thread', function (assert) { +QUnit.test('receive is typing notification from unselected thread', async function (assert) { assert.expect(7); this.data.initMessaging.channel_slots.channel_channel.push({ @@ -637,7 +640,7 @@ QUnit.test('receive is typing notification from unselected thread', function (as }); var self = this; - createDiscuss({ + var discuss = await createDiscuss({ id: 1, context: {}, params: {}, @@ -647,7 +650,7 @@ QUnit.test('receive is typing notification from unselected thread', function (as // when receiving an 'is typing' notification, fetch the list of // members of this channel if we haven't done yet. if (args.method === 'channel_fetch_listeners') { - return $.when([ + return Promise.resolve([ { id: self.myPartnerID, name: self.myName }, { id: 42, name: "Someone" }, ]); @@ -655,44 +658,44 @@ QUnit.test('receive is typing notification from unselected thread', function (as return this._super.apply(this, arguments); }, session: { partner_id: this.myPartnerID }, - }).then(function (discuss) { - var $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - var $other = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=2]'); - assert.strictEqual($general.length, 1, - "should have the channel item with id 1"); - assert.hasAttrValue($general, 'title', 'general', - "should have the title 'general'"); - assert.strictEqual($other.length, 1, - "should have the channel item with id 2"); - assert.hasAttrValue($other, 'title', 'other', - "should have the title 'other'"); - - // click on general - testUtils.dom.click($general); - - assert.containsNone($general, '.o_mail_thread_typing_icon', - "should no longer show someone is typing in sidebar of discuss"); - - self.simulateIsTyping({ - channelID: 2, - isTyping: true, - partnerID: 42, - widget: discuss, - }); - - $general = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=1]'); - $other = discuss.$('.o_mail_discuss_sidebar') - .find('.o_mail_discuss_item[data-thread-id=2]'); - assert.containsNone($general, '.o_mail_thread_typing_icon', - "should not have a thread typing icon next to general icon in the sidebar"); - assert.containsOnce($other, '.o_mail_thread_typing_icon', - "should have a thread typing icon next to other icon in the sidebar"); - - discuss.destroy(); }); + var $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + var $other = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=2]'); + assert.strictEqual($general.length, 1, + "should have the channel item with id 1"); + assert.hasAttrValue($general, 'title', 'general', + "should have the title 'general'"); + assert.strictEqual($other.length, 1, + "should have the channel item with id 2"); + assert.hasAttrValue($other, 'title', 'other', + "should have the title 'other'"); + + // click on general + await testUtils.dom.click($general); + + assert.containsNone($general, '.o_mail_thread_typing_icon', + "should no longer show someone is typing in sidebar of discuss"); + + self.simulateIsTyping({ + channelID: 2, + isTyping: true, + partnerID: 42, + widget: discuss, + }); + await testUtils.nextMicrotaskTick(); + + $general = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=1]'); + $other = discuss.$('.o_mail_discuss_sidebar') + .find('.o_mail_discuss_item[data-thread-id=2]'); + assert.containsNone($general, '.o_mail_thread_typing_icon', + "should not have a thread typing icon next to general icon in the sidebar"); + assert.containsOnce($other, '.o_mail_thread_typing_icon', + "should have a thread typing icon next to other icon in the sidebar"); + + discuss.destroy(); }); }); diff --git a/addons/mail/static/tests/document_thread_window_tests.js b/addons/mail/static/tests/document_thread_window_tests.js index 93095362306..51daf3a23b1 100644 --- a/addons/mail/static/tests/document_thread_window_tests.js +++ b/addons/mail/static/tests/document_thread_window_tests.js @@ -87,7 +87,7 @@ QUnit.module('Document Thread', { }, }); -QUnit.test('open a document thread in a thread window', function (assert) { +QUnit.test('open a document thread in a thread window', async function (assert) { assert.expect(6); var messagingMenu = new MessagingMenu(); @@ -107,13 +107,13 @@ QUnit.test('open a document thread in a thread window', function (assert) { return this._super.apply(this, arguments); }, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); // toggle the messaging menu and open the documentThread - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_mail_preview', "should display one preview"); - testUtils.dom.click(messagingMenu.$('.o_mail_preview')); + await testUtils.dom.click(messagingMenu.$('.o_mail_preview')); assert.strictEqual($('.o_thread_window').length, 1, "should have open the DocumentThread in a thread window"); @@ -125,7 +125,7 @@ QUnit.test('open a document thread in a thread window', function (assert) { messagingMenu.destroy(); }); -QUnit.test('expand a document thread window', function (assert) { +QUnit.test('expand a document thread window', async function (assert) { assert.expect(4); var messagingMenu = new MessagingMenu(); @@ -144,13 +144,13 @@ QUnit.test('expand a document thread window', function (assert) { }, }, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); // toggle the messaging menu and open the documentThread - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_mail_preview', "should display one preview"); - testUtils.dom.click(messagingMenu.$('.o_mail_preview')); + await testUtils.dom.click(messagingMenu.$('.o_mail_preview')); assert.strictEqual($('.o_thread_window').length, 1, "should have open the DocumentThread in a thread window"); @@ -158,12 +158,12 @@ QUnit.test('expand a document thread window', function (assert) { 'Open document', "button should have correct title"); // click to expand - testUtils.dom.click($('.o_thread_window .o_thread_window_expand')); + await testUtils.dom.click($('.o_thread_window .o_thread_window_expand')); messagingMenu.destroy(); }); -QUnit.test('post messages in a document thread window', function (assert) { +QUnit.test('post messages in a document thread window', async function (assert) { assert.expect(8); var newMessageBody = 'Some message'; @@ -193,7 +193,7 @@ QUnit.test('post messages in a document thread window', function (assert) { // add the message to the fake DB this.data['mail.message'].records.push(newMessage); this.data['some.res.model'].records[0].message_ids.push(6783); - return $.when(6783); + return Promise.resolve(6783); } return this._super.apply(this, arguments); }, @@ -205,73 +205,74 @@ QUnit.test('post messages in a document thread window', function (assert) { "should write sent message in local storage, to share info with other tabs"); } }, true); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); // toggle the messaging menu and open the documentThread - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_mail_preview', "should display one preview"); - testUtils.dom.click(messagingMenu.$('.o_mail_preview')); + await testUtils.dom.click(messagingMenu.$('.o_mail_preview')); assert.strictEqual($('.o_thread_window').length, 1, "should have open the DocumentThread in a thread window"); assert.strictEqual($('.o_thread_window .o_thread_message').length, 2, "should display 2 messages in the thread window"); // post a message - $('.o_thread_window .o_composer_text_field') - .val(newMessageBody) - .trigger($.Event('keydown', {which: $.ui.keyCode.ENTER})); - + await testUtils.fields.editAndTrigger($('.o_thread_window .o_composer_text_field'), newMessageBody, + [$.Event('keydown', {which: $.ui.keyCode.ENTER})]); assert.strictEqual($('.o_thread_window .o_thread_message').length, 3, "should display 3 messages in the thread window"); messagingMenu.destroy(); }); -QUnit.test('open, fold, unfold and close a document thread window', function (assert) { +QUnit.test('open, fold, unfold and close a document thread window', async function (assert) { assert.expect(8); - var messagingMenu = new MessagingMenu(); testUtils.mock.addMockEnvironment(messagingMenu, { services: this.services, data: this.data, session: this.session, }); + await testUtils.nextTick(); testUtils.mock.intercept(messagingMenu, 'call_service', function (ev) { if (ev.data.service === 'local_storage' && ev.data.method === 'setItem') { - assert.step(ev.data.args); + assert.step(JSON.stringify(ev.data.args)); } }, true); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); // toggle the messaging menu and open the documentThread - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_mail_preview', "should display one preview"); - testUtils.dom.click(messagingMenu.$('.o_mail_preview')); + await testUtils.dom.click(messagingMenu.$('.o_mail_preview')); assert.strictEqual($('.o_thread_window').length, 1, "should have open the DocumentThread in a thread window"); assert.strictEqual($('.o_thread_window .o_thread_message').length, 2, "should display 2 messages in the thread window"); // fold and unfold thread window - testUtils.dom.click($('.o_thread_window .o_thread_window_title')); - testUtils.dom.click($('.o_thread_window .o_thread_window_title')); + await testUtils.dom.click($('.o_thread_window .o_thread_window_title')); + await testUtils.nextTick(); + await testUtils.dom.click($('.o_thread_window .o_thread_window_title')); + await testUtils.nextTick(); // close thread window - testUtils.dom.click($('.o_thread_window .o_thread_window_close')); + await testUtils.dom.click($('.o_thread_window .o_thread_window_close')); + await testUtils.nextTick(); assert.verifySteps([ - ['mail.document_threads_state', {"some.res.model_1": {"name": "Some Record", "windowState": "open"}}], - ['mail.document_threads_state', {"some.res.model_1": {"name": "Some Record", "windowState": "folded"}}], - ['mail.document_threads_state', {"some.res.model_1": {"name": "Some Record", "windowState": "open"}}], - ['mail.document_threads_state', {"some.res.model_1": {"name": "Some Record", "windowState": "closed"}}], + '["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"open"}}]', + '["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"folded"}}]', + '["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"open"}}]', + '["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"closed"}}]', ]); messagingMenu.destroy(); }); -QUnit.test('do not open thread window on fetch message failure', function (assert) { +QUnit.test('do not open thread window on fetch message failure', async function (assert) { // this may happen when the user receives a notification from a document // that he does not have access rights at the moment. assert.expect(4); @@ -284,17 +285,17 @@ QUnit.test('do not open thread window on fetch message failure', function (asser mockRPC: function (route, args) { if (args.method === 'read' && args.model === 'some.res.model' && args.args[0][0] === 1) { assert.step('some.res.model/1/read'); - return $.Deferred().reject(); // simulate failure + return Promise.reject(); // simulate failure } return this._super.apply(this, arguments); }, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_mail_preview'); - testUtils.dom.click(messagingMenu.$('.o_mail_preview')); + await testUtils.dom.click(messagingMenu.$('.o_mail_preview')); assert.verifySteps(['some.res.model/1/read']); assert.strictEqual($('.o_thread_window').length, 0, "should not have open the DocumentThread in a thread window on fetch messages failure"); diff --git a/addons/mail/static/tests/document_viewer_tests.js b/addons/mail/static/tests/document_viewer_tests.js index 87fa2dbed94..5d3dcf7069f 100644 --- a/addons/mail/static/tests/document_viewer_tests.js +++ b/addons/mail/static/tests/document_viewer_tests.js @@ -20,16 +20,16 @@ var createViewer = function (params) { var mockRPC = function (route) { if (route === '/web/static/lib/pdfjs/web/viewer.html?file=/web/content/1?model%3Dir.attachment') { - return $.when(); + return Promise.resolve(); } if (route === 'https://www.youtube.com/embed/FYqW0Gdwbzk') { - return $.when(); + return Promise.resolve(); } if (route === '/web/content/4?model%3Dir.attachment') { - return $.when(); + return Promise.resolve(); } if (route === '/web/image/6?unique=1&signature=999&model=ir.attachment') { - return $.when(); + return Promise.resolve(); } }; testUtils.mock.addMockEnvironment(parent, { @@ -51,14 +51,15 @@ var createViewer = function (params) { $target = $('body'); $target.addClass('debug'); } - viewer.appendTo($target); // actually destroy the parent when the viewer is destroyed viewer.destroy = function () { delete viewer.destroy; parent.destroy(); }; - return viewer; + return viewer.appendTo($target).then(function() { + return viewer; + }); }; QUnit.module('DocumentViewer', { @@ -74,10 +75,10 @@ QUnit.module('DocumentViewer', { }, }, function () { - QUnit.test('basic rendering', function (assert) { + QUnit.test('basic rendering', async function (assert) { assert.expect(7); - var viewer = createViewer({ + var viewer = await createViewer({ attachmentID: 1, attachments: this.attachments, }); @@ -100,11 +101,11 @@ QUnit.module('DocumentViewer', { viewer.destroy(); }); - QUnit.test('Document Viewer Youtube', function (assert) { + QUnit.test('Document Viewer Youtube', async function (assert) { assert.expect(3); var youtubeURL = 'https://www.youtube.com/embed/FYqW0Gdwbzk'; - var viewer = createViewer({ + var viewer = await createViewer({ attachmentID: 2, attachments: this.attachments, mockRPC: function (route) { @@ -123,10 +124,10 @@ QUnit.module('DocumentViewer', { viewer.destroy(); }); - QUnit.test('Document Viewer html/(txt)', function (assert) { + QUnit.test('Document Viewer html/(txt)', async function (assert) { assert.expect(2); - var viewer = createViewer({ + var viewer = await createViewer({ attachmentID: 4, attachments: this.attachments, }); @@ -139,10 +140,10 @@ QUnit.module('DocumentViewer', { viewer.destroy(); }); - QUnit.test('Document Viewer mp4', function (assert) { + QUnit.test('Document Viewer mp4', async function (assert) { assert.expect(2); - var viewer = createViewer({ + var viewer = await createViewer({ attachmentID: 5, attachments: this.attachments, }); @@ -155,10 +156,10 @@ QUnit.module('DocumentViewer', { viewer.destroy(); }); - QUnit.test('Document Viewer jpg', function (assert) { + QUnit.test('Document Viewer jpg', async function (assert) { assert.expect(2); - var viewer = createViewer({ + var viewer = await createViewer({ attachmentID: 6, attachments: this.attachments, }); @@ -171,10 +172,10 @@ QUnit.module('DocumentViewer', { viewer.destroy(); }); - QUnit.test('is closable by button', function (assert) { + QUnit.test('is closable by button', async function (assert) { assert.expect(3); - var viewer = createViewer({ + var viewer = await createViewer({ attachmentID: 6, attachments: this.attachments, }); @@ -184,15 +185,15 @@ QUnit.module('DocumentViewer', { assert.containsOnce(viewer, '.o_close_btn', "should have a close button"); - testUtils.dom.click(viewer.$('.o_close_btn')); + await testUtils.dom.click(viewer.$('.o_close_btn')); assert.ok(viewer.isDestroyed(), 'viewer should be destroyed'); }); - QUnit.test('is closable by clicking on the wrapper', function (assert) { + QUnit.test('is closable by clicking on the wrapper', async function (assert) { assert.expect(3); - var viewer = createViewer({ + var viewer = await createViewer({ attachmentID: 6, attachments: this.attachments, }); @@ -202,7 +203,7 @@ QUnit.module('DocumentViewer', { assert.containsOnce(viewer, '.o_viewer_img_wrapper', "should have a wrapper"); - testUtils.dom.click(viewer.$('.o_viewer_img_wrapper')); + await testUtils.dom.click(viewer.$('.o_viewer_img_wrapper')); assert.ok(viewer.isDestroyed(), 'viewer should be destroyed'); }); diff --git a/addons/mail/static/tests/helpers/mock_server.js b/addons/mail/static/tests/helpers/mock_server.js index 54ceae92ce3..32203db5dfb 100644 --- a/addons/mail/static/tests/helpers/mock_server.js +++ b/addons/mail/static/tests/helpers/mock_server.js @@ -183,29 +183,29 @@ MockServer.include({ _performRpc: function (route, args) { // routes if (route === '/mail/init_messaging') { - return $.when(this._mockInitMessaging(args)); + return Promise.resolve(this._mockInitMessaging(args)); } // methods if (args.method === 'channel_fetch_listeners') { - return $.when([]); + return Promise.resolve([]); } if (args.method === 'channel_fetch_preview') { - return $.when(this._mockChannelFetchPreview(args)); + return Promise.resolve(this._mockChannelFetchPreview(args)); } if (args.method === 'channel_minimize') { - return $.when(); + return Promise.resolve(); } if (args.method === 'channel_seen') { - return $.when(); + return Promise.resolve(); } if (args.method === 'channel_fetched') { - return $.when(); + return Promise.resolve(); } if (args.method === 'message_fetch') { - return $.when(this._mockMessageFetch(args)); + return Promise.resolve(this._mockMessageFetch(args)); } if (args.method === 'message_format') { - return $.when(this._mockMessageFormat(args)); + return Promise.resolve(this._mockMessageFormat(args)); } if (args.method === 'activity_format') { var res = this._mockRead(args.model, args.args, args.kwargs); @@ -217,19 +217,19 @@ MockServer.include({ } return record; }); - return $.when(res); + return Promise.resolve(res); } if (args.method === 'set_message_done') { - return $.when(this._mockSetMessageDone(args)); + return Promise.resolve(this._mockSetMessageDone(args)); } if (args.method === 'moderate') { - return $.when(this._mockModerate(args)); + return Promise.resolve(this._mockModerate(args)); } if (args.method === 'notify_typing') { - return $.when(); + return Promise.resolve(); } if (args.method === 'set_message_done') { - return $.when(); + return Promise.resolve(); } return this._super(route, args); }, diff --git a/addons/mail/static/tests/helpers/test_utils.js b/addons/mail/static/tests/helpers/test_utils.js index 063e3a9eb03..af86d3c2e45 100644 --- a/addons/mail/static/tests/helpers/test_utils.js +++ b/addons/mail/static/tests/helpers/test_utils.js @@ -26,9 +26,9 @@ var Widget = require('web.Widget'); * This is async due to mail_manager/mail_service that needs to be ready. * * @param {Object} params - * @return {$.Promise} resolved with the discuss widget + * @return {Promise} resolved with the discuss widget */ -function createDiscuss(params) { +async function createDiscuss(params) { var Parent = Widget.extend({ do_push_state: function () {}, }); @@ -49,7 +49,7 @@ function createDiscuss(params) { parent.destroy(); }; - return discuss.appendTo($(selector)).then(function () { + return discuss.appendTo($(selector)).then(function () { return discuss; }); } diff --git a/addons/mail/static/tests/mail_status_manager_tests.js b/addons/mail/static/tests/mail_status_manager_tests.js index a1ae329c9dc..be769e8d0b0 100644 --- a/addons/mail/static/tests/mail_status_manager_tests.js +++ b/addons/mail/static/tests/mail_status_manager_tests.js @@ -38,7 +38,7 @@ QUnit.test('simple set im_status', function (assert) { parent.destroy(); }); -QUnit.test('multi get_im_status', function (assert) { +QUnit.test('multi get_im_status', async function (assert) { assert.expect(9); var readCount = 0; var parent = testUtils.createParent({ @@ -52,7 +52,7 @@ QUnit.test('multi get_im_status', function (assert) { assert.deepEqual(args.args[0], [2,3]); assert.deepEqual(args.args[1], ['id', 'im_status']); readCount++; - return $.when([ + return Promise.resolve([ {id: 2, im_status: 'away'}, {id: 3, im_status: 'im_partner'} ]); @@ -64,22 +64,27 @@ QUnit.test('multi get_im_status', function (assert) { id: 1, im_status: 'online', }]); + await testUtils.nextTick(); assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 1 }), 'online'); assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 2 }), undefined); assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 3 }), undefined); this.timeoutMock.runPendingTimeouts(); + await testUtils.nextTick(); + assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 1 }), 'online'); assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 2 }), 'away'); assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 3 }), 'im_partner'); this.timeoutMock.runPendingTimeouts(); + await testUtils.nextTick(); + assert.strictEqual(readCount, 1, 'Only one read on partner should have been performed'); parent.destroy(); }); -QUnit.test('update loop', function (assert) { +QUnit.test('update loop', async function (assert) { assert.expect(12); var readCount = 0; var parent = testUtils.createParent({ @@ -92,7 +97,7 @@ QUnit.test('update loop', function (assert) { assert.deepEqual(args.args[0], [1, 2]); assert.deepEqual(args.args[1], ['id', 'im_status']); readCount++; - return $.when([ + return Promise.resolve([ {"id": 1, "im_status": "online"}, {"id": 2, "im_status": "away"} ]); @@ -106,11 +111,14 @@ QUnit.test('update loop', function (assert) { { id: 2, im_status: 'offline' }, { id: 3, im_status: 'im_partner' }, //shouldn't be updated !!!! ]); + await testUtils.nextTick(); //_updateImStatusLoop should be running at one second per iteration, lets make a minute pass. assert.strictEqual(readCount, 0); this.timeoutMock.addTime(50*1000); + await testUtils.nextTick(); assert.strictEqual(readCount, 0); this.timeoutMock.addTime(1000); + await testUtils.nextTick(); assert.strictEqual(readCount, 1, 'one call should have been made after 50 seconds' ); assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 1 }), 'online'); assert.strictEqual(parent.call('mail_service', 'getImStatus', { partnerID: 2 }), 'away'); @@ -118,21 +126,27 @@ QUnit.test('update loop', function (assert) { //simulate change of focus //original listener: $(window).on("blur", this._onWindowFocusChange.bind(this, false); + unload, ... parent.call('mail_service', '_onWindowFocusChange', false); // remove focus from tab + await testUtils.nextTick(); this.timeoutMock.addTime(5*60*1000); // x minutes without focus, no rpc should be done during this time + await testUtils.nextTick(); assert.strictEqual(readCount, 1, 'No more call should have been performed'); //simulate change of focus //original listener: $(window).on("focus", this._onWindowFocusChange.bind(this, true); parent.call('mail_service', '_onWindowFocusChange', true); // give focus to tab + await testUtils.nextTick(); var nextUpdateDelay = this.timeoutMock.getNextTimeoutDelay(); + await testUtils.nextTick(); assert.strictEqual(nextUpdateDelay, 1000, "next update should be done in maximum one second"); this.timeoutMock.addTime(nextUpdateDelay); // one second should be enough + await testUtils.nextTick(); assert.strictEqual(readCount, 2, 'One more call should have been done once tab focused'); this.timeoutMock.runPendingTimeouts(); + await testUtils.nextTick(); parent.destroy(); }); -QUnit.test('update status', function (assert) { +QUnit.test('update status', async function (assert) { // the current solution to look for updatable im_status in dom is not perfect, but waiting for // ability to include widgets in views, this is the most simple solution assert.expect(2); @@ -159,7 +173,7 @@ QUnit.test('update status', function (assert) { throw new Error(_.str.sprintf('No rpc call should be performed: %s, %s \n %s', args.model, args.method, route)); }, }); - statusWidget.appendTo($('#qunit-fixture')); + await statusWidget.appendTo($('#qunit-fixture')); //Render unknow im_status: // set initial status assert.ok(statusWidget.$('.o_updatable_im_status i').hasClass('o_user_online')); @@ -167,6 +181,7 @@ QUnit.test('update status', function (assert) { statusWidget.call('mail_service', 'updateImStatus', [ { id: 1, im_status: 'offline' }, ]); + await testUtils.nextTick(); assert.notOk(statusWidget.$('.o_updatable_im_status i').hasClass('o_user_online')); this.timeoutMock.runPendingTimeouts(); statusWidget.destroy(); diff --git a/addons/mail/static/tests/models/cc_throttle_function_tests.js b/addons/mail/static/tests/models/cc_throttle_function_tests.js index f5deb6c1f3c..5ab00ef6048 100644 --- a/addons/mail/static/tests/models/cc_throttle_function_tests.js +++ b/addons/mail/static/tests/models/cc_throttle_function_tests.js @@ -10,7 +10,7 @@ QUnit.module('mail', { beforeEach: function () { var self = this; - this.timeoutCDDef = $.Deferred(); + this.timeoutCDDef = testUtils.makeTestPromise(); this.patch = function () { self.ORIGINAL_CCTFO_ON_CD_TIMEOUT = CCThrottleFunctionObject.prototype._onCooldownTimeout; @@ -36,7 +36,7 @@ QUnit.test('cancel()', function (assert) { assert.expect(3); var self = this; - var def = $.Deferred(); + var def = testUtils.makeTestPromise(); var step = 1; var widget = testUtils.createParent({ @@ -52,7 +52,7 @@ QUnit.test('cancel()', function (assert) { } step++; - return $.when(); + return Promise.resolve(); } return this._super.apply(this, arguments); }, @@ -80,7 +80,7 @@ QUnit.test('cancel()', function (assert) { self.timeoutCDDef.resolve(); }); this.timeoutCDDef.then(function () { - assert.verifySteps(['__rpc__1'], + assert.verifySteps([], "function should still have been called once after 2nd function call cancelled"); widget.destroy(); done(); @@ -92,8 +92,8 @@ QUnit.test('clear()', function (assert) { assert.expect(5); var self = this; - var def1 = $.Deferred(); - var def2 = $.Deferred(); + var def1 = testUtils.makeTestPromise(); + var def2 = testUtils.makeTestPromise(); var step = 1; var widget = testUtils.createParent({ @@ -110,7 +110,7 @@ QUnit.test('clear()', function (assert) { } step++; - return $.when(); + return Promise.resolve(); } return this._super.apply(this, arguments); }, @@ -135,14 +135,14 @@ QUnit.test('clear()', function (assert) { }).then(function () { cctFunc(2); }).then(function () { - assert.verifySteps(['__rpc__1'], + assert.verifySteps([], "function should still have been called once (due to long throttle)"); cctFunc.clear(); self.timeoutCDDef.resolve(); def2.resolve(); }); - $.when(self.timeoutCDDef, def2).then(function () { - assert.verifySteps(['__rpc__1', '__rpc__2'], + Promise.all([self.timeoutCDDef, def2]).then(function () { + assert.verifySteps(['__rpc__2'], "function should have been called twice after 'clear' (buffered 2nd function call)"); widget.destroy(); done(); diff --git a/addons/mail/static/tests/models/timer_tests.js b/addons/mail/static/tests/models/timer_tests.js index 535c5649ec9..9a66e80ad22 100644 --- a/addons/mail/static/tests/models/timer_tests.js +++ b/addons/mail/static/tests/models/timer_tests.js @@ -1,31 +1,27 @@ odoo.define('mail.model.TimerTests', function (require) { "use strict"; +var testUtils = require("web.test_utils"); var Timer = require('mail.model.Timer'); QUnit.module('mail', {}, function () { QUnit.module('model', {}, function () { QUnit.module('Timer'); -QUnit.test('start()', function (assert) { - var done = assert.async(); +QUnit.test('start()', async function (assert) { assert.expect(2); - var def = $.Deferred(); - var func = function () { - def.resolve(); - }; + var prom = testUtils.makeTestPromise(); var t = new Timer({ duration: 0, - onTimeout: func, + onTimeout: prom.resolve, }); assert.verifySteps([], "should not have called the function"); t.start(); - def.then(function () { + await prom.then(function () { assert.ok(true, "should have called the function"); - done(); }); }); @@ -51,11 +47,10 @@ QUnit.test('clear()', function (assert) { }, 0); }); -QUnit.test('reset()', function (assert) { - var done = assert.async(); +QUnit.test('reset()', async function (assert) { assert.expect(4); - var def = $.Deferred(); + var prom = testUtils.makeTestPromise(); var func = function () { assert.step('function_called'); }; @@ -67,19 +62,17 @@ QUnit.test('reset()', function (assert) { t.start(); // Called after 1st timer timeout + await testUtils.nextTick(); + assert.verifySteps(['function_called'], "should have called the function once"); + t.reset(); + // Called after 2nd timer timeout setTimeout(function () { - assert.verifySteps(['function_called'], "should have called the function once"); - t.reset(); - // Called after 2nd timer timeout - setTimeout(function () { - def.resolve(); - }, 0); + prom.resolve(); }, 0); - def.then(function () { - assert.verifySteps(['function_called', 'function_called'], + await prom.then(function () { + assert.verifySteps(['function_called'], "should have called the function twice (2nd time from reset)"); - done(); }); }); diff --git a/addons/mail/static/tests/systray/systray_activity_menu_tests.js b/addons/mail/static/tests/systray/systray_activity_menu_tests.js index abf4dc6daf7..b2f76feb832 100644 --- a/addons/mail/static/tests/systray/systray_activity_menu_tests.js +++ b/addons/mail/static/tests/systray/systray_activity_menu_tests.js @@ -76,7 +76,7 @@ QUnit.module('ActivityMenu', { } }); -QUnit.test('activity menu widget: menu with no records', function (assert) { +QUnit.test('activity menu widget: menu with no records', async function (assert) { assert.expect(1); var activityMenu = new ActivityMenu(); @@ -84,17 +84,18 @@ QUnit.test('activity menu widget: menu with no records', function (assert) { services: this.services, mockRPC: function (route, args) { if (args.method === 'systray_get_activities') { - return $.when([]); + return Promise.resolve([]); } return this._super(route, args); }, }); - activityMenu.appendTo($('#qunit-fixture')); - assert.hasClass(activityMenu.$('.o_no_activity'),'o_no_activity', "should not have instance of widget"); + await activityMenu.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); + assert.containsOnce(activityMenu, '.o_no_activity'); activityMenu.destroy(); }); -QUnit.test('activity menu widget: activity menu with 3 records', function (assert) { +QUnit.test('activity menu widget: activity menu with 3 records', async function (assert) { assert.expect(10); var self = this; var activityMenu = new ActivityMenu(); @@ -102,12 +103,13 @@ QUnit.test('activity menu widget: activity menu with 3 records', function (asser services: this.services, mockRPC: function (route, args) { if (args.method === 'systray_get_activities') { - return $.when(self.data['mail.activity.menu']['records']); + return Promise.resolve(self.data['mail.activity.menu']['records']); } return this._super(route, args); }, }); - activityMenu.appendTo($('#qunit-fixture')); + await activityMenu.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); assert.hasClass(activityMenu.$el, 'o_mail_systray_item', 'should be the instance of widget'); // the assertion below has not been replace because there are includes of ActivityMenu that modify the length. assert.ok(activityMenu.$('.o_mail_preview').length); @@ -123,34 +125,34 @@ QUnit.test('activity menu widget: activity menu with 3 records', function (asser context = { search_default_activities_overdue: 1, }; - testUtils.dom.click(activityMenu.$('.dropdown-toggle')); + await testUtils.dom.click(activityMenu.$('.dropdown-toggle')); assert.hasClass(activityMenu.$el, 'show', 'ActivityMenu should be open'); - testUtils.dom.click(activityMenu.$(".o_activity_filter_button[data-model_name='Issue'][data-filter='overdue']")); + await testUtils.dom.click(activityMenu.$(".o_activity_filter_button[data-model_name='Issue'][data-filter='overdue']")); assert.doesNotHaveClass(activityMenu.$el, 'show', 'ActivityMenu should be closed'); // case 2: click on "today" context = { search_default_activities_today: 1, }; - testUtils.dom.click(activityMenu.$('.dropdown-toggle')); - testUtils.dom.click(activityMenu.$(".o_activity_filter_button[data-model_name='Issue'][data-filter='today']")); + await testUtils.dom.click(activityMenu.$('.dropdown-toggle')); + await testUtils.dom.click(activityMenu.$(".o_activity_filter_button[data-model_name='Issue'][data-filter='today']")); // case 3: click on "future" context = { search_default_activities_upcoming_all: 1, }; - testUtils.dom.click(activityMenu.$('.dropdown-toggle')); - testUtils.dom.click(activityMenu.$(".o_activity_filter_button[data-model_name='Issue'][data-filter='upcoming_all']")); + await testUtils.dom.click(activityMenu.$('.dropdown-toggle')); + await testUtils.dom.click(activityMenu.$(".o_activity_filter_button[data-model_name='Issue'][data-filter='upcoming_all']")); // case 4: click anywere else context = { search_default_activities_overdue: 1, search_default_activities_today: 1, }; - testUtils.dom.click(activityMenu.$('.dropdown-toggle')); - testUtils.dom.click(activityMenu.$(".o_mail_systray_dropdown_items > div[data-model_name='Issue']")); + await testUtils.dom.click(activityMenu.$('.dropdown-toggle')); + await testUtils.dom.click(activityMenu.$(".o_mail_systray_dropdown_items > div[data-model_name='Issue']")); activityMenu.destroy(); }); -QUnit.test('activity menu widget: activity view icon', function (assert) { +QUnit.test('activity menu widget: activity view icon', async function (assert) { assert.expect(10); var self = this; var activityMenu = new ActivityMenu(); @@ -158,12 +160,13 @@ QUnit.test('activity menu widget: activity view icon', function (assert) { services: this.services, mockRPC: function (route, args) { if (args.method === 'systray_get_activities') { - return $.when(self.data['mail.activity.menu'].records); + return Promise.resolve(self.data['mail.activity.menu'].records); } return this._super(route, args); }, }); - activityMenu.appendTo($('#qunit-fixture')); + await activityMenu.appendTo($('#qunit-fixture')); + await testUtils.nextTick(); assert.containsN(activityMenu, '.o_mail_activity_action', 2, "widget should have 2 activity view icons"); @@ -189,11 +192,11 @@ QUnit.test('activity menu widget: activity view icon', function (assert) { }, true); // click on the "Issue" activity icon - testUtils.dom.click(activityMenu.$('.dropdown-toggle')); - testUtils.dom.click(activityMenu.$(".o_mail_activity_action[data-model_name='Issue']")); + await testUtils.dom.click(activityMenu.$('.dropdown-toggle')); + await testUtils.dom.click(activityMenu.$(".o_mail_activity_action[data-model_name='Issue']")); // click on the "Note" activity icon - testUtils.dom.click(activityMenu.$(".o_mail_activity_action[data-model_name='Note']")); + await testUtils.dom.click(activityMenu.$(".o_mail_activity_action[data-model_name='Note']")); assert.verifySteps([ 'do_action:Issue', diff --git a/addons/mail/static/tests/systray/systray_messaging_menu_mail_failure_tests.js b/addons/mail/static/tests/systray/systray_messaging_menu_mail_failure_tests.js index c03259697cd..f2c8e95ac29 100644 --- a/addons/mail/static/tests/systray/systray_messaging_menu_mail_failure_tests.js +++ b/addons/mail/static/tests/systray/systray_messaging_menu_mail_failure_tests.js @@ -66,7 +66,7 @@ QUnit.module('MessagingMenu (Mail Failures)', { } }); -QUnit.test('preview of mail failure', function (assert) { +QUnit.test('preview of mail failure', async function (assert) { assert.expect(4); this.data['mail.message'].records = [{ @@ -86,8 +86,8 @@ QUnit.test('preview of mail failure', function (assert) { data: this.data, services: this.services, }); - messagingMenu.appendTo($('#qunit-fixture')); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_mail_preview', "should display one preview for the mail failure"); @@ -101,7 +101,7 @@ QUnit.test('preview of mail failure', function (assert) { messagingMenu.destroy(); }); -QUnit.test('preview grouped failures by document', function (assert) { +QUnit.test('preview grouped failures by document', async function (assert) { // If some failures linked to a document refers to a same document, a single // preview should group all those failures assert.expect(3); @@ -142,8 +142,8 @@ QUnit.test('preview grouped failures by document', function (assert) { data: this.data, services: this.services, }); - messagingMenu.appendTo($('#qunit-fixture')); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsN(messagingMenu, '.o_mail_preview', 2, "should have a two messaging previews for the mail failures"); @@ -156,7 +156,7 @@ QUnit.test('preview grouped failures by document', function (assert) { messagingMenu.destroy(); }); -QUnit.test('preview grouped failures by document model', function (assert) { +QUnit.test('preview grouped failures by document model', async function (assert) { // If all failures linked to a document model refers to different documents, // a single preview should group all failures that are linked to this // document model @@ -198,8 +198,8 @@ QUnit.test('preview grouped failures by document model', function (assert) { data: this.data, services: this.services, }); - messagingMenu.appendTo($('#qunit-fixture')); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsN(messagingMenu, '.o_mail_preview', 2, "should have a two messaging previews for the mail failures"); diff --git a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js index 00abb2d0503..f3ae277ee56 100644 --- a/addons/mail/static/tests/systray/systray_messaging_menu_tests.js +++ b/addons/mail/static/tests/systray/systray_messaging_menu_tests.js @@ -101,7 +101,7 @@ QUnit.module('MessagingMenu', { } }); -QUnit.test('messaging menu widget: menu with no records', function (assert) { +QUnit.test('messaging menu widget: menu with no records', async function (assert) { assert.expect(1); var messagingMenu = new MessagingMenu(); @@ -109,27 +109,27 @@ QUnit.test('messaging menu widget: menu with no records', function (assert) { services: this.services, mockRPC: function (route, args) { if (args.method === 'message_fetch') { - return $.when([]); + return Promise.resolve([]); } return this._super.apply(this, arguments); } }); - messagingMenu.appendTo($('#qunit-fixture')); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.hasClass(messagingMenu.$('.o_no_activity'),'o_no_activity', "should not have instance of widget"); messagingMenu.destroy(); }); -QUnit.test('messaging menu widget: messaging menu with 1 record', function (assert) { +QUnit.test('messaging menu widget: messaging menu with 1 record', async function (assert) { assert.expect(3); var messagingMenu = new MessagingMenu(); testUtils.mock.addMockEnvironment(messagingMenu, { services: this.services, data: this.data, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_mail_preview', "should display a preview"); @@ -146,7 +146,7 @@ QUnit.test('messaging menu widget: messaging menu with 1 record', function (asse messagingMenu.destroy(); }); -QUnit.test('messaging menu widget: open inbox for needaction not linked to any document', function (assert) { +QUnit.test('messaging menu widget: open inbox for needaction not linked to any document', async function (assert) { assert.expect(4); var messagingMenu = new MessagingMenu(); @@ -157,7 +157,7 @@ QUnit.test('messaging menu widget: open inbox for needaction not linked to any d partner_id: 1, }, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); // Simulate received needaction message without associated document, // so that we have a message in inbox without a model and a resID @@ -174,7 +174,7 @@ QUnit.test('messaging menu widget: open inbox for needaction not linked to any d messagingMenu.call('bus_service', 'trigger', 'notification', notifications); // Open messaging menu - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); var $firstChannelPreview = messagingMenu.$('.o_mail_preview').first(); @@ -190,7 +190,7 @@ QUnit.test('messaging menu widget: open inbox for needaction not linked to any d assert.step('do_action:' + ev.data.action + ':' + ev.data.options.active_id); } }, true); - testUtils.dom.click($firstChannelPreview); + await testUtils.dom.click($firstChannelPreview); assert.verifySteps( ['do_action:mail.action_discuss:mailbox_inbox'], "should open Discuss with Inbox"); @@ -198,7 +198,7 @@ QUnit.test('messaging menu widget: open inbox for needaction not linked to any d messagingMenu.destroy(); }); -QUnit.test("messaging menu widget: mark as read on thread preview", function ( assert ) { +QUnit.test("messaging menu widget: mark as read on thread preview", async function ( assert ) { assert.expect(8); testUtils.mock.patch(DocumentThread, { @@ -230,8 +230,8 @@ QUnit.test("messaging menu widget: mark as read on thread preview", function ( a data: this.data, }); - messagingMenu.appendTo($('#qunit-fixture')); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.hasClass(messagingMenu.$el,'o_mail_systray_item', 'should be the instance of widget'); assert.hasClass(messagingMenu.$el,'show', @@ -247,7 +247,7 @@ QUnit.test("messaging menu widget: mark as read on thread preview", function ( a assert.containsOnce(messagingMenu, '.o_mail_preview_mark_as_read', "should have mark as read icon next to preview"); - testUtils.dom.click(messagingMenu.$(".o_mail_preview_mark_as_read")); + await testUtils.dom.click(messagingMenu.$(".o_mail_preview_mark_as_read")); assert.verifySteps(['markedAsRead'], "the document thread should be marked as read"); @@ -256,7 +256,7 @@ QUnit.test("messaging menu widget: mark as read on thread preview", function ( a messagingMenu.destroy(); }); -QUnit.test('needaction messages in channels should appear, in addition to channel preview', function (assert) { +QUnit.test('needaction messages in channels should appear, in addition to channel preview', async function (assert) { // Let's suppose a channel whose before-last message (msg1) is a needaction, // but not the last message (msg2). In that case, the systray messaging // menu should display the needaction message msg1 in the preview, and the @@ -295,9 +295,9 @@ QUnit.test('needaction messages in channels should appear, in addition to channe partner_id: partnerID, }, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsN(messagingMenu, '.o_mail_preview', 2, "should display two previews"); @@ -314,7 +314,7 @@ QUnit.test('needaction messages in channels should appear, in addition to channe messagingMenu.destroy(); }); -QUnit.test('preview of message on a document + mark as read', function (assert) { +QUnit.test('preview of message on a document + mark as read', async function (assert) { assert.expect(7); this.data.initMessaging = { @@ -351,11 +351,11 @@ QUnit.test('preview of message on a document + mark as read', function (assert) partner_id: partnerID, }, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); assert.strictEqual(messagingMenu.$('.o_notification_counter').text(), '1', "should display a counter of 1 on the messaging menu icon"); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_mail_preview', "should display one preview"); @@ -367,7 +367,7 @@ QUnit.test('preview of message on a document + mark as read', function (assert) assert.strictEqual(messagingMenu.$('.o_preview_unread .o_last_message_preview').text().replace(/\s/g, ''), "Demo:*MessageOnDocument*", "should correctly display the preview"); - testUtils.dom.click(messagingMenu.$('.o_mail_preview_mark_as_read')); + await testUtils.dom.click(messagingMenu.$('.o_mail_preview_mark_as_read')); assert.strictEqual(messagingMenu.$('.o_notification_counter').text(), '0', "should display a counter of 0 on the messaging menu icon"); assert.containsNone(messagingMenu, '.o_mail_preview', @@ -376,7 +376,7 @@ QUnit.test('preview of message on a document + mark as read', function (assert) messagingMenu.destroy(); }); -QUnit.test('update messaging preview on receiving a new message in channel preview', function (assert) { +QUnit.test('update messaging preview on receiving a new message in channel preview', async function (assert) { assert.expect(8); var messagingMenu = new MessagingMenu(); @@ -384,9 +384,9 @@ QUnit.test('update messaging preview on receiving a new message in channel previ services: this.services, data: this.data, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_mail_preview', "should display a single channel preview"); @@ -410,7 +410,7 @@ QUnit.test('update messaging preview on receiving a new message in channel previ }; var notification = [[false, 'mail.channel', 1], data]; messagingMenu.call('bus_service', 'trigger', 'notification', [notification]); - + await testUtils.nextTick(); assert.containsOnce(messagingMenu, '.o_mail_preview', "should still display a single channel preview"); assert.strictEqual(messagingMenu.$('.o_preview_name').text().trim(), "general", @@ -427,7 +427,7 @@ QUnit.test('update messaging preview on receiving a new message in channel previ messagingMenu.destroy(); }); -QUnit.test('preview of inbox message not linked to document + mark as read', function (assert) { +QUnit.test('preview of inbox message not linked to document + mark as read', async function (assert) { assert.expect(17); this.data.initMessaging = { @@ -459,19 +459,16 @@ QUnit.test('preview of inbox message not linked to document + mark as read', fun }, mockRPC: function (route, args) { if (args.method === 'set_message_done') { - assert.step({ - method: 'set_message_done', - messageIDs: args.args[0], - }); + assert.step('method: set_message_done, messageIDs: ' + JSON.stringify(args.args[0])); } return this._super.apply(this, arguments); }, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); assert.strictEqual(messagingMenu.$('.o_notification_counter').text(), '2', "should display a counter of 2 on the messaging menu icon"); - messagingMenu.$('.dropdown-toggle').click(); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.strictEqual(messagingMenu.$('.o_mail_preview').length, 2, "should display two previews"); @@ -494,11 +491,9 @@ QUnit.test('preview of inbox message not linked to document + mark as read', fun assert.strictEqual($preview2.find('.o_last_message_preview').text().replace(/\s/g, ''), "Demo:*Message2*", "should correctly display the 2nd preview"); - $preview1.find('.o_mail_preview_mark_as_read').click(); - assert.verifySteps([{ - method: 'set_message_done', - messageIDs: [689], - }], "should mark 1st preview as read"); + await testUtils.dom.click($preview1.find('.o_mail_preview_mark_as_read')); + assert.verifySteps(['method: set_message_done, messageIDs: [689]'], + "should mark 1st preview as read"); assert.strictEqual(messagingMenu.$('.o_notification_counter').text(), '1', "should display a counter of 1 on the messaging menu icon after marking one preview as read"); assert.strictEqual(messagingMenu.$('.o_mail_preview').length, 1, @@ -507,14 +502,9 @@ QUnit.test('preview of inbox message not linked to document + mark as read', fun "Demo:*Message2*", "preview 2 should be the remaining one"); $preview2 = messagingMenu.$('.o_mail_preview'); - $preview2.find('.o_mail_preview_mark_as_read').click(); - assert.verifySteps([{ - method: 'set_message_done', - messageIDs: [689], - }, { - method: 'set_message_done', - messageIDs: [690], - }], "should mark 2nd preview as read"); + await testUtils.dom.click($preview2.find('.o_mail_preview_mark_as_read')); + assert.verifySteps(['method: set_message_done, messageIDs: [690]'], + "should mark 2nd preview as read"); assert.strictEqual(messagingMenu.$('.o_notification_counter').text(), '0', "should display a counter of 0 on the messaging menu icon after marking both previews as read"); assert.strictEqual(messagingMenu.$('.o_mail_preview').length, 0, @@ -523,7 +513,7 @@ QUnit.test('preview of inbox message not linked to document + mark as read', fun messagingMenu.destroy(); }); -QUnit.test('grouped preview for needaction messages linked to same document', function (assert) { +QUnit.test('grouped preview for needaction messages linked to same document', async function (assert) { assert.expect(5); // simulate two (read) needaction (mention) messages in channel 'general' @@ -561,9 +551,9 @@ QUnit.test('grouped preview for needaction messages linked to same document', fu partner_id: 44, }, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); - messagingMenu.$('.dropdown-toggle').click(); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); var $previews = messagingMenu.$('.o_mail_preview'); assert.strictEqual($previews.length, 2, @@ -586,7 +576,7 @@ QUnit.test('grouped preview for needaction messages linked to same document', fu messagingMenu.destroy(); }); -QUnit.test("messaging menu widget: channel seen notification", function (assert) { +QUnit.test("messaging menu widget: channel seen notification", async function (assert) { assert.expect(4); this.data.initMessaging.channel_slots = { @@ -606,8 +596,8 @@ QUnit.test("messaging menu widget: channel seen notification", function (assert) session: { partner_id: 3 }, }); - messagingMenu.appendTo($('#qunit-fixture')); - messagingMenu.$('.dropdown-toggle').click(); + await messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.strictEqual(messagingMenu.$('.o_notification_counter').text(), '1', "should have correct messaging menu counter (1 unread message in channel)"); @@ -625,7 +615,7 @@ QUnit.test("messaging menu widget: channel seen notification", function (assert) [['myDB', 'mail.channel', 1], message] ]; messagingMenu.call('bus_service', 'trigger', 'notification', notifications); - + await testUtils.nextTick(); assert.strictEqual(messagingMenu.$('.o_notification_counter').text(), '0', "should no longer have a messaging menu counter (no unread message in channel)"); assert.strictEqual(messagingMenu.$('.o_preview_counter').text().replace(/\s/g, ''), '', @@ -634,7 +624,7 @@ QUnit.test("messaging menu widget: channel seen notification", function (assert) messagingMenu.destroy(); }); -QUnit.test("messaging menu widget: no traceback when receiving channel_fetched notification with blank thread window", function (assert) { +QUnit.test("messaging menu widget: no traceback when receiving channel_fetched notification with blank thread window", async function (assert) { assert.expect(3); this.data.initMessaging.channel_slots = { @@ -654,12 +644,12 @@ QUnit.test("messaging menu widget: no traceback when receiving channel_fetched n session: { partner_id: 3 }, }); - messagingMenu.appendTo($('#qunit-fixture')); - messagingMenu.$('.dropdown-toggle').click(); + await messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_new_message', "should have button to open blank thread window"); - testUtils.dom.click(messagingMenu.$('.o_new_message')); + await testUtils.dom.click(messagingMenu.$('.o_new_message')); var $threadWindow = $('.o_thread_window'); assert.strictEqual($threadWindow.length, 1, "should have an open thread window"); assert.ok($threadWindow.hasClass('o_thread_less'), "should be a blank thread window"); @@ -674,11 +664,12 @@ QUnit.test("messaging menu widget: no traceback when receiving channel_fetched n [['myDB', 'mail.channel', 1], message] ]; messagingMenu.call('bus_service', 'trigger', 'notification', notifications); + await testUtils.nextTick(); messagingMenu.destroy(); }); -QUnit.test("messaging menu widget: preview with no message should be undated", function ( assert ) { +QUnit.test("messaging menu widget: preview with no message should be undated", async function ( assert ) { assert.expect(2); // remove any message on the channel @@ -691,9 +682,8 @@ QUnit.test("messaging menu widget: preview with no message should be undated", f data: this.data, }); - messagingMenu.appendTo($('#qunit-fixture')); - - messagingMenu.$('.dropdown-toggle').click(); + await messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.strictEqual(messagingMenu.$('.o_mail_preview').length, 1, "should have two previews in messaging menu"); @@ -704,7 +694,7 @@ QUnit.test("messaging menu widget: preview with no message should be undated", f messagingMenu.destroy(); }); -QUnit.test("messaging menu widget: sort dated previews before undated previews", function ( assert ) { +QUnit.test("messaging menu widget: sort dated previews before undated previews", async function ( assert ) { assert.expect(5); var dm = { @@ -722,8 +712,8 @@ QUnit.test("messaging menu widget: sort dated previews before undated previews", data: this.data, }); - messagingMenu.appendTo($('#qunit-fixture')); - messagingMenu.$('.dropdown-toggle').click(); + await messagingMenu.appendTo($('#qunit-fixture')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.strictEqual(messagingMenu.$('.o_mail_preview').length, 2, "should have two previews in messaging menu"); @@ -743,7 +733,7 @@ QUnit.test("messaging menu widget: sort dated previews before undated previews", messagingMenu.destroy(); }); -QUnit.test('global counter with channel previews', function (assert) { +QUnit.test('global counter with channel previews', async function (assert) { assert.expect(5); var channels = [{ @@ -812,11 +802,10 @@ QUnit.test('global counter with channel previews', function (assert) { partner_id: 3, }, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); assert.strictEqual(messagingMenu.$('.o_notification_counter').text(), '2', "should display a counter of 2 on the messaging menu icon"); - - messagingMenu.$('.dropdown-toggle').click(); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.strictEqual(messagingMenu.$('.o_mail_preview').length, 3, "should display 3 previews"); @@ -834,7 +823,7 @@ QUnit.test('global counter with channel previews', function (assert) { messagingMenu.destroy(); }); -QUnit.test('messaging menu widget: do not open chat window twice on preview clicked', function (assert) { +QUnit.test('messaging menu widget: do not open chat window twice on preview clicked', async function (assert) { // This test assumes that a condition for opening chat window is to // successfully fetch messages beforehand. assert.expect(4); @@ -844,7 +833,7 @@ QUnit.test('messaging menu widget: do not open chat window twice on preview clic // This is necessary `message_fetch` on mailbox_inbox is required to // display the previews. var lockMessageFetch = false; - var messageFetchDef = $.Deferred(); + var messageFetchDef = testUtils.makeTestPromise(); var messagingMenu = new MessagingMenu(); testUtils.addMockEnvironment(messagingMenu, { @@ -873,19 +862,20 @@ QUnit.test('messaging menu widget: do not open chat window twice on preview clic return this._super.apply(this, arguments); }, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); // Opening chat window from messaging menu (pending from `messageFetchDef`) - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); lockMessageFetch = true; - testUtils.dom.click(messagingMenu.$('.o_mail_preview')); + await testUtils.dom.click(messagingMenu.$('.o_mail_preview')); messageFetchDef.resolve(); + await testUtils.nextTick(); assert.strictEqual($('.o_thread_window').length, 1, "should only display a single chat window"); assert.verifySteps([ - 'channel_minimize', 'message_fetch', + 'channel_minimize', ], "should have fetched messages only once"); messagingMenu.destroy(); diff --git a/addons/mail/static/tests/thread_window/basic_thread_window_tests.js b/addons/mail/static/tests/thread_window/basic_thread_window_tests.js index 5a0bc62f880..d5af9cf5357 100644 --- a/addons/mail/static/tests/thread_window/basic_thread_window_tests.js +++ b/addons/mail/static/tests/thread_window/basic_thread_window_tests.js @@ -55,7 +55,7 @@ QUnit.module('Basic', { }, }); -QUnit.test('basic rendering thread window', function (assert) { +QUnit.test('basic rendering thread window', async function (assert) { assert.expect(10); var parent = this.createParent({ @@ -63,9 +63,13 @@ QUnit.test('basic rendering thread window', function (assert) { services: this.services, }); + await testUtils.nextTick(); // detach channel 1, so that it opens corresponding thread window. - parent.call('mail_service', 'getChannel', 1).detach(); + var result = parent.call('mail_service', 'getChannel', 1); + await testUtils.nextTick(); + result.detach(); + await testUtils.nextTick(); var $threadWindow = $('.o_thread_window'); assert.strictEqual($threadWindow.length, 1, "thread window should be open"); @@ -98,7 +102,7 @@ QUnit.test('basic rendering thread window', function (assert) { parent.destroy(); }); -QUnit.test('close thread window using ESCAPE key', function (assert) { +QUnit.test('close thread window using ESCAPE key', async function (assert) { assert.expect(5); var parent = this.createParent({ @@ -107,28 +111,31 @@ QUnit.test('close thread window using ESCAPE key', function (assert) { mockRPC: function (route, args) { if (args.method === 'channel_fold') { assert.ok(true, "should call channel_fold"); - return $.when(); + return Promise.resolve(); } return this._super.apply(this, arguments); }, }); + await testUtils.nextTick(); // get channel instance to link to thread window var channel = parent.call('mail_service', 'getChannel', 1); + await testUtils.nextTick(); assert.ok(channel, "there should exist a channel locally with ID 1"); channel.detach(); + await testUtils.nextTick(); assert.strictEqual($('.o_thread_window').length, 1, "there should be a thread window that is opened"); // focus on the thread window and press ESCAPE - testUtils.dom.click($('.o_thread_window .o_composer_text_field')); + await testUtils.dom.click($('.o_thread_window .o_composer_text_field')); assert.strictEqual(document.activeElement, $('.o_thread_window .o_composer_text_field')[0], "thread window's input should now be focused"); var upKeyEvent = $.Event( "keyup", { which: 27 }); - $('.o_thread_window .o_composer_text_field').trigger(upKeyEvent); + await testUtils.dom.triggerEvents($('.o_thread_window .o_composer_text_field'), [upKeyEvent]); assert.strictEqual($('.o_thread_window').length, 0, "the thread window should be closed"); @@ -136,7 +143,7 @@ QUnit.test('close thread window using ESCAPE key', function (assert) { parent.destroy(); }); -QUnit.test('thread window\'s input can still be focused when the UI is blocked', function (assert) { +QUnit.test('thread window\'s input can still be focused when the UI is blocked', async function (assert) { assert.expect(2); var parent = this.createParent({ @@ -147,11 +154,13 @@ QUnit.test('thread window\'s input can still be focused when the UI is blocked', var $dom = $('#qunit-fixture'); // get channel instance to link to thread window + await testUtils.nextTick(); var channel = parent.call('mail_service', 'getChannel', 1); + await testUtils.nextTick(); channel.detach(); - + await testUtils.nextTick(); var $input = $('', {type: 'text'}).appendTo($dom); - testUtils.dom.click($input.focus()); + await testUtils.dom.click($input.focus()); assert.strictEqual(document.activeElement, $input[0], "fake input should be focused"); @@ -160,16 +169,17 @@ QUnit.test('thread window\'s input can still be focused when the UI is blocked', // makes no sense, this test is just about // making sure that the code which forces the // focus on click is not removed - testUtils.dom.click($('.o_thread_window .o_composer_text_field')); + await testUtils.dom.click($('.o_thread_window .o_composer_text_field')); assert.strictEqual(document.activeElement, $('.o_thread_window .o_composer_text_field')[0], "thread window's input should now be focused"); framework.unblockUI(); parent.destroy(); + $input.remove(); }); -QUnit.test('emoji popover should open correctly in thread windows', function (assert) { +QUnit.test('emoji popover should open correctly in thread windows', async function (assert) { assert.expect(1); var parent = this.createParent({ @@ -178,23 +188,20 @@ QUnit.test('emoji popover should open correctly in thread windows', function (as }); // get channel instance to link to thread window + await testUtils.nextTick(); var channel = parent.call('mail_service', 'getChannel', 1); + await testUtils.nextTick(); channel.detach(); - + await testUtils.nextTick(); var $emojiButton = $('.o_composer_button_emoji'); - testUtils.dom.click($emojiButton.trigger('focusin').focus()); + await testUtils.dom.click($emojiButton.trigger('focusin').focus()); var $popover = $('.o_mail_emoji_container'); - var done = assert.async(); - // Async is needed as the popover focusout hiding is deferred - setTimeout(function () { - assert.ok($popover.is(':visible'), "emoji popover should have stayed opened"); - parent.destroy(); - done(); - }, 0); + assert.ok($popover.is(':visible'), "emoji popover should have stayed opened"); + parent.destroy(); }); -QUnit.test('do not increase unread counter when receiving message with myself as author', function (assert) { +QUnit.test('do not increase unread counter when receiving message with myself as author', async function (assert) { assert.expect(4); var parent = this.createParent({ @@ -204,9 +211,11 @@ QUnit.test('do not increase unread counter when receiving message with myself as }); // get channel instance to link to thread window + await testUtils.nextTick(); var channel = parent.call('mail_service', 'getChannel', 1); + await testUtils.nextTick(); channel.detach(); - + await testUtils.nextTick(); var threadWindowHeaderText = $('.o_thread_window_header').text().replace(/\s/g, ""); assert.strictEqual(threadWindowHeaderText, "#general", @@ -226,6 +235,7 @@ QUnit.test('do not increase unread counter when receiving message with myself as var notification = [[false, 'mail.channel', 1], messageData]; parent.call('bus_service', 'trigger', 'notification', [notification]); + await testUtils.nextTick(); threadWindowHeaderText = $('.o_thread_window_header').text().replace(/\s/g, ""); assert.strictEqual(threadWindowHeaderText, "#general", @@ -236,7 +246,7 @@ QUnit.test('do not increase unread counter when receiving message with myself as parent.destroy(); }); -QUnit.test('do not increment unread counter with focus on thread window', function (assert) { +QUnit.test('do not increment unread counter with focus on thread window', async function (assert) { // 'hard' focus means that the user has clicked on the thread window in // order to set the focus on it. assert.expect(2); @@ -248,14 +258,17 @@ QUnit.test('do not increment unread counter with focus on thread window', functi }); // get channel instance to link to thread window + await testUtils.nextTick(); var channel = parent.call('mail_service', 'getChannel', 1); + await testUtils.nextTick(); channel.detach(); + await testUtils.nextTick(); assert.strictEqual(channel.getUnreadCounter(), 0, "thread should have unread counter to 0 initially"); // hard focus on thread window composer - testUtils.dom.click($('.o_composer_text_field')); + await testUtils.dom.click($('.o_composer_text_field')); // simulate receiving message from someone else var messageData = { @@ -268,6 +281,7 @@ QUnit.test('do not increment unread counter with focus on thread window', functi }; var notification = [[false, 'mail.channel', 1], messageData]; parent.call('bus_service', 'trigger', 'notification', [notification]); + await testUtils.nextTick(); assert.strictEqual(channel.getUnreadCounter(), 0, "thread should not have incremented its unread counter after receiving the message"); @@ -275,7 +289,7 @@ QUnit.test('do not increment unread counter with focus on thread window', functi parent.destroy(); }); -QUnit.test('do not mark as read the newly open thread window from received message', function (assert) { +QUnit.test('do not mark as read the newly open thread window from received message', async function (assert) { assert.expect(5); this.data['mail.channel'] = { @@ -315,7 +329,7 @@ QUnit.test('do not mark as read the newly open thread window from received messa if (args.method === 'channel_join_and_get_info') { this.data['mail.channel'].records[0].state = 'open'; this.data['mail.channel'].records[0].is_minimized = true; - return $.when(this.data['mail.channel'].records[0]); + return Promise.resolve(this.data['mail.channel'].records[0]); } return this._super.apply(this, arguments); }, @@ -338,7 +352,7 @@ QUnit.test('do not mark as read the newly open thread window from received messa // a new message from a new channel. this.data['mail.channel'].records[0].message_unread_counter++; parent.call('bus_service', 'trigger', 'notification', [notification]); - + await testUtils.nextTick(); var $threadWindow = $('.o_thread_window'); assert.strictEqual($threadWindow.length, 1, "a thread window should be open after receiving a new message on a new DM chat"); @@ -347,14 +361,14 @@ QUnit.test('do not mark as read the newly open thread window from received messa assert.strictEqual($threadWindow.find('.o_thread_window_title').text().replace(/\s/g, ''), '#DM(1)', "open DM chat window should have one unread message"); - testUtils.dom.click($threadWindow.find('.o_thread_composer')); + await testUtils.dom.click($threadWindow.find('.o_thread_composer')); assert.strictEqual($threadWindow.find('.o_thread_window_title').text().replace(/\s/g, ''), '#DM', "open DM chat window should have message marked as read on composer focus"); parent.destroy(); }); -QUnit.test('show document link of message linked to a document', function (assert) { +QUnit.test('show document link of message linked to a document', async function (assert) { assert.expect(6); this.data['mail.channel'] = { @@ -410,8 +424,11 @@ QUnit.test('show document link of message linked to a document', function (asser "no thread window should be open initially"); // get channel instance to link to thread window + await testUtils.nextTick(); var channel = parent.call('mail_service', 'getChannel', 2); + await testUtils.nextTick(); channel.detach(); + await testUtils.nextTick(); var $threadWindow = $('.o_thread_window'); assert.strictEqual($threadWindow.length, 1, @@ -429,7 +446,7 @@ QUnit.test('show document link of message linked to a document', function (asser parent.destroy(); }); -QUnit.test('do not autofocus chat window on receiving new direct message', function (assert) { +QUnit.test('do not autofocus chat window on receiving new direct message', async function (assert) { // Receiving a message doesn't make other input loose focus assert.expect(3); @@ -473,7 +490,7 @@ QUnit.test('do not autofocus chat window on receiving new direct message', funct }], }; - var form = testUtils.createView({ + var form = await testUtils.createView({ View: FormView, model: 'partner', data: this.data, @@ -487,7 +504,7 @@ QUnit.test('do not autofocus chat window on receiving new direct message', funct }, mockRPC: function (route, args) { if (args.method === 'channel_join_and_get_info') { - return $.when(this.data['mail.channel'].records[0]); + return Promise.resolve(this.data['mail.channel'].records[0]); } return this._super.apply(this, arguments); } @@ -512,7 +529,7 @@ QUnit.test('do not autofocus chat window on receiving new direct message', funct this.data['mail.message'].records.push(messageData); var notification = [[false, 'mail.channel', 2], messageData]; form.call('bus_service', 'trigger', 'notification', [notification]); - + await testUtils.nextTick(); assert.ok($('.o_thread_window.o_in_home_menu').length, 'Chat window is opened'); @@ -522,7 +539,7 @@ QUnit.test('do not autofocus chat window on receiving new direct message', funct form.destroy(); }); -QUnit.test('do not auto-focus chat window on receiving new message from new DM', function (assert) { +QUnit.test('do not auto-focus chat window on receiving new message from new DM', async function (assert) { assert.expect(10); var self = this; @@ -564,7 +581,7 @@ QUnit.test('do not auto-focus chat window on receiving new message from new DM', session: { partner_id: 3 }, mockRPC: function (route, args) { if (args.method === 'channel_join_and_get_info') { - return $.when(_.extend({}, self.data['mail.channel'].records[0], { info: 'join' })); + return Promise.resolve(_.extend({}, self.data['mail.channel'].records[0], { info: 'join' })); } if (args.method === 'channel_minimize') { _.extend(self.data['mail.channel'].records[0], { @@ -594,6 +611,7 @@ QUnit.test('do not auto-focus chat window on receiving new message from new DM', this.data['mail.message'].records.push(messageData); var notification = [[false, 'mail.channel', 2], messageData]; parent.call('bus_service', 'trigger', 'notification', [notification]); + await testUtils.nextTick(); assert.strictEqual($('.o_thread_window').length, 1, "should have DM window open"); @@ -610,6 +628,7 @@ QUnit.test('do not auto-focus chat window on receiving new message from new DM', }); notification = [[false, 'res.partner', 3], dmInfo]; parent.call('bus_service', 'trigger', 'notification', [notification]); + await testUtils.nextTick(); assert.strictEqual($('.o_thread_window').length, 1, "should still have DM window open after receiving DM info from polling"); @@ -621,6 +640,7 @@ QUnit.test('do not auto-focus chat window on receiving new message from new DM', // simulate receiving detached DM notification (cross-tab synchronization) notification = [[false, 'res.partner', 3], self.data['mail.channel'].records[0]]; parent.call('bus_service', 'trigger', 'notification', [notification]); + await testUtils.nextTick(); assert.strictEqual($('.o_thread_window').length, 1, "should still have DM open after receiving detached info from polling"); @@ -632,7 +652,7 @@ QUnit.test('do not auto-focus chat window on receiving new message from new DM', parent.destroy(); }); -QUnit.test('out-of-office status in thread window', function (assert) { +QUnit.test('out-of-office status in thread window', async function (assert) { assert.expect(1); this.data = { 'mail.message': { @@ -655,8 +675,13 @@ QUnit.test('out-of-office status in thread window', function (assert) { data: this.data, services: this.services, }); + await testUtils.nextTick(); + // detach channel 1, so that it opens corresponding thread window. - parent.call('mail_service', 'getChannel', 1).detach(); + var channel = parent.call('mail_service', 'getChannel', 1); + await testUtils.nextTick(); + channel.detach(); + await testUtils.nextTick(); var $threadWindow = $('.o_thread_window'); assert.containsOnce($threadWindow, '.o_out_of_office_text'); @@ -664,7 +689,7 @@ QUnit.test('out-of-office status in thread window', function (assert) { parent.destroy(); }); -QUnit.test('no out-of-office status in thread window', function (assert) { +QUnit.test('no out-of-office status in thread window', async function (assert) { assert.expect(1); this.data = { 'mail.message': { @@ -687,11 +712,17 @@ QUnit.test('no out-of-office status in thread window', function (assert) { data: this.data, services: this.services, }); + await testUtils.nextTick(); + // detach channel 1, so that it opens corresponding thread window. - parent.call('mail_service', 'getChannel', 1).detach(); + var channel = parent.call('mail_service', 'getChannel', 1); + await testUtils.nextTick(); + channel.detach(); + await testUtils.nextTick(); var $threadWindow = $('.o_thread_window'); assert.containsNone($threadWindow, '.o_out_of_office_text'); + parent.destroy(); }); diff --git a/addons/mail/static/tests/thread_window/blank_thread_window_tests.js b/addons/mail/static/tests/thread_window/blank_thread_window_tests.js index 4f103f71163..4a24a80d4c3 100644 --- a/addons/mail/static/tests/thread_window/blank_thread_window_tests.js +++ b/addons/mail/static/tests/thread_window/blank_thread_window_tests.js @@ -53,8 +53,8 @@ QUnit.module('Blank', { * functions end. * * @param {Object} params - * @param {$.Deferred} params.selectDef - * @param {$.Deferred} params.sourceDef + * @param {Promise} params.selectDef + * @param {Promise} params.sourceDef */ this.patchAutocomplete = function (params) { var selectDef = params.selectDef; @@ -87,7 +87,7 @@ QUnit.module('Blank', { }, }); -QUnit.test('basic rendering blank thread window', function (assert) { +QUnit.test('basic rendering blank thread window', async function (assert) { assert.expect(5); var parent = this.createParent({ @@ -97,7 +97,7 @@ QUnit.test('basic rendering blank thread window', function (assert) { // open blank thread window parent.call('mail_service', 'openBlankThreadWindow'); - + await testUtils.nextMicrotaskTick(); assert.strictEqual($('.o_thread_window').length, 1, "should have a thread window open"); assert.strictEqual($('.o_thread_window_title').text().trim(), @@ -114,7 +114,7 @@ QUnit.test('basic rendering blank thread window', function (assert) { parent.destroy(); }); -QUnit.test('close blank thread window', function (assert) { +QUnit.test('close blank thread window', async function (assert) { assert.expect(1); var parent = this.createParent({ @@ -124,8 +124,9 @@ QUnit.test('close blank thread window', function (assert) { // open blank thread window parent.call('mail_service', 'openBlankThreadWindow'); + await testUtils.nextMicrotaskTick(); - testUtils.dom.click($('.o_thread_window_close')); + await testUtils.dom.click($('.o_thread_window_close')); assert.strictEqual($('.o_thread_window').length, 0, "blank thread window should be closed"); @@ -133,10 +134,10 @@ QUnit.test('close blank thread window', function (assert) { parent.destroy(); }); -QUnit.test('fold blank thread window', function (assert) { +QUnit.test('fold blank thread window', async function (assert) { // This test requires full height of thread windows when they are open. // (e.g. 400px) - assert.expect(2); + assert.expect(3); var parent = this.createParent({ data: this.data, @@ -153,34 +154,37 @@ QUnit.test('fold blank thread window', function (assert) { // Open blank thread window parent.call('mail_service', 'openBlankThreadWindow'); + await testUtils.nextTick(); + + assert.containsOnce(document.body, '.o_thread_window'); assert.strictEqual($('.o_thread_window').css('height'), HEIGHT_OPEN, "blank thread window should be open"); if ($('.o_thread_window').css('height') !== HEIGHT_OPEN) { console.warn('Assertion above may fail due to too narrow height of browser'); } - testUtils.dom.click($('.o_thread_window_title')); + await testUtils.dom.click($('.o_thread_window_title')); assert.strictEqual($('.o_thread_window').css('height'), HEIGHT_FOLDED, - "blank thread window should be open"); + "blank thread window should be folded"); parent.destroy(); testUtils.mock.unpatch(AbstractThreadWindow); }); -QUnit.test('open new DM chat from blank thread window', function (assert) { +QUnit.test('open new DM chat from blank thread window', async function (assert) { assert.expect(6); var done = assert.async(); var self = this; - var selectDef = $.Deferred(); - var sourceDef = $.Deferred(); + var selectDef = testUtils.makeTestPromise(); + var sourceDef = testUtils.makeTestPromise(); this.patchAutocomplete({ selectDef: selectDef, sourceDef: sourceDef, }); - var def = $.Deferred(); + var def = testUtils.makeTestPromise(); this.data['res.partner'] = { fields: {}, @@ -193,13 +197,13 @@ QUnit.test('open new DM chat from blank thread window', function (assert) { mockRPC: function (route, args) { if (args.method === 'im_search') { def.resolve(); - return $.when([ + return Promise.resolve([ { id: 1, name: 'DemoUser1' }, { id: 2, name: 'DemoUser2', } ]); } if (args.method === 'channel_get_and_minimize') { - return $.when({ + return Promise.resolve({ channel_type: 'chat', direct_partner: [{ id: args.args[0][0], name: 'DemoUser1', im_status: '' }], id: 50, @@ -213,19 +217,20 @@ QUnit.test('open new DM chat from blank thread window', function (assert) { // open blank thread window parent.call('mail_service', 'openBlankThreadWindow'); + await testUtils.nextMicrotaskTick(); + await testUtils.fields.editAndTrigger($('.o_thread_search_input input'), 'D', 'keydown'); + await testUtils.nextTick(); + await Promise.all([sourceDef, def]); + await testUtils.nextTick(); + assert.strictEqual($('.ui-menu-item a').length, 2, + "should suggest 2 partners for DM"); + assert.strictEqual($('.ui-menu-item a').eq(0).text(), "DemoUser1", + "first suggestion should be 'DemoUser1'"); + assert.strictEqual($('.ui-menu-item a').eq(1).text(), "DemoUser2", + "second suggestion should be 'DemoUser2'"); - $('.o_thread_search_input input').val("D").trigger('keydown'); - - $.when(sourceDef, def).then(function () { - assert.strictEqual($('.ui-menu-item a').length, 2, - "should suggest 2 partners for DM"); - assert.strictEqual($('.ui-menu-item a').eq(0).text(), "DemoUser1", - "first suggestion should be 'DemoUser1'"); - assert.strictEqual($('.ui-menu-item a').eq(1).text(), "DemoUser2", - "second suggestion should be 'DemoUser2'"); - - testUtils.dom.click($('.ui-menu-item a').eq(0)); - }); + await testUtils.dom.clickFirst($('.ui-menu-item a'),{allowInvisible:true}); + await testUtils.nextTick(); selectDef.then(function () { assert.strictEqual($('.o_thread_window').length, 1, "should be a single window"); @@ -240,22 +245,22 @@ QUnit.test('open new DM chat from blank thread window', function (assert) { }); }); -QUnit.test('open already detached DM chat from blank thread window', function (assert) { +QUnit.test('open already detached DM chat from blank thread window', async function (assert) { // when opening an already detach DM chat from the blank thread window, // the blank thread window should disappear assert.expect(6); var done = assert.async(); var self = this; - var selectDef = $.Deferred(); - var sourceDef = $.Deferred(); + var selectDef = testUtils.makeTestPromise(); + var sourceDef = testUtils.makeTestPromise(); this.patchAutocomplete({ selectDef: selectDef, sourceDef: sourceDef, }); - var def = $.Deferred(); + var def = testUtils.makeTestPromise(); this.data['res.partner'] = { fields: {}, @@ -275,7 +280,7 @@ QUnit.test('open already detached DM chat from blank thread window', function (a mockRPC: function (route, args) { if (args.method === 'im_search') { def.resolve(); - return $.when([ + return Promise.resolve([ { id: 1, name: 'DemoUser1' }, ]); } @@ -283,6 +288,7 @@ QUnit.test('open already detached DM chat from blank thread window', function (a }, }); + await testUtils.nextTick(); assert.strictEqual($('.o_thread_window').length, 1, "should be a single window"); assert.strictEqual($('.o_thread_window_title').text().trim(), "DemoUser1", @@ -292,15 +298,16 @@ QUnit.test('open already detached DM chat from blank thread window', function (a // open blank thread window parent.call('mail_service', 'openBlankThreadWindow'); - + await testUtils.nextMicrotaskTick(); assert.strictEqual($('.o_thread_window').length, 2, "should have two thread windows open"); + await testUtils.fields.editAndTrigger($('.o_thread_search_input input'), 'D', 'keydown'); - $('.o_thread_search_input input').val("D").trigger('keydown'); - $.when(sourceDef, def).then(function () { - testUtils.dom.click($('.ui-menu-item a').eq(0)); - }); + await Promise.all([sourceDef, def]); + await testUtils.nextTick(); + await testUtils.dom.click($('.ui-menu-item a'),{allowInvisible:true}); + await testUtils.nextTick(); selectDef.then(function () { assert.strictEqual($('.o_thread_window').length, 1, "should now have a single thread window open"); diff --git a/addons/mail/static/tests/thread_window/hidden_thread_window_tests.js b/addons/mail/static/tests/thread_window/hidden_thread_window_tests.js index b87dbe0f794..691e37df362 100644 --- a/addons/mail/static/tests/thread_window/hidden_thread_window_tests.js +++ b/addons/mail/static/tests/thread_window/hidden_thread_window_tests.js @@ -48,12 +48,15 @@ QUnit.module('Hidden', { }; }, afterEach: function () { + // remove thread window dropup appended by mail service + var $target = $(this.services.mail_service.prototype.THREAD_WINDOW_APPENDTO); + $target.find('.o_thread_window_dropdown').remove(); // reset thread window append to body this.services.mail_service.prototype.THREAD_WINDOW_APPENDTO = 'body'; }, }); -QUnit.test('hidden thread windows dropdown when not enough horizontal space (version 1)', function (assert) { +QUnit.test('hidden thread windows dropdown when not enough horizontal space (version 1)', async function (assert) { // This test has hard-set global width, so that it does not depend on // the width of the screen: at most 2 thread windows visible at the // same time, and additional thread windows are in hidden while and @@ -87,9 +90,10 @@ QUnit.test('hidden thread windows dropdown when not enough horizontal space (ver data: this.data, services: this.services, }); - + await testUtils.nextTick(); parent.call('mail_service', 'getChannel', 0).detach(); parent.call('mail_service', 'getChannel', 1).detach(); + await testUtils.nextTick(); var $visibleThreadWindows = $('.o_thread_window:not(.o_thread_window_dropdown, .o_hidden)'); @@ -106,6 +110,7 @@ QUnit.test('hidden thread windows dropdown when not enough horizontal space (ver // detach a channel so that it exceeds available slots parent.call('mail_service', 'getChannel', 2).detach(); + await testUtils.nextTick(); // update list of visible thread windows $visibleThreadWindows = $('.o_thread_window:not(.o_thread_window_dropdown, .o_hidden)'); @@ -131,7 +136,7 @@ QUnit.test('hidden thread windows dropdown when not enough horizontal space (ver testUtils.mock.unpatch(this.services.mail_service); }); -QUnit.test('hidden thread windows dropdown when not enough horizontal space (version 2)', function (assert) { +QUnit.test('hidden thread windows dropdown when not enough horizontal space (version 2)', async function (assert) { // This is almost the same test as the one before, except there are // at most 3 thread windows visible, and 2 thread windows are visible // when it shows the 'hidden thread window' button. @@ -171,10 +176,11 @@ QUnit.test('hidden thread windows dropdown when not enough horizontal space (ver data: this.data, services: this.services, }); - + await testUtils.nextTick(); parent.call('mail_service', 'getChannel', 0).detach(); parent.call('mail_service', 'getChannel', 1).detach(); parent.call('mail_service', 'getChannel', 2).detach(); + await testUtils.nextTick(); var $visibleThreadWindows = $('.o_thread_window:not(.o_thread_window_dropdown, .o_hidden)'); @@ -193,6 +199,7 @@ QUnit.test('hidden thread windows dropdown when not enough horizontal space (ver // detach a channel so that it exceeds available slots parent.call('mail_service', 'getChannel', 3).detach(); + await testUtils.nextTick(); // update list of visible thread windows $visibleThreadWindows = $('.o_thread_window:not(.o_thread_window_dropdown, .o_hidden)'); diff --git a/addons/mail/static/tests/thread_window/moderation_thread_window_tests.js b/addons/mail/static/tests/thread_window/moderation_thread_window_tests.js index b35405a2243..732a5fe7938 100644 --- a/addons/mail/static/tests/thread_window/moderation_thread_window_tests.js +++ b/addons/mail/static/tests/thread_window/moderation_thread_window_tests.js @@ -84,7 +84,7 @@ QUnit.module('Moderation', { }, }); -QUnit.test('moderator: moderated channel with pending moderation message', function (assert) { +QUnit.test('moderator: moderated channel with pending moderation message', async function (assert) { assert.expect(5); this.data.initMessaging = { @@ -114,9 +114,10 @@ QUnit.test('moderator: moderated channel with pending moderation message', funct data: this.data, services: this.services, }); - + await testUtils.nextTick(); // detach channel 1, so that it opens corresponding thread window. parent.call('mail_service', 'getChannel', 1).detach(); + await testUtils.nextTick(); assert.strictEqual($('.o_thread_message ').length, 1, "should display a message in the thread window"); diff --git a/addons/mail/static/tests/thread_window/typing_thread_window_tests.js b/addons/mail/static/tests/thread_window/typing_thread_window_tests.js index 549cfdb7184..be2bd4beec8 100644 --- a/addons/mail/static/tests/thread_window/typing_thread_window_tests.js +++ b/addons/mail/static/tests/thread_window/typing_thread_window_tests.js @@ -40,7 +40,7 @@ QUnit.module('Typing', { * @param {integer} params.partnerID * @param {Widget} params.widget a widget that can call the bus_service */ - this.simulateIsTyping = function (params) { + this.simulateIsTyping = async function (params) { var typingData = { info: 'typing_status', partner_id: params.partnerID, @@ -48,6 +48,7 @@ QUnit.module('Typing', { }; var notification = [[false, 'mail.channel', params.channelID], typingData]; params.widget.call('bus_service', 'trigger', 'notification', [notification]); + await testUtils.nextMicrotaskTick(); }; this.createParent = function (params) { @@ -72,7 +73,7 @@ QUnit.module('Typing', { }, }); -QUnit.test('receive typing notification', function (assert) { +QUnit.test('receive typing notification', async function (assert) { assert.expect(4); var parent = this.createParent({ @@ -82,22 +83,24 @@ QUnit.test('receive typing notification', function (assert) { // when receiving an 'is typing' notification, fetch the list of // members of this channel if we haven't done yet. if (args.method === 'channel_fetch_listeners') { - return $.when([ + return Promise.resolve([ { id: 42, name: "Someone" }, ]); } return this._super.apply(this, arguments); }, }); + await testUtils.nextTick(); // detach channel 1, so that it opens corresponding thread window. parent.call('mail_service', 'getChannel', 1).detach(); + await testUtils.nextTick(); var $threadWindow = $('.o_thread_window'); assert.containsNone($threadWindow, '.o_mail_thread_typing_icon', "should not display an icon that someone is typing something on this thread"); - this.simulateIsTyping({ + await this.simulateIsTyping({ channelID: 1, isTyping: true, partnerID: 42, @@ -110,7 +113,7 @@ QUnit.test('receive typing notification', function (assert) { "Someone is typing...", "should display someone is typing something on hover of this icon"); - this.simulateIsTyping({ + await this.simulateIsTyping({ channelID: 1, isTyping: false, partnerID: 42, diff --git a/addons/mail/static/tests/tools/debug_manager_tests.js b/addons/mail/static/tests/tools/debug_manager_tests.js index 8b9312ca733..7671150f0d8 100644 --- a/addons/mail/static/tests/tools/debug_manager_tests.js +++ b/addons/mail/static/tests/tools/debug_manager_tests.js @@ -7,7 +7,7 @@ var createDebugManager = testUtils.createDebugManager; QUnit.module('Mail DebugManager', {}, function () { - QUnit.test("Manage Messages", function (assert) { + QUnit.test("Manage Messages", async function (assert) { assert.expect(3); var debugManager = createDebugManager({ @@ -47,15 +47,16 @@ QUnit.module('Mail DebugManager', {}, function () { }, modelName: 'testModel', }; - debugManager.update('action', action, view); + await testUtils.nextTick(); + await debugManager.update('action', action, view); var $messageMenu = debugManager.$('a[data-action=getMailMessages]'); assert.strictEqual($messageMenu.length, 1, "should have Manage Message menu item"); assert.strictEqual($messageMenu.text().trim(), "Manage Messages", "should have correct menu item text"); - testUtils.dom.click(debugManager.$('> a')); // open dropdown - testUtils.dom.click($messageMenu); + await testUtils.dom.click(debugManager.$('> a')); // open dropdown + await testUtils.dom.click($messageMenu); debugManager.destroy(); }); diff --git a/addons/mail_bot/static/src/js/systray_messaging_menu.js b/addons/mail_bot/static/src/js/systray_messaging_menu.js index 3b7ee2f9e38..f7bccee84ed 100644 --- a/addons/mail_bot/static/src/js/systray_messaging_menu.js +++ b/addons/mail_bot/static/src/js/systray_messaging_menu.js @@ -33,7 +33,7 @@ return MessagingMenu.include({ * * @override * @private - * @returns {$.Promise} resolved with list of previews that are + * @returns {Promise} resolved with list of previews that are * compatible with the 'mail.Preview' template. */ _getPreviews: function () { diff --git a/addons/mail_bot/static/tests/systray_messaging_menu_tests.js b/addons/mail_bot/static/tests/systray_messaging_menu_tests.js index 9cae63a9b1c..e2d4af883bf 100644 --- a/addons/mail_bot/static/tests/systray_messaging_menu_tests.js +++ b/addons/mail_bot/static/tests/systray_messaging_menu_tests.js @@ -31,7 +31,7 @@ QUnit.module('MessagingMenu', { // By default, permission are to ask user for push notification ("default"). // Use requestPermissionDef to simulate permission change, e.g. "granted" - this.requestPermissionDef = $.Deferred(); + this.requestPermissionDef = testUtils.makeTestPromise(); this.ORIGINAL_WINDOW_NOTIFICATION = window.Notification; window.Notification = { permission: "default", @@ -66,7 +66,7 @@ QUnit.module('MessagingMenu', { } }); -QUnit.test('messaging menu widget: rendering with OdooBot has a request', function (assert) { +QUnit.test('messaging menu widget: rendering with OdooBot has a request', async function (assert) { assert.expect(5); var messagingMenu = new MessagingMenu(); @@ -74,14 +74,14 @@ QUnit.test('messaging menu widget: rendering with OdooBot has a request', functi data: this.data, services: this.services, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); assert.containsOnce(messagingMenu, '.o_notification_counter', "should display a notification counter next to the messaging menu"); assert.strictEqual(messagingMenu.$('.o_notification_counter').text(), '1', "should display a counter of '1' next to the messaging menu"); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsOnce(messagingMenu, '.o_preview_info', "should display a preview in the messaging menu"); assert.strictEqual(messagingMenu.$('.o_preview_name').text().trim(), @@ -93,7 +93,7 @@ QUnit.test('messaging menu widget: rendering with OdooBot has a request', functi messagingMenu.destroy(); }); -QUnit.test('messaging menu widget: rendering without OdooBot has a request (denied)', function (assert) { +QUnit.test('messaging menu widget: rendering without OdooBot has a request (denied)', async function (assert) { assert.expect(3); window.Notification.permission = 'denied'; @@ -103,20 +103,20 @@ QUnit.test('messaging menu widget: rendering without OdooBot has a request (deni data: this.data, services: this.services, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); assert.containsOnce(messagingMenu, '.o_notification_counter', "should display a notification counter next to the messaging menu"); assert.strictEqual(messagingMenu.$('.o_notification_counter').text(), '0', "should display a counter of '0' next to the messaging menu"); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsNone(messagingMenu, '.o_preview_info', "should display no preview in the messaging menu"); messagingMenu.destroy(); }); -QUnit.test('messaging menu widget: rendering without OdooBot has a request (accepted)', function (assert) { +QUnit.test('messaging menu widget: rendering without OdooBot has a request (accepted)', async function (assert) { assert.expect(3); window.Notification.permission = 'granted'; @@ -126,20 +126,20 @@ QUnit.test('messaging menu widget: rendering without OdooBot has a request (acce data: this.data, services: this.services, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); assert.containsOnce(messagingMenu, '.o_notification_counter', "should display a notification counter next to the messaging menu"); assert.strictEqual(messagingMenu.$('.o_notification_counter').text(), '0', "should display a counter of '0' next to the messaging menu"); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsNone(messagingMenu, '.o_preview_info', "should display no preview in the messaging menu"); messagingMenu.destroy(); }); -QUnit.test('messaging menu widget: respond to notification prompt', function (assert) { +QUnit.test('messaging menu widget: respond to notification prompt', async function (assert) { assert.expect(4); var messagingMenu = new MessagingMenu(); @@ -147,35 +147,35 @@ QUnit.test('messaging menu widget: respond to notification prompt', function (as data: this.data, services: this.services, }); - messagingMenu.appendTo($('#qunit-fixture')); + await messagingMenu.appendTo($('#qunit-fixture')); assert.containsOnce(messagingMenu, '.o_notification_counter', "should display a notification counter next to the messaging menu"); assert.strictEqual(messagingMenu.$('.o_notification_counter').text(), '1', "should display a counter of '1' next to the messaging menu"); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); - testUtils.dom.click(messagingMenu.$('.o_preview_info')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.o_preview_info')); // simulate "default" response, which is equivalent to "Not Now" in Firefox. this.requestPermissionDef.resolve("default"); - + await testUtils.nextTick(); assert.strictEqual(messagingMenu.$('.o_notification_counter').text(), '0', "should display a counter of '0' next to the messaging menu"); - testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); + await testUtils.dom.click(messagingMenu.$('.dropdown-toggle')); assert.containsNone(messagingMenu, '.o_preview_info', "should display no preview in the messaging menu"); messagingMenu.destroy(); }); -QUnit.test('notification_alert widget: display blocked notification alert', function (assert) { +QUnit.test('notification_alert widget: display blocked notification alert', async function (assert) { assert.expect(2); window.Notification.permission = 'denied'; - var form = testUtils.createView({ + var form = await testUtils.createView({ View: FormView, model: 'mail.message', data: this.data, @@ -186,11 +186,10 @@ QUnit.test('notification_alert widget: display blocked notification alert', func assert.containsOnce(form, '.o_notification_alert', "Blocked notification alert should be displayed"); window.Notification.permission = 'granted'; - form.reload(); + await form.reload(); assert.containsNone(form, '.o_notification_alert', "Blocked notification alert should not be displayed"); form.destroy(); }); - }); }); diff --git a/addons/mass_mailing/static/src/js/mass_mailing_widget.js b/addons/mass_mailing/static/src/js/mass_mailing_widget.js index 142a88360f0..866213b9092 100644 --- a/addons/mass_mailing/static/src/js/mass_mailing_widget.js +++ b/addons/mass_mailing/static/src/js/mass_mailing_widget.js @@ -54,8 +54,8 @@ var MassMailingFieldHtml = FieldHtml.extend({ var $editable = this.wysiwyg.getEditable(); - return this.wysiwyg.save().then(function (isDirty) { - self._isDirty = isDirty; + return this.wysiwyg.save().then(function (result) { + self._isDirty = result.isDirty; convertInline.attachmentThumbnailToLinkImg($editable); convertInline.fontToImg($editable); diff --git a/addons/mass_mailing/static/src/js/unsubscribe.js b/addons/mass_mailing/static/src/js/unsubscribe.js index 5da1a195dd5..ae1c34f864b 100644 --- a/addons/mass_mailing/static/src/js/unsubscribe.js +++ b/addons/mass_mailing/static/src/js/unsubscribe.js @@ -13,7 +13,7 @@ odoo.define('mass_mailing.unsubscribe', function (require) { var token = (location.search.split('token' + '=')[1] || '').split('&')[0]; if (!$('.o_unsubscribe_form').length) { - return $.Deferred().reject("DOM doesn't contain '.o_unsubscribe_form'"); + return Promise.reject("DOM doesn't contain '.o_unsubscribe_form'"); } if (email != '' && email != undefined){ ajax.jsonRpc('/mailing/blacklist/check', 'call', {'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'token': token}) @@ -35,7 +35,7 @@ odoo.define('mass_mailing.unsubscribe', function (require) { $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); } }) - .fail(function () { + .guardedCatch(function () { $('#subscription_info').html(_t('An error occured. Please try again later or contact us.')); $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); }); @@ -80,7 +80,7 @@ odoo.define('mass_mailing.unsubscribe', function (require) { $('#info_state').removeClass('alert-info').addClass('alert-warning'); } }) - .fail(function () { + .guardedCatch(function () { $('#subscription_info').html(_t('An error occurred. Your changes have not been saved, try again later.')); $('#info_state').removeClass('alert-info').addClass('alert-warning'); }); @@ -115,7 +115,7 @@ odoo.define('mass_mailing.unsubscribe', function (require) { $('#unsubscribed_info').hide(); } }) - .fail(function () { + .guardedCatch(function () { $('#subscription_info').html(_t('An error occured. Please try again later or contact us.')); $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); }); @@ -147,7 +147,7 @@ odoo.define('mass_mailing.unsubscribe', function (require) { $('#unsubscribed_info').hide(); } }) - .fail(function () { + .guardedCatch(function () { $('#subscription_info').html(_t('An error occured. Please try again later or contact us.')); $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-warning').addClass('alert-error'); }); @@ -175,7 +175,7 @@ odoo.define('mass_mailing.unsubscribe', function (require) { $('#info_state').removeClass('alert-success').removeClass('alert-info').removeClass('alert-error').addClass('alert-warning'); } }) - .fail(function () { + .guardedCatch(function () { $('#subscription_info').html(_t('An error occured. Please try again later or contact us.')); $('#info_state').removeClass('alert-info').removeClass('alert-success').removeClass('alert-error').addClass('alert-warning'); }); diff --git a/addons/mass_mailing/static/tests/mass_mailing_html_tests.js b/addons/mass_mailing/static/tests/mass_mailing_html_tests.js index f1b58acb0f4..9a5f9ce259c 100644 --- a/addons/mass_mailing/static/tests/mass_mailing_html_tests.js +++ b/addons/mass_mailing/static/tests/mass_mailing_html_tests.js @@ -41,13 +41,13 @@ QUnit.module('field html', { testUtils.mock.patch(ajax, { loadAsset: function (xmlId) { if (xmlId === 'template.assets') { - return $.when({ + return Promise.resolve({ cssLibs: [], cssContents: ['.field_body {background-color: red;}'] }); } if (xmlId === 'template.assets_all_style') { - return $.when({ + return Promise.resolve({ cssLibs: $('link[href]:not([type="image/x-icon"])').map(function () { return $(this).attr('href'); }).get(), @@ -63,11 +63,10 @@ QUnit.module('field html', { }, }, function () { -QUnit.test('save arch and html', function (assert) { - var done = assert.async(); +QUnit.test('save arch and html', async function (assert) { assert.expect(6); - testUtils.createAsyncView({ + var form = await testUtils.createView({ View: FormView, model: 'mail.mass_mailing', data: this.data, @@ -79,7 +78,7 @@ QUnit.test('save arch and html', function (assert) { ' />'+ ' ' + - '
'+ - '
'+ - ' '+ + '
' + + '
' + + ' ' + '', res_id: 1, intercepts: { execute_action: function (event) { event.data.on_success(); - } + }, }, - }).then(function (form) { - testUtils.dom.click($('[name=put_in_queue]')) - testUtils.form.clickEdit(form) - var $fieldEdit = form.$('.oe_form_field[name="body_arch"]'); - var $iframe = $fieldEdit.find('iframe'); - - $iframe.data('load-def').then(function () { - testUtils.dom.click($('[name=put_in_queue]')) - assert.verifySteps(["MassMailingFieldHtml", "FieldHtml", "MassMailingFieldHtml"]); - testUtils.mock.unpatch(FieldHtml); - testUtils.mock.unpatch(MassMailingFieldHtml); - - form.destroy(); - done(); - }); - }); + + await testUtils.dom.click($('[name=put_in_queue]')); + await testUtils.form.clickEdit(form); + var $fieldEdit = form.$('.oe_form_field[name="body_arch"]'); + var $iframe = $fieldEdit.find('iframe'); + + await $iframe.data('loadDef'); + await testUtils.nextTick(); + await testUtils.dom.click($('[name=put_in_queue]')); + assert.verifySteps(["MassMailingFieldHtml", "FieldHtml", "MassMailingFieldHtml"]); + + testUtils.mock.unpatch(FieldHtml); + testUtils.mock.unpatch(MassMailingFieldHtml); + form.destroy(); }); }); diff --git a/addons/mrp/static/src/js/mrp.js b/addons/mrp/static/src/js/mrp.js index 154b65ce473..f9045ee2833 100644 --- a/addons/mrp/static/src/js/mrp.js +++ b/addons/mrp/static/src/js/mrp.js @@ -68,7 +68,7 @@ var TimeCounter = AbstractField.extend({ }); } }); - return $.when(this._super.apply(this, arguments), def); + return Promise.all([this._super.apply(this, arguments), def]); }, destroy: function () { diff --git a/addons/mrp/static/tests/mrp_tests.js b/addons/mrp/static/tests/mrp_tests.js index 7e548234bb1..c8a38be42c3 100644 --- a/addons/mrp/static/tests/mrp_tests.js +++ b/addons/mrp/static/tests/mrp_tests.js @@ -29,10 +29,10 @@ QUnit.module('mrp', { }, }, function () { - QUnit.test("bullet_state: basic rendering", function (assert) { + QUnit.test("bullet_state: basic rendering", async function (assert) { assert.expect(2); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -51,10 +51,10 @@ QUnit.module('mrp', { form.destroy(); }); - QUnit.test("mrp_time_counter: basic rendering", function (assert) { + QUnit.test("mrp_time_counter: basic rendering", async function (assert) { assert.expect(2); - var form = createView({ + var form = await createView({ View: FormView, model: 'partner', data: this.data, @@ -66,7 +66,7 @@ QUnit.module('mrp', { mockRPC: function (route, args) { if (args.method === 'search_read' && args.model === 'mrp.workcenter.productivity') { assert.ok(true, "the widget should fetch the mrp.workcenter.productivity"); - return $.when([{ + return Promise.resolve([{ date_start: '2017-01-01 08:00:00', date_end: '2017-01-01 10:00:00', }, { diff --git a/addons/note/static/src/js/systray_activity_menu.js b/addons/note/static/src/js/systray_activity_menu.js index f8a5e8e288e..a853a8b5cbb 100644 --- a/addons/note/static/src/js/systray_activity_menu.js +++ b/addons/note/static/src/js/systray_activity_menu.js @@ -89,14 +89,16 @@ ActivityMenu.include({ * @param {MouseEvent} ev */ _onAddNoteClick: function (ev) { + var self = this; ev.stopPropagation(); if (!this.noteDateTimeWidget){ this.noteDateTimeWidget = new datepicker.DateWidget(this, {useCurrent: true}); } - this.noteDateTimeWidget.appendTo(this.$('.o_note_datetime')); - this.noteDateTimeWidget.$input.attr('placeholder', _t("Today")); - this.$('.o_note_show, .o_note').toggleClass('d-none'); - this.$('.o_note_input').val('').focus(); + this.noteDateTimeWidget.appendTo(this.$('.o_note_datetime')).then(function() { + self.noteDateTimeWidget.$input.attr('placeholder', _t("Today")); + self.$('.o_note_show, .o_note').toggleClass('d-none'); + self.$('.o_note_input').val('').focus(); + }); }, /** * When focusing on input for new quick note systerm tray must be open. diff --git a/addons/note/static/tests/systray_activity_menu_tests.js b/addons/note/static/tests/systray_activity_menu_tests.js index 151af26dfbc..768412df2eb 100644 --- a/addons/note/static/tests/systray_activity_menu_tests.js +++ b/addons/note/static/tests/systray_activity_menu_tests.js @@ -34,7 +34,7 @@ QUnit.module("ActivityMenu", { } }); -QUnit.test('note activity menu widget: create note from activity menu', function (assert) { +QUnit.test('note activity menu widget: create note from activity menu', async function (assert) { assert.expect(15); var self = this; var activityMenu = new ActivityMenu(); @@ -42,7 +42,7 @@ QUnit.test('note activity menu widget: create note from activity menu', function services: this.services, mockRPC: function (route, args) { if (args.method === 'systray_get_activities') { - return $.when(self.data['mail.activity.menu'].records); + return Promise.resolve(self.data['mail.activity.menu'].records); } if (route === '/note/new') { if (args.date_deadline) { @@ -66,32 +66,32 @@ QUnit.test('note activity menu widget: create note from activity menu', function self.data['mail.activity.menu'].records[0].today_count++; self.data['mail.activity.menu'].records[0].total_count++; } - return $.when(); + return Promise.resolve(); } return this._super(route, args); }, }); - activityMenu.appendTo($('#qunit-fixture')); + await activityMenu.appendTo($('#qunit-fixture')); assert.hasClass(activityMenu.$el,'o_mail_systray_item', 'should be the instance of widget'); assert.strictEqual(activityMenu.$('.o_notification_counter').text(), '0', "should not have any activity notification initially"); // toggle quick create for note - testUtils.dom.click(activityMenu.$('.dropdown-toggle')); + await testUtils.dom.click(activityMenu.$('.dropdown-toggle')); assert.containsOnce(activityMenu, '.o_no_activity', "should not have any activity preview"); assert.doesNotHaveClass(activityMenu.$('.o_note_show'), 'd-none', 'ActivityMenu should have Add new note CTA'); - testUtils.dom.click(activityMenu.$('.o_note_show')); + await testUtils.dom.click(activityMenu.$('.o_note_show')); assert.hasClass(activityMenu.$('.o_note_show'), 'd-none', 'ActivityMenu should hide CTA when entering a new note'); assert.doesNotHaveClass(activityMenu.$('.o_note'), 'd-none', 'ActivityMenu should display input for new note'); // creating quick note without date - activityMenu.$("input.o_note_input").val("New Note"); - testUtils.dom.click(activityMenu.$(".o_note_save")); + await testUtils.fields.editInput(activityMenu.$("input.o_note_input"), "New Note"); + await testUtils.dom.click(activityMenu.$(".o_note_save")); assert.strictEqual(activityMenu.$('.o_notification_counter').text(), '1', "should increment activity notification counter after creating a note"); assert.containsOnce(activityMenu, '.o_mail_preview[data-res_model="note.note"]', @@ -106,9 +106,9 @@ QUnit.test('note activity menu widget: create note from activity menu', function 'ActivityMenu add note input should be hidden'); // creating quick note with date - testUtils.dom.click(activityMenu.$('.o_note_show')); + await testUtils.dom.click(activityMenu.$('.o_note_show')); activityMenu.$('input.o_note_input').val("New Note"); - testUtils.dom.click(activityMenu.$(".o_note_save")); + await testUtils.dom.click(activityMenu.$(".o_note_save")); assert.strictEqual(activityMenu.$('.o_notification_counter').text(), '2', "should increment activity notification counter after creating a second note"); assert.strictEqual(activityMenu.$('.o_activity_filter_button[data-filter="today"]').text().trim(), diff --git a/addons/pad/static/src/js/pad.js b/addons/pad/static/src/js/pad.js index e13000009d0..0edfd6e9076 100644 --- a/addons/pad/static/src/js/pad.js +++ b/addons/pad/static/src/js/pad.js @@ -28,7 +28,7 @@ var FieldPad = AbstractField.extend({ FieldPad.prototype.isPadConfigured = result; }); } - return $.when(); + return this._super.apply(this, arguments); }, /** * @override @@ -37,8 +37,9 @@ var FieldPad = AbstractField.extend({ if (!this.isPadConfigured) { this.$(".oe_unconfigured").removeClass('d-none'); this.$(".oe_configured").addClass('d-none'); - return; + return Promise.resolve(); } + var defs = []; if (this.mode === 'edit' && _.str.startsWith(this.value, 'http')) { this.url = this.value; // please close your eyes and look elsewhere... @@ -55,10 +56,11 @@ var FieldPad = AbstractField.extend({ // Guess what we decided... var url = {}; url.toJSON = _.constant(this.url); - this._setValue(url, {doNotSetDirty: true}); + defs.push(this._setValue(url, {doNotSetDirty: true})); } - return this._super.apply(this, arguments); + defs.push(this._super.apply(this, arguments)); + return Promise.all(defs); }, //-------------------------------------------------------------------------- @@ -156,7 +158,7 @@ var FieldPad = AbstractField.extend({ .removeClass('oe_pad_loading') .html('
'); self.$('.oe_pad_readonly').html(data); - }).fail(function () { + }).guardedCatch(function () { self.$('.oe_pad_content').text(_t('Unable to load pad')); }); } else { diff --git a/addons/pad/static/tests/pad_tests.js b/addons/pad/static/tests/pad_tests.js index b062e39470e..97568cdf200 100644 --- a/addons/pad/static/tests/pad_tests.js +++ b/addons/pad/static/tests/pad_tests.js @@ -34,10 +34,10 @@ QUnit.module('pad widget', { }, }); - QUnit.test('pad widget display help if server not configured', function (assert) { + QUnit.test('pad widget display help if server not configured', async function (assert) { assert.expect(4); - var form = createView({ + var form = await createView({ View: FormView, model: 'task', data: this.data, @@ -51,28 +51,28 @@ QUnit.module('pad widget', { res_id: 1, mockRPC: function (route, args) { if (args.method === 'pad_is_configured') { - return $.when(false); + return Promise.resolve(false); } return this._super.apply(this, arguments); }, }); - assert.ok(form.$('p.oe_unconfigured').is(':visible'), + assert.isVisible(form.$('p.oe_unconfigured'), "help message should be visible"); - assert.notOk(form.$('p.oe_pad_content').is(':visible'), - "content should not be visible"); - testUtils.form.clickEdit(form); - assert.ok(form.$('p.oe_unconfigured').is(':visible'), + assert.containsNone(form, 'p.oe_pad_content', + "content should not be displayed"); + await testUtils.form.clickEdit(form); + assert.isVisible(form.$('p.oe_unconfigured'), "help message should be visible"); - assert.notOk(form.$('p.oe_pad_content').is(':visible'), - "content should not be visible"); + assert.containsNone(form, 'p.oe_pad_content', + "content should not be displayed"); form.destroy(); delete FieldPad.prototype.isPadConfigured; }); - QUnit.test('pad widget works, basic case', function (assert) { + QUnit.test('pad widget works, basic case', async function (assert) { assert.expect(5); - var form = createView({ + var form = await createView({ View: FormView, model: 'task', data: this.data, @@ -87,7 +87,7 @@ QUnit.module('pad widget', { mockRPC: function (route, args) { if (route === 'https://pad.odoo.pad/p/test/1?showChat=false&userName=batman') { assert.ok(true, "should have an iframe with correct src"); - return $.when(true); + return Promise.resolve(true); } return this._super.apply(this, arguments); }, @@ -95,14 +95,14 @@ QUnit.module('pad widget', { userName: "batman", }, }); - assert.notOk(form.$('p.oe_unconfigured').is(':visible'), + assert.isNotVisible(form.$('p.oe_unconfigured'), "help message should not be visible"); - assert.ok(form.$('.oe_pad_content').is(':visible'), + assert.isVisible(form.$('.oe_pad_content'), "content should be visible"); - assert.strictEqual(form.$('.oe_pad_content:contains(This pad will be)').length, 1, + assert.containsOnce(form, '.oe_pad_content:contains(This pad will be)', "content should display a message when not initialized"); - testUtils.form.clickEdit(form); + await testUtils.form.clickEdit(form); assert.containsOnce(form, '.oe_pad_content iframe', "should have an iframe"); @@ -111,12 +111,12 @@ QUnit.module('pad widget', { delete FieldPad.prototype.isPadConfigured; }); - QUnit.test('pad widget works, with existing data', function (assert) { + QUnit.test('pad widget works, with existing data', async function (assert) { assert.expect(3); - var contentDef = $.Deferred(); + var contentDef = testUtils.makeTestPromise(); - var form = createView({ + var form = await createView({ View: FormView, model: 'task', data: this.data, @@ -130,7 +130,7 @@ QUnit.module('pad widget', { res_id: 2, mockRPC: function (route, args) { if (_.str.startsWith(route, 'http')) { - return $.when(true); + return Promise.resolve(true); } var result = this._super.apply(this, arguments); if (args.method === 'pad_get_content') { @@ -149,19 +149,20 @@ QUnit.module('pad widget', { assert.strictEqual(form.$('.oe_pad_content').text(), "Loading", "should display loading message"); contentDef.resolve(); + await testUtils.nextTick(); assert.strictEqual(form.$('.oe_pad_content').text(), "we should rewrite this server in haskell", "should display proper value"); - testUtils.form.clickEdit(form); - testUtils.form.clickSave(form); + await testUtils.form.clickEdit(form); + await testUtils.form.clickSave(form); form.destroy(); delete FieldPad.prototype.isPadConfigured; }); - QUnit.test('pad widget is not considered dirty at creation', function (assert) { + QUnit.test('pad widget is not considered dirty at creation', async function (assert) { assert.expect(2); - var form = createView({ + var form = await createView({ View: FormView, model: 'task', data: this.data, @@ -174,7 +175,7 @@ QUnit.module('pad widget', { '', mockRPC: function (route, args) { if (!args.method) { - return $.when(true); + return Promise.resolve(true); } return this._super.apply(this, arguments); }, @@ -183,20 +184,24 @@ QUnit.module('pad widget', { }, }); var def = form.canBeDiscarded(); + var defState = 'unresolved'; + def.then(function () { + defState = 'resolved'; + }); assert.strictEqual($('.modal').length, 0, "should have no confirmation modal opened"); - - assert.strictEqual(def.state(), 'resolved', + await testUtils.nextTick(); + assert.strictEqual(defState, 'resolved', "can be discarded was succesfully resolved"); form.destroy(); delete FieldPad.prototype.isPadConfigured; }); - QUnit.test('pad widget is not considered dirty at edition', function (assert) { + QUnit.test('pad widget is not considered dirty at edition', async function (assert) { assert.expect(2); - var form = createView({ + var form = await createView({ View: FormView, model: 'task', data: this.data, @@ -210,7 +215,7 @@ QUnit.module('pad widget', { res_id: 2, mockRPC: function (route, args) { if (!args.method) { - return $.when(true); + return Promise.resolve(true); } return this._super.apply(this, arguments); }, @@ -218,22 +223,26 @@ QUnit.module('pad widget', { userName: "batman", }, }); - testUtils.form.clickEdit(form); + await testUtils.form.clickEdit(form); var def = form.canBeDiscarded(); + var defState = 'unresolved'; + def.then(function () { + defState = 'resolved'; + }); assert.strictEqual($('.modal').length, 0, "should have no confirmation modal opened"); - - assert.strictEqual(def.state(), 'resolved', + await testUtils.nextTick(); + assert.strictEqual(defState, 'resolved', "can be discarded was succesfully resolved"); form.destroy(); delete FieldPad.prototype.isPadConfigured; }); - QUnit.test('record should be discarded properly even if only pad has changed', function (assert) { + QUnit.test('record should be discarded properly even if only pad has changed', async function (assert) { assert.expect(1); - var form = createView({ + var form = await createView({ View: FormView, model: 'task', data: this.data, @@ -247,7 +256,7 @@ QUnit.module('pad widget', { res_id: 2, mockRPC: function (route, args) { if (!args.method) { - return $.when(true); + return Promise.resolve(true); } return this._super.apply(this, arguments); }, @@ -255,8 +264,8 @@ QUnit.module('pad widget', { userName: "batman", }, }); - testUtils.form.clickEdit(form); - testUtils.form.clickDiscard(form); + await testUtils.form.clickEdit(form); + await testUtils.form.clickDiscard(form); assert.strictEqual(form.$('.oe_pad_readonly').text(), this.data.task.pad_get_content(), "pad content should not have changed"); form.destroy(); diff --git a/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js b/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js index 74d32c74acc..08d0a9a5149 100644 --- a/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js +++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js @@ -23,51 +23,51 @@ var PartnerAutocompleteMixin = { * Get list of companies via Autocomplete API * * @param {string} value - * @returns {Deferred} + * @returns {Promise} * @private */ _autocomplete: function (value) { + var self = this; value = value.trim(); - var def = $.Deferred(), - isVAT = this._isVAT(value), - odooSuggestions = [], - clearbitSuggestions = []; + var isVAT = this._isVAT(value); + var odooSuggestions = []; + var clearbitSuggestions = []; + return new Promise(function (resolve, reject) { + var odooPromise = self._getOdooSuggestions(value, isVAT).then(function (suggestions){ + odooSuggestions = suggestions; + }); - var odooPromise = this._getOdooSuggestions(value, isVAT).then(function (suggestions){ - odooSuggestions = suggestions; + // Only get Clearbit suggestions if not a VAT number + var clearbitPromise = isVAT ? false : self._getClearbitSuggestions(value).then(function (suggestions){ + clearbitSuggestions = suggestions; + }); + + var concatResults = function () { + // Add Clearbit result with Odoo result (with unique domain) + if (clearbitSuggestions && clearbitSuggestions.length) { + var websites = odooSuggestions.map(function (suggestion) { + return suggestion.website; + }); + clearbitSuggestions.forEach(function (suggestion) { + if (websites.indexOf(suggestion.domain) < 0) { + websites.push(suggestion.domain); + odooSuggestions.push(suggestion); + } + }); + } + + odooSuggestions = _.filter(odooSuggestions, function (suggestion) { + return !suggestion.ignored; + }); + _.each(odooSuggestions, function(suggestion){ + delete suggestion.ignored; + }); + return resolve(odooSuggestions); + }; + + self._whenAll([odooPromise, clearbitPromise]).then(concatResults, concatResults); }); - // Only get Clearbit suggestions if not a VAT number - var clearbitPromise = isVAT ? false : this._getClearbitSuggestions(value).then(function (suggestions){ - clearbitSuggestions = suggestions; - }); - - var concatResults = function () { - // Add Clearbit result with Odoo result (with unique domain) - if (clearbitSuggestions && clearbitSuggestions.length) { - var websites = odooSuggestions.map(function (suggestion) { - return suggestion.website; - }); - clearbitSuggestions.forEach(function (suggestion) { - if (websites.indexOf(suggestion.domain) < 0) { - websites.push(suggestion.domain); - odooSuggestions.push(suggestion); - } - }); - } - - odooSuggestions = _.filter(odooSuggestions, function (suggestion) { - return !suggestion.ignored; - }); - _.each(odooSuggestions, function(suggestion){ - delete suggestion.ignored; - }); - return def.resolve(odooSuggestions); - }; - - this._whenAll([odooPromise, clearbitPromise]).then(concatResults, concatResults); - - return def; }, /** @@ -77,7 +77,7 @@ var PartnerAutocompleteMixin = { * @param {string} company.website * @param {string} company.partner_gid * @param {string} company.vat - * @returns {Deferred} + * @returns {Promise} * @private */ _enrichCompany: function (company) { @@ -92,7 +92,7 @@ var PartnerAutocompleteMixin = { * Get the company logo as Base 64 image from url * * @param {string} url - * @returns {Deferred} + * @returns {Promise} * @private */ _getCompanyLogo: function (url) { @@ -106,7 +106,7 @@ var PartnerAutocompleteMixin = { * Get enriched data + logo before populating partner form * * @param {Object} company - * @returns {Deferred} + * @returns {Promise} */ _getCreateData: function (company) { var self = this; @@ -123,40 +123,38 @@ var PartnerAutocompleteMixin = { }); }; - var def = $.Deferred(); + return new Promise(function (resolve) { + // Fetch additional company info via Autocomplete Enrichment API + var enrichPromise = self._enrichCompany(company); - // Fetch additional company info via Autocomplete Enrichment API - var enrichPromise = this._enrichCompany(company); + // Get logo + var logoPromise = company.logo ? self._getCompanyLogo(company.logo) : false; + self._whenAll([enrichPromise, logoPromise]).then(function (result) { + var company_data = result[0]; + var logo_data = result[1]; - // Get logo - var logoPromise = company.logo ? this._getCompanyLogo(company.logo) : false; + if (company_data.error && company_data.error_message === 'Insufficient Credit') { + self._notifyNoCredits(); + company_data = company; + } - this._whenAll([enrichPromise, logoPromise]).always(function (company_data, logo_data){ - if (Array.isArray(company_data)) company_data = company_data[0]; - else company_data = {}; + if (_.isEmpty(company_data)) { + company_data = company; + } - if (Array.isArray(logo_data)) logo_data = logo_data[0]; - else logo_data = ''; + // Delete attribute to avoid "Field_changed" errors + removeUselessFields(company_data); - if (company_data.error && company_data.error_message === 'Insufficient Credit'){ - self._notifyNoCredits(); - company_data = company; - } - - if (_.isEmpty(company_data)) company_data = company; - - // Delete attribute to avoid "Field_changed" errors - removeUselessFields(company_data); - - // Assign VAT coming from parent VIES VAT query - if (company.vat) company_data.vat = company.vat; - def.resolve({ - company: company_data, - logo: logo_data + // Assign VAT coming from parent VIES VAT query + if (company.vat) { + company_data.vat = company.vat; + } + resolve({ + company: company_data, + logo: logo_data + }); }); }); - - return def; }, /** @@ -186,35 +184,35 @@ var PartnerAutocompleteMixin = { //-------------------------------------------------------------------------- /** - * Returns a deferred which will be resolved with the base64 data of the + * Returns a promise which will be resolved with the base64 data of the * image fetched from the given url. * * @private * @param {string} url : the url where to find the image to fetch - * @returns {Deferred} + * @returns {Promise} */ _getBase64Image: function (url) { - var def = $.Deferred(); - var xhr = new XMLHttpRequest(); - xhr.onload = function () { - var reader = new FileReader(); - reader.onloadend = function () { - def.resolve(reader.result); + return new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.onload = function () { + var reader = new FileReader(); + reader.onloadend = function () { + resolve(reader.result); + }; + reader.readAsDataURL(xhr.response); }; - reader.readAsDataURL(xhr.response); - }; - xhr.open('GET', url); - xhr.responseType = 'blob'; - xhr.onerror = def.reject.bind(def); - xhr.send(); - return def; + xhr.open('GET', url); + xhr.responseType = 'blob'; + xhr.onerror = reject; + xhr.send(); + }); }, /** * Use Clearbit Autocomplete API to return suggestions * * @param {string} value - * @returns {Deferred} + * @returns {Promise} * @private */ _getClearbitSuggestions: function (value) { @@ -242,7 +240,7 @@ var PartnerAutocompleteMixin = { * * @param {string} value * @param {boolean} isVAT - * @returns {Deferred} + * @returns {Promise} * @private */ _getOdooSuggestions: function (value, isVAT) { @@ -298,72 +296,26 @@ var PartnerAutocompleteMixin = { /** * Utility to wait for multiple promises - * $.when will reject all promises whenever a promise is rejected + * Promise.all will reject all promises whenever a promise is rejected * This utility will continue - * Source : https://gist.github.com/fearphage/4341799 * - * @param array - * @returns {*} + * @param {Promise[]} promises + * @returns {Promise} * @private */ - _whenAll: function (array) { - var slice = [].slice; - - var - resolveValues = arguments.length === 1 && $.isArray(array) - ? array - : slice.call(arguments) - , length = resolveValues.length - , remaining = length - , deferred = $.Deferred() - , i = 0 - , failed = 0 - , rejectContexts = Array(length) - , rejectValues = Array(length) - , resolveContexts = Array(length) - , value - ; - - function updateFunc(index, contexts, values) { - return function () { - !(values === resolveValues) && failed++; - deferred.notifyWith( - contexts[index] = this - , values[index] = slice.call(arguments) - ); - if (!(--remaining)) { - deferred[(!failed ? 'resolve' : 'reject') + 'With'](contexts, values); - } - }; - } - - for (; i < length; i++) { - if ((value = resolveValues[i]) && $.isFunction(value.promise)) { - value.promise() - .done(updateFunc(i, resolveContexts, resolveValues)) - .fail(updateFunc(i, rejectContexts, rejectValues)) - ; - } - else { - deferred.notifyWith(this, value); - --remaining; - } - } - - if (!remaining) { - deferred.resolveWith(resolveContexts, resolveValues); - } - - return deferred.promise(); + _whenAll: function (promises) { + return Promise.all(promises.map(function (p) { + return Promise.resolve(p); + })); }, /** - * @private - * @returns {$.Promise} + * @private + * @returns {Promise} */ _notifyNoCredits: function () { var self = this; - this._rpc({ + return this._rpc({ model: 'iap.account', method: 'get_credits_url', args: ['partner_autocomplete'], diff --git a/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js b/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js index 3a6374de5fc..efac26ca098 100644 --- a/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js +++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js @@ -135,14 +135,15 @@ var FieldAutocomplete = FieldChar.extend(AutocompleteMixin, { self.trigger_up('field_changed', { dataPointID: self.dataPointID, changes: data.company, + onSuccess: function () { + // update the input's value directly + if (self.onlyVAT) + self.$input.val(self._formatValue(company.vat)); + else + self.$input.val(self._formatValue(company.name)); + }, }); }); - - // update the input's value directly - if (this.onlyVAT) - this.$input.val(this._formatValue(company.vat)); - else - this.$input.val(this._formatValue(company.name)); this._removeDropdown(); }, diff --git a/addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js b/addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js index 5a3334e8094..a594373f720 100644 --- a/addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js +++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js @@ -35,7 +35,7 @@ var PartnerField = FieldMany2One.extend(AutocompleteMixin, { * Action : create popup form with pre-filled values from Autocomplete * * @param {Object} company - * @returns {Deferred} + * @returns {Promise} * @private */ _createPartner: function (company) { @@ -78,7 +78,7 @@ var PartnerField = FieldMany2One.extend(AutocompleteMixin, { */ _modifyAutompleteRendering: function (){ var api = this.$input.data('ui-autocomplete'); - // FIXME: bugfix to prevent traceback in mobile apps due to override + // FIXME: bugfix to prevent traceback in mobile apps due to override // of Many2one widget with native implementation. if (!api) { return; @@ -114,40 +114,38 @@ var PartnerField = FieldMany2One.extend(AutocompleteMixin, { * * @override * @param search_val {string} - * @returns {Deferred} + * @returns {Promise} * @private */ _searchSuggestions: function (search_val) { - var def = $.Deferred(); + var self = this; + return new Promise(function (resolve, reject) { + if (self._isOnline()) { - if (this._isOnline()) { - var self = this; + self._autocomplete(search_val).then(function (suggestions) { + var choices = []; + if (suggestions && suggestions.length) { + _.each(suggestions, function (suggestion) { + var label = ' '; + label += _.str.sprintf('%s, %s', suggestion.label, suggestion.description); - self._autocomplete(search_val).then(function (suggestions) { - var choices = []; - if (suggestions && suggestions.length) { - _.each(suggestions, function (suggestion) { - var label = ' '; - label += _.str.sprintf('%s, %s', suggestion.label, suggestion.description); - - choices.push({ - label: label, - action: function () { - self._createPartner(suggestion); - }, - logo: suggestion.logo, - classname: 'o_partner_autocomplete_dropdown_item', + choices.push({ + label: label, + action: function () { + self._createPartner(suggestion); + }, + logo: suggestion.logo, + classname: 'o_partner_autocomplete_dropdown_item', + }); }); - }); - } + } - def.resolve(choices); - }); - } else { - def.resolve([]); - } - - return def; + resolve(choices); + }); + } else { + resolve([]); + } + }); }, }); diff --git a/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js b/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js index bae95b395a5..0f5a7fb7d1c 100644 --- a/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js +++ b/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js @@ -8,7 +8,7 @@ odoo.define('partner_autocomplete.tests', function (require) { var PartnerField = require('partner.autocomplete.many2one'); var NotificationService = require('web.NotificationService'); - var createView = testUtils.createAsyncView; + var createView = testUtils.createView; function _compareResultFields(assert, form, fields, createData) { var type, formatted, $fieldInput; @@ -54,7 +54,7 @@ odoo.define('partner_autocomplete.tests', function (require) { _.each(fieldsToPatch, function (fieldToPatch) { testUtils.mock.patch(fieldToPatch, { _getBase64Image: function (url) { - return $.when(url === "odoo.com/logo.png" ? "odoobase64" : ""); + return Promise.resolve(url === "odoo.com/logo.png" ? "odoobase64" : ""); }, _isOnline: function () { return true; @@ -67,7 +67,7 @@ odoo.define('partner_autocomplete.tests', function (require) { return def; }, _enrichCompany: function (company) { - return $.when(enrichData); + return Promise.resolve(enrichData); }, _getOdooSuggestions: function (value, isVAT) { var results = _.filter(suggestions, function (suggestion) { @@ -75,7 +75,7 @@ odoo.define('partner_autocomplete.tests', function (require) { if (isVAT) return (suggestion.vat.toLowerCase().indexOf(value) >= 0); else return (suggestion.name.toLowerCase().indexOf(value) >= 0); }); - return $.when(results); + return Promise.resolve(results); }, _getClearbitSuggestions: function (value) { return this._getOdooSuggestions(value); @@ -185,11 +185,10 @@ odoo.define('partner_autocomplete.tests', function (require) { }); - QUnit.test("Partner autocomplete : Company type = Company / Name search", function (assert) { - assert.expect(21); - var done = assert.async(); + QUnit.test("Partner autocomplete : Company type = Company / Name search", async function (assert) { + assert.expect(18); var fields = this.data['res.partner'].fields; - createView({ + var form = await createView({ View: FormView, model: 'res.partner', data: this.data, @@ -209,34 +208,31 @@ odoo.define('partner_autocomplete.tests', function (require) { '' + '' + '', - mockRPC: function (route, args) { - if (args.method) { - assert.step(args.method); - } + mockRPC: function (route) { if (route === "/web/static/src/img/placeholder.png" || route === "odoo.com/logo.png" || route === "data:image/png;base64,odoobase64") { // land here as it is not valid base64 content - return $.when(); + return Promise.resolve(); } return this._super.apply(this, arguments); }, - }).then(function (form){ + }); // Set company type to Company var $company_type = form.$("select[name='company_type']"); - testUtils.fields.editSelect($company_type, '"company"'); + await testUtils.fields.editSelect($company_type, '"company"'); // Check input exists var $input = form.$(".o_field_partner_autocomplete > input:visible"); assert.strictEqual($input.length, 1, "there should be an for the field"); // Change input val and assert changes - testUtils.fields.editInput($input, "odoo") - + await testUtils.fields.editInput($input, "odoo"); + await testUtils.nextTick(); var $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible"); assert.strictEqual($dropdown.length, 1, "there should be an opened dropdown"); - assert.strictEqual($dropdown.children().length, 1, "there should be only one proposition"); + assert.strictEqual($dropdown.children().length, 1, "there should be only ne proposition"); - testUtils.dom.click($dropdown.find("a").first()); + await testUtils.dom.click($dropdown.find("a").first()); $input = form.$(".o_field_partner_autocomplete > input"); assert.strictEqual($input.val(), "Odoo", "Input value should have been updated to \"Odoo\""); assert.strictEqual(form.$("input.o_field_widget").val(), "odoo.com", "website value should have been updated to \"odoo.com\""); @@ -244,31 +240,29 @@ odoo.define('partner_autocomplete.tests', function (require) { _compareResultFields(assert, form, fields, createData); // Try suggestion with bullshit query - testUtils.fields.editInput($input, "ZZZZZZZZZZZZZZZZZZZZZZ") + await testUtils.fields.editInput($input, "ZZZZZZZZZZZZZZZZZZZZZZ"); $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible"); assert.strictEqual($dropdown.length, 0, "there should be no opened dropdown when no result"); // Try autocomplete again - testUtils.fields.editInput($input, "odoo") + await testUtils.fields.editInput($input, "odoo"); + await testUtils.nextTick(); $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible"); assert.strictEqual($dropdown.length, 1, "there should be an opened dropdown when typing odoo letters again"); // Test if dropdown closes on focusout $input.trigger("focusout"); + await testUtils.nextTick(); $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible"); assert.strictEqual($dropdown.length, 0, "unfocusing the input should close the dropdown"); form.destroy(); - - done(); - }); }); - QUnit.test("Partner autocomplete : Company type = Company / VAT search", function (assert) { - assert.expect(32); - var done = assert.async(); + QUnit.test("Partner autocomplete : Company type = Company / VAT search", async function (assert) { + assert.expect(29); var fields = this.data['res.partner'].fields; - createView({ + var form = await createView({ View: FormView, model: 'res.partner', data: this.data, @@ -288,21 +282,18 @@ odoo.define('partner_autocomplete.tests', function (require) { '' + '' + '', - mockRPC: function (route, args) { - if (args.method) { - assert.step(args.method); - } + mockRPC: function (route) { if (route === "/web/static/src/img/placeholder.png" || route === "odoo.com/logo.png" || route === "data:image/png;base64,odoobase64") { // land here as it is not valid base64 content - return $.when(); + return Promise.resolve(); } return this._super.apply(this, arguments); }, - }).then(function (form){ + }); // Set company type to Company var $company_type = form.$("select[name='company_type']"); - testUtils.fields.editSelect($company_type, '"company"'); + await testUtils.fields.editSelect($company_type, '"company"'); // Check input exists @@ -310,33 +301,33 @@ odoo.define('partner_autocomplete.tests', function (require) { assert.strictEqual($input.length, 1, "there should be an for the field"); // Set incomplete VAT and assert changes - testUtils.fields.editInput($input, "BE047747270") + await testUtils.fields.editInput($input, "BE047747270") var $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible"); assert.strictEqual($dropdown.length, 0, "there should be no opened dropdown no results with incomplete VAT number"); // Set complete VAT and assert changes // First suggestion (only vat result) - testUtils.fields.editInput($input, "BE0477472701") + await testUtils.fields.editInput($input, "BE0477472701") $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible"); assert.strictEqual($dropdown.length, 1, "there should be an opened dropdown"); assert.strictEqual($dropdown.children().length, 1, "there should be one proposition for complete VAT number"); - testUtils.dom.click($dropdown.find("a").first()); + await testUtils.dom.click($dropdown.find("a").first()); $input = form.$(".o_field_partner_autocomplete > input"); assert.strictEqual($input.val(), "Odoo", "Input value should have been updated to \"Odoo\""); _compareResultFields(assert, form, fields, createData); - + await testUtils.nextTick(); // Set complete VAT and assert changes // Second suggestion (only vat + clearbit result) - testUtils.fields.editInput($input, "BE0477472701") + await testUtils.fields.editInput($input, "BE0477472701") $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible"); assert.strictEqual($dropdown.length, 1, "there should be an opened dropdown"); assert.strictEqual($dropdown.children().length, 1, "there should be one proposition for complete VAT number"); - testUtils.dom.click($dropdown.find("a").first()); + await testUtils.dom.click($dropdown.find("a").first()); $input = form.$(".o_field_partner_autocomplete > input"); assert.strictEqual($input.val(), "Odoo", "Input value should have been updated to \"Odoo\""); @@ -350,8 +341,6 @@ odoo.define('partner_autocomplete.tests', function (require) { form.destroy(); - done(); - }); }); QUnit.test("Partner autocomplete : render Many2one", function (assert) { @@ -390,16 +379,15 @@ odoo.define('partner_autocomplete.tests', function (require) { }); }); - QUnit.test("Partner autocomplete : Notify not enough credits", function (assert) { + QUnit.test("Partner autocomplete : Notify not enough credits", async function (assert) { assert.expect(1); - var done = assert.async(); enrichData = { error: true, error_message: 'Insufficient Credit', }; - createView({ + var form = await createView({ View: FormView, model: 'res.partner', data: this.data, @@ -413,26 +401,24 @@ odoo.define('partner_autocomplete.tests', function (require) { }, mockRPC: function (route, args) { if (args.method === "get_credits_url"){ - return $.when('credits_url'); + return Promise.resolve('credits_url'); } return this._super.apply(this, arguments); }, - }).then(function (form){ + }); // Set company type to Company var $company_type = form.$("select[name='company_type']"); - testUtils.fields.editSelect($company_type, '"company"'); + await testUtils.fields.editSelect($company_type, '"company"'); var $input = form.$(".o_field_partner_autocomplete > input:visible"); - testUtils.fields.editInput($input, "BE0477472701"); + await testUtils.fields.editInput($input, "BE0477472701"); var $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible"); - testUtils.dom.click($dropdown.find("a").first()); + await testUtils.dom.click($dropdown.find("a").first()); var $notify = $(".o_partner_autocomplete_test_notify"); assert.isVisible($notify, "there should be an 'Insufficient Credit' notification"); form.destroy(); - done(); - }); }); }); diff --git a/addons/payment/static/src/js/payment_form.js b/addons/payment/static/src/js/payment_form.js index 6bbe4604dd4..c90b07875df 100644 --- a/addons/payment/static/src/js/payment_form.js +++ b/addons/payment/static/src/js/payment_form.js @@ -209,7 +209,7 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({ else { checked_radio.value = data.id; // set the radio value to the new card id form.submit(); - return $.Deferred(); + return new Promise(function () {}); } } // if the server has returned false, we display an error @@ -228,7 +228,7 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({ $(button).attr('disabled', false); $(button).children('.fa').addClass('fa-plus-circle'); $(button).find('span.o_loader').remove(); - }).fail(function (error, event) { + }).guardedCatch(function (error) { // if the rpc fails, pretty obvious $(button).attr('disabled', false); $(button).children('.fa').addClass('fa-plus-circle'); @@ -274,7 +274,7 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({ $(newForm).find('input[data-remove-me]').remove(); // remove all the input that should be removed if(action_url) { newForm.submit(); // and finally submit the form - return $.Deferred(); + return new Promise(function () {}); } } else { @@ -283,7 +283,7 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({ _t("We are not able to redirect you to the payment form.") ); } - }).fail(function (error, event) { + }).guardedCatch(function (error) { self.displayError( _t('Server Error'), _t("We are not able to redirect you to the payment form. ") + @@ -301,7 +301,7 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({ } else { // if the user is using an old payment then we just submit the form form.submit(); - return $.Deferred(); + return new Promise(function () {}); } } else { @@ -408,7 +408,7 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({ $(button).attr('disabled', false); $(button).children('.fa').addClass('fa-plus-circle'); $(button).find('span.o_loader').remove(); - }).fail(function (error, event) { + }).guardedCatch(function (error) { // if the rpc fails, pretty obvious $(button).attr('disabled', false); $(button).children('.fa').addClass('fa-plus-circle'); diff --git a/addons/payment/static/src/js/payment_portal.js b/addons/payment/static/src/js/payment_portal.js index 0753af1b3eb..82f8647fbb6 100644 --- a/addons/payment/static/src/js/payment_portal.js +++ b/addons/payment/static/src/js/payment_portal.js @@ -1,4 +1,4 @@ -$(document).ready(function () { +$(function () { $('input#cc_number').payment('formatCardNumber'); $('input#cc_cvc').payment('formatCardCVC'); diff --git a/addons/payment/static/src/js/payment_processing.js b/addons/payment/static/src/js/payment_processing.js index 3497297d665..0ee3e934176 100644 --- a/addons/payment/static/src/js/payment_processing.js +++ b/addons/payment/static/src/js/payment_processing.js @@ -63,11 +63,14 @@ odoo.define('payment.processing', function (require) { } } self.startPolling(); - }).always(function(){ + }).then(function() { + if ($.blockUI) { + $.unblockUI(); + } + }).guardedCatch(function() { if ($.blockUI) { $.unblockUI(); } - }).fail(function(e) { self.displayContent("payment.rpc_error", {}); self.startPolling(); }); diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js index 262068fd12d..e0aeaeaacbd 100644 --- a/addons/payment_stripe/static/src/js/stripe.js +++ b/addons/payment_stripe/static/src/js/stripe.js @@ -47,17 +47,19 @@ odoo.define('payment_stripe.stripe', function(require) { invoice_num: $("input[name='invoice_num']").val(), tx_ref: $("input[name='invoice_num']").val(), return_url: $("input[name='return_url']").val() - }).always(function(){ + }).then(function(data){ + handler.isTokenGenerate = false; + window.location.href = data; if ($.blockUI) { $.unblockUI(); } - }).done(function(data){ - handler.isTokenGenerate = false; - window.location.href = data; - }).fail(function(data){ + }).guardedCatch(function(data){ var msg = data && data.data && data.data.message; var wizard = $(qweb.render('stripe.error', {'msg': msg || _t('Payment error')})); wizard.appendTo($('body')).modal({'keyboard': true}); + if ($.blockUI) { + $.unblockUI(); + } }); }, }); @@ -66,7 +68,7 @@ odoo.define('payment_stripe.stripe', function(require) { require('web.dom_ready'); if (!$('.o_payment_form').length) { - return $.Deferred().reject("DOM doesn't contain '.o_payment_form'"); + return Promise.reject("DOM doesn't contain '.o_payment_form'"); } var observer = new MutationObserver(function(mutations, observer) { @@ -109,7 +111,7 @@ odoo.define('payment_stripe.stripe', function(require) { try { provider_form[0].innerHTML = data; } catch (e) {} // Restore 'Pay Now' button HTML since data might have changed it. $(provider_form[0]).find('#pay_stripe').replaceWith($pay_stripe); - }).done(function () { + }).then(function () { getStripeHandler().open({ name: merchant, description: invoice_num, diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js index 026682d5135..ffc5d5cc860 100644 --- a/addons/point_of_sale/static/src/js/chrome.js +++ b/addons/point_of_sale/static/src/js/chrome.js @@ -453,34 +453,33 @@ var ClientScreenWidget = PosBaseWidget.extend({ var self = this; function loop() { if (self.pos.proxy.posbox_supports_display) { - var deffered = self.pos.proxy.test_ownership_of_client_screen(); - if (deffered) { - deffered.then( - function(data) { - if (typeof data === 'string') { - data = JSON.parse(data); - } - if (data.status === 'OWNER') { - self.change_status_display('success'); - } else { - self.change_status_display('warning'); - } - }, - - function(err) { - if (typeof err == "undefined") { - self.change_status_display('failure'); - } else { - self.change_status_display('not_found'); - self.pos.proxy.posbox_supports_display = false; - } - }) - - .always(function () { - setTimeout(loop,3000); - }); - } - } + self.pos.proxy.test_ownership_of_client_screen().then( + function (data) { + if (typeof data === 'string') { + data = JSON.parse(data); + } + if (data.status === 'OWNER') { + self.change_status_display('success'); + } else { + self.change_status_display('warning'); + } + setTimeout(loop, 3000); + }, + function (err) { + if (err.abort) { + // Stop the loop + return; + } + if (typeof err == "undefined") { + self.change_status_display('failure'); + } else { + self.change_status_display('not_found'); + self.pos.proxy.posbox_supports_display = false; + } + setTimeout(loop, 3000); + } + ); + } } loop(); }, @@ -563,12 +562,12 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, { this.logo_click_time = 0; this.logo_click_count = 0; - this.previous_touch_y_coordinate = -1; + this.previous_touch_y_coordinate = -1; this.widget = {}; // contains references to subwidgets instances this.cleanup_dom(); - this.pos.ready.done(function(){ + this.pos.ready.then(function(){ self.build_chrome(); self.build_widgets(); self.disable_rubberbanding(); @@ -577,7 +576,7 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, { self.loading_hide(); self.replace_crashmanager(); self.pos.push_order(); - }).fail(function(err){ // error when loading models data from the backend + }).guardedCatch(function (err) { // error when loading models data from the backend self.loading_error(err); }); }, @@ -859,6 +858,7 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, { // This method instantiates all the screens, widgets, etc. build_widgets: function() { + var self = this; this.load_widgets(this.widgets); this.screens = {}; @@ -874,15 +874,15 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, { } this.popups = {}; - for (i = 0; i < this.gui.popup_classes.length; i++) { - classe = this.gui.popup_classes[i]; - if (!classe.condition || classe.condition.call(this)) { - var popup = new classe.widget(this,{}); - popup.appendTo(this.$('.popups')); - this.popups[classe.name] = popup; - this.gui.add_popup(classe.name, popup); + _.forEach(this.gui.popup_classes, function (classe) { + if (!classe.condition || classe.condition.call(self)) { + var popup = new classe.widget(self,{}); + popup.appendTo(self.$('.popups')).then(function () { + self.popups[classe.name] = popup; + self.gui.add_popup(classe.name, popup); + }); } - } + }); this.gui.set_startup_screen('products'); this.gui.set_default_screen('products'); diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js index c1db7f61884..683ca3f6853 100644 --- a/addons/point_of_sale/static/src/js/devices.js +++ b/addons/point_of_sale/static/src/js/devices.js @@ -11,7 +11,7 @@ var QWeb = core.qweb; var _t = core._t; // the JobQueue schedules a sequence of 'jobs'. each job is -// a function returning a deferred. the queue waits for each job to finish +// a function returning a promise. The queue waits for each job to finish // before launching the next. Each job can also be scheduled with a delay. // the is used to prevent parallel requests to the proxy. @@ -19,17 +19,19 @@ var JobQueue = function(){ var queue = []; var running = false; var scheduled_end_time = 0; - var end_of_queue = (new $.Deferred()).resolve(); + var end_of_queue = Promise.resolve(); var stoprepeat = false; - var run = function(){ - if(end_of_queue.state() === 'resolved'){ - end_of_queue = new $.Deferred(); - } - if(queue.length > 0){ + var run = function () { + var runNextJob = function () { + if (queue.length === 0) { + running = false; + scheduled_end_time = 0; + return Promise.resolve(); + } running = true; var job = queue[0]; - if(!job.opts.repeat || stoprepeat){ + if (!job.opts.repeat || stoprepeat) { queue.shift(); stoprepeat = false; } @@ -37,30 +39,37 @@ var JobQueue = function(){ // the time scheduled for this job scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0); - // we run the job and put in def when it finishes - var def = job.fun() || (new $.Deferred()).resolve(); + // we run the job and put in prom when it finishes + var prom = job.fun() || Promise.resolve(); - // we don't care if a job fails ... - def.always(function(){ + var always = function () { // we run the next job after the scheduled_end_time, even if it finishes before - setTimeout(function(){ - run(); - }, Math.max(0, scheduled_end_time - (new Date()).getTime()) ); - }); - }else{ - running = false; - scheduled_end_time = 0; - end_of_queue.resolve(); + return new Promise(function (resolve, reject) { + setTimeout( + resolve, + Math.max(0, scheduled_end_time - (new Date()).getTime()) + ); + }); + }; + // we don't care if a job fails ... + return prom.then(always, always).then(runNextJob); + }; + + if (!running) { + end_of_queue = runNextJob(); } }; - // adds a job to the schedule. - // opts : { - // duration : the job is guaranteed to finish no quicker than this (milisec) - // repeat : if true, the job will be endlessly repeated - // important : if true, the scheduled job cannot be canceled by a queue.clear() - // } - this.schedule = function(fun, opts){ + /** + * Adds a job to the schedule. + * + * @param {function} fun must return a promise + * @param {object} [opts] + * @param {number} [opts.duration] the job is guaranteed to finish no quicker than this (milisec) + * @param {boolean} [opts.repeat] if true, the job will be endlessly repeated + * @param {boolean} [opts.important] if true, the scheduled job cannot be canceled by a queue.clear() + */ + this.schedule = function (fun, opts) { queue.push({fun:fun, opts:opts || {}}); if(!running){ run(); @@ -77,10 +86,13 @@ var JobQueue = function(){ stoprepeat = true; }; - // returns a deferred that resolves when all scheduled - // jobs have been run. - // ( jobs added after the call to this method are considered as well ) - this.finished = function(){ + /** + * Returns a promise that resolves when all scheduled jobs have been run. + * (jobs added after the call to this method are considered as well) + * + * @returns {Promise} + */ + this.finished = function () { return end_of_queue; }; @@ -153,11 +165,16 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ } }, - // connects to the specified url + /** + * Connects to the specified url. + * + * @param {string} url + * @returns {Promise} + */ connect: function(url){ var self = this; this.connection = new Session(undefined,url, { use_cors: true}); - this.host = url; + this.host = url; this.set_connection_status('connecting',{}); return this.message('handshake').then(function(response){ @@ -175,82 +192,99 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ }); }, - // find a proxy and connects to it. for options see find_proxy - // - force_ip : only try to connect to the specified ip. - // - port: what port to listen to (default 8069) - // - progress(fac) : callback for search progress ( fac in [0,1] ) - autoconnect: function(options){ + /** + * Find a proxy and connects to it. + * + * @param {Object} [options] + * @param {string} [options.force_ip] only try to connect to the specified ip. + * @param {string} [options.port] @see find_proxy + * @param {function} [options.progress] @see find_proxy + * @returns {Promise} + */ + autoconnect: function (options) { var self = this; this.set_connection_status('connecting',{}); - var found_url = new $.Deferred(); - var success = new $.Deferred(); + var found_url = new Promise(function () {}); - if ( options.force_ip ){ + if (options.force_ip) { // if the ip is forced by server config, bailout on fail found_url = this.try_hard_to_connect(options.force_ip, options); - }else if( localStorage.hw_proxy_url ){ + } else if (localStorage.hw_proxy_url) { // try harder when we remember a good proxy url found_url = this.try_hard_to_connect(localStorage.hw_proxy_url, options) - .then(null,function(){ - if (window.location.protocol != 'https:'){ + .catch(function () { + if (window.location.protocol != 'https:') { return self.find_proxy(options); } }); - }else{ + } else { // just find something quick if (window.location.protocol != 'https:'){ found_url = this.find_proxy(options); } } - success = found_url.then(function(url){ - return self.connect(url); - }); + var successProm = found_url.then(function (url) { + return self.connect(url); + }); - success.fail(function(){ + successProm.catch(function () { self.set_connection_status('disconnected'); }); - return success; + return successProm; }, // starts a loop that updates the connection status - keepalive: function(){ + keepalive: function () { var self = this; function status(){ + var always = function () { + setTimeout(status, 5000); + }; self.connection.rpc('/hw_proxy/status_json',{},{shadow: true, timeout:2500}) - .then(function(driver_status){ + .then(function (driver_status) { self.set_connection_status('connected',driver_status); - },function(){ + }, function () { if(self.get('status').status !== 'connecting'){ self.set_connection_status('disconnected'); } - }).always(function(){ - setTimeout(status,5000); - }); + }).then(always, always); } - if(!this.keptalive){ + if (!this.keptalive) { this.keptalive = true; status(); } }, - message : function(name,params){ + /** + * @param {string} name + * @param {Object} [params] + * @returns {Promise} + */ + message : function (name, params) { var callbacks = this.notifications[name] || []; - for(var i = 0; i < callbacks.length; i++){ + for (var i = 0; i < callbacks.length; i++) { callbacks[i](params); } - if(this.get('status').status !== 'disconnected'){ + if (this.get('status').status !== 'disconnected') { return this.connection.rpc('/hw_proxy/' + name, params || {}, {shadow: true}); - }else{ - return (new $.Deferred()).reject(); + } else { + return Promise.reject(); } }, - // try several time to connect to a known proxy url - try_hard_to_connect: function(url,options){ + /** + * Tries several time to connect to a known proxy url. + * + * @param {*} url + * @param {Object} [options] + * @param {string} [options.port=8069] what port to listen to + * @returns {Promise} + */ + try_hard_to_connect: function (url, options) { options = options || {}; var protocol = window.location.protocol; var port = ( !options.port && protocol == "https:") ? ':443' : ':' + (options.port || '8069'); @@ -266,35 +300,36 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ } // try real hard to connect to url, with a 1sec timeout and up to 'retries' retries - function try_real_hard_to_connect(url, retries, done){ - - done = done || new $.Deferred(); - - $.ajax({ - url: url + '/hw_proxy/hello', - method: 'GET', - timeout: 1000, - }) - .done(function(){ - done.resolve(url); - }) - .fail(function(resp){ - if(retries > 0){ - try_real_hard_to_connect(url,retries-1,done); - }else{ - done.reject(resp.statusText, url); - } - }); - return done; + function try_real_hard_to_connect(url, retries) { + return Promise.resolve( + $.ajax({ + url: url + '/hw_proxy/hello', + method: 'GET', + timeout: 1000, + }) + .then(function () { + return Promise.resolve(url); + }, function (resp) { + if (retries > 0) { + return try_real_hard_to_connect(url, retries-1); + } else { + return Promise.reject([resp.statusText, url]); + } + }) + ); } - return try_real_hard_to_connect(url,3); + return try_real_hard_to_connect(url, 3); }, - // returns as a deferred a valid host url that can be used as proxy. - // options: - // - port: what port to listen to (default 8069) - // - progress(fac) : callback for search progress ( fac in [0,1] ) + /** + * Returns as a promise a valid host url that can be used as proxy. + * + * @param {Object} [options] + * @param {string} [options.port] what port to listen to (default 8069) + * @param {function} [options.progress] callback for search progress ( fac in [0,1] ) + * @returns {Promise} will be resolved with the proxy valid url + */ find_proxy: function(options){ options = options || {}; var self = this; @@ -302,7 +337,6 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ var urls = []; var found = false; var parallel = 8; - var done = new $.Deferred(); // will be resolved with the proxies valid urls var threads = []; var progress = 0; @@ -323,51 +357,47 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ } } - function thread(done){ + function thread () { var url = urls.shift(); - done = done || new $.Deferred(); - - if( !url || found || !self.searching_for_proxy ){ - done.resolve(); - return done; + if (!url || found || !self.searching_for_proxy) { + return Promise.resolve(); } - $.ajax({ + return Promise.resolve( + $.ajax({ url: url + '/hw_proxy/hello', method: 'GET', timeout: 400, - }).done(function(){ + }).then(function () { found = true; update_progress(); - done.resolve(url); - }) - .fail(function(){ + return Promise.resolve(url); + }, function () { update_progress(); - thread(done); - }); - - return done; + return thread(); + }) + ); } this.searching_for_proxy = true; - var len = Math.min(parallel,urls.length); + var len = Math.min(parallel, urls.length); for(i = 0; i < len; i++){ threads.push(thread()); } - $.when.apply($,threads).then(function(){ - var urls = []; - for(var i = 0; i < arguments.length; i++){ - if(arguments[i]){ - urls.push(arguments[i]); + return new Promise(function (resolve, reject) { + Promise.all(threads).then(function(results){ + var urls = []; + for(var i = 0; i < results.length; i++){ + if(results[i]){ + urls.push(results[i]); + } } - } - done.resolve(urls[0]); + resolve(urls[0]); + }); }); - - return done; }, stop_searching: function(){ @@ -384,20 +414,24 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ this.notifications[name].push(callback); }, - // returns the weight on the scale. - scale_read: function(){ + /** + * Returns the weight on the scale. + * + * @returns {Promise} + */ + scale_read: function () { var self = this; - var ret = new $.Deferred(); if (self.use_debug_weight) { - return (new $.Deferred()).resolve({weight:this.debug_weight, unit:'Kg', info:'ok'}); + return Promise.resolve({weight:this.debug_weight, unit:'Kg', info:'ok'}); } - this.message('scale_read',{}) - .then(function(weight){ - ret.resolve(weight); - }, function(){ //failed to read weight - ret.resolve({weight:0.0, unit:'Kg', info:'ok'}); + return new Promise(function (resolve, reject) { + self.message('scale_read',{}) + .then(function (weight) { + resolve(weight); + }, function () { //failed to read weight + resolve({weight:0.0, unit:'Kg', info:'ok'}); }); - return ret; + }); }, // sets a custom weight, ignoring the proxy returned value. @@ -476,15 +510,22 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ } }, + /** + * @param {string} html + * @returns {Promise} + */ take_ownership_over_client_screen: function(html) { return this.message("take_control", { html: html }); }, + /** + * @returns {Promise} + */ test_ownership_of_client_screen: function() { if (this.connection) { return this.message("test_ownership", {}); } - return null; + return Promise.reject({abort: true}); }, // asks the proxy to log some information, as with the debug.log you can provide several arguments. diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index b1d99e2e199..0184b07a984 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -1,6 +1,6 @@ odoo.define('point_of_sale.gui', function (require) { "use strict"; -// this file contains the Gui, which is the pos 'controller'. +// this file contains the Gui, which is the pos 'controller'. // It contains high level methods to manipulate the interface // such as changing between screens, creating popups, etc. // @@ -24,7 +24,7 @@ var Gui = core.Class.extend({ this.default_screen = null; this.startup_screen = null; this.current_popup = null; - this.current_screen = null; + this.current_screen = null; this.chrome.ready.then(function(){ self.close_other_tabs(); @@ -55,7 +55,7 @@ var Gui = core.Class.extend({ // sets the screen that will be displayed // for new orders - set_default_screen: function(name){ + set_default_screen: function(name){ this.default_screen = name; }, @@ -73,8 +73,8 @@ var Gui = core.Class.extend({ options = options || {}; this.close_popup(); if (order) { - this.show_screen(order.get_screen_data('screen') || - options.default_screen || + this.show_screen(order.get_screen_data('screen') || + options.default_screen || this.default_screen, null,'refresh'); } else { @@ -82,7 +82,7 @@ var Gui = core.Class.extend({ } }, - // display a screen. + // display a screen. // If there is an order, the screen will be saved in the order // - params: used to load a screen with parameters, for // example loading a 'product_details' screen for a specific product. @@ -120,7 +120,7 @@ var Gui = core.Class.extend({ this.current_screen.show(refresh); } }, - + // returns the current screen. get_current_screen: function() { return this.pos.get_order() ? ( this.pos.get_order().get_screen_data('screen') || this.default_screen ) : this.startup_screen; @@ -213,7 +213,7 @@ var Gui = core.Class.extend({ msg.session == self.pos.pos_session.id && msg.window_uid != now) { - console.info('POS / Session opened in another window. EXITING POS') + console.info('POS / Session opened in another window. EXITING POS'); self._close(); } } @@ -224,28 +224,28 @@ var Gui = core.Class.extend({ /* ---- Gui: ACCESS CONTROL ---- */ // A Generic UI that allow to select a user from a list. - // It returns a deferred that resolves with the selected user + // It returns a promise that resolves with the selected user // upon success. Several options are available : // - security: passwords will be asked // - only_managers: restricts the list to managers - // - current_user: password will not be asked if this + // - current_user: password will not be asked if this // user is selected. // - title: The title of the employee selection list. - + // checks if the current user (or the user provided) has manager // access rights. If not, a popup is shown allowing the user to - // temporarily login as an administrator. - // This method returns a deferred, that succeeds with the + // temporarily login as an administrator. + // This method returns a promise, that succeeds with the // manager user when the login is successfull. sudo: function(user){ user = user || this.pos.get_cashier(); if (user.role === 'manager') { - return new $.Deferred().resolve(user); + return Promise.resolve(user); } else { return this.select_employee({ - security: true, + security: true, only_managers: true, title: _t('Login as a Manager'), }); @@ -261,14 +261,14 @@ var Gui = core.Class.extend({ if (!pending) { this._close(); } else { - this.pos.push_order().always(function() { + var always = function () { var pending = self.pos.db.get_orders().length; if (!pending) { self._close(); } else { - var reason = self.pos.get('failed') ? - 'configuration errors' : - 'internet connection issues'; + var reason = self.pos.get('failed') ? + 'configuration errors' : + 'internet connection issues'; self.show_popup('confirm', { 'title': _t('Offline Orders'), @@ -282,7 +282,8 @@ var Gui = core.Class.extend({ }, }); } - }); + }; + this.pos.push_order().then(always, always); } }, @@ -314,10 +315,10 @@ var Gui = core.Class.extend({ /* ---- Gui: FILE I/O ---- */ - // This will make the browser download 'contents' as a + // This will make the browser download 'contents' as a // file named 'name' // if 'contents' is not a string, it is converted into - // a JSON representation of the contents. + // a JSON representation of the contents. // TODO: remove me in master: deprecated in favor of prepare_download_link @@ -329,9 +330,9 @@ var Gui = core.Class.extend({ evt.initEvent("click"); $("",href_params).get(0).dispatchEvent(evt); - - }, - + + }, + prepare_download_link: function(contents, filename, src, target) { var href_params = this.prepare_file_blob(contents, filename); @@ -344,11 +345,11 @@ var Gui = core.Class.extend({ $(src).removeClass('oe_hidden'); $(this).addClass('oe_hidden'); }); - }, - + }, + prepare_file_blob: function(contents, name) { var URL = window.URL || window.webkitURL; - + if (typeof contents !== 'string') { contents = JSON.stringify(contents,null,2); } @@ -366,21 +367,21 @@ var Gui = core.Class.extend({ // prefilled. send_email: function(address, subject, body) { - window.open("mailto:" + address + + window.open("mailto:" + address + "?subject=" + (subject ? window.encodeURIComponent(subject) : '') + "&body=" + (body ? window.encodeURIComponent(body) : '')); }, /* ---- Gui: KEYBOARD INPUT ---- */ - // This is a helper to handle numpad keyboard input. + // This is a helper to handle numpad keyboard input. // - buffer: an empty or number string // - input: '[0-9],'+','-','.','CLEAR','BACKSPACE' // - options: 'firstinput' -> will clear buffer if // input is '[0-9]' or '.' // returns the new buffer containing the modifications - // (the original is not touched) - numpad_input: function(buffer, input, options) { + // (the original is not touched) + numpad_input: function(buffer, input, options) { var newbuf = buffer.slice(0); options = options || {}; var newbuf_float = newbuf === '-' ? newbuf : field_utils.parse.float(newbuf); @@ -395,8 +396,8 @@ var Gui = core.Class.extend({ newbuf = newbuf + decimal_point; } } else if (input === 'CLEAR') { - newbuf = ""; - } else if (input === 'BACKSPACE') { + newbuf = ""; + } else if (input === 'BACKSPACE') { newbuf = newbuf.substring(0,newbuf.length - 1); } else if (input === '+') { if ( newbuf[0] === '-' ) { diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 4cf483dfcde..2840b6b2866 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -23,12 +23,12 @@ var round_pr = utils.round_precision; var exports = {}; // The PosModel contains the Point Of Sale's representation of the backend. -// Since the PoS must work in standalone ( Without connection to the server ) -// it must contains a representation of the server's PoS backend. +// Since the PoS must work in standalone ( Without connection to the server ) +// it must contains a representation of the server's PoS backend. // (taxes, product list, configuration options, etc.) this representation -// is fetched and stored by the PosModel at the initialisation. -// this is done asynchronously, a ready deferred alows the GUI to wait interactively -// for the loading to be completed +// is fetched and stored by the PosModel at the initialisation. +// this is done asynchronously, a ready promise alows the GUI to wait interactively +// for the loading to be completed // There is a single instance of the PosModel for each Front-End instance, it is usually called // 'pos' and is available to all widgets extending PosWidget. @@ -116,48 +116,49 @@ exports.PosModel = Backbone.Model.extend({ this.barcode_reader.disconnect_from_proxy(); }, - connect_to_proxy: function(){ + connect_to_proxy: function () { var self = this; - var done = new $.Deferred(); - this.barcode_reader.disconnect_from_proxy(); - this.chrome.loading_message(_t('Connecting to the IoT Box'),0); - this.chrome.loading_skip(function(){ + return new Promise(function (resolve, reject) { + self.barcode_reader.disconnect_from_proxy(); + self.chrome.loading_message(_t('Connecting to the IoT Box'), 0); + self.chrome.loading_skip(function () { self.proxy.stop_searching(); }); - this.proxy.autoconnect({ + self.proxy.autoconnect({ force_ip: self.config.proxy_ip || undefined, progress: function(prog){ self.chrome.loading_progress(prog); }, }).then( - function(){ - if(self.config.iface_scan_via_proxy){ - self.barcode_reader.connect_to_proxy(); - } - done.resolve(); + function () { + if (self.config.iface_scan_via_proxy) { + self.barcode_reader.connect_to_proxy(); + } + resolve(); }, - function(statusText, url){ - if (statusText == 'error' && window.location.protocol == 'https:') { - var error = {message: 'TLSError', url: url}; - self.chrome.loading_error(error); - } else { - done.resolve(); - } - }); - return done; + function (statusText, url) { + if (statusText == 'error' && window.location.protocol == 'https:') { + var error = {message: 'TLSError', url: url}; + self.chrome.loading_error(error); + } else { + resolve(); + } + } + ); + }); }, // Server side model loaders. This is the list of the models that need to be loaded from // the server. The models are loaded one by one by this list's order. The 'loaded' callback // is used to store the data in the appropriate place once it has been loaded. This callback - // can return a deferred that will pause the loading of the next module. + // can return a promise that will pause the loading of the next module. // a shared temporary dictionary is available for loaders to communicate private variables // used during loading such as object ids, etc. models: [ { label: 'version', - loaded: function(self){ - return session.rpc('/web/webclient/version_info',{}).done(function(version) { + loaded: function (self) { + return session.rpc('/web/webclient/version_info',{}).then(function (version) { self.version = version; }); }, @@ -447,55 +448,51 @@ exports.PosModel = Backbone.Model.extend({ }, { label: 'fonts', loaded: function(){ - var fonts_loaded = new $.Deferred(); - // Waiting for fonts to be loaded to prevent receipt printing - // from printing empty receipt while loading Inconsolata - // ( The font used for the receipt ) - waitForWebfonts(['Lato','Inconsolata'], function(){ - fonts_loaded.resolve(); + return new Promise(function (resolve, reject) { + // Waiting for fonts to be loaded to prevent receipt printing + // from printing empty receipt while loading Inconsolata + // ( The font used for the receipt ) + waitForWebfonts(['Lato','Inconsolata'], function () { + resolve(); + }); + // The JS used to detect font loading is not 100% robust, so + // do not wait more than 5sec + setTimeout(resolve, 5000); }); - // The JS used to detect font loading is not 100% robust, so - // do not wait more than 5sec - setTimeout(function(){ - fonts_loaded.resolve(); - },5000); - - return fonts_loaded; }, },{ label: 'pictures', - loaded: function(self){ + loaded: function (self) { self.company_logo = new Image(); - var logo_loaded = new $.Deferred(); - self.company_logo.onload = function(){ - var img = self.company_logo; - var ratio = 1; - var targetwidth = 300; - var maxheight = 150; - if( img.width !== targetwidth ){ - ratio = targetwidth / img.width; - } - if( img.height * ratio > maxheight ){ - ratio = maxheight / img.height; - } - var width = Math.floor(img.width * ratio); - var height = Math.floor(img.height * ratio); - var c = document.createElement('canvas'); + return new Promise(function (resolve, reject) { + self.company_logo.onload = function () { + var img = self.company_logo; + var ratio = 1; + var targetwidth = 300; + var maxheight = 150; + if( img.width !== targetwidth ){ + ratio = targetwidth / img.width; + } + if( img.height * ratio > maxheight ){ + ratio = maxheight / img.height; + } + var width = Math.floor(img.width * ratio); + var height = Math.floor(img.height * ratio); + var c = document.createElement('canvas'); c.width = width; c.height = height; - var ctx = c.getContext('2d'); + var ctx = c.getContext('2d'); ctx.drawImage(self.company_logo,0,0, width, height); - self.company_logo_base64 = c.toDataURL(); - logo_loaded.resolve(); - }; - self.company_logo.onerror = function(){ - logo_loaded.reject(); - }; - self.company_logo.crossOrigin = "anonymous"; - self.company_logo.src = '/web/binary/company_logo' +'?dbname=' + session.db + '&_'+Math.random(); - - return logo_loaded; + self.company_logo_base64 = c.toDataURL(); + resolve(); + }; + self.company_logo.onerror = function () { + reject(); + }; + self.company_logo.crossOrigin = "anonymous"; + self.company_logo.src = '/web/binary/company_logo' + '?dbname=' + session.db + '&_' + Math.random(); + }); }, }, { label: 'barcodes', @@ -507,93 +504,94 @@ exports.PosModel = Backbone.Model.extend({ } ], - // loads all the needed data on the sever. returns a deferred indicating when all the data has loaded. + // loads all the needed data on the sever. returns a promise indicating when all the data has loaded. load_server_data: function(){ var self = this; - var loaded = new $.Deferred(); var progress = 0; var progress_step = 1.0 / self.models.length; var tmp = {}; // this is used to share a temporary state between models loaders - function load_model(index){ - if(index >= self.models.length){ - loaded.resolve(); - }else{ - var model = self.models[index]; - self.chrome.loading_message(_t('Loading')+' '+(model.label || model.model || ''), progress); + var loaded = new Promise(function (resolve, reject) { + function load_model(index) { + if (index >= self.models.length) { + resolve(); + } else { + var model = self.models[index]; + self.chrome.loading_message(_t('Loading')+' '+(model.label || model.model || ''), progress); - var cond = typeof model.condition === 'function' ? model.condition(self,tmp) : true; - if (!cond) { - load_model(index+1); - return; - } - - var fields = typeof model.fields === 'function' ? model.fields(self,tmp) : model.fields; - var domain = typeof model.domain === 'function' ? model.domain(self,tmp) : model.domain; - var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context || {}; - var ids = typeof model.ids === 'function' ? model.ids(self,tmp) : model.ids; - var order = typeof model.order === 'function' ? model.order(self,tmp): model.order; - progress += progress_step; - - if( model.model ){ - var params = { - model: model.model, - context: _.extend(context, session.user_context || {}), - }; - - if (model.ids) { - params.method = 'read'; - params.args = [ids, fields]; - } else { - params.method = 'search_read'; - params.domain = domain; - params.fields = fields; - params.orderBy = order; + var cond = typeof model.condition === 'function' ? model.condition(self,tmp) : true; + if (!cond) { + load_model(index+1); + return; } - rpc.query(params).then(function(result){ - try{ // catching exceptions in model.loaded(...) - $.when(model.loaded(self,result,tmp)) - .then(function(){ load_model(index + 1); }, - function(err){ loaded.reject(err); }); - }catch(err){ - console.error(err.message, err.stack); - loaded.reject(err); + var fields = typeof model.fields === 'function' ? model.fields(self,tmp) : model.fields; + var domain = typeof model.domain === 'function' ? model.domain(self,tmp) : model.domain; + var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context || {}; + var ids = typeof model.ids === 'function' ? model.ids(self,tmp) : model.ids; + var order = typeof model.order === 'function' ? model.order(self,tmp): model.order; + progress += progress_step; + + if( model.model ){ + var params = { + model: model.model, + context: _.extend(context, session.user_context || {}), + }; + + if (model.ids) { + params.method = 'read'; + params.args = [ids, fields]; + } else { + params.method = 'search_read'; + params.domain = domain; + params.fields = fields; + params.orderBy = order; } - },function(err){ - loaded.reject(err); - }); - }else if( model.loaded ){ - try{ // catching exceptions in model.loaded(...) - $.when(model.loaded(self,tmp)) - .then( function(){ load_model(index +1); }, - function(err){ loaded.reject(err); }); - }catch(err){ - loaded.reject(err); + + rpc.query(params).then(function (result) { + try { // catching exceptions in model.loaded(...) + Promise.resolve(model.loaded(self, result, tmp)) + .then(function () { load_model(index + 1); }, + function (err) { reject(err); }); + } catch (err) { + console.error(err.message, err.stack); + reject(err); + } + }, function (err) { + reject(err); + }); + } else if (model.loaded) { + try { // catching exceptions in model.loaded(...) + Promise.resolve(model.loaded(self, tmp)) + .then(function () { load_model(index +1); }, + function (err) { reject(err); }); + } catch (err) { + reject(err); + } + } else { + load_model(index + 1); } - }else{ - load_model(index + 1); } } - } - try{ - load_model(0); - }catch(err){ - loaded.reject(err); - } + try { + return load_model(0); + } catch (err) { + return Promise.reject(err); + } + }); return loaded; }, - // reload the list of partner, returns as a deferred that resolves if there were + // reload the list of partner, returns as a promise that resolves if there were // updated partners, and fails if not load_new_partners: function(){ var self = this; - var def = new $.Deferred(); - var fields = _.find(this.models,function(model){ return model.model === 'res.partner'; }).fields; - var domain = [['customer','=',true],['write_date','>',this.db.get_partner_write_date()]]; - rpc.query({ + return new Promise(function (resolve, reject) { + var fields = _.find(self.models, function(model){ return model.model === 'res.partner'; }).fields; + var domain = [['customer','=',true],['write_date','>',self.db.get_partner_write_date()]]; + rpc.query({ model: 'res.partner', method: 'search_read', args: [domain, fields], @@ -601,14 +599,14 @@ exports.PosModel = Backbone.Model.extend({ timeout: 3000, shadow: true, }) - .then(function(partners){ + .then(function (partners) { if (self.db.add_partners(partners)) { // check if the partners we got were real updates - def.resolve(); + resolve(); } else { - def.reject(); + reject(); } - }, function(type,err){ def.reject(); }); - return def; + }, function (type, err) { reject(); }); + }); }, // this is called when an order is removed from the order collection. It ensures that there is always an existing @@ -718,27 +716,26 @@ exports.PosModel = Backbone.Model.extend({ }, _convert_product_img_to_base64: function (product, url) { - var deferred = new $.Deferred(); - var img = new Image(); + return new Promise(function (resolve, reject) { + var img = new Image(); - img.onload = function () { - var canvas = document.createElement('CANVAS'); - var ctx = canvas.getContext('2d'); + img.onload = function () { + var canvas = document.createElement('CANVAS'); + var ctx = canvas.getContext('2d'); - canvas.height = this.height; - canvas.width = this.width; - ctx.drawImage(this,0,0); + canvas.height = this.height; + canvas.width = this.width; + ctx.drawImage(this,0,0); - var dataURL = canvas.toDataURL('image/jpeg'); - product.image_base64 = dataURL; - canvas = null; + var dataURL = canvas.toDataURL('image/jpeg'); + product.image_base64 = dataURL; + canvas = null; - deferred.resolve(); - }; - img.crossOrigin = 'use-credentials'; - img.src = url; - - return deferred; + resolve(); + }; + img.crossOrigin = 'use-credentials'; + img.src = url; + }); }, send_current_order_to_customer_facing_display: function() { @@ -748,6 +745,9 @@ exports.PosModel = Backbone.Model.extend({ }); }, + /** + * @returns {Promise} + */ render_html_for_customer_facing_display: function () { var self = this; var order = this.get_order(); @@ -758,7 +758,7 @@ exports.PosModel = Backbone.Model.extend({ // IoT Box is not logged in and thus doesn't have the access // rights to access product.product. So instead we'll base64 // encode it and embed it in the HTML. - var get_image_deferreds = []; + var get_image_promises = []; if (order) { order.get_orderlines().forEach(function (orderline) { @@ -767,13 +767,13 @@ exports.PosModel = Backbone.Model.extend({ // only download and convert image if we haven't done it before if (! product.image_base64) { - get_image_deferreds.push(self._convert_product_img_to_base64(product, image_url)); + get_image_promises.push(self._convert_product_img_to_base64(product, image_url)); } }); } // when all images are loaded in product.image_base64 - return $.when.apply($, get_image_deferreds).then(function () { + return Promise.all(get_image_promises).then(function () { var rendered_order_lines = ""; var rendered_payment_lines = ""; var order_total_with_tax = self.chrome.format_currency(0); @@ -813,87 +813,81 @@ exports.PosModel = Backbone.Model.extend({ }, // saves the order locally and try to send it to the backend. - // it returns a deferred that succeeds after having tried to send the order and all the other pending orders. - push_order: function(order, opts) { + // it returns a promise that succeeds after having tried to send the order and all the other pending orders. + push_order: function (order, opts) { opts = opts || {}; var self = this; - if(order){ + if (order) { this.db.add_order(order.export_as_JSON()); } - var pushed = new $.Deferred(); + return new Promise(function (resolve, reject) { + self.flush_mutex.exec(function () { + var flushed = self._flush_orders(self.db.get_orders(), opts); - this.flush_mutex.exec(function(){ - var flushed = self._flush_orders(self.db.get_orders(), opts); + flushed.then(resolve, resolve); - flushed.always(function(ids){ - pushed.resolve(); + return flushed; }); - - return flushed; }); - return pushed; }, // saves the order locally and try to send it to the backend and make an invoice - // returns a deferred that succeeds when the order has been posted and successfully generated + // returns a promise that succeeds when the order has been posted and successfully generated // an invoice. This method can fail in various ways: // error-no-client: the order must have an associated partner_id. You can retry to make an invoice once // this error is solved // error-transfer: there was a connection error during the transfer. You can retry to make the invoice once // the network connection is up - push_and_invoice_order: function(order){ + push_and_invoice_order: function (order) { var self = this; - var invoiced = new $.Deferred(); + var invoiced = new Promise(function (resolveInvoiced, rejectInvoiced) { + if(!order.get_client()){ + rejectInvoiced({code:400, message:'Missing Customer', data:{}}); + } + else { + var order_id = self.db.add_order(order.export_as_JSON()); - if(!order.get_client()){ - invoiced.reject({code:400, message:'Missing Customer', data:{}}); - return invoiced; - } + self.flush_mutex.exec(function () { + var done = new Promise(function (resolveDone, rejectDone) { + // send the order to the server + // we have a 30 seconds timeout on this push. + // FIXME: if the server takes more than 30 seconds to accept the order, + // the client will believe it wasn't successfully sent, and very bad + // things will happen as a duplicate will be sent next time + // so we must make sure the server detects and ignores duplicated orders - var order_id = this.db.add_order(order.export_as_JSON()); + var transfer = self._flush_orders([self.db.get_order(order_id)], {timeout:30000, to_invoice:true}); - this.flush_mutex.exec(function(){ - var done = new $.Deferred(); // holds the mutex + transfer.catch(function (error) { + rejectInvoiced(error); + rejectDone(); + }); - // send the order to the server - // we have a 30 seconds timeout on this push. - // FIXME: if the server takes more than 30 seconds to accept the order, - // the client will believe it wasn't successfully sent, and very bad - // things will happen as a duplicate will be sent next time - // so we must make sure the server detects and ignores duplicated orders - - var transfer = self._flush_orders([self.db.get_order(order_id)], {timeout:30000, to_invoice:true}); - - transfer.fail(function(error){ - invoiced.reject(error); - done.reject(); - }); - - // on success, get the order id generated by the server - transfer.pipe(function(order_server_id){ - - // generate the pdf and download it - if (order_server_id.length) { - self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{ - active_ids:order_server_id, - }}).done(function () { - invoiced.resolve(); - done.resolve(); + // on success, get the order id generated by the server + transfer.then(function(order_server_id){ + // generate the pdf and download it + if (order_server_id.length) { + self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{ + active_ids:order_server_id, + }}).then(function () { + resolveInvoiced(); + resolveDone(); + }); + } else { + // The order has been pushed separately in batch when + // the connection came back. + // The user has to go to the backend to print the invoice + rejectInvoiced({code:401, message:'Backend Invoice', data:{order: order}}); + rejectDone(); + } + }); + return done; }); - } else { - // The order has been pushed separately in batch when - // the connection came back. - // The user has to go to the backend to print the invoice - invoiced.reject({code:401, message:'Backend Invoice', data:{order: order}}); - done.reject(); - } - }); - - return done; - + }); + } }); return invoiced; @@ -904,7 +898,7 @@ exports.PosModel = Backbone.Model.extend({ var self = this; this.set('synch',{ state: 'connecting', pending: orders.length}); - return self._save_to_server(orders, options).done(function (server_ids) { + return self._save_to_server(orders, options).then(function (server_ids) { var pending = self.db.get_orders().length; self.set('synch', { @@ -913,7 +907,7 @@ exports.PosModel = Backbone.Model.extend({ }); return server_ids; - }).fail(function(error, event){ + }).catch(function(){ var pending = self.db.get_orders().length; if (self.get('failed')) { self.set('synch', { state: 'error', pending: pending }); @@ -926,13 +920,11 @@ exports.PosModel = Backbone.Model.extend({ // send an array of orders to the server // available options: // - timeout: timeout for the rpc call in ms - // returns a deferred that resolves with the list of + // returns a promise that resolves with the list of // server generated ids for the sent orders _save_to_server: function (orders, options) { if (!orders || !orders.length) { - var result = $.Deferred(); - result.resolve([]); - return result; + return Promise.resolve([]); } options = options || {}; @@ -966,7 +958,8 @@ exports.PosModel = Backbone.Model.extend({ }); self.set('failed',false); return server_ids; - }).fail(function (type, error){ + }).catch(function (reason){ + var error = reason.message; if(error.code === 200 ){ // Business Logic Error, not a connection problem //if warning do not need to display traceback!! if (error.data.exception_type == 'warning') { @@ -1152,7 +1145,7 @@ exports.load_fields = function(model_name, fields) { // Loads openerp models at the point of sale startup. // load_models take an array of model loader declarations. -// - The models will be loaded in the array order. +// - The models will be loaded in the array order. // - If no openerp model name is provided, no server data // will be loaded, but the system can be used to preprocess // data before load. @@ -1168,9 +1161,9 @@ exports.load_fields = function(model_name, fields) { // models: [{ // model: [string] the name of the openerp model to load. // label: [string] The label displayed during load. -// fields: [[string]|function] the list of fields to be loaded. +// fields: [[string]|function] the list of fields to be loaded. // Empty Array / Null loads all fields. -// order: [[string]|function] the models will be ordered by +// order: [[string]|function] the models will be ordered by // the provided fields // domain: [domain|function] the domain that determines what // models need to be loaded. Null loads everything @@ -1179,9 +1172,9 @@ exports.load_fields = function(model_name, fields) { // context: [Dict|function] the openerp context for the model read // condition: [function] do not load the models if it evaluates to // false. -// loaded: [function(self,model)] this function is called once the +// loaded: [function(self,model)] this function is called once the // models have been loaded, with the data as second argument -// if the function returns a deferred, the next model will +// if the function returns a promise, the next model will // wait until it resolves before loading. // }] // @@ -1311,7 +1304,7 @@ exports.Product = Backbone.Model.extend({ var orderline_id = 1; // An orderline represent one element of the content of a client's shopping cart. -// An orderline contains a product, its quantity, its price, discount. etc. +// An orderline contains a product, its quantity, its price, discount. etc. // An Order contains zero or more Orderlines. exports.Orderline = Backbone.Model.extend({ initialize: function(attr,options){ @@ -1980,8 +1973,8 @@ var PaymentlineCollection = Backbone.Collection.extend({ model: exports.Paymentline, }); -// An order more or less represents the content of a client's shopping cart (the OrderLines) -// plus the associated payment information (the Paymentlines) +// An order more or less represents the content of a client's shopping cart (the OrderLines) +// plus the associated payment information (the Paymentlines) // there is always an active ('selected') order in the Pos, a new one is created // automaticaly once an order is completed and sent to the server. exports.Order = Backbone.Model.extend({ diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 2d017af9934..56e460c4a85 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -1107,7 +1107,7 @@ var ClientListScreenWidget = ScreenWidget.extend({ this.display_client_details('show',this.old_client,0); } - this.$('.client-list-contents').delegate('.client-line','click',function(event){ + this.$('.client-list-contents').on('click', '.client-line', function(event){ self.line_select(event,$(this),parseInt($(this).data('id'))); }); @@ -1317,11 +1317,16 @@ var ClientListScreenWidget = ScreenWidget.extend({ contents.on('click','.button.save',function(){ self.save_client_details(partner); }); }); }, - + // what happens when we've just pushed modifications for a partner of id partner_id - saved_client_details: function(partner_id){ + saved_client_details: function (partner_id) { var self = this; - this.reload_partners().then(function(){ + var always = function () { + $(".client-details-contents").on('click', '.button.save', function () { + self.save_client_details(partner); + }); + }; + this.reload_partners().then( function() { var partner = self.pos.db.get_partner_by_id(partner_id); if (partner) { self.new_client = partner; @@ -1329,12 +1334,10 @@ var ClientListScreenWidget = ScreenWidget.extend({ self.display_client_details('show',partner); } else { // should never happen, because create_from_ui must return the id of the partner it - // has created, and reload_partner() must have loaded the newly created partner. + // has created, and reload_partner() must have loaded the newly created partner. self.display_client_details('hide'); } - }).always(function(){ - $(".client-details-contents").on('click','.button.save',function(){ self.save_client_details(partner); }); - }); + }).then(always, always); }, // resizes an image, keeping the aspect ratio intact, @@ -2081,7 +2084,7 @@ var PaymentScreenWidget = ScreenWidget.extend({ var invoiced = this.pos.push_and_invoice_order(order); this.invoicing = true; - invoiced.fail(function(error){ + invoiced.catch(function (error) { self.invoicing = false; order.finalized = false; if (error.message === 'Missing Customer') { @@ -2121,7 +2124,7 @@ var PaymentScreenWidget = ScreenWidget.extend({ } }); - invoiced.done(function(){ + invoiced.then(function () { self.invoicing = false; self.gui.show_screen('receipt'); }); diff --git a/addons/point_of_sale/static/src/js/tests.js b/addons/point_of_sale/static/src/js/tests.js index 59cddd0f352..027af38dcbb 100644 --- a/addons/point_of_sale/static/src/js/tests.js +++ b/addons/point_of_sale/static/src/js/tests.js @@ -39,7 +39,7 @@ odoo.define('point_of_sale.tour.pricelist', function (require) { var backend_price = backend_result[0].price; assert(frontend_price === backend_price, JSON.stringify(debug_info) + ' DOESN\'T MATCH -> ' + backend_price + ' (backend) != ' + frontend_price + ' (frontend)'); - return (new $.Deferred()).resolve(); + return Promise.resolve(); }); }; } diff --git a/addons/portal/static/src/js/portal.js b/addons/portal/static/src/js/portal.js index 08e37bb2681..8c117167699 100644 --- a/addons/portal/static/src/js/portal.js +++ b/addons/portal/static/src/js/portal.js @@ -34,7 +34,7 @@ publicWidget.registry.portalDetails = publicWidget.Widget.extend({ var countryID = ($country.val() || 0); this.$stateOptions.detach(); var $displayedState = this.$stateOptions.filter('[data-country_id=' + countryID + ']'); - var nb = $displayedState.appendTo(this.$state).show().size(); + var nb = $displayedState.appendTo(this.$state).show().length; this.$state.parent().toggle(nb >= 1); }, diff --git a/addons/portal/static/src/js/portal_chatter.js b/addons/portal/static/src/js/portal_chatter.js index 081a8f02ad3..56a833a03a7 100644 --- a/addons/portal/static/src/js/portal_chatter.js +++ b/addons/portal/static/src/js/portal_chatter.js @@ -44,7 +44,7 @@ var PortalComposer = publicWidget.Widget.extend({ * @private */ _onSubmitButtonClick: function () { - return $.Deferred(); + return new Promise(function() {}); }, }); @@ -102,10 +102,10 @@ var PortalChatter = publicWidget.Widget.extend({ * @override */ willStart: function () { - return $.when( + return Promise.all([ this._super.apply(this, arguments), this._chatterInit() - ); + ]); }, /** * @override @@ -133,7 +133,7 @@ var PortalChatter = publicWidget.Widget.extend({ defs.push(this._composer.replace(this.$('.o_portal_chatter_composer'))); } - return $.when.apply($, defs); + return Promise.all(defs); }, //-------------------------------------------------------------------------- @@ -145,7 +145,7 @@ var PortalChatter = publicWidget.Widget.extend({ * current page and current domain. * * @param {Array} domain - * @returns {Deferred} + * @returns {Promise} */ messageFetch: function (domain) { var self = this; @@ -307,7 +307,7 @@ publicWidget.registry.portalChatter = publicWidget.Widget.extend({ var defs = [this._super.apply(this, arguments)]; var chatter = new PortalChatter(this, this.$el.data()); defs.push(chatter.appendTo(this.$el)); - return $.when.apply($, defs); + return Promise.all(defs); }, }); diff --git a/addons/portal/static/src/js/portal_signature.js b/addons/portal/static/src/js/portal_signature.js index 0174eeacdfe..471ea3b1e4a 100644 --- a/addons/portal/static/src/js/portal_signature.js +++ b/addons/portal/static/src/js/portal_signature.js @@ -129,7 +129,7 @@ var SignatureForm = publicWidget.Widget.extend({ window.location.reload(); } // no resolve if we reload the page - return $.Deferred(); + return new Promise(function () { }); } }); }, @@ -180,10 +180,10 @@ publicWidget.registry.SignatureForm = publicWidget.Widget.extend({ } }); - return $.when( + return Promise.all([ this._super.apply(this, arguments), form.appendTo(this.$el) - ); + ]); }, }); diff --git a/addons/pos_hr/static/src/js/gui.js b/addons/pos_hr/static/src/js/gui.js index 60694041920..1f1887d69f6 100644 --- a/addons/pos_hr/static/src/js/gui.js +++ b/addons/pos_hr/static/src/js/gui.js @@ -16,7 +16,6 @@ gui.Gui.include({ select_employee: function(options) { options = options || {}; var self = this; - var def = new $.Deferred(); var list = []; this.pos.employees.forEach(function(employee) { @@ -28,15 +27,19 @@ gui.Gui.include({ } }); - this.show_popup('selection', { - title: options.title || _t('Select User'), - list: list, - confirm: function(employee){ def.resolve(employee); }, - cancel: function(){ def.reject(); }, - is_selected: function(employee){ return employee === self.pos.get_cashier(); }, + var prom = new Promise(function (resolve, reject) { + this.show_popup('selection', { + title: options.title || _t('Select User'), + list: list, + confirm: resolve, + cancel: reject, + is_selected: function (employee) { + return employee === self.pos.get_cashier(); + }, + }); }); - return def.then(function(employee){ + return prom.then(function (employee) { return self.ask_password(employee.pin).then(function(){ return employee; }); @@ -44,27 +47,28 @@ gui.Gui.include({ }, // Ask for a password, and checks if it this // the same as specified by the function call. - // returns a deferred that resolves on success, + // Returns a promise that resolves on success, // fails on failure. ask_password: function(password) { var self = this; - var ret = new $.Deferred(); - if (password) { - this.show_popup('password',{ - 'title': _t('Password ?'), - confirm: function(pw) { - if (pw !== password) { - self.show_popup('error',_t('Incorrect Password')); - ret.reject(); - } else { - ret.resolve(); - } - }, - }); - } else { - ret.resolve(); - } - return ret; + var prom = new Promise(function (resolve, reject) { + if (password) { + self.show_popup('password',{ + 'title': _t('Password ?'), + confirm: function (pw) { + if (pw !== password) { + self.show_popup('error', _t('Incorrect Password')); + reject(); + } else { + resolve(); + } + }, + }); + } else { + resolve(); + } + }); + return prom; }, }); }); \ No newline at end of file diff --git a/addons/pos_mercury/static/src/js/pos_mercury.js b/addons/pos_mercury/static/src/js/pos_mercury.js index 733330c9c7a..818a17db300 100644 --- a/addons/pos_mercury/static/src/js/pos_mercury.js +++ b/addons/pos_mercury/static/src/js/pos_mercury.js @@ -491,7 +491,7 @@ PaymentScreenWidget.include({ } } - }).fail(function (type, error) { + }).catch(function () { self.retry_mercury_transaction(def, null, retry_nr, false, self.credit_code_transaction, [parsed_result, def, retry_nr + 1]); }); }, @@ -606,7 +606,7 @@ PaymentScreenWidget.include({ }); } } - }).fail(function (type, error) { + }).catch(function () { self.retry_mercury_transaction(def, null, retry_nr, false, self.do_reversal, [line, is_voidsale, def, retry_nr + 1]); }); }, diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js index 213b0afef09..da8ae75425e 100644 --- a/addons/pos_restaurant/static/src/js/floors.js +++ b/addons/pos_restaurant/static/src/js/floors.js @@ -421,7 +421,7 @@ var FloorScreenWidget = screens.ScreenWidget.extend({ method: 'write', args: [[this.floor.id], {'background_color': background}], }) - .fail(function (type, err){ + .guardedCatch(function (){ self.gui.show_popup('error',{ 'title':_t('Changes could not be saved'), 'body': _t('You must be connected to the internet to save your changes.'), diff --git a/addons/project/static/tests/project_tests.js b/addons/project/static/tests/project_tests.js index 1e33ff2d15d..08900370bca 100644 --- a/addons/project/static/tests/project_tests.js +++ b/addons/project/static/tests/project_tests.js @@ -70,9 +70,9 @@ QUnit.module('project', { }, function () { QUnit.module('image test'); - QUnit.test('cover_image_test', function (assert) { + QUnit.test('cover_image_test', async function (assert) { assert.expect(6); - var kanban = createView({ + var kanban = await createView({ View: KanbanView, model: 'project.task', data: this.data, @@ -98,7 +98,7 @@ QUnit.module('project', { '', mockRPC: function(route, args) { if (args.model === 'ir.attachment' && args.method === 'search_read') { - return $.when([{ + return Promise.resolve([{ id: 1, name: "1.png" },{ @@ -107,29 +107,30 @@ QUnit.module('project', { }]); } if (args.model === 'project.task' && args.method === 'write') { - assert.step(args.args[0][0]); + assert.step(String(args.args[0][0])); return this._super(route, args); } return this._super(route, args); }, }); - testUtils.dom.click(kanban.$('.o_dropdown_kanban:first > a.dropdown-toggle')); - testUtils.dom.click(kanban.$('.o_dropdown_kanban:first [data-type=set_cover]')); + await testUtils.dom.click(kanban.$('.o_dropdown_kanban:first > a.dropdown-toggle')); + await testUtils.dom.click(kanban.$('.o_dropdown_kanban:first [data-type=set_cover]')); assert.containsNone(kanban, 'img', "Initially there is no image."); // single click on image, and confirm - testUtils.dom.click($('.modal').find("img[data-id='1']")); - testUtils.dom.click($('.modal-footer .btn-primary')); + await testUtils.dom.click($('.modal').find("img[data-id='1']")); + await testUtils.dom.click($('.modal-footer .btn-primary')); assert.containsOnce(kanban, 'img[data-src*="/web/image/1"]'); - testUtils.dom.click(kanban.$('.o_dropdown_kanban:nth(1) > a.dropdown-toggle')); - testUtils.dom.click(kanban.$('.o_dropdown_kanban:nth(1) [data-type=set_cover]')); + await testUtils.dom.click(kanban.$('.o_dropdown_kanban:nth(1) > a.dropdown-toggle')); + await testUtils.dom.click(kanban.$('.o_dropdown_kanban:nth(1) [data-type=set_cover]')); // double click on image $('.modal').find("img[data-id='2']").dblclick(); + await testUtils.nextTick(); assert.containsOnce(kanban, 'img[data-src*="/web/image/2"]'); // verify writes on both kanban records - assert.verifySteps([1, 2]); + assert.verifySteps(['1', '2']); kanban.destroy(); }); diff --git a/addons/sale/static/src/js/product_configurator_controller.js b/addons/sale/static/src/js/product_configurator_controller.js index bf45779bd31..7ca3d32c292 100644 --- a/addons/sale/static/src/js/product_configurator_controller.js +++ b/addons/sale/static/src/js/product_configurator_controller.js @@ -94,14 +94,12 @@ var ProductConfiguratorFormController = FormController.extend({ ]; var productId = parseInt($modal.find(productSelector.join(', ')).first().val(), 10); - var productReady = this.renderer.selectOrCreateProduct( + this.renderer.selectOrCreateProduct( $modal, productId, $modal.find('.product_template_id').val(), false - ); - - productReady.done(function (productId) { + ).then(function (productId) { $modal.find(productSelector.join(', ')).val(productId); var variantValues = self diff --git a/addons/sale/static/src/js/product_configurator_mixin.js b/addons/sale/static/src/js/product_configurator_mixin.js index 1a74764aacc..1bf7cca730a 100644 --- a/addons/sale/static/src/js/product_configurator_mixin.js +++ b/addons/sale/static/src/js/product_configurator_mixin.js @@ -280,25 +280,25 @@ var ProductConfiguratorMixin = { }, /** - * Will return a deferred: + * Will return a promise: * * - If the product already exists, immediately resolves it with the product_id * - If the product does not exist yet ("dynamic" variant creation), this method will - * create the product first and then resolve the deferred with the created product's id + * create the product first and then resolve the promise with the created product's id * * @param {$.Element} $container the container to look into * @param {integer} productId the product id * @param {integer} productTemplateId the corresponding product template id * @param {boolean} useAjax wether the rpc call should be done using ajax.jsonRpc or using _rpc - * @returns {$.Deferred} the deferred that will be resolved with a {integer} productId + * @returns {Promise} the promise that will be resolved with a {integer} productId */ selectOrCreateProduct: function ($container, productId, productTemplateId, useAjax) { var self = this; productId = parseInt(productId); productTemplateId = parseInt(productTemplateId); - var productReady = $.Deferred(); + var productReady = Promise.resolve(); if (productId) { - productReady.resolve(productId); + productReady = Promise.resolve(productId); } else { var params = { product_template_id: productTemplateId, diff --git a/addons/sale/static/src/js/product_configurator_modal.js b/addons/sale/static/src/js/product_configurator_modal.js index c30c10b92b9..e8ad60b66d6 100644 --- a/addons/sale/static/src/js/product_configurator_modal.js +++ b/addons/sale/static/src/js/product_configurator_modal.js @@ -103,7 +103,7 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, ProductConfiguratorMixi }); var parentInit = self._super.apply(self, arguments); - return $.when(getModalContent, parentInit); + return Promise.all([getModalContent, parentInit]); }, /** @@ -124,7 +124,7 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, ProductConfiguratorMixi self.$modal.removeAttr("aria-hidden"); self.$modal.modal().appendTo(self.container); self.$modal.focus(); - self._opened.resolve(); + self._openedResolver(); } }); if (options && options.shouldFocusButtons) { @@ -353,14 +353,12 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, ProductConfiguratorMixi $main_product.after($parent); } - var productReady = this.selectOrCreateProduct( + this.selectOrCreateProduct( $parent, $parent.find('.product_id').val(), productTemplateId, true - ); - - productReady.done(function (productId){ + ).then(function (productId) { $parent.find('.product_id').val(productId); ajax.jsonRpc(self._getUri("/product_configurator/optional_product_items"), 'call', { diff --git a/addons/sale/static/src/js/product_configurator_renderer.js b/addons/sale/static/src/js/product_configurator_renderer.js index 8d16f2a329a..f3f437461ea 100644 --- a/addons/sale/static/src/js/product_configurator_renderer.js +++ b/addons/sale/static/src/js/product_configurator_renderer.js @@ -16,8 +16,10 @@ var ProductConfiguratorFormRenderer = FormRenderer.extend(ProductConfiguratorMix * @override */ start: function () { - this._super.apply(this, arguments); - this.$el.append($('
', {class: 'configurator_container'})); + var self = this; + return this._super.apply(this, arguments).then(function () { + self.$el.append($('
', {class: 'configurator_container'})); + }); }, //-------------------------------------------------------------------------- diff --git a/addons/sale/static/src/js/sale.js b/addons/sale/static/src/js/sale.js index 9d2d9313bac..b81e6ea91c7 100644 --- a/addons/sale/static/src/js/sale.js +++ b/addons/sale/static/src/js/sale.js @@ -39,7 +39,6 @@ KanbanRecord.include({ * Mostly a handler for what happens to the input "this.$target_input" * * @private - * @param {JqueryEvent} ev * */ _onSalesTeamTargetSet: function () { @@ -48,10 +47,12 @@ KanbanRecord.include({ if (isNaN(value)) { this.do_warn(_t("Wrong value entered!"), _t("Only Integer Value should be valid.")); } else { - this.trigger_up('kanban_record_update', {invoiced_target: value}); - this.trigger_up('reload'); - // TODO: previous lines can be refactored as follows (in master) - // self.trigger_up('kanban_record_update', {invoiced_target: value}); + this.trigger_up('kanban_record_update', { + invoiced_target: value, + onSuccess: function () { + self.trigger_up('reload'); + }, + }); } }, }); diff --git a/addons/sale/static/tests/product_configurator.test.js b/addons/sale/static/tests/product_configurator.test.js index df7046b2a20..c751eb6c111 100644 --- a/addons/sale/static/tests/product_configurator.test.js +++ b/addons/sale/static/tests/product_configurator.test.js @@ -91,12 +91,12 @@ odoo.define('sale.product.configurator.tests', function (require) { } } }; - } + }, }, function (){ - QUnit.test('click on "Configure a product" and check for form loading', function (assert) { + QUnit.test('click on "Configure a product" and check for form loading', async function (assert) { assert.expect(2); - var form = createView({ + var form = await createView({ View: FormView, model: 'sale_order', data: this.data, @@ -104,25 +104,26 @@ odoo.define('sale.product.configurator.tests', function (require) { mockRPC: function (route) { if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') { assert.ok(true); - return $.Deferred().then(_.constant(1)); + return Promise.resolve(1); } return this._super.apply(this, arguments); }, }); - assert.strictEqual(form.$("a:contains('Configure a product')").length, 1); + assert.containsOnce(form, "a:contains('Configure a product')"); - testUtils.dom.click(form.$("a:contains('Configure a product')")); + await testUtils.dom.click(form.$("a:contains('Configure a product')")); + form.destroy(); }); - QUnit.test('trigger_up the "add_record" event and checks that rows are correctly added to the list', function (assert) { + QUnit.test('trigger_up the "add_record" event and checks that rows are correctly added to the list', async function (assert) { assert.expect(1); - var form = createView({ + var form = await createView({ View: FormView, model: 'sale_order', data: this.data, - arch: getArch() + arch: getArch(), }); var list = form.renderer.allFieldWidgets[form.handle][1]; @@ -132,14 +133,16 @@ odoo.define('sale.product.configurator.tests', function (require) { forceEditable: "bottom" , allowWarning: true }); + await testUtils.nextTick(); - assert.strictEqual(list.$("tr.o_data_row").length, 2); + assert.containsN(list, "tr.o_data_row", 2); + form.destroy(); }); - QUnit.test('Select a product in the list and check for template loading', function (assert){ + QUnit.test('Select a product in the list and check for template loading', async function (assert){ assert.expect(1); - var product_configurator_form = createView({ + var product_configurator_form = await createView({ View: ProductConfiguratorFormView, model: 'sale_product_configurator', data: this.data, @@ -156,13 +159,14 @@ odoo.define('sale.product.configurator.tests', function (require) { mockRPC: function (route) { if (route === '/product_configurator/configure') { assert.ok(true); - return $.Deferred().then(_.constant(1)); + return Promise.resolve('
plop
'); } return this._super.apply(this, arguments); } }); - testUtils.dom.click(product_configurator_form.$('.o_input')); - testUtils.dom.click($("ul.ui-autocomplete li a:contains('Customizable Desk')").mouseenter()); + await testUtils.dom.click(product_configurator_form.$('.o_input')); + await testUtils.dom.click($("ul.ui-autocomplete li a:contains('Customizable Desk')").mouseenter()); + product_configurator_form.destroy(); }); }); }); diff --git a/addons/sale/static/tests/sales_team_dashboard_tests.js b/addons/sale/static/tests/sales_team_dashboard_tests.js index 5a79c1ff4d4..a9ab7604d93 100644 --- a/addons/sale/static/tests/sales_team_dashboard_tests.js +++ b/addons/sale/static/tests/sales_team_dashboard_tests.js @@ -22,10 +22,10 @@ QUnit.module('Sales Team Dashboard', { } }); -QUnit.test('edit target with several o_kanban_primary_bottom divs [REQUIRE FOCUS]', function (assert) { +QUnit.test('edit target with several o_kanban_primary_bottom divs [REQUIRE FOCUS]', async function (assert) { assert.expect(6); - var kanban = createView({ + var kanban = await createView({ View: KanbanView, model: 'crm.team', data: this.data, @@ -60,24 +60,24 @@ QUnit.test('edit target with several o_kanban_primary_bottom divs [REQUIRE FOCUS assert.containsN(kanban, '.o_kanban_primary_bottom', 2, "should have two divs with classname 'o_kanban_primary_bottom'"); - testUtils.dom.click(kanban.$('a.sales_team_target_definition')); + await testUtils.dom.click(kanban.$('a.sales_team_target_definition')); assert.containsOnce(kanban, '.o_kanban_primary_bottom:last input', "should have rendered an input in the last o_kanban_primary_bottom div"); kanban.$('.o_kanban_primary_bottom:last input').focus(); kanban.$('.o_kanban_primary_bottom:last input').val('123'); - kanban.$('.o_kanban_primary_bottom:last input').blur(); - + kanban.$('.o_kanban_primary_bottom:last input').trigger('blur'); + await testUtils.nextTick(); assert.strictEqual(kanban.$('.o_kanban_record').text(), "123Click to define a target", 'The kanban record should display the updated target value'); kanban.destroy(); }); -QUnit.test('edit target supports push Enter', function (assert) { +QUnit.test('edit target supports push Enter', async function (assert) { assert.expect(3); - var kanban = createView({ + var kanban = await createView({ View: KanbanView, model: 'crm.team', data: this.data, @@ -107,12 +107,12 @@ QUnit.test('edit target supports push Enter', function (assert) { }, }); - kanban.$('a.sales_team_target_definition').click(); + await testUtils.dom.click(kanban.$('a.sales_team_target_definition')); kanban.$('.o_kanban_primary_bottom:last input').focus(); kanban.$('.o_kanban_primary_bottom:last input').val('123'); kanban.$('.o_kanban_primary_bottom:last input').trigger($.Event('keydown', {which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER})); - + await testUtils.nextTick(); assert.strictEqual(kanban.$('.o_kanban_record').text(), "123Click to define a target", 'The kanban record should display the updated target value'); diff --git a/addons/sale_stock/static/src/js/tour.js b/addons/sale_stock/static/src/js/tour.js index d27080305d5..9127714cf15 100644 --- a/addons/sale_stock/static/src/js/tour.js +++ b/addons/sale_stock/static/src/js/tour.js @@ -37,7 +37,9 @@ odoo.define("sale_stock.tour", function (require) { // Check if sale_management is installed since sale_stock is adding an extra // step to add to SO (not enough inventory) - if ('sale.product_configurator_pricelist_tour' in odoo.__DEBUG__.services) { + // This works under 2 assumptions: this file must be after the file defining + // the tour in the assets, and the module defining the tour must be sync + if ('sale.product_configurator_pricelist_tour' in odoo.__DEBUG__.factories) { var steps = tour.tours.sale_product_configurator_pricelist_tour.steps; for (var k=0; k', }); - assert.ok(form.$('.o_sms_count').length, "Should have a sms counter"); - assert.strictEqual(form.$('.o_sms_count').text(), '0 chars, fits in 0 SMS (GSM7) ', 'Should be "0 chars, fits in 0 SMS (GSM7) " by default'); + assert.containsOnce(form, '.o_sms_count', "Should have a sms counter"); + assert.strictEqual(form.$('.o_sms_count').text(), '0 chars, fits in 0 SMS (GSM7) ', + 'Should be "0 chars, fits in 0 SMS (GSM7) " by default'); // GSM-7 - form.$('.o_input').val("Hello from Odoo").trigger('input'); - assert.strictEqual(form.$('.o_sms_count').text(), '15 chars, fits in 1 SMS (GSM7) ', 'Should be "15 chars, fits in 1 SMS (GSM7) " for "Hello from Odoo"'); + await testUtils.fields.editAndTrigger(form.$('.o_input'), "Hello from Odoo", 'input'); + assert.strictEqual(form.$('.o_sms_count').text(), '15 chars, fits in 1 SMS (GSM7) ', + 'Should be "15 chars, fits in 1 SMS (GSM7) " for "Hello from Odoo"'); // GSM-7 with \n => this one count as 2 characters form.$('.o_input').val("Hello from Odoo\n").trigger('input'); - assert.strictEqual(form.$('.o_sms_count').text(), '17 chars, fits in 1 SMS (GSM7) ', 'Should be "17 chars, fits in 1 SMS (GSM7) " for "Hello from Odoo\\n"'); + assert.strictEqual(form.$('.o_sms_count').text(), '17 chars, fits in 1 SMS (GSM7) ', + 'Should be "17 chars, fits in 1 SMS (GSM7) " for "Hello from Odoo\\n"'); // Unicode => ê form.$('.o_input').val("Hêllo from Odoo").trigger('input'); - assert.strictEqual(form.$('.o_sms_count').text(), '15 chars, fits in 1 SMS (UNICODE) ', 'Should be "15 chars, fits in 1 SMS (UNICODE) " for "Hêllo from Odoo"'); + assert.strictEqual(form.$('.o_sms_count').text(), '15 chars, fits in 1 SMS (UNICODE) ', + 'Should be "15 chars, fits in 1 SMS (UNICODE) " for "Hêllo from Odoo"'); // GSM-7 with 160c var text = Array(161).join('a'); - form.$('.o_input').val(text).trigger('input'); - assert.strictEqual(form.$('.o_sms_count').text(), '160 chars, fits in 1 SMS (GSM7) ', 'Should be "160 chars, fits in 1 SMS (GSM7) " for 160 x "a"'); + await testUtils.fields.editAndTrigger(form.$('.o_input'), text, 'input'); + assert.strictEqual(form.$('.o_sms_count').text(), '160 chars, fits in 1 SMS (GSM7) ', + 'Should be "160 chars, fits in 1 SMS (GSM7) " for 160 x "a"'); // GSM-7 with 161c text = Array(162).join('a'); - form.$('.o_input').val(text).trigger('input'); - assert.strictEqual(form.$('.o_sms_count').text(), '161 chars, fits in 2 SMS (GSM7) ', 'Should be "161 chars, fits in 2 SMS (GSM7) " for 161 x "a"'); + await testUtils.fields.editAndTrigger(form.$('.o_input'), text, 'input'); + assert.strictEqual(form.$('.o_sms_count').text(), '161 chars, fits in 2 SMS (GSM7) ', + 'Should be "161 chars, fits in 2 SMS (GSM7) " for 161 x "a"'); // Unicode with 70c text = Array(71).join('ê'); - form.$('.o_input').val(text).trigger('input'); - assert.strictEqual(form.$('.o_sms_count').text(), '70 chars, fits in 1 SMS (UNICODE) ', 'Should be "70 chars, fits in 1 SMS (UNICODE) " for 70 x "ê"'); + await testUtils.fields.editAndTrigger(form.$('.o_input'), text, 'input'); + assert.strictEqual(form.$('.o_sms_count').text(), '70 chars, fits in 1 SMS (UNICODE) ', + 'Should be "70 chars, fits in 1 SMS (UNICODE) " for 70 x "ê"'); // Unicode with 71c text = Array(72).join('ê'); - form.$('.o_input').val(text).trigger('input'); - assert.strictEqual(form.$('.o_sms_count').text(), '71 chars, fits in 2 SMS (UNICODE) ', 'Should be "71 chars, fits in 2 SMS (UNICODE) " for 71 x "ê"'); + await testUtils.fields.editAndTrigger(form.$('.o_input'), text, 'input'); + assert.strictEqual(form.$('.o_sms_count').text(), '71 chars, fits in 2 SMS (UNICODE) ', + 'Should be "71 chars, fits in 2 SMS (UNICODE) " for 71 x "ê"'); form.destroy(); }); diff --git a/addons/snailmail/static/tests/chatter_snailmail_tests.js b/addons/snailmail/static/tests/chatter_snailmail_tests.js index 312a77727c3..a56a0e51879 100644 --- a/addons/snailmail/static/tests/chatter_snailmail_tests.js +++ b/addons/snailmail/static/tests/chatter_snailmail_tests.js @@ -101,13 +101,13 @@ QUnit.module('Chatter', { } }); -QUnit.test('Sent', function (assert) { +QUnit.test('Sent', async function (assert) { assert.expect(7); this.data['mail.message'].records[0].snailmail_status = 'sent'; this.data['mail.message'].records[0].snailmail_error = false; - var form = createView({ + var form = await createView({ View: FormView, model: 'account.invoice', res_id: 1, @@ -131,18 +131,18 @@ QUnit.test('Sent', function (assert) { assert.ok($('.o_thread_tooltip_snailmail:contains("Sent")').length, "Tooltip should show correct text"); - testUtils.dom.click(form.$('.o_thread_message_snailmail_sent')); + await testUtils.dom.click(form.$('.o_thread_message_snailmail_sent')); assert.containsNone($('.modal'), "No modal should open on click"); form.destroy(); }); -QUnit.test('Canceled', function (assert) { +QUnit.test('Canceled', async function (assert) { assert.expect(7); this.data['mail.message'].records[0].snailmail_status = 'canceled'; - var form = createView({ + var form = await createView({ View: FormView, model: 'account.invoice', res_id: 1, @@ -166,19 +166,19 @@ QUnit.test('Canceled', function (assert) { assert.ok($('.o_thread_tooltip_snailmail:contains("Canceled")').length, "Tooltip should show correct text"); - testUtils.dom.click(form.$('.o_thread_message_snailmail_canceled')); + await testUtils.dom.click(form.$('.o_thread_message_snailmail_canceled')); assert.containsNone($('.modal'), "No modal should open on click"); form.destroy(); }); -QUnit.test('Pending', function (assert) { +QUnit.test('Pending', async function (assert) { assert.expect(7); this.data['mail.message'].records[0].snailmail_status = 'pending'; this.data['mail.message'].records[0].snailmail_error = false; - var form = createView({ + var form = await createView({ View: FormView, model: 'account.invoice', res_id: 1, @@ -202,19 +202,19 @@ QUnit.test('Pending', function (assert) { assert.ok($('.o_thread_tooltip_snailmail:contains("Awaiting Dispatch")').length, "Tooltip should show correct text"); - testUtils.dom.click(form.$('.o_thread_message_snailmail_pending')); + await testUtils.dom.click(form.$('.o_thread_message_snailmail_pending')); assert.containsNone($('.modal'), "No modal should open on click"); form.destroy(); }); -QUnit.test('No Price Available', function (assert) { +QUnit.test('No Price Available', async function (assert) { assert.expect(11); this.data['mail.message'].records[0].snailmail_status = 'no_price_available'; this.data['mail.message'].records[0].snailmail_error = true; - var form = createView({ + var form = await createView({ View: FormView, model: 'account.invoice', res_id: 1, @@ -224,7 +224,7 @@ QUnit.test('No Price Available', function (assert) { mockRPC: function (route, args) { if (args.method === 'cancel_letter' && args.model === 'mail.message' && args.args[0][0] === 11) { assert.step(args.method); - return $.when(); + return Promise.resolve(); } return this._super.apply(this, arguments); } @@ -245,24 +245,24 @@ QUnit.test('No Price Available', function (assert) { assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length, "Tooltip should show correct text"); - testUtils.dom.click(form.$('.o_thread_message_snailmail_no_price_available')); + await testUtils.dom.click(form.$('.o_thread_message_snailmail_no_price_available')); var $modal = $('.modal'); assert.ok($modal.length, "A modal should open on click"); assert.containsOnce($modal, 'button:contains("Cancel letter")', "Modal should have a 'Cancel letter' button"); var $cancelButton = $('.modal').find('button:contains("Cancel letter")'); - testUtils.dom.click($cancelButton); + await testUtils.dom.click($cancelButton); assert.notOk($('.modal').length, "The modal should be closed after click on 'Cancel letter'"); - assert.verifySteps(['cancel_letter'], + assert.verifySteps(['cancel_letter'], "Should have made a RPC call to 'cancel_letter'"); form.destroy(); }); -QUnit.test('Format Error', function (assert) { +QUnit.test('Format Error', async function (assert) { assert.expect(8); this.data['mail.message'].records[0].snailmail_status = 'format_error'; @@ -274,7 +274,7 @@ QUnit.test('Format Error', function (assert) { }, }); - var form = createView({ + var form = await createView({ View: FormView, model: 'account.invoice', res_id: 1, @@ -284,7 +284,7 @@ QUnit.test('Format Error', function (assert) { mockRPC: function (route, args) { if (args.method === 'cancel_letter' && args.model === 'mail.message' && args.args[0][0] === 11) { assert.step(args.method); - return $.when(); + return Promise.resolve(); } return this._super.apply(this, arguments); } @@ -305,20 +305,20 @@ QUnit.test('Format Error', function (assert) { assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length, "Tooltip should show correct text"); - testUtils.dom.click(form.$('.o_thread_message_snailmail_format_error')); + await testUtils.dom.click(form.$('.o_thread_message_snailmail_format_error')); assert.verifySteps(['do_action'], "'do_action' should have been called"); form.destroy(); testUtils.mock.unpatch(ThreadWidget); }); -QUnit.test('Credit Error', function (assert) { +QUnit.test('Credit Error', async function (assert) { assert.expect(14); this.data['mail.message'].records[0].snailmail_status = 'credit_error'; this.data['mail.message'].records[0].snailmail_error = true; - var form = createView({ + var form = await createView({ View: FormView, model: 'account.invoice', res_id: 1, @@ -329,11 +329,11 @@ QUnit.test('Credit Error', function (assert) { if (args.model === 'mail.message') { if ((args.method === 'cancel_letter' && args.args[0][0] === 11) || args.method === 'send_letter') { assert.step(args.method); - return $.when(); + return Promise.resolve(); } } if (args.method === 'get_credits_url' && args.model === 'iap.account') { - return $.when('credits_url'); + return Promise.resolve('credits_url'); } return this._super.apply(this, arguments); } @@ -354,39 +354,39 @@ QUnit.test('Credit Error', function (assert) { assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length, "Tooltip should show correct text"); - testUtils.dom.click(form.$('.o_thread_message_snailmail_credit_error')); + await testUtils.dom.click(form.$('.o_thread_message_snailmail_credit_error')); var $modal = $('.modal'); assert.ok($modal.length, "A modal should open on click"); assert.containsOnce($modal, 'button:contains("Re-send letter")', "Modal should have a 'Re-send letter' button"); var $resendButton = $('.modal').find('button:contains("Re-send letter")'); - testUtils.dom.click($resendButton); + await testUtils.dom.click($resendButton); assert.notOk($('.modal').length, "The modal should be closed after click on 'Re-send letter'"); - testUtils.dom.click(form.$('.o_thread_message_snailmail_credit_error')); + await testUtils.dom.click(form.$('.o_thread_message_snailmail_credit_error')); assert.containsOnce($modal, 'button:contains("Cancel letter")', "Modal should have a 'Cancel letter' button"); var $cancelButton = $('.modal').find('button:contains("Cancel letter")'); - testUtils.dom.click($cancelButton); + await testUtils.dom.click($cancelButton); assert.containsNone($('.modal'), "The modal should be closed after click on 'Cancel letter'"); - assert.verifySteps(['send_letter', 'cancel_letter'], + assert.verifySteps(['send_letter', 'cancel_letter'], "Should have made RPC calls to 'send_letter' and 'cancel_letter'"); form.destroy(); }); -QUnit.test('Trial Error', function (assert) { +QUnit.test('Trial Error', async function (assert) { assert.expect(14); this.data['mail.message'].records[0].snailmail_status = 'trial_error'; this.data['mail.message'].records[0].snailmail_error = true; - var form = createView({ + var form = await createView({ View: FormView, model: 'account.invoice', res_id: 1, @@ -397,11 +397,11 @@ QUnit.test('Trial Error', function (assert) { if (args.model === 'mail.message') { if ((args.method === 'cancel_letter' && args.args[0][0] === 11) || args.method === 'send_letter') { assert.step(args.method); - return $.when(); + return Promise.resolve(); } } if (args.method === 'get_credits_url' && args.model === 'iap.account') { - return $.when('credits_url'); + return Promise.resolve('credits_url'); } return this._super.apply(this, arguments); } @@ -422,34 +422,34 @@ QUnit.test('Trial Error', function (assert) { assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length, "Tooltip should show correct text"); - testUtils.dom.click(form.$('.o_thread_message_snailmail_trial_error')); + await testUtils.dom.click(form.$('.o_thread_message_snailmail_trial_error')); var $modal = $('.modal'); assert.ok($modal.length, "A modal should open on click"); assert.containsOnce($modal, 'button:contains("Re-send letter")', "Modal should have a 'Re-send letter' button"); var $resendButton = $('.modal').find('button:contains("Re-send letter")'); - testUtils.dom.click($resendButton); + await testUtils.dom.click($resendButton); assert.containsNone($('.modal'), "The modal should be closed after click on 'Re-send letter'"); - testUtils.dom.click(form.$('.o_thread_message_snailmail_trial_error')); + await testUtils.dom.click(form.$('.o_thread_message_snailmail_trial_error')); $modal = $('.modal'); assert.containsOnce($modal, 'button:contains("Cancel letter")', "Modal should have a 'Cancel letter' button"); var $cancelButton = $modal.find('button:contains("Cancel letter")'); - testUtils.dom.click($cancelButton); + await testUtils.dom.click($cancelButton); assert.containsNone($('.modal'), "The modal should be closed after click on 'Cancel letter'"); - assert.verifySteps(['send_letter', 'cancel_letter'], + assert.verifySteps(['send_letter', 'cancel_letter'], "Should have made RPC calls to 'send_letter' and 'cancel_letter'"); form.destroy(); }); -QUnit.test('Missing Required Fields', function (assert) { +QUnit.test('Missing Required Fields', async function (assert) { assert.expect(8); this.data['mail.message'].records[0].snailmail_status = 'missing_required_fields'; @@ -461,7 +461,7 @@ QUnit.test('Missing Required Fields', function (assert) { }, }); - var form = createView({ + var form = await createView({ View: FormView, model: 'account.invoice', res_id: 1, @@ -470,7 +470,7 @@ QUnit.test('Missing Required Fields', function (assert) { arch: getArch(), mockRPC: function (route, args) { if (args.model === 'snailmail.letter' && args.method === 'search') { - return $.when([2]); + return Promise.resolve([2]); } return this._super.apply(this, arguments); } @@ -491,7 +491,7 @@ QUnit.test('Missing Required Fields', function (assert) { assert.ok($('.o_thread_tooltip_snailmail:contains("Error")').length, "Tooltip should show correct text"); - testUtils.dom.click(form.$('.o_thread_message_snailmail_missing_required_fields')); + await testUtils.dom.click(form.$('.o_thread_message_snailmail_missing_required_fields')); assert.verifySteps(['do_action'], "'do_action' should have been called"); form.destroy(); diff --git a/addons/stock/static/src/js/stock_traceability_report_backend.js b/addons/stock/static/src/js/stock_traceability_report_backend.js index aee712a38b3..56673f4e094 100644 --- a/addons/stock/static/src/js/stock_traceability_report_backend.js +++ b/addons/stock/static/src/js/stock_traceability_report_backend.js @@ -29,11 +29,11 @@ var stock_report_generic = AbstractAction.extend({ this.given_context.lot_name = action.context.lot_name || false; }, willStart: function() { - return $.when(this._super.apply(this, arguments), this.get_html()); + return Promise.all([this._super.apply(this, arguments), this.get_html()]); }, set_html: function() { var self = this; - var def = $.when(); + var def = Promise.resolve(); if (!this.report_widget) { this.report_widget = new ReportWidget(this, this.given_context); def = this.report_widget.appendTo(this.$('.o_content')); @@ -67,7 +67,7 @@ var stock_report_generic = AbstractAction.extend({ self.html = result.html; self.renderButtons(); defs.push(self.update_cp()); - return $.when.apply($, defs); + return Promise.all(defs); }); }, // Updates the control panel and render the elements that have yet to be rendered diff --git a/addons/survey/static/src/js/survey.js b/addons/survey/static/src/js/survey.js index 83db485fb37..b055c8beb89 100644 --- a/addons/survey/static/src/js/survey.js +++ b/addons/survey/static/src/js/survey.js @@ -15,7 +15,7 @@ var _t = core._t; var the_form = $('.js_surveyform'); if(!the_form.length) { - return $.Deferred().reject("DOM doesn't contain '.js_surveyform'"); + return Promise.reject("DOM doesn't contain '.js_surveyform'"); } var prefill_controller = the_form.attr("data-prefill"); diff --git a/addons/survey/static/src/js/survey_result.js b/addons/survey/static/src/js/survey_result.js index f70aefe9a1f..75905289845 100644 --- a/addons/survey/static/src/js/survey_result.js +++ b/addons/survey/static/src/js/survey_result.js @@ -4,7 +4,7 @@ odoo.define('survey.result', function (require) { require('web.dom_ready'); if(!$('.js_surveyresult').length) { - return $.Deferred().reject("DOM doesn't contain '.js_surveyresult'"); + return Promise.reject("DOM doesn't contain '.js_surveyresult'"); } //Script For Pagination diff --git a/addons/survey/static/src/js/survey_timer.js b/addons/survey/static/src/js/survey_timer.js index bb7af08104f..7d6c0925819 100644 --- a/addons/survey/static/src/js/survey_timer.js +++ b/addons/survey/static/src/js/survey_timer.js @@ -4,20 +4,20 @@ odoo.define('survey.timer', function (require) { require('web.dom_ready'); if (!$('.js_survey_timer').length) { - return $.Deferred().reject("DOM doesn't contain '.js_survey_timer'"); + return Promise.reject("DOM doesn't contain '.js_survey_timer'"); } var $parent = $('.js_survey_timer'); var timeLimitMinutes = parseInt($parent.data('time_limit_minutes')); if (timeLimitMinutes <= 0) { - return $.Deferred().reject("Timer is not positive"); + return Promise.reject("Timer is not positive"); } var countDownDate = moment.utc($parent.data('timer')).add(timeLimitMinutes, 'minutes'); if (countDownDate.diff(moment.utc(), 'seconds') < 0) { - return $.Deferred().reject("Timer is already finished"); + return Promise.reject("Timer is already finished"); } var formatTime = function (time) { diff --git a/addons/web/doc/client_action.rst b/addons/web/doc/client_action.rst index 9b729e228a6..4852400df10 100644 --- a/addons/web/doc/client_action.rst +++ b/addons/web/doc/client_action.rst @@ -83,18 +83,19 @@ method. As required by :js:class:`~openerp.web.Widget`'s contract, if :js:func:`~openerp.web.Widget.start()` executes any asynchronous - code it should return a ``$.Deferred`` so callers know when it's + code it should return a ``Promise`` so callers know when it's ready for interaction. .. code-block:: javascript start: function () { - return $.when( + return Promise.all([ this._super(), // Simply read the res.widget object this action should display new instance.web.Model('res.widget').call( 'read', [[this.widget_id], ['title']]) - .then(this.proxy('on_widget_loaded')); + .then(this.proxy('on_widget_loaded') + ]); } The client action can then behave exactly as it wishes to within its diff --git a/addons/web/static/lib/es6-promise/es6-promise-polyfill.js b/addons/web/static/lib/es6-promise/es6-promise-polyfill.js new file mode 100644 index 00000000000..527ac8bdd57 --- /dev/null +++ b/addons/web/static/lib/es6-promise/es6-promise-polyfill.js @@ -0,0 +1,1176 @@ +/*! + * @overview es6-promise - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE + * @version v4.2.5+7f2b526d + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.ES6Promise = factory()); +}(this, (function () { + 'use strict'; + + function objectOrFunction(x) { + var type = typeof x; + return x !== null && (type === 'object' || type === 'function'); + } + + function isFunction(x) { + return typeof x === 'function'; + } + + + + var _isArray = void 0; + if (Array.isArray) { + _isArray = Array.isArray; + } else { + _isArray = function (x) { + return Object.prototype.toString.call(x) === '[object Array]'; + }; + } + + var isArray = _isArray; + + var len = 0; + var vertxNext = void 0; + var customSchedulerFn = void 0; + + var asap = function asap(callback, arg) { + queue[len] = callback; + queue[len + 1] = arg; + len += 2; + if (len === 2) { + // If len is 2, that means that we need to schedule an async flush. + // If additional callbacks are queued before the queue is flushed, they + // will be processed by this flush that we are scheduling. + if (customSchedulerFn) { + customSchedulerFn(flush); + } else { + scheduleFlush(); + } + } + }; + + function setScheduler(scheduleFn) { + customSchedulerFn = scheduleFn; + } + + function setAsap(asapFn) { + asap = asapFn; + } + + var browserWindow = typeof window !== 'undefined' ? window : undefined; + var browserGlobal = browserWindow || {}; + var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; + var isNode = false; + + // test for web worker but not in IE10 + var isWorker = typeof Uint8ClampedArray !== 'undefined' && typeof importScripts !== 'undefined' && typeof MessageChannel !== 'undefined'; + + // node + function useNextTick() { + // node version 0.10.x displays a deprecation warning when nextTick is used recursively + // see https://github.com/cujojs/when/issues/410 for details + return function () { + return process.nextTick(flush); + }; + } + + // vertx + function useVertxTimer() { + if (typeof vertxNext !== 'undefined') { + return function () { + vertxNext(flush); + }; + } + + return useSetTimeout(); + } + + function useMutationObserver() { + var iterations = 0; + var observer = new BrowserMutationObserver(flush); + var node = document.createTextNode(''); + observer.observe(node, { characterData: true }); + + return function () { + node.data = iterations = ++iterations % 2; + }; + } + + // web worker + function useMessageChannel() { + var channel = new MessageChannel(); + channel.port1.onmessage = flush; + return function () { + return channel.port2.postMessage(0); + }; + } + + function useSetTimeout() { + // Store setTimeout reference so es6-promise will be unaffected by + // other code modifying setTimeout (like sinon.useFakeTimers()) + var globalSetTimeout = setTimeout; + return function () { + return globalSetTimeout(flush, 1); + }; + } + + var queue = new Array(1000); + function flush() { + for (var i = 0; i < len; i += 2) { + var callback = queue[i]; + var arg = queue[i + 1]; + + callback(arg); + + queue[i] = undefined; + queue[i + 1] = undefined; + } + + len = 0; + } + + function attemptVertx() { + try { + var vertx = Function('return this')().require('vertx'); + vertxNext = vertx.runOnLoop || vertx.runOnContext; + return useVertxTimer(); + } catch (e) { + return useSetTimeout(); + } + } + + var scheduleFlush = void 0; + // Decide what async method to use to triggering processing of queued callbacks: + if (isNode) { + scheduleFlush = useNextTick(); + } else if (BrowserMutationObserver) { + scheduleFlush = useMutationObserver(); + } else if (isWorker) { + scheduleFlush = useMessageChannel(); + } else if (browserWindow === undefined && typeof require === 'function') { + scheduleFlush = attemptVertx(); + } else { + scheduleFlush = useSetTimeout(); + } + + function then(onFulfillment, onRejection) { + var parent = this; + + var child = new this.constructor(noop); + + if (child[PROMISE_ID] === undefined) { + makePromise(child); + } + + var _state = parent._state; + + + if (_state) { + var callback = arguments[_state - 1]; + asap(function () { + return invokeCallback(_state, child, callback, parent._result); + }); + } else { + subscribe(parent, child, onFulfillment, onRejection); + } + + return child; + } + + /** + `Promise.resolve` returns a promise that will become resolved with the + passed `value`. It is shorthand for the following: + + ```javascript + let promise = new Promise(function(resolve, reject){ + resolve(1); + }); + + promise.then(function(value){ + // value === 1 + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + let promise = Promise.resolve(1); + + promise.then(function(value){ + // value === 1 + }); + ``` + + @method resolve + @static + @param {Any} value value that the returned promise will be resolved with + Useful for tooling. + @return {Promise} a promise that will become fulfilled with the given + `value` + */ + function resolve$1(object) { + /*jshint validthis:true */ + var Constructor = this; + + if (object && typeof object === 'object' && object.constructor === Constructor) { + return object; + } + + var promise = new Constructor(noop); + resolve(promise, object); + return promise; + } + + var PROMISE_ID = Math.random().toString(36).substring(2); + + function noop() { } + + var PENDING = void 0; + var FULFILLED = 1; + var REJECTED = 2; + + var TRY_CATCH_ERROR = { error: null }; + + function selfFulfillment() { + return new TypeError("You cannot resolve a promise with itself"); + } + + function cannotReturnOwn() { + return new TypeError('A promises callback cannot return that same promise.'); + } + + function getThen(promise) { + try { + return promise.then; + } catch (error) { + TRY_CATCH_ERROR.error = error; + return TRY_CATCH_ERROR; + } + } + + function tryThen(then$$1, value, fulfillmentHandler, rejectionHandler) { + try { + then$$1.call(value, fulfillmentHandler, rejectionHandler); + } catch (e) { + return e; + } + } + + function handleForeignThenable(promise, thenable, then$$1) { + asap(function (promise) { + var sealed = false; + var error = tryThen(then$$1, thenable, function (value) { + if (sealed) { + return; + } + sealed = true; + if (thenable !== value) { + resolve(promise, value); + } else { + fulfill(promise, value); + } + }, function (reason) { + if (sealed) { + return; + } + sealed = true; + + reject(promise, reason); + }, 'Settle: ' + (promise._label || ' unknown promise')); + + if (!sealed && error) { + sealed = true; + reject(promise, error); + } + }, promise); + } + + function handleOwnThenable(promise, thenable) { + if (thenable._state === FULFILLED) { + fulfill(promise, thenable._result); + } else if (thenable._state === REJECTED) { + reject(promise, thenable._result); + } else { + subscribe(thenable, undefined, function (value) { + return resolve(promise, value); + }, function (reason) { + return reject(promise, reason); + }); + } + } + + function handleMaybeThenable(promise, maybeThenable, then$$1) { + if (maybeThenable.constructor === promise.constructor && then$$1 === then && maybeThenable.constructor.resolve === resolve$1) { + handleOwnThenable(promise, maybeThenable); + } else { + if (then$$1 === TRY_CATCH_ERROR) { + reject(promise, TRY_CATCH_ERROR.error); + TRY_CATCH_ERROR.error = null; + } else if (then$$1 === undefined) { + fulfill(promise, maybeThenable); + } else if (isFunction(then$$1)) { + handleForeignThenable(promise, maybeThenable, then$$1); + } else { + fulfill(promise, maybeThenable); + } + } + } + + function resolve(promise, value) { + if (promise === value) { + reject(promise, selfFulfillment()); + } else if (objectOrFunction(value)) { + handleMaybeThenable(promise, value, getThen(value)); + } else { + fulfill(promise, value); + } + } + + function publishRejection(promise) { + if (promise._onerror) { + promise._onerror(promise._result); + } + + publish(promise); + } + + function fulfill(promise, value) { + if (promise._state !== PENDING) { + return; + } + + promise._result = value; + promise._state = FULFILLED; + + if (promise._subscribers.length !== 0) { + asap(publish, promise); + } + } + + function reject(promise, reason) { + if (promise._state !== PENDING) { + return; + } + promise._state = REJECTED; + promise._result = reason; + + asap(publishRejection, promise); + } + + function subscribe(parent, child, onFulfillment, onRejection) { + var _subscribers = parent._subscribers; + var length = _subscribers.length; + + + parent._onerror = null; + + _subscribers[length] = child; + _subscribers[length + FULFILLED] = onFulfillment; + _subscribers[length + REJECTED] = onRejection; + + if (length === 0 && parent._state) { + asap(publish, parent); + } + } + + function publish(promise) { + var subscribers = promise._subscribers; + var settled = promise._state; + + if (subscribers.length === 0) { + return; + } + + var child = void 0, + callback = void 0, + detail = promise._result; + + for (var i = 0; i < subscribers.length; i += 3) { + child = subscribers[i]; + callback = subscribers[i + settled]; + + if (child) { + invokeCallback(settled, child, callback, detail); + } else { + callback(detail); + } + } + + promise._subscribers.length = 0; + } + + function tryCatch(callback, detail) { + try { + return callback(detail); + } catch (e) { + TRY_CATCH_ERROR.error = e; + return TRY_CATCH_ERROR; + } + } + + function invokeCallback(settled, promise, callback, detail) { + var hasCallback = isFunction(callback), + value = void 0, + error = void 0, + succeeded = void 0, + failed = void 0; + + if (hasCallback) { + value = tryCatch(callback, detail); + + if (value === TRY_CATCH_ERROR) { + failed = true; + error = value.error; + value.error = null; + } else { + succeeded = true; + } + + if (promise === value) { + reject(promise, cannotReturnOwn()); + return; + } + } else { + value = detail; + succeeded = true; + } + + if (promise._state !== PENDING) { + // noop + } else if (hasCallback && succeeded) { + resolve(promise, value); + } else if (failed) { + reject(promise, error); + } else if (settled === FULFILLED) { + fulfill(promise, value); + } else if (settled === REJECTED) { + reject(promise, value); + } + } + + function initializePromise(promise, resolver) { + try { + resolver(function resolvePromise(value) { + resolve(promise, value); + }, function rejectPromise(reason) { + reject(promise, reason); + }); + } catch (e) { + reject(promise, e); + } + } + + var id = 0; + function nextId() { + return id++; + } + + function makePromise(promise) { + promise[PROMISE_ID] = id++; + promise._state = undefined; + promise._result = undefined; + promise._subscribers = []; + } + + function validationError() { + return new Error('Array Methods must be provided an Array'); + } + + var Enumerator = function () { + function Enumerator(Constructor, input) { + this._instanceConstructor = Constructor; + this.promise = new Constructor(noop); + + if (!this.promise[PROMISE_ID]) { + makePromise(this.promise); + } + + if (isArray(input)) { + this.length = input.length; + this._remaining = input.length; + + this._result = new Array(this.length); + + if (this.length === 0) { + fulfill(this.promise, this._result); + } else { + this.length = this.length || 0; + this._enumerate(input); + if (this._remaining === 0) { + fulfill(this.promise, this._result); + } + } + } else { + reject(this.promise, validationError()); + } + } + + Enumerator.prototype._enumerate = function _enumerate(input) { + for (var i = 0; this._state === PENDING && i < input.length; i++) { + this._eachEntry(input[i], i); + } + }; + + Enumerator.prototype._eachEntry = function _eachEntry(entry, i) { + var c = this._instanceConstructor; + var resolve$$1 = c.resolve; + + + if (resolve$$1 === resolve$1) { + var _then = getThen(entry); + + if (_then === then && entry._state !== PENDING) { + this._settledAt(entry._state, i, entry._result); + } else if (typeof _then !== 'function') { + this._remaining--; + this._result[i] = entry; + } else if (c === Promise$2) { + var promise = new c(noop); + handleMaybeThenable(promise, entry, _then); + this._willSettleAt(promise, i); + } else { + this._willSettleAt(new c(function (resolve$$1) { + return resolve$$1(entry); + }), i); + } + } else { + this._willSettleAt(resolve$$1(entry), i); + } + }; + + Enumerator.prototype._settledAt = function _settledAt(state, i, value) { + var promise = this.promise; + + + if (promise._state === PENDING) { + this._remaining--; + + if (state === REJECTED) { + reject(promise, value); + } else { + this._result[i] = value; + } + } + + if (this._remaining === 0) { + fulfill(promise, this._result); + } + }; + + Enumerator.prototype._willSettleAt = function _willSettleAt(promise, i) { + var enumerator = this; + + subscribe(promise, undefined, function (value) { + return enumerator._settledAt(FULFILLED, i, value); + }, function (reason) { + return enumerator._settledAt(REJECTED, i, reason); + }); + }; + + return Enumerator; + }(); + + /** + `Promise.all` accepts an array of promises, and returns a new promise which + is fulfilled with an array of fulfillment values for the passed promises, or + rejected with the reason of the first passed promise to be rejected. It casts all + elements of the passed iterable to promises as it runs this algorithm. + + Example: + + ```javascript + let promise1 = resolve(1); + let promise2 = resolve(2); + let promise3 = resolve(3); + let promises = [ promise1, promise2, promise3 ]; + + Promise.all(promises).then(function(array){ + // The array here would be [ 1, 2, 3 ]; + }); + ``` + + If any of the `promises` given to `all` are rejected, the first promise + that is rejected will be given as an argument to the returned promises's + rejection handler. For example: + + Example: + + ```javascript + let promise1 = resolve(1); + let promise2 = reject(new Error("2")); + let promise3 = reject(new Error("3")); + let promises = [ promise1, promise2, promise3 ]; + + Promise.all(promises).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(error) { + // error.message === "2" + }); + ``` + + @method all + @static + @param {Array} entries array of promises + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled when all `promises` have been + fulfilled, or rejected if any of them become rejected. + @static + */ + function all(entries) { + return new Enumerator(this, entries).promise; + } + + /** + `Promise.race` returns a new promise which is settled in the same way as the + first passed promise to settle. + + Example: + + ```javascript + let promise1 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 1'); + }, 200); + }); + + let promise2 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 2'); + }, 100); + }); + + Promise.race([promise1, promise2]).then(function(result){ + // result === 'promise 2' because it was resolved before promise1 + // was resolved. + }); + ``` + + `Promise.race` is deterministic in that only the state of the first + settled promise matters. For example, even if other promises given to the + `promises` array argument are resolved, but the first settled promise has + become rejected before the other promises became fulfilled, the returned + promise will become rejected: + + ```javascript + let promise1 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 1'); + }, 200); + }); + + let promise2 = new Promise(function(resolve, reject){ + setTimeout(function(){ + reject(new Error('promise 2')); + }, 100); + }); + + Promise.race([promise1, promise2]).then(function(result){ + // Code here never runs + }, function(reason){ + // reason.message === 'promise 2' because promise 2 became rejected before + // promise 1 became fulfilled + }); + ``` + + An example real-world use case is implementing timeouts: + + ```javascript + Promise.race([ajax('foo.json'), timeout(5000)]) + ``` + + @method race + @static + @param {Array} promises array of promises to observe + Useful for tooling. + @return {Promise} a promise which settles in the same way as the first passed + promise to settle. + */ + function race(entries) { + /*jshint validthis:true */ + var Constructor = this; + + if (!isArray(entries)) { + return new Constructor(function (_, reject) { + return reject(new TypeError('You must pass an array to race.')); + }); + } else { + return new Constructor(function (resolve, reject) { + var length = entries.length; + for (var i = 0; i < length; i++) { + Constructor.resolve(entries[i]).then(resolve, reject); + } + }); + } + } + + /** + `Promise.reject` returns a promise rejected with the passed `reason`. + It is shorthand for the following: + + ```javascript + let promise = new Promise(function(resolve, reject){ + reject(new Error('WHOOPS')); + }); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + let promise = Promise.reject(new Error('WHOOPS')); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + @method reject + @static + @param {Any} reason value that the returned promise will be rejected with. + Useful for tooling. + @return {Promise} a promise rejected with the given `reason`. + */ + function reject$1(reason) { + /*jshint validthis:true */ + var Constructor = this; + var promise = new Constructor(noop); + reject(promise, reason); + return promise; + } + + function needsResolver() { + throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); + } + + function needsNew() { + throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); + } + + /** + Promise objects represent the eventual result of an asynchronous operation. The + primary way of interacting with a promise is through its `then` method, which + registers callbacks to receive either a promise's eventual value or the reason + why the promise cannot be fulfilled. + + Terminology + ----------- + + - `promise` is an object or function with a `then` method whose behavior conforms to this specification. + - `thenable` is an object or function that defines a `then` method. + - `value` is any legal JavaScript value (including undefined, a thenable, or a promise). + - `exception` is a value that is thrown using the throw statement. + - `reason` is a value that indicates why a promise was rejected. + - `settled` the final resting state of a promise, fulfilled or rejected. + + A promise can be in one of three states: pending, fulfilled, or rejected. + + Promises that are fulfilled have a fulfillment value and are in the fulfilled + state. Promises that are rejected have a rejection reason and are in the + rejected state. A fulfillment value is never a thenable. + + Promises can also be said to *resolve* a value. If this value is also a + promise, then the original promise's settled state will match the value's + settled state. So a promise that *resolves* a promise that rejects will + itself reject, and a promise that *resolves* a promise that fulfills will + itself fulfill. + + + Basic Usage: + ------------ + + ```js + let promise = new Promise(function(resolve, reject) { + // on success + resolve(value); + + // on failure + reject(reason); + }); + + promise.then(function(value) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Advanced Usage: + --------------- + + Promises shine when abstracting away asynchronous interactions such as + `XMLHttpRequest`s. + + ```js + function getJSON(url) { + return new Promise(function(resolve, reject){ + let xhr = new XMLHttpRequest(); + + xhr.open('GET', url); + xhr.onreadystatechange = handler; + xhr.responseType = 'json'; + xhr.setRequestHeader('Accept', 'application/json'); + xhr.send(); + + function handler() { + if (this.readyState === this.DONE) { + if (this.status === 200) { + resolve(this.response); + } else { + reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']')); + } + } + }; + }); + } + + getJSON('/posts.json').then(function(json) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Unlike callbacks, promises are great composable primitives. + + ```js + Promise.all([ + getJSON('/posts'), + getJSON('/comments') + ]).then(function(values){ + values[0] // => postsJSON + values[1] // => commentsJSON + + return values; + }); + ``` + + @class Promise + @param {Function} resolver + Useful for tooling. + @constructor + */ + + var Promise$2 = function () { + function Promise(resolver) { + this[PROMISE_ID] = nextId(); + this._result = this._state = undefined; + this._subscribers = []; + + if (noop !== resolver) { + typeof resolver !== 'function' && needsResolver(); + this instanceof Promise ? initializePromise(this, resolver) : needsNew(); + } + } + + /** + The primary way of interacting with a promise is through its `then` method, + which registers callbacks to receive either a promise's eventual value or the + reason why the promise cannot be fulfilled. + ```js + findUser().then(function(user){ + // user is available + }, function(reason){ + // user is unavailable, and you are given the reason why + }); + ``` + Chaining + -------- + The return value of `then` is itself a promise. This second, 'downstream' + promise is resolved with the return value of the first promise's fulfillment + or rejection handler, or rejected if the handler throws an exception. + ```js + findUser().then(function (user) { + return user.name; + }, function (reason) { + return 'default name'; + }).then(function (userName) { + // If `findUser` fulfilled, `userName` will be the user's name, otherwise it + // will be `'default name'` + }); + findUser().then(function (user) { + throw new Error('Found user, but still unhappy'); + }, function (reason) { + throw new Error('`findUser` rejected and we're unhappy'); + }).then(function (value) { + // never reached + }, function (reason) { + // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'. + // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'. + }); + ``` + If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. + ```js + findUser().then(function (user) { + throw new PedagogicalException('Upstream error'); + }).then(function (value) { + // never reached + }).then(function (value) { + // never reached + }, function (reason) { + // The `PedgagocialException` is propagated all the way down to here + }); + ``` + Assimilation + ------------ + Sometimes the value you want to propagate to a downstream promise can only be + retrieved asynchronously. This can be achieved by returning a promise in the + fulfillment or rejection handler. The downstream promise will then be pending + until the returned promise is settled. This is called *assimilation*. + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // The user's comments are now available + }); + ``` + If the assimliated promise rejects, then the downstream promise will also reject. + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // If `findCommentsByAuthor` fulfills, we'll have the value here + }, function (reason) { + // If `findCommentsByAuthor` rejects, we'll have the reason here + }); + ``` + Simple Example + -------------- + Synchronous Example + ```javascript + let result; + try { + result = findResult(); + // success + } catch(reason) { + // failure + } + ``` + Errback Example + ```js + findResult(function(result, err){ + if (err) { + // failure + } else { + // success + } + }); + ``` + Promise Example; + ```javascript + findResult().then(function(result){ + // success + }, function(reason){ + // failure + }); + ``` + Advanced Example + -------------- + Synchronous Example + ```javascript + let author, books; + try { + author = findAuthor(); + books = findBooksByAuthor(author); + // success + } catch(reason) { + // failure + } + ``` + Errback Example + ```js + function foundBooks(books) { + } + function failure(reason) { + } + findAuthor(function(author, err){ + if (err) { + failure(err); + // failure + } else { + try { + findBoooksByAuthor(author, function(books, err) { + if (err) { + failure(err); + } else { + try { + foundBooks(books); + } catch(reason) { + failure(reason); + } + } + }); + } catch(error) { + failure(err); + } + // success + } + }); + ``` + Promise Example; + ```javascript + findAuthor(). + then(findBooksByAuthor). + then(function(books){ + // found books + }).catch(function(reason){ + // something went wrong + }); + ``` + @method then + @param {Function} onFulfilled + @param {Function} onRejected + Useful for tooling. + @return {Promise} + */ + + /** + `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same + as the catch block of a try/catch statement. + ```js + function findAuthor(){ + throw new Error('couldn't find that author'); + } + // synchronous + try { + findAuthor(); + } catch(reason) { + // something went wrong + } + // async with promises + findAuthor().catch(function(reason){ + // something went wrong + }); + ``` + @method catch + @param {Function} onRejection + Useful for tooling. + @return {Promise} + */ + + + Promise.prototype.catch = function _catch(onRejection) { + return this.then(null, onRejection); + }; + + /** + `finally` will be invoked regardless of the promise's fate just as native + try/catch/finally behaves + + Synchronous example: + + ```js + findAuthor() { + if (Math.random() > 0.5) { + throw new Error(); + } + return new Author(); + } + + try { + return findAuthor(); // succeed or fail + } catch(error) { + return findOtherAuther(); + } finally { + // always runs + // doesn't affect the return value + } + ``` + + Asynchronous example: + + ```js + findAuthor().catch(function(reason){ + return findOtherAuther(); + }).finally(function(){ + // author was either found, or not + }); + ``` + + @method finally + @param {Function} callback + @return {Promise} + */ + + + Promise.prototype.finally = function _finally(callback) { + var promise = this; + var constructor = promise.constructor; + + if (isFunction(callback)) { + return promise.then(function (value) { + return constructor.resolve(callback()).then(function () { + return value; + }); + }, function (reason) { + return constructor.resolve(callback()).then(function () { + throw reason; + }); + }); + } + + return promise.then(callback, callback); + }; + + return Promise; + }(); + + Promise$2.prototype.then = then; + Promise$2.all = all; + Promise$2.race = race; + Promise$2.resolve = resolve$1; + Promise$2.reject = reject$1; + Promise$2._setScheduler = setScheduler; + Promise$2._setAsap = setAsap; + Promise$2._asap = asap; + + /*global self*/ + function polyfill() { + var local = void 0; + + try { + local = Function('return this')(); + } catch (e) { + throw new Error('polyfill failed because global object is unavailable in this environment'); + } + + + var P = local.Promise; + + if (P) { + var promiseToString = null; + try { + promiseToString = Object.prototype.toString.call(P.resolve()); + } catch (e) { + // silently ignored + } + + if (promiseToString === '[object Promise]' && !P.cast) { + return; + } + } + + local.Promise = Promise$2; + } + + // Strange compat.. + Promise$2.polyfill = polyfill; + Promise$2.Promise = Promise$2; + + Promise$2.polyfill(); + + return Promise$2; + +}))); diff --git a/addons/web/static/lib/jquery.ui/images/ui-icons_444444_256x240.png b/addons/web/static/lib/jquery.ui/images/ui-icons_444444_256x240.png index a802263b589..19f664d9701 100644 Binary files a/addons/web/static/lib/jquery.ui/images/ui-icons_444444_256x240.png and b/addons/web/static/lib/jquery.ui/images/ui-icons_444444_256x240.png differ diff --git a/addons/web/static/lib/jquery.ui/images/ui-icons_555555_256x240.png b/addons/web/static/lib/jquery.ui/images/ui-icons_555555_256x240.png index 7009bf752f5..e965f6d97c6 100644 Binary files a/addons/web/static/lib/jquery.ui/images/ui-icons_555555_256x240.png and b/addons/web/static/lib/jquery.ui/images/ui-icons_555555_256x240.png differ diff --git a/addons/web/static/lib/jquery.ui/images/ui-icons_777620_256x240.png b/addons/web/static/lib/jquery.ui/images/ui-icons_777620_256x240.png index e0a1fdfdc01..9785948a293 100644 Binary files a/addons/web/static/lib/jquery.ui/images/ui-icons_777620_256x240.png and b/addons/web/static/lib/jquery.ui/images/ui-icons_777620_256x240.png differ diff --git a/addons/web/static/lib/jquery.ui/images/ui-icons_777777_256x240.png b/addons/web/static/lib/jquery.ui/images/ui-icons_777777_256x240.png index 8e26ee4fd8c..323c4564a74 100644 Binary files a/addons/web/static/lib/jquery.ui/images/ui-icons_777777_256x240.png and b/addons/web/static/lib/jquery.ui/images/ui-icons_777777_256x240.png differ diff --git a/addons/web/static/lib/jquery.ui/images/ui-icons_cc0000_256x240.png b/addons/web/static/lib/jquery.ui/images/ui-icons_cc0000_256x240.png index 28154300a67..45ac7787cd2 100644 Binary files a/addons/web/static/lib/jquery.ui/images/ui-icons_cc0000_256x240.png and b/addons/web/static/lib/jquery.ui/images/ui-icons_cc0000_256x240.png differ diff --git a/addons/web/static/lib/jquery.ui/images/ui-icons_ffffff_256x240.png b/addons/web/static/lib/jquery.ui/images/ui-icons_ffffff_256x240.png index 4d66f596e59..fe41d2d0fdd 100644 Binary files a/addons/web/static/lib/jquery.ui/images/ui-icons_ffffff_256x240.png and b/addons/web/static/lib/jquery.ui/images/ui-icons_ffffff_256x240.png differ diff --git a/addons/web/static/lib/jquery.ui/jquery-ui.css b/addons/web/static/lib/jquery.ui/jquery-ui.css index d6a4df13807..88204173668 100644 --- a/addons/web/static/lib/jquery.ui/jquery-ui.css +++ b/addons/web/static/lib/jquery.ui/jquery-ui.css @@ -1,9 +1,13 @@ -/*! jQuery UI - v1.11.4 - 2016-04-14 +/*! jQuery UI - v1.12.1 - 2019-01-18 * http://jqueryui.com -* Includes: core.css, draggable.css, resizable.css, selectable.css, sortable.css, autocomplete.css, datepicker.css, menu.css, tooltip.css, theme.css -* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&fwDefault=normal&cornerRadius=3px&bgColorHeader=e9e9e9&bgTextureHeader=flat&borderColorHeader=dddddd&fcHeader=333333&iconColorHeader=444444&bgColorContent=ffffff&bgTextureContent=flat&borderColorContent=dddddd&fcContent=333333&iconColorContent=444444&bgColorDefault=f6f6f6&bgTextureDefault=flat&borderColorDefault=c5c5c5&fcDefault=454545&iconColorDefault=777777&bgColorHover=ededed&bgTextureHover=flat&borderColorHover=cccccc&fcHover=2b2b2b&iconColorHover=555555&bgColorActive=007fff&bgTextureActive=flat&borderColorActive=003eff&fcActive=ffffff&iconColorActive=ffffff&bgColorHighlight=fffa90&bgTextureHighlight=flat&borderColorHighlight=dad55e&fcHighlight=777620&iconColorHighlight=777620&bgColorError=fddfdf&bgTextureError=flat&borderColorError=f1a899&fcError=5f3f3f&iconColorError=cc0000&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=666666&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=5px&offsetTopShadow=0px&offsetLeftShadow=0px&cornerRadiusShadow=8px +* Includes: draggable.css, core.css, resizable.css, selectable.css, sortable.css, autocomplete.css, menu.css, datepicker.css, tooltip.css, theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=base&cornerRadiusShadow=8px&offsetLeftShadow=0px&offsetTopShadow=0px&thicknessShadow=5px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=666666&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cc0000&fcError=5f3f3f&borderColorError=f1a899&bgTextureError=flat&bgColorError=fddfdf&iconColorHighlight=777620&fcHighlight=777620&borderColorHighlight=dad55e&bgTextureHighlight=flat&bgColorHighlight=fffa90&iconColorActive=ffffff&fcActive=ffffff&borderColorActive=003eff&bgTextureActive=flat&bgColorActive=007fff&iconColorHover=555555&fcHover=2b2b2b&borderColorHover=cccccc&bgTextureHover=flat&bgColorHover=ededed&iconColorDefault=777777&fcDefault=454545&borderColorDefault=c5c5c5&bgTextureDefault=flat&bgColorDefault=f6f6f6&iconColorContent=444444&fcContent=333333&borderColorContent=dddddd&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=444444&fcHeader=333333&borderColorHeader=dddddd&bgTextureHeader=flat&bgColorHeader=e9e9e9&cornerRadius=3px&fwDefault=normal&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif * Copyright jQuery Foundation and other contributors; Licensed MIT */ +.ui-draggable-handle { + -ms-touch-action: none; + touch-action: none; +} /* Layout helpers ----------------------------------*/ .ui-helper-hidden { @@ -38,9 +42,6 @@ .ui-helper-clearfix:after { clear: both; } -.ui-helper-clearfix { - min-height: 0; /* support: IE7 */ -} .ui-helper-zfix { width: 100%; height: 100%; @@ -60,20 +61,27 @@ ----------------------------------*/ .ui-state-disabled { cursor: default !important; + pointer-events: none; } /* Icons ----------------------------------*/ - -/* states and images */ .ui-icon { - display: block; + display: inline-block; + vertical-align: middle; + margin-top: -.25em; + position: relative; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } +.ui-widget-icon-block { + left: 50%; + margin-left: -8px; + display: block; +} /* Misc visuals ----------------------------------*/ @@ -86,10 +94,6 @@ width: 100%; height: 100%; } -.ui-draggable-handle { - -ms-touch-action: none; - touch-action: none; -} .ui-resizable { position: relative; } @@ -179,6 +183,60 @@ left: 0; cursor: default; } +.ui-menu { + list-style: none; + padding: 0; + margin: 0; + display: block; + outline: 0; +} +.ui-menu .ui-menu { + position: absolute; +} +.ui-menu .ui-menu-item { + margin: 0; + cursor: pointer; + /* support: IE10, see #8844 */ + list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); +} +.ui-menu .ui-menu-item-wrapper { + position: relative; + padding: 3px 1em 3px .4em; +} +.ui-menu .ui-menu-divider { + margin: 5px 0; + height: 0; + font-size: 0; + line-height: 0; + border-width: 1px 0 0 0; +} +.ui-menu .ui-state-focus, +.ui-menu .ui-state-active { + margin: -1px; +} + +/* icon support */ +.ui-menu-icons { + position: relative; +} +.ui-menu-icons .ui-menu-item-wrapper { + padding-left: 2em; +} + +/* left-aligned */ +.ui-menu .ui-icon { + position: absolute; + top: 0; + bottom: 0; + left: .2em; + margin: auto 0; +} + +/* right-aligned */ +.ui-menu .ui-menu-icon { + left: auto; + right: 0; +} .ui-datepicker { width: 17em; padding: .2em .2em 0; @@ -344,66 +402,21 @@ border-right-width: 0; border-left-width: 1px; } -.ui-menu { - list-style: none; - padding: 0; - margin: 0; + +/* Icons */ +.ui-datepicker .ui-icon { display: block; - outline: none; -} -.ui-menu .ui-menu { - position: absolute; -} -.ui-menu .ui-menu-item { - position: relative; - margin: 0; - padding: 3px 1em 3px .4em; - cursor: pointer; - min-height: 0; /* support: IE7 */ - /* support: IE10, see #8844 */ - list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); -} -.ui-menu .ui-menu-divider { - margin: 5px 0; - height: 0; - font-size: 0; - line-height: 0; - border-width: 1px 0 0 0; -} -.ui-menu .ui-state-focus, -.ui-menu .ui-state-active { - margin: -1px; -} - -/* icon support */ -.ui-menu-icons { - position: relative; -} -.ui-menu-icons .ui-menu-item { - padding-left: 2em; -} - -/* left-aligned */ -.ui-menu .ui-icon { - position: absolute; - top: 0; - bottom: 0; - left: .2em; - margin: auto 0; -} - -/* right-aligned */ -.ui-menu .ui-menu-icon { - left: auto; - right: 0; + text-indent: -99999px; + overflow: hidden; + background-repeat: no-repeat; + left: .5em; + top: .3em; } .ui-tooltip { padding: 8px; position: absolute; z-index: 9999; max-width: 300px; - -webkit-box-shadow: 0 0 5px #aaa; - box-shadow: 0 0 5px #aaa; } body .ui-tooltip { border-width: 2px; @@ -425,6 +438,9 @@ body .ui-tooltip { font-family: Arial,Helvetica,sans-serif; font-size: 1em; } +.ui-widget.ui-widget-content { + border: 1px solid #c5c5c5; +} .ui-widget-content { border: 1px solid #dddddd; background: #ffffff; @@ -447,7 +463,13 @@ body .ui-tooltip { ----------------------------------*/ .ui-state-default, .ui-widget-content .ui-state-default, -.ui-widget-header .ui-state-default { +.ui-widget-header .ui-state-default, +.ui-button, + +/* We use html here because we need a greater specificity to make sure disabled +works properly when clicked or hovered */ +html .ui-button.ui-state-disabled:hover, +html .ui-button.ui-state-disabled:active { border: 1px solid #c5c5c5; background: #f6f6f6; font-weight: normal; @@ -455,7 +477,11 @@ body .ui-tooltip { } .ui-state-default a, .ui-state-default a:link, -.ui-state-default a:visited { +.ui-state-default a:visited, +a.ui-button, +a:link.ui-button, +a:visited.ui-button, +.ui-button { color: #454545; text-decoration: none; } @@ -464,7 +490,9 @@ body .ui-tooltip { .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, -.ui-widget-header .ui-state-focus { +.ui-widget-header .ui-state-focus, +.ui-button:hover, +.ui-button:focus { border: 1px solid #cccccc; background: #ededed; font-weight: normal; @@ -477,18 +505,32 @@ body .ui-tooltip { .ui-state-focus a, .ui-state-focus a:hover, .ui-state-focus a:link, -.ui-state-focus a:visited { +.ui-state-focus a:visited, +a.ui-button:hover, +a.ui-button:focus { color: #2b2b2b; text-decoration: none; } + +.ui-visual-focus { + box-shadow: 0 0 3px 1px rgb(94, 158, 214); +} .ui-state-active, .ui-widget-content .ui-state-active, -.ui-widget-header .ui-state-active { +.ui-widget-header .ui-state-active, +a.ui-button:active, +.ui-button:active, +.ui-button.ui-state-active:hover { border: 1px solid #003eff; background: #007fff; font-weight: normal; color: #ffffff; } +.ui-icon-background, +.ui-state-active .ui-icon-background { + border: #003eff; + background-color: #ffffff; +} .ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { @@ -505,6 +547,10 @@ body .ui-tooltip { background: #fffa90; color: #777620; } +.ui-state-checked { + border: 1px solid #dad55e; + background: #fffa90; +} .ui-state-highlight a, .ui-widget-content .ui-state-highlight a, .ui-widget-header .ui-state-highlight a { @@ -565,41 +611,45 @@ body .ui-tooltip { .ui-widget-header .ui-icon { background-image: url("images/ui-icons_444444_256x240.png"); } -.ui-state-default .ui-icon { - background-image: url("images/ui-icons_777777_256x240.png"); -} .ui-state-hover .ui-icon, -.ui-state-focus .ui-icon { +.ui-state-focus .ui-icon, +.ui-button:hover .ui-icon, +.ui-button:focus .ui-icon { background-image: url("images/ui-icons_555555_256x240.png"); } -.ui-state-active .ui-icon { +.ui-state-active .ui-icon, +.ui-button:active .ui-icon { background-image: url("images/ui-icons_ffffff_256x240.png"); } -.ui-state-highlight .ui-icon { +.ui-state-highlight .ui-icon, +.ui-button .ui-state-highlight.ui-icon { background-image: url("images/ui-icons_777620_256x240.png"); } .ui-state-error .ui-icon, .ui-state-error-text .ui-icon { background-image: url("images/ui-icons_cc0000_256x240.png"); } +.ui-button .ui-icon { + background-image: url("images/ui-icons_777777_256x240.png"); +} /* positioning */ .ui-icon-blank { background-position: 16px 16px; } -.ui-icon-carat-1-n { background-position: 0 0; } -.ui-icon-carat-1-ne { background-position: -16px 0; } -.ui-icon-carat-1-e { background-position: -32px 0; } -.ui-icon-carat-1-se { background-position: -48px 0; } -.ui-icon-carat-1-s { background-position: -64px 0; } -.ui-icon-carat-1-sw { background-position: -80px 0; } -.ui-icon-carat-1-w { background-position: -96px 0; } -.ui-icon-carat-1-nw { background-position: -112px 0; } -.ui-icon-carat-2-n-s { background-position: -128px 0; } -.ui-icon-carat-2-e-w { background-position: -144px 0; } +.ui-icon-caret-1-n { background-position: 0 0; } +.ui-icon-caret-1-ne { background-position: -16px 0; } +.ui-icon-caret-1-e { background-position: -32px 0; } +.ui-icon-caret-1-se { background-position: -48px 0; } +.ui-icon-caret-1-s { background-position: -65px 0; } +.ui-icon-caret-1-sw { background-position: -80px 0; } +.ui-icon-caret-1-w { background-position: -96px 0; } +.ui-icon-caret-1-nw { background-position: -112px 0; } +.ui-icon-caret-2-n-s { background-position: -128px 0; } +.ui-icon-caret-2-e-w { background-position: -144px 0; } .ui-icon-triangle-1-n { background-position: 0 -16px; } .ui-icon-triangle-1-ne { background-position: -16px -16px; } .ui-icon-triangle-1-e { background-position: -32px -16px; } .ui-icon-triangle-1-se { background-position: -48px -16px; } -.ui-icon-triangle-1-s { background-position: -64px -16px; } +.ui-icon-triangle-1-s { background-position: -65px -16px; } .ui-icon-triangle-1-sw { background-position: -80px -16px; } .ui-icon-triangle-1-w { background-position: -96px -16px; } .ui-icon-triangle-1-nw { background-position: -112px -16px; } @@ -609,7 +659,7 @@ body .ui-tooltip { .ui-icon-arrow-1-ne { background-position: -16px -32px; } .ui-icon-arrow-1-e { background-position: -32px -32px; } .ui-icon-arrow-1-se { background-position: -48px -32px; } -.ui-icon-arrow-1-s { background-position: -64px -32px; } +.ui-icon-arrow-1-s { background-position: -65px -32px; } .ui-icon-arrow-1-sw { background-position: -80px -32px; } .ui-icon-arrow-1-w { background-position: -96px -32px; } .ui-icon-arrow-1-nw { background-position: -112px -32px; } @@ -621,7 +671,7 @@ body .ui-tooltip { .ui-icon-arrowstop-1-e { background-position: -208px -32px; } .ui-icon-arrowstop-1-s { background-position: -224px -32px; } .ui-icon-arrowstop-1-w { background-position: -240px -32px; } -.ui-icon-arrowthick-1-n { background-position: 0 -48px; } +.ui-icon-arrowthick-1-n { background-position: 1px -48px; } .ui-icon-arrowthick-1-ne { background-position: -16px -48px; } .ui-icon-arrowthick-1-e { background-position: -32px -48px; } .ui-icon-arrowthick-1-se { background-position: -48px -48px; } @@ -798,10 +848,6 @@ body .ui-tooltip { filter: Alpha(Opacity=30); /* support: IE8 */ } .ui-widget-shadow { - margin: 0px 0 0 0px; - padding: 5px; - background: #666666; - opacity: .3; - filter: Alpha(Opacity=30); /* support: IE8 */ - border-radius: 8px; + -webkit-box-shadow: 0px 0px 5px #666666; + box-shadow: 0px 0px 5px #666666; } diff --git a/addons/web/static/lib/jquery.ui/jquery-ui.js b/addons/web/static/lib/jquery.ui/jquery-ui.js index cea262f0577..46dc1070efd 100644 --- a/addons/web/static/lib/jquery.ui/jquery-ui.js +++ b/addons/web/static/lib/jquery.ui/jquery-ui.js @@ -1,6 +1,6 @@ -/*! jQuery UI - v1.11.4 - 2016-04-14 +/*! jQuery UI - v1.12.1 - 2019-01-18 * http://jqueryui.com -* Includes: core.js, widget.js, mouse.js, position.js, draggable.js, droppable.js, resizable.js, selectable.js, sortable.js, autocomplete.js, datepicker.js, menu.js, tooltip.js, effect.js, effect-bounce.js +* Includes: widget.js, position.js, data.js, disable-selection.js, focusable.js, keycode.js, scroll-parent.js, unique-id.js, widgets/draggable.js, widgets/droppable.js, widgets/resizable.js, widgets/selectable.js, widgets/sortable.js, widgets/autocomplete.js, widgets/datepicker.js, widgets/menu.js, widgets/mouse.js, widgets/tooltip.js, effect.js, effects/effect-bounce.js * Copyright jQuery Foundation and other contributors; Licensed MIT */ (function( factory ) { @@ -14,319 +14,36 @@ factory( jQuery ); } }(function( $ ) { -/*! - * jQuery UI Core 1.11.4 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - * http://api.jqueryui.com/category/ui-core/ - */ - -// $.ui might exist from components with no dependencies, e.g., $.ui.position $.ui = $.ui || {}; -$.extend( $.ui, { - version: "1.11.4", - - keyCode: { - BACKSPACE: 8, - COMMA: 188, - DELETE: 46, - DOWN: 40, - END: 35, - ENTER: 13, - ESCAPE: 27, - HOME: 36, - LEFT: 37, - PAGE_DOWN: 34, - PAGE_UP: 33, - PERIOD: 190, - RIGHT: 39, - SPACE: 32, - TAB: 9, - UP: 38 - } -}); - -// plugins -$.fn.extend({ - scrollParent: function( includeHidden ) { - var position = this.css( "position" ), - excludeStaticParent = position === "absolute", - overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/, - scrollParent = this.parents().filter( function() { - var parent = $( this ); - if ( excludeStaticParent && parent.css( "position" ) === "static" ) { - return false; - } - return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + parent.css( "overflow-x" ) ); - }).eq( 0 ); - - return position === "fixed" || !scrollParent.length ? $( this[ 0 ].ownerDocument || document ) : scrollParent; - }, - - uniqueId: (function() { - var uuid = 0; - - return function() { - return this.each(function() { - if ( !this.id ) { - this.id = "ui-id-" + ( ++uuid ); - } - }); - }; - })(), - - removeUniqueId: function() { - return this.each(function() { - if ( /^ui-id-\d+$/.test( this.id ) ) { - $( this ).removeAttr( "id" ); - } - }); - } -}); - -// selectors -function focusable( element, isTabIndexNotNaN ) { - var map, mapName, img, - nodeName = element.nodeName.toLowerCase(); - if ( "area" === nodeName ) { - map = element.parentNode; - mapName = map.name; - if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { - return false; - } - img = $( "img[usemap='#" + mapName + "']" )[ 0 ]; - return !!img && visible( img ); - } - return ( /^(input|select|textarea|button|object)$/.test( nodeName ) ? - !element.disabled : - "a" === nodeName ? - element.href || isTabIndexNotNaN : - isTabIndexNotNaN) && - // the element and all of its ancestors must be visible - visible( element ); -} - -function visible( element ) { - return $.expr.filters.visible( element ) && - !$( element ).parents().addBack().filter(function() { - return $.css( this, "visibility" ) === "hidden"; - }).length; -} - -$.extend( $.expr[ ":" ], { - data: $.expr.createPseudo ? - $.expr.createPseudo(function( dataName ) { - return function( elem ) { - return !!$.data( elem, dataName ); - }; - }) : - // support: jQuery <1.8 - function( elem, i, match ) { - return !!$.data( elem, match[ 3 ] ); - }, - - focusable: function( element ) { - return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); - }, - - tabbable: function( element ) { - var tabIndex = $.attr( element, "tabindex" ), - isTabIndexNaN = isNaN( tabIndex ); - return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); - } -}); - -// support: jQuery <1.8 -if ( !$( "
" ).outerWidth( 1 ).jquery ) { - $.each( [ "Width", "Height" ], function( i, name ) { - var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], - type = name.toLowerCase(), - orig = { - innerWidth: $.fn.innerWidth, - innerHeight: $.fn.innerHeight, - outerWidth: $.fn.outerWidth, - outerHeight: $.fn.outerHeight - }; - - function reduce( elem, size, border, margin ) { - $.each( side, function() { - size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; - if ( border ) { - size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; - } - if ( margin ) { - size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; - } - }); - return size; - } - - $.fn[ "inner" + name ] = function( size ) { - if ( size === undefined ) { - return orig[ "inner" + name ].call( this ); - } - - return this.each(function() { - $( this ).css( type, reduce( this, size ) + "px" ); - }); - }; - - $.fn[ "outer" + name] = function( size, margin ) { - if ( typeof size !== "number" ) { - return orig[ "outer" + name ].call( this, size ); - } - - return this.each(function() { - $( this).css( type, reduce( this, size, true, margin ) + "px" ); - }); - }; - }); -} - -// support: jQuery <1.8 -if ( !$.fn.addBack ) { - $.fn.addBack = function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); - }; -} - -// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413) -if ( $( "" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) { - $.fn.removeData = (function( removeData ) { - return function( key ) { - if ( arguments.length ) { - return removeData.call( this, $.camelCase( key ) ); - } else { - return removeData.call( this ); - } - }; - })( $.fn.removeData ); -} - -// deprecated -$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); - -$.fn.extend({ - focus: (function( orig ) { - return function( delay, fn ) { - return typeof delay === "number" ? - this.each(function() { - var elem = this; - setTimeout(function() { - $( elem ).focus(); - if ( fn ) { - fn.call( elem ); - } - }, delay ); - }) : - orig.apply( this, arguments ); - }; - })( $.fn.focus ), - - disableSelection: (function() { - var eventType = "onselectstart" in document.createElement( "div" ) ? - "selectstart" : - "mousedown"; - - return function() { - return this.bind( eventType + ".ui-disableSelection", function( event ) { - event.preventDefault(); - }); - }; - })(), - - enableSelection: function() { - return this.unbind( ".ui-disableSelection" ); - }, - - zIndex: function( zIndex ) { - if ( zIndex !== undefined ) { - return this.css( "zIndex", zIndex ); - } - - if ( this.length ) { - var elem = $( this[ 0 ] ), position, value; - while ( elem.length && elem[ 0 ] !== document ) { - // Ignore z-index if position is set to a value where z-index is ignored by the browser - // This makes behavior of this function consistent across browsers - // WebKit always returns auto if the element is positioned - position = elem.css( "position" ); - if ( position === "absolute" || position === "relative" || position === "fixed" ) { - // IE returns 0 when zIndex is not specified - // other browsers return a string - // we ignore the case of nested elements with an explicit value of 0 - //
- value = parseInt( elem.css( "zIndex" ), 10 ); - if ( !isNaN( value ) && value !== 0 ) { - return value; - } - } - elem = elem.parent(); - } - } - - return 0; - } -}); - -// $.ui.plugin is deprecated. Use $.widget() extensions instead. -$.ui.plugin = { - add: function( module, option, set ) { - var i, - proto = $.ui[ module ].prototype; - for ( i in set ) { - proto.plugins[ i ] = proto.plugins[ i ] || []; - proto.plugins[ i ].push( [ option, set[ i ] ] ); - } - }, - call: function( instance, name, args, allowDisconnected ) { - var i, - set = instance.plugins[ name ]; - - if ( !set ) { - return; - } - - if ( !allowDisconnected && ( !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) ) { - return; - } - - for ( i = 0; i < set.length; i++ ) { - if ( instance.options[ set[ i ][ 0 ] ] ) { - set[ i ][ 1 ].apply( instance.element, args ); - } - } - } -}; +var version = $.ui.version = "1.12.1"; /*! - * jQuery UI Widget 1.11.4 + * jQuery UI Widget 1.12.1 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license - * - * http://api.jqueryui.com/jQuery.widget/ */ +//>>label: Widget +//>>group: Core +//>>description: Provides a factory for creating stateful widgets with a common API. +//>>docs: http://api.jqueryui.com/jQuery.widget/ +//>>demos: http://jqueryui.com/widget/ -var widget_uuid = 0, - widget_slice = Array.prototype.slice; -$.cleanData = (function( orig ) { + +var widgetUuid = 0; +var widgetSlice = Array.prototype.slice; + +$.cleanData = ( function( orig ) { return function( elems ) { var events, elem, i; - for ( i = 0; (elem = elems[i]) != null; i++ ) { + for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) { try { // Only trigger remove when necessary to save time @@ -335,29 +52,34 @@ $.cleanData = (function( orig ) { $( elem ).triggerHandler( "remove" ); } - // http://bugs.jquery.com/ticket/8235 + // Http://bugs.jquery.com/ticket/8235 } catch ( e ) {} } orig( elems ); }; -})( $.cleanData ); +} )( $.cleanData ); $.widget = function( name, base, prototype ) { - var fullName, existingConstructor, constructor, basePrototype, - // proxiedPrototype allows the provided prototype to remain unmodified - // so that it can be used as a mixin for multiple widgets (#8876) - proxiedPrototype = {}, - namespace = name.split( "." )[ 0 ]; + var existingConstructor, constructor, basePrototype; + // ProxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + var proxiedPrototype = {}; + + var namespace = name.split( "." )[ 0 ]; name = name.split( "." )[ 1 ]; - fullName = namespace + "-" + name; + var fullName = namespace + "-" + name; if ( !prototype ) { prototype = base; base = $.Widget; } - // create selector for plugin + if ( $.isArray( prototype ) ) { + prototype = $.extend.apply( null, [ {} ].concat( prototype ) ); + } + + // Create selector for plugin $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { return !!$.data( elem, fullName ); }; @@ -365,30 +87,35 @@ $.widget = function( name, base, prototype ) { $[ namespace ] = $[ namespace ] || {}; existingConstructor = $[ namespace ][ name ]; constructor = $[ namespace ][ name ] = function( options, element ) { - // allow instantiation without "new" keyword + + // Allow instantiation without "new" keyword if ( !this._createWidget ) { return new constructor( options, element ); } - // allow instantiation without initializing for simple inheritance + // Allow instantiation without initializing for simple inheritance // must use "new" keyword (the code above always passes args) if ( arguments.length ) { this._createWidget( options, element ); } }; - // extend with the existing constructor to carry over any static properties + + // Extend with the existing constructor to carry over any static properties $.extend( constructor, existingConstructor, { version: prototype.version, - // copy the object used to create the prototype in case we need to + + // Copy the object used to create the prototype in case we need to // redefine the widget later _proto: $.extend( {}, prototype ), - // track widgets that inherit from this widget in case this widget is + + // Track widgets that inherit from this widget in case this widget is // redefined after a widget inherits from it _childConstructors: [] - }); + } ); basePrototype = new base(); - // we need to make the options hash a property directly on the new instance + + // We need to make the options hash a property directly on the new instance // otherwise we'll modify the options hash on the prototype that we're // inheriting from basePrototype.options = $.widget.extend( {}, basePrototype.options ); @@ -397,17 +124,19 @@ $.widget = function( name, base, prototype ) { proxiedPrototype[ prop ] = value; return; } - proxiedPrototype[ prop ] = (function() { - var _super = function() { - return base.prototype[ prop ].apply( this, arguments ); - }, - _superApply = function( args ) { - return base.prototype[ prop ].apply( this, args ); - }; + proxiedPrototype[ prop ] = ( function() { + function _super() { + return base.prototype[ prop ].apply( this, arguments ); + } + + function _superApply( args ) { + return base.prototype[ prop ].apply( this, args ); + } + return function() { - var __super = this._super, - __superApply = this._superApply, - returnValue; + var __super = this._super; + var __superApply = this._superApply; + var returnValue; this._super = _super; this._superApply = _superApply; @@ -419,19 +148,20 @@ $.widget = function( name, base, prototype ) { return returnValue; }; - })(); - }); + } )(); + } ); constructor.prototype = $.widget.extend( basePrototype, { + // TODO: remove support for widgetEventPrefix // always use the name + a colon as the prefix, e.g., draggable:start // don't prefix for widgets that aren't DOM-based - widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name + widgetEventPrefix: existingConstructor ? ( basePrototype.widgetEventPrefix || name ) : name }, proxiedPrototype, { constructor: constructor, namespace: namespace, widgetName: name, widgetFullName: fullName - }); + } ); // If this widget is being redefined then we need to find all widgets that // are inheriting from it and redefine all of them so that they inherit from @@ -441,11 +171,13 @@ $.widget = function( name, base, prototype ) { $.each( existingConstructor._childConstructors, function( i, child ) { var childPrototype = child.prototype; - // redefine the child widget using the same prototype that was + // Redefine the child widget using the same prototype that was // originally used, but inherit from the new version of the base - $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); - }); - // remove the list of existing child constructors from the old constructor + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, + child._proto ); + } ); + + // Remove the list of existing child constructors from the old constructor // so the old child constructors can be garbage collected delete existingConstructor._childConstructors; } else { @@ -458,21 +190,25 @@ $.widget = function( name, base, prototype ) { }; $.widget.extend = function( target ) { - var input = widget_slice.call( arguments, 1 ), - inputIndex = 0, - inputLength = input.length, - key, - value; + var input = widgetSlice.call( arguments, 1 ); + var inputIndex = 0; + var inputLength = input.length; + var key; + var value; + for ( ; inputIndex < inputLength; inputIndex++ ) { for ( key in input[ inputIndex ] ) { value = input[ inputIndex ][ key ]; if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + // Clone objects if ( $.isPlainObject( value ) ) { target[ key ] = $.isPlainObject( target[ key ] ) ? $.widget.extend( {}, target[ key ], value ) : + // Don't extend strings, arrays, etc. with objects $.widget.extend( {}, value ); + // Copy everything else by reference } else { target[ key ] = value; @@ -486,41 +222,55 @@ $.widget.extend = function( target ) { $.widget.bridge = function( name, object ) { var fullName = object.prototype.widgetFullName || name; $.fn[ name ] = function( options ) { - var isMethodCall = typeof options === "string", - args = widget_slice.call( arguments, 1 ), - returnValue = this; + var isMethodCall = typeof options === "string"; + var args = widgetSlice.call( arguments, 1 ); + var returnValue = this; if ( isMethodCall ) { - this.each(function() { - var methodValue, - instance = $.data( this, fullName ); - if ( options === "instance" ) { - returnValue = instance; - return false; - } - if ( !instance ) { - return $.error( "cannot call methods on " + name + " prior to initialization; " + - "attempted to call method '" + options + "'" ); - } - if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { - return $.error( "no such method '" + options + "' for " + name + " widget instance" ); - } - methodValue = instance[ options ].apply( instance, args ); - if ( methodValue !== instance && methodValue !== undefined ) { - returnValue = methodValue && methodValue.jquery ? - returnValue.pushStack( methodValue.get() ) : - methodValue; - return false; - } - }); + + // If this is an empty collection, we need to have the instance method + // return undefined instead of the jQuery instance + if ( !this.length && options === "instance" ) { + returnValue = undefined; + } else { + this.each( function() { + var methodValue; + var instance = $.data( this, fullName ); + + if ( options === "instance" ) { + returnValue = instance; + return false; + } + + if ( !instance ) { + return $.error( "cannot call methods on " + name + + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + + if ( !$.isFunction( instance[ options ] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + + " widget instance" ); + } + + methodValue = instance[ options ].apply( instance, args ); + + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + } ); + } } else { // Allow multiple hashes to be passed on init if ( args.length ) { - options = $.widget.extend.apply( null, [ options ].concat(args) ); + options = $.widget.extend.apply( null, [ options ].concat( args ) ); } - this.each(function() { + this.each( function() { var instance = $.data( this, fullName ); if ( instance ) { instance.option( options || {} ); @@ -530,7 +280,7 @@ $.widget.bridge = function( name, object ) { } else { $.data( this, fullName, new object( options, this ) ); } - }); + } ); } return returnValue; @@ -544,21 +294,25 @@ $.Widget.prototype = { widgetName: "widget", widgetEventPrefix: "", defaultElement: "
", + options: { + classes: {}, disabled: false, - // callbacks + // Callbacks create: null }, + _createWidget: function( options, element ) { element = $( element || this.defaultElement || this )[ 0 ]; this.element = $( element ); - this.uuid = widget_uuid++; + this.uuid = widgetUuid++; this.eventNamespace = "." + this.widgetName + this.uuid; this.bindings = $(); this.hoverable = $(); this.focusable = $(); + this.classesElementLookup = {}; if ( element !== this ) { $.data( element, this.widgetFullName, this ); @@ -568,13 +322,15 @@ $.Widget.prototype = { this.destroy(); } } - }); + } ); this.document = $( element.style ? - // element within the document + + // Element within the document element.ownerDocument : - // element is window or document + + // Element is window or document element.document || element ); - this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); + this.window = $( this.document[ 0 ].defaultView || this.document[ 0 ].parentWindow ); } this.options = $.widget.extend( {}, @@ -583,36 +339,46 @@ $.Widget.prototype = { options ); this._create(); + + if ( this.options.disabled ) { + this._setOptionDisabled( this.options.disabled ); + } + this._trigger( "create", null, this._getCreateEventData() ); this._init(); }, - _getCreateOptions: $.noop, + + _getCreateOptions: function() { + return {}; + }, + _getCreateEventData: $.noop, + _create: $.noop, + _init: $.noop, destroy: function() { + var that = this; + this._destroy(); - // we can probably remove the unbind calls in 2.0 + $.each( this.classesElementLookup, function( key, value ) { + that._removeClass( value, key ); + } ); + + // We can probably remove the unbind calls in 2.0 // all event bindings should go through this._on() this.element - .unbind( this.eventNamespace ) - .removeData( this.widgetFullName ) - // support: jquery <1.6.3 - // http://bugs.jquery.com/ticket/9413 - .removeData( $.camelCase( this.widgetFullName ) ); + .off( this.eventNamespace ) + .removeData( this.widgetFullName ); this.widget() - .unbind( this.eventNamespace ) - .removeAttr( "aria-disabled" ) - .removeClass( - this.widgetFullName + "-disabled " + - "ui-state-disabled" ); + .off( this.eventNamespace ) + .removeAttr( "aria-disabled" ); - // clean up events and states - this.bindings.unbind( this.eventNamespace ); - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); + // Clean up events and states + this.bindings.off( this.eventNamespace ); }, + _destroy: $.noop, widget: function() { @@ -620,18 +386,20 @@ $.Widget.prototype = { }, option: function( key, value ) { - var options = key, - parts, - curOption, - i; + var options = key; + var parts; + var curOption; + var i; if ( arguments.length === 0 ) { - // don't return a reference to the internal hash + + // Don't return a reference to the internal hash return $.widget.extend( {}, this.options ); } if ( typeof key === "string" ) { - // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + + // Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } options = {}; parts = key.split( "." ); key = parts.shift(); @@ -658,6 +426,7 @@ $.Widget.prototype = { return this; }, + _setOptions: function( options ) { var key; @@ -667,42 +436,152 @@ $.Widget.prototype = { return this; }, + _setOption: function( key, value ) { + if ( key === "classes" ) { + this._setOptionClasses( value ); + } + this.options[ key ] = value; if ( key === "disabled" ) { - this.widget() - .toggleClass( this.widgetFullName + "-disabled", !!value ); - - // If the widget is becoming disabled, then nothing is interactive - if ( value ) { - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - } + this._setOptionDisabled( value ); } return this; }, - enable: function() { - return this._setOptions({ disabled: false }); + _setOptionClasses: function( value ) { + var classKey, elements, currentElements; + + for ( classKey in value ) { + currentElements = this.classesElementLookup[ classKey ]; + if ( value[ classKey ] === this.options.classes[ classKey ] || + !currentElements || + !currentElements.length ) { + continue; + } + + // We are doing this to create a new jQuery object because the _removeClass() call + // on the next line is going to destroy the reference to the current elements being + // tracked. We need to save a copy of this collection so that we can add the new classes + // below. + elements = $( currentElements.get() ); + this._removeClass( currentElements, classKey ); + + // We don't use _addClass() here, because that uses this.options.classes + // for generating the string of classes. We want to use the value passed in from + // _setOption(), this is the new value of the classes option which was passed to + // _setOption(). We pass this value directly to _classes(). + elements.addClass( this._classes( { + element: elements, + keys: classKey, + classes: value, + add: true + } ) ); + } }, + + _setOptionDisabled: function( value ) { + this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, !!value ); + + // If the widget is becoming disabled, then nothing is interactive + if ( value ) { + this._removeClass( this.hoverable, null, "ui-state-hover" ); + this._removeClass( this.focusable, null, "ui-state-focus" ); + } + }, + + enable: function() { + return this._setOptions( { disabled: false } ); + }, + disable: function() { - return this._setOptions({ disabled: true }); + return this._setOptions( { disabled: true } ); + }, + + _classes: function( options ) { + var full = []; + var that = this; + + options = $.extend( { + element: this.element, + classes: this.options.classes || {} + }, options ); + + function processClassString( classes, checkOption ) { + var current, i; + for ( i = 0; i < classes.length; i++ ) { + current = that.classesElementLookup[ classes[ i ] ] || $(); + if ( options.add ) { + current = $( $.unique( current.get().concat( options.element.get() ) ) ); + } else { + current = $( current.not( options.element ).get() ); + } + that.classesElementLookup[ classes[ i ] ] = current; + full.push( classes[ i ] ); + if ( checkOption && options.classes[ classes[ i ] ] ) { + full.push( options.classes[ classes[ i ] ] ); + } + } + } + + this._on( options.element, { + "remove": "_untrackClassesElement" + } ); + + if ( options.keys ) { + processClassString( options.keys.match( /\S+/g ) || [], true ); + } + if ( options.extra ) { + processClassString( options.extra.match( /\S+/g ) || [] ); + } + + return full.join( " " ); + }, + + _untrackClassesElement: function( event ) { + var that = this; + $.each( that.classesElementLookup, function( key, value ) { + if ( $.inArray( event.target, value ) !== -1 ) { + that.classesElementLookup[ key ] = $( value.not( event.target ).get() ); + } + } ); + }, + + _removeClass: function( element, keys, extra ) { + return this._toggleClass( element, keys, extra, false ); + }, + + _addClass: function( element, keys, extra ) { + return this._toggleClass( element, keys, extra, true ); + }, + + _toggleClass: function( element, keys, extra, add ) { + add = ( typeof add === "boolean" ) ? add : extra; + var shift = ( typeof element === "string" || element === null ), + options = { + extra: shift ? keys : extra, + keys: shift ? element : keys, + element: shift ? this.element : element, + add: add + }; + options.element.toggleClass( this._classes( options ), add ); + return this; }, _on: function( suppressDisabledCheck, element, handlers ) { - var delegateElement, - instance = this; + var delegateElement; + var instance = this; - // no suppressDisabledCheck flag, shuffle arguments + // No suppressDisabledCheck flag, shuffle arguments if ( typeof suppressDisabledCheck !== "boolean" ) { handlers = element; element = suppressDisabledCheck; suppressDisabledCheck = false; } - // no element argument, shuffle and use this.element + // No element argument, shuffle and use this.element if ( !handlers ) { handlers = element; element = this.element; @@ -714,39 +593,41 @@ $.Widget.prototype = { $.each( handlers, function( event, handler ) { function handlerProxy() { - // allow widgets to customize the disabled handling + + // Allow widgets to customize the disabled handling // - disabled as an array instead of boolean // - disabled class as method for disabling individual parts if ( !suppressDisabledCheck && ( instance.options.disabled === true || - $( this ).hasClass( "ui-state-disabled" ) ) ) { + $( this ).hasClass( "ui-state-disabled" ) ) ) { return; } return ( typeof handler === "string" ? instance[ handler ] : handler ) .apply( instance, arguments ); } - // copy the guid so direct unbinding works + // Copy the guid so direct unbinding works if ( typeof handler !== "string" ) { handlerProxy.guid = handler.guid = handler.guid || handlerProxy.guid || $.guid++; } - var match = event.match( /^([\w:-]*)\s*(.*)$/ ), - eventName = match[1] + instance.eventNamespace, - selector = match[2]; + var match = event.match( /^([\w:-]*)\s*(.*)$/ ); + var eventName = match[ 1 ] + instance.eventNamespace; + var selector = match[ 2 ]; + if ( selector ) { - delegateElement.delegate( selector, eventName, handlerProxy ); + delegateElement.on( eventName, selector, handlerProxy ); } else { - element.bind( eventName, handlerProxy ); + element.on( eventName, handlerProxy ); } - }); + } ); }, _off: function( element, eventName ) { - eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + + eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; - element.unbind( eventName ).undelegate( eventName ); + element.off( eventName ).off( eventName ); // Clear the stack to avoid memory leaks (#10056) this.bindings = $( this.bindings.not( element ).get() ); @@ -767,40 +648,41 @@ $.Widget.prototype = { this.hoverable = this.hoverable.add( element ); this._on( element, { mouseenter: function( event ) { - $( event.currentTarget ).addClass( "ui-state-hover" ); + this._addClass( $( event.currentTarget ), null, "ui-state-hover" ); }, mouseleave: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-hover" ); + this._removeClass( $( event.currentTarget ), null, "ui-state-hover" ); } - }); + } ); }, _focusable: function( element ) { this.focusable = this.focusable.add( element ); this._on( element, { focusin: function( event ) { - $( event.currentTarget ).addClass( "ui-state-focus" ); + this._addClass( $( event.currentTarget ), null, "ui-state-focus" ); }, focusout: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-focus" ); + this._removeClass( $( event.currentTarget ), null, "ui-state-focus" ); } - }); + } ); }, _trigger: function( type, event, data ) { - var prop, orig, - callback = this.options[ type ]; + var prop, orig; + var callback = this.options[ type ]; data = data || {}; event = $.Event( event ); event.type = ( type === this.widgetEventPrefix ? type : this.widgetEventPrefix + type ).toLowerCase(); - // the original event may come from any element + + // The original event may come from any element // so we need to reset the target on the new event event.target = this.element[ 0 ]; - // copy original event properties over to the new event + // Copy original event properties over to the new event orig = event.originalEvent; if ( orig ) { for ( prop in orig ) { @@ -812,7 +694,7 @@ $.Widget.prototype = { this.element.trigger( event, data ); return !( $.isFunction( callback ) && - callback.apply( this.element[0], [ event ].concat( data ) ) === false || + callback.apply( this.element[ 0 ], [ event ].concat( data ) ) === false || event.isDefaultPrevented() ); } }; @@ -822,228 +704,47 @@ $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { if ( typeof options === "string" ) { options = { effect: options }; } - var hasOptions, - effectName = !options ? - method : - options === true || typeof options === "number" ? - defaultEffect : - options.effect || defaultEffect; + + var hasOptions; + var effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + options = options || {}; if ( typeof options === "number" ) { options = { duration: options }; } + hasOptions = !$.isEmptyObject( options ); options.complete = callback; + if ( options.delay ) { element.delay( options.delay ); } + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { element[ method ]( options ); } else if ( effectName !== method && element[ effectName ] ) { element[ effectName ]( options.duration, options.easing, callback ); } else { - element.queue(function( next ) { + element.queue( function( next ) { $( this )[ method ](); if ( callback ) { callback.call( element[ 0 ] ); } next(); - }); + } ); } }; -}); +} ); var widget = $.widget; /*! - * jQuery UI Mouse 1.11.4 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - * http://api.jqueryui.com/mouse/ - */ - - -var mouseHandled = false; -$( document ).mouseup( function() { - mouseHandled = false; -}); - -var mouse = $.widget("ui.mouse", { - version: "1.11.4", - options: { - cancel: "input,textarea,button,select,option", - distance: 1, - delay: 0 - }, - _mouseInit: function() { - var that = this; - - this.element - .bind("mousedown." + this.widgetName, function(event) { - return that._mouseDown(event); - }) - .bind("click." + this.widgetName, function(event) { - if (true === $.data(event.target, that.widgetName + ".preventClickEvent")) { - $.removeData(event.target, that.widgetName + ".preventClickEvent"); - event.stopImmediatePropagation(); - return false; - } - }); - - this.started = false; - }, - - // TODO: make sure destroying one instance of mouse doesn't mess with - // other instances of mouse - _mouseDestroy: function() { - this.element.unbind("." + this.widgetName); - if ( this._mouseMoveDelegate ) { - this.document - .unbind("mousemove." + this.widgetName, this._mouseMoveDelegate) - .unbind("mouseup." + this.widgetName, this._mouseUpDelegate); - } - }, - - _mouseDown: function(event) { - // don't let more than one widget handle mouseStart - if ( mouseHandled ) { - return; - } - - this._mouseMoved = false; - - // we may have missed mouseup (out of window) - (this._mouseStarted && this._mouseUp(event)); - - this._mouseDownEvent = event; - - var that = this, - btnIsLeft = (event.which === 1), - // event.target.nodeName works around a bug in IE 8 with - // disabled inputs (#7620) - elIsCancel = (typeof this.options.cancel === "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false); - if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) { - return true; - } - - this.mouseDelayMet = !this.options.delay; - if (!this.mouseDelayMet) { - this._mouseDelayTimer = setTimeout(function() { - that.mouseDelayMet = true; - }, this.options.delay); - } - - if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { - this._mouseStarted = (this._mouseStart(event) !== false); - if (!this._mouseStarted) { - event.preventDefault(); - return true; - } - } - - // Click event may never have fired (Gecko & Opera) - if (true === $.data(event.target, this.widgetName + ".preventClickEvent")) { - $.removeData(event.target, this.widgetName + ".preventClickEvent"); - } - - // these delegates are required to keep context - this._mouseMoveDelegate = function(event) { - return that._mouseMove(event); - }; - this._mouseUpDelegate = function(event) { - return that._mouseUp(event); - }; - - this.document - .bind( "mousemove." + this.widgetName, this._mouseMoveDelegate ) - .bind( "mouseup." + this.widgetName, this._mouseUpDelegate ); - - event.preventDefault(); - - mouseHandled = true; - return true; - }, - - _mouseMove: function(event) { - // Only check for mouseups outside the document if you've moved inside the document - // at least once. This prevents the firing of mouseup in the case of IE<9, which will - // fire a mousemove event if content is placed under the cursor. See #7778 - // Support: IE <9 - if ( this._mouseMoved ) { - // IE mouseup check - mouseup happened when mouse was out of window - if ($.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && !event.button) { - return this._mouseUp(event); - - // Iframe mouseup check - mouseup occurred in another document - } else if ( !event.which ) { - return this._mouseUp( event ); - } - } - - if ( event.which || event.button ) { - this._mouseMoved = true; - } - - if (this._mouseStarted) { - this._mouseDrag(event); - return event.preventDefault(); - } - - if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { - this._mouseStarted = - (this._mouseStart(this._mouseDownEvent, event) !== false); - (this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event)); - } - - return !this._mouseStarted; - }, - - _mouseUp: function(event) { - this.document - .unbind( "mousemove." + this.widgetName, this._mouseMoveDelegate ) - .unbind( "mouseup." + this.widgetName, this._mouseUpDelegate ); - - if (this._mouseStarted) { - this._mouseStarted = false; - - if (event.target === this._mouseDownEvent.target) { - $.data(event.target, this.widgetName + ".preventClickEvent", true); - } - - this._mouseStop(event); - } - - mouseHandled = false; - return false; - }, - - _mouseDistanceMet: function(event) { - return (Math.max( - Math.abs(this._mouseDownEvent.pageX - event.pageX), - Math.abs(this._mouseDownEvent.pageY - event.pageY) - ) >= this.options.distance - ); - }, - - _mouseDelayMet: function(/* event */) { - return this.mouseDelayMet; - }, - - // These are placeholder methods, to be overriden by extending plugin - _mouseStart: function(/* event */) {}, - _mouseDrag: function(/* event */) {}, - _mouseStop: function(/* event */) {}, - _mouseCapture: function(/* event */) { return true; } -}); - - -/*! - * jQuery UI Position 1.11.4 + * jQuery UI Position 1.12.1 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors @@ -1053,14 +754,17 @@ var mouse = $.widget("ui.mouse", { * http://api.jqueryui.com/position/ */ -(function() { +//>>label: Position +//>>group: Core +//>>description: Positions elements relative to other elements. +//>>docs: http://api.jqueryui.com/position/ +//>>demos: http://jqueryui.com/position/ -$.ui = $.ui || {}; -var cachedScrollbarWidth, supportsOffsetFractions, +( function() { +var cachedScrollbarWidth, max = Math.max, abs = Math.abs, - round = Math.round, rhorizontal = /left|center|right/, rvertical = /top|center|bottom/, roffset = /[\+\-]\d+(\.[\d]+)?%?/, @@ -1080,7 +784,7 @@ function parseCss( element, property ) { } function getDimensions( elem ) { - var raw = elem[0]; + var raw = elem[ 0 ]; if ( raw.nodeType === 9 ) { return { width: elem.width(), @@ -1115,8 +819,10 @@ $.position = { return cachedScrollbarWidth; } var w1, w2, - div = $( "
" ), - innerDiv = div.children()[0]; + div = $( "
" + + "
" ), + innerDiv = div.children()[ 0 ]; $( "body" ).append( div ); w1 = innerDiv.offsetWidth; @@ -1125,12 +831,12 @@ $.position = { w2 = innerDiv.offsetWidth; if ( w1 === w2 ) { - w2 = div[0].clientWidth; + w2 = div[ 0 ].clientWidth; } div.remove(); - return (cachedScrollbarWidth = w1 - w2); + return ( cachedScrollbarWidth = w1 - w2 ); }, getScrollInfo: function( within ) { var overflowX = within.isWindow || within.isDocument ? "" : @@ -1138,9 +844,9 @@ $.position = { overflowY = within.isWindow || within.isDocument ? "" : within.element.css( "overflow-y" ), hasOverflowX = overflowX === "scroll" || - ( overflowX === "auto" && within.width < within.element[0].scrollWidth ), + ( overflowX === "auto" && within.width < within.element[ 0 ].scrollWidth ), hasOverflowY = overflowY === "scroll" || - ( overflowY === "auto" && within.height < within.element[0].scrollHeight ); + ( overflowY === "auto" && within.height < within.element[ 0 ].scrollHeight ); return { width: hasOverflowY ? $.position.scrollbarWidth() : 0, height: hasOverflowX ? $.position.scrollbarWidth() : 0 @@ -1148,20 +854,18 @@ $.position = { }, getWithinInfo: function( element ) { var withinElement = $( element || window ), - isWindow = $.isWindow( withinElement[0] ), - isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9; + isWindow = $.isWindow( withinElement[ 0 ] ), + isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9, + hasOffset = !isWindow && !isDocument; return { element: withinElement, isWindow: isWindow, isDocument: isDocument, - offset: withinElement.offset() || { left: 0, top: 0 }, + offset: hasOffset ? $( element ).offset() : { left: 0, top: 0 }, scrollLeft: withinElement.scrollLeft(), scrollTop: withinElement.scrollTop(), - - // support: jQuery 1.6.x - // jQuery 1.6 doesn't support .outerWidth/Height() on documents or windows - width: isWindow || isDocument ? withinElement.width() : withinElement.outerWidth(), - height: isWindow || isDocument ? withinElement.height() : withinElement.outerHeight() + width: withinElement.outerWidth(), + height: withinElement.outerHeight() }; } }; @@ -1171,7 +875,7 @@ $.fn.position = function( options ) { return _position.apply( this, arguments ); } - // make a copy, we don't want to modify arguments + // Make a copy, we don't want to modify arguments options = $.extend( {}, options ); var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions, @@ -1182,24 +886,26 @@ $.fn.position = function( options ) { offsets = {}; dimensions = getDimensions( target ); - if ( target[0].preventDefault ) { - // force left top to allow flipping + if ( target[ 0 ].preventDefault ) { + + // Force left top to allow flipping options.at = "left top"; } targetWidth = dimensions.width; targetHeight = dimensions.height; targetOffset = dimensions.offset; - // clone to reuse original targetOffset later + + // Clone to reuse original targetOffset later basePosition = $.extend( {}, targetOffset ); - // force my and at to have valid horizontal and vertical positions + // Force my and at to have valid horizontal and vertical positions // if a value is missing or invalid, it will be converted to center $.each( [ "my", "at" ], function() { var pos = ( options[ this ] || "" ).split( " " ), horizontalOffset, verticalOffset; - if ( pos.length === 1) { + if ( pos.length === 1 ) { pos = rhorizontal.test( pos[ 0 ] ) ? pos.concat( [ "center" ] ) : rvertical.test( pos[ 0 ] ) ? @@ -1209,7 +915,7 @@ $.fn.position = function( options ) { pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center"; pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center"; - // calculate offsets + // Calculate offsets horizontalOffset = roffset.exec( pos[ 0 ] ); verticalOffset = roffset.exec( pos[ 1 ] ); offsets[ this ] = [ @@ -1217,14 +923,14 @@ $.fn.position = function( options ) { verticalOffset ? verticalOffset[ 0 ] : 0 ]; - // reduce to just the positions without the offsets + // Reduce to just the positions without the offsets options[ this ] = [ rposition.exec( pos[ 0 ] )[ 0 ], rposition.exec( pos[ 1 ] )[ 0 ] ]; - }); + } ); - // normalize collision option + // Normalize collision option if ( collision.length === 1 ) { collision[ 1 ] = collision[ 0 ]; } @@ -1245,15 +951,17 @@ $.fn.position = function( options ) { basePosition.left += atOffset[ 0 ]; basePosition.top += atOffset[ 1 ]; - return this.each(function() { + return this.each( function() { var collisionPosition, using, elem = $( this ), elemWidth = elem.outerWidth(), elemHeight = elem.outerHeight(), marginLeft = parseCss( this, "marginLeft" ), marginTop = parseCss( this, "marginTop" ), - collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + scrollInfo.width, - collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + scrollInfo.height, + collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + + scrollInfo.width, + collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + + scrollInfo.height, position = $.extend( {}, basePosition ), myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() ); @@ -1272,12 +980,6 @@ $.fn.position = function( options ) { position.left += myOffset[ 0 ]; position.top += myOffset[ 1 ]; - // if the browser doesn't support fractions, then round for consistent results - if ( !supportsOffsetFractions ) { - position.left = round( position.left ); - position.top = round( position.top ); - } - collisionPosition = { marginLeft: marginLeft, marginTop: marginTop @@ -1298,12 +1000,13 @@ $.fn.position = function( options ) { at: options.at, within: within, elem: elem - }); + } ); } - }); + } ); if ( options.using ) { - // adds feedback as second argument to using callback, if present + + // Adds feedback as second argument to using callback, if present using = function( props ) { var left = targetOffset.left - position.left, right = left + targetWidth - elemWidth, @@ -1343,7 +1046,7 @@ $.fn.position = function( options ) { } elem.offset( $.extend( position, { using: using } ) ); - }); + } ); }; $.ui.position = { @@ -1357,16 +1060,20 @@ $.ui.position = { overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset, newOverRight; - // element is wider than within + // Element is wider than within if ( data.collisionWidth > outerWidth ) { - // element is initially over the left side of within + + // Element is initially over the left side of within if ( overLeft > 0 && overRight <= 0 ) { - newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset; + newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - + withinOffset; position.left += overLeft - newOverRight; - // element is initially over right side of within + + // Element is initially over right side of within } else if ( overRight > 0 && overLeft <= 0 ) { position.left = withinOffset; - // element is initially over both left and right sides of within + + // Element is initially over both left and right sides of within } else { if ( overLeft > overRight ) { position.left = withinOffset + outerWidth - data.collisionWidth; @@ -1374,13 +1081,16 @@ $.ui.position = { position.left = withinOffset; } } - // too far left -> align with left edge + + // Too far left -> align with left edge } else if ( overLeft > 0 ) { position.left += overLeft; - // too far right -> align with right edge + + // Too far right -> align with right edge } else if ( overRight > 0 ) { position.left -= overRight; - // adjust based on position and margin + + // Adjust based on position and margin } else { position.left = max( position.left - collisionPosLeft, position.left ); } @@ -1394,16 +1104,20 @@ $.ui.position = { overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset, newOverBottom; - // element is taller than within + // Element is taller than within if ( data.collisionHeight > outerHeight ) { - // element is initially over the top of within + + // Element is initially over the top of within if ( overTop > 0 && overBottom <= 0 ) { - newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset; + newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - + withinOffset; position.top += overTop - newOverBottom; - // element is initially over bottom of within + + // Element is initially over bottom of within } else if ( overBottom > 0 && overTop <= 0 ) { position.top = withinOffset; - // element is initially over both top and bottom of within + + // Element is initially over both top and bottom of within } else { if ( overTop > overBottom ) { position.top = withinOffset + outerHeight - data.collisionHeight; @@ -1411,13 +1125,16 @@ $.ui.position = { position.top = withinOffset; } } - // too far up -> align with top + + // Too far up -> align with top } else if ( overTop > 0 ) { position.top += overTop; - // too far down -> align with bottom edge + + // Too far down -> align with bottom edge } else if ( overBottom > 0 ) { position.top -= overBottom; - // adjust based on position and margin + + // Adjust based on position and margin } else { position.top = max( position.top - collisionPosTop, position.top ); } @@ -1447,12 +1164,14 @@ $.ui.position = { newOverLeft; if ( overLeft < 0 ) { - newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset; + newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - + outerWidth - withinOffset; if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) { position.left += myOffset + atOffset + offset; } } else if ( overRight > 0 ) { - newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + atOffset + offset - offsetLeft; + newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + + atOffset + offset - offsetLeft; if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) { position.left += myOffset + atOffset + offset; } @@ -1481,12 +1200,14 @@ $.ui.position = { newOverTop, newOverBottom; if ( overTop < 0 ) { - newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset; + newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - + outerHeight - withinOffset; if ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) { position.top += myOffset + atOffset + offset; } } else if ( overBottom > 0 ) { - newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + offset - offsetTop; + newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + + offset - offsetTop; if ( newOverTop > 0 || abs( newOverTop ) < overBottom ) { position.top += myOffset + atOffset + offset; } @@ -1505,64 +1226,570 @@ $.ui.position = { } }; -// fraction support test -(function() { - var testElement, testElementParent, testElementStyle, offsetLeft, i, - body = document.getElementsByTagName( "body" )[ 0 ], - div = document.createElement( "div" ); - - //Create a "fake body" for testing based on method used in jQuery.support - testElement = document.createElement( body ? "div" : "body" ); - testElementStyle = { - visibility: "hidden", - width: 0, - height: 0, - border: 0, - margin: 0, - background: "none" - }; - if ( body ) { - $.extend( testElementStyle, { - position: "absolute", - left: "-1000px", - top: "-1000px" - }); - } - for ( i in testElementStyle ) { - testElement.style[ i ] = testElementStyle[ i ]; - } - testElement.appendChild( div ); - testElementParent = body || document.documentElement; - testElementParent.insertBefore( testElement, testElementParent.firstChild ); - - div.style.cssText = "position: absolute; left: 10.7432222px;"; - - offsetLeft = $( div ).offset().left; - supportsOffsetFractions = offsetLeft > 10 && offsetLeft < 11; - - testElement.innerHTML = ""; - testElementParent.removeChild( testElement ); -})(); - -})(); +} )(); var position = $.ui.position; /*! - * jQuery UI Draggable 1.11.4 + * jQuery UI :data 1.12.1 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license - * - * http://api.jqueryui.com/draggable/ */ +//>>label: :data Selector +//>>group: Core +//>>description: Selects elements which have data stored under the specified key. +//>>docs: http://api.jqueryui.com/data-selector/ -$.widget("ui.draggable", $.ui.mouse, { - version: "1.11.4", + +var data = $.extend( $.expr[ ":" ], { + data: $.expr.createPseudo ? + $.expr.createPseudo( function( dataName ) { + return function( elem ) { + return !!$.data( elem, dataName ); + }; + } ) : + + // Support: jQuery <1.8 + function( elem, i, match ) { + return !!$.data( elem, match[ 3 ] ); + } +} ); + +/*! + * jQuery UI Disable Selection 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: disableSelection +//>>group: Core +//>>description: Disable selection of text content within the set of matched elements. +//>>docs: http://api.jqueryui.com/disableSelection/ + +// This file is deprecated + + +var disableSelection = $.fn.extend( { + disableSelection: ( function() { + var eventType = "onselectstart" in document.createElement( "div" ) ? + "selectstart" : + "mousedown"; + + return function() { + return this.on( eventType + ".ui-disableSelection", function( event ) { + event.preventDefault(); + } ); + }; + } )(), + + enableSelection: function() { + return this.off( ".ui-disableSelection" ); + } +} ); + + +/*! + * jQuery UI Focusable 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: :focusable Selector +//>>group: Core +//>>description: Selects elements which can be focused. +//>>docs: http://api.jqueryui.com/focusable-selector/ + + + +// Selectors +$.ui.focusable = function( element, hasTabindex ) { + var map, mapName, img, focusableIfVisible, fieldset, + nodeName = element.nodeName.toLowerCase(); + + if ( "area" === nodeName ) { + map = element.parentNode; + mapName = map.name; + if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { + return false; + } + img = $( "img[usemap='#" + mapName + "']" ); + return img.length > 0 && img.is( ":visible" ); + } + + if ( /^(input|select|textarea|button|object)$/.test( nodeName ) ) { + focusableIfVisible = !element.disabled; + + if ( focusableIfVisible ) { + + // Form controls within a disabled fieldset are disabled. + // However, controls within the fieldset's legend do not get disabled. + // Since controls generally aren't placed inside legends, we skip + // this portion of the check. + fieldset = $( element ).closest( "fieldset" )[ 0 ]; + if ( fieldset ) { + focusableIfVisible = !fieldset.disabled; + } + } + } else if ( "a" === nodeName ) { + focusableIfVisible = element.href || hasTabindex; + } else { + focusableIfVisible = hasTabindex; + } + + return focusableIfVisible && $( element ).is( ":visible" ) && visible( $( element ) ); +}; + +// Support: IE 8 only +// IE 8 doesn't resolve inherit to visible/hidden for computed values +function visible( element ) { + var visibility = element.css( "visibility" ); + while ( visibility === "inherit" ) { + element = element.parent(); + visibility = element.css( "visibility" ); + } + return visibility !== "hidden"; +} + +$.extend( $.expr[ ":" ], { + focusable: function( element ) { + return $.ui.focusable( element, $.attr( element, "tabindex" ) != null ); + } +} ); + +var focusable = $.ui.focusable; + + +/*! + * jQuery UI Keycode 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Keycode +//>>group: Core +//>>description: Provide keycodes as keynames +//>>docs: http://api.jqueryui.com/jQuery.ui.keyCode/ + + +var keycode = $.ui.keyCode = { + BACKSPACE: 8, + COMMA: 188, + DELETE: 46, + DOWN: 40, + END: 35, + ENTER: 13, + ESCAPE: 27, + HOME: 36, + LEFT: 37, + PAGE_DOWN: 34, + PAGE_UP: 33, + PERIOD: 190, + RIGHT: 39, + SPACE: 32, + TAB: 9, + UP: 38 +}; + + +/*! + * jQuery UI Scroll Parent 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: scrollParent +//>>group: Core +//>>description: Get the closest ancestor element that is scrollable. +//>>docs: http://api.jqueryui.com/scrollParent/ + + + +var scrollParent = $.fn.scrollParent = function( includeHidden ) { + var position = this.css( "position" ), + excludeStaticParent = position === "absolute", + overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/, + scrollParent = this.parents().filter( function() { + var parent = $( this ); + if ( excludeStaticParent && parent.css( "position" ) === "static" ) { + return false; + } + return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + + parent.css( "overflow-x" ) ); + } ).eq( 0 ); + + return position === "fixed" || !scrollParent.length ? + $( this[ 0 ].ownerDocument || document ) : + scrollParent; +}; + + +/*! + * jQuery UI Unique ID 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: uniqueId +//>>group: Core +//>>description: Functions to generate and remove uniqueId's +//>>docs: http://api.jqueryui.com/uniqueId/ + + + +var uniqueId = $.fn.extend( { + uniqueId: ( function() { + var uuid = 0; + + return function() { + return this.each( function() { + if ( !this.id ) { + this.id = "ui-id-" + ( ++uuid ); + } + } ); + }; + } )(), + + removeUniqueId: function() { + return this.each( function() { + if ( /^ui-id-\d+$/.test( this.id ) ) { + $( this ).removeAttr( "id" ); + } + } ); + } +} ); + + + + +// This file is deprecated +var ie = $.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); + +/*! + * jQuery UI Mouse 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Mouse +//>>group: Widgets +//>>description: Abstracts mouse-based interactions to assist in creating certain widgets. +//>>docs: http://api.jqueryui.com/mouse/ + + + +var mouseHandled = false; +$( document ).on( "mouseup", function() { + mouseHandled = false; +} ); + +var widgetsMouse = $.widget( "ui.mouse", { + version: "1.12.1", + options: { + cancel: "input, textarea, button, select, option", + distance: 1, + delay: 0 + }, + _mouseInit: function() { + var that = this; + + this.element + .on( "mousedown." + this.widgetName, function( event ) { + return that._mouseDown( event ); + } ) + .on( "click." + this.widgetName, function( event ) { + if ( true === $.data( event.target, that.widgetName + ".preventClickEvent" ) ) { + $.removeData( event.target, that.widgetName + ".preventClickEvent" ); + event.stopImmediatePropagation(); + return false; + } + } ); + + this.started = false; + }, + + // TODO: make sure destroying one instance of mouse doesn't mess with + // other instances of mouse + _mouseDestroy: function() { + this.element.off( "." + this.widgetName ); + if ( this._mouseMoveDelegate ) { + this.document + .off( "mousemove." + this.widgetName, this._mouseMoveDelegate ) + .off( "mouseup." + this.widgetName, this._mouseUpDelegate ); + } + }, + + _mouseDown: function( event ) { + + // don't let more than one widget handle mouseStart + if ( mouseHandled ) { + return; + } + + this._mouseMoved = false; + + // We may have missed mouseup (out of window) + ( this._mouseStarted && this._mouseUp( event ) ); + + this._mouseDownEvent = event; + + var that = this, + btnIsLeft = ( event.which === 1 ), + + // event.target.nodeName works around a bug in IE 8 with + // disabled inputs (#7620) + elIsCancel = ( typeof this.options.cancel === "string" && event.target.nodeName ? + $( event.target ).closest( this.options.cancel ).length : false ); + if ( !btnIsLeft || elIsCancel || !this._mouseCapture( event ) ) { + return true; + } + + this.mouseDelayMet = !this.options.delay; + if ( !this.mouseDelayMet ) { + this._mouseDelayTimer = setTimeout( function() { + that.mouseDelayMet = true; + }, this.options.delay ); + } + + if ( this._mouseDistanceMet( event ) && this._mouseDelayMet( event ) ) { + this._mouseStarted = ( this._mouseStart( event ) !== false ); + if ( !this._mouseStarted ) { + event.preventDefault(); + return true; + } + } + + // Click event may never have fired (Gecko & Opera) + if ( true === $.data( event.target, this.widgetName + ".preventClickEvent" ) ) { + $.removeData( event.target, this.widgetName + ".preventClickEvent" ); + } + + // These delegates are required to keep context + this._mouseMoveDelegate = function( event ) { + return that._mouseMove( event ); + }; + this._mouseUpDelegate = function( event ) { + return that._mouseUp( event ); + }; + + this.document + .on( "mousemove." + this.widgetName, this._mouseMoveDelegate ) + .on( "mouseup." + this.widgetName, this._mouseUpDelegate ); + + event.preventDefault(); + + mouseHandled = true; + return true; + }, + + _mouseMove: function( event ) { + + // Only check for mouseups outside the document if you've moved inside the document + // at least once. This prevents the firing of mouseup in the case of IE<9, which will + // fire a mousemove event if content is placed under the cursor. See #7778 + // Support: IE <9 + if ( this._mouseMoved ) { + + // IE mouseup check - mouseup happened when mouse was out of window + if ( $.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && + !event.button ) { + return this._mouseUp( event ); + + // Iframe mouseup check - mouseup occurred in another document + } else if ( !event.which ) { + + // Support: Safari <=8 - 9 + // Safari sets which to 0 if you press any of the following keys + // during a drag (#14461) + if ( event.originalEvent.altKey || event.originalEvent.ctrlKey || + event.originalEvent.metaKey || event.originalEvent.shiftKey ) { + this.ignoreMissingWhich = true; + } else if ( !this.ignoreMissingWhich ) { + return this._mouseUp( event ); + } + } + } + + if ( event.which || event.button ) { + this._mouseMoved = true; + } + + if ( this._mouseStarted ) { + this._mouseDrag( event ); + return event.preventDefault(); + } + + if ( this._mouseDistanceMet( event ) && this._mouseDelayMet( event ) ) { + this._mouseStarted = + ( this._mouseStart( this._mouseDownEvent, event ) !== false ); + ( this._mouseStarted ? this._mouseDrag( event ) : this._mouseUp( event ) ); + } + + return !this._mouseStarted; + }, + + _mouseUp: function( event ) { + this.document + .off( "mousemove." + this.widgetName, this._mouseMoveDelegate ) + .off( "mouseup." + this.widgetName, this._mouseUpDelegate ); + + if ( this._mouseStarted ) { + this._mouseStarted = false; + + if ( event.target === this._mouseDownEvent.target ) { + $.data( event.target, this.widgetName + ".preventClickEvent", true ); + } + + this._mouseStop( event ); + } + + if ( this._mouseDelayTimer ) { + clearTimeout( this._mouseDelayTimer ); + delete this._mouseDelayTimer; + } + + this.ignoreMissingWhich = false; + mouseHandled = false; + event.preventDefault(); + }, + + _mouseDistanceMet: function( event ) { + return ( Math.max( + Math.abs( this._mouseDownEvent.pageX - event.pageX ), + Math.abs( this._mouseDownEvent.pageY - event.pageY ) + ) >= this.options.distance + ); + }, + + _mouseDelayMet: function( /* event */ ) { + return this.mouseDelayMet; + }, + + // These are placeholder methods, to be overriden by extending plugin + _mouseStart: function( /* event */ ) {}, + _mouseDrag: function( /* event */ ) {}, + _mouseStop: function( /* event */ ) {}, + _mouseCapture: function( /* event */ ) { return true; } +} ); + + + + +// $.ui.plugin is deprecated. Use $.widget() extensions instead. +var plugin = $.ui.plugin = { + add: function( module, option, set ) { + var i, + proto = $.ui[ module ].prototype; + for ( i in set ) { + proto.plugins[ i ] = proto.plugins[ i ] || []; + proto.plugins[ i ].push( [ option, set[ i ] ] ); + } + }, + call: function( instance, name, args, allowDisconnected ) { + var i, + set = instance.plugins[ name ]; + + if ( !set ) { + return; + } + + if ( !allowDisconnected && ( !instance.element[ 0 ].parentNode || + instance.element[ 0 ].parentNode.nodeType === 11 ) ) { + return; + } + + for ( i = 0; i < set.length; i++ ) { + if ( instance.options[ set[ i ][ 0 ] ] ) { + set[ i ][ 1 ].apply( instance.element, args ); + } + } + } +}; + + + +var safeActiveElement = $.ui.safeActiveElement = function( document ) { + var activeElement; + + // Support: IE 9 only + // IE9 throws an "Unspecified error" accessing document.activeElement from an


', - check: function () { + check: async function () { assert.strictEqual($('.note-video-popover').css('display'), 'block', testName + ' (popover)'); }, }, @@ -3752,14 +3590,14 @@ QUnit.test('Video', function (assert) { async: true, content: '

tata yoyo

', start: "p:contents()[0]->4", - do: function () { - _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx'); + do: async function () { + await _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx'); }, test: { content: '

tata

' + '
 
 
' + '

yoyo

', - check: function () { + check: async function () { assert.strictEqual($('.note-video-popover').css('display'), 'block', testName + ' (popover)'); }, }, @@ -3770,13 +3608,13 @@ QUnit.test('Video', function (assert) { async: true, content: '


', start: "p->0", - do: function () { - _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx'); + do: async function () { + await _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx'); }, test: { content: '


', - check: function () { - $('.note-image-popover .note-btn .note-icon-trash').mousedown().click(); + check: async function () { + await testUtils.dom.triggerEvents($('.note-image-popover .note-btn .note-icon-trash'), ['mousedown', 'click']); }, }, }, @@ -3787,14 +3625,14 @@ QUnit.test('Video', function (assert) { async: true, content: '


', start: "p->0", - do: function () { - _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx'); + do: async function () { + await _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx'); }, test: { content: '


 
 


', - check: function () { - $('.note-image-popover .note-float .note-icon-align-center').mousedown().click(); - $('.note-image-popover .note-float .note-icon-align-left').mousedown().click(); + check: async function () { + await testUtils.dom.triggerEvents($('.note-image-popover .note-float .note-icon-align-center'), ['mousedown', 'click']); + await testUtils.dom.triggerEvents($('.note-image-popover .note-float .note-icon-align-left'), ['mousedown', 'click']); }, }, }, @@ -3804,11 +3642,11 @@ QUnit.test('Video', function (assert) { async: true, content: '

', start: "img->0", - do: function () { - _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx'); + do: async function () { + await _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx'); }, test: { - check: function () { + check: async function () { assert.deepEqual(wysiwyg.getValue(), '


 
 


', testName); @@ -3821,11 +3659,11 @@ QUnit.test('Video', function (assert) { async: true, content: '


 
 


', start: "div->0", - do: function () { - _insertPictogram('fa-glass'); + do: async function () { + await _insertPictogram('fa-glass'); }, test: { - check: function () { + check: async function () { assert.deepEqual(wysiwyg.getValue(), '

\u200B\u200B

', testName); @@ -3837,11 +3675,11 @@ QUnit.test('Video', function (assert) { async: true, content: '

aaa

 
 

bbb

', start: "div->0", - do: function () { - _insertPictogram('fa-glass'); + do: async function () { + await _insertPictogram('fa-glass'); }, test: { - check: function () { + check: async function () { assert.deepEqual(wysiwyg.getValue(), '

aaabbb

', testName); @@ -3850,20 +3688,19 @@ QUnit.test('Video', function (assert) { }, ]; - var def = $.when(); + var def = Promise.resolve(); _.each(mediaTests, function (test) { - def = def.then(function () { + def = def.then(async function () { testName = test.name; wysiwyg.setValue(test.content); var range = weTestUtils.select(test.start, test.end, $editable); - $(range.sc).mousedown().click(); + await testUtils.dom.triggerEvents($(range.sc), ['mousedown', 'click']); Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo); return _clickMedia(wysiwyg, assert, test.do, test.test); }); }); - def.then(function () { + return def.then(function () { wysiwyg.destroy(); - done(); }); }); }); diff --git a/addons/web_editor/views/editor.xml b/addons/web_editor/views/editor.xml index 6bbf98f9b30..7f68d0be5d8 100644 --- a/addons/web_editor/views/editor.xml +++ b/addons/web_editor/views/editor.xml @@ -138,7 +138,7 @@