Files
odoo_source/addons/web/static/tests/helpers/test_utils_mock.js
T
Alexandre Kühn 7d8977182b [IMP] web:tests: patch Date.now
Before this commit, when we use `testUtils.patchDate`,
the function `Date.now` was not using the fake date for reference.

This is an issue with the lib momentjs, which instantiate the
current date as follow:

```
new Date(Date.now())
```

Because of this, we were handling patched date and unpatched moment
objects.

Task-ID 1902848
2019-08-13 07:57:10 +00:00

584 lines
21 KiB
JavaScript

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 dom = require('web.dom');
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 });
}
$(node).trigger('load');
}
}
}
/**
* 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 createView 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 initialDOMDebounceValue = dom.DEBOUNCE;
dom.DEBOUNCE = 0;
var initialSession, initialConfig, initialParameters, initialDebounce, initialThrottle;
initialSession = _.extend({}, session);
session.getTZOffset = function () {
return 0; // by default, but may be overridden 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) {
odoo.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');
Object.keys(services).forEach(function(s) {
var service = services[s];
if (service && !service.isDestroyed())
service.destroy();
});
DebouncedField.prototype.DEBOUNCE = initialDebounceValue;
dom.DEBOUNCE = initialDOMDebounceValue;
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');
$('body').removeClass('debug');
$('.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
// exception: 'now' uses fake date as reference
Date.now = function () {
var date = new NativeDate();
var time = date.getTime();
time -= timeInterval;
return time;
};
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;
}
window.originalSetTimeout = window.setTimeout;
function patchSetTimeout() {
var original = window.setTimeout;
var self = this;
window.setTimeout = function (handler, delay) {
console.log("calling setTimeout on " + (handler.name || "some function") + "with delay of " + delay);
console.trace();
var handlerArguments = Array.prototype.slice.call(arguments, 1);
return original(function () {
handler.bind(self, handlerArguments)();
console.log('after doing the action of the setTimeout');
}, delay);
};
return function () {
window.setTimeout = original;
};
}
return {
addMockEnvironment: addMockEnvironment,
fieldsViewGet: fieldsViewGet,
intercept: intercept,
patchDate: patchDate,
patch: patch,
unpatch: unpatch,
patchSetTimeout: patchSetTimeout,
};
});