summaryrefslogtreecommitdiff
path: root/addons/web/static/tests/helpers/test_utils_dom.js
diff options
context:
space:
mode:
authorRobot Odoo <xmo+roboto@odoo.com>2018-11-19 15:38:25 +0100
committerGitHub <noreply@github.com>2018-11-19 15:38:25 +0100
commit7c3d5dc067c7c331dcc0b52bafbdfd067087a3c4 (patch)
tree6058f8a893815bf0ed2495649c125523f4def762 /addons/web/static/tests/helpers/test_utils_dom.js
parent8fc246da31aa2a1c770732ae97e0cb4e840f07cf (diff)
parentabf32b8b21394491040e5c11c1fea809c1d7c62c (diff)
[MERGE][REF] *: update js test suite to use helpers
This rev. introduces robust helpers to use in the JS tests suite to interact with DOM and components, and starts using them (almost) everywhere. All the helpers are exposed though testUtils.js. There are 2 kinds of helpers: 1. Assertions ------------- * assert.containsNone, containsN, containsOnce check that the DOM (or a specific part of the DOM) contains a `selector`. It generates a correct error message automatically. ex: assert.notOk(form.$('button').hasClass('btn-primary')); -> assert.doesNotHaveClass(form.$('button'), 'btn-primary'); * assert.isVisible, isNotVisible check that the DOM has an element visible or not. They also check that the element is actually in the DOM (before most tests didn't verify this). * assert.hasClass, doesNotHaveClass, hasAttrVAlue, check specific properties of a DOM element, and also validate that it is applied on a single existing DOM element (before most tests didn't verify this). 2. Utilities ------------ The goal of the utilities is to centralize the definition of many standard components and interactions, ensuring that when we refactor the JS framework, we do not need to change all the tests. Existing mock utilities (addMockEnvironment, intercept, path, patchDate, unpatch and fieldsViewGet) are moved to 'testUtils.mock.*'. Existing DOM utilities are moved to 'testUtils.dom.*'. New dom utilities are created for opendDatePicker, click, clickFirst and clickLast. Helper `click` verifies that there is exactly 1 element visible in the DOM you click on, `clickFirst` and `clickLast` verify that there are more than one element on the DOM. ex: form.$('button').click(); -> testUtils.dom.click(form.$('button')); New Form utilities: (testUtils.form.*) clickEdit, clickSave, clickCreate, clickDiscard, all clicks on the control panel buttons of the form. `reload` reloads the form data. New modal, graph, kanban and pivot utils (testUtils.pivot.*, testUtils.kanban.*, etc.). New fields utils: (testUtils.fields.*) * editInput, editSelect: allow to change the value of a field, using a selector to identify it. They validate that the input exists and trigger the change event automatically. * editAndTrigger: allow to modify a field and trigger specific events after the value change * many2one (testUtils.fields.many2one.*) clickOpenDropdown, clickHighlightedItem, clickItem, searchAndClickItem: use a field name instead of a selector and do all the complex mechanism to open, filter and highlight many2one fields. Joint work with aab, dam, ged, mge, svs and vsc. closes odoo/odoo#28784
Diffstat (limited to 'addons/web/static/tests/helpers/test_utils_dom.js')
-rw-r--r--addons/web/static/tests/helpers/test_utils_dom.js235
1 files changed, 235 insertions, 0 deletions
diff --git a/addons/web/static/tests/helpers/test_utils_dom.js b/addons/web/static/tests/helpers/test_utils_dom.js
new file mode 100644
index 00000000000..b89f5a8eb5f
--- /dev/null
+++ b/addons/web/static/tests/helpers/test_utils_dom.js
@@ -0,0 +1,235 @@
+odoo.define('web.test_utils_dom', function () {
+"use strict";
+
+/**
+ * DOM Test Utils
+ *
+ * This module defines various utility functions to help simulate DOM events.
+ *
+ * Note that all methods defined in this module are exported in the main
+ * testUtils file.
+ */
+
+
+/**
+ * simulate a drag and drop operation between 2 jquery nodes: $el and $to.
+ * This is a crude simulation, with only the mousedown, mousemove and mouseup
+ * events, but it is enough to help test drag and drop operations with jqueryUI
+ * sortable.
+ *
+ * @param {jqueryElement} $el
+ * @param {jqueryElement} $to
+ * @param {Object} [options]
+ * @param {string|Object} [options.position='center'] target position:
+ * can either be one of {'top', 'bottom', 'left', 'right'} or
+ * an object with two attributes (top and left))
+ * @param {boolean} [options.disableDrop=false] whether to trigger the drop action
+ * @param {boolean} [options.continueMove=false] whether to trigger the
+ * mousedown action (will only work after another call of this function with
+ * without this option)
+ */
+function dragAndDrop($el, $to, options) {
+ var position = (options && options.position) || 'center';
+ var elementCenter = $el.offset();
+ var toOffset = $to.offset();
+
+ if (_.isObject(position)) {
+ toOffset.top += position.top;
+ toOffset.left += position.left;
+ } else {
+ toOffset.top += $to.outerHeight() / 2;
+ toOffset.left += $to.outerWidth() / 2;
+ var vertical_offset = (toOffset.top < elementCenter.top) ? -1 : 1;
+ if (position === 'top') {
+ toOffset.top -= $to.outerHeight() / 2 + vertical_offset;
+ } else if (position === 'bottom') {
+ toOffset.top += $to.outerHeight() / 2 - vertical_offset;
+ } else if (position === 'left') {
+ toOffset.left -= $to.outerWidth() / 2;
+ } else if (position === 'right') {
+ toOffset.left += $to.outerWidth() / 2;
+ }
+ }
+
+ if ($to[0].ownerDocument !== document) {
+ // we are in an iframe
+ var bound = $('iframe')[0].getBoundingClientRect();
+ toOffset.left += bound.left;
+ toOffset.top += bound.top;
+ }
+ $el.trigger($.Event("mouseenter"));
+ if (!(options && options.continueMove)) {
+ elementCenter.left += $el.outerWidth() / 2;
+ elementCenter.top += $el.outerHeight() / 2;
+
+ $el.trigger($.Event("mousedown", {
+ which: 1,
+ pageX: elementCenter.left,
+ pageY: elementCenter.top
+ }));
+ }
+
+ $el.trigger($.Event("mousemove", {
+ which: 1,
+ pageX: toOffset.left,
+ pageY: toOffset.top
+ }));
+
+ if (!(options && options.disableDrop)) {
+ $el.trigger($.Event("mouseup", {
+ which: 1,
+ pageX: toOffset.left,
+ pageY: toOffset.top
+ }));
+ } 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"));
+ });
+ }
+}
+
+/**
+ * simulate a mouse event with a custom event who add the item position. This is
+ * 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 {string} type a mouse event type, such as 'mousedown' or 'mousemove'
+ */
+function triggerMouseEvent($el, type) {
+ var pos = $el.offset();
+ var e = new $.Event(type);
+ e.pageX = e.layerX = e.screenX = pos.left;
+ e.pageY = e.layerY = e.screenY = pos.top;
+ e.which = 1;
+ $el.trigger(e);
+}
+
+/**
+ * simulate a mouse event with a custom event on a position x and y. This is
+ * sometimes necessary because the basic way to trigger an event (such as
+ * $el.trigger('mousemove')); ) is too crude for some uses.
+ *
+ * @param {integer} x
+ * @param {integer} y
+ * @param {string} type a mouse event type, such as 'mousedown' or 'mousemove'
+ */
+function triggerPositionalMouseEvent(x, y, type) {
+ var ev = document.createEvent("MouseEvent");
+ var el = document.elementFromPoint(x, y);
+ ev.initMouseEvent(
+ type,
+ true /* bubble */,
+ true /* cancelable */,
+ window, null,
+ x, y, x, y, /* coordinates */
+ false, false, false, false, /* modifier keys */
+ 0 /*left button*/, null
+ );
+ el.dispatchEvent(ev);
+ return el;
+}
+
+/**
+ * simulate a keypress event for a given character
+ *
+ * @param {string} char the character, or 'ENTER'
+ */
+function triggerKeypressEvent(char) {
+ var keycode;
+ if (char === "Enter") {
+ keycode = $.ui.keyCode.ENTER;
+ } else {
+ keycode = char.charCodeAt(0);
+ }
+ return $('body').trigger($.Event('keypress', { which: keycode, keyCode: keycode }));
+}
+
+/**
+ * Opens the datepicker of a given element.
+ *
+ * @param {jQuery} $datepickerEl element to which a datepicker is attached
+ */
+function openDatepicker($datepickerEl) {
+ $datepickerEl.find('.o_datepicker_input').trigger('focus.datetimepicker');
+}
+
+/**
+ * Click on a specified element. If the option first or last is not specified,
+ * this method also check the unicity and the visibility of the target.
+ *
+ * @param {string|NodeList|jQuery} el (if string: it is a (jquery) selector)
+ * @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
+ */
+function click(el, options) {
+ options = options || {};
+ var matches = typeof el === 'string' ? $(el) : el;
+ var selectorMsg = el.selector ? `(selector: ${el.selector})` : '';
+ if (!matches.filter) { // it might be an array of dom elements
+ matches = $(matches);
+ }
+
+ var visibleMatches = matches.filter(':visible');
+ if (options.first) {
+ if (visibleMatches.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();
+ } else if (options.last) {
+ if (visibleMatches.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();
+ }
+ if (visibleMatches.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}`);
+ }
+ visibleMatches.click();
+}
+
+/**
+ * Click on the first element of a list of elements. Note that if the list has
+ * only one visible element, we trigger an error. In that case, it is better to
+ * use the click helper instead.
+ *
+ * @param {string|NodeList|jQuery} el (if string: it is a (jquery) selector)
+ */
+function clickFirst(el) {
+ click(el, {first: true});
+}
+
+/**
+ * Click on the last element of a list of elements. Note that if the list has
+ * only one visible element, we trigger an error. In that case, it is better to
+ * use the click helper instead.
+ *
+ * @param {string|NodeList|jQuery} el
+ */
+function clickLast(el) {
+ click(el, {last: true});
+}
+
+return {
+ triggerKeypressEvent: triggerKeypressEvent,
+ triggerMouseEvent: triggerMouseEvent,
+ triggerPositionalMouseEvent: triggerPositionalMouseEvent,
+ dragAndDrop: dragAndDrop,
+ openDatepicker: openDatepicker,
+ click: click,
+ clickFirst: clickFirst,
+ clickLast: clickLast,
+};
+
+});