From 91ea5a2ca449549ba50f12c4a778421510ef84fc Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Fri, 23 Aug 2019 10:35:36 +0000 Subject: [PATCH] [REF] web,*: native event dispatching in test utils *: mail,partner_autocomplete: adapted tests Added a new function to trigger the adequate type of event on an element (native JS event to DOM Nodes and jQuery event to jQuery objects), and refactored the generic event-triggering functions to call this new function. --- addons/mail/static/tests/activity_tests.js | 2 +- .../discuss_typing_notification_tests.js | 4 +- .../tests/partner_autocomplete_tests.js | 4 +- .../static/tests/fields/basic_fields_tests.js | 16 +-- .../relational_fields/field_one2many_tests.js | 2 +- addons/web/static/tests/helpers/test_utils.js | 1 + .../static/tests/helpers/test_utils_dom.js | 118 +++++++++++++----- .../static/tests/helpers/test_utils_fields.js | 7 +- addons/web/static/tests/views/kanban_tests.js | 4 +- addons/web/static/tests/views/list_tests.js | 2 +- 10 files changed, 111 insertions(+), 49 deletions(-) diff --git a/addons/mail/static/tests/activity_tests.js b/addons/mail/static/tests/activity_tests.js index 6ab924973ec..6725aab2e33 100644 --- a/addons/mail/static/tests/activity_tests.js +++ b/addons/mail/static/tests/activity_tests.js @@ -179,7 +179,7 @@ QUnit.test('activity view: simple activity rendering', async function (assert) { assert.containsN(activity, td, 2, 'should contain an empty cell as no activity scheduled yet.'); // schedule an activity (this triggers a do_action) - testUtils.fields.editAndTrigger(activity.$(td + ':first'), null, ['mouseenter', 'click']); + await testUtils.fields.editAndTrigger(activity.$(td + ':first'), null, ['mouseenter', 'click']); assert.containsOnce(activity, 'table tfoot tr .o_record_selector', 'should contain search more selector to choose the record to schedule an activity for it'); diff --git a/addons/mail/static/tests/discuss_typing_notification_tests.js b/addons/mail/static/tests/discuss_typing_notification_tests.js index d77d32b9f1f..4d434b80afd 100644 --- a/addons/mail/static/tests/discuss_typing_notification_tests.js +++ b/addons/mail/static/tests/discuss_typing_notification_tests.js @@ -205,13 +205,13 @@ QUnit.test('notify is typing', async function (assert) { // STEP 1: current user is typing something step = 1; $input.focus(); - testUtils.fields.editInput($input, '1'); + await testUtils.fields.editInput($input, '1'); assert.verifySteps(['notify_typing']); // STEP 2: current user clears input step = 2; - testUtils.fields.editInput($input, ''); + await testUtils.fields.editInput($input, ''); assert.verifySteps(['notify_typing']); diff --git a/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js b/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js index c605c9bc33c..60fa4679f03 100644 --- a/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js +++ b/addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js @@ -360,11 +360,11 @@ odoo.define('partner_autocomplete.tests', function (require) { '' + '' + '', - }).then(function (form) { + }).then(async function (form) { var $input = form.$('.o_field_many2one[name="parent_id"] input:visible'); assert.strictEqual($input.length, 1, "there should be an for the Many2one"); - testUtils.fields.editInput($input, 'odoo'); + await testUtils.fields.editInput($input, 'odoo'); concurrency.delay(0).then(function () { var $dropdown = $input.autocomplete('widget'); diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index a67d4e19cbc..112a1c74b7b 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -2967,17 +2967,17 @@ QUnit.module('basic_fields', { // switch to edit mode await testUtils.form.clickEdit(form); // open datepicker and select another value - testUtils.dom.openDatepicker(form.$('.o_datepicker')); - testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch').first()); - testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch:eq(1)')); - testUtils.dom.click($('.bootstrap-datetimepicker-widget .year').eq(11)); - testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(11)); - testUtils.dom.click($('.day:contains(31)')); + await testUtils.dom.openDatepicker(form.$('.o_datepicker')); + await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch').first()); + await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch:eq(1)')); + await testUtils.dom.click($('.bootstrap-datetimepicker-widget .year').eq(11)); + await testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(11)); + await testUtils.dom.click($('.day:contains(31)')); var $warn = form.$('.o_datepicker_warning:visible'); assert.strictEqual($warn.length, 1, "should have a warning in the form view"); - testUtils.fields.editSelect(form.$('.o_field_widget[name=date] input'), ''); // remove the value + await testUtils.fields.editSelect(form.$('.o_field_widget[name=date] input'), ''); // remove the value $warn = form.$('.o_datepicker_warning:visible'); assert.strictEqual($warn.length, 0, "the warning in the form view should be hidden"); @@ -3100,7 +3100,7 @@ QUnit.module('basic_fields', { assert.strictEqual(form.$('.o_datepicker_input').val(), '02/03/2017', 'the date should be correct in edit mode'); - testUtils.fields.editAndTrigger(form.$('.o_datepicker_input'), '', ['input', 'change', 'focusout']); + await testUtils.fields.editAndTrigger(form.$('.o_datepicker_input'), '', ['input', 'change', 'focusout']); assert.strictEqual(form.$('.o_datepicker_input').val(), '', 'should have correctly removed the value'); diff --git a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js index eb4cd9986cc..d9bb4a9624b 100644 --- a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js @@ -6611,7 +6611,7 @@ QUnit.module('fields', {}, function () { await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); prom = testUtils.makeTestPromise(); - testUtils.fields.editAndTrigger(form.$('.o_field_widget[name=int_field]'), '44', + await testUtils.fields.editAndTrigger(form.$('.o_field_widget[name=int_field]'), '44', ['input', { type: 'keydown', which: $.ui.keyCode.TAB }]); prom.resolve(); await testUtils.nextTick(); diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index 90a581e7b33..24ebb0db105 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -135,6 +135,7 @@ return Promise.all([ clickFirst: testUtilsDom.clickFirst, clickLast: testUtilsDom.clickLast, triggerEvents: testUtilsDom.triggerEvents, + triggerEvent: testUtilsDom.triggerEvent, }, form: { clickEdit: testUtilsForm.clickEdit, diff --git a/addons/web/static/tests/helpers/test_utils_dom.js b/addons/web/static/tests/helpers/test_utils_dom.js index 4c1ef8fbbcc..87013bd4696 100644 --- a/addons/web/static/tests/helpers/test_utils_dom.js +++ b/addons/web/static/tests/helpers/test_utils_dom.js @@ -23,8 +23,8 @@ var concurrency = require('web.concurrency'); * merged. This is not the default as of now, because handlers are triggered * synchronously, which is not the same as the 'reality'. * - * @param {jqueryElement} $el - * @param {jqueryElement} $to + * @param {jqueryElement|HTMLElement} $el + * @param {jqueryElement|HTMLElement} $to * @param {Object} [options] * @param {string|Object} [options.position='center'] target position: * can either be one of {'top', 'bottom', 'left', 'right'} or @@ -36,8 +36,21 @@ var concurrency = require('web.concurrency'); * @param {boolean} [options.withTrailingClick=false] if true, this utility * function will also trigger a click on the target after the mouseup event * (this is actually what happens when a drag and drop operation is done) + * @param {jQuery|HTMLElement} [options.mouseenterTarget=undefined] target of the mouseenter event + * @param {jQuery|HTMLElement} [options.mousedownTarget=undefined] target of the mousedown event + * @param {jQuery|HTMLElement} [options.mousemoveTarget=undefined] target of the mousemove event + * @param {jQuery|HTMLElement} [options.mouseupTarget=undefined] target of the mouseup event + * @returns {Promise} */ function dragAndDrop($el, $to, options) { + let el = null; + if ($el instanceof EventTarget) { + el = $el; + $el = $(el); + } + if ($to instanceof EventTarget) { + $to = $($to); + } options = options || {}; var position = options.position || 'center'; var elementCenter = $el.offset(); @@ -67,40 +80,40 @@ function dragAndDrop($el, $to, options) { toOffset.left += bound.left; toOffset.top += bound.top; } - $el.trigger($.Event("mouseenter")); + triggerEvent(options.mouseenterTarget || el || $el, 'mouseenter'); if (!(options.continueMove)) { elementCenter.left += $el.outerWidth() / 2; elementCenter.top += $el.outerHeight() / 2; - $el.trigger($.Event("mousedown", { + triggerEvent(options.mousedownTarget || el || $el, 'mousedown', { which: 1, pageX: elementCenter.left, pageY: elementCenter.top - })); + }); } - $el.trigger($.Event("mousemove", { + triggerEvent(options.mousemoveTarget || el || $el, 'mousemove', { which: 1, pageX: toOffset.left, pageY: toOffset.top - })); + }); if (!options.disableDrop) { - $el.trigger($.Event("mouseup", { + triggerEvent(options.mouseupTarget || el || $el, 'mouseup', { which: 1, pageX: toOffset.left, pageY: toOffset.top - })); + }); if (options.withTrailingClick) { - $el.click(); + triggerEvent(options.mouseupTarget || el || $el, 'click'); } } else { // It's impossible to drag another element when one is already // being dragged. So it's necessary to finish the drop when the test is // over otherwise it's impossible for the next tests to drag and // drop elements. - $el.on("remove", function () { - $el.trigger($.Event("mouseup")); + $el.on('remove', function () { + triggerEvent($el, 'mouseup'); }); } return concurrency.delay(0); @@ -111,17 +124,24 @@ function dragAndDrop($el, $to, options) { * sometimes necessary because the basic way to trigger an event (such as * $el.trigger('mousemove')); ) is too crude for some uses. * - * @param {jqueryElement} $el + * @param {jQuery|HTMLElement} $el * @param {string} type a mouse event type, such as 'mousedown' or 'mousemove' + * @returns {Promise} */ function triggerMouseEvent($el, type) { - var pos = $el.offset(); - var e = new $.Event(type); + const el = $el instanceof jQuery ? $el[0] : $el; + if (!el) { + throw new Error(`no target found to trigger MouseEvent`); + } + const rect = el.getBoundingClientRect(); // little fix since it seems on chrome, it triggers 1px too on the left - e.pageX = e.layerX = e.screenX = pos.left + 1; - e.pageY = e.layerY = e.screenY = pos.top; - e.which = 1; - $el.trigger(e); + const left = rect.x + 1; + const top = rect.y; + return triggerEvent($el, type, { + which: 1, + pageX: left, layerX: left, screenX: left, + pageY: top, layerY: top, screenY: top, + }); } /** @@ -132,6 +152,7 @@ function triggerMouseEvent($el, type) { * @param {integer} x * @param {integer} y * @param {string} type a mouse event type, such as 'mousedown' or 'mousemove' + * @returns {HTMLElement} */ function triggerPositionalMouseEvent(x, y, type) { var ev = document.createEvent("MouseEvent"); @@ -153,6 +174,7 @@ function triggerPositionalMouseEvent(x, y, type) { * simulate a keypress event for a given character * * @param {string} char the character, or 'ENTER' + * @returns {Promise} */ function triggerKeypressEvent(char) { var keycode; @@ -163,7 +185,10 @@ function triggerKeypressEvent(char) { } else { keycode = char.charCodeAt(0); } - return $('body').trigger($.Event('keypress', { which: keycode, keyCode: keycode })); + return triggerEvent(document.body, 'keypress', { + keyCode: keycode, + which: keycode, + }); } /** @@ -178,6 +203,7 @@ function triggerKeypressEvent(char) { * element event if it is invisible * @param {boolean} [options.shouldTriggerClick=false] if true, trigger the * click event without calling the function click of jquery + * @returns {Promise} */ function click(el, options) { options = options || {}; @@ -209,7 +235,7 @@ function click(el, options) { throw new Error(`Found ${validMatches.length} elements to click on, instead of 1 ${selectorMsg}`); } if (options.shouldTriggerClick) { - validMatches.trigger('click'); + return triggerEvent(validMatches, 'click'); } else { validMatches.click(); } @@ -227,6 +253,7 @@ function click(el, options) { * element event if it is invisible * @param {boolean} [options.shouldTriggerClick=false] if true, trigger the * click event without calling the function click of jquery + * @returns {Promise} */ function clickFirst(el, options) { return click(el, _.extend({}, options, {first: true})); @@ -242,28 +269,58 @@ function clickFirst(el, options) { * element event if it is invisible * @param {boolean} [options.shouldTriggerClick=false] if true, trigger the * click event without calling the function click of jquery + * @returns {Promise} */ function clickLast(el, options) { return click(el, _.extend({}, options, {last: true})); } - /** - * trigger events on the specified target - * @param {jQuery} $el should target a single dom node + * Trigger events on the specified target + * @param {jQuery|HTMLElement} $el should target a single dom node * @param {string[]} events the events you want to trigger - * @returns Promise + * @returns {Promise} */ function triggerEvents($el, events) { - if ($el.length !== 1) { - throw new Error(`target has length ${$el.length} instead of 1`); + if ($el instanceof jQuery) { + if ($el.length !== 1) { + throw new Error(`target has length ${$el.length} instead of 1`); + } + } else if (!($el instanceof EventTarget)) { + throw new Error(`target is neither a jQuery element nor an HTML element`); } if (typeof events === 'string') { events = [events]; } - events.forEach(function (event) { + return events.reduce((previous, event) => { + return previous.then(() => triggerEvent($el, event)); + }, Promise.resolve()); +} + +/** + * Trigger an event on the specified target. + * This function will dispatch a native event to an HTMLElement or a + * jQuery event to a jQuery object. + * @param {jQuery|HTMLElement} $el event target. + * @param {string} type event type + * @param {Object} [options] event attributes + * @returns {Promise} + */ +function triggerEvent($el, type, options={}) { + if ($el instanceof EventTarget) { + const event = new Event(type); + Object.assign(event, options); + Object.defineProperty(event, 'target', { + writable: false, + value: $el, + }); + $el.dispatchEvent(event); + } else { + const event = Object.keys(options).length ? + $.Event(type, options) : + type; $el.trigger(event); - }); + } return concurrency.delay(0); } @@ -274,7 +331,7 @@ function triggerEvents($el, events) { * @param {jQuery} $datepickerEl element to which a datepicker is attached */ function openDatepicker($datepickerEl) { - click($datepickerEl.find('.o_datepicker_input')); + return click($datepickerEl.find('.o_datepicker_input')); } @@ -288,6 +345,7 @@ return { clickFirst: clickFirst, clickLast: clickLast, triggerEvents: triggerEvents, + triggerEvent: triggerEvent, }; }); diff --git a/addons/web/static/tests/helpers/test_utils_fields.js b/addons/web/static/tests/helpers/test_utils_fields.js index 359f5f56b08..dcc9329bcbe 100644 --- a/addons/web/static/tests/helpers/test_utils_fields.js +++ b/addons/web/static/tests/helpers/test_utils_fields.js @@ -27,6 +27,7 @@ var domUtils = require('web.test_utils_dom'); * @param {jQuery} $el should target an input, textarea or select * @param {string|number} value * @param {string[]} events + * @returns {Promise} */ function editAndTrigger($el, value, events) { if ($el.length !== 1) { @@ -46,6 +47,7 @@ function editAndTrigger($el, value, events) { * * @param {jQuery} $el * @param {string|number} value + * @returns {Promise} */ function editInput($el, value) { return editAndTrigger($el, value, ['input']); @@ -61,10 +63,10 @@ function editInput($el, value) { * * @param {jQuery} $el * @param {string|number} value + * @returns {Promise} */ function editSelect($el, value) { - editAndTrigger($el, value, ['change']); - return concurrency.delay(0); + return editAndTrigger($el, value, ['change']); } /** @@ -112,6 +114,7 @@ function clickM2OHighlightedItem(fieldName, selector) { * * @param {string} fieldName * @param {string} searchText + * @returns {Promise} */ function clickM2OItem(fieldName, searchText) { var m2oSelector = `.o_field_many2one[name=${fieldName}] input`; diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index d3ff979de8b..9e6b8ddd11d 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -1036,7 +1036,7 @@ QUnit.module('Views', { keyCode: $.ui.keyCode.ENTER, which: $.ui.keyCode.ENTER, }; - testUtils.fields.editAndTrigger( + await testUtils.fields.editAndTrigger( kanban.$('.o_kanban_quick_create').find('input[name=foo]'), 'new partner', ['input', $.Event('keydown', enterEvent), $.Event('keydown', enterEvent)] @@ -1189,7 +1189,7 @@ QUnit.module('Views', { which: $.ui.keyCode.ENTER, }; - testUtils.fields.editAndTrigger( + await testUtils.fields.editAndTrigger( kanban.$('.o_kanban_quick_create').find('input[name=foo]'), 'new partner', ['input', $.Event('keydown', enterEvent), $.Event('keydown', enterEvent)] diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 05791ad9d74..b35b7c64362 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -969,7 +969,7 @@ QUnit.module('Views', { n += 1; }); await testUtils.dom.click($td); - testUtils.fields.editInput($td.find('input'), 'abc'); + await testUtils.fields.editInput($td.find('input'), 'abc'); assert.strictEqual(n, 1, "field_changed should have been triggered"); await testUtils.dom.click(list.$('td:not(.o_list_record_selector)').eq(2)); assert.strictEqual(n, 1, "field_changed should not have been triggered");