diff options
Diffstat (limited to 'addons/web/static/tests/helpers/test_utils_mock.js')
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils_mock.js | 706 |
1 files changed, 409 insertions, 297 deletions
diff --git a/addons/web/static/tests/helpers/test_utils_mock.js b/addons/web/static/tests/helpers/test_utils_mock.js index 414a9c0df64..6df49cf59f3 100644 --- a/addons/web/static/tests/helpers/test_utils_mock.js +++ b/addons/web/static/tests/helpers/test_utils_mock.js @@ -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; @@ -26,6 +30,199 @@ const DebouncedField = basic_fields.DebouncedField; //------------------------------------------------------------------------------ /** + * 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. * * @param {Widget} widget @@ -43,15 +240,57 @@ function _observe(widget) { //------------------------------------------------------------------------------ /** + * 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,8 +307,8 @@ function removeSrcAttribute(el, widget) { } else { node.setAttribute('src', 'about:blank'); } - if (widget) { - widget._rpc({ route: src }); + if (rpc) { + rpc(src, []); } $(node).trigger('load'); } @@ -77,6 +316,127 @@ function removeSrcAttribute(el, widget) { } /** + * 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, * or the translation settings. @@ -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,311 +512,61 @@ 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); - }); - - // 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; - }; + // 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; - 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, "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; + } + ev.data.on_success(views); }); - - intercept(widget, "get_session", function (event) { - event.data.callback(session); + intercept(widget, "get_session", ev => { + ev.data.callback(session); }); - - intercept(widget, "load_filters", function (event) { - if (params.debug) { - console.log('[mock] load_filters', event.data); - } - event.data.on_success([]); + 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. * * Usage: @@ -644,7 +756,7 @@ function patchSetTimeout() { return { addMockEnvironment: addMockEnvironment, fieldsViewGet: fieldsViewGet, - getMockedOwlEnv: getMockedOwlEnv, + addMockEnvironmentOwl: addMockEnvironmentOwl, intercept: intercept, patchDate: patchDate, patch: patch, |
