summaryrefslogtreecommitdiff
path: root/addons/web/static/tests/helpers
diff options
context:
space:
mode:
authorJulien Mougenot <jum@odoo.com>2020-03-11 09:38:27 +0000
committerJulien Mougenot <jum@odoo.com>2020-03-13 11:39:53 +0000
commit0a129284e4856b6485c48a194fd86c3ab5c736e7 (patch)
treedc0adb0c9528605019af5c915ad2c877f9aa1c57 /addons/web/static/tests/helpers
parent72f1633532a586e1ccee4d2a961d745553b9d8b8 (diff)
[IMP] web,*: Enhance test helpers
1. Test utils helpers adapted to properly instantiate the new Owl control panel 2. New file 'test_utils_control_panel' to create a control panel and use helpers 3. New helper: createComponent (creates a generic component, see jsdoc) 4. Use of the new helpers in the current tests (more in sibling commits) 5. Helper 'addMockEnvironment' is now asynchronous 6. Owl config mode set to 'dev' in test suite (props validation) Part of task 2196029 Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
Diffstat (limited to 'addons/web/static/tests/helpers')
-rw-r--r--addons/web/static/tests/helpers/qunit_asserts.js2
-rw-r--r--addons/web/static/tests/helpers/test_env.js52
-rw-r--r--addons/web/static/tests/helpers/test_utils.js53
-rw-r--r--addons/web/static/tests/helpers/test_utils_control_panel.js446
-rw-r--r--addons/web/static/tests/helpers/test_utils_create.js226
-rw-r--r--addons/web/static/tests/helpers/test_utils_dom.js45
-rw-r--r--addons/web/static/tests/helpers/test_utils_mock.js44
7 files changed, 733 insertions, 135 deletions
diff --git a/addons/web/static/tests/helpers/qunit_asserts.js b/addons/web/static/tests/helpers/qunit_asserts.js
index 0f22acbbf78..8a5948dc676 100644
--- a/addons/web/static/tests/helpers/qunit_asserts.js
+++ b/addons/web/static/tests/helpers/qunit_asserts.js
@@ -13,6 +13,8 @@ odoo.define('web.qunit_asserts', function (require) {
const Widget = require('web.Widget');
+ /** @todo use testUtilsDom.getNode to extract the element from the 'w' argument */
+
//-------------------------------------------------------------------------
// Private functions
//-------------------------------------------------------------------------
diff --git a/addons/web/static/tests/helpers/test_env.js b/addons/web/static/tests/helpers/test_env.js
index d934bd3ac84..68659e366c3 100644
--- a/addons/web/static/tests/helpers/test_env.js
+++ b/addons/web/static/tests/helpers/test_env.js
@@ -8,35 +8,6 @@ odoo.define('web.test_env', async function (require) {
const session = require('web.session');
/**
- * Wrap the target object in a Proxy, giving it a generic getter that will
- * throw an error instead of returning `undefined` in case the property has
- * not been set.
- *
- * @param {Object} target
- * @param {string} name
- * @returns {Proxy}
- */
- function _proxify(target, name) {
- for (const prop in target) {
- if (
- target.hasOwnProperty(prop) &&
- typeof target[prop] === 'object' &&
- target[prop] !== null
- ) {
- target[prop] = _proxify(target[prop], `${name}.${prop}`);
- }
- }
- return new Proxy(target, {
- get(object, property) {
- if (typeof property === 'string' && !(property in object)) {
- throw new Error(`Property "${property}" not implemented in "${name}".`);
- }
- return object[property];
- },
- });
- }
-
- /**
* Creates a test environment with the given environment object.
* Any access to a key that has not been explicitly defined in the given environment object
* will result in an error.
@@ -46,18 +17,26 @@ odoo.define('web.test_env', async function (require) {
* @returns {Proxy}
*/
function makeTestEnvironment(env = {}, providedRPC = null) {
- const proxiedEnv = _proxify(env, 'env');
const RamStorageService = AbstractStorageService.extend({
storage: new RamStorage(),
});
+ const database = {
+ parameters: {
+ code: "en_US",
+ date_format: '%m/%d/%Y',
+ decimal_point: ".",
+ direction: 'ltr',
+ grouping: [],
+ thousands_sep: ",",
+ time_format: '%H:%M:%S',
+ },
+ };
let testEnv = {};
const defaultEnv = {
- _t: env._t || (s => s),
- _lt: env._lt || (s => s),
+ _t: env._t || Object.assign((s => s), { database }),
+ _lt: env._lt || Object.assign((s => s), { database }),
bus: new Bus(),
- device: Object.assign({
- isMobile: false,
- }, env.device),
+ device: Object.assign({ isMobile: false }, env.device),
isDebug: env.isDebug || (() => false),
qweb: new owl.QWeb({ templates: session.owlTemplates }),
services: Object.assign({
@@ -77,6 +56,7 @@ odoo.define('web.test_env', async function (require) {
},
local_storage: new RamStorageService(),
session_storage: new RamStorageService(),
+ notification: { notify() { } },
}, env.services),
session: Object.assign({
rpc(route, params, options) {
@@ -87,7 +67,7 @@ odoo.define('web.test_env', async function (require) {
},
}, env.session),
};
- testEnv = Object.assign(proxiedEnv, defaultEnv);
+ testEnv = Object.assign(env, defaultEnv);
return testEnv;
}
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index 2ff77de4e49..f462df514a1 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -15,6 +15,7 @@ odoo.define('web.test_utils', async function (require) {
const relationalFields = require('web.relational_fields');
const session = require('web.session');
const testUtilsCreate = require('web.test_utils_create');
+ const testUtilsControlPanel = require('web.test_utils_control_panel');
const testUtilsDom = require('web.test_utils_dom');
const testUtilsFields = require('web.test_utils_fields');
const testUtilsFile = require('web.test_utils_file');
@@ -128,11 +129,60 @@ odoo.define('web.test_utils', async function (require) {
fieldsViewGet: testUtilsMock.fieldsViewGet,
patchSetTimeout: testUtilsMock.patchSetTimeout,
},
+ controlPanel: {
+ // Generic interactions
+ toggleMenu: testUtilsControlPanel.toggleMenu,
+ toggleMenuItem: testUtilsControlPanel.toggleMenuItem,
+ toggleMenuItemOption: testUtilsControlPanel.toggleMenuItemOption,
+ isItemSelected: testUtilsControlPanel.isItemSelected,
+ isOptionSelected: testUtilsControlPanel.isOptionSelected,
+ getMenuItemTexts: testUtilsControlPanel.getMenuItemTexts,
+ // Button interactions
+ getButtons: testUtilsControlPanel.getButtons,
+ // FilterMenu interactions
+ toggleFilterMenu: testUtilsControlPanel.toggleFilterMenu,
+ toggleAddCustomFilter: testUtilsControlPanel.toggleAddCustomFilter,
+ applyFilter: testUtilsControlPanel.applyFilter,
+ // GroupByMenu interactions
+ toggleGroupByMenu: testUtilsControlPanel.toggleGroupByMenu,
+ toggleAddCustomGroup: testUtilsControlPanel.toggleAddCustomGroup,
+ selectGroup: testUtilsControlPanel.selectGroup,
+ applyGroup: testUtilsControlPanel.applyGroup,
+ // FavoriteMenu interactions
+ toggleFavoriteMenu: testUtilsControlPanel.toggleFavoriteMenu,
+ toggleSaveFavorite: testUtilsControlPanel.toggleSaveFavorite,
+ editFavoriteName: testUtilsControlPanel.editFavoriteName,
+ saveFavorite: testUtilsControlPanel.saveFavorite,
+ deleteFavorite: testUtilsControlPanel.deleteFavorite,
+ // TimeRangeMenu interactions
+ toggleTimeRangeMenu: testUtilsControlPanel.toggleTimeRangeMenu,
+ selectField: testUtilsControlPanel.selectField,
+ selectRange: testUtilsControlPanel.selectRange,
+ selectComparisonRange: testUtilsControlPanel.selectComparisonRange,
+ toggleTimeRangeMenuBox: testUtilsControlPanel.toggleTimeRangeMenuBox,
+ applyTimeRange: testUtilsControlPanel.applyTimeRange,
+ // SearchBar interactions
+ getFacetTexts: testUtilsControlPanel.getFacetTexts,
+ removeFacet: testUtilsControlPanel.removeFacet,
+ editSearch: testUtilsControlPanel.editSearch,
+ validateSearch: testUtilsControlPanel.validateSearch,
+ // Action menus interactions
+ toggleActionMenu: testUtilsControlPanel.toggleActionMenu,
+ // Pager interactions
+ pagerPrevious: testUtilsControlPanel.pagerPrevious,
+ pagerNext: testUtilsControlPanel.pagerNext,
+ getPagerValue: testUtilsControlPanel.getPagerValue,
+ getPagerSize: testUtilsControlPanel.getPagerSize,
+ setPagerValue: testUtilsControlPanel.setPagerValue,
+ // View switcher
+ switchView: testUtilsControlPanel.switchView,
+ },
dom: {
triggerKeypressEvent: testUtilsDom.triggerKeypressEvent,
triggerMouseEvent: testUtilsDom.triggerMouseEvent,
triggerPositionalMouseEvent: testUtilsDom.triggerPositionalMouseEvent,
dragAndDrop: testUtilsDom.dragAndDrop,
+ getNode: testUtilsDom.getNode,
openDatepicker: testUtilsDom.openDatepicker,
click: testUtilsDom.click,
clickFirst: testUtilsDom.clickFirst,
@@ -187,10 +237,11 @@ odoo.define('web.test_utils', async function (require) {
},
createActionManager: testUtilsCreate.createActionManager,
+ createComponent: testUtilsCreate.createComponent,
+ createControlPanel: testUtilsCreate.createControlPanel,
createDebugManager: testUtilsCreate.createDebugManager,
createAsyncView: testUtilsCreate.createView,
createCalendarView: testUtilsCreate.createCalendarView,
- createControlPanel: testUtilsCreate.createControlPanel,
createView: testUtilsCreate.createView,
createModel: testUtilsCreate.createModel,
createParent: testUtilsCreate.createParent,
diff --git a/addons/web/static/tests/helpers/test_utils_control_panel.js b/addons/web/static/tests/helpers/test_utils_control_panel.js
new file mode 100644
index 00000000000..36a0a6b9bbf
--- /dev/null
+++ b/addons/web/static/tests/helpers/test_utils_control_panel.js
@@ -0,0 +1,446 @@
+odoo.define('web.test_utils_control_panel', function (require) {
+ "use strict";
+
+ const { click, triggerEvent, getNode } = require('web.test_utils_dom');
+ const { editInput, editSelect, editAndTrigger } = require('web.test_utils_fields');
+
+ //-------------------------------------------------------------------------
+ // Private functions
+ //-------------------------------------------------------------------------
+
+ /**
+ * Helper method to retrieve a distinct item from a collection of elements defined
+ * by the given 'selector' string. It can either be the index of the item or its
+ * inner text.
+ * @private
+ * @param {HTMLElement} el
+ * @param {string} selector
+ * @param {(number|string)} elFinder
+ */
+ function findItem(el, selector, elFinder) {
+ const elements = [...el.querySelectorAll(selector)];
+ if (!elements.length) {
+ throw new Error(`No element found with selector "${selector}".`);
+ }
+ switch (typeof elFinder) {
+ case 'number':
+ const element = elements[elFinder];
+ if (!element) {
+ throw new Error(`No element with selector "${selector}" at index ${elFinder}.`);
+ }
+ return element;
+ case 'string':
+ const stringMatch = elements.find(
+ e => e.innerText.trim().toLowerCase() === elFinder.toLowerCase()
+ );
+ if (!stringMatch) {
+ throw new Error(`No element with selector "${selector}" containing "${elFinder}".`);
+ }
+ return stringMatch;
+ default:
+ throw new Error(`Invalid provided element finder: must be a number|string|function.`);
+ }
+ }
+
+ //-------------------------------------------------------------------------
+ // Exported functions
+ //-------------------------------------------------------------------------
+
+ /**
+ * @param {EventTarget} el
+ * @param {(number|string)} menuFinder
+ * @returns {Promise}
+ */
+ async function toggleMenu(el, menuFinder) {
+ const menu = findItem(getNode(el), `.o_dropdown > button`, menuFinder);
+ await click(menu);
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {(number|string)} itemFinder
+ * @returns {Promise}
+ */
+ async function toggleMenuItem(el, itemFinder) {
+ const item = findItem(getNode(el), `.o_menu_item > a`, itemFinder);
+ await click(item);
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {(number|string)} itemFinder
+ * @param {(number|string)} optionFinder
+ * @returns {Promise}
+ */
+ async function toggleMenuItemOption(el, itemFinder, optionFinder) {
+ const item = findItem(getNode(el), `.o_menu_item > a`, itemFinder);
+ const option = findItem(item.parentNode, '.o_item_option > a', optionFinder);
+ await click(option);
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {(number|string)} itemFinder
+ * @returns {boolean}
+ */
+ function isItemSelected(el, itemFinder) {
+ const item = findItem(getNode(el), `.o_menu_item > a`, itemFinder);
+ return item.classList.contains('selected');
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {(number|string)} itemuFinder
+ * @param {(number|string)} optionFinder
+ * @returns {boolean}
+ */
+ function isOptionSelected(el, itemFinder, optionFinder) {
+ const item = findItem(getNode(el), `.o_menu_item > a`, itemFinder);
+ const option = findItem(item.parentNode, '.o_item_option > a', optionFinder);
+ return option.classList.contains('selected');
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {string[]}
+ */
+ function getMenuItemTexts(el) {
+ return [...getNode(el).querySelectorAll(`.o_dropdown ul .o_menu_item`)].map(
+ e => e.innerText.trim()
+ );
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {HTMLButtonElement[]}
+ */
+ function getButtons(el) {
+ return [...getNode(el).querySelector((`div.o_cp_bottom div.o_cp_buttons`)).children];
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function toggleFilterMenu(el) {
+ await click(getNode(el).querySelector(`.o_filter_menu button`));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function toggleAddCustomFilter(el) {
+ await click(getNode(el).querySelector(`button.o_add_custom_filter`));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function applyFilter(el) {
+ await click(getNode(el).querySelector(`div.o_add_filter_menu > button.o_apply_filter`));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function toggleGroupByMenu(el) {
+ await click(getNode(el).querySelector(`.o_group_by_menu button`));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function toggleAddCustomGroup(el) {
+ await click(getNode(el).querySelector(`button.o_add_custom_group_by`));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {string} fieldName
+ * @returns {Promise}
+ */
+ async function selectGroup(el, fieldName) {
+ await editSelect(
+ getNode(el).querySelector(`select.o_group_by_selector`),
+ fieldName
+ );
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function applyGroup(el) {
+ await click(getNode(el).querySelector(`div.o_add_group_by_menu > button.o_apply_group_by`));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function toggleFavoriteMenu(el) {
+ await click(getNode(el).querySelector(`.o_favorite_menu button`));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function toggleSaveFavorite(el) {
+ await click(getNode(el).querySelector(`.o_favorite_menu .o_add_favorite button`));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {string} name
+ * @returns {Promise}
+ */
+ async function editFavoriteName(el, name) {
+ await editInput(getNode(el).querySelector(`.o_favorite_menu .o_add_favorite input[type="text"]`), name);
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function saveFavorite(el) {
+ await click(getNode(el).querySelector(`.o_favorite_menu .o_add_favorite button.o_save_favorite`));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {(string|number)} favoriteFinder
+ * @returns {Promise}
+ */
+ async function deleteFavorite(el, favoriteFinder) {
+ const favorite = findItem(getNode(el), `.o_favorite_menu .o_menu_item`, favoriteFinder);
+ await click(favorite.querySelector('i.fa-trash-o'));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function toggleTimeRangeMenu(el) {
+ await click(getNode(el).querySelector(`div.o_time_range_menu > button`));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {string} fieldName
+ * @returns {Promise}
+ */
+ async function selectField(el, fieldName) {
+ await editSelect(
+ findItem(getNode(el), `div.o_time_range_section select`, 0),
+ fieldName
+ );
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {string} range
+ * @returns {Promise}
+ */
+ async function selectRange(el, range) {
+ await editSelect(
+ findItem(getNode(el), `div.o_time_range_section select`, 1),
+ range
+ );
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {string} comparisonRange
+ * @returns {Promise}
+ */
+ async function selectComparisonRange(el, comparisonRange) {
+ await editSelect(
+ findItem(getNode(el), `div.o_time_range_section select`, 2),
+ comparisonRange
+ );
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function toggleTimeRangeMenuBox(el) {
+ await click(getNode(el).querySelector(`div.o_time_range_section input`));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function applyTimeRange(el) {
+ await click(getNode(el).querySelector(`div.o_time_range_menu ul button`));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ function getFacetTexts(el) {
+ return [...getNode(el).querySelectorAll(`.o_searchview .o_searchview_facet`)].map(
+ facet => facet.innerText.trim()
+ );
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {(string|number)} facetFinder
+ * @returns {Promise}
+ */
+ async function removeFacet(el, facetFinder = 0) {
+ const facet = findItem(getNode(el), `.o_searchview .o_searchview_facet`, facetFinder);
+ await click(facet.querySelector('.o_facet_remove'));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {string} value
+ * @returns {Promise}
+ */
+ async function editSearch(el, value) {
+ await editInput(getNode(el).querySelector(`.o_searchview_input`), value);
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function validateSearch(el) {
+ await triggerEvent(
+ getNode(el).querySelector(`.o_searchview_input`),
+ 'keydown', { key: 'Enter' }
+ );
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {string} [menuFinder="Action"]
+ * @returns {Promise}
+ */
+ async function toggleActionMenu(el, menuFinder = "Action") {
+ const dropdown = findItem(getNode(el), `.o_cp_action_menus button`, menuFinder);
+ await click(dropdown);
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function pagerPrevious(el) {
+ await click(getNode(el).querySelector(`.o_pager button.o_pager_previous`));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {Promise}
+ */
+ async function pagerNext(el) {
+ await click(getNode(el).querySelector(`.o_pager button.o_pager_next`));
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {string}
+ */
+ function getPagerValue(el) {
+ const pagerValue = getNode(el).querySelector(`.o_pager_counter .o_pager_value`);
+ switch (pagerValue.tagName) {
+ case 'INPUT':
+ return pagerValue.value;
+ case 'SPAN':
+ return pagerValue.innerText.trim();
+ }
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @returns {string}
+ */
+ function getPagerSize(el) {
+ return getNode(el).querySelector(`.o_pager_counter span.o_pager_limit`).innerText.trim();
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {string} value
+ * @returns {Promise}
+ */
+ async function setPagerValue(el, value) {
+ let pagerValue = getNode(el).querySelector(`.o_pager_counter .o_pager_value`);
+ if (pagerValue.tagName === 'SPAN') {
+ await click(pagerValue);
+ }
+ pagerValue = getNode(el).querySelector(`.o_pager_counter input.o_pager_value`);
+ if (!pagerValue) {
+ throw new Error("Pager value is being edited and cannot be changed.");
+ }
+ await editAndTrigger(pagerValue, value, ['change', 'blur']);
+ }
+
+ /**
+ * @param {EventTarget} el
+ * @param {string} viewType
+ * @returns {Promise}
+ */
+ async function switchView(el, viewType) {
+ await click(getNode(el).querySelector(`button.o_switch_view.o_${viewType}`));
+ }
+
+ return {
+ // Generic interactions
+ toggleMenu,
+ toggleMenuItem,
+ toggleMenuItemOption,
+ isItemSelected,
+ isOptionSelected,
+ getMenuItemTexts,
+ // Button interactions
+ getButtons,
+ // FilterMenu interactions
+ toggleFilterMenu,
+ toggleAddCustomFilter,
+ applyFilter,
+ // GroupByMenu interactions
+ toggleGroupByMenu,
+ toggleAddCustomGroup,
+ selectGroup,
+ applyGroup,
+ // FavoriteMenu interactions
+ toggleFavoriteMenu,
+ toggleSaveFavorite,
+ editFavoriteName,
+ saveFavorite,
+ deleteFavorite,
+ // TimeRangeMenu interactions
+ toggleTimeRangeMenu,
+ selectField,
+ selectRange,
+ selectComparisonRange,
+ toggleTimeRangeMenuBox,
+ applyTimeRange,
+ // SearchBar interactions
+ getFacetTexts,
+ removeFacet,
+ editSearch,
+ validateSearch,
+ // Action menus interactions
+ toggleActionMenu,
+ // Pager interactions
+ pagerPrevious,
+ pagerNext,
+ getPagerValue,
+ getPagerSize,
+ setPagerValue,
+ // View switcher
+ switchView,
+ };
+});
diff --git a/addons/web/static/tests/helpers/test_utils_create.js b/addons/web/static/tests/helpers/test_utils_create.js
index 21bc6cc7850..8130e74ba03 100644
--- a/addons/web/static/tests/helpers/test_utils_create.js
+++ b/addons/web/static/tests/helpers/test_utils_create.js
@@ -11,14 +11,23 @@ odoo.define('web.test_utils_create', function (require) {
*/
const ActionManager = require('web.ActionManager');
+ const ActionMenus = require('web.ActionMenus');
const concurrency = require('web.concurrency');
const config = require('web.config');
- const ControlPanelView = require('web.ControlPanelView');
+ const ControlPanel = require('web.ControlPanel');
+ const ControlPanelModel = require('web.ControlPanelModel');
+ const customHooks = require('web.custom_hooks');
const DebugManager = require('web.DebugManager.Backend');
const dom = require('web.dom');
+ const makeTestEnvironment = require('web.test_env');
+ const Registry = require('web.Registry');
const testUtilsMock = require('web.test_utils_mock');
const Widget = require('web.Widget');
+ const { Component } = owl;
+ const { useRef, useState } = owl.hooks;
+ const { xml } = owl.tags;
+
/**
* Create and return an instance of ActionManager with all rpcs going through a
* mock method using the data, actions and archs objects as sources.
@@ -50,13 +59,16 @@ odoo.define('web.test_utils_create', function (require) {
return this._super(...arguments);
},
});
- testUtilsMock.addMockEnvironment(widget, Object.assign({ debounce: false }, params));
+ const mockServer = await testUtilsMock.addMockEnvironment(widget, Object.assign({ debounce: false }, params));
await widget.prependTo(target);
widget.el.classList.add('o_web_client');
if (config.device.isMobile) {
widget.el.classList.add('o_touch_device');
}
+ params.server = mockServer;
+ Component.env = testUtilsMock.getMockedOwlEnv(params);
+
const userContext = params.context && params.context.user_context || {};
const actionManager = new ActionManager(widget, userContext);
@@ -106,86 +118,116 @@ odoo.define('web.test_utils_create', function (require) {
}
/**
- * Create a controlPanel from various parameters.
- *
- * It returns an instance of ControlPanelController
- *
+ * Create a simple component environment with a basic Parent component, an
+ * extensible env and a mocked server. The returned value is the instance of
+ * the given constructor.
+ * @param {class} constructor Component class to instantiate
+ * @param {Object} [params = {}]
+ * @param {boolean} [params.debug]
+ * @param {Object} [params.env]
+ * @param {Object} [params.intercepts] object in which the keys represent the
+ * intercepted event names and the values are their callbacks.
+ * @param {Object} [params.props]
+ * @returns {Promise<Component>} instance of `constructor`
+ */
+ async function createComponent(constructor, params = {}) {
+ if (!constructor) {
+ throw new Error(`Missing argument "constructor".`);
+ }
+ if (!(constructor.prototype instanceof Component)) {
+ throw new Error(`Argument "constructor" must be an Owl Component.`);
+ }
+ const env = Object.assign(testUtilsMock.getMockedOwlEnv(params), params.env);
+ class Parent extends Component {
+ constructor() {
+ super(...arguments);
+ this.Component = constructor;
+ this.state = useState(params.props || {});
+ this.component = useRef('component');
+ for (const eventName in params.intercepts || {}) {
+ customHooks.useListener(eventName, params.intercepts[eventName]);
+ }
+ }
+ }
+ Parent.env = env;
+ Parent.template = xml`<t t-component="Component" t-props="state" t-ref="component"/>`;
+ const parent = new Parent();
+ await parent.mount(prepareTarget(params.debug), { position: 'first-child' });
+ const child = parent.component.comp;
+ const originalDestroy = child.destroy;
+ child.destroy = function () {
+ child.destroy = originalDestroy;
+ parent.destroy();
+ };
+ return child;
+ }
+
+ /**
+ * Create a Control Panel instance, with an extensible environment and
+ * its related Control Panel Model. Event interception is done through
+ * params['get-controller-query-params'] and params.search, for the two
+ * available event handlers respectively.
* @param {Object} [params={}]
- * @param {Object} [params.action={}]
- * @param {Object} [params.context={}]
- * @param {string} [params.domain=[]]
- * @param {integer} [params.fieldDebounce=0] the debounce value to use for the
- * duration of the test.
- * @param {Object} params.intercepts an object with event names as key, and
- * callback as value. Each key,value will be used to intercept the event.
- * 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
- * @param {string} [params.modelName]
- * @param {string[]} [params.searchMenuTypes = ['filter', 'groupBy', 'favorite']]
- * determines search menus displayed.
- * @param {string} [params.template] the QWeb template to render
- * @param {Object} [params.viewInfo={arch: '<controlpanel/>', fields: {}}]
- a controlpanel (or search) fieldsview
- * @param {string} [params.viewInfo.arch]
- * @param {boolean} [params.context.no_breadcrumbs=false] if set to true,
- * breadcrumbs won't be rendered
- * @param {boolean} [params.withBreadcrumbs=true] if set to false,
- * breadcrumbs won't be rendered
- * @param {boolean} [params.withSearchBar=true] if set to false, no default
- * search bar will be rendered
- *
- * @returns {Promise<ControlPanel>} resolves with an instance of the ControlPanelController
+ * @param {Object} [params.cpProps]
+ * @param {Object} [params.cpStoreConfig]
+ * @param {boolean} [params.debug]
+ * @param {Object} [params.env]
+ * @returns {Object} useful control panel testing elements:
+ * - controlPanel: the control panel instance
+ * - el: the control panel HTML element
+ * - helpers: a suite of bound helpers (see above functions for all
+ * available helpers)
*/
- async function createControlPanel(params) {
- params = params || {};
- const target = prepareTarget(params.debug);
- // reproduce the DOM environment of a view control panel
- const $webClient = $('<div>').addClass('o_web_client').prependTo(target);
- const $actionManager = $('<div>').addClass('o_action_manager').appendTo($webClient);
- const $action = $('<div>').addClass('o_action').appendTo($actionManager);
+ async function createControlPanel(params = {}) {
+ const config = params.cpStoreConfig || {};
+ const debug = params.debug || false;
+ const env = params.env || {};
+ const props = Object.assign({
+ action: {},
+ fields: {},
+ }, params.cpProps);
- const widget = new Widget();
- // add mock environment: mock server, session, fieldviewget, ...
- const mockServer = testUtilsMock.addMockEnvironment(widget, params);
- if (!params.viewInfo) {
- try {
- params.viewInfo = testUtilsMock.fieldsViewGet(mockServer, params);
- } catch (e) {
- // if an error occurs we keep params.viewInfo undefined.
- // It will be set to {arch: '<controlpanel/>', fields: {}} in
- // ControlPanelView init function.
+ class Parent extends Component {
+ constructor() {
+ super();
+ config.env = this.env;
+ this._controlPanelModel = new ControlPanelModel(config);
+ this.state = useState(props);
+ this.controlPanel = useRef("controlPanel");
+ }
+ async willStart() {
+ await this._controlPanelModel.isReady;
+ }
+ mounted() {
+ if (params['get-controller-query-params']) {
+ this._controlPanelModel.on('get-controller-query-params', this,
+ params['get-controller-query-params']);
+ }
+ if (params.search) {
+ this._controlPanelModel.on('search', this, params.search);
+ }
}
}
+ Parent.components = { ControlPanel };
+ Parent.env = makeTestEnvironment(env);
+ Parent.template = xml`
+ <ControlPanel
+ t-ref="controlPanel"
+ t-props="state"
+ controlPanelModel="_controlPanelModel"
+ />`;
- const viewOptions = Object.assign({
- context: params.context,
- modelName: params.model,
- searchMenuTypes: params.searchMenuTypes,
- viewInfo: params.viewInfo,
- }, params.viewOptions);
- const controlPanelView = new ControlPanelView(viewOptions);
- const controlPanel = await controlPanelView.getController(widget);
- // override the controlPanel's 'destroy' so that it calls 'destroy' on
- // the widget instead, as the widget is the parent of the controlPanel
- // and the mockServer.
- controlPanel.__destroy = controlPanel.destroy;
+ const parent = new Parent();
+ await parent.mount(prepareTarget(debug), { position: 'first-child' });
+
+ const controlPanel = parent.controlPanel.comp;
+ const destroy = controlPanel.destroy;
controlPanel.destroy = function () {
- // remove the override to properly destroy the controlPanel and its
- // children when it will be called the second time (by its parent)
- delete controlPanel.destroy;
- widget.destroy();
- $webClient.remove();
+ controlPanel.destroy = destroy;
+ parent.destroy();
};
+ controlPanel.getQuery = () => parent._controlPanelModel.getQuery();
- // render the controlPanel in a fragment as it must be able to render
- // correctly without being in the DOM
- const fragment = document.createDocumentFragment();
- await controlPanel.appendTo(fragment);
- dom.prepend($action, fragment, {
- callbacks: [{ widget: controlPanel }],
- in_DOM: true,
- });
return controlPanel;
}
@@ -279,6 +321,7 @@ odoo.define('web.test_utils_create', function (require) {
* @param {any[]} [params.domain] the initial domain for the view
* @param {Object} [params.context] the initial context for the view
* @param {string[]} [params.groupBy] the initial groupBy for the view
+ * @param {Object[]} [params.favoriteFilters] the favorite filters one would like to have at initialization
* @param {integer} [params.fieldDebounce=0] the debounce value to use for the
* duration of the test.
* @param {AbstractView} params.View the class that will be instantiated
@@ -306,18 +349,29 @@ odoo.define('web.test_utils_create', function (require) {
webClient.append(actionManager);
// add mock environment: mock server, session, fieldviewget, ...
- const mockServer = testUtilsMock.addMockEnvironment(widget, params);
+ const mockServer = await testUtilsMock.addMockEnvironment(widget, params);
const viewInfo = testUtilsMock.fieldsViewGet(mockServer, params);
+ params.server = mockServer;
+ const env = Object.assign(testUtilsMock.getMockedOwlEnv(params));
+ Component.env = env;
+
// create the view
const View = params.View;
+ const modelName = params.model || 'foo';
+ const defaultAction = {
+ res_model: modelName,
+ context: {},
+ };
const viewOptions = Object.assign({
- modelName: params.model || 'foo',
+ action: Object.assign(defaultAction, params.action),
+ view: viewInfo,
+ modelName: modelName,
ids: 'res_id' in params ? [params.res_id] : undefined,
currentId: 'res_id' in params ? params.res_id : undefined,
domain: params.domain || [],
context: params.context || {},
- hasSidebar: false,
+ hasActionMenus: false,
}, params.viewOptions);
// patch the View to handle the groupBy given in params, as we can't give it
// in init (unlike the domain and context which can be set in the action)
@@ -335,16 +389,14 @@ odoo.define('web.test_utils_create', function (require) {
let view;
if (viewInfo.type === 'controlpanel' || viewInfo.type === 'search') {
// TODO: probably needs to create an helper just for that
- view = new params.View({
- viewInfo: viewInfo,
- modelName: params.model || 'foo',
- });
+ view = new params.View({ viewInfo, modelName });
} else {
- viewOptions.controlPanelFieldsView = testUtilsMock.fieldsViewGet(mockServer, {
+ viewOptions.controlPanelFieldsView = Object.assign(testUtilsMock.fieldsViewGet(mockServer, {
arch: params.archs && params.archs[params.model + ',false,search'] || '<search/>',
fields: viewInfo.fields,
model: params.model,
- });
+ }), { favoriteFilters: params.favoriteFilters });
+
view = new params.View(viewInfo, viewOptions);
}
@@ -354,6 +406,12 @@ odoo.define('web.test_utils_create', function (require) {
}
}
+ // Override the ActionMenus registry unless told otherwise.
+ let actionMenusRegistry = ActionMenus.registry;
+ if (params.actionMenusRegistry !== true) {
+ ActionMenus.registry = new Registry();
+ }
+
const viewController = await view.getController(widget);
// override the view's 'destroy' so that it calls 'destroy' on the widget
// instead, as the widget is the parent of the view and the mockServer.
@@ -364,6 +422,9 @@ odoo.define('web.test_utils_create', function (require) {
delete viewController.destroy;
widget.destroy();
webClient.remove();
+ if (params.actionMenusRegistry !== true) {
+ ActionMenus.registry = actionMenusRegistry;
+ }
};
// render the viewController in a fragment as they must be able to render correctly
@@ -401,6 +462,7 @@ odoo.define('web.test_utils_create', function (require) {
return {
createActionManager,
createCalendarView,
+ createComponent,
createControlPanel,
createDebugManager,
createModel,
diff --git a/addons/web/static/tests/helpers/test_utils_dom.js b/addons/web/static/tests/helpers/test_utils_dom.js
index ceca0d21fc9..8a7d3553d9e 100644
--- a/addons/web/static/tests/helpers/test_utils_dom.js
+++ b/addons/web/static/tests/helpers/test_utils_dom.js
@@ -2,6 +2,7 @@ odoo.define('web.test_utils_dom', function (require) {
"use strict";
const concurrency = require('web.concurrency');
+ const Widget = require('web.Widget');
/**
* DOM Test Utils
@@ -68,6 +69,11 @@ odoo.define('web.test_utils_dom', function (require) {
dragleave: { constructor: DragEvent, processParameters: onlyBubble },
dragover: { constructor: DragEvent, processParameters: onlyBubble },
drop: { constructor: DragEvent, processParameters: onlyBubble },
+
+ input: { constructor: InputEvent, processParameters: onlyBubble },
+
+ compositionstart: { constructor: CompositionEvent, processParameters: onlyBubble },
+ compositionend: { constructor: CompositionEvent, processParameters: onlyBubble },
};
/**
@@ -285,6 +291,44 @@ odoo.define('web.test_utils_dom', function (require) {
}
/**
+ * Helper function used to extract an HTML EventTarget element from a given
+ * target. The extracted element will depend on the target type:
+ * - Component|Widget -> el
+ * - jQuery -> associated element (must have 1)
+ * - HTMLCollection (or similar) -> first element (must have 1)
+ * - string -> result of document.querySelector with string
+ * - else -> as is
+ * @private
+ * @param {(Component|Widget|jQuery|HTMLCollection|HTMLElement|string)} target
+ * @returns {EventTarget}
+ */
+ function getNode(target) {
+ let nodes;
+ if (target instanceof owl.Component || target instanceof Widget) {
+ nodes = [target.el];
+ } else if (typeof target === 'string') {
+ nodes = document.querySelectorAll(target);
+ } else if (target === jQuery) { // jQuery (or $)
+ nodes = [document.body];
+ } else if (target.length) { // jQuery instance, HTMLCollection or array
+ nodes = target;
+ } else {
+ nodes = [target];
+ }
+ if (nodes.length !== 1) {
+ throw new Error(`Found ${nodes.length} nodes instead of 1.`);
+ }
+ const node = nodes[0];
+ if (!node) {
+ throw new Error(`Expected a node and got ${node}.`);
+ }
+ if (!_isEventTarget(node)) {
+ throw new Error(`Expected node to be an instance of EventTarget and got ${node.constructor.name}.`);
+ }
+ return node;
+ }
+
+ /**
* Open the datepicker of a given element.
*
* @param {jQuery} $datepickerEl element to which a datepicker is attached
@@ -453,6 +497,7 @@ odoo.define('web.test_utils_dom', function (require) {
clickFirst,
clickLast,
dragAndDrop,
+ getNode,
openDatepicker,
returnAfterNextAnimationFrame,
triggerEvent,
diff --git a/addons/web/static/tests/helpers/test_utils_mock.js b/addons/web/static/tests/helpers/test_utils_mock.js
index bb2d1ef24a5..45b5dd6abcf 100644
--- a/addons/web/static/tests/helpers/test_utils_mock.js
+++ b/addons/web/static/tests/helpers/test_utils_mock.js
@@ -125,11 +125,11 @@ function removeSrcAttribute(el, widget) {
* @param {boolean} [throttle=false] set to true to keep the throttling, which
* is completely removed by default.
*
- * @returns {MockServer} the instance of the mock server, created by this
+ * @returns {Promise<MockServer>} the instance of the mock server, created by this
* function. It is necessary for createView so that method can call some
* other methods on it.
*/
-function addMockEnvironment(widget, params) {
+async function addMockEnvironment(widget, params) {
var Server = MockServer;
params.services = params.services || {};
if (params.mockRPC) {
@@ -287,6 +287,9 @@ function addMockEnvironment(widget, params) {
views = _.mapObject(views, function (viewParams) {
return fieldsViewGet(mockServer, viewParams);
});
+ if ('search' in views && params.favoriteFilters) {
+ views.search.favoriteFilters = params.favoriteFilters;
+ }
event.data.on_success(views);
});
});
@@ -312,6 +315,7 @@ function addMockEnvironment(widget, params) {
// Deploy services
var done = false;
var servicesToDeploy = _.clone(params.services);
+ const servicePromises = [];
if (!servicesToDeploy.ajax) {
services.ajax = null; // use mocked ajax from mocked server
}
@@ -329,8 +333,7 @@ function addMockEnvironment(widget, params) {
intercept(service, "get_session", function (event) {
event.data.callback(session);
});
-
- service.start();
+ servicePromises.push(service.start());
} else {
var serviceNames = _.keys(servicesToDeploy);
if (serviceNames.length) {
@@ -340,6 +343,8 @@ function addMockEnvironment(widget, params) {
}
}
+ await Promise.all(servicePromises);
+
return mockServer;
}
@@ -368,21 +373,28 @@ function fieldsViewGet(server, params) {
* @param {Object} [params.archs] this archs given to the mock server
* @param {Object} [params.data] the business data given to the mock server
* @param {boolean} [params.debug]
+ * @param {Object} [params.env]
* @param {function} [params.mockRPC]
+ * @param {function} [params.server] an already instantiated server to avoid
+ * creating another one.
* @returns {Object}
*/
-function getMockedOwlEnv(params) {
- params = params || {};
- let Server = MockServer;
- if (params.mockRPC) {
- Server = MockServer.extend({ _performRpc: params.mockRPC });
+function getMockedOwlEnv(params = {}) {
+ let server;
+ if (params.server) {
+ server = params.server;
+ } else {
+ let Server = MockServer;
+ if (params.mockRPC) {
+ Server = MockServer.extend({ _performRpc: params.mockRPC });
+ }
+ server = new Server(params.data, {
+ actions: params.actions,
+ archs: params.archs,
+ debug: params.debug,
+ });
}
- const server = new Server(params.data, {
- actions: params.actions,
- archs: params.archs,
- debug: params.debug,
- });
- const env = {
+ const env = Object.assign({
dataManager: {
load_action: (actionID, context) => {
return server.performRpc('/web/action/load', {
@@ -416,7 +428,7 @@ function getMockedOwlEnv(params) {
},
},
session: params.session || {},
- };
+ }, params.env);
return makeTestEnvironment(env, server.performRpc.bind(server));
}