[IMP] web: tests: backport of new helpers
This rev. is a partial backport of abf32b8b, so that the newly
introduced helpers are already available in 12.0.
closes odoo/odoo#28827
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,288 @@
|
||||
odoo.define('web.test_utils_create', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Create Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help creating mock widgets
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
var ActionManager = require('web.ActionManager');
|
||||
var config = require('web.config');
|
||||
var ControlPanel = require('web.ControlPanel');
|
||||
var dom = require('web.dom');
|
||||
var DebugManager = require('web.DebugManager');
|
||||
var testUtilsMock = require('web.test_utils_mock');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
/**
|
||||
* create and return an instance of ActionManager with all rpcs going through a
|
||||
* mock method using the data, actions and archs objects as sources.
|
||||
*
|
||||
* @param {Object} [params]
|
||||
* @param {Object} [params.actions] the actions given to the mock server
|
||||
* @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 {function} [params.mockRPC]
|
||||
* @returns {ActionManager}
|
||||
*/
|
||||
function createActionManager (params) {
|
||||
params = params || {};
|
||||
var $target = $('#qunit-fixture');
|
||||
if (params.debug) {
|
||||
$target = $('body');
|
||||
$target.addClass('debug');
|
||||
}
|
||||
|
||||
var widget = new Widget();
|
||||
// when 'document' addon is installed, the sidebar does a 'search_read' on
|
||||
// model 'ir_attachment' each time a record is open, so we monkey-patch
|
||||
// 'mockRPC' to mute those RPCs, so that the tests can be written uniformly,
|
||||
// whether or not 'document' is installed
|
||||
var mockRPC = params.mockRPC;
|
||||
_.extend(params, {
|
||||
mockRPC: function (route, args) {
|
||||
if (args.model === 'ir.attachment') {
|
||||
return $.when([]);
|
||||
}
|
||||
if (mockRPC) {
|
||||
return mockRPC.apply(this, arguments);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
testUtilsMock.addMockEnvironment(widget, _.defaults(params, { debounce: false }));
|
||||
widget.prependTo($target);
|
||||
widget.$el.addClass('o_web_client');
|
||||
if (config.device.isMobile) {
|
||||
widget.$el.addClass('o_touch_device');
|
||||
}
|
||||
|
||||
var userContext = params.context && params.context.user_context || {};
|
||||
var actionManager = new ActionManager(widget, userContext);
|
||||
|
||||
var originalDestroy = ActionManager.prototype.destroy;
|
||||
actionManager.destroy = function () {
|
||||
actionManager.destroy = originalDestroy;
|
||||
widget.destroy();
|
||||
};
|
||||
actionManager.appendTo(widget.$el);
|
||||
|
||||
return actionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* create a view from various parameters. Here, a view means a javascript
|
||||
* instance of an AbstractView class, such as a form view, a list view or a
|
||||
* kanban view.
|
||||
*
|
||||
* It returns the instance of the view, properly created, with all rpcs going
|
||||
* 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
|
||||
* @param {Object} [params.context] the initial context for the view
|
||||
* @param {Object} [params.debug=false] if true, the widget will be appended in
|
||||
* the DOM. Also, RPCs and uncaught OdooEvent will be logged
|
||||
* @param {string[]} [params.groupBy] the initial groupBy for the view
|
||||
* @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
|
||||
* @param {string} params.model a model name, will be given to the view
|
||||
* @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
|
||||
* @returns {Deferred<AbstractView>} resolves with the instance of the view
|
||||
*/
|
||||
function createAsyncView(params) {
|
||||
var $target = $('#qunit-fixture');
|
||||
var widget = new Widget();
|
||||
if (params.debug) {
|
||||
$target = $('body');
|
||||
$target.addClass('debug');
|
||||
}
|
||||
|
||||
// add mock environment: mock server, session, fieldviewget, ...
|
||||
var mockServer = testUtilsMock.addMockEnvironment(widget, params);
|
||||
var viewInfo = testUtilsMock.fieldsViewGet(mockServer, params);
|
||||
// create the view
|
||||
var viewOptions = {
|
||||
modelName: params.model || 'foo',
|
||||
ids: 'res_id' in params ? [params.res_id] : undefined,
|
||||
currentId: 'res_id' in params ? params.res_id : undefined,
|
||||
domain: params.domain || [],
|
||||
context: params.context || {},
|
||||
groupBy: params.groupBy || [],
|
||||
};
|
||||
if (params.hasSelectors) {
|
||||
viewOptions.hasSelectors = params.hasSelectors;
|
||||
}
|
||||
|
||||
_.extend(viewOptions, params.viewOptions);
|
||||
|
||||
var view = new params.View(viewInfo, viewOptions);
|
||||
|
||||
// reproduce the DOM environment of views
|
||||
var $web_client = $('<div>').addClass('o_web_client').prependTo($target);
|
||||
var controlPanel = new ControlPanel(widget);
|
||||
controlPanel.appendTo($web_client);
|
||||
var $content = $('<div>').addClass('o_content').appendTo($web_client);
|
||||
|
||||
if (params.interceptsPropagate) {
|
||||
_.each(params.interceptsPropagate, function (cb, name) {
|
||||
testUtilsMock.intercept(widget, name, cb, true);
|
||||
});
|
||||
}
|
||||
|
||||
return view.getController(widget).then(function (view) {
|
||||
// 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.
|
||||
view.__destroy = view.destroy;
|
||||
view.destroy = function () {
|
||||
// remove the override to properly destroy the view and its children
|
||||
// when it will be called the second time (by its parent)
|
||||
delete view.destroy;
|
||||
widget.destroy();
|
||||
};
|
||||
|
||||
// link the view to the control panel
|
||||
view.set_cp_bus(controlPanel.get_bus());
|
||||
|
||||
// render the view in a fragment as they must be able to render correctly
|
||||
// without being in the DOM
|
||||
var fragment = document.createDocumentFragment();
|
||||
return view.appendTo(fragment).then(function () {
|
||||
dom.append($content, fragment, {
|
||||
callbacks: [{ widget: view }],
|
||||
in_DOM: true,
|
||||
});
|
||||
view.$el.on('click', 'a', function (ev) {
|
||||
ev.preventDefault();
|
||||
});
|
||||
|
||||
return view;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return an instance of DebugManager with all rpcs going through a
|
||||
* mock method, assuming that the user has access rights, and is an admin.
|
||||
*
|
||||
* @param {Object} [params={}]
|
||||
*/
|
||||
function createDebugManager (params) {
|
||||
params = params || {};
|
||||
var mockRPC = params.mockRPC;
|
||||
_.extend(params, {
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'check_access_rights') {
|
||||
return $.when(true);
|
||||
}
|
||||
if (args.method === 'xmlid_to_res_id') {
|
||||
return $.when(true);
|
||||
}
|
||||
if (mockRPC) {
|
||||
return mockRPC.apply(this, arguments);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
session: {
|
||||
user_has_group: function (group) {
|
||||
if (group === 'base.group_no_one') {
|
||||
return $.when(true);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
},
|
||||
});
|
||||
var debugManager = new DebugManager();
|
||||
testUtilsMock.addMockEnvironment(debugManager, params);
|
||||
return debugManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* create a model from given parameters.
|
||||
*
|
||||
* @param {Object} params This object will be given to addMockEnvironment, so
|
||||
* any parameters from that method applies
|
||||
* @param {Class} params.Model the model class to use
|
||||
* @returns {Model}
|
||||
*/
|
||||
function createModel(params) {
|
||||
var widget = new Widget();
|
||||
|
||||
var model = new params.Model(widget);
|
||||
|
||||
testUtilsMock.addMockEnvironment(widget, params);
|
||||
|
||||
// override the model's 'destroy' so that it calls 'destroy' on the widget
|
||||
// instead, as the widget is the parent of the model and the mockServer.
|
||||
model.destroy = function () {
|
||||
// remove the override to properly destroy the model when it will be
|
||||
// called the second time (by its parent)
|
||||
delete model.destroy;
|
||||
widget.destroy();
|
||||
};
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* create a widget parent from given parameters.
|
||||
*
|
||||
* @param {Object} params This object will be given to addMockEnvironment, so
|
||||
* any parameters from that method applies
|
||||
* @returns {Widget}
|
||||
*/
|
||||
function createParent(params) {
|
||||
var widget = new Widget();
|
||||
testUtilsMock.addMockEnvironment(widget, 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,
|
||||
createDebugManager: createDebugManager,
|
||||
createModel: createModel,
|
||||
createParent: createParent,
|
||||
createView: createView,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
odoo.define('web.test_utils_fields', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Field Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help testing field widgets.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
var concurrency = require('web.concurrency');
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sets the value of an element and then, trigger all specified events.
|
||||
* Note that this helper also checks the unicity of the target.
|
||||
*
|
||||
* Example:
|
||||
* testUtils.fields.editAndTrigger($('selector'), 'test', ['input', 'change']);
|
||||
*
|
||||
* @param {jQuery} $el should target an input, textarea or select
|
||||
* @param {string|number} value
|
||||
* @param {string[]} events
|
||||
*/
|
||||
function editAndTrigger($el, value, events) {
|
||||
if ($el.length !== 1) {
|
||||
throw new Error(`target ${$el.selector} has length ${$el.length} instead of 1`);
|
||||
}
|
||||
$el.val(value);
|
||||
events.forEach(function (event) {
|
||||
$el.trigger(event);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of an input.
|
||||
*
|
||||
* Note that this helper also checks the unicity of the target.
|
||||
*
|
||||
* Example:
|
||||
* testUtils.fields.editInput($('selector'), 'somevalue');
|
||||
*
|
||||
* @param {jQuery} $el
|
||||
* @param {string|number} value
|
||||
*/
|
||||
function editInput($el, value) {
|
||||
editAndTrigger($el, value, ['input']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of a select.
|
||||
*
|
||||
* Note that this helper also checks the unicity of the target.
|
||||
*
|
||||
* Example:
|
||||
* testUtils.fields.editSelect($('selector'), 'somevalue');
|
||||
*
|
||||
* @param {jQuery} $el
|
||||
* @param {string|number} value
|
||||
*/
|
||||
function editSelect($el, value) {
|
||||
editAndTrigger($el, value, ['change']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click to open the dropdown on a many2one
|
||||
*
|
||||
* @param {string} fieldName
|
||||
* @param {[string]} selector if set, this will restrict the search for the m2o
|
||||
* input
|
||||
* @returns {HTMLInputElement} the main many2one input
|
||||
*/
|
||||
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];
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the active (highlighted) selection in a m2o dropdown.
|
||||
*
|
||||
* @param {string} fieldName
|
||||
* @param {[string]} selector if set, this will restrict the search for the m2o
|
||||
* input
|
||||
*/
|
||||
function clickM2OHighlightedItem(fieldName, selector) {
|
||||
var m2oSelector = `${selector || ''} .o_field_many2one[name=${fieldName}] input`;
|
||||
var $dropdown = $(m2oSelector).autocomplete('widget');
|
||||
// clicking on an li (no matter which one), will select the focussed one
|
||||
$dropdown.find('li:first()').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on a menuitem in the m2o dropdown. This helper will target an element
|
||||
* which contains some specific text. Note that it assumes that the dropdown
|
||||
* is currently open.
|
||||
*
|
||||
* Example:
|
||||
* testUtils.fields.many2one.clickM2OItem('partner_id', 'George');
|
||||
*
|
||||
* @param {string} fieldName
|
||||
* @param {string} searchText
|
||||
*/
|
||||
function clickM2OItem(fieldName, searchText) {
|
||||
var m2oSelector = `.o_field_many2one[name=${fieldName}] input`;
|
||||
var $dropdown = $(m2oSelector).autocomplete('widget');
|
||||
var $target = $dropdown.find(`li:contains(${searchText})`).first();
|
||||
if ($target.length !== 1 || !$target.is(':visible')) {
|
||||
throw new Error('Menu item should be unique and visible');
|
||||
}
|
||||
$target.mouseenter().click();
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper is useful to test many2one fields. Here is what it does:
|
||||
* - click to open the dropdown
|
||||
* - enter a search string in the input
|
||||
* - wait for the selection
|
||||
* - click on the active menuitem
|
||||
*
|
||||
* Example:
|
||||
* testUtils.fields.many2one.searchAndClickM2OItem('partner_id', 'George');
|
||||
*
|
||||
* @param {string} fieldName
|
||||
* @param {[Object]} options
|
||||
* @param {[string]} options.selector
|
||||
* @param {[string]} options.search
|
||||
* @returns {Promise}
|
||||
*/
|
||||
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 $.when(def).then(function () {
|
||||
clickM2OHighlightedItem(fieldName, options.selector);
|
||||
});
|
||||
}
|
||||
return {
|
||||
clickOpenM2ODropdown: clickOpenM2ODropdown,
|
||||
clickM2OHighlightedItem: clickM2OHighlightedItem,
|
||||
clickM2OItem: clickM2OItem,
|
||||
editAndTrigger: editAndTrigger,
|
||||
editInput: editInput,
|
||||
editSelect: editSelect,
|
||||
searchAndClickM2OItem: searchAndClickM2OItem,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
odoo.define('web.test_utils_form', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Form Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help test form views.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
var testUtilsDom = require('web.test_utils_dom');
|
||||
|
||||
/**
|
||||
* Clicks on the Edit button in a form view, to set it to edit mode. Note that
|
||||
* it checks that the button is visible, so calling this method in edit mode
|
||||
* will fail.
|
||||
*
|
||||
* @param {FormController} form
|
||||
*/
|
||||
function clickEdit(form) {
|
||||
testUtilsDom.click(form.$buttons.find('.o_form_button_edit'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the Save button in a form view. Note that this method checks that
|
||||
* the Save button is visible.
|
||||
*
|
||||
* @param {FormController} form
|
||||
*/
|
||||
function clickSave(form) {
|
||||
testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the Create button in a form view. Note that this method checks that
|
||||
* the Create button is visible.
|
||||
*
|
||||
* @param {FormController} form
|
||||
*/
|
||||
function clickCreate(form) {
|
||||
testUtilsDom.click(form.$buttons.find('.o_form_button_create'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks on the Discard button in a form view. Note that this method checks that
|
||||
* the Discard button is visible.
|
||||
*
|
||||
* @param {FormController} form
|
||||
*/
|
||||
function clickDiscard(form) {
|
||||
testUtilsDom.click(form.$buttons.find('.o_form_button_cancel'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads a form view.
|
||||
*
|
||||
* @param {FormController} form
|
||||
* @param {[Object]} params given to the controller reload method
|
||||
*/
|
||||
function reload(form, params) {
|
||||
form.reload(params);
|
||||
}
|
||||
|
||||
return {
|
||||
clickEdit: clickEdit,
|
||||
clickSave: clickSave,
|
||||
clickCreate: clickCreate,
|
||||
clickDiscard: clickDiscard,
|
||||
reload: reload,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
odoo.define('web.test_utils_graph', function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Graph Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help test graph views.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Reloads a graph view.
|
||||
*
|
||||
* @param {GraphController} graph
|
||||
* @param {[Object]} params given to the controller reload method
|
||||
*/
|
||||
function reload(graph, params) {
|
||||
graph.reload(params);
|
||||
}
|
||||
|
||||
return {
|
||||
reload: reload,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
odoo.define('web.test_utils_kanban', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Kanban Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help testing kanban views.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
var testUtilsDom = require('web.test_utils_dom');
|
||||
var testUtilsFields = require('web.test_utils_fields');
|
||||
|
||||
/**
|
||||
* Clicks on the Create button in a kanban view. Note that this method checks that
|
||||
* the Create button is visible.
|
||||
*
|
||||
* @param {KanbanController} kanban
|
||||
*/
|
||||
function clickCreate(kanban) {
|
||||
testUtilsDom.click(kanban.$buttons.find('.o-kanban-button-new'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the settings menu for a column (in a grouped kanban view)
|
||||
*
|
||||
* @param {jQuery} $column
|
||||
*/
|
||||
function toggleGroupSettings($column) {
|
||||
var $dropdownToggler = $column.find('.o_kanban_config > a.dropdown-toggle');
|
||||
if (!$dropdownToggler.is(':visible')) {
|
||||
$dropdownToggler.css('display', 'block');
|
||||
}
|
||||
testUtilsDom.click($dropdownToggler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a value in a quickcreate form view (this method assumes that the quick
|
||||
* create feature is active, and a sub form view is open)
|
||||
*
|
||||
* @param {kanbanController} kanban
|
||||
* @param {string|number} value
|
||||
* @param {[string]} fieldName
|
||||
*/
|
||||
function quickCreate(kanban, value, fieldName) {
|
||||
var additionalSelector = fieldName ? ('[name=' + fieldName + ']'): '';
|
||||
var enterEvent = $.Event(
|
||||
'keydown',
|
||||
{
|
||||
which: $.ui.keyCode.ENTER,
|
||||
keyCode: $.ui.keyCode.ENTER,
|
||||
}
|
||||
);
|
||||
testUtilsFields.editAndTrigger(
|
||||
kanban.$('.o_kanban_quick_create input' + additionalSelector),
|
||||
value,
|
||||
['input', enterEvent]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads a kanban view.
|
||||
*
|
||||
* @param {KanbanController} kanban
|
||||
* @param {[Object]} params given to the controller reload method
|
||||
*/
|
||||
function reload(kanban, params) {
|
||||
kanban.reload(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the setting dropdown of a kanban record. Note that the template of a
|
||||
* kanban record is not standardized, so this method will fail if the template
|
||||
* does not comply with the usual dom structure.
|
||||
*
|
||||
* @param {jQuery} $record
|
||||
*/
|
||||
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 {
|
||||
clickCreate: clickCreate,
|
||||
quickCreate: quickCreate,
|
||||
reload: reload,
|
||||
toggleGroupSettings: toggleGroupSettings,
|
||||
toggleRecordDropdown: toggleRecordDropdown,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -0,0 +1,545 @@
|
||||
odoo.define('web.test_utils_mock', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Mock Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help mocking data.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
var basic_fields = require('web.basic_fields');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var MockServer = require('web.MockServer');
|
||||
var session = require('web.session');
|
||||
|
||||
var DebouncedField = basic_fields.DebouncedField;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Private functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* logs all event going through the target widget.
|
||||
*
|
||||
* @param {Widget} widget
|
||||
*/
|
||||
function _observe(widget) {
|
||||
var _trigger_up = widget._trigger_up.bind(widget);
|
||||
widget._trigger_up = function (event) {
|
||||
console.log('%c[event] ' + event.name, 'color: blue; font-weight: bold;', event);
|
||||
_trigger_up(event);
|
||||
};
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Removes the src attribute on images and iframes to prevent not found errors,
|
||||
* and optionally triggers an rpc with the src url as route on a widget.
|
||||
* This method is critical and must be fastest (=> no jQuery, no underscore)
|
||||
*
|
||||
* @param {DOM Node} el
|
||||
* @param {[Widget]} widget the widget on which the rpc should be performed
|
||||
*/
|
||||
function removeSrcAttribute(el, widget) {
|
||||
var nodes;
|
||||
if (el.nodeName === 'IMG' || el.nodeName === 'IFRAME') {
|
||||
nodes = [el];
|
||||
} else {
|
||||
nodes = Array.prototype.slice.call(el.getElementsByTagName('img'))
|
||||
.concat(Array.prototype.slice.call(el.getElementsByTagName('iframe')));
|
||||
}
|
||||
var node;
|
||||
while (node = nodes.pop()) {
|
||||
var src = node.attributes.src && node.attributes.src.value;
|
||||
if (src && src !== 'about:blank') {
|
||||
node.setAttribute('data-src', src);
|
||||
if (node.nodeName === 'IMG') {
|
||||
node.attributes.removeNamedItem('src');
|
||||
} else {
|
||||
node.setAttribute('src', 'about:blank');
|
||||
}
|
||||
if (widget) {
|
||||
widget._rpc({ route: src });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a mock environment to a widget. This helper function can simulate
|
||||
* various kind of side effects, such as mocking RPCs, changing the session,
|
||||
* or the translation settings.
|
||||
*
|
||||
* The simulated environment lasts for the lifecycle of the widget, meaning it
|
||||
* disappears when the widget is destroyed. It is particularly relevant for the
|
||||
* session mocks, because the previous session is restored during the destroy
|
||||
* call. So, it means that you have to be careful and make sure that it is
|
||||
* properly destroyed before another test is run, otherwise you risk having
|
||||
* interferences between tests.
|
||||
*
|
||||
* @param {Widget} widget
|
||||
* @param {Object} params
|
||||
* @param {Object} [params.archs] a map of string [model,view_id,view_type] to
|
||||
* a arch object. It is used to mock answers to 'load_views' custom events.
|
||||
* This is useful when the widget instantiate a formview dialog that needs
|
||||
* to load a particular arch.
|
||||
* @param {string} [params.currentDate] a string representation of the current
|
||||
* date. It is given to the mock server.
|
||||
* @param {Object} params.data the data given to the created mock server. It is
|
||||
* used to generate mock answers for every kind of routes supported by odoo
|
||||
* @param {number} [params.debug] if set to true, logs RPCs and uncaught Odoo
|
||||
* events.
|
||||
* @param {function} [params.mockRPC] a function that will be used to override
|
||||
* the _performRpc method from the mock server. It is really useful to add
|
||||
* some custom rpc mocks, or to check some assertions.
|
||||
* @param {Object} [params.session] if it is given, it will be used as answer
|
||||
* for all calls to this.getSession() by the widget, of its children. Also,
|
||||
* it will be used to extend the current, real session. This side effect is
|
||||
* undone when the widget is destroyed.
|
||||
* @param {Object} [params.translateParameters] if given, it will be used to
|
||||
* extend the core._t.database.parameters object. After the widget
|
||||
* destruction, the original parameters will be restored.
|
||||
* @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. Some events ('call_service', "load_views",
|
||||
* "get_session", "load_filters") have a special treatment beforehand.
|
||||
* @param {Object} [params.services={}] list of services to load in
|
||||
* addition to the ajax service. For instance, if a test needs the local
|
||||
* storage service in order to work, it can provide a mock version of it.
|
||||
* @param {boolean} [debounce=true] set to false to completely remove the
|
||||
* debouncing, forcing the handler to be called directly (not on the next
|
||||
* execution stack, like it does with delay=0).
|
||||
* @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
|
||||
* function. It is necessary for createAsyncView so that method can call some
|
||||
* other methods on it.
|
||||
*/
|
||||
function addMockEnvironment(widget, params) {
|
||||
var Server = MockServer;
|
||||
params.services = params.services || {};
|
||||
if (params.mockRPC) {
|
||||
Server = MockServer.extend({ _performRpc: params.mockRPC });
|
||||
}
|
||||
if (params.debug) {
|
||||
_observe(widget);
|
||||
var separator = window.location.href.indexOf('?') !== -1 ? "&" : "?";
|
||||
var url = window.location.href + separator + 'testId=' + QUnit.config.current.testId;
|
||||
console.log('%c[debug] debug mode activated', 'color: blue; font-weight: bold;', url);
|
||||
}
|
||||
|
||||
var mockServer = new Server(params.data, {
|
||||
actions: params.actions,
|
||||
archs: params.archs,
|
||||
currentDate: params.currentDate,
|
||||
debug: params.debug,
|
||||
widget: widget,
|
||||
});
|
||||
|
||||
// make sure images do not trigger a GET on the server
|
||||
$('body').on('DOMNodeInserted.removeSRC', function (event) {
|
||||
removeSrcAttribute(event.target, widget);
|
||||
});
|
||||
|
||||
// make sure the debounce value for input fields is set to 0
|
||||
var initialDebounceValue = DebouncedField.prototype.DEBOUNCE;
|
||||
DebouncedField.prototype.DEBOUNCE = params.fieldDebounce || 0;
|
||||
var initialSession, initialConfig, initialParameters, initialDebounce, initialThrottle;
|
||||
initialSession = _.extend({}, session);
|
||||
session.getTZOffset = function () {
|
||||
return 0; // by default, but may be overriden in specific tests
|
||||
};
|
||||
if ('session' in params) {
|
||||
_.extend(session, params.session);
|
||||
}
|
||||
if ('config' in params) {
|
||||
initialConfig = _.clone(config);
|
||||
initialConfig.device = _.clone(config.device);
|
||||
if ('device' in params.config) {
|
||||
_.extend(config.device, params.config.device);
|
||||
}
|
||||
if ('debug' in params.config) {
|
||||
config.debug = params.config.debug;
|
||||
}
|
||||
}
|
||||
if ('translateParameters' in params) {
|
||||
initialParameters = _.extend({}, core._t.database.parameters);
|
||||
_.extend(core._t.database.parameters, params.translateParameters);
|
||||
}
|
||||
if (params.debounce === false) {
|
||||
initialDebounce = _.debounce;
|
||||
_.debounce = function (func) {
|
||||
return func;
|
||||
};
|
||||
}
|
||||
if (!('throttle' in params) || !params.throttle) {
|
||||
initialThrottle = _.throttle;
|
||||
_.throttle = function (func) {
|
||||
return func;
|
||||
};
|
||||
}
|
||||
|
||||
var widgetDestroy = widget.destroy;
|
||||
widget.destroy = function () {
|
||||
// clear the caches (e.g. data_manager, ModelFieldSelector) when the
|
||||
// widget is destroyed, at the end of each test to avoid collisions
|
||||
core.bus.trigger('clear_cache');
|
||||
|
||||
DebouncedField.prototype.DEBOUNCE = initialDebounceValue;
|
||||
if (params.debounce === false) {
|
||||
_.debounce = initialDebounce;
|
||||
}
|
||||
if (!('throttle' in params) || !params.throttle) {
|
||||
_.throttle = initialThrottle;
|
||||
}
|
||||
|
||||
var key;
|
||||
if ('session' in params) {
|
||||
for (key in session) {
|
||||
delete session[key];
|
||||
}
|
||||
}
|
||||
_.extend(session, initialSession);
|
||||
if ('config' in params) {
|
||||
for (key in config) {
|
||||
delete config[key];
|
||||
}
|
||||
_.extend(config, initialConfig);
|
||||
}
|
||||
if ('translateParameters' in params) {
|
||||
for (key in core._t.database.parameters) {
|
||||
delete core._t.database.parameters[key];
|
||||
}
|
||||
_.extend(core._t.database.parameters, initialParameters);
|
||||
}
|
||||
|
||||
$('body').off('DOMNodeInserted.removeSRC');
|
||||
$('.blockUI').remove();
|
||||
|
||||
widgetDestroy.call(this);
|
||||
};
|
||||
|
||||
// Dispatch service calls
|
||||
// Note: some services could call other services at init,
|
||||
// Which is why we have to init services after that
|
||||
var services = {};
|
||||
intercept(widget, 'call_service', function (ev) {
|
||||
var args, result;
|
||||
if (services[ev.data.service]) {
|
||||
var service = services[ev.data.service];
|
||||
args = (ev.data.args || []);
|
||||
result = service[ev.data.method].apply(service, args);
|
||||
} else if (ev.data.service === 'ajax') {
|
||||
// use ajax service that is mocked by the server
|
||||
var route = ev.data.args[0];
|
||||
args = ev.data.args[1];
|
||||
result = mockServer.performRpc(route, args);
|
||||
}
|
||||
ev.data.callback(result);
|
||||
});
|
||||
|
||||
intercept(widget, 'load_action', function (event) {
|
||||
mockServer.performRpc('/web/action/load', {
|
||||
kwargs: {
|
||||
action_id: event.data.actionID,
|
||||
additional_context: event.data.context,
|
||||
},
|
||||
}).then(function (action) {
|
||||
event.data.on_success(action);
|
||||
});
|
||||
});
|
||||
|
||||
intercept(widget, "load_views", function (event) {
|
||||
mockServer.performRpc('/web/dataset/call_kw/' + event.data.modelName, {
|
||||
args: [],
|
||||
kwargs: {
|
||||
context: event.data.context,
|
||||
options: event.data.options,
|
||||
views: event.data.views,
|
||||
},
|
||||
method: 'load_views',
|
||||
model: event.data.modelName,
|
||||
}).then(function (views) {
|
||||
views = _.mapObject(views, function (viewParams) {
|
||||
return fieldsViewGet(mockServer, viewParams);
|
||||
});
|
||||
event.data.on_success(views);
|
||||
});
|
||||
});
|
||||
|
||||
intercept(widget, "get_session", function (event) {
|
||||
event.data.callback(session);
|
||||
});
|
||||
|
||||
intercept(widget, "load_filters", function (event) {
|
||||
if (params.debug) {
|
||||
console.log('[mock] load_filters', event.data);
|
||||
}
|
||||
event.data.on_success([]);
|
||||
});
|
||||
|
||||
// make sure all Odoo events bubbling up are intercepted
|
||||
if ('intercepts' in params) {
|
||||
_.each(params.intercepts, function (cb, name) {
|
||||
intercept(widget, name, cb);
|
||||
});
|
||||
}
|
||||
|
||||
// Deploy services
|
||||
var done = false;
|
||||
var servicesToDeploy = _.clone(params.services);
|
||||
if (!servicesToDeploy.ajax) {
|
||||
services.ajax = null; // use mocked ajax from mocked server
|
||||
}
|
||||
while (!done) {
|
||||
var serviceName = _.findKey(servicesToDeploy, function (Service) {
|
||||
return !_.some(Service.prototype.dependencies, function (depName) {
|
||||
return !_.has(services, depName);
|
||||
});
|
||||
});
|
||||
if (serviceName) {
|
||||
var Service = servicesToDeploy[serviceName];
|
||||
var service = services[serviceName] = new Service(widget);
|
||||
delete servicesToDeploy[serviceName];
|
||||
|
||||
intercept(service, "get_session", function (event) {
|
||||
event.data.callback(session);
|
||||
});
|
||||
|
||||
service.start();
|
||||
} else {
|
||||
var serviceNames = _.keys(servicesToDeploy);
|
||||
if (serviceNames.length) {
|
||||
console.warn("Non loaded services:", serviceNames);
|
||||
}
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
|
||||
return mockServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* performs a fields_view_get, and mocks the postprocessing done by the
|
||||
* data_manager to return an equivalent structure.
|
||||
*
|
||||
* @param {MockServer} server
|
||||
* @param {Object} params
|
||||
* @param {string} params.model
|
||||
* @returns {Object} an object with 3 keys: arch, fields and viewFields
|
||||
*/
|
||||
function fieldsViewGet(server, params) {
|
||||
var fieldsView = server.fieldsViewGet(params);
|
||||
// mock the structure produced by the DataManager
|
||||
fieldsView.viewFields = fieldsView.fields;
|
||||
fieldsView.fields = server.fieldsGet(params.model);
|
||||
return fieldsView;
|
||||
}
|
||||
|
||||
/**
|
||||
* intercepts an event bubbling up the widget hierarchy. The event intercepted
|
||||
* must be a "custom event", i.e. an event generated by the method 'trigger_up'.
|
||||
*
|
||||
* Note that this method really intercepts the event if @propagate is not set.
|
||||
* It will not be propagated further, and even the handlers on the target will
|
||||
* not fire.
|
||||
*
|
||||
* @param {Widget} widget the target widget (any Odoo widget)
|
||||
* @param {string} eventName description of the event
|
||||
* @param {function} fn callback executed when the even is intercepted
|
||||
* @param {boolean} [propagate=false]
|
||||
*/
|
||||
function intercept(widget, eventName, fn, propagate) {
|
||||
var _trigger_up = widget._trigger_up.bind(widget);
|
||||
widget._trigger_up = function (event) {
|
||||
if (event.name === eventName) {
|
||||
fn(event);
|
||||
if (!propagate) { return; }
|
||||
}
|
||||
_trigger_up(event);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch window.Date so that the time starts its flow from the provided Date.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* ```
|
||||
* var unpatchDate = testUtils.mock.patchDate(2018, 0, 10, 17, 59, 30)
|
||||
* new window.Date(); // "Wed Jan 10 2018 17:59:30 GMT+0100 (Central European Standard Time)"
|
||||
* ... // 5 hours delay
|
||||
* new window.Date(); // "Wed Jan 10 2018 22:59:30 GMT+0100 (Central European Standard Time)"
|
||||
* ...
|
||||
* unpatchDate();
|
||||
* new window.Date(); // actual current date time
|
||||
* ```
|
||||
*
|
||||
* @param {integer} year
|
||||
* @param {integer} month index of the month, starting from zero.
|
||||
* @param {integer} day the day of the month.
|
||||
* @param {integer} hours the digits for hours (24h)
|
||||
* @param {integer} minutes
|
||||
* @param {integer} seconds
|
||||
* @returns {function} a callback to unpatch window.Date.
|
||||
*/
|
||||
function patchDate(year, month, day, hours, minutes, seconds) {
|
||||
var RealDate = window.Date;
|
||||
var actualDate = new RealDate();
|
||||
var fakeDate = new RealDate(year, month, day, hours, minutes, seconds);
|
||||
var timeInterval = actualDate.getTime() - (fakeDate.getTime());
|
||||
|
||||
Date = (function (NativeDate) {
|
||||
function Date(Y, M, D, h, m, s, ms) {
|
||||
var length = arguments.length;
|
||||
if (arguments.length > 0) {
|
||||
var date = length == 1 && String(Y) === Y ? // isString(Y)
|
||||
// We explicitly pass it through parse:
|
||||
new NativeDate(Date.parse(Y)) :
|
||||
// We have to manually make calls depending on argument
|
||||
// length here
|
||||
length >= 7 ? new NativeDate(Y, M, D, h, m, s, ms) :
|
||||
length >= 6 ? new NativeDate(Y, M, D, h, m, s) :
|
||||
length >= 5 ? new NativeDate(Y, M, D, h, m) :
|
||||
length >= 4 ? new NativeDate(Y, M, D, h) :
|
||||
length >= 3 ? new NativeDate(Y, M, D) :
|
||||
length >= 2 ? new NativeDate(Y, M) :
|
||||
length >= 1 ? new NativeDate(Y) :
|
||||
new NativeDate();
|
||||
// Prevent mixups with unfixed Date object
|
||||
date.constructor = Date;
|
||||
return date;
|
||||
} else {
|
||||
var date = new NativeDate();
|
||||
var time = date.getTime();
|
||||
time -= timeInterval;
|
||||
date.setTime(time);
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy any custom methods a 3rd party library may have added
|
||||
for (var key in NativeDate) {
|
||||
Date[key] = NativeDate[key];
|
||||
}
|
||||
|
||||
// Copy "native" methods explicitly; they may be non-enumerable
|
||||
Date.now = NativeDate.now;
|
||||
Date.UTC = NativeDate.UTC;
|
||||
Date.prototype = NativeDate.prototype;
|
||||
Date.prototype.constructor = Date;
|
||||
|
||||
// Upgrade Date.parse to handle simplified ISO 8601 strings
|
||||
Date.parse = NativeDate.parse;
|
||||
return Date;
|
||||
})(Date);
|
||||
|
||||
return function () { window.Date = RealDate; };
|
||||
}
|
||||
|
||||
var patches = {};
|
||||
/**
|
||||
* Patches a given Class or Object with the given properties.
|
||||
*
|
||||
* @param {Class|Object} target
|
||||
* @param {Object} props
|
||||
*/
|
||||
function patch(target, props) {
|
||||
var patchID = _.uniqueId('patch_');
|
||||
target.__patchID = patchID;
|
||||
patches[patchID] = {
|
||||
target: target,
|
||||
otherPatchedProps: [],
|
||||
ownPatchedProps: [],
|
||||
};
|
||||
if (target.prototype) {
|
||||
_.each(props, function (value, key) {
|
||||
if (target.prototype.hasOwnProperty(key)) {
|
||||
patches[patchID].ownPatchedProps.push({
|
||||
key: key,
|
||||
initialValue: target.prototype[key],
|
||||
});
|
||||
} else {
|
||||
patches[patchID].otherPatchedProps.push(key);
|
||||
}
|
||||
});
|
||||
target.include(props);
|
||||
} else {
|
||||
_.each(props, function (value, key) {
|
||||
if (key in target) {
|
||||
var oldValue = target[key];
|
||||
patches[patchID].ownPatchedProps.push({
|
||||
key: key,
|
||||
initialValue: oldValue,
|
||||
});
|
||||
if (typeof value === 'function') {
|
||||
target[key] = function () {
|
||||
var oldSuper = this._super;
|
||||
this._super = oldValue;
|
||||
var result = value.apply(this, arguments);
|
||||
if (oldSuper === undefined) {
|
||||
delete this._super;
|
||||
} else {
|
||||
this._super = oldSuper;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
} else {
|
||||
target[key] = value;
|
||||
}
|
||||
} else {
|
||||
patches[patchID].otherPatchedProps.push(key);
|
||||
target[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpatches a given Class or Object.
|
||||
*
|
||||
* @param {Class|Object} target
|
||||
*/
|
||||
function unpatch(target) {
|
||||
var patchID = target.__patchID;
|
||||
var patch = patches[patchID];
|
||||
if (target.prototype) {
|
||||
_.each(patch.ownPatchedProps, function (p) {
|
||||
target.prototype[p.key] = p.initialValue;
|
||||
});
|
||||
_.each(patch.otherPatchedProps, function (key) {
|
||||
delete target.prototype[key];
|
||||
});
|
||||
} else {
|
||||
_.each(patch.ownPatchedProps, function (p) {
|
||||
target[p.key] = p.initialValue;
|
||||
});
|
||||
_.each(patch.otherPatchedProps, function (key) {
|
||||
delete target[key];
|
||||
});
|
||||
}
|
||||
delete patches[patchID];
|
||||
delete target.__patchID;
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
addMockEnvironment: addMockEnvironment,
|
||||
fieldsViewGet: fieldsViewGet,
|
||||
intercept: intercept,
|
||||
patchDate: patchDate,
|
||||
patch: patch,
|
||||
unpatch: unpatch,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
odoo.define('web.test_utils_modal', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Modal Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help test pivot views.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
var core = require('web.core');
|
||||
|
||||
/**
|
||||
* Click on a button in the footer of a modal (which contains a given string).
|
||||
* Note that this method checks the unicity of the button.
|
||||
*
|
||||
* @param {string} text (in english: this method will perform the translation)
|
||||
*/
|
||||
function clickButton(text) {
|
||||
var selector = `.modal-footer button:contains(${core._t(text)})`;
|
||||
var $button = $(selector);
|
||||
if ($button.length !== 1) {
|
||||
throw new Error(`Found ${$button.length} button(s) containing '${text}'`);
|
||||
}
|
||||
$button.click();
|
||||
}
|
||||
|
||||
return {
|
||||
clickButton: clickButton,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
odoo.define('web.test_utils_pivot', function (require) {
|
||||
"use strict";
|
||||
|
||||
var testUtilsDom = require('web.test_utils_dom');
|
||||
|
||||
/**
|
||||
* Pivot Test Utils
|
||||
*
|
||||
* This module defines various utility functions to help test pivot views.
|
||||
*
|
||||
* Note that all methods defined in this module are exported in the main
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Select a measure by clicking on the corresponding dropdown item (in the
|
||||
* control panel 'Measure' submenu).
|
||||
*
|
||||
* Note that this method assumes that the dropdown menu is open.
|
||||
* @see toggleMeasuresDropdown
|
||||
*
|
||||
* @param {PivotController} pivot
|
||||
* @param {string} measure
|
||||
*/
|
||||
function clickMeasure(pivot, measure) {
|
||||
testUtilsDom.click(pivot.$buttons.find(`.dropdown-item[data-field=${measure}]`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the 'Measure' dropdown menu (in the control panel)
|
||||
*
|
||||
* @see clickMeasure
|
||||
*
|
||||
* @param {PivotController} pivot
|
||||
*/
|
||||
function toggleMeasuresDropdown(pivot) {
|
||||
testUtilsDom.click(pivot.$buttons.filter('.btn-group:first').find('> button'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads a graph view.
|
||||
*
|
||||
* @param {PivotController} pivot
|
||||
* @param {[Object]} params given to the controller reload method
|
||||
*/
|
||||
function reload(pivot, params) {
|
||||
pivot.reload(params);
|
||||
}
|
||||
|
||||
return {
|
||||
clickMeasure: clickMeasure,
|
||||
reload: reload,
|
||||
toggleMeasuresDropdown: toggleMeasuresDropdown,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -489,6 +489,15 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_create.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_dom.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_fields.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_form.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_graph.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_kanban.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_mock.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_modal.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils_pivot.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/test_utils.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/helpers/mock_server.js"></script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user