diff options
| author | Aaron Bohy <aab@odoo.com> | 2020-06-05 07:20:11 +0000 |
|---|---|---|
| committer | Aaron Bohy <aab@odoo.com> | 2020-06-12 09:47:48 +0000 |
| commit | dc260c324170a90fa737cd21dd528e39d658d9b6 (patch) | |
| tree | 9d839c10e56f7fa913e7f761bc8b2c5debc3d28b /addons/web | |
| parent | 26fe23201bb495176d14c15c8866e8fc72ca044c (diff) | |
[IMP] web: tests: rework mock helpers
This commit reworks the test helpers, following recent changes on
services deployed in the Owl env.
Diffstat (limited to 'addons/web')
| -rw-r--r-- | addons/web/static/tests/helpers/test_env.js | 34 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils.js | 2 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils_create.js | 23 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils_mock.js | 706 | ||||
| -rw-r--r-- | addons/web/static/tests/owl_compatibility_tests.js | 43 | ||||
| -rw-r--r-- | addons/web/static/tests/views/view_dialogs_tests.js | 3 |
6 files changed, 454 insertions, 357 deletions
diff --git a/addons/web/static/tests/helpers/test_env.js b/addons/web/static/tests/helpers/test_env.js index 5b197451517..5a3256e98fd 100644 --- a/addons/web/static/tests/helpers/test_env.js +++ b/addons/web/static/tests/helpers/test_env.js @@ -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); } /** diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index f6114b84fcd..1a1f38f7f09 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -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, diff --git a/addons/web/static/tests/helpers/test_utils_create.js b/addons/web/static/tests/helpers/test_utils_create.js index 494a0aa2436..801120bf64b 100644 --- a/addons/web/static/tests/helpers/test_utils_create.js +++ b/addons/web/static/tests/helpers/test_utils_create.js @@ -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; 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, diff --git a/addons/web/static/tests/owl_compatibility_tests.js b/addons/web/static/tests/owl_compatibility_tests.js index f49fee5f78a..1cc463d0eeb 100644 --- a/addons/web/static/tests/owl_compatibility_tests.js +++ b/addons/web/static/tests/owl_compatibility_tests.js @@ -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) { diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js index 26fc002605a..7d1554dc78d 100644 --- a/addons/web/static/tests/views/view_dialogs_tests.js +++ b/addons/web/static/tests/views/view_dialogs_tests.js @@ -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; } |
