[IMP] web: tests: rework mock helpers
This commit reworks the test helpers, following recent changes on services deployed in the Owl env.
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
odoo.define('web.test_env', async function (require) {
|
||||
"use strict";
|
||||
|
||||
const AbstractStorageService = require('web.AbstractStorageService');
|
||||
const Bus = require("web.Bus");
|
||||
const RamStorage = require('web.RamStorage');
|
||||
const { buildQuery } = require("web.rpc");
|
||||
const Bus = require('web.Bus');
|
||||
const { buildQuery } = require('web.rpc');
|
||||
const session = require('web.session');
|
||||
|
||||
/**
|
||||
@@ -17,9 +15,6 @@ odoo.define('web.test_env', async function (require) {
|
||||
* @returns {Proxy}
|
||||
*/
|
||||
function makeTestEnvironment(env = {}, providedRPC = null) {
|
||||
const RamStorageService = AbstractStorageService.extend({
|
||||
storage: new RamStorage(),
|
||||
});
|
||||
const database = {
|
||||
parameters: {
|
||||
code: "en_US",
|
||||
@@ -31,33 +26,26 @@ odoo.define('web.test_env', async function (require) {
|
||||
time_format: '%H:%M:%S',
|
||||
},
|
||||
};
|
||||
let testEnv = {};
|
||||
const defaultEnv = {
|
||||
_t: env._t || Object.assign((s => s), { database }),
|
||||
bus: new Bus(),
|
||||
bus: env.bus || new Bus(),
|
||||
device: Object.assign({ isMobile: false }, env.device),
|
||||
isDebug: env.isDebug || (() => false),
|
||||
qweb: new owl.QWeb({ templates: session.owlTemplates }),
|
||||
services: Object.assign({
|
||||
ajax: { // for legacy subwidgets
|
||||
ajax: {
|
||||
rpc() {
|
||||
const prom = testEnv.session.rpc(...arguments);
|
||||
prom.abort = function () {
|
||||
throw new Error("Can't abort this request");
|
||||
};
|
||||
return prom;
|
||||
},
|
||||
return env.session.rpc(...arguments); // Compatibility Legacy Widgets
|
||||
}
|
||||
},
|
||||
getCookie() { },
|
||||
httpRequest() {
|
||||
getCookie() {},
|
||||
httpRequest(/* route, params = {}, readMethod = 'json' */) {
|
||||
return Promise.resolve('');
|
||||
},
|
||||
rpc(params, options) {
|
||||
const query = buildQuery(params);
|
||||
return testEnv.session.rpc(query.route, query.params, options);
|
||||
return env.session.rpc(query.route, query.params, options);
|
||||
},
|
||||
local_storage: new RamStorageService(),
|
||||
session_storage: new RamStorageService(),
|
||||
notification: { notify() { } },
|
||||
}, env.services),
|
||||
session: Object.assign({
|
||||
@@ -67,10 +55,10 @@ odoo.define('web.test_env', async function (require) {
|
||||
}
|
||||
throw new Error(`No method to perform RPC`);
|
||||
},
|
||||
url: session.url,
|
||||
}, env.session),
|
||||
};
|
||||
testEnv = Object.assign(env, defaultEnv);
|
||||
return testEnv;
|
||||
return Object.assign(env, defaultEnv);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -121,7 +121,7 @@ odoo.define('web.test_utils', async function (require) {
|
||||
return {
|
||||
mock: {
|
||||
addMockEnvironment: testUtilsMock.addMockEnvironment,
|
||||
getMockedOwlEnv: testUtilsMock.getMockedOwlEnv,
|
||||
addMockEnvironmentOwl: testUtilsMock.addMockEnvironmentOwl,
|
||||
intercept: testUtilsMock.intercept,
|
||||
patch: testUtilsMock.patch,
|
||||
patchDate: testUtilsMock.patchDate,
|
||||
|
||||
@@ -67,7 +67,6 @@ odoo.define('web.test_utils_create', function (require) {
|
||||
}
|
||||
|
||||
params.server = mockServer;
|
||||
Component.env = testUtilsMock.getMockedOwlEnv(params);
|
||||
|
||||
const userContext = params.context && params.context.user_context || {};
|
||||
const actionManager = new ActionManager(widget, userContext);
|
||||
@@ -142,7 +141,7 @@ odoo.define('web.test_utils_create', function (require) {
|
||||
if (!(constructor.prototype instanceof Component)) {
|
||||
throw new Error(`Argument "constructor" must be an Owl Component.`);
|
||||
}
|
||||
const env = Object.assign(testUtilsMock.getMockedOwlEnv(params), params.env);
|
||||
const cleanUp = await testUtilsMock.addMockEnvironmentOwl(Component, params);
|
||||
class Parent extends Component {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
@@ -154,7 +153,6 @@ odoo.define('web.test_utils_create', function (require) {
|
||||
}
|
||||
}
|
||||
}
|
||||
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' });
|
||||
@@ -162,6 +160,7 @@ odoo.define('web.test_utils_create', function (require) {
|
||||
const originalDestroy = child.destroy;
|
||||
child.destroy = function () {
|
||||
child.destroy = originalDestroy;
|
||||
cleanUp();
|
||||
parent.destroy();
|
||||
};
|
||||
return child;
|
||||
@@ -241,9 +240,9 @@ odoo.define('web.test_utils_create', function (require) {
|
||||
* mock method, assuming that the user has access rights, and is an admin.
|
||||
*
|
||||
* @param {Object} [params={}]
|
||||
* @returns {DebugManager}
|
||||
* @returns {Promise<DebugManager>}
|
||||
*/
|
||||
function createDebugManager(params = {}) {
|
||||
async function createDebugManager(params = {}) {
|
||||
const mockRPC = params.mockRPC;
|
||||
Object.assign(params, {
|
||||
async mockRPC(route, args) {
|
||||
@@ -268,7 +267,7 @@ odoo.define('web.test_utils_create', function (require) {
|
||||
},
|
||||
});
|
||||
const debugManager = new DebugManager();
|
||||
testUtilsMock.addMockEnvironment(debugManager, params);
|
||||
await testUtilsMock.addMockEnvironment(debugManager, params);
|
||||
return debugManager;
|
||||
}
|
||||
|
||||
@@ -280,12 +279,12 @@ odoo.define('web.test_utils_create', function (require) {
|
||||
* @param {Class} params.Model the model class to use
|
||||
* @returns {Model}
|
||||
*/
|
||||
function createModel(params) {
|
||||
async function createModel(params) {
|
||||
const widget = new Widget();
|
||||
|
||||
const model = new params.Model(widget);
|
||||
|
||||
testUtilsMock.addMockEnvironment(widget, params);
|
||||
await 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.
|
||||
@@ -304,11 +303,11 @@ odoo.define('web.test_utils_create', function (require) {
|
||||
*
|
||||
* @param {Object} params This object will be given to addMockEnvironment, so
|
||||
* any parameters from that method applies
|
||||
* @returns {Widget}
|
||||
* @returns {Promise<Widget>}
|
||||
*/
|
||||
function createParent(params) {
|
||||
async function createParent(params) {
|
||||
const widget = new Widget();
|
||||
testUtilsMock.addMockEnvironment(widget, params);
|
||||
await testUtilsMock.addMockEnvironment(widget, params);
|
||||
return widget;
|
||||
}
|
||||
|
||||
@@ -358,8 +357,6 @@ odoo.define('web.test_utils_create', function (require) {
|
||||
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;
|
||||
|
||||
@@ -10,12 +10,16 @@ odoo.define('web.test_utils_mock', function (require) {
|
||||
* testUtils file.
|
||||
*/
|
||||
|
||||
const AbstractStorageService = require('web.AbstractStorageService');
|
||||
const AjaxService = require('web.AjaxService');
|
||||
const basic_fields = require('web.basic_fields');
|
||||
const Bus = require('web.Bus');
|
||||
const config = require('web.config');
|
||||
const core = require('web.core');
|
||||
const dom = require('web.dom');
|
||||
const makeTestEnvironment = require('web.test_env');
|
||||
const MockServer = require('web.MockServer');
|
||||
const RamStorage = require('web.RamStorage');
|
||||
const session = require('web.session');
|
||||
|
||||
const DebouncedField = basic_fields.DebouncedField;
|
||||
@@ -25,6 +29,199 @@ const DebouncedField = basic_fields.DebouncedField;
|
||||
// Private functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns a mocked environment to be used by OWL components in tests, with
|
||||
* requested services (+ ajax, local_storage and session_storage) deployed.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} params
|
||||
* @param {Bus} [params.bus]
|
||||
* @param {boolean} [params.debug]
|
||||
* @param {Object} [params.env]
|
||||
* @param {Bus} [params.env.bus]
|
||||
* @param {Object} [params.env.dataManager]
|
||||
* @param {Object} [params.env.services]
|
||||
* @param {Object[]} [params.favoriteFilters]
|
||||
* @param {Object} [params.services]
|
||||
* @param {Object} [params.session]
|
||||
* @param {MockServer} [mockServer]
|
||||
* @returns {Promise<Object>} env
|
||||
*/
|
||||
async function _getMockedOwlEnv(params, mockServer) {
|
||||
params.env = params.env || {};
|
||||
|
||||
// build the env
|
||||
const favoriteFilters = params.favoriteFilters;
|
||||
const debug = params.debug;
|
||||
const services = {};
|
||||
const env = Object.assign({}, params.env, {
|
||||
bus: params.bus || params.env.bus || new Bus(),
|
||||
dataManager: Object.assign({
|
||||
load_action: (actionID, context) => {
|
||||
return mockServer.performRpc('/web/action/load', {
|
||||
action_id: actionID,
|
||||
additional_context: context,
|
||||
});
|
||||
},
|
||||
load_views: (params, options) => {
|
||||
return mockServer.performRpc('/web/dataset/call_kw/' + params.model, {
|
||||
args: [],
|
||||
kwargs: {
|
||||
context: params.context,
|
||||
options: options,
|
||||
views: params.views_descr,
|
||||
},
|
||||
method: 'load_views',
|
||||
model: params.model,
|
||||
}).then(function (views) {
|
||||
views = _.mapObject(views, viewParams => {
|
||||
return fieldsViewGet(mockServer, viewParams);
|
||||
});
|
||||
if (favoriteFilters && 'search' in views) {
|
||||
views.search.favoriteFilters = favoriteFilters;
|
||||
}
|
||||
return views;
|
||||
});
|
||||
},
|
||||
load_filters: params => {
|
||||
if (debug) {
|
||||
console.log('[mock] load_filters', params);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
}, params.env.dataManager),
|
||||
services: Object.assign(services, params.env.services),
|
||||
session: params.session || {},
|
||||
});
|
||||
|
||||
// deploy services into the env
|
||||
// determine services to instantiate (classes), and already register function services
|
||||
const servicesToDeploy = {};
|
||||
for (const name in params.services || {}) {
|
||||
const Service = params.services[name];
|
||||
if (Service.constructor.name === 'Class') {
|
||||
servicesToDeploy[name] = Service;
|
||||
} else {
|
||||
services[name] = Service;
|
||||
}
|
||||
}
|
||||
// always deploy ajax, local storage and session storage
|
||||
if (!servicesToDeploy.ajax) {
|
||||
const MockedAjaxService = AjaxService.extend({
|
||||
rpc: mockServer.performRpc.bind(mockServer),
|
||||
});
|
||||
services.ajax = new MockedAjaxService(env);
|
||||
}
|
||||
const RamStorageService = AbstractStorageService.extend({
|
||||
storage: new RamStorage(),
|
||||
});
|
||||
if (!servicesToDeploy.local_storage) {
|
||||
services.local_storage = new RamStorageService(env);
|
||||
}
|
||||
if (!servicesToDeploy.session_storage) {
|
||||
services.session_storage = new RamStorageService(env);
|
||||
}
|
||||
// deploy other requested services
|
||||
let done = false;
|
||||
while (!done) {
|
||||
const serviceName = Object.keys(servicesToDeploy).find(serviceName => {
|
||||
const Service = servicesToDeploy[serviceName];
|
||||
return Service.prototype.dependencies.every(depName => {
|
||||
return env.services[depName];
|
||||
});
|
||||
});
|
||||
if (serviceName) {
|
||||
const Service = servicesToDeploy[serviceName];
|
||||
services[serviceName] = new Service(env);
|
||||
delete servicesToDeploy[serviceName];
|
||||
services[serviceName].start();
|
||||
} else {
|
||||
const serviceNames = _.keys(servicesToDeploy);
|
||||
if (serviceNames.length) {
|
||||
console.warn("Non loaded services:", serviceNames);
|
||||
}
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
// wait for asynchronous services to properly start
|
||||
await new Promise(setTimeout);
|
||||
|
||||
return env;
|
||||
}
|
||||
/**
|
||||
* This function is used to mock global objects (session, config...) in tests.
|
||||
* It is necessary for legacy widgets. It returns a cleanUp function to call at
|
||||
* the end of the test.
|
||||
*
|
||||
* The function could be removed as soon as we do not support legacy widgets
|
||||
* anymore.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} params
|
||||
* @param {Object} [params.config] if given, it is used to extend the global
|
||||
* config,
|
||||
* @param {Object} [params.session] if given, it is used to extend the current,
|
||||
* real session.
|
||||
* @param {Object} [params.translateParameters] if given, it will be used to
|
||||
* extend the core._t.database.parameters object.
|
||||
* @returns {function} a cleanUp function to restore everything, to call at the
|
||||
* end of the test
|
||||
*/
|
||||
function _mockGlobalObjects(params) {
|
||||
// patch session
|
||||
let initialSession = Object.assign({}, session);
|
||||
session.getTZOffset = function () {
|
||||
return 0; // by default, but may be overridden in specific tests
|
||||
};
|
||||
if ('session' in params) {
|
||||
Object.assign(session, params.session);
|
||||
}
|
||||
|
||||
// patch config
|
||||
let initialConfig;
|
||||
if ('config' in params) {
|
||||
initialConfig = Object.assign({}, config);
|
||||
initialConfig.device = Object.assign({}, config.device);
|
||||
if ('device' in params.config) {
|
||||
Object.assign(config.device, params.config.device);
|
||||
}
|
||||
if ('debug' in params.config) {
|
||||
odoo.debug = params.config.debug;
|
||||
}
|
||||
}
|
||||
|
||||
// patch translate params
|
||||
let initialParameters;
|
||||
if ('translateParameters' in params) {
|
||||
initialParameters = Object.assign({}, core._t.database.parameters);
|
||||
Object.assign(core._t.database.parameters, params.translateParameters);
|
||||
}
|
||||
|
||||
// build the cleanUp function to restore everything at the end of the test
|
||||
function cleanUp() {
|
||||
let key;
|
||||
if ('session' in params) {
|
||||
for (key in session) {
|
||||
delete session[key];
|
||||
}
|
||||
}
|
||||
Object.assign(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);
|
||||
}
|
||||
}
|
||||
|
||||
return cleanUp;
|
||||
}
|
||||
/**
|
||||
* logs all event going through the target widget.
|
||||
*
|
||||
@@ -42,16 +239,58 @@ function _observe(widget) {
|
||||
// Public functions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {HTMLElement} el
|
||||
* @param {[Widget]} widget the widget on which the rpc should be performed
|
||||
* @param {[function]} rpc
|
||||
*/
|
||||
function removeSrcAttribute(el, widget) {
|
||||
function removeSrcAttribute(el, rpc) {
|
||||
var nodes;
|
||||
el = el.nodeType === 8 ? el.nextSibling : el;
|
||||
if (el.nodeName === 'IMG' || el.nodeName === 'IFRAME') {
|
||||
nodes = [el];
|
||||
} else {
|
||||
@@ -68,14 +307,135 @@ function removeSrcAttribute(el, widget) {
|
||||
} else {
|
||||
node.setAttribute('src', 'about:blank');
|
||||
}
|
||||
if (widget) {
|
||||
widget._rpc({ route: src });
|
||||
if (rpc) {
|
||||
rpc(src, []);
|
||||
}
|
||||
$(node).trigger('load');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a mock environment to test Owl Components. This function generates a test
|
||||
* env and sets it on the given Component. It also has several side effects,
|
||||
* like patching the global session or config objects. It returns a cleanup
|
||||
* function to call at the end of the test.
|
||||
*
|
||||
* @param {Component} Component
|
||||
* @param {Object} [params]
|
||||
* @param {Object} [params.actions]
|
||||
* @param {Object} [params.archs]
|
||||
* @param {string} [params.currentDate]
|
||||
* @param {Object} [params.data]
|
||||
* @param {boolean} [params.debug]
|
||||
* @param {function} [params.mockRPC]
|
||||
* @param {number} [params.fieldDebounce=0] the value of the DEBOUNCE attribute
|
||||
* of fields
|
||||
* @param {boolean} [params.debounce=true] if false, patch _.debounce to remove
|
||||
* its behavior
|
||||
* @param {boolean} [params.throttle=false] by default, _.throttle is patched to
|
||||
* remove its behavior, except if this params is set to true
|
||||
* @param {boolean} [params.mockSRC=false] if true, redirect src GET requests to
|
||||
* the mockServer
|
||||
* @param {MockServer} [mockServer]
|
||||
* @returns {Promise<function>} the cleanup function
|
||||
*/
|
||||
async function addMockEnvironmentOwl(Component, params, mockServer) {
|
||||
params = params || {};
|
||||
|
||||
// instantiate a mockServer if not provided
|
||||
if (!mockServer) {
|
||||
let Server = MockServer;
|
||||
if (params.mockRPC) {
|
||||
Server = MockServer.extend({ _performRpc: params.mockRPC });
|
||||
}
|
||||
mockServer = new Server(params.data, {
|
||||
actions: params.actions,
|
||||
archs: params.archs,
|
||||
currentDate: params.currentDate,
|
||||
debug: params.debug,
|
||||
});
|
||||
}
|
||||
|
||||
// make sure the debounce value for input fields is set to 0
|
||||
const initialDebounceValue = DebouncedField.prototype.DEBOUNCE;
|
||||
DebouncedField.prototype.DEBOUNCE = params.fieldDebounce || 0;
|
||||
const initialDOMDebounceValue = dom.DEBOUNCE;
|
||||
dom.DEBOUNCE = 0;
|
||||
|
||||
// patch underscore debounce/throttle functions
|
||||
const initialDebounce = _.debounce;
|
||||
if (params.debounce === false) {
|
||||
_.debounce = function (func) {
|
||||
return func;
|
||||
};
|
||||
}
|
||||
// fixme: throttle is inactive by default, should we make it explicit ?
|
||||
const initialThrottle = _.throttle;
|
||||
if (!('throttle' in params) || !params.throttle) {
|
||||
_.throttle = function (func) {
|
||||
return func;
|
||||
};
|
||||
}
|
||||
|
||||
// make sure images do not trigger a GET on the server
|
||||
$('body').on('DOMNodeInserted.removeSRC', function (ev) {
|
||||
let rpc;
|
||||
if (params.mockSRC) {
|
||||
rpc = mockServer.performRpc.bind(mockServer);
|
||||
}
|
||||
removeSrcAttribute(ev.target, rpc);
|
||||
});
|
||||
|
||||
// mock global objects for legacy widgets (session, config...)
|
||||
const restoreMockedGlobalObjects = _mockGlobalObjects(params);
|
||||
|
||||
// set the test env on owl Component
|
||||
const env = await _getMockedOwlEnv(params, mockServer);
|
||||
const originalEnv = Component.env;
|
||||
Component.env = makeTestEnvironment(env, mockServer.performRpc.bind(mockServer));
|
||||
|
||||
// while we have a mix between Owl and legacy stuff, some of them triggering
|
||||
// events on the env.bus (a new Bus instance especially created for the current
|
||||
// test), the others using core.bus, we have to ensure that events triggered
|
||||
// on env.bus are also triggered on core.bus (note that outside the testing
|
||||
// environment, both are the exact same instance of Bus)
|
||||
const envBusTrigger = env.bus.trigger;
|
||||
env.bus.trigger = function () {
|
||||
core.bus.trigger(...arguments);
|
||||
envBusTrigger.call(env.bus, ...arguments);
|
||||
};
|
||||
|
||||
// build the clean up function to call at the end of the test
|
||||
function cleanUp() {
|
||||
env.bus.destroy();
|
||||
Object.keys(env.services).forEach(function (s) {
|
||||
var service = env.services[s];
|
||||
if (service.destroy && !service.isDestroyed()) {
|
||||
service.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
DebouncedField.prototype.DEBOUNCE = initialDebounceValue;
|
||||
dom.DEBOUNCE = initialDOMDebounceValue;
|
||||
_.debounce = initialDebounce;
|
||||
_.throttle = initialThrottle;
|
||||
|
||||
// clear the caches (e.g. data_manager, ModelFieldSelector) at the end
|
||||
// of each test to avoid collisions
|
||||
core.bus.trigger('clear_cache');
|
||||
|
||||
$('body').off('DOMNodeInserted.removeSRC');
|
||||
$('.blockUI').remove(); // fixme: move to qunit_config in OdooAfterTestHook?
|
||||
|
||||
restoreMockedGlobalObjects();
|
||||
|
||||
Component.env = originalEnv;
|
||||
}
|
||||
|
||||
return cleanUp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a mock environment to a widget. This helper function can simulate
|
||||
* various kind of side effects, such as mocking RPCs, changing the session,
|
||||
@@ -100,6 +460,7 @@ function removeSrcAttribute(el, widget) {
|
||||
* 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 {Object} [params.bus] the instance of Bus that will be used (in the env)
|
||||
* @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.
|
||||
@@ -130,11 +491,7 @@ function removeSrcAttribute(el, widget) {
|
||||
* other methods on it.
|
||||
*/
|
||||
async function addMockEnvironment(widget, params) {
|
||||
var Server = MockServer;
|
||||
params.services = params.services || {};
|
||||
if (params.mockRPC) {
|
||||
Server = MockServer.extend({ _performRpc: params.mockRPC });
|
||||
}
|
||||
// log events triggered up if debug flag is true
|
||||
if (params.debug) {
|
||||
_observe(widget);
|
||||
var separator = window.location.href.indexOf('?') !== -1 ? "&" : "?";
|
||||
@@ -142,6 +499,11 @@ async function addMockEnvironment(widget, params) {
|
||||
console.log('%c[debug] debug mode activated', 'color: blue; font-weight: bold;', url);
|
||||
}
|
||||
|
||||
// instantiate mock server
|
||||
var Server = MockServer;
|
||||
if (params.mockRPC) {
|
||||
Server = MockServer.extend({ _performRpc: params.mockRPC });
|
||||
}
|
||||
var mockServer = new Server(params.data, {
|
||||
actions: params.actions,
|
||||
archs: params.archs,
|
||||
@@ -150,310 +512,60 @@ async function addMockEnvironment(widget, params) {
|
||||
widget: widget,
|
||||
});
|
||||
|
||||
// make sure images do not trigger a GET on the server
|
||||
$('body').on('DOMNodeInserted.removeSRC', function (event) {
|
||||
removeSrcAttribute(event.target, widget);
|
||||
});
|
||||
// build and set the Owl env on Component
|
||||
if (!('mockSRC' in params)) { // redirect src rpcs to the mock server
|
||||
params.mockSRC = true;
|
||||
}
|
||||
const cleanUp = await addMockEnvironmentOwl(owl.Component, params, mockServer);
|
||||
const env = owl.Component.env;
|
||||
|
||||
// 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;
|
||||
// ensure to clean up everything when the widget will be destroyed
|
||||
const destroy = 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);
|
||||
cleanUp();
|
||||
destroy.call(this, ...arguments);
|
||||
};
|
||||
|
||||
// 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 service/data manager calls and redirect them to the env
|
||||
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' && ev.data.method === 'rpc') {
|
||||
// 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);
|
||||
if (env.services[ev.data.service]) {
|
||||
var service = env.services[ev.data.service];
|
||||
const result = service[ev.data.method].apply(service, ev.data.args || []);
|
||||
ev.data.callback(result);
|
||||
}
|
||||
ev.data.callback(result);
|
||||
});
|
||||
|
||||
intercept(widget, 'load_action', function (event) {
|
||||
mockServer.performRpc('/web/action/load', {
|
||||
action_id: event.data.actionID,
|
||||
additional_context: event.data.context,
|
||||
}).then(function (action) {
|
||||
event.data.on_success(action);
|
||||
});
|
||||
intercept(widget, 'load_action', async ev => {
|
||||
const action = await env.dataManager.load_action(ev.data.actionID, ev.data.context);
|
||||
ev.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);
|
||||
});
|
||||
if ('search' in views && params.favoriteFilters) {
|
||||
views.search.favoriteFilters = params.favoriteFilters;
|
||||
}
|
||||
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);
|
||||
intercept(widget, "load_views", async ev => {
|
||||
const params = {
|
||||
model: ev.data.modelName,
|
||||
context: ev.data.context,
|
||||
views_descr: ev.data.views,
|
||||
};
|
||||
const views = await env.dataManager.load_views(params, ev.data.options);
|
||||
if ('search' in views && params.favoriteFilters) {
|
||||
views.search.favoriteFilters = params.favoriteFilters;
|
||||
}
|
||||
event.data.on_success([]);
|
||||
ev.data.on_success(views);
|
||||
});
|
||||
intercept(widget, "get_session", ev => {
|
||||
ev.data.callback(session);
|
||||
});
|
||||
intercept(widget, "load_filters", async ev => {
|
||||
const filters = await env.dataManager.load_filters(ev.data);
|
||||
ev.data.on_success(filters);
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for asynchronous services to properly start
|
||||
await new Promise(setTimeout);
|
||||
// make sure all other Odoo events bubbling up are intercepted
|
||||
Object.keys(params.intercepts || {}).forEach(function (name) {
|
||||
intercept(widget, name, params.intercepts[name]);
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mocked environment to be used by OWL components in tests.
|
||||
*
|
||||
* @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 {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 = {}) {
|
||||
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 env = Object.assign({
|
||||
dataManager: {
|
||||
load_action: (actionID, context) => {
|
||||
return server.performRpc('/web/action/load', {
|
||||
kwargs: {
|
||||
action_id: actionID,
|
||||
additional_context: context,
|
||||
},
|
||||
});
|
||||
},
|
||||
load_views: (params, options) => {
|
||||
return server.performRpc('/web/dataset/call_kw/' + params.model, {
|
||||
args: [],
|
||||
kwargs: {
|
||||
context: params.context,
|
||||
options: options,
|
||||
views: params.views_descr,
|
||||
},
|
||||
method: 'load_views',
|
||||
model: params.model,
|
||||
}).then(function (views) {
|
||||
return _.mapObject(views, viewParams => {
|
||||
return fieldsViewGet(server, viewParams);
|
||||
});
|
||||
});
|
||||
},
|
||||
load_filters: params => {
|
||||
if (params.debug) {
|
||||
console.log('[mock] load_filters', params);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
},
|
||||
session: params.session || {},
|
||||
}, params.env);
|
||||
return makeTestEnvironment(env, server.performRpc.bind(server));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -644,7 +756,7 @@ function patchSetTimeout() {
|
||||
return {
|
||||
addMockEnvironment: addMockEnvironment,
|
||||
fieldsViewGet: fieldsViewGet,
|
||||
getMockedOwlEnv: getMockedOwlEnv,
|
||||
addMockEnvironmentOwl: addMockEnvironmentOwl,
|
||||
intercept: intercept,
|
||||
patchDate: patchDate,
|
||||
patch: patch,
|
||||
|
||||
@@ -5,9 +5,9 @@ odoo.define('web.OwlCompatibilityTests', function (require) {
|
||||
const testUtils = require('web.test_utils');
|
||||
const Widget = require('web.Widget');
|
||||
|
||||
const getMockedOwlEnv = testUtils.mock.getMockedOwlEnv;
|
||||
const makeTestPromise = testUtils.makeTestPromise;
|
||||
const nextTick = testUtils.nextTick;
|
||||
const addMockEnvironmentOwl = testUtils.mock.addMockEnvironmentOwl;
|
||||
|
||||
const { Component, tags, useState } = owl;
|
||||
const { xml } = tags;
|
||||
@@ -448,17 +448,17 @@ odoo.define('web.OwlCompatibilityTests', function (require) {
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
}
|
||||
Parent.env = getMockedOwlEnv({
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(`${route} ${args.val}`);
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter Component="MyWidget"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
const cleanUp = await addMockEnvironmentOwl(Parent, {
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(`${route} ${args.val}`);
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
@@ -468,6 +468,7 @@ odoo.define('web.OwlCompatibilityTests', function (require) {
|
||||
assert.verifySteps(['some/route 2']);
|
||||
|
||||
parent.destroy();
|
||||
cleanUp();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget that calls a service", async function (assert) {
|
||||
@@ -493,16 +494,14 @@ odoo.define('web.OwlCompatibilityTests', function (require) {
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
}
|
||||
const env = getMockedOwlEnv();
|
||||
env.services.math = {
|
||||
sqrt: v => Math.sqrt(v),
|
||||
};
|
||||
Parent.env = env;
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter Component="MyWidget"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
Parent.env.services.math = {
|
||||
sqrt: v => Math.sqrt(v),
|
||||
};
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
@@ -525,20 +524,21 @@ odoo.define('web.OwlCompatibilityTests', function (require) {
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
}
|
||||
Parent.env = getMockedOwlEnv({
|
||||
session: { key: 'value' },
|
||||
});
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter Component="MyWidget"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
const cleanUp = await addMockEnvironmentOwl(Parent, {
|
||||
session: { key: 'value' },
|
||||
});
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
await parent.mount(target);
|
||||
|
||||
parent.destroy();
|
||||
cleanUp();
|
||||
});
|
||||
|
||||
QUnit.test("sub widget that calls load_views", async function (assert) {
|
||||
@@ -555,7 +555,12 @@ odoo.define('web.OwlCompatibilityTests', function (require) {
|
||||
this.MyWidget = MyWidget;
|
||||
}
|
||||
}
|
||||
Parent.env = getMockedOwlEnv({
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter Component="MyWidget"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
const cleanUp = await addMockEnvironmentOwl(Parent, {
|
||||
mockRPC: function (route, args) {
|
||||
assert.strictEqual(route, '/web/dataset/call_kw/some_model');
|
||||
assert.deepEqual(args.kwargs.context, { x: 2 });
|
||||
@@ -563,11 +568,6 @@ odoo.define('web.OwlCompatibilityTests', function (require) {
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<ComponentAdapter Component="MyWidget"/>
|
||||
</div>`;
|
||||
Parent.components = { ComponentAdapter };
|
||||
|
||||
const target = testUtils.prepareTarget();
|
||||
const parent = new Parent();
|
||||
@@ -576,6 +576,7 @@ odoo.define('web.OwlCompatibilityTests', function (require) {
|
||||
assert.strictEqual(parent.el.innerHTML, '<div></div>');
|
||||
|
||||
parent.destroy();
|
||||
cleanUp();
|
||||
});
|
||||
|
||||
QUnit.test("sub widgets in a t-if/t-else", async function (assert) {
|
||||
|
||||
@@ -12,8 +12,7 @@ var createView = testUtils.createView;
|
||||
|
||||
async function createParent(params) {
|
||||
var widget = new Widget();
|
||||
params.server = await testUtils.mock.addMockEnvironment(widget, params);
|
||||
owl.Component.env = testUtils.mock.getMockedOwlEnv(params);
|
||||
params.server = await testUtils.mock.addMockEnvironment(widget, params);
|
||||
return widget;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user