From 850427e4bac74ca52c496071db17fd3bc67a8327 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 2 Aug 2023 13:17:45 +0000 Subject: [PATCH] [REF] web,mail: remove legacy MockServer and related utils This commit removes the legacy implementation of the MockServer and test utils making it available in the test environment (e.g. addMockEnvironment). Part of task~3439226 closes odoo/odoo#130671 Related: odoo/enterprise#45141 Signed-off-by: Francois Georis (fge) --- .../static/tests/helpers/webclient_setup.js | 27 +- .../static/tests/legacy/core/dialog_tests.js | 7 +- .../static/tests/legacy/core/widget_tests.js | 23 +- .../keyboard_navigation_mixin_tests.js | 5 +- .../tests/legacy/helpers/mock_server.js | 2240 ----------------- .../static/tests/legacy/helpers/test_utils.js | 18 - .../tests/legacy/helpers/test_utils_mock.js | 428 +--- .../legacy/mock_relational_fields_tests.js | 137 - .../static/tests/legacy/mockserver_tests.js | 465 ---- .../tests/legacy/owl_compatibility_tests.js | 63 - .../widgets/name_and_signature_tests.js | 9 +- .../views/fields/many2one_field_tests.js | 1 - .../views/fields/one2many_field_tests.js | 4 - addons/web/static/tests/views/helpers.js | 19 - addons/web/static/tests/webclient/helpers.js | 111 +- addons/web_editor/static/tests/test_utils.js | 23 - 16 files changed, 32 insertions(+), 3548 deletions(-) delete mode 100644 addons/web/static/tests/legacy/helpers/mock_server.js delete mode 100644 addons/web/static/tests/legacy/mock_relational_fields_tests.js delete mode 100644 addons/web/static/tests/legacy/mockserver_tests.js diff --git a/addons/mail/static/tests/helpers/webclient_setup.js b/addons/mail/static/tests/helpers/webclient_setup.js index 4001f807e05..3a6c40e6faf 100644 --- a/addons/mail/static/tests/helpers/webclient_setup.js +++ b/addons/mail/static/tests/helpers/webclient_setup.js @@ -15,13 +15,7 @@ const ROUTES_TO_IGNORE = [ "/web/dataset/call_kw/res.users/load_views", "/web/dataset/call_kw/res.users/systray_get_activities", ]; -const WEBCLIENT_PARAMETER_NAMES = new Set([ - "legacyParams", - "mockRPC", - "serverData", - "target", - "webClientClass", -]); +const WEBCLIENT_PARAMETER_NAMES = new Set(["mockRPC", "serverData", "target", "webClientClass"]); const SERVICES_PARAMETER_NAMES = new Set([ "legacyServices", "loadingBaseDelayDuration", @@ -129,17 +123,14 @@ export const setupManager = { */ setupServiceRegistries({ loadingBaseDelayDuration = 0, messagingBus, services = {} } = {}) { const OriginalAudio = window.Audio; - patchWithCleanup( - window, - { - Audio: function () { - const audio = new OriginalAudio(); - audio.preload = "none"; - audio.play = () => {}; - return audio; - }, - } - ); + patchWithCleanup(window, { + Audio: function () { + const audio = new OriginalAudio(); + audio.preload = "none"; + audio.play = () => {}; + return audio; + }, + }); patchWithCleanup(session, { show_effect: true }); services["messagingValues"] = services["messagingValues"] ?? { start() { diff --git a/addons/web/static/tests/legacy/core/dialog_tests.js b/addons/web/static/tests/legacy/core/dialog_tests.js index e1200c4712e..882127e6fa5 100644 --- a/addons/web/static/tests/legacy/core/dialog_tests.js +++ b/addons/web/static/tests/legacy/core/dialog_tests.js @@ -7,12 +7,7 @@ import Widget from "@web/legacy/js/core/widget"; var ESCAPE_KEY = $.Event("keyup", { which: 27 }); async function createEmptyParent(debug) { - var widget = new Widget(); - - await testUtils.mock.addMockEnvironment(widget, { - debug: debug || false, - }); - return widget; + return new Widget(); } QUnit.module('core', {}, function () { diff --git a/addons/web/static/tests/legacy/core/widget_tests.js b/addons/web/static/tests/legacy/core/widget_tests.js index 5996dba78cf..bd459904d90 100644 --- a/addons/web/static/tests/legacy/core/widget_tests.js +++ b/addons/web/static/tests/legacy/core/widget_tests.js @@ -5,6 +5,8 @@ import core from "@web/legacy/js/services/core"; import Dialog from "@web/legacy/js/core/dialog"; import QWeb from "@web/legacy/js/core/qweb"; import Widget from "@web/legacy/js/core/widget"; +import session from "web.session"; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; import testUtils from "@web/../tests/legacy/helpers/test_utils"; QUnit.module('core', {}, function () { @@ -401,18 +403,19 @@ QUnit.module('core', {}, function () { var def; var parent = new Widget(); - await testUtils.mock.addMockEnvironment(parent, { - session: { - rpc: function () { - def = testUtils.makeTestPromise(); - def.abort = def.reject; - return def; - }, - }, - services: { - ajax: AjaxService + patchWithCleanup(session, { + rpc: () => { + def = testUtils.makeTestPromise(); + def.abort = def.reject; + return def; }, }); + var ajaxService = new AjaxService(); + testUtils.mock.intercept(parent, 'call_service', function ({ data }) { + if (data.service === "ajax" && data.method === "rpc") { + data.callback(ajaxService.rpc(...data.args)); + } + }); var widget = new Widget(parent); widget._rpc({route: '/a/route'}).then(function () { diff --git a/addons/web/static/tests/legacy/frontend/keyboard_navigation_mixin_tests.js b/addons/web/static/tests/legacy/frontend/keyboard_navigation_mixin_tests.js index a22120ac072..dfa93227cd5 100644 --- a/addons/web/static/tests/legacy/frontend/keyboard_navigation_mixin_tests.js +++ b/addons/web/static/tests/legacy/frontend/keyboard_navigation_mixin_tests.js @@ -1,7 +1,6 @@ /** @odoo-module **/ import KeyboardNavigationMixin from "@web/legacy/frontend/keyboard_navigation_mixin"; -import testUtils from "@web/../tests/legacy/helpers/test_utils"; import Widget from "@web/legacy/js/core/widget"; QUnit.module('KeyboardNavigationMixin', function () { @@ -25,7 +24,7 @@ QUnit.module('KeyboardNavigationMixin', function () { return this._super(...arguments); }, }); - var parent = await testUtils.createParent({}); + var parent = new Widget(); var w = new KeyboardWidget(parent); await w.appendTo($target); @@ -66,7 +65,7 @@ QUnit.module('KeyboardNavigationMixin', function () { return this._super(...arguments); }, }); - var parent = await testUtils.createParent({}); + var parent = new Widget(); var w = new KeyboardWidget(parent); await w.appendTo($target); diff --git a/addons/web/static/tests/legacy/helpers/mock_server.js b/addons/web/static/tests/legacy/helpers/mock_server.js deleted file mode 100644 index 697c05782ce..00000000000 --- a/addons/web/static/tests/legacy/helpers/mock_server.js +++ /dev/null @@ -1,2240 +0,0 @@ -/** @odoo-module **/ - -import { Domain } from "@web/core/domain"; -import { unique } from "@web/core/utils/arrays"; -import { pick } from "@web/core/utils/objects"; -import Class from "@web/legacy/js/core/class"; -import { evaluateExpr } from "@web/core/py_js/py"; -import { - parseDateTime, - serializeDate, - serializeDateTime, - deserializeDate, - deserializeDateTime -} from "@web/core/l10n/dates"; - -var MockServer = Class.extend({ - /** - * @constructor - * @param {Object} data - * @param {Object} options - * @param {Object[]} [options.actions=[]] - * @param {Object} [options.archs={}] dict of archs with keys being strings like - * 'model,id,viewType' - * @param {boolean} [options.debug=false] logs RPCs if set to true - * @param {string} [options.currentDate] formatted string, default to - * current day - */ - init: function (data, options) { - options = options || {}; - this.data = data; - for (var modelName in this.data) { - var model = this.data[modelName]; - if (!('id' in model.fields)) { - model.fields.id = {string: "ID", type: "integer"}; - } - if (!('display_name' in model.fields)) { - model.fields.display_name = {string: "Display Name", type: "char"}; - } - if (!('write_date' in model.fields)) { - model.fields.write_date = {string: "Last Updated on", type: "datetime"}; - } - if (!('name' in model.fields)) { - model.fields.name = {string: "Name", type: "char", default: "name"}; - } - model.records = model.records || []; - - for (var i = 0; i < model.records.length; i++) { - const values = model.records[i]; - // add potentially missing id - const id = values.id === undefined - ? this._getUnusedID(modelName) : - values.id; - // create a clean object, initial values are passed to write - model.records[i] = { id }; - // ensure initial data goes through proper conversion (x2m, ...) - this._applyDefaults(model, values); - this._writeRecord(modelName, values, id, { - ensureIntegrity: false, - }); - } - } - // used to prevent _updateComodelRelationalFields to be trigerred during - // initial record creation. - this.isInitialized = true; - - // fill relational fields' inverse. - for (const modelName in this.data) { - this.data[modelName].records.forEach(record => this._updateComodelRelationalFields(modelName, record)); - } - this.debug = options.debug; - - this.actions = options.actions || []; - this.archs = options.archs || {}; - }, - - /** - * Perform asynchronous setup after the initialization of the mockServer. - */ - setup: async function () {}, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Mocks a fields_get RPC for a given model. - * - * @param {string} model - * @returns {Object} - */ - fieldsGet: function (model) { - return this.data[model].fields; - }, - /** - * helper: read a string describing an arch, and returns a simulated - * 'view_get' call to the server. Calls processViews() of data_manager - * to mimick the real behavior of a call to loadViews(). - * - * @param {Object} params - * @param {string|Object} params.arch a string OR a parsed xml document - * @param {Number} [params.view_id] the id of the arch's view - * @param {string} params.model a model name (that should be in this.data) - * @param {Object} params.toolbar the actions possible in the toolbar - * @param {Object} [params.viewOptions] the view options set in the test (optional) - * @returns {Object} an object with 2 keys: arch and fields - */ - getView: function (params) { - var model = params.model; - var toolbar = params.toolbar; - var viewId = params.view_id; - var viewOptions = params.viewOptions || {}; - if (!(model in this.data)) { - throw new Error('Model ' + model + ' was not defined in mock server data'); - } - var fields = $.extend(true, {}, this.data[model].fields); - var view = this._getView(params.arch, model, fields, viewOptions.context || {}); - if (toolbar) { - view.toolbar = toolbar; - } - if (viewId) { - view.id = viewId; - } - return view; - }, - /** - * Simulates a complete fetch call. - * - * @param {string} resource - * @param {Object} init - * @returns {any} - */ - async performFetch(resource, init) { - if (this.debug) { - console.log( - '%c[fetch] request ' + resource, 'color: blue; font-weight: bold;', - JSON.parse(JSON.stringify(init)) - ); - } - const res = await this._performFetch(resource, init); - if (this.debug) { - console.log('%c[fetch] response' + resource, 'color: blue; font-weight: bold;', res); - } - return res; - }, - /** - * Simulate a complete RPC call. This is the main method for this class. - * - * This method also log incoming and outgoing data, and stringify/parse data - * to simulate a barrier between the server and the client. It also simulate - * server errors. - * - * @param {string} route - * @param {Object} args - * @returns {Promise} - * Resolved with the result of the RPC, stringified then parsed. - * If the RPC should fail, the promise will be rejected with the - * error object, stringified then parsed. - */ - performRpc: function (route, args) { - var debug = this.debug; - args = JSON.parse(JSON.stringify(args)); - if (debug) { - console.log('%c[rpc] request ' + route, 'color: blue; font-weight: bold;', args); - args = JSON.parse(JSON.stringify(args)); - } - var def = this._performRpc(route, args); - - var abort = def.abort || def.reject; - if (abort) { - abort = abort.bind(def); - } else { - abort = function (rejectError = true) { - if (rejectError) { - throw new Error("XmlHttpRequestError abort"); - } - } - } - - def = def.then(function (result) { - var resultString = JSON.stringify(result || false); - if (debug) { - console.log('%c[rpc] response' + route, 'color: blue; font-weight: bold;', JSON.parse(resultString)); - } - return JSON.parse(resultString); - }, function (result) { - var message = result && result.message; - var event = result && result.event; - var errorString = typeof message !== "string" ? JSON.stringify(message || false) : message; - if (debug) { - console.warn( - '%c[rpc] response (error) %s%s, during test %s', - 'color: orange; font-weight: bold;', - route, - message != null && ` -> ${errorString}`, - JSON.stringify(QUnit.config.current.testName) - ); - } - return Promise.reject({message: errorString, event: event || $.Event()}); - }); - - def.abort = abort; - return def; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Apply the default values when creating an object in the local database. - * - * @private - * @param {Object} model a model object from the local database - * @param {Object} record - */ - _applyDefaults: function (model, record) { - record.display_name = record.display_name || record.name; - for (var fieldName in model.fields) { - if (fieldName === 'id') { - continue; - } - if (!(fieldName in record)) { - if ('default' in model.fields[fieldName]) { - const def = model.fields[fieldName].default; - record[fieldName] = typeof def === 'function' ? def.call(this) : def; - } else if (["one2many", "many2many"].includes(model.fields[fieldName].type)) { - record[fieldName] = []; - } else { - record[fieldName] = false; - } - } - } - }, - /** - * Converts an Object representing a record to actual return Object of the - * python `onchange` method. - * Specifically, it reads `display_name` on many2one's and transforms raw id - * list in orm command lists for x2many's. - * For x2m fields that add or update records (ORM commands 0 and 1), it is - * recursive. - * - * @private - * @param {string} model: the model's name - * @param {Object} values: an object representing a record - * @returns {Object} - */ - _convertToOnChange(model, values) { - Object.entries(values).forEach(([fname, val]) => { - const field = this.data[model].fields[fname]; - if (field.type === 'many2one' && typeof val === 'number') { - const m2oRecord = this.data[field.relation].records.find(r => r.id === val); - values[fname] = [val, m2oRecord.display_name]; - } else if (field.type === 'one2many' || field.type === 'many2many') { - // TESTS ONLY - // one2many_ids = [1,2,3] is a simpler way to express it than orm commands - const isCommandList = val.length && Array.isArray(val[0]); - if (!isCommandList) { - values[fname] = [[6, false, val]]; - } else { - val.forEach(cmd => { - if (cmd[0] === 0 || cmd[0] === 1) { - cmd[2] = this._convertToOnChange(field.relation, cmd[2]); - } - }); - } - } - }); - return values; - }, - /** - * helper to evaluate a domain for given field values. - * Currently, this is only a wrapper of the Domain.compute function in - * "web.Domain". - * - * @param {Array} domain - * @param {Object} fieldValues - * @returns {boolean} - */ - _evaluateDomain: function (domain, fieldValues) { - return new Domain(domain).contains(fieldValues); - }, - /** - * helper: read a string describing an arch, and returns a simulated - * 'get_view' call to the server. - * - * @private - * @param {string} arch a string OR a parsed xml document - * @param {string} model a model name (that should be in this.data) - * @param {Object} fields - * @param {Object} context - * @returns {Object} an object with 2 keys: arch and fields (the fields - * appearing in the views) - */ - _getView: function (arch, model, fields, context) { - var self = this; - var modifiersNames = ['invisible', 'readonly', 'required']; - var onchanges = this.data[model].onchanges || {}; - var fieldNodes = {}; - var groupbyNodes = {}; - const relatedModels = new Set([model]); - - var doc; - if (typeof arch === 'string') { - doc = $.parseXML(arch).documentElement; - } else { - doc = arch; - } - - var inTreeView = (doc.tagName === 'tree'); - - // mock _postprocess_access_rights - const isBaseModel = !context.base_model_name || (model === context.base_model_name); - var views = ['kanban', 'tree', 'form', 'gantt', 'activity']; - if (isBaseModel && views.indexOf(doc.tagName) !== -1) { - for (let action of ['create', 'delete', 'edit', 'write']) { - if (!doc.getAttribute(action) && action in context && !context[action]) { - doc.setAttribute(action, 'false'); - } - } - } - - this._traverse(doc, function (node) { - if (node.nodeType === Node.TEXT_NODE) { - return false; - } - var modifiers = {}; - - var isField = (node.tagName === 'field'); - var isGroupby = (node.tagName === 'groupby'); - - if (isField) { - var fieldName = node.getAttribute('name'); - fieldNodes[fieldName] = node; - - // 'transfer_field_to_modifiers' simulation - var field = fields[fieldName]; - - if (!field) { - throw new Error("Field " + fieldName + " does not exist"); - } - var defaultValues = {}; - var stateExceptions = {}; - modifiersNames.forEach((attr) => { - stateExceptions[attr] = []; - defaultValues[attr] = !!field[attr]; - }); - for (const [state, modifs] of Object.entries(field.states || {})) { - modifs.forEach((modif) => { - if (defaultValues[modif[0]] !== modif[1]) { - stateExceptions[modif[0]].append(state); - } - }); - } - for (const [attr, defaultValue] of Object.entries(defaultValues)) { - if (stateExceptions[attr].length) { - modifiers[attr] = [("state", defaultValue ? "not in" : "in", stateExceptions[attr])]; - } else { - modifiers[attr] = defaultValue; - } - } - } else if (isGroupby && !node._isProcessed) { - var groupbyName = node.getAttribute('name'); - fieldNodes[groupbyName] = node; - groupbyNodes[groupbyName] = node; - } - - // 'transfer_node_to_modifiers' simulation - var attrs = node.getAttribute('attrs'); - if (attrs) { - attrs = evaluateExpr(attrs); - Object.assign(modifiers, attrs); - } - - var states = node.getAttribute('states'); - if (states) { - if (!modifiers.invisible) { - modifiers.invisible = []; - } - modifiers.invisible.push(["state", "not in", states.split(",")]); - } - - const inListHeader = inTreeView && node.closest('header'); - modifiersNames.forEach((a) => { - var mod = node.getAttribute(a); - if (mod) { - var v = evaluateExpr(mod, context || {}) ? true: false; - if (inTreeView && !inListHeader && a === 'invisible') { - modifiers.column_invisible = v; - } else if (v || !(a in modifiers) || !Array.isArray(modifiers[a])) { - modifiers[a] = v; - } - } - }); - - modifiersNames.forEach((a) => { - if (a in modifiers && (!!modifiers[a] === false || (Array.isArray(modifiers[a]) && !modifiers[a].length))) { - delete modifiers[a]; - } - }); - - if (Object.keys(modifiers).length) { - node.setAttribute('modifiers', JSON.stringify(modifiers)); - } - - if (isGroupby && !node._isProcessed) { - return false; - } - - return !isField; - }); - - let relModel, relFields; - for (const [name, node] of Object.entries(fieldNodes)) { - var field = fields[name]; - if (field.type === "many2one" || field.type === "many2many") { - var canCreate = node.getAttribute('can_create'); - node.setAttribute('can_create', canCreate || "true"); - var canWrite = node.getAttribute('can_write'); - node.setAttribute('can_write', canWrite || "true"); - } - if (field.type === "one2many" || field.type === "many2many") { - relModel = field.relation; - relatedModels.add(relModel); - node.childNodes.forEach((childNode) => { - if (childNode.tagName) { // skip text nodes - relFields = $.extend(true, {}, self.data[relModel].fields); - // this is hackhish, but _getView modifies the subview document in place, - // especially to generate the "modifiers" attribute - const { models } = self._getView(childNode, relModel, - relFields, Object.assign({}, context, {base_model_name: model})); - [...models].forEach((modelName) => relatedModels.add(modelName)); - } - }); - } - - // add onchanges - if (name in onchanges) { - node.setAttribute('on_change', "1"); - } - } - for (const [name, node] of Object.entries(groupbyNodes)) { - var field = fields[name]; - if (field.type !== 'many2one') { - throw new Error('groupby can only target many2one'); - } - field.views = {}; - relModel = field.relation; - relatedModels.add(relModel); - relFields = $.extend(true, {}, self.data[relModel].fields); - node._isProcessed = true; - // postprocess simulation - const { models } = self._getView(node, relModel, relFields, context); - [...models].forEach((modelName) => relatedModels.add(modelName)); - } - - var xmlSerializer = new XMLSerializer(); - var processedArch = xmlSerializer.serializeToString(doc); - return { - arch: processedArch, - model: model, - type: doc.tagName === 'tree' ? 'list' : doc.tagName, - models: relatedModels, - }; - }, - /** - * Get all records from a model matching a domain. The only difficulty is - * that if we have an 'active' field, we implicitely add active = true in - * the domain. - * - * @private - * @param {string} model a model name - * @param {any[]} domain - * @param {Object} [params={}] - * @param {boolean} [params.active_test=true] - * @returns {Object[]} a list of records - */ - _getRecords: function (model, domain, { active_test = true } = {}) { - if (!Array.isArray(domain)) { - throw new Error("MockServer._getRecords: given domain has to be an array."); - } - - var self = this; - var records = this.data[model].records; - - if (active_test && 'active' in this.data[model].fields) { - // add ['active', '=', true] to the domain if 'active' is not yet present in domain - var activeInDomain = false; - domain.forEach((subdomain) => { - activeInDomain = activeInDomain || subdomain[0] === 'active'; - }); - if (!activeInDomain) { - domain = [['active', '=', true]].concat(domain); - } - } - - if (domain.length) { - domain = domain.map((criterion) => { - // 'child_of' operator isn't supported by domain.js, so we replace - // in by the 'in' operator (with the ids of children) - if (criterion[1] === 'child_of') { - var oldLength = 0; - var childIDs = [criterion[2]]; - while (childIDs.length > oldLength) { - oldLength = childIDs.length; - records.forEach((r) => { - if (childIDs.indexOf(r.parent_id) >= 0) { - childIDs.push(r.id); - } - }); - } - criterion = [criterion[0], 'in', childIDs]; - } - // In case of many2many field, if domain operator is '=' generally change it to 'in' operator - const field = this.data[model].fields[criterion[0]] || {}; - if (field.type === "many2many" && criterion[1] === "=") { - if (criterion[2] === false) { - // if undefined value asked, domain.js require equality with empty array - criterion = [criterion[0], "=", []]; - } else { - criterion = [criterion[0], "in", [criterion[2]]]; - } - } - return criterion; - }); - records = records.filter(record => self._evaluateDomain(domain, record)); - } - - return records; - }, - /** - * Helper function, to find an available ID. The current algorithm is to - * return the currently highest id + 1. - * - * @private - * @param {string} modelName - * @returns {integer} a valid ID (> 0) - */ - _getUnusedID: function (modelName) { - var model = this.data[modelName]; - return model.records.reduce((max, record) => { - if (!Number.isInteger(record.id)) { - return max; - } - return Math.max(record.id, max); - }, 0) + 1; - }, - /** - * Simulate a 'call_button' operation from a view. - * - * @private - * @param {Object} param0 - * @param {Array} param0.args - * @param {Object} [param0.kargs] - * @param {string} param0.method - * @param {string} param0.model - * @returns {any} - * @throws {Error} in case the call button of provided model/method is not - * implemented. - */ - _mockCallButton({ args, kwargs, method, model }) { - throw new Error(`Unimplemented mocked call button on "${model}"/"${method}"`); - }, - /** - * Simulate a 'copy' operation, so we simply try to duplicate a record in - * memory - * - * @private - * @param {string} modelName - * @param {integer} id the ID of a valid record - * @returns {integer} the ID of the duplicated record - */ - _mockCopy: function (modelName, id) { - var model = this.data[modelName]; - var newID = this._getUnusedID(modelName); - var originalRecord = model.records.find((r) => r.id === id); - var duplicateRecord = Object.assign({}, originalRecord, { id: newID }); - duplicateRecord.display_name = originalRecord.display_name + " (copy)"; - model.records.push(duplicateRecord); - return newID; - }, - /** - * Simulate a 'create' operation. This is basically a 'write' with the - * added work of getting a valid ID and applying default values. - * - * @private - * @param {string} modelName - * @param {Object} values - * @returns {integer} - */ - _mockCreate: function (modelName, values) { - if ('id' in values) { - throw new Error("Cannot create a record with a predefinite id"); - } - var model = this.data[modelName]; - var id = this._getUnusedID(modelName); - var record = {id: id}; - model.records.push(record); - this._applyDefaults(model, values); - this._writeRecord(modelName, values, id); - if (this.isInitialized) { - this._updateComodelRelationalFields(modelName, record); - } - return id; - }, - /** - * Simulate a 'default_get' operation - * - * @private - * @param {string} modelName - * @param {array[]} args a list with a list of fields in the first position - * @param {Object} [kwargs={}] - * @param {Object} [kwargs.context] the context to eventually read default - * values - * @returns {Object} - */ - _mockDefaultGet: function (modelName, args, kwargs = {}) { - const fields = args[0]; - const model = this.data[modelName]; - const result = {}; - for (const fieldName of fields) { - const key = "default_" + fieldName; - if (kwargs.context && key in kwargs.context) { - result[fieldName] = kwargs.context[key]; - continue; - } - const field = model.fields[fieldName]; - if ('default' in field) { - result[fieldName] = field.default; - continue; - } - } - for (const fieldName in result) { - const field = model.fields[fieldName]; - if (field.type === "many2one") { - const recordExists = this.data[field.relation].records.some( - (r) => r.id === result[fieldName] - ); - if (!recordExists) { - delete result[fieldName]; - } - } - } - return result; - }, - /** - * Simulate a 'fields_get' operation - * - * @private - * @param {string} modelName - * @param {any} args - * @returns {Object} - */ - _mockFieldsGet: function (modelName, args) { - var modelFields = this.data[modelName].fields; - // Get only the asked fields (args[0] could be the field names) - if (args[0] && args[0].length) { - modelFields = pick(modelFields, ...Object.keys(modelFields).concat(args[0])); - } - // Get only the asked attributes (args[1] could be the attribute names) - if (args[1] && args[1].length) { - modelFields = pick(modelFields, ...Object.keys(modelFields).concat(args[1])); - } - return modelFields; - }, - /** - * Simulates a call to the server '_search_panel_field_image' method. - * - * @private - * @param {string} model - * @param {string} fieldName - * @param {Object} kwargs - * @see _mockSearchPanelDomainImage() - */ - _mockSearchPanelFieldImage(model, fieldName, kwargs) { - const enableCounters = kwargs.enable_counters; - const onlyCounters = kwargs.only_counters; - const extraDomain = kwargs.extra_domain || []; - const normalizedExtra = Domain.prototype.normalizeArray(extraDomain); - const noExtra = JSON.stringify(normalizedExtra) === "[]"; - const modelDomain = kwargs.model_domain || []; - const countDomain = Domain.prototype.normalizeArray([ - ...modelDomain, - ...extraDomain, - ]); - - const limit = kwargs.limit; - const setLimit = kwargs.set_limit; - - if (onlyCounters) { - return this._mockSearchPanelDomainImage(model, fieldName, countDomain, true); - } - - const modelDomainImage = this._mockSearchPanelDomainImage( - model, - fieldName, - modelDomain, - enableCounters && noExtra, - setLimit && limit - ); - if (enableCounters && !noExtra) { - const countDomainImage = this._mockSearchPanelDomainImage( - model, - fieldName, - countDomain, - true - ); - for (const [id, values] of modelDomainImage.entries()) { - const element = countDomainImage.get(id); - values.__count = element ? element.__count : 0; - } - } - - return modelDomainImage; - }, - - /** - * Simulates a call to the server '_search_panel_domain_image' method. - * - * @private - * @param {string} model - * @param {Array[]} domain - * @param {string} fieldName - * @param {boolean} setCount - * @returns {Map} - */ - _mockSearchPanelDomainImage: function (model, fieldName, domain, setCount=false, limit=false) { - const field = this.data[model].fields[fieldName]; - let groupIdName; - if (field.type === 'many2one') { - groupIdName = value => value || [false, undefined]; - // mockReadGroup does not take care of the condition [fieldName, '!=', false] - // in the domain defined below !!! - } else if (field.type === 'selection') { - const selection = {}; - for (const [value, label] of this.data[model].fields[fieldName].selection) { - selection[value] = label; - } - groupIdName = value => [value, selection[value]]; - } - domain = Domain.prototype.normalizeArray([ - ...domain, - [fieldName, '!=', false], - ]); - const groups = this._mockReadGroup(model, { - domain, - fields: [fieldName], - groupby: [fieldName], - limit, - }); - const domainImage = new Map(); - for (const group of groups) { - const [id, display_name] = groupIdName(group[fieldName]); - const values = { id, display_name }; - if (setCount) { - values.__count = group[fieldName + '_count']; - } - domainImage.set(id, values); - } - return domainImage; - }, - /** - * Simulates a call to the server '_search_panel_global_counters' method. - * - * @private - * @param {Map} valuesRange - * @param {(string|boolean)} parentName 'parent_id' or false - */ - _mockSearchPanelGlobalCounters: function (valuesRange, parentName) { - const localCounters = [...valuesRange.keys()].map(id => valuesRange.get(id).__count); - for (let [id, values] of valuesRange.entries()) { - const count = localCounters[id]; - if (count) { - let parent_id = values[parentName]; - while (parent_id) { - values = valuesRange.get(parent_id); - values.__count += count; - parent_id = values[parentName]; - } - } - } - }, - /** - * Simulates a call to the server '_search_panel_sanitized_parent_hierarchy' method. - * - * @private - * @param {Object[]} records - * @param {(string|boolean)} parentName 'parent_id' or false - * @param {number[]} ids - * @returns {Object[]} - */ - _mockSearchPanelSanitizedParentHierarchy: function (records, parentName, ids) { - const getParentId = record => record[parentName] && record[parentName][0]; - const allowedRecords = {}; - for (const record of records) { - allowedRecords[record.id] = record; - } - const recordsToKeep = {}; - for (const id of ids) { - const ancestorChain = {}; - let recordId = id; - let chainIsFullyIncluded = true; - while (chainIsFullyIncluded && recordId) { - const knownStatus = recordsToKeep[recordId]; - if (knownStatus !== undefined) { - chainIsFullyIncluded = knownStatus; - break; - } - const record = allowedRecords[recordId]; - if (record) { - ancestorChain[recordId] = record; - recordId = getParentId(record); - } else { - chainIsFullyIncluded = false; - } - } - for (const id in ancestorChain) { - recordsToKeep[id] = chainIsFullyIncluded; - } - } - return records.filter(rec => recordsToKeep[rec.id]); - }, - /** - * Simulates a call to the server 'search_panel_selection_range' method. - * - * @private - * @param {string} model - * @param {string} fieldName - * @param {Object} kwargs - * @returns {Object[]} - */ - _mockSearchPanelSelectionRange: function (model, fieldName, kwargs) { - const enableCounters = kwargs.enable_counters; - const expand = kwargs.expand; - let domainImage; - if (enableCounters || !expand) { - const newKwargs = Object.assign({}, kwargs, { - only_counters: expand, - }); - domainImage = this._mockSearchPanelFieldImage(model, fieldName, newKwargs); - } - if (!expand) { - return [...domainImage.values()]; - } - const selection = this.data[model].fields[fieldName].selection; - const selectionRange = []; - for (const [value, label] of selection) { - const values = { - id: value, - display_name: label, - }; - if (enableCounters) { - values.__count = domainImage.get(value) ? domainImage.get(value).__count : 0; - } - selectionRange.push(values); - } - return selectionRange; - }, - /** - * Simulates a call to the server 'search_panel_select_range' method. - * - * @private - * @param {string} model - * @param {string[]} args - * @param {string} args[fieldName] - * @param {Object} [kwargs={}] - * @param {Array[]} [kwargs.category_domain] domain generated by categories - * (this parameter is used in _search_panel_range) - * @param {Array[]} [kwargs.comodel_domain] domain of field values (if relational) - * (this parameter is used in _search_panel_range) - * @param {boolean} [kwargs.enable_counters] whether to count records by value - * @param {Array[]} [kwargs.filter_domain] domain generated by filters - * @param {integer} [kwargs.limit] maximal number of values to fetch - * @param {Array[]} [kwargs.search_domain] base domain of search (this parameter - * is used in _search_panel_range) - * @returns {Object} - */ - _mockSearchPanelSelectRange: function (model, [fieldName], kwargs) { - const field = this.data[model].fields[fieldName]; - const supportedTypes = ['many2one', 'selection']; - if (!supportedTypes.includes(field.type)) { - throw new Error(`Only types ${supportedTypes} are supported for category (found type ${field.type})`); - } - - const modelDomain = kwargs.search_domain || []; - const extraDomain = Domain.prototype.normalizeArray([ - ...(kwargs.category_domain || []), - ...(kwargs.filter_domain || []), - ]); - - if (field.type === 'selection') { - const newKwargs = Object.assign({}, kwargs, { - model_domain: modelDomain, - extra_domain: extraDomain, - }); - kwargs.model_domain = modelDomain; - return { - parent_field: false, - values: this._mockSearchPanelSelectionRange(model, fieldName, newKwargs), - }; - } - - const fieldNames = ['display_name']; - let hierarchize = 'hierarchize' in kwargs ? kwargs.hierarchize : true; - let getParentId; - let parentName = false; - if (hierarchize && this.data[field.relation].fields.parent_id) { - parentName = 'parent_id'; // in tests, parent field is always 'parent_id' - fieldNames.push(parentName); - getParentId = record => record.parent_id && record.parent_id[0]; - } else { - hierarchize = false; - } - let comodelDomain = kwargs.comodel_domain || []; - const enableCounters = kwargs.enable_counters; - const expand = kwargs.expand; - const limit = kwargs.limit; - let domainImage; - if (enableCounters || !expand) { - const newKwargs = Object.assign({}, kwargs, { - model_domain: modelDomain, - extra_domain: extraDomain, - only_counters: expand, - set_limit: limit && !(expand || hierarchize || comodelDomain), - }); - domainImage = this._mockSearchPanelFieldImage(model, fieldName, newKwargs); - } - if (!expand && !hierarchize && !comodelDomain.length) { - if (limit && domainImage.size === limit) { - return { error_msg: "Too many items to display." }; - } - return { - parent_field: parentName, - values: [...domainImage.values()], - }; - } - let imageElementIds; - if (!expand) { - imageElementIds = [...domainImage.keys()].map(Number); - let condition; - if (hierarchize) { - const records = this.data[field.relation].records; - const ancestorIds = new Set(); - for (const id of imageElementIds) { - let recordId = id; - let record; - while (recordId) { - ancestorIds.add(recordId); - record = records.find(rec => rec.id === recordId); - recordId = record[parentName]; - } - } - condition = ['id', 'in', [...new Set(ancestorIds)]]; - } else { - condition = ['id', 'in', imageElementIds]; - } - comodelDomain = Domain.prototype.normalizeArray([ - ...comodelDomain, - condition, - ]); - } - let comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], { limit }); - - if (hierarchize) { - const ids = expand ? comodelRecords.map(rec => rec.id) : imageElementIds; - comodelRecords = this._mockSearchPanelSanitizedParentHierarchy(comodelRecords, parentName, ids); - } - - if (limit && comodelRecords.length === limit) { - return { error_msg: "Too many items to display." }; - } - // A map is used to keep the initial order. - const fieldRange = new Map(); - for (const record of comodelRecords) { - const values = { - id: record.id, - display_name: record.display_name, - }; - if (hierarchize) { - values[parentName] = getParentId(record); - } - if (enableCounters) { - values.__count = domainImage.get(record.id) ? domainImage.get(record.id).__count : 0; - } - fieldRange.set(record.id, values); - } - - if (hierarchize && enableCounters) { - this._mockSearchPanelGlobalCounters(fieldRange, parentName); - } - - return { - parent_field: parentName, - values: [...fieldRange.values()], - }; - }, - /** - * Simulates a call to the server 'search_panel_select_multi_range' method. - * - * @private - * @param {string} model - * @param {string[]} args - * @param {string} args[fieldName] - * @param {Object} [kwargs={}] - * @param {Array[]} [kwargs.category_domain] domain generated by categories - * @param {Array[]} [kwargs.comodel_domain] domain of field values (if relational) - * (this parameter is used in _search_panel_range) - * @param {boolean} [kwargs.enable_counters] whether to count records by value - * @param {Array[]} [kwargs.filter_domain] domain generated by filters - * @param {string} [kwargs.group_by] extra field to read on comodel, to group - * comodel records - * @param {Array[]} [kwargs.group_domain] dict, one domain for each activated - * group for the group_by (if any). Those domains are used to fech accurate - * counters for values in each group - * @param {integer} [kwargs.limit] maximal number of values to fetch - * @param {Array[]} [kwargs.search_domain] base domain of search - * @returns {Object} - */ - _mockSearchPanelSelectMultiRange: function (model, [fieldName], kwargs) { - const field = this.data[model].fields[fieldName]; - const supportedTypes = ['many2one', 'many2many', 'selection']; - if (!supportedTypes.includes(field.type)) { - throw new Error(`Only types ${supportedTypes} are supported for filter (found type ${field.type})`); - } - let modelDomain = kwargs.search_domain || []; - let extraDomain = Domain.prototype.normalizeArray([ - ...(kwargs.category_domain || []), - ...(kwargs.filter_domain || []), - ]); - if (field.type === 'selection') { - const newKwargs = Object.assign({}, kwargs, { - model_domain: modelDomain, - extra_domain: extraDomain, - }); - return { - values: this._mockSearchPanelSelectionRange(model, fieldName, newKwargs), - }; - } - const fieldNames = ['display_name']; - const groupBy = kwargs.group_by; - let groupIdName; - if (groupBy) { - const groupByField = this.data[field.relation].fields[groupBy]; - fieldNames.push(groupBy); - if (groupByField.type === 'many2one') { - groupIdName = value => value || [false, "Not set"]; - } else if (groupByField.type === 'selection') { - const groupBySelection = Object.assign({}, this.data[field.relation].fields[groupBy].selection); - groupBySelection[false] = "Not Set"; - groupIdName = value => [value, groupBySelection[value]]; - } else { - groupIdName = value => value ? [value, value] : [false, "Not set"]; - } - } - let comodelDomain = kwargs.comodel_domain || []; - const enableCounters = kwargs.enable_counters; - const expand = kwargs.expand; - const limit = kwargs.limit; - if (field.type === 'many2many') { - const comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], { limit }); - if (expand && limit && comodelRecords.length === limit) { - return { error_msg: "Too many items to display." }; - } - - const groupDomain = kwargs.group_domain; - const fieldRange = []; - for (const record of comodelRecords) { - const values= { - id: record.id, - display_name: record.display_name, - }; - let groupId; - if (groupBy) { - const [gId, gName] = groupIdName(record[groupBy]); - values.group_id = groupId = gId; - values.group_name = gName; - } - let count; - let inImage; - if (enableCounters || !expand) { - const searchDomain = Domain.prototype.normalizeArray([ - ...modelDomain, - [fieldName, "in", record.id] - ]); - let localExtraDomain = extraDomain; - if (groupBy && groupDomain) { - localExtraDomain = Domain.prototype.normalizeArray([ - ...localExtraDomain, - ...(groupDomain[JSON.stringify(groupId)] || []), - ]); - } - const searchCountDomain = Domain.prototype.normalizeArray([ - ...searchDomain, - ...localExtraDomain, - ]); - if (enableCounters) { - count = this._mockSearchCount(model, [searchCountDomain]); - } - if (!expand) { - if ( - enableCounters && - JSON.stringify(localExtraDomain) === "[]" - ) { - inImage = count; - } else { - inImage = (this._mockSearch(model, [searchDomain], { limit: 1 })).length; - } - } - } - if (expand || inImage) { - if (enableCounters) { - values.__count = count; - } - fieldRange.push(values); - } - } - - if (!expand && limit && fieldRange.length === limit) { - return { error_msg: "Too many items to display." }; - } - - return { values: fieldRange }; - } - - if (field.type === 'many2one') { - let domainImage; - if (enableCounters || !expand) { - extraDomain = Domain.prototype.normalizeArray([ - ...extraDomain, - ...(kwargs.group_domain || []), - ]); - modelDomain = Domain.prototype.normalizeArray([ - ...modelDomain, - ...(kwargs.group_domain || []), - ]); - const newKwargs = Object.assign({}, kwargs, { - model_domain: modelDomain, - extra_domain: extraDomain, - only_counters: expand, - set_limit: limit && !(expand || groupBy || comodelDomain), - }); - domainImage = this._mockSearchPanelFieldImage(model, fieldName, newKwargs); - } - if (!expand && !groupBy && !comodelDomain.length) { - if (limit && domainImage.size === limit) { - return { error_msg: "Too many items to display." }; - } - return { values: [...domainImage.values()] }; - } - if (!expand) { - const imageElementIds = [...domainImage.keys()].map(Number); - comodelDomain = Domain.prototype.normalizeArray([ - ...comodelDomain, - ['id', 'in', imageElementIds], - ]); - } - const comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], { limit }); - if (limit && comodelRecords.length === limit) { - return { error_msg: "Too many items to display." }; - } - - const fieldRange = []; - for (const record of comodelRecords) { - const values= { - id: record.id, - display_name: record.display_name, - }; - if (groupBy) { - const [groupId, groupName] = groupIdName(record[groupBy]); - values.group_id = groupId; - values.group_name = groupName; - } - if (enableCounters) { - values.__count = domainImage.get(record.id) ? domainImage.get(record.id).__count : 0; - } - fieldRange.push(values); - } - return { values: fieldRange }; - } - }, - /** - * Simulate a call to the '/web/action/load' route - * - * @private - * @param {Object} kwargs - * @param {integer} kwargs.action_id - * @returns {Object} - */ - _mockLoadAction: function (kwargs) { - var action = this.actions.find((action) => action.id === parseInt(kwargs.action_id)); - if (!action) { - // when the action doesn't exist, the real server doesn't crash, it - // simply returns false - console.warn(`No action found for ID ${kwargs.action_id} during test ${QUnit.config.current.testName} (legacy)`); - } - return action || false; - }, - /** - * Simulate a 'get_views' operation - * - * @param {string} model - * @param {Array} args - * @param {Object} kwargs - * @param {Array} kwargs.views - * @param {Object} kwargs.options - * @param {Object} kwargs.context - * @returns {Object} - */ - _mockGetViews: function (model, kwargs) { - var self = this; - var views = {}; - kwargs.views.forEach((view_descr) => { - var viewID = view_descr[0] || false; - var viewType = view_descr[1]; - if (!viewID) { - var contextKey = (viewType === 'list' ? 'tree' : viewType) + '_view_ref'; - if (contextKey in kwargs.context) { - viewID = parseInt(kwargs.context[contextKey]); - } - } - var key = [model, viewID, viewType].join(','); - var arch = self.archs[key] || Object.keys(self.archs).find(k => { - var ka = k.split(','); - viewID = parseInt(ka[1], 10); - return ka[0] === model && ka[2] === viewType; - }); - if (!arch) { - throw new Error('No arch found for key ' + key); - } - views[viewType] = { - arch: arch, - view_id: viewID, - model: model, - viewOptions: { - context: kwargs.context, - }, - }; - }); - return views; - }, - /** - * Simulate a 'name_create' operation - * - * @private - * @param {string} model - * @param {Array} args - * @returns {Array} a couple [id, name] - */ - _mockNameCreate: function (model, args) { - var name = args[0]; - var values = { - name: name, - display_name: name, - }; - var id = this._mockCreate(model, values); - return [id, name]; - }, - /** - * Simulate a 'name_search' operation. - * - * not yet fully implemented (missing: limit, and evaluate operators) - * domain works but only to filter on ids - * - * @private - * @param {string} model - * @param {Array} args - * @param {string} args[0] - * @param {Array} args[1], search domain - * @param {Object} _kwargs - * @param {number} [_kwargs.limit=100] server-side default limit - * @returns {Array[]} a list of [id, display_name] - */ - _mockNameSearch: function (model, args, _kwargs) { - var str = args && typeof args[0] === 'string' ? args[0] : _kwargs.name; - const limit = _kwargs.limit || 100; - var domain = (args && args[1]) || _kwargs.args || []; - var records = this._getRecords(model, domain); - if (str.length) { - records = records.filter(record => record.display_name.indexOf(str) !== -1); - } - var result = records.map(record => { - return [record.id, record.display_name]; - }); - return result.slice(0, limit); - }, - /** - * Simulate an 'onchange' rpc - * - * @private - * @param {string} model - * @param {Object} args - * @param {Object} args[1] the current record data - * @param {string|string[]} [args[2]] a list of field names, or just a field name - * @param {Object} args[3] the onchange spec - * @param {Object} [kwargs] - * @returns {Object} - */ - _mockOnchange: function (model, args, kwargs) { - const currentData = args[1]; - let fields = args[2]; - const onChangeSpec = args[3]; - var onchanges = this.data[model].onchanges || {}; - - if (fields && !(fields instanceof Array)) { - fields = [fields]; - } - const firstOnChange = !fields || !fields.length; - const onchangeVals = {}; - let defaultVals; - let nullValues; - if (firstOnChange) { - const fieldsFromView = Object.keys(onChangeSpec).reduce((acc, fname) => { - fname = fname.split('.', 1)[0]; - if (!acc.includes(fname)) { - acc.push(fname); - } - return acc; - }, []); - const defaultingFields = fieldsFromView.filter(fname => !(fname in currentData)); - defaultVals = this._mockDefaultGet(model, [defaultingFields], kwargs); - // It is the new semantics: no field in arguments means we are in - // a default_get + onchange situation - fields = fieldsFromView; - nullValues = {}; - fields.filter(fName => !Object.keys(defaultVals).includes(fName)).forEach(fName => { - nullValues[fName] = false; - }); - } - Object.assign(currentData, defaultVals); - fields.forEach(field => { - if (field in onchanges) { - const changes = Object.assign({}, nullValues, currentData); - onchanges[field](changes); - Object.entries(changes).forEach(([key, value]) => { - if (currentData[key] !== value) { - onchangeVals[key] = value; - } - }); - } - }); - - return { - value: this._convertToOnChange(model, Object.assign({}, defaultVals, onchangeVals)), - }; - }, - /** - * Simulate a 'read' operation. - * - * @private - * @param {string} model - * @param {Array} args - * @param {Object} _kwargs ignored... is that correct? - * @returns {Object} - */ - _mockRead: function (model, args, _kwargs) { - var self = this; - var ids = args[0]; - if (!Array.isArray(ids)) { - ids = [ids]; - } - var fields = args[1] && args[1].length ? unique(args[1].concat(['id'])) : Object.keys(this.data[model].fields); - var records = ids.reduce((records, id) => { - if (!id) { - throw new Error( - "mock read: falsy value given as id, would result in an access error in actual server !" - ); - } - var record = self.data[model].records.find((record) => record.id === id); - return record ? records.concat(record) : records; - }, []); - var results = records.map((record) => { - var result = {}; - for (var i = 0; i < fields.length; i++) { - var field = self.data[model].fields[fields[i]]; - if (!field) { - // the field doens't exist on the model, so skip it - continue; - } - if (field.type === 'float' || - field.type === 'integer' || - field.type === 'monetary') { - // read should return 0 for unset numeric fields - result[fields[i]] = record[fields[i]] || 0; - } else if (field.type === 'many2one') { - var relatedRecord = self.data[field.relation].records.find( - (r) => r.id === record[fields[i]] - ); - if (relatedRecord) { - result[fields[i]] = - [record[fields[i]], relatedRecord.display_name]; - } else { - result[fields[i]] = false; - } - } else if (field.type === 'one2many' || field.type === 'many2many') { - result[fields[i]] = record[fields[i]] || []; - } else { - result[fields[i]] = record[fields[i]] || false; - } - } - return result; - }); - return results; - }, - /** - * Simulate a 'read_group' call to the server. - * - * Note: most of the keys in kwargs are still ignored - * - * @private - * @param {string} model a string describing an existing model - * @param {Object} kwargs various options supported by read_group - * @param {string[]} kwargs.groupby fields that we are grouping - * @param {string[]} kwargs.fields fields that we are aggregating - * @param {Array} kwargs.domain the domain used for the read_group - * @param {boolean} kwargs.lazy still mostly ignored - * @param {integer} [kwargs.limit] - * @param {integer} [kwargs.offset] - * @returns {Object[]} - */ - _mockReadGroup: function (model, kwargs) { - if (!('lazy' in kwargs)) { - kwargs.lazy = true; - } - var self = this; - var fields = this.data[model].fields; - var aggregatedFields = []; - kwargs.fields.forEach((field) => { - var split = field.split(":"); - var fieldName = split[0]; - if (kwargs.groupby.indexOf(fieldName) > 0) { - // grouped fields are not aggregated - return; - } - if (fields[fieldName] && (fields[fieldName].type === 'many2one') && split[1] !== 'count_distinct') { - return; - } - aggregatedFields.push(fieldName); - }); - var groupBy = []; - if (kwargs.groupby.length) { - groupBy = kwargs.lazy ? [kwargs.groupby[0]] : kwargs.groupby; - } - var records = this._getRecords(model, kwargs.domain); - - // if no fields have been given, the server picks all stored fields - if (kwargs.fields.length === 0) { - aggregatedFields = Object.keys(this.data[model].fields); - } - - var groupByFieldNames = groupBy.map((groupByField) => { - return groupByField.split(":")[0]; - }); - - // filter out non existing fields - aggregatedFields = aggregatedFields.filter((name) => { - return name in self.data[model].fields && !groupByFieldNames.includes(name); - }); - - function aggregateFields(group, records) { - var type; - for (var i = 0; i < aggregatedFields.length; i++) { - type = fields[aggregatedFields[i]].type; - if (type === 'float' || type === 'integer') { - group[aggregatedFields[i]] = null; - for (var j = 0; j < records.length; j++) { - var value = group[aggregatedFields[i]] || 0; - group[aggregatedFields[i]] = value + records[j][aggregatedFields[i]]; - } - } - if (type === 'many2one') { - var ids = records.map((record) => aggregatedFields[i]); - group[aggregatedFields[i]] = unique(ids).length || null; - } - } - } - function formatValue(groupByField, val) { - if (val === false || val === undefined) { - return false; - } - const [fieldName, aggregateFunction = "month"] = groupByField.split(':'); - const { type } = fields[fieldName]; - if (type === "date") { - const date = deserializeDate(val); - if (aggregateFunction === "day") { - return date.toFormat("yyyy-MM-dd"); - } else if (aggregateFunction === "week") { - return `W${date.toFormat("WW kkkk")}`; - } else if (aggregateFunction === "quarter") { - return `Q${date.toFormat("q yyyy")}`; - } else if (aggregateFunction === "year") { - return date.toFormat("yyyy"); - } else { - return date.toFormat("MMMM yyyy"); - } - } else if (type === "datetime") { - const date = deserializeDateTime(val); - if (aggregateFunction === 'hour') { - return date.toFormat("HH:00 dd MMM yyyy"); - } else if (aggregateFunction === "day") { - return date.toFormat("yyyy-MM-dd"); - } else if (aggregateFunction === "week") { - return `W${date.toFormat("WW kkkk")}`; - } else if (aggregateFunction === "quarter") { - return `Q${date.toFormat("q yyyy")}`; - } else if (aggregateFunction === "year") { - return date.toFormat("yyyy"); - } else { - return date.toFormat("MMMM yyyy"); - } - } else if (Array.isArray(val)) { - if (val.length === 0) { - return false; - } - return type === "many2many" ? val : val[0]; - } else { - return val; - } - } - - if (!groupBy.length) { - var group = { __count: records.length }; - aggregateFields(group, records); - return [group]; - } - - const groups = {}; - for (const r of records) { - let recordGroupValues = []; - for (const gbField of groupBy) { - const [fieldName] = gbField.split(":"); - let value =formatValue(gbField, r[fieldName]); - if (!Array.isArray(value)) { - value = [value]; - } - recordGroupValues = value.reduce((acc, val) => { - const newGroup = {}; - newGroup[gbField] = val; - if (recordGroupValues.length === 0) { - acc.push(newGroup); - } else { - for (const groupValue of recordGroupValues) { - acc.push({ ...groupValue, ...newGroup }); - } - } - return acc; - }, []); - } - for (const groupValue of recordGroupValues) { - const valueKey = JSON.stringify(groupValue); - groups[valueKey] = groups[valueKey] || []; - groups[valueKey].push(r); - } - } - - let readGroupResult = []; - for (const [groupId, groupRecords] of Object.entries(groups)) { - const group = { - ...JSON.parse(groupId), - __domain: kwargs.domain || [], - __range: {}, - }; - for (const gbField of groupBy) { - if (!(gbField in group)) { - group[gbField] = false; - continue; - } - - const [fieldName, dateRange] = gbField.split(":"); - const value = Number.isInteger(group[gbField]) - ? group[gbField] - : group[gbField] || false; - const { relation, type } = fields[fieldName]; - - if (["many2one", "many2many"].includes(type) && !Array.isArray(value)) { - const relatedRecord = this.data[relation].records.find((r) => r.id === value); - if (relatedRecord) { - group[gbField] = [value, relatedRecord.display_name]; - } else { - group[gbField] = false; - } - } - - if (["date", "datetime"].includes(type)) { - if (value) { - let startDate, endDate; - switch (dateRange) { - case "hour": { - startDate = parseDateTime(value, { format: "HH:00 dd MMM yyyy" }); - group[gbField] = startDate.toFormat("HH:00 dd MMM"); - endDate = startDate.plus({ hours: 1 }); - // Remove the year from the result value of the group. It was needed - // to compute the startDate and endDate. - break; - } - case "day": { - startDate = parseDateTime(value, { format: "yyyy-MM-dd" }); - endDate = startDate.plus({ days: 1 }); - break; - } - case "week": { - startDate = parseDateTime(value, { format: "WW kkkk" }); - endDate = startDate.plus({ weeks: 1 }); - break; - } - case "quarter": { - startDate = parseDateTime(value, { format: "q yyyy" }); - endDate = startDate.plus({ quarters: 1 }); - break; - } - case "year": { - startDate = parseDateTime(value, { format: "y" }); - endDate = startDate.plus({ years: 1 }); - break; - } - case "month": - default: { - startDate = parseDateTime(value, { format: "MMMM yyyy" }); - endDate = startDate.plus({ months: 1 }); - break; - } - } - const serialize = type === "date" ? serializeDate : serializeDateTime; - const from = serialize(startDate); - const to = serialize(endDate); - // NOTE THAT the range and the domain computed here are not really accurate - // due to a the timezone not really taken into account. - // FYI, the non legacy version of the mock server handles this correctly. - group.__range[gbField] = { from, to }; - group.__domain = [ - [fieldName, ">=", from], - [fieldName, "<", to], - ].concat(group.__domain); - } else { - group.__range[gbField] = false; - group.__domain = [[fieldName, "=", value]].concat(group.__domain); - } - } else { - group.__domain = [[fieldName, "=", value]].concat(group.__domain); - } - } - if (Object.keys(group.__range || {}).length === 0) { - delete group.__range; - } - // compute count key to match dumb server logic... - const countKey = kwargs.lazy - ? groupBy[0].split(":")[0] + "_count" - : "__count"; - group[countKey] = groupRecords.length; - aggregateFields(group, groupRecords); - readGroupResult.push(group); - } - - if (kwargs.orderby) { - // only consider first sorting level - kwargs.orderby = kwargs.orderby.split(',')[0]; - const fieldName = kwargs.orderby.split(' ')[0]; - const order = kwargs.orderby.split(' ')[1]; - readGroupResult = this._sortByField(readGroupResult, model, fieldName, order); - } - - if (kwargs.limit) { - const offset = kwargs.offset || 0; - readGroupResult = readGroupResult.slice(offset, kwargs.limit + offset); - } - - return readGroupResult; - }, - /** - * Simulates a 'read_progress_bar' operation - * - * @private - * @param {string} model - * @param {Object} kwargs - * @returns {Object[][]} - */ - _mockReadProgressBar: function (model, kwargs) { - var domain = kwargs.domain; - var groupBy = kwargs.group_by; - var progress_bar = kwargs.progress_bar; - - var records = this._getRecords(model, domain || []); - - var data = {}; - records.forEach((record) => { - var groupByValue = record[groupBy]; // always technical value here - - // special case for bool values: rpc call response with capitalized strings - if (!(groupByValue in data)) { - if (groupByValue === true) { - groupByValue = "True"; - } else if (groupByValue === false) { - groupByValue = "False"; - } - } - - if (!(groupByValue in data)) { - data[groupByValue] = {}; - Object.keys(progress_bar.colors).forEach((key) => { - data[groupByValue][key] = 0; - }); - } - - var fieldValue = record[progress_bar.field]; - if (fieldValue in data[groupByValue]) { - data[groupByValue][fieldValue]++; - } - }); - - return data; - }, - /** - * Simulates a 'resequence' operation - * - * @private - * @param {string} model - * @param {string} field - * @param {Array} ids - */ - _mockResequence: function (args) { - var offset = args.offset ? Number(args.offset) : 0; - var field = args.field ? args.field : 'sequence'; - var records = this.data[args.model].records; - if (!(field in this.data[args.model].fields)) { - return false; - } - for (var i in args.ids) { - var record = records.find((record) => record.id === args.ids[i]); - record[field] = Number(i) + offset; - } - return true; - }, - /** - * Simulate a 'search' operation - * - * @private - * @param {string} model - * @param {Array} args - * @param {Object} kwargs - * @param {integer} [kwargs.limit] - * @returns {integer[]} - */ - _mockSearch: function (model, args, kwargs) { - const limit = kwargs.limit || Number.MAX_VALUE; - const { context } = kwargs; - const active_test = - context && "active_test" in context ? context.active_test : true; - return this._getRecords(model, args[0], { active_test }).map(r => r.id).slice(0, limit); - }, - /** - * Simulate a 'search_count' operation - * - * @private - * @param {string} model - * @param {Array} args - * @returns {integer} - */ - _mockSearchCount: function (model, args) { - return this._getRecords(model, args[0]).length; - }, - /** - * Simulate a 'search_read' operation on a model - * - * @private - * @param {Object} args - * @param {Array} args.domain - * @param {string} args.model - * @param {Array} [args.fields] defaults to the list of all fields - * @param {integer} [args.limit] - * @param {integer} [args.offset=0] - * @param {string[]} [args.sort] - * @returns {Object} - */ - _mockSearchRead: function (model, args, kwargs) { - var result = this._mockSearchReadController({ - model: model, - domain: kwargs.domain || args[0], - fields: kwargs.fields || args[1], - offset: kwargs.offset || args[2], - limit: kwargs.limit || args[3], - sort: kwargs.order || args[4], - context: kwargs.context, - }); - return result.records; - }, - /** - * Simulate a 'search_read' operation, from the controller point of view - * - * @private - * @private - * @param {Object} args - * @param {Array} args.domain - * @param {string} args.model - * @param {Array} [args.fields] defaults to the list of all fields - * @param {integer} [args.limit] - * @param {integer} [args.offset=0] - * @param {string[]} [args.sort] - * @returns {Object} - */ - _mockSearchReadController: function (args) { - var self = this; - const { context } = args; - const active_test = - context && "active_test" in context ? context.active_test : true; - var records = this._getRecords(args.model, args.domain || [], { - active_test, - }); - var fields = args.fields && args.fields.length ? args.fields : Object.keys(this.data[args.model].fields); - var nbRecords = records.length; - var offset = args.offset || 0; - if (args.sort) { - // warning: only consider first level of sort - args.sort = args.sort.split(',')[0]; - var fieldName = args.sort.split(' ')[0]; - var order = args.sort.split(' ')[1]; - records = this._sortByField(records, args.model, fieldName, order); - } - records = records.slice(offset, args.limit ? (offset + args.limit) : nbRecords); - var processedRecords = records.map((r) => { - var result = {}; - unique(fields.concat(['id'])).forEach((fieldName) => { - var field = self.data[args.model].fields[fieldName]; - if (field.type === 'many2one') { - var related_record = self.data[field.relation].records.find( - (record) => record.id === r[fieldName] - ); - result[fieldName] = - related_record ? [r[fieldName], related_record.display_name] : false; - } else { - result[fieldName] = r[fieldName]; - } - }); - return result; - }); - var result = { - length: nbRecords, - records: processedRecords, - }; - return $.extend(true, {}, result); - }, - /** - * Simulate a 'unlink' operation - * - * @private - * @param {string} model - * @param {Array} args - * @returns {boolean} currently, always returns true - */ - _mockUnlink: function (model, args) { - var ids = args[0]; - if (!Array.isArray(ids)) { - ids = [ids]; - } - this.data[model].records = this.data[model].records.filter( - (record) => !ids.includes(record.id) - ); - - // update value of relationnal fields pointing to the deleted records - Object.values(this.data).forEach((d) => { - const fields_to_pick = Object.keys(d.fields || {}).filter((key) => { - const field = d.fields[key]; - return field.relation === model; - }); - var relatedFields = pick(d.fields, ...fields_to_pick); - Object.keys(relatedFields).forEach((relatedField) => { - d.records.forEach((record) => { - if (Array.isArray(record[relatedField])) { - record[relatedField] = record[relatedField].filter( - (x) => ids.indexOf(x) < 0 - ); - } else if (ids.includes(record[relatedField])) { - record[relatedField] = false; - } - }); - }); - }); - - return true; - }, - /** - * Simulate a 'web_read_group' call to the server. - * - * Note: some keys in kwargs are still ignored - * - * @private - * @param {string} model a string describing an existing model - * @param {Object} kwargs various options supported by read_group - * @param {string[]} kwargs.groupby fields that we are grouping - * @param {string[]} kwargs.fields fields that we are aggregating - * @param {Array} kwargs.domain the domain used for the read_group - * @param {boolean} kwargs.lazy still mostly ignored - * @param {integer} [kwargs.limit] - * @param {integer} [kwargs.offset] - * @returns {Object[]} - */ - _mockWebReadGroup: function (model, kwargs) { - var groups = this._mockReadGroup(model, kwargs); - var allGroups = this._mockReadGroup(model, { - domain: kwargs.domain, - fields: ['display_name'], - groupby: kwargs.groupby, - lazy: kwargs.lazy, - }); - return { - groups: groups, - length: allGroups.length, - }; - }, - /** - * Simulate a 'write' operation - * - * @private - * @param {string} model - * @param {Array} args - * @returns {boolean} currently, always return 'true' - */ - _mockWrite: function (model, args) { - args[0].forEach((id) => { - const originalRecord = this._mockSearchRead(model, [[['id', '=', id]]], {})[0]; - this._writeRecord(model, args[1], id); - const updatedRecord = this.data[model].records.find(record => record.id === id); - this._updateComodelRelationalFields(model, updatedRecord, originalRecord); - }); - return true; - }, - /** - * Dispatches a fetch call to the correct helper function. - * - * @param {string} resource - * @param {Object} init - * @returns {any} - */ - _performFetch(resource, init) { - if (resource.match(/\/static(\/\S+\/|\/)libs?/)) { - // every lib must be includes into the test bundle. - return true; - } - if (resource.match(/\/web\/bundle\/[^.]+\.[^.]+/)) { - // every asset must be includes into the test bundle. - return true; - } - throw new Error("Unimplemented resource: " + resource); - }, - /** - * Dispatch a RPC call to the correct helper function - * - * @see performRpc - * - * @private - * @param {string} route - * @param {Object} args - * @returns {Promise} - * Resolved with the result of the RPC. If the RPC should fail, the - * promise should either be rejected or the call should throw an - * exception (@see performRpc for error handling). - */ - async _performRpc(route, args) { - switch (route) { - case '/web/dataset/call_button': - return this._mockCallButton(args); - case '/web/action/load': - return this._mockLoadAction(args); - - case '/web/dataset/search_read': - return this._mockSearchReadController(args); - - case '/web/dataset/resequence': - return this._mockResequence(args); - } - if ( - route.indexOf("/web/image") >= 0 || - [".png", ".jpg"].includes(route.substr(route.length - 4)) - ) { - return; - } - switch (args.method) { - case "render_public_asset": { - return true; - } - case 'copy': - return this._mockCopy(args.model, args.args[0]); - - case 'create': - return this._mockCreate(args.model, args.args[0]); - - case 'fields_get': - return this._mockFieldsGet(args.model, args.args); - - case 'search_panel_select_range': - return this._mockSearchPanelSelectRange(args.model, args.args, args.kwargs); - - case 'search_panel_select_multi_range': - return this._mockSearchPanelSelectMultiRange(args.model, args.args, args.kwargs); - - case 'get_views': - return this._mockGetViews(args.model, args.kwargs); - - - case 'name_create': - return this._mockNameCreate(args.model, args.args); - - case 'name_search': - return this._mockNameSearch(args.model, args.args, args.kwargs); - - case 'onchange': - return this._mockOnchange(args.model, args.args, args.kwargs); - - case 'read': - return this._mockRead(args.model, args.args, args.kwargs); - - case 'read_group': - return this._mockReadGroup(args.model, args.kwargs); - - case 'web_read_group': - return this._mockWebReadGroup(args.model, args.kwargs); - - case 'read_progress_bar': - return this._mockReadProgressBar(args.model, args.kwargs); - - case 'search': - return this._mockSearch(args.model, args.args, args.kwargs); - - case 'search_count': - return this._mockSearchCount(args.model, args.args); - - case 'search_read': - return this._mockSearchRead(args.model, args.args, args.kwargs); - - case 'unlink': - return this._mockUnlink(args.model, args.args); - - case 'write': - return this._mockWrite(args.model, args.args); - } - var model = this.data[args.model]; - if (model && typeof model[args.method] === 'function') { - return this.data[args.model][args.method](args.args, args.kwargs); - } - - throw new Error("Unimplemented route: " + route); - }, - /** - * @private - * @param {Object[]} records the records to sort - * @param {string} model the model of records - * @param {string} fieldName the field to sort on - * @param {string} [order="DESC"] "ASC" or "DESC" - * @returns {Object} - */ - _sortByField: function (records, model, fieldName, order) { - const field = this.data[model].fields[fieldName]; - records.sort((r1, r2) => { - let v1 = r1[fieldName]; - let v2 = r2[fieldName]; - if (field.type === 'many2one') { - const coRecords = this.data[field.relation].records; - if (this.data[field.relation].fields.sequence) { - // use sequence field of comodel to sort records - v1 = coRecords.find(r => r.id === v1[0]).sequence; - v2 = coRecords.find(r => r.id === v2[0]).sequence; - } else { - // sort by id - v1 = v1[0]; - v2 = v2[0]; - } - } - if (v1 < v2) { - return order === 'ASC' ? -1 : 1; - } - if (v1 > v2) { - return order === 'ASC' ? 1 : -1; - } - return 0; - }); - return records; - }, - /** - * helper function: traverse a tree and apply the function f to each of its - * nodes. - * - * Note: this should be abstracted somewhere in @web/legacy/js/core/utils, or in - * web.tree_utils - * - * @param {Object} tree object with a 'children' key, which contains an - * array of trees. - * @param {function} f - */ - _traverse: function (tree, f) { - var self = this; - if (f(tree)) { - Object.values(tree.childNodes || {}).forEach((c) => { - self._traverse(c, f); - }); - } - }, - /** - * Fill all inverse fields of the relational fields present in the record - * to be created/updated. - * - * @param {string} modelName - * @param {Object} record record that have been created/updated. - * @param {Object|undefined} originalRecord record before update. - */ - _updateComodelRelationalFields(modelName, record, originalRecord) { - for (const fname in record) { - const field = this.data[modelName].fields[fname]; - const comodelName = field.relation || record[field['model_name_ref_fname']]; - const inverseFieldName = field['inverse_fname_by_model_name'] && field['inverse_fname_by_model_name'][comodelName]; - if (!inverseFieldName) { - // field has no inverse, skip it. - continue; - } - const relatedRecordIds = Array.isArray(record[fname]) ? record[fname] : [record[fname]]; - // we only want to set a value for comodel inverse field if the model field has a value. - if (record[fname]) { - for (const relatedRecordId of relatedRecordIds) { - let inverseFieldNewValue = record.id; - const relatedRecord = this.data[comodelName].records.find(record => record.id === relatedRecordId); - const relatedFieldValue = relatedRecord && relatedRecord[inverseFieldName]; - if ( - relatedFieldValue === undefined || - relatedFieldValue === record.id || - field.type !== 'one2many' && relatedFieldValue.includes(record.id) - ) { - // related record does not exist or the related value is already up to date. - continue; - } - if (Array.isArray(relatedFieldValue)) { - inverseFieldNewValue = [...relatedFieldValue, record.id]; - } - this._writeRecord(comodelName, { [inverseFieldName]: inverseFieldNewValue }, relatedRecordId); - } - } else if (field.type === 'many2one_reference') { - // we need to clean the many2one_field as well. - const comodel_inverse_field = this.data[comodelName].fields[inverseFieldName]; - const model_many2one_field = comodel_inverse_field['inverse_fname_by_model_name'][modelName]; - this._writeRecord(modelName, { [model_many2one_field]: false }, record.id); - } - // it's an update, get the records that were originally referenced but are not - // anymore and update their relational fields. - if (originalRecord) { - const originalRecordIds = Array.isArray(originalRecord[fname]) ? originalRecord[fname] : [originalRecord[fname]]; - // search read returns [id, name], let's ensure the removedRecordIds are integers. - const removedRecordIds = originalRecordIds.filter(recordId => Number.isInteger(recordId) && !relatedRecordIds.includes(recordId)); - for (const removedRecordId of removedRecordIds) { - const removedRecord = this.data[comodelName].records.find(record => record.id === removedRecordId); - if (!removedRecord) { - continue; - } - let inverseFieldNewValue = false; - if (Array.isArray(removedRecord[inverseFieldName])) { - inverseFieldNewValue = removedRecord[inverseFieldName].filter(id => id !== record.id); - } - this._writeRecord(comodelName, { [inverseFieldName]: inverseFieldNewValue }, removedRecordId); - } - } - } - }, - /** - * Write a record. The main difficulty is that we have to apply x2many - * commands - * - * @private - * @param {string} model - * @param {Object} values - * @param {integer} id - * @param {Object} [params={}] - * @param {boolean} [params.ensureIntegrity=true] writing non-existing id - * in many2one field will throw if this param is true - */ - _writeRecord: function (model, values, id, { ensureIntegrity = true } = {}) { - var self = this; - var record = this.data[model].records.find((record) => record.id === id); - for (var field_changed in values) { - var field = this.data[model].fields[field_changed]; - var value = values[field_changed]; - if (!field) { - throw Error(`Mock: Can't write value "${JSON.stringify(value)}" on field "${field_changed}" on record "${model},${id}" (field is undefined)`); - } - if (["one2many", "many2many"].includes(field.type)) { - var ids = record[field_changed] ? [...record[field_changed]] : []; - - if ( - Array.isArray(value) && - value.reduce((hasOnlyInt, val) => hasOnlyInt && Number.isInteger(val), true) - ) { - // fallback to command 6 when given a simple list of ids - value = [[6, 0, value]]; - } else if (value === false) { - // delete all command - value = [[5]]; - } - // convert commands - for (const command of value || []) { - if (command[0] === 0) { // CREATE - const inverseData = command[2]; // write in place instead of copy, because some tests rely on the object given being updated - const inverseFieldName = field.inverse_fname_by_model_name && field.inverse_fname_by_model_name[field.relation]; - if (inverseFieldName) { - inverseData[inverseFieldName] = id; - } - const newId = self._mockCreate(field.relation, inverseData); - ids.push(newId); - } else if (command[0] === 1) { // UPDATE - self._mockWrite(field.relation, [[command[1]], command[2]]); - } else if (command[0] === 2) { // DELETE - ids = ids.filter((x) => x !== command[1]); - } else if (command[0] === 3) { // FORGET - ids = ids.filter((x) => x !== command[1]); - } else if (command[0] === 4) { // LINK_TO - if (!ids.includes(command[1])) { - ids.push(command[1]); - } - } else if (command[0] === 5) { // DELETE ALL - ids = []; - } else if (command[0] === 6) { // REPLACE WITH - // copy array to avoid leak by reference (eg. of default data) - ids = [...command[2]]; - } else { - throw Error(`Command "${JSON.stringify(value)}" not supported by the MockServer on field "${field_changed}" on record "${model},${id}"`); - } - } - record[field_changed] = ids; - } else if (field.type === 'many2one') { - if (value) { - var relatedRecord = this.data[field.relation].records.find( - (r) => r.id === value - ); - if (!relatedRecord && ensureIntegrity) { - throw Error(`Wrong id "${JSON.stringify(value)}" for a many2one on field "${field_changed}" on record "${model},${id}"`); - } - record[field_changed] = value; - } else { - record[field_changed] = false; - } - } else { - record[field_changed] = value; - } - } - }, -}); - -export default MockServer; diff --git a/addons/web/static/tests/legacy/helpers/test_utils.js b/addons/web/static/tests/legacy/helpers/test_utils.js index 16257034813..899354a039d 100644 --- a/addons/web/static/tests/legacy/helpers/test_utils.js +++ b/addons/web/static/tests/legacy/helpers/test_utils.js @@ -11,7 +11,6 @@ import testUtilsFields from "@web/../tests/legacy/helpers/test_utils_fields"; import testUtilsFile from "@web/../tests/legacy/helpers/test_utils_file"; import testUtilsMock from "@web/../tests/legacy/helpers/test_utils_mock"; - import Widget from "@web/legacy/js/core/widget"; function deprecated(fn, type) { return function () { @@ -22,19 +21,6 @@ }; } - /** - * Create a widget parent from given parameters. - * - * @param {Object} params This object will be given to addMockEnvironment, so - * any parameters from that method applies - * @returns {Promise} - */ - async function createParent(params) { - const widget = new Widget(); - await testUtilsMock.addMockEnvironment(widget, params); - return widget; - } - /** * Helper function, make a promise with a public resolve function. Note that * this is not standard and should not be used outside of tests... @@ -99,8 +85,6 @@ } export const mock = { - addMockEnvironment: testUtilsMock.addMockEnvironment, - addMockEnvironmentOwl: testUtilsMock.addMockEnvironmentOwl, intercept: testUtilsMock.intercept, patch: testUtilsMock.patch, patchDate: testUtilsMock.patchDate, @@ -137,14 +121,12 @@ fields, file, - createParent, makeTestPromise: makeTestPromise, makeTestPromiseWithAssert: makeTestPromiseWithAssert, nextMicrotaskTick: nextMicrotaskTick, nextTick: nextTick, // backward-compatibility - addMockEnvironment: deprecated(testUtilsMock.addMockEnvironment, 'mock'), dragAndDrop: deprecated(testUtilsDom.dragAndDrop, 'dom'), getView: deprecated(testUtilsMock.getView, 'mock'), intercept: deprecated(testUtilsMock.intercept, 'mock'), diff --git a/addons/web/static/tests/legacy/helpers/test_utils_mock.js b/addons/web/static/tests/legacy/helpers/test_utils_mock.js index 14e6b82b325..dd9e9d3678e 100644 --- a/addons/web/static/tests/legacy/helpers/test_utils_mock.js +++ b/addons/web/static/tests/legacy/helpers/test_utils_mock.js @@ -9,199 +9,10 @@ * testUtils file. */ -import AbstractStorageService from "@web/legacy/js/core/abstract_storage_service"; -import AjaxService from "@web/legacy/js/services/ajax_service"; -import Bus from "@web/legacy/js/core/bus"; -import config from "@web/legacy/js/services/config"; -import core from "@web/legacy/js/services/core"; -import dom from "@web/legacy/js/core/dom"; -import makeTestEnvironment from "@web/../tests/legacy/helpers/test_env"; -import MockServer from "@web/../tests/legacy/helpers/mock_server"; -import RamStorage from "@web/legacy/js/core/ram_storage"; -import session from "web.session"; -import { patchWithCleanup, patchDate } from "@web/../tests/helpers/utils"; -import { browser } from "@web/core/browser/browser"; -import { assets } from "@web/core/assets"; +import { patchDate } from "@web/../tests/helpers/utils"; -import { Component } from "@odoo/owl"; import { uniqueId } from "@web/core/utils/functions"; - -//------------------------------------------------------------------------------ -// 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 {Object} [params.env] - * @param {Bus} [params.env.bus] - * @param {Object} [params.env.services] - * @param {Object} [params.services] - * @param {Object} [params.session] - * @param {MockServer} [mockServer] - * @returns {Promise} env - */ -async function _getMockedOwlEnv(params, mockServer) { - params.env = params.env || {}; - - const database = {parameters: params.translateParameters || {}}; - - // build the env - const services = {}; - const env = Object.assign({}, params.env, { - _t: params.env && params.env._t || Object.assign((s => s), { database }), - browser: Object.assign({ - fetch: (resource, init) => mockServer.performFetch(resource, init), - }, params.env.browser), - bus: params.bus || params.env.bus || new Bus(), - services: Object.assign(services, params.env.services), - session: params.env.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 = Object.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) { - // store initial session state (for restoration) - const initialSession = Object.assign({}, session); - const sessionPatch = Object.assign({ - getTZOffset() { return 0; }, - async user_has_group() { return false; }, - }, params.session); - // patch session - Object.assign(session, sessionPatch); - - // 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; - for (key in sessionPatch) { - delete session[key]; - } - Object.assign(session, initialSession); - if ('config' in params) { - for (key in config) { - delete config[key]; - } - Object.assign(config, initialConfig); - } - if ('translateParameters' in params) { - for (key in core._t.database.parameters) { - delete core._t.database.parameters[key]; - } - Object.assign(core._t.database.parameters, initialParameters); - } - } - - return cleanUp; -} -/** - * logs all event going through the target widget. - * - * @param {Widget} widget - */ -function _observe(widget) { - var _trigger_up = widget._trigger_up.bind(widget); - widget._trigger_up = function (event) { - console.log('%c[event] ' + event.name, 'color: blue; font-weight: bold;', event); - _trigger_up(event); - }; -} - //------------------------------------------------------------------------------ // Public functions //------------------------------------------------------------------------------ @@ -230,241 +41,6 @@ function intercept(widget, eventName, fn, propagate) { }; } -/** - * 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.mockFetch] - * @param {function} [params.mockRPC] - * @param {number} [params.fieldDebounce=0] the value of the DEBOUNCE attribute - * of fields - * @param {boolean} [params.mockSRC=false] if true, redirect src GET requests to - * the mockServer - * @param {MockServer} [mockServer] - * @returns {Promise} the cleanup function - */ -async function addMockEnvironmentOwl(Component, params, mockServer) { - params = params || {}; - - // instantiate a mockServer if not provided - if (!mockServer) { - let Server = MockServer; - if (params.mockFetch) { - Server = Server.extend({ _performFetch: params.mockFetch }); - } - if (params.mockRPC) { - Server = Server.extend({ _performRpc: params.mockRPC }); - } - mockServer = new Server(params.data, { - actions: params.actions, - archs: params.archs, - currentDate: params.currentDate, - debug: params.debug, - }); - } - - patchWithCleanup(browser, { - fetch: async (url, args) => { - const result = await mockServer.performFetch(url, args || {}); - return { - json: () => result, - text: () => result, - }; - }, - }); - - if (params.mockFetch) { - const { loadJS, loadCSS } = assets; - patchWithCleanup(assets, { - loadJS: async function (ressource) { - let res = await params.mockFetch(ressource, {}); - if (res === undefined) { - res = await loadJS(ressource); - } else { - console.log("%c[assets] fetch (mock) JS ressource " + ressource, "color: #66e; font-weight: bold;"); - } - return res; - }, - loadCSS: async function (ressource) { - let res = await params.mockFetch(ressource, {}); - if (res === undefined) { - res = await loadCSS(ressource); - } else { - console.log("%c[assets] fetch (mock) CSS ressource " + ressource, "color: #66e; font-weight: bold;"); - } - return res; - }, - }); - } - - const initialDOMDebounceValue = dom.DEBOUNCE; - dom.DEBOUNCE = 0; - - - // 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; - const __env = makeTestEnvironment(env, mockServer.performRpc.bind(mockServer)); - owl.Component.env = __env; - - // 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(); - } - }); - - dom.DEBOUNCE = initialDOMDebounceValue; - - // 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. - * - * @param {Widget} widget - * @param {Object} params - * @param {Object} [params.archs] a map of string [model,view_id,view_type] to - * a arch object. It is used to mock answers to 'load_views' custom events. - * This is useful when the widget instantiate a formview dialog that needs - * to load a particular arch. - * @param {string} [params.currentDate] a string representation of the current - * date. It is given to the mock server. - * @param {Object} params.data the data given to the created mock server. It is - * used to generate mock answers for every kind of routes supported by odoo - * @param {number} [params.debug] if set to true, logs RPCs and uncaught Odoo - * events. - * @param {Object} [params.bus] the instance of Bus that will be used (in the env) - * @param {function} [params.mockFetch] a function that will be used to override - * the _performFetch method from the mock server. It is really useful to add - * some custom fetch mocks, or to check some assertions. - * @param {function} [params.mockRPC] a function that will be used to override - * the _performRpc method from the mock server. It is really useful to add - * some custom rpc mocks, or to check some assertions. - * @param {Object} [params.session] if it is given, it will be used as answer - * for all calls to this.getSession() by the widget, of its children. Also, - * it will be used to extend the current, real session. This side effect is - * undone when the widget is destroyed. - * @param {Object} [params.translateParameters] if given, it will be used to - * extend the core._t.database.parameters object. After the widget - * destruction, the original parameters will be restored. - * @param {Object} [params.intercepts] an object with event names as key, and - * callback as value. Each key,value will be used to intercept the event. - * Note that this is particularly useful if you want to intercept events going - * up in the init process of the view, because there are no other way to do it - * after this method returns. Some events ('call_service', "load_views", - * "get_session", "load_filters") have a special treatment beforehand. - * @param {Object} [params.services={}] list of services to load in - * addition to the ajax service. For instance, if a test needs the local - * storage service in order to work, it can provide a mock version of it. - * @param {boolean} [debounce=true] set to false to completely remove the - * debouncing, forcing the handler to be called directly (not on the next - * execution stack, like it does with delay=0). - * @param {boolean} [throttle=false] set to true to keep the throttling, which - * is completely removed by default. - * - * @returns {Promise} the instance of the mock server, created by this - * function. It is necessary for createView so that method can call some - * other methods on it. - */ -async function addMockEnvironment(widget, params) { - // log events triggered up if debug flag is true - if (params.debug) { - _observe(widget); - var separator = window.location.href.indexOf('?') !== -1 ? "&" : "?"; - var url = window.location.href + separator + 'testId=' + QUnit.config.current.testId; - console.log('%c[debug] debug mode activated', 'color: blue; font-weight: bold;', url); - } - - // instantiate mock server - var Server = MockServer; - if (params.mockFetch) { - Server = MockServer.extend({ _performFetch: params.mockFetch }); - } - if (params.mockRPC) { - Server = Server.extend({ _performRpc: params.mockRPC }); - } - var mockServer = new Server(params.data, { - actions: params.actions, - archs: params.archs, - currentDate: params.currentDate, - debug: params.debug, - widget: 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(Component, params, mockServer); - const env = Component.env; - - // ensure to clean up everything when the widget will be destroyed - const destroy = widget.destroy; - widget.destroy = function () { - cleanUp(); - destroy.call(this, ...arguments); - }; - - // intercept service/data manager calls and redirect them to the env - intercept(widget, 'call_service', function (ev) { - 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); - } - }); - intercept(widget, "get_session", ev => { - ev.data.callback(session); - }); - // 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; -} - /** * Patch window.Date so that the time starts its flow from the provided Date. * @@ -580,8 +156,6 @@ function unpatch(target) { } export default { - addMockEnvironment: addMockEnvironment, - addMockEnvironmentOwl: addMockEnvironmentOwl, intercept: intercept, patchDate: legacyPatchDate, patch: patch, diff --git a/addons/web/static/tests/legacy/mock_relational_fields_tests.js b/addons/web/static/tests/legacy/mock_relational_fields_tests.js deleted file mode 100644 index 24a1d89cb14..00000000000 --- a/addons/web/static/tests/legacy/mock_relational_fields_tests.js +++ /dev/null @@ -1,137 +0,0 @@ -/** @odoo-module **/ - -import MockServer from '@web/../tests/legacy/helpers/mock_server'; - -QUnit.module('web', {}, function () { -QUnit.module('legacy', {}, function () { -QUnit.module('mock_relational_fields_tests.js', { - beforeEach() { - this.data = { - foo: { - fields: { - one2many_field: { type: 'one2many', relation: 'bar', inverse_fname_by_model_name: { bar: 'many2one_field' } }, - many2one_field: { type: 'many2one', relation: 'bar', inverse_fname_by_model_name: { bar: 'one2many_field' } }, - many2many_field: { type: 'many2many', relation: 'bar', inverse_fname_by_model_name: { bar: 'many2many_field' } }, - many2one_reference: { type: 'many2one_reference', model_name_ref_fname: 'res_model', inverse_fname_by_model_name: { bar: 'one2many_field' } }, - res_model: { type: 'char' }, - - }, - records: [], - }, - bar: { - fields: { - many2one_field: { type: 'many2one', relation: 'foo' }, - one2many_field: { type: 'one2many', relation: 'foo', inverse_fname_by_model_name: { foo: 'many2one_field' } }, - many2many_field: { type: 'many2many', relation: 'foo', inverse_fname_by_model_name: { foo: 'many2many_field' } }, - }, - records: [], - }, - }; - } -}); - -QUnit.test('many2one_ref should auto fill inverse field', async function (assert) { - this.data['bar'].records.push({ id: 1 }); - this.data['foo'].records.push({ - id: 2, - res_model: 'bar', - many2one_reference: 1, - }); - const mockServer = new MockServer(this.data, {}); - assert.deepEqual([2], mockServer.data['bar'].records[0].one2many_field); - - mockServer._mockUnlink('foo', [2]); - assert.deepEqual([], mockServer.data['bar'].records[0].one2many_field); -}); - -QUnit.test('many2one should auto fill inverse field', async function (assert) { - this.data['bar'].records.push({ id: 1 }); - this.data['foo'].records.push({ - id: 2, - many2one_field: 1, - }); - const mockServer = new MockServer(this.data, {}); - assert.deepEqual([2], mockServer.data['bar'].records[0].one2many_field); - - mockServer._mockUnlink('foo', [2]); - assert.deepEqual([], mockServer.data['bar'].records[0].one2many_field); -}); - -QUnit.test('one2many should auto fill inverse field', async function (assert) { - this.data['bar'].records.push({ id: 1 }); - this.data['bar'].records.push({ id: 2 }); - this.data['foo'].records.push({ - id: 3, - one2many_field: [1, 2], - }); - const mockServer = new MockServer(this.data, {}); - assert.strictEqual(3, mockServer.data['bar'].records[0].many2one_field); - assert.strictEqual(3, mockServer.data['bar'].records[1].many2one_field); - - mockServer._mockUnlink('foo', [3]); - assert.strictEqual(false, mockServer.data['bar'].records[0].many2one_field); - assert.strictEqual(false, mockServer.data['bar'].records[1].many2one_field); -}); - -QUnit.test('many2many should auto fill inverse field', async function (assert) { - this.data['bar'].records.push({ id: 1 }); - this.data['foo'].records.push({ - id: 2, - many2many_field: [1], - }); - const mockServer = new MockServer(this.data, {}); - assert.deepEqual([2], mockServer.data['bar'].records[0].many2many_field); - - mockServer._mockUnlink('foo', [2]); - assert.deepEqual([], mockServer.data['bar'].records[0].many2many_field); -}); - -QUnit.test('one2many update should update inverse field', async function (assert) { - this.data['bar'].records.push({ id: 1 }); - this.data['bar'].records.push({ id: 2 }); - this.data['foo'].records.push({ - id: 3, - one2many_field: [1, 2], - }); - const mockServer = new MockServer(this.data, {}); - mockServer._mockWrite('foo', [[3], { one2many_field: [1] }]); - assert.strictEqual(3, mockServer.data['bar'].records[0].many2one_field); - assert.strictEqual(false, mockServer.data['bar'].records[1].many2one_field); -}); - -QUnit.test('many2many update should update inverse field', async function (assert) { - this.data['bar'].records.push({ id: 1 }); - this.data['foo'].records.push({ - id: 2, - many2many_field: [1], - }); - const mockServer = new MockServer(this.data, {}); - mockServer._mockWrite('foo', [[2], { many2many_field: [] }]); - assert.deepEqual([], mockServer.data['bar'].records[0].many2many_field); -}); - -QUnit.test('many2one update should update inverse field', async function (assert) { - this.data['bar'].records.push({ id: 1 }); - this.data['foo'].records.push({ - id: 2, - many2one_field: 1, - }); - const mockServer = new MockServer(this.data, {}); - mockServer._mockWrite('foo', [[2], { many2one_field: false }]); - assert.deepEqual([], mockServer.data['bar'].records[0].one2many_field); -}); - -QUnit.test('many2one_ref update should update inverse field', async function (assert) { - this.data['bar'].records.push({ id: 1 }); - this.data['foo'].records.push({ - id: 2, - res_model: 'bar', - many2one_reference: 1, - }); - const mockServer = new MockServer(this.data, {}); - mockServer._mockWrite('foo', [[2], { many2one_reference: false }]); - assert.deepEqual([], mockServer.data['bar'].records[0].one2many_field); -}); - -}); -}); diff --git a/addons/web/static/tests/legacy/mockserver_tests.js b/addons/web/static/tests/legacy/mockserver_tests.js deleted file mode 100644 index 5aa8a8ab95c..00000000000 --- a/addons/web/static/tests/legacy/mockserver_tests.js +++ /dev/null @@ -1,465 +0,0 @@ -/** @odoo-module **/ - -import MockServer from "@web/../tests/legacy/helpers/mock_server"; - -QUnit.module("Legacy MockServer", { - beforeEach() { - this.data = { - "res.partner": { - fields: { - name: { - string: "Name", - type: "string", - }, - email: { - string: "Email", - type: "string", - }, - active: { - string: "Active", - type: "bool", - default: true, - }, - }, - records: [ - { id: 1, name: "Jean-Michel", email: "jean.michel@example.com" }, - { id: 2, name: "Raoul", email: "raoul@example.com", active: false }, - ], - }, - bar: { - fields: { - foo: { - string: "Foo", - type: "integer", - searchable: true, - group_operator: "sum", - }, - date: { string: "Date", type: "date", store: true, sortable: true }, - datetime: { - string: "DateTime", - type: "datetime", - store: true, - sortable: true, - }, - partners: { string: "Buddies", type: "many2many", relation: "res.partner" }, - }, - records: [ - { - id: 1, - foo: 12, - date: "2016-12-14", - datetime: "2016-12-14 12:34:56", - partners: [1, 2], - }, - { - id: 2, - foo: 1, - date: "2016-10-26", - datetime: "2016-10-26 12:34:56", - partners: [1], - }, - { - id: 3, - foo: 17, - date: "2016-12-15", - datetime: "2016-12-15 12:34:56", - partners: [2], - }, - { id: 4, foo: 2, date: "2016-04-11", datetime: "2016-04-11 12:34:56" }, - { id: 5, foo: 0, date: "2016-12-15", datetime: "2016-12-15 12:34:56" }, - { id: 6, foo: 42, date: "2019-12-30", datetime: "2019-12-30 12:34:56" }, - ], - }, - }; - }, -}, function () { - QUnit.test("performRpc: search_read with an empty array of fields", async function (assert) { - assert.expect(1); - const server = new MockServer(this.data, {}); - const result = await server.performRpc("", { - model: "res.partner", - method: "search_read", - args: [], - kwargs: { - fields: [], - }, - }); - const expectedFields = ["id", "email", "name", "display_name"]; - assert.strictEqual( - expectedFields.filter((x) => Object.keys(result[0]).indexOf(x) < 0).length, - 0, - "should contains all the fields" - ); - }); - - QUnit.test("performRpc: search_read without fields", async function (assert) { - assert.expect(1); - const server = new MockServer(this.data, {}); - const result = await server.performRpc("", { - model: "res.partner", - method: "search_read", - args: [], - kwargs: {}, - }); - const expectedFields = ["id", "email", "name", "display_name"]; - assert.strictEqual( - expectedFields.filter((x) => Object.keys(result[0]).indexOf(x) < 0).length, - 0, - "should contains all the fields" - ); - }); - - QUnit.test("performRpc: search with active_test=false", async function (assert) { - assert.expect(1); - const server = new MockServer(this.data, {}); - const result = await server.performRpc("", { - model: "res.partner", - method: "search", - args: [[]], - kwargs: { - context: { active_test: false }, - }, - }); - assert.deepEqual(result, [1, 2]); - }); - - QUnit.test("performRpc: search with active_test=true", async function (assert) { - assert.expect(1); - const server = new MockServer(this.data, {}); - const result = await server.performRpc("", { - model: "res.partner", - method: "search", - args: [[]], - kwargs: { - context: { active_test: true }, - }, - }); - assert.deepEqual(result, [1]); - }); - - QUnit.test("performRpc: search_read with active_test=false", async function (assert) { - assert.expect(1); - const server = new MockServer(this.data, {}); - const result = await server.performRpc("", { - model: "res.partner", - method: "search_read", - args: [[]], - kwargs: { - fields: ["name"], - context: { active_test: false }, - }, - }); - assert.deepEqual(result, [{id: 1, name: "Jean-Michel"}, {id: 2, name: "Raoul"}]); - }); - - QUnit.test("performRpc: search_read with active_test=true", async function (assert) { - assert.expect(1); - const server = new MockServer(this.data, {}); - const result = await server.performRpc("", { - model: "res.partner", - method: "search_read", - args: [[]], - kwargs: { - fields: ["name"], - context: { active_test: true }, - }, - }); - assert.deepEqual(result, [{id: 1, name: "Jean-Michel"}]); - }); - - QUnit.test("performRpc: read_group, group by date", async function (assert) { - assert.expect(10); - const server = new MockServer(this.data, {}); - let result = await server.performRpc("", { - model: "bar", - method: "read_group", - args: [[]], - kwargs: { - fields: ["foo"], - domain: [], - groupby: ["date"], //Month by default - }, - }); - assert.deepEqual( - result.map((x) => x.date), - ["December 2016", "October 2016", "April 2016", "December 2019"] - ); - assert.deepEqual( - result.map((x) => x.date_count), - [3, 1, 1, 1] - ); - - result = await server.performRpc("", { - model: "bar", - method: "read_group", - args: [[]], - kwargs: { - fields: ["foo"], - domain: [], - groupby: ["date:day"], - }, - }); - assert.deepEqual( - result.map((x) => x["date:day"]), - ["2016-12-14", "2016-10-26", "2016-12-15", "2016-04-11", "2019-12-30"] - ); - assert.deepEqual( - result.map((x) => x.date_count), - [1, 1, 2, 1, 1] - ); - - result = await server.performRpc("", { - model: "bar", - method: "read_group", - args: [[]], - kwargs: { - fields: ["foo"], - domain: [], - groupby: ["date:week"], - }, - }); - assert.deepEqual( - result.map((x) => x["date:week"]), - ["W50 2016", "W43 2016", "W15 2016", "W01 2020"] - ); - assert.deepEqual( - result.map((x) => x.date_count), - [3, 1, 1, 1] - ); - - result = await server.performRpc("", { - model: "bar", - method: "read_group", - args: [[]], - kwargs: { - fields: ["foo"], - domain: [], - groupby: ["date:quarter"], - }, - }); - assert.deepEqual( - result.map((x) => x["date:quarter"]), - ["Q4 2016", "Q2 2016", "Q4 2019"] - ); - assert.deepEqual( - result.map((x) => x.date_count), - [4, 1, 1] - ); - - result = await server.performRpc("", { - model: "bar", - method: "read_group", - args: [[]], - kwargs: { - fields: ["foo"], - domain: [], - groupby: ["date:year"], - }, - }); - assert.deepEqual( - result.map((x) => x["date:year"]), - ["2016", "2019"] - ); - assert.deepEqual( - result.map((x) => x.date_count), - [5, 1] - ); - }); - - QUnit.test("performRpc: read_group, group by datetime", async function (assert) { - const server = new MockServer(this.data, {}); - let result = await server.performRpc("", { - model: "bar", - method: "read_group", - args: [[]], - kwargs: { - fields: ["foo"], - domain: [], - groupby: ["datetime"], //Month by default - }, - }); - assert.deepEqual( - result.map((x) => x.datetime), - ["December 2016", "October 2016", "April 2016", "December 2019"] - ); - assert.deepEqual( - result.map((x) => x.datetime_count), - [3, 1, 1, 1] - ); - - result = await server.performRpc("", { - model: "bar", - method: "read_group", - args: [[]], - kwargs: { - fields: ["foo"], - domain: [], - groupby: ["datetime:hour"], - }, - }); - assert.deepEqual( - result.map((x) => x["datetime:hour"]), - ["13:00 14 Dec", "13:00 26 Oct", "13:00 15 Dec", "13:00 11 Apr", "13:00 30 Dec"] - ); - assert.deepEqual( - result.map((x) => x.datetime_count), - [1, 1, 2, 1, 1] - ); - - result = await server.performRpc("", { - model: "bar", - method: "read_group", - args: [[]], - kwargs: { - fields: ["foo"], - domain: [], - groupby: ["datetime:day"], - }, - }); - assert.deepEqual( - result.map((x) => x["datetime:day"]), - ["2016-12-14", "2016-10-26", "2016-12-15", "2016-04-11", "2019-12-30"] - ); - assert.deepEqual( - result.map((x) => x.datetime_count), - [1, 1, 2, 1, 1] - ); - - result = await server.performRpc("", { - model: "bar", - method: "read_group", - args: [[]], - kwargs: { - fields: ["foo"], - domain: [], - groupby: ["datetime:week"], - }, - }); - assert.deepEqual( - result.map((x) => x["datetime:week"]), - ["W50 2016", "W43 2016", "W15 2016", "W01 2020"] - ); - assert.deepEqual( - result.map((x) => x.datetime_count), - [3, 1, 1, 1] - ); - - result = await server.performRpc("", { - model: "bar", - method: "read_group", - args: [[]], - kwargs: { - fields: ["foo"], - domain: [], - groupby: ["datetime:quarter"], - }, - }); - assert.deepEqual( - result.map((x) => x["datetime:quarter"]), - ["Q4 2016", "Q2 2016", "Q4 2019"] - ); - assert.deepEqual( - result.map((x) => x.datetime_count), - [4, 1, 1] - ); - - result = await server.performRpc("", { - model: "bar", - method: "read_group", - args: [[]], - kwargs: { - fields: ["foo"], - domain: [], - groupby: ["datetime:year"], - }, - }); - assert.deepEqual( - result.map((x) => x["datetime:year"]), - ["2016", "2019"] - ); - assert.deepEqual( - result.map((x) => x.datetime_count), - [5, 1] - ); - }); - - QUnit.test("performRpc: read_group, group by m2m", async function (assert) { - const server = new MockServer(this.data, {}); - const result = await server.performRpc("", { - model: "bar", - method: "read_group", - args: [[]], - kwargs: { - fields: ["partners"], - domain: [], - groupby: ["partners"], - }, - }); - assert.deepEqual( - result, - [ - { - __domain: [["partners", "=", 1]], - partners: [1, "Jean-Michel"], - partners_count: 2, - }, - { - __domain: [["partners", "=", 2]], - partners: [2, "Raoul"], - partners_count: 2, - }, - { - __domain: [["partners", "=", false]], - partners: false, - partners_count: 3, - }, - ] - ); - }); - - QUnit.test("performRpc: read_group, group by integer", async function (assert) { - const server = new MockServer(this.data, {}); - const result = await server.performRpc("", { - model: "bar", - method: "read_group", - args: [[]], - kwargs: { - fields: ["foo"], - domain: [], - groupby: ["foo"], - }, - }); - assert.deepEqual(result, [ - { - __domain: [["foo", "=", 12]], - foo: 12, - foo_count: 1, - }, - { - __domain: [["foo", "=", 1]], - foo: 1, - foo_count: 1, - }, - { - __domain: [["foo", "=", 17]], - foo: 17, - foo_count: 1, - }, - { - __domain: [["foo", "=", 2]], - foo: 2, - foo_count: 1, - }, - { - __domain: [["foo", "=", 0]], - foo: 0, - foo_count: 1, - }, - { - __domain: [["foo", "=", 42]], - foo: 42, - foo_count: 1, - }, - ]); - }); -}); diff --git a/addons/web/static/tests/legacy/owl_compatibility_tests.js b/addons/web/static/tests/legacy/owl_compatibility_tests.js index cdbeaf93be4..c980f02a4ed 100644 --- a/addons/web/static/tests/legacy/owl_compatibility_tests.js +++ b/addons/web/static/tests/legacy/owl_compatibility_tests.js @@ -17,7 +17,6 @@ const makeTestPromise = testUtils.makeTestPromise; const nextTick = testUtils.nextTick; - const addMockEnvironmentOwl = testUtils.mock.addMockEnvironmentOwl; const { Component, @@ -426,40 +425,6 @@ assert.verifySteps(['a', 'b']); }); - QUnit.test("sub widget that calls _rpc", async function (assert) { - assert.expect(3); - - const MyWidget = Widget.extend({ - willStart: function () { - return this._rpc({ route: 'some/route', params: { val: 2 } }); - }, - }); - class Parent extends LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - } - } - Parent.template = xml` -
- -
`; - Parent.components = { ComponentAdapter }; - const cleanUp = await addMockEnvironmentOwl(Parent, { - mockRPC: function (route, args) { - assert.step(`${route} ${args.val}`); - return Promise.resolve(); - }, - }); - - const target = getFixture(); - const parent = await mount(Parent, target, { env: owl.Component.env }); - - assert.strictEqual(parent.el.innerHTML, '
'); - assert.verifySteps(['some/route 2']); - cleanUp(); - }); - QUnit.test("sub widget that calls a service", async function (assert) { assert.expect(1); @@ -501,34 +466,6 @@ await mount(Parent, target, { env }); }); - QUnit.test("sub widget that requests the session", async function (assert) { - assert.expect(1); - - const MyWidget = Widget.extend({ - start: function () { - assert.strictEqual(this.getSession().key, 'value'); - }, - }); - class Parent extends LegacyComponent { - constructor() { - super(...arguments); - this.MyWidget = MyWidget; - } - } - Parent.template = xml` -
- -
`; - Parent.components = { ComponentAdapter }; - const cleanUp = await addMockEnvironmentOwl(Parent, { - session: { key: 'value' }, - }); - - const target = getFixture(); - await mount(Parent, target, { env: owl.Component.env }); - cleanUp(); - }); - QUnit.test("sub widgets in a t-if/t-else", async function (assert) { assert.expect(3); diff --git a/addons/web/static/tests/legacy/widgets/name_and_signature_tests.js b/addons/web/static/tests/legacy/widgets/name_and_signature_tests.js index 87930340646..dd4eb5638bc 100644 --- a/addons/web/static/tests/legacy/widgets/name_and_signature_tests.js +++ b/addons/web/static/tests/legacy/widgets/name_and_signature_tests.js @@ -2,6 +2,7 @@ import { NameAndSignature } from "@web/legacy/js/widgets/name_and_signature"; import testUtils from "@web/../tests/legacy/helpers/test_utils"; + const MockedNameAndSignature = NameAndSignature.extend({ events: { ...NameAndSignature.prototype.events, @@ -14,10 +15,10 @@ async function MockedNameAndSignatureGenerator (options) { const parent = $("#qunit-fixture"); const mockedNameAndSignature = new MockedNameAndSignature(parent, options); - await testUtils.mock.addMockEnvironment(mockedNameAndSignature, { - mockRPC: function (route, args) { - if (route == "/web/sign/get_fonts/") { - return Promise.resolve(); + testUtils.mock.intercept(mockedNameAndSignature, 'call_service', function ({ data }) { + if (data.service === "ajax" && data.method === "rpc") { + if (data.args[0] === "/web/sign/get_fonts/") { + data.callback(Promise.resolve()); } } }); diff --git a/addons/web/static/tests/views/fields/many2one_field_tests.js b/addons/web/static/tests/views/fields/many2one_field_tests.js index a8adebbb6f9..0f2231f9f32 100644 --- a/addons/web/static/tests/views/fields/many2one_field_tests.js +++ b/addons/web/static/tests/views/fields/many2one_field_tests.js @@ -4242,7 +4242,6 @@ QUnit.module("Fields", (hooks) => { }; const webClient = await createWebClient({ serverData, - legacyParams: { withLegacyMockServer: true }, mockRPC: function (route, args) { if (args.method === "get_formview_action") { assert.step("get_formview_action"); diff --git a/addons/web/static/tests/views/fields/one2many_field_tests.js b/addons/web/static/tests/views/fields/one2many_field_tests.js index 606754ae631..8cc6bda3ca1 100644 --- a/addons/web/static/tests/views/fields/one2many_field_tests.js +++ b/addons/web/static/tests/views/fields/one2many_field_tests.js @@ -10443,10 +10443,6 @@ QUnit.module("Fields", (hooks) => { mockRPC(route, args) { assert.step(args.method); }, - // we set a fieldDebounce to precisely mock the behavior of the webclient: changes are - // not sent to the model at keystrokes, but when the input is left - - // legacyParams: { fieldDebounce: 5000 }, // WOWL keep this in view API? }); await addRow(target); diff --git a/addons/web/static/tests/views/helpers.js b/addons/web/static/tests/views/helpers.js index 66b529af955..d5bee6b5273 100644 --- a/addons/web/static/tests/views/helpers.js +++ b/addons/web/static/tests/views/helpers.js @@ -17,7 +17,6 @@ import { setupControlPanelFavoriteMenuRegistry, setupControlPanelServiceRegistry, } from "../search/helpers"; -import { addLegacyMockEnvironment } from "../webclient/helpers"; import { Component, useSubEnv, xml } from "@odoo/owl"; @@ -52,11 +51,9 @@ async function _makeView(params, inDialog = false) { ...getDefaultConfig(), ...props.config, }; - const legacyParams = props.legacyParams || {}; delete props.serverData; delete props.mockRPC; - delete props.legacyParams; delete props.config; if (props.arch) { @@ -73,22 +70,6 @@ async function _makeView(params, inDialog = false) { const env = await makeTestEnv({ serverData, mockRPC }); Object.assign(env, createDebugContext(env)); // This is needed if the views are in debug mode - /** Legacy Environment, for compatibility sakes - * Remove this as soon as we drop the legacy support - */ - const models = params.serverData.models; - if (legacyParams && legacyParams.withLegacyMockServer && models) { - legacyParams.models = Object.assign({}, 0); - // In lagacy, data may not be sole models, but can contain some other variables - // So we filter them out for our WOWL mockServer - Object.entries(legacyParams.models).forEach(([k, v]) => { - if (!(v instanceof Object) || !("fields" in v)) { - delete models[k]; - } - }); - } - await addLegacyMockEnvironment(env, legacyParams); - const target = getFixture(); const viewEnv = Object.assign(Object.create(env), { config }); diff --git a/addons/web/static/tests/webclient/helpers.js b/addons/web/static/tests/webclient/helpers.js index 4d8624960dd..7eb40ce363a 100644 --- a/addons/web/static/tests/webclient/helpers.js +++ b/addons/web/static/tests/webclient/helpers.js @@ -6,21 +6,12 @@ import { ormService } from "@web/core/orm_service"; import { popoverService } from "@web/core/popover/popover_service"; import { registry } from "@web/core/registry"; import { legacyServiceProvider } from "@web/legacy/legacy_service_provider"; -import { - makeLegacyNotificationService, - mapLegacyEnvToWowlEnv, - makeLegacySessionService, -} from "@web/legacy/utils"; -import { makeLegacyActionManagerService } from "@web/legacy/backend_utils"; import { viewService } from "@web/views/view_service"; import { actionService } from "@web/webclient/actions/action_service"; import { effectService } from "@web/core/effects/effect_service"; import { hotkeyService } from "@web/core/hotkeys/hotkey_service"; import { menuService } from "@web/webclient/menus/menu_service"; import { WebClient } from "@web/webclient/webclient"; -import AbstractService from "@web/legacy/js/core/abstract_service"; -import core from "@web/legacy/js/services/core"; -import makeTestEnvironment from "@web/../tests/legacy/helpers/test_env"; import { registerCleanup } from "../helpers/cleanup"; import { makeTestEnv } from "../helpers/mock_env"; import { @@ -31,15 +22,10 @@ import { makeFakeHTTPService, makeFakeUserService, } from "../helpers/mock_services"; -import { getFixture, mount, nextTick, patchWithCleanup } from "../helpers/utils"; -import session from "web.session"; -import LegacyMockServer from "@web/../tests/legacy/helpers/mock_server"; -import Widget from "@web/legacy/js/core/widget"; +import { getFixture, mount, nextTick } from "../helpers/utils"; import { uiService } from "@web/core/ui/ui_service"; import { commandService } from "@web/core/commands/command_service"; -import { ConnectionAbortedError } from "@web/core/network/rpc_service"; import { CustomFavoriteItem } from "@web/search/custom_favorite_item/custom_favorite_item"; -import { standaloneAdapter } from "@web/legacy/js/owl_compatibility"; import { overlayService } from "@web/core/overlay/overlay_service"; import { Component, xml } from "@odoo/owl"; @@ -99,87 +85,6 @@ export function setupWebClientRegistries() { } } -/** - * Remove this as soon as we drop the legacy support - */ -export async function addLegacyMockEnvironment(env, legacyParams = {}) { - // setup a legacy env - let localSession; - if (legacyParams && legacyParams.getTZOffset) { - patchWithCleanup(session, { - getTZOffset: legacyParams.getTZOffset, - }); - localSession = { getTZOffset: legacyParams.getTZOffset }; - } - - const baseEnv = { bus: core.bus, session: localSession }; - const legacyEnv = makeTestEnvironment(Object.assign(baseEnv, legacyParams.env)); - - if (legacyParams.serviceRegistry) { - const legacyServiceMap = core.serviceRegistry.map; - core.serviceRegistry.map = legacyParams.serviceRegistry.map; - // notification isn't a deployed service, but it is added by `makeTestEnvironment`. - // Here, we want full control on the deployed services, so we simply remove it. - delete legacyEnv.services.notification; - AbstractService.prototype.deployServices(legacyEnv); - registerCleanup(() => { - core.serviceRegistry.map = legacyServiceMap; - }); - } - - Component.env = legacyEnv; - mapLegacyEnvToWowlEnv(legacyEnv, env); - function patchLegacySession() { - const userContext = Object.getOwnPropertyDescriptor(session, "user_context"); - registerCleanup(() => { - Object.defineProperty(session, "user_context", userContext); - }); - } - patchLegacySession(); - serviceRegistry.add("legacy_session", makeLegacySessionService(legacyEnv, session)); - // deploy the legacyActionManagerService (in Wowl env) - const legacyActionManagerService = makeLegacyActionManagerService(legacyEnv); - serviceRegistry.add("legacy_action_manager", legacyActionManagerService); - serviceRegistry.add("legacy_notification", makeLegacyNotificationService(legacyEnv)); - // deploy wowl services into the legacy env. - const wowlToLegacyServiceMappers = registry.category("wowlToLegacyServiceMappers").getEntries(); - for (const [legacyServiceName, wowlToLegacyServiceMapper] of wowlToLegacyServiceMappers) { - serviceRegistry.add(legacyServiceName, wowlToLegacyServiceMapper(legacyEnv)); - } - - if (legacyParams.withLegacyMockServer) { - const adapter = standaloneAdapter({ Component }); - registerCleanup(() => adapter.__owl__.app.destroy()); - adapter.env = legacyEnv; - const W = Widget.extend({ do_push_state() {} }); - const widget = new W(adapter); - const legacyMockServer = new LegacyMockServer(legacyParams.models, { widget }); - const originalRPC = env.services.rpc; - const rpc = async (...args) => { - try { - return await originalRPC(...args); - } catch (e) { - if (e.message.includes("Unimplemented")) { - return legacyMockServer._performRpc(...args); - } else { - throw e; - } - } - }; - env.services.rpc = function () { - let rejectFn; - const rpcProm = new Promise((resolve, reject) => { - rejectFn = reject; - rpc(...arguments) - .then(resolve) - .catch(reject); - }); - rpcProm.abort = () => rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort")); - return rpcProm; - }; - } -} - /** * This method create a web client instance properly configured. * @@ -191,26 +96,12 @@ export async function addLegacyMockEnvironment(env, legacyParams = {}) { export async function createWebClient(params) { setupWebClientRegistries(); - const legacyParams = params.legacyParams; params.serverData = params.serverData || {}; - const models = params.serverData.models; - if (legacyParams && legacyParams.withLegacyMockServer && models) { - legacyParams.models = Object.assign({}, models); - // In lagacy, data may not be sole models, but can contain some other variables - // So we filter them out for our WOWL mockServer - Object.entries(legacyParams.models).forEach(([k, v]) => { - if (!(v instanceof Object) || !("fields" in v)) { - delete models[k]; - } - }); - } - const mockRPC = params.mockRPC || undefined; const env = await makeTestEnv({ serverData: params.serverData, mockRPC, }); - await addLegacyMockEnvironment(env, legacyParams); const WebClientClass = params.WebClientClass || WebClient; const target = params && params.target ? params.target : getFixture(); diff --git a/addons/web_editor/static/tests/test_utils.js b/addons/web_editor/static/tests/test_utils.js index d15b4ae06d8..0d38080ad2b 100644 --- a/addons/web_editor/static/tests/test_utils.js +++ b/addons/web_editor/static/tests/test_utils.js @@ -2,7 +2,6 @@ import ajax from "@web/legacy/js/core/ajax"; import { MockServer } from "@web/../tests/helpers/mock_server"; -import MockServerLegacy from "@web/../tests/legacy/helpers/mock_server"; import testUtils from "@web/../tests/legacy/helpers/test_utils"; import * as coreUtils from "@web/core/utils/patch"; import * as OdooEditorLib from "@web_editor/js/editor/odoo-editor/src/OdooEditor"; @@ -117,28 +116,6 @@ coreUtils.patch(MockServer.prototype, { return super._performRPC(...arguments); }, }); -MockServerLegacy.include({ - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - * @private - * @returns {Promise} - */ - async _performRpc(route, args) { - if (args.model === "ir.ui.view" && args.method === 'render_public_asset') { - if (args.args[0] === "web_editor.colorpicker") { - return COLOR_PICKER_TEMPLATE; - } - if (args.args[0] === "web_editor.snippets") { - return SNIPPETS_TEMPLATE; - } - } - return this._super(...arguments); - }, -}); /** * Options with animation and edition for test.