').addClass('o_web_client').prependTo(target);
- const $actionManager = $('
').addClass('o_action_manager').appendTo($webClient);
- const $action = $('
').addClass('o_action').appendTo($actionManager);
-
- 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: '', fields: {}} in
- // ControlPanelView init function.
+ 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]);
+ }
}
}
-
- 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;
- 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();
+ Parent.env = env;
+ Parent.template = xml``;
+ 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.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 = {}) {
+ const config = params.cpStoreConfig || {};
+ const debug = params.debug || false;
+ const env = params.env || {};
+ const props = Object.assign({
+ action: {},
+ fields: {},
+ }, params.cpProps);
+
+ 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`
+ `;
+
+ const parent = new Parent();
+ await parent.mount(prepareTarget(debug), { position: 'first-child' });
+
+ const controlPanel = parent.controlPanel.comp;
+ const destroy = controlPanel.destroy;
+ controlPanel.destroy = function () {
+ 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'] || '',
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 },
};
/**
@@ -284,6 +290,44 @@ odoo.define('web.test_utils_dom', function (require) {
return returnAfterNextAnimationFrame();
}
+ /**
+ * 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.
*
@@ -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} 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));
}
diff --git a/addons/web/static/tests/main_tests.js b/addons/web/static/tests/main_tests.js
index 079a14573d2..97d9b051bbb 100644
--- a/addons/web/static/tests/main_tests.js
+++ b/addons/web/static/tests/main_tests.js
@@ -5,6 +5,8 @@ odoo.define('web.web_client', async function (require) {
const session = require("web.session");
const WebClient = require('web.WebClient');
+ owl.config.mode = "dev";
+
const webClient = new WebClient();
await session.is_bound;
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index 369b7cbe836..6111f0e5732 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -638,6 +638,7 @@
+
diff --git a/addons/web_tour/static/src/js/tour_step_utils.js b/addons/web_tour/static/src/js/tour_step_utils.js
index cd92f6fe019..2c3b7726dac 100644
--- a/addons/web_tour/static/src/js/tour_step_utils.js
+++ b/addons/web_tour/static/src/js/tour_step_utils.js
@@ -121,15 +121,15 @@ return core.Class.extend({
extra_trigger: '.modal:not(.o_inactive_modal) .dropdown-menu.o_searchview_autocomplete',
position: 'bottom',
run: action => {
- const keyboardEventUp = new KeyboardEvent('keyup', {
+ const keyEventEnter = new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
key: 'Enter',
code: 'Enter',
which: 13,
- keyCode: 13
+ keyCode: 13,
});
- action.tip_widget.$anchor[0].dispatchEvent(keyboardEventUp);
+ action.tip_widget.$anchor[0].dispatchEvent(keyEventEnter);
},
debugHelp: this._getHelpMessage('simulateEnterKeyboardInSearchModal'),
};