diff options
| author | Vincent Schippefilt <vsc@odoo.com> | 2019-03-06 14:02:32 +0100 |
|---|---|---|
| committer | Aaron Bohy <aab@odoo.com> | 2019-03-06 20:07:17 +0100 |
| commit | ab56e637b7f97ddcdbcc9baea99ae097e342409c (patch) | |
| tree | 4cb47da9f7ef89b19d18429220a7507470ec12a9 /addons/web/static/tests/helpers | |
| parent | eeed894fd50e477135e1ac9a82f6e98ce063ab90 (diff) | |
[REF] web: adapt code after jQuery update
Part of task 1896658
Co-authored-by: Aaron Bohy <aab@odoo.com>
Co-authored-by: Christophe Matthieu <chm@odoo.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
Co-authored-by: David Monjoie <dmo@odoo.com>
Co-authored-by: Martin Geubelle <mge@odoo.com>
Co-authored-by: svs-odoo <svs@odoo.com>
Co-authored-by: Vincent Schippefilt <vsc@odoo.com>
Diffstat (limited to 'addons/web/static/tests/helpers')
| -rw-r--r-- | addons/web/static/tests/helpers/mock_server.js | 71 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/qunit_asserts.js | 17 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/qunit_config.js | 58 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils.js | 85 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils_create.js | 94 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils_dom.js | 78 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils_fields.js | 102 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils_file.js | 35 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils_form.js | 10 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils_graph.js | 2 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils_kanban.js | 15 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils_mock.js | 31 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils_modal.js | 2 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils_pivot.js | 6 |
14 files changed, 440 insertions, 166 deletions
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index ff75722b333..44cc276e1c8 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -103,9 +103,9 @@ var MockServer = Class.extend({ * * @param {string} route * @param {Object} args - * @returns {Deferred<any>} + * @returns {Promise<any>} * Resolved with the result of the RPC, stringified then parsed. - * If the RPC should fail, the deferred will be rejected with the + * If the RPC should fail, the promise will be rejected with the * error object, stringified then parsed. */ performRpc: function (route, args) { @@ -132,17 +132,18 @@ var MockServer = Class.extend({ console.log('%c[rpc] response' + route, 'color: blue; font-weight: bold;', JSON.parse(resultString)); } return JSON.parse(resultString); - }, function (result, event) { - var errorString = JSON.stringify(result || false); + }, function (result) { + var message = result && result.message; + var event = result && result.event; + var errorString = JSON.stringify(message || false); if (debug) { console.log('%c[rpc] response (error) ' + route, 'color: orange; font-weight: bold;', JSON.parse(errorString)); } - return $.Deferred().reject(errorString, event || $.Event()); + return Promise.reject({message: errorString, event: event || $.Event()}); }); - var promise = def.promise(); - promise.abort = abort; - return promise; + def.abort = abort; + return def; }, //-------------------------------------------------------------------------- @@ -861,7 +862,7 @@ var MockServer = Class.extend({ } } if (type === 'many2one') { - var ids = _.pluck(records, aggregatedFields[i]); + var ids = _.pluck(records, aggregatedFields[i]); group[aggregatedFields[i]] = _.uniq(ids).length || null; } } @@ -1064,7 +1065,7 @@ var MockServer = Class.extend({ var result = this._mockSearchReadController({ model: model, domain: kwargs.domain || args[0], - fields: kwargs.fields || args[1], + fields: kwargs.fields || args[1], offset: kwargs.offset || args[2], limit: kwargs.limit || args[3], order: kwargs.order || args[4], @@ -1181,83 +1182,83 @@ var MockServer = Class.extend({ * @private * @param {string} route * @param {Object} args - * @returns {Deferred<any>} + * @returns {Promise<any>} * Resolved with the result of the RPC. If the RPC should fail, the - * deferred should either be rejected or the call should throw an + * promise should either be rejected or the call should throw an * exception (@see performRpc for error handling). */ _performRpc: function (route, args) { switch (route) { case '/web/action/load': - return $.when(this._mockLoadAction(args.kwargs)); + return Promise.resolve(this._mockLoadAction(args.kwargs)); case '/web/dataset/search_read': - return $.when(this._mockSearchReadController(args)); + return Promise.resolve(this._mockSearchReadController(args)); case '/web/dataset/resequence': - return $.when(this._mockResequence(args)); + return Promise.resolve(this._mockResequence(args)); } if (route.indexOf('/web/image') >= 0 || _.contains(['.png', '.jpg'], route.substr(route.length - 4))) { - return $.when(); + return Promise.resolve(); } switch (args.method) { case 'copy': - return $.when(this._mockCopy(args.model, args.args[0])); + return Promise.resolve(this._mockCopy(args.model, args.args[0])); case 'create': - return $.when(this._mockCreate(args.model, args.args[0])); + return Promise.resolve(this._mockCreate(args.model, args.args[0])); case 'default_get': - return $.when(this._mockDefaultGet(args.model, args.args, args.kwargs)); + return Promise.resolve(this._mockDefaultGet(args.model, args.args, args.kwargs)); case 'fields_get': - return $.when(this._mockFieldsGet(args.model, args.args)); + return Promise.resolve(this._mockFieldsGet(args.model, args.args)); case 'search_panel_select_range': - return $.when(this._mockSearchPanelSelectRange(args.model, args.args, args.kwargs)); + return Promise.resolve(this._mockSearchPanelSelectRange(args.model, args.args, args.kwargs)); case 'search_panel_select_multi_range': - return $.when(this._mockSearchPanelSelectMultiRange(args.model, args.args, args.kwargs)); + return Promise.resolve(this._mockSearchPanelSelectMultiRange(args.model, args.args, args.kwargs)); case 'load_views': - return $.when(this._mockLoadViews(args.model, args.kwargs)); + return Promise.resolve(this._mockLoadViews(args.model, args.kwargs)); case 'name_get': - return $.when(this._mockNameGet(args.model, args.args)); + return Promise.resolve(this._mockNameGet(args.model, args.args)); case 'name_create': - return $.when(this._mockNameCreate(args.model, args.args)); + return Promise.resolve(this._mockNameCreate(args.model, args.args)); case 'name_search': - return $.when(this._mockNameSearch(args.model, args.args, args.kwargs)); + return Promise.resolve(this._mockNameSearch(args.model, args.args, args.kwargs)); case 'onchange': - return $.when(this._mockOnchange(args.model, args.args)); + return Promise.resolve(this._mockOnchange(args.model, args.args)); case 'read': - return $.when(this._mockRead(args.model, args.args, args.kwargs)); + return Promise.resolve(this._mockRead(args.model, args.args, args.kwargs)); case 'read_group': - return $.when(this._mockReadGroup(args.model, args.kwargs)); + return Promise.resolve(this._mockReadGroup(args.model, args.kwargs)); case 'read_progress_bar': - return $.when(this._mockReadProgressBar(args.model, args.kwargs)); + return Promise.resolve(this._mockReadProgressBar(args.model, args.kwargs)); case 'search_count': - return $.when(this._mockSearchCount(args.model, args.args)); + return Promise.resolve(this._mockSearchCount(args.model, args.args)); case 'search_read': - return $.when(this._mockSearchRead(args.model, args.args, args.kwargs)); + return Promise.resolve(this._mockSearchRead(args.model, args.args, args.kwargs)); case 'unlink': - return $.when(this._mockUnlink(args.model, args.args)); + return Promise.resolve(this._mockUnlink(args.model, args.args)); case 'write': - return $.when(this._mockWrite(args.model, args.args)); + return Promise.resolve(this._mockWrite(args.model, args.args)); } var model = this.data[args.model]; if (model && typeof model[args.method] === 'function') { - return $.when(this.data[args.model][args.method](args.args, args.kwargs)); + return Promise.resolve(this.data[args.model][args.method](args.args, args.kwargs)); } throw new Error("Unimplemented route: " + route); diff --git a/addons/web/static/tests/helpers/qunit_asserts.js b/addons/web/static/tests/helpers/qunit_asserts.js index bce6ef2a777..e8f9948d347 100644 --- a/addons/web/static/tests/helpers/qunit_asserts.js +++ b/addons/web/static/tests/helpers/qunit_asserts.js @@ -37,7 +37,7 @@ function _checkClass(w, className, shouldHaveClass, msg) { var descr = `${shouldHaveClass ? 'hasClass' : 'doesNotHaveClass'} ${className}`; QUnit.assert.ok(false, `Assertion '${descr}' targets ${$el.length} elements instead of 1`); } else { - msg = msg || `target ${$el.selector || ''} should ${shouldHaveClass ? '' : 'not'} have class ${className}`; + msg = msg || `target should ${shouldHaveClass ? '' : 'not'} have class ${className}`; var hasClass = $el.hasClass(className); var condition = shouldHaveClass ? hasClass : !hasClass; QUnit.assert.ok(condition, msg); @@ -63,8 +63,15 @@ function _checkVisible(w, shouldBeVisible, msg) { var descr = `${shouldBeVisible ? 'isVisible' : 'isNotVisible'}`; QUnit.assert.ok(false, `Assertion '${descr}' targets ${$el.length} elements instead of 1`); } else { - msg = msg || `target ${$el.selector} should ${shouldBeVisible ? '' : 'not'} be visible`; + msg = msg || `target should ${shouldBeVisible ? '' : 'not'} be visible`; var isVisible = $el.is(':visible'); + if (isVisible) { + // Additional test to see if $el is really visible, since jQuery + // considers an element visible if it has a DOWRect, even if its + // width and height are equal to zero. + var boundingClientRect = $el[0].getBoundingClientRect(); + isVisible = boundingClientRect.width + boundingClientRect.height; + } var condition = shouldBeVisible ? isVisible : !isVisible; QUnit.assert.ok(condition, msg); } @@ -96,9 +103,7 @@ function containsN(w, selector, n, msg) { var $matches = $el.find(selector); if (!msg) { msg = `Selector '${selector}' should have exactly ${n} matches`; - if ($el.selector) { - msg += ` (inside '${$el.selector}')`; - } + msg += ` (inside the target)`; } QUnit.assert.strictEqual($matches.length, n, msg); } @@ -181,7 +186,7 @@ function hasAttrValue(w, attr, value, msg) { var descr = `hasAttrValue (${attr}: ${value})`; QUnit.assert.ok(false, `Assertion '${descr}' targets ${$el.length} elements instead of 1`); } else { - msg = msg || `attribute '${attr}' of target ${$el.selector || ''} should be '${value}'`; + msg = msg || `attribute '${attr}' of target should be '${value}'`; QUnit.assert.strictEqual($el.attr(attr), value, msg); } } diff --git a/addons/web/static/tests/helpers/qunit_config.js b/addons/web/static/tests/helpers/qunit_config.js index 511b9cd2fce..8c6496bb8f9 100644 --- a/addons/web/static/tests/helpers/qunit_config.js +++ b/addons/web/static/tests/helpers/qunit_config.js @@ -92,6 +92,64 @@ QUnit.moduleDone(function(result) { }); /** + * After each test, we check that there is no leftover in the DOM. + * + * Note: this event is not QUnit standard, we added it for this specific use case. + */ +QUnit.on('OdooAfterTestHook', function () { + // check for leftover elements in the body + var $bodyChilds = $('body > *'); + var validElements = [ + // always in the body: + {tagName: 'DIV', attrToCompare: 'id', value: 'qunit'}, + {tagName: 'DIV', attrToCompare: 'id', value: 'qunit-fixture'}, + {tagName: 'SCRIPT', attrToCompare: 'id', value: ''}, + // shouldn't be in the body after a test but are tolerated: + {tagName: 'DIV', attrToCompare: 'className', value: 'o_notification_manager'}, + {tagName: 'DIV', attrToCompare: 'className', value: 'tooltip fade bs-tooltip-auto'}, + {tagName: 'DIV', attrToCompare: 'className', value: 'tooltip fade bs-tooltip-auto show'}, + {tagName: 'I', attrToCompare: 'title', value: 'Raphaël Colour Picker'}, + {tagName: 'SPAN', attrToCompare: 'className', value: 'select2-hidden-accessible'}, + // Due to a Document Kanban bug (already present in 12.0) + {tagName: 'DIV', attrToCompare: 'className', value: 'ui-helper-hidden-accessible'}, + {tagName: 'UL', attrToCompare: 'className', value: 'ui-menu ui-widget ui-widget-content ui-autocomplete ui-front'}, + ]; + if ($bodyChilds.length > 3) { + for (var i = 0; i < $bodyChilds.length; i++) { + var bodyChild = $bodyChilds[i]; + var isValid = false; + + for (var j = 0; j < validElements.length; j++) { + var toleratedElement = validElements[j]; + if (toleratedElement.tagName === bodyChild.tagName) { + var attr = toleratedElement.attrToCompare; + if (toleratedElement.value === bodyChild[attr]) { + isValid = true; + break; + } + } + } + + if (!isValid) { + if (!document.body.classList.contains('debug')) { + $(bodyChild).remove(); + } + QUnit.pushFailure(`Body still contains undesirable elements`); + } + } + } + + // check for leftovers in #qunit-fixture + var qunitFixture = document.getElementById('qunit-fixture'); + if (qunitFixture.children.length) { + QUnit.pushFailure(`#qunit-fixture still contains elements`); + if (!document.body.classList.contains('debug')) { + $(qunitFixture.children).remove(); + } + } +}); + +/** * Add a sort button on top of the QUnit result page, so we can see which tests * take the most time. */ diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index 2897b02214c..ccf5d8daf6b 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -11,7 +11,9 @@ odoo.define('web.test_utils', function (require) { */ var ajax = require('web.ajax'); +var concurrency = require('web.concurrency'); var core = require('web.core'); +var relationalFields = require('web.relational_fields'); var session = require('web.session'); var testUtilsCreate = require('web.test_utils_create'); var testUtilsDom = require('web.test_utils_dom'); @@ -25,22 +27,89 @@ var testUtilsModal = require('web.test_utils_modal'); var testUtilsPivot = require('web.test_utils_pivot'); var tools = require('web.tools'); + function deprecated(fn, type) { var msg = `Helper 'testUtils.${fn.name}' is deprecated. ` + `Please use 'testUtils.${type}.${fn.name}' instead.`; return tools.deprecated(fn, msg); } +/** + * Helper function, make a promise with a public resolve function. Note that + * this is not standard and should not be used outside of tests... + * + * @returns {Promise + resolve and reject function} + */ +function makeTestPromise() { + var resolve; + var reject; + var promise = new Promise(function (_resolve, _reject) { + resolve = _resolve; + reject = _reject; + }); + promise.resolve = function () { + resolve.apply(null, arguments); + return promise; + }; + promise.reject = function () { + reject.apply(null, arguments); + return promise; + }; + return promise; +} + +/** + * Make a promise with public resolve and reject functions (see + * @makeTestPromise). Perform an assert.step when the promise is + * resolved/rejected. + * + * @param {Object} assert instance object with the assertion methods + * @param {function} assert.step + * @param {string} str message to pass to assert.step + * @returns {Promise + resolve and reject function} + */ +function makeTestPromiseWithAssert(assert, str) { + var prom = makeTestPromise(); + prom.then(() => assert.step('ok ' + str)).catch(function () {}); + prom.catch(() => assert.step('ko ' + str)); + return prom; +} + +/** + * Create a new promise that can be waited by the caller in order to execute + * code after the next microtask tick and before the next jobqueue tick. + * + * @return {Promise} an already fulfilled promise + */ +async function nextMicrotaskTick() { + return Promise.resolve(); +} + +/** + * Returns a promise that is resolved in the next jobqueue tick so that the + * caller can wait on it in order to execute code in the next jobqueue tick. + * + * @return {Promise} a promise that will be fulfilled in the next jobqueue tick + */ +async function nextTick() { + return concurrency.delay(0); +} + // Loading static files cannot be properly simulated when their real content is // really needed. This is the case for static XML files so we load them here, // before starting the qunit test suite. // (session.js is in charge of loading the static xml bundle and we also have // to load xml files that are normally lazy loaded by specific widgets). -return $.when( +return Promise.all([ session.is_bound, ajax.loadXML('/web/static/src/xml/dialog.xml', core.qweb) -).then(function () { +]).then(function () { setTimeout(function () { + // jquery autocomplete refines the search in a setTimeout() parameterized + // with a delay, so we force this delay to 0 s.t. the dropdown is filtered + // directly on the next tick + relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; + // this is done with the hope that tests are // only started all together... QUnit.start(); @@ -53,6 +122,7 @@ return $.when( patchDate: testUtilsMock.patchDate, unpatch: testUtilsMock.unpatch, fieldsViewGet: testUtilsMock.fieldsViewGet, + patchSetTimeout: testUtilsMock.patchSetTimeout, }, dom: { triggerKeypressEvent: testUtilsDom.triggerKeypressEvent, @@ -63,6 +133,7 @@ return $.when( click: testUtilsDom.click, clickFirst: testUtilsDom.clickFirst, clickLast: testUtilsDom.clickLast, + triggerEvents: testUtilsDom.triggerEvents, }, form: { clickEdit: testUtilsForm.clickEdit, @@ -99,6 +170,9 @@ return $.when( editInput: testUtilsFields.editInput, editSelect: testUtilsFields.editSelect, editAndTrigger: testUtilsFields.editAndTrigger, + triggerKey: testUtilsFields.triggerKey, + triggerKeydown: testUtilsFields.triggerKeydown, + triggerKeyup: testUtilsFields.triggerKeyup, }, file: { createFile: testUtilsFile.createFile, @@ -108,11 +182,16 @@ return $.when( createActionManager: testUtilsCreate.createActionManager, createDebugManager: testUtilsCreate.createDebugManager, - createAsyncView: testUtilsCreate.createAsyncView, + createAsyncView: testUtilsCreate.createView, + createCalendarView: testUtilsCreate.createCalendarView, createControlPanel: testUtilsCreate.createControlPanel, createView: testUtilsCreate.createView, createModel: testUtilsCreate.createModel, createParent: testUtilsCreate.createParent, + makeTestPromise: makeTestPromise, + makeTestPromiseWithAssert: makeTestPromiseWithAssert, + nextMicrotaskTick: nextMicrotaskTick, + nextTick: nextTick, // backward-compatibility addMockEnvironment: deprecated(testUtilsMock.addMockEnvironment, 'mock'), diff --git a/addons/web/static/tests/helpers/test_utils_create.js b/addons/web/static/tests/helpers/test_utils_create.js index 5a64512260e..7e4de1060bc 100644 --- a/addons/web/static/tests/helpers/test_utils_create.js +++ b/addons/web/static/tests/helpers/test_utils_create.js @@ -13,6 +13,7 @@ odoo.define('web.test_utils_create', function (require) { var ActionManager = require('web.ActionManager'); var config = require('web.config'); var ControlPanelView = require('web.ControlPanelView'); +var concurrency = require('web.concurrency'); var DebugManager = require('web.DebugManager'); var dom = require('web.dom'); var testUtilsMock = require('web.test_utils_mock'); @@ -30,7 +31,7 @@ var Widget = require('web.Widget'); * @param {function} [params.mockRPC] * @returns {ActionManager} */ -function createActionManager (params) { +var createActionManager = async function (params) { params = params || {}; var $target = $('#qunit-fixture'); if (params.debug) { @@ -47,7 +48,7 @@ function createActionManager (params) { _.extend(params, { mockRPC: function (route, args) { if (args.model === 'ir.attachment') { - return $.when([]); + return Promise.resolve([]); } if (mockRPC) { return mockRPC.apply(this, arguments); @@ -56,7 +57,7 @@ function createActionManager (params) { }, }); testUtilsMock.addMockEnvironment(widget, _.defaults(params, { debounce: false })); - widget.prependTo($target); + await widget.prependTo($target); widget.$el.addClass('o_web_client'); if (config.device.isMobile) { widget.$el.addClass('o_touch_device'); @@ -71,14 +72,14 @@ function createActionManager (params) { widget.destroy(); }; var fragment = document.createDocumentFragment(); - actionManager.appendTo(fragment).then(function () { + return actionManager.appendTo(fragment).then(function () { dom.append(widget.$el, fragment, { callbacks: [{ widget: actionManager }], in_DOM: true, }); + return actionManager; }); - return actionManager; -} +}; /** * create a view from various parameters. Here, a view means a javascript @@ -89,10 +90,6 @@ function createActionManager (params) { * through a mock method using the data object as source, and already loaded/ * started. * - * Most views can be tested synchronously (@see createView), but some view have - * external dependencies (like lazy loaded libraries). In that case, it is - * necessary to use this method. - * * @param {Object} params * @param {string} params.arch the xml (arch) of the view to be instantiated * @param {any[]} [params.domain] the initial domain for the view @@ -109,9 +106,9 @@ function createActionManager (params) { * Note that this is particularly useful if you want to intercept events going * up in the init process of the view, because there are no other way to do it * after this method returns - * @returns {Deferred<AbstractView>} resolves with the instance of the view + * @returns {Promise<AbstractController>} resolves with the instance of the view */ -function createAsyncView(params) { +async function createView(params) { var $target = $('#qunit-fixture'); var widget = new Widget(); if (params.debug) { @@ -175,6 +172,7 @@ function createAsyncView(params) { // when it will be called the second time (by its parent) delete view.destroy; widget.destroy(); + $webClient.remove(); testUtilsMock.unpatch(View); }; @@ -187,14 +185,45 @@ function createAsyncView(params) { callbacks: [{ widget: view }], in_DOM: true, }); + view.$el.on('click', 'a', function (ev) { ev.preventDefault(); }); - return view; }); }); } + +/** + * Similar as createView, but specific for calendar views. Some calendar + * tests need to trigger positional clicks on the DOM produced by fullcalendar. + * Those tests must use this helper with option positionalClicks set to true. + * This will move the rendered calendar to the body (required to do positional + * clicks), and wait for a setTimeout(0) before returning, because fullcalendar + * makes the calendar scroll to 6:00 in a setTimeout(0), which might have an + * impact according to where we want to trigger positional clicks. + * + * @param {Object} params see @createView + * @param {Object} [options] + * @param {boolean} [options.positionalClicks=false] + * @returns {Promise<CalendarController>} + */ +async function createCalendarView(params, options) { + var calendar = await createView(params); + if (!options || !options.positionalClicks) { + return calendar; + } + var $view = $('#qunit-fixture').contents(); + $view.prependTo('body'); + var destroy = calendar.destroy; + calendar.destroy = function () { + $view.remove(); + destroy(); + }; + await concurrency.delay(0); + return calendar; +} + /** * create a controlPanel from various parameters. * @@ -227,7 +256,7 @@ function createAsyncView(params) { * @param {boolean} [params.withSearchBar=true] if set to false, no default * search bar will be rendered * - * @returns {Deferred<ControlPanel>} resolves with an instance of the ControlPanelController + * @returns {Promise<ControlPanel>} resolves with an instance of the ControlPanelController */ function createControlPanel(params) { params = params || {}; @@ -270,6 +299,7 @@ function createControlPanel(params) { // children when it will be called the second time (by its parent) delete controlPanel.destroy; widget.destroy(); + $webClient.remove(); }; // render the controlPanel in a fragment as it must be able to render @@ -290,16 +320,16 @@ function createControlPanel(params) { * * @param {Object} [params={}] */ -function createDebugManager (params) { +var createDebugManager = function (params) { params = params || {}; var mockRPC = params.mockRPC; _.extend(params, { mockRPC: function (route, args) { if (args.method === 'check_access_rights') { - return $.when(true); + return Promise.resolve(true); } if (args.method === 'xmlid_to_res_id') { - return $.when(true); + return Promise.resolve(true); } if (mockRPC) { return mockRPC.apply(this, arguments); @@ -309,7 +339,7 @@ function createDebugManager (params) { session: { user_has_group: function (group) { if (group === 'base.group_no_one') { - return $.when(true); + return Promise.resolve(true); } return this._super.apply(this, arguments); }, @@ -318,7 +348,7 @@ function createDebugManager (params) { var debugManager = new DebugManager(); testUtilsMock.addMockEnvironment(debugManager, params); return debugManager; -} +}; /** * create a model from given parameters. @@ -360,32 +390,10 @@ function createParent(params) { return widget; } -/** - * create a view synchronously. This method uses the createAsyncView method. - * Most views are synchronous, so the deferred can be resolved immediately and - * this method will work. - * - * Be careful, if for some reason a view is async, this method will crash. - * @see createAsyncView - * - * @param {Object} params will be given to createAsyncView - * @returns {AbstractView} - */ -function createView(params) { - var view; - createAsyncView(params).then(function (result) { - view = result; - }); - if (!view) { - throw "The view that you are trying to create is async. Please use createAsyncView instead"; - } - return view; -} - - return { createActionManager: createActionManager, - createAsyncView: createAsyncView, + createView: createView, + createCalendarView: createCalendarView, createControlPanel: createControlPanel, createDebugManager: createDebugManager, createModel: createModel, diff --git a/addons/web/static/tests/helpers/test_utils_dom.js b/addons/web/static/tests/helpers/test_utils_dom.js index dcf9eab6e15..315a00cbfe7 100644 --- a/addons/web/static/tests/helpers/test_utils_dom.js +++ b/addons/web/static/tests/helpers/test_utils_dom.js @@ -1,6 +1,8 @@ -odoo.define('web.test_utils_dom', function () { +odoo.define('web.test_utils_dom', function (require) { "use strict"; +var concurrency = require('web.concurrency'); + /** * DOM Test Utils * @@ -101,6 +103,7 @@ function dragAndDrop($el, $to, options) { $el.trigger($.Event("mouseup")); }); } + return concurrency.delay(0); } /** @@ -114,7 +117,8 @@ function dragAndDrop($el, $to, options) { function triggerMouseEvent($el, type) { var pos = $el.offset(); var e = new $.Event(type); - e.pageX = e.layerX = e.screenX = pos.left; + // 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); @@ -154,6 +158,8 @@ function triggerKeypressEvent(char) { var keycode; if (char === "Enter") { keycode = $.ui.keyCode.ENTER; + } else if (char === "Tab") { + keycode = $.ui.keyCode.TAB; } else { keycode = char.charCodeAt(0); } @@ -177,37 +183,47 @@ function openDatepicker($datepickerEl) { * @param {Object} [options] * @param {boolean} [options.first=false] if true, clicks on the first element * @param {boolean} [options.last=false] if true, clicks on the last element + * @param {boolean} [options.allowInvisible=false] if true, clicks on the + * element event if it is invisible + * @param {boolean} [options.shouldTriggerClick=false] if true, trigger the + * click event without calling the function click of jquery */ function click(el, options) { options = options || {}; var matches = typeof el === 'string' ? $(el) : el; - var selectorMsg = el.selector ? `(selector: ${el.selector})` : ''; + var selectorMsg = typeof el === 'string' ? `(selector: ${el})` : ''; if (!matches.filter) { // it might be an array of dom elements matches = $(matches); } + var validMatches = options.allowInvisible ? matches : matches.filter(':visible'); - var visibleMatches = matches.filter(':visible'); if (options.first) { - if (visibleMatches.length === 1) { + if (validMatches.length === 1) { throw new Error(`There should be more than one visible target ${selectorMsg}. If` + ' you are sure that there is exactly one target, please use the ' + 'click function instead of the clickFirst function'); } - visibleMatches = visibleMatches.first(); + validMatches = validMatches.first(); } else if (options.last) { - if (visibleMatches.length === 1) { + if (validMatches.length === 1) { throw new Error(`There should be more than one visible target ${selectorMsg}. If` + ' you are sure that there is exactly one target, please use the ' + 'click function instead of the clickLast function'); } - visibleMatches = visibleMatches.last(); + validMatches = validMatches.last(); } - if (visibleMatches.length === 0 && matches.length > 0) { + if (validMatches.length === 0 && matches.length > 0) { throw new Error(`Element to click on is not visible ${selectorMsg}`); - } else if (visibleMatches.length !== 1) { - throw new Error(`Found ${visibleMatches.length} elements to click on, instead of 1 ${selectorMsg}`); + } else if (validMatches.length !== 1) { + throw new Error(`Found ${validMatches.length} elements to click on, instead of 1 ${selectorMsg}`); + } + if (options.shouldTriggerClick) { + validMatches.trigger('click'); + } else { + validMatches.click(); } - visibleMatches.click(); + + return concurrency.delay(0); } /** @@ -216,9 +232,13 @@ function click(el, options) { * use the click helper instead. * * @param {string|NodeList|jQuery} el (if string: it is a (jquery) selector) + * @param {boolean} [options.allowInvisible=false] if true, clicks on the + * element event if it is invisible + * @param {boolean} [options.shouldTriggerClick=false] if true, trigger the + * click event without calling the function click of jquery */ -function clickFirst(el) { - click(el, {first: true}); +function clickFirst(el, options) { + return click(el, _.extend({}, options, {first: true})); } /** @@ -227,11 +247,36 @@ function clickFirst(el) { * use the click helper instead. * * @param {string|NodeList|jQuery} el + * @param {boolean} [options.allowInvisible=false] if true, clicks on the + * element event if it is invisible + * @param {boolean} [options.shouldTriggerClick=false] if true, trigger the + * click event without calling the function click of jquery + */ +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 + * @param {string[]} events the events you want to trigger + * @returns Promise */ -function clickLast(el) { - click(el, {last: true}); +function triggerEvents($el, events) { + if ($el.length !== 1) { + throw new Error(`target has length ${$el.length} instead of 1`); + } + if (typeof events === 'string') { + events = [events]; + } + events.forEach(function (event) { + $el.trigger(event); + }); + return concurrency.delay(0); } + return { triggerKeypressEvent: triggerKeypressEvent, triggerMouseEvent: triggerMouseEvent, @@ -241,6 +286,7 @@ return { click: click, clickFirst: clickFirst, clickLast: clickLast, + triggerEvents: triggerEvents, }; }); diff --git a/addons/web/static/tests/helpers/test_utils_fields.js b/addons/web/static/tests/helpers/test_utils_fields.js index f0f29839a9d..359f5f56b08 100644 --- a/addons/web/static/tests/helpers/test_utils_fields.js +++ b/addons/web/static/tests/helpers/test_utils_fields.js @@ -11,6 +11,7 @@ odoo.define('web.test_utils_fields', function (require) { */ var concurrency = require('web.concurrency'); +var domUtils = require('web.test_utils_dom'); //------------------------------------------------------------------------------ // Public functions @@ -32,9 +33,7 @@ function editAndTrigger($el, value, events) { throw new Error(`target ${$el.selector} has length ${$el.length} instead of 1`); } $el.val(value); - events.forEach(function (event) { - $el.trigger(event); - }); + return domUtils.triggerEvents($el, events); } /** @@ -49,7 +48,7 @@ function editAndTrigger($el, value, events) { * @param {string|number} value */ function editInput($el, value) { - editAndTrigger($el, value, ['input']); + return editAndTrigger($el, value, ['input']); } /** @@ -65,6 +64,7 @@ function editInput($el, value) { */ function editSelect($el, value) { editAndTrigger($el, value, ['change']); + return concurrency.delay(0); } /** @@ -75,14 +75,16 @@ function editSelect($el, value) { * input * @returns {HTMLInputElement} the main many2one input */ -function clickOpenM2ODropdown(fieldName, selector) { +async function clickOpenM2ODropdown(fieldName, selector) { var m2oSelector = `${selector || ''} .o_field_many2one[name=${fieldName}] input`; var matches = document.querySelectorAll(m2oSelector); if (matches.length !== 1) { throw new Error(`cannot open m2o: selector ${selector} has been found ${matches.length} instead of 1`); } matches[0].click(); - return matches[0]; + return await concurrency.delay(0).then(function () { + return matches[0]; + }); } /** @@ -97,6 +99,7 @@ function clickM2OHighlightedItem(fieldName, selector) { var $dropdown = $(m2oSelector).autocomplete('widget'); // clicking on an li (no matter which one), will select the focussed one $dropdown.find('li:first()').click(); + return concurrency.delay(0); } /** @@ -117,7 +120,10 @@ function clickM2OItem(fieldName, searchText) { if ($target.length !== 1 || !$target.is(':visible')) { throw new Error('Menu item should be unique and visible'); } - $target.mouseenter().click(); + $target.mouseenter(); + $target.click(); + + return concurrency.delay(0); } /** @@ -140,30 +146,69 @@ function clickM2OItem(fieldName, searchText) { function searchAndClickM2OItem(fieldName, options) { options = options || {}; - var input = clickOpenM2ODropdown(fieldName, options.selector); - - var def; - if (options.search) { - // jquery autocomplete refines the search in a setTimeout() parameterized - // with a delay, so we force this delay to 0 s.t. the dropdown is filtered - // directly on the next tick - var $input = $(input); - var delay = $input.autocomplete('option', 'delay'); - $input.autocomplete('option', 'delay', 0); - input.value = options.search; - input.dispatchEvent(new Event('input')); - $input.autocomplete('option', 'delay', delay); - def = concurrency.delay(0); - } + return clickOpenM2ODropdown(fieldName, options.selector).then(function (input) { + var def; + if (options.search) { + input.value = options.search; + input.dispatchEvent(new Event('input')); + def = concurrency.delay(0); + } + return Promise.resolve(def).then(function () { + if (options.item) { + return clickM2OItem(fieldName, options.item); + } else { + return clickM2OHighlightedItem(fieldName, options.selector); + } + }); + }); +} - return $.when(def).then(function () { - if (options.item) { - clickM2OItem(fieldName, options.item); +/** + * Helper to trigger a key event on an element. + * + * @param {string} type type of key event ('press', 'up' or 'down') + * @param {jQuery} $el + * @param {number|string} keyCode used as number, but if string, it'll check if + * the string corresponds to a key -otherwise it will keep only the first + * char to get a letter key- and convert it into a keyCode. + * @returns {Promise} + */ +function triggerKey(type, $el, keyCode) { + type = 'key' + type; + if (typeof keyCode === 'string') { + if (keyCode.length > 1) { + keyCode = keyCode.toUpperCase(); + keyCode = $.ui.keyCode[keyCode]; } else { - clickM2OHighlightedItem(fieldName, options.selector); + keyCode = keyCode.charCodeAt(0); } - }); + } + $el.trigger({type: type, which: keyCode, keyCode: keyCode}); + return concurrency.delay(0); } + +/** + * Helper to trigger a keydown event on an element. + * + * @param {jQuery} $el + * @param {number|string} keyCode @see triggerKey + * @returns {Promise} + */ +function triggerKeydown($el, keyCode) { + return triggerKey('down', $el, keyCode); +} + +/** + * Helper to trigger a keyup event on an element. + * + * @param {jQuery} $el + * @param {number|string} keyCode @see triggerKey + * @returns {Promise} + */ +function triggerKeyup($el, keyCode) { + return triggerKey('up', $el, keyCode); +} + return { clickOpenM2ODropdown: clickOpenM2ODropdown, clickM2OHighlightedItem: clickM2OHighlightedItem, @@ -172,6 +217,9 @@ return { editInput: editInput, editSelect: editSelect, searchAndClickM2OItem: searchAndClickM2OItem, + triggerKey: triggerKey, + triggerKeydown: triggerKeydown, + triggerKeyup: triggerKeyup, }; }); diff --git a/addons/web/static/tests/helpers/test_utils_file.js b/addons/web/static/tests/helpers/test_utils_file.js index 64e044e3180..e5b68a28e0b 100644 --- a/addons/web/static/tests/helpers/test_utils_file.js +++ b/addons/web/static/tests/helpers/test_utils_file.js @@ -47,29 +47,30 @@ function _createFakeDataTransfer(file) { * @param {string} data.name * @param {string} data.content * @param {string} data.contentType - * @returns {$.Promise<Object>} resolved with file created + * @returns {Promise<Object>} resolved with file created */ function createFile(data) { - var def = $.Deferred(); - var requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; - if (!requestFileSystem) { - throw new Error('FileSystem API is not supported'); - } - requestFileSystem(window.TEMPORARY, 1024 * 1024, function (fileSystem) { - fileSystem.root.getFile(data.name, { create: true }, function (fileEntry) { - fileEntry.createWriter(function (fileWriter) { - fileWriter.onwriteend = function (e) { - fileSystem.root.getFile(data.name, {}, function (fileEntry) { - fileEntry.file(function (file) { - def.resolve(file); + // Note: this is only supported by Chrome, and does not work in Incognito mode + return new Promise(function (resolve, reject) { + var requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; + if (!requestFileSystem) { + throw new Error('FileSystem API is not supported'); + } + requestFileSystem(window.TEMPORARY, 1024 * 1024, function (fileSystem) { + fileSystem.root.getFile(data.name, { create: true }, function (fileEntry) { + fileEntry.createWriter(function (fileWriter) { + fileWriter.onwriteend = function (e) { + fileSystem.root.getFile(data.name, {}, function (fileEntry) { + fileEntry.file(function (file) { + resolve(file); + }); }); - }); - }; - fileWriter.write(new Blob([ data.content ], { type: data.contentType })); + }; + fileWriter.write(new Blob([ data.content ], { type: data.contentType })); + }); }); }); }); - return def; } /** diff --git a/addons/web/static/tests/helpers/test_utils_form.js b/addons/web/static/tests/helpers/test_utils_form.js index 379350e1ca6..b4bff154ce7 100644 --- a/addons/web/static/tests/helpers/test_utils_form.js +++ b/addons/web/static/tests/helpers/test_utils_form.js @@ -20,7 +20,7 @@ var testUtilsDom = require('web.test_utils_dom'); * @param {FormController} form */ function clickEdit(form) { - testUtilsDom.click(form.$buttons.find('.o_form_button_edit')); + return testUtilsDom.click(form.$buttons.find('.o_form_button_edit')); } /** @@ -30,7 +30,7 @@ function clickEdit(form) { * @param {FormController} form */ function clickSave(form) { - testUtilsDom.click(form.$buttons.find('.o_form_button_save')); + return testUtilsDom.click(form.$buttons.find('.o_form_button_save')); } /** @@ -40,7 +40,7 @@ function clickSave(form) { * @param {FormController} form */ function clickCreate(form) { - testUtilsDom.click(form.$buttons.find('.o_form_button_create')); + return testUtilsDom.click(form.$buttons.find('.o_form_button_create')); } /** @@ -50,7 +50,7 @@ function clickCreate(form) { * @param {FormController} form */ function clickDiscard(form) { - testUtilsDom.click(form.$buttons.find('.o_form_button_cancel')); + return testUtilsDom.click(form.$buttons.find('.o_form_button_cancel')); } /** @@ -60,7 +60,7 @@ function clickDiscard(form) { * @param {[Object]} params given to the controller reload method */ function reload(form, params) { - form.reload(params); + return form.reload(params); } return { diff --git a/addons/web/static/tests/helpers/test_utils_graph.js b/addons/web/static/tests/helpers/test_utils_graph.js index 8867f82c349..f6b50a1b571 100644 --- a/addons/web/static/tests/helpers/test_utils_graph.js +++ b/addons/web/static/tests/helpers/test_utils_graph.js @@ -18,7 +18,7 @@ odoo.define('web.test_utils_graph', function () { * @param {[Object]} params given to the controller reload method */ function reload(graph, params) { - graph.reload(params); + return graph.reload(params); } return { diff --git a/addons/web/static/tests/helpers/test_utils_kanban.js b/addons/web/static/tests/helpers/test_utils_kanban.js index 166ae2a2572..76af632b417 100644 --- a/addons/web/static/tests/helpers/test_utils_kanban.js +++ b/addons/web/static/tests/helpers/test_utils_kanban.js @@ -18,22 +18,24 @@ var testUtilsFields = require('web.test_utils_fields'); * the Create button is visible. * * @param {KanbanController} kanban + * @returns {Promise} */ function clickCreate(kanban) { - testUtilsDom.click(kanban.$buttons.find('.o-kanban-button-new')); + return testUtilsDom.click(kanban.$buttons.find('.o-kanban-button-new')); } /** * Open the settings menu for a column (in a grouped kanban view) * * @param {jQuery} $column + * @returns {Promise} */ function toggleGroupSettings($column) { var $dropdownToggler = $column.find('.o_kanban_config > a.dropdown-toggle'); if (!$dropdownToggler.is(':visible')) { $dropdownToggler.css('display', 'block'); } - testUtilsDom.click($dropdownToggler); + return testUtilsDom.click($dropdownToggler); } /** @@ -43,6 +45,7 @@ function toggleGroupSettings($column) { * @param {kanbanController} kanban * @param {string|number} value * @param {[string]} fieldName + * @returns {Promise} */ function quickCreate(kanban, value, fieldName) { var additionalSelector = fieldName ? ('[name=' + fieldName + ']'): ''; @@ -53,7 +56,7 @@ function quickCreate(kanban, value, fieldName) { keyCode: $.ui.keyCode.ENTER, } ); - testUtilsFields.editAndTrigger( + return testUtilsFields.editAndTrigger( kanban.$('.o_kanban_quick_create input' + additionalSelector), value, ['input', enterEvent] @@ -65,9 +68,10 @@ function quickCreate(kanban, value, fieldName) { * * @param {KanbanController} kanban * @param {[Object]} params given to the controller reload method + * @returns {Promise} */ function reload(kanban, params) { - kanban.reload(params); + return kanban.reload(params); } /** @@ -76,13 +80,14 @@ function reload(kanban, params) { * does not comply with the usual dom structure. * * @param {jQuery} $record + * @returns {Promise} */ function toggleRecordDropdown($record) { var $dropdownToggler = $record.find('.o_dropdown_kanban > a.dropdown-toggle'); if (!$dropdownToggler.is(':visible')) { $dropdownToggler.css('display', 'block'); } - testUtilsDom.click($dropdownToggler); + return testUtilsDom.click($dropdownToggler); } diff --git a/addons/web/static/tests/helpers/test_utils_mock.js b/addons/web/static/tests/helpers/test_utils_mock.js index 2ecc655db7e..ccb4d20959e 100644 --- a/addons/web/static/tests/helpers/test_utils_mock.js +++ b/addons/web/static/tests/helpers/test_utils_mock.js @@ -124,7 +124,7 @@ function removeSrcAttribute(el, widget) { * is completely removed by default. * * @returns {MockServer} the instance of the mock server, created by this - * function. It is necessary for createAsyncView so that method can call some + * function. It is necessary for createView so that method can call some * other methods on it. */ function addMockEnvironment(widget, params) { @@ -199,10 +199,11 @@ function addMockEnvironment(widget, params) { // widget is destroyed, at the end of each test to avoid collisions core.bus.trigger('clear_cache'); - _(services).chain() - .compact() // services can be defined but null (e.g. ajax) - .reject(function (s) { return s.isDestroyed(); }) - .invoke('destroy'); + Object.keys(services).forEach(function(s) { + var service = services[s]; + if (service && !service.isDestroyed()) + service.destroy(); + }); DebouncedField.prototype.DEBOUNCE = initialDebounceValue; dom.DEBOUNCE = initialDOMDebounceValue; @@ -543,6 +544,25 @@ function unpatch(target) { delete target.__patchID; } +window.originalSetTimeout = window.setTimeout; +function patchSetTimeout() { + var original = window.setTimeout; + var self = this; + window.setTimeout = function (handler, delay) { + console.log("calling setTimeout on " + (handler.name || "some function") + "with delay of " + delay); + console.trace(); + var handlerArguments = Array.prototype.slice.call(arguments, 1); + return original(function () { + handler.bind(self, handlerArguments)(); + console.log('after doing the action of the setTimeout'); + }, delay); + }; + + return function () { + window.setTimeout = original; + }; +} + return { addMockEnvironment: addMockEnvironment, @@ -551,6 +571,7 @@ return { patchDate: patchDate, patch: patch, unpatch: unpatch, + patchSetTimeout: patchSetTimeout, }; }); diff --git a/addons/web/static/tests/helpers/test_utils_modal.js b/addons/web/static/tests/helpers/test_utils_modal.js index d2162aebfa0..5587eecfc01 100644 --- a/addons/web/static/tests/helpers/test_utils_modal.js +++ b/addons/web/static/tests/helpers/test_utils_modal.js @@ -11,6 +11,7 @@ odoo.define('web.test_utils_modal', function (require) { */ var core = require('web.core'); +var concurrency = require('web.concurrency'); /** * Click on a button in the footer of a modal (which contains a given string). @@ -25,6 +26,7 @@ function clickButton(text) { throw new Error(`Found ${$button.length} button(s) containing '${text}'`); } $button.click(); + return concurrency.delay(0); } return { diff --git a/addons/web/static/tests/helpers/test_utils_pivot.js b/addons/web/static/tests/helpers/test_utils_pivot.js index 18a5a7307f3..fe059b533ed 100644 --- a/addons/web/static/tests/helpers/test_utils_pivot.js +++ b/addons/web/static/tests/helpers/test_utils_pivot.js @@ -24,7 +24,7 @@ var testUtilsDom = require('web.test_utils_dom'); * @param {string} measure */ function clickMeasure(pivot, measure) { - testUtilsDom.click(pivot.$buttons.find(`.dropdown-item[data-field=${measure}]`)); + return testUtilsDom.click(pivot.$buttons.find(`.dropdown-item[data-field=${measure}]`)); } /** @@ -35,7 +35,7 @@ function clickMeasure(pivot, measure) { * @param {PivotController} pivot */ function toggleMeasuresDropdown(pivot) { - testUtilsDom.click(pivot.$buttons.filter('.btn-group:first').find('> button')); + return testUtilsDom.click(pivot.$buttons.filter('.btn-group:first').find('> button')); } /** @@ -45,7 +45,7 @@ function toggleMeasuresDropdown(pivot) { * @param {[Object]} params given to the controller reload method */ function reload(pivot, params) { - pivot.reload(params); + return pivot.reload(params); } return { |
