diff --git a/addons/mass_mailing/static/tests/mass_mailing_favourite_filter_tests.js b/addons/mass_mailing/static/tests/mass_mailing_favourite_filter_tests.js index 18750d5c68c..9702a1149c4 100644 --- a/addons/mass_mailing/static/tests/mass_mailing_favourite_filter_tests.js +++ b/addons/mass_mailing/static/tests/mass_mailing_favourite_filter_tests.js @@ -3,6 +3,8 @@ import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; import * as testUtils from "@web/../tests/helpers/utils"; import weTestUtils from "web_editor.test_utils"; +import { fieldService } from "@web/core/field_service"; +import { registry } from "@web/core/registry"; let fixture; let serverData; @@ -107,6 +109,7 @@ QUnit.module('favorite filter widget', (hooks) => { }); serverData = { models }; setupViewRegistries(); + registry.category("services").add("field", fieldService); }); QUnit.test('create favorite filter', async (assert) => { diff --git a/addons/web/static/src/core/domain_selector/domain_selector.js b/addons/web/static/src/core/domain_selector/domain_selector.js index 6bcc5115ef7..3582e3e5442 100644 --- a/addons/web/static/src/core/domain_selector/domain_selector.js +++ b/addons/web/static/src/core/domain_selector/domain_selector.js @@ -41,7 +41,7 @@ export class DomainSelector extends Component { }; setup() { - this.view = useService("view"); + this.fieldService = useService("field"); this.treeBuilder = new DomainTreeBuilder(); this.tree = useState({ isSupported: false, @@ -107,16 +107,16 @@ export class DomainSelector extends Component { if ("01".includes(field.toString())) { return { type: "integer" }; } - - const names = field.length ? field.split(".") : []; - let currentModel = resModel; - let fieldDef = null; - for (const name of names) { - const model = await this.view.loadFields(currentModel); - fieldDef = model[name]; - currentModel = fieldDef.relation; + if (typeof field !== "string" || !field) { + return null; } - return fieldDef; + const { isInvalid, names, modelsInfo } = await this.fieldService.loadPath(resModel, field); + if (isInvalid) { + return null; + } + const name = names.at(-1); + const { fieldDefs } = modelsInfo.at(-1); + return fieldDefs[name]; } createNewLeaf() { diff --git a/addons/web/static/src/core/field_service.js b/addons/web/static/src/core/field_service.js new file mode 100644 index 00000000000..d35cd0f999e --- /dev/null +++ b/addons/web/static/src/core/field_service.js @@ -0,0 +1,102 @@ +/** @odoo-module **/ + +import { Cache } from "@web/core/utils/cache"; +import { registry } from "@web/core/registry"; + +/** + * @typedef {Object} LoadFieldsOptions + * @property {string[]|false} [fieldNames] + * @property {string[]} [attributes] + */ + +export const fieldService = { + dependencies: ["orm"], + start(env, { orm }) { + const cache = new Cache( + (resModel, options) => { + return orm + .call(resModel, "fields_get", [options.fieldNames, options.attributes]) + .catch((error) => { + cache.clear(resModel, options); + return Promise.reject(error); + }); + }, + (resModel, options) => + JSON.stringify([resModel, options.fieldNames, options.attributes]) + ); + + env.bus.addEventListener("CLEAR-CACHES", () => cache.invalidate()); + + /** + * @param {string} resModel + * @param {LoadFieldsOptions} [options] + * @returns {Promise} + */ + async function loadFields(resModel, options = {}) { + if (typeof resModel !== "string" || !resModel) { + throw new Error(`Invalid model name: ${resModel}`); + } + return cache.read(resModel, options); + } + + /** + * @param {string} resModel valid model name + * @param {Object} fieldDefs + * @param {string[]} names + */ + async function _loadPath(resModel, fieldDefs, names) { + const [name, ...remainingNames] = names; + const modelsInfo = [{ resModel, fieldDefs }]; + + const fieldDef = fieldDefs[name]; + if (name !== "*" && !fieldDef) { + return { isInvalid: "path", names, modelsInfo }; + } + + if (!remainingNames.length) { + return { names, modelsInfo }; + } + + if (name === "*") { + return { isInvalid: "path", names, modelsInfo }; + } + + if (fieldDef.relation) { + const subResult = await _loadPath( + fieldDef.relation, + await loadFields(fieldDef.relation), + remainingNames + ); + const result = { + names, + modelsInfo: [...modelsInfo, ...subResult.modelsInfo], + }; + if (subResult.isInvalid) { + result.isInvalid = "path"; + } + return result; + } + + return { isInvalid: "path", names, modelsInfo }; + } + + /** + * Note: the symbol * can be used at the end of path (e.g path="*" or path="user_id.*"). + * It says to load the fields of the appropriate model. + * @param {string} resModel + * @param {string} path + * @returns {Promise} + */ + async function loadPath(resModel, path = "*") { + const fieldDefs = await loadFields(resModel); + if (typeof path !== "string" || !path) { + throw new Error(`Invalid path: ${path}`); + } + return _loadPath(resModel, fieldDefs, path.split(".")); + } + + return { loadFields, loadPath }; + }, +}; + +registry.category("services").add("field", fieldService); diff --git a/addons/web/static/src/core/model_field_selector/model_field_hook.js b/addons/web/static/src/core/model_field_selector/model_field_hook.js index ffe44171df1..ca31dfd4fb3 100644 --- a/addons/web/static/src/core/model_field_selector/model_field_hook.js +++ b/addons/web/static/src/core/model_field_selector/model_field_hook.js @@ -1,33 +1,32 @@ /** @odoo-module **/ import { useService } from "@web/core/utils/hooks"; +import { zipWith } from "@web/core/utils/arrays"; export function useModelField() { - const view = useService("view"); + const fieldService = useService("field"); const loadModelFields = (resModel) => { - return view.loadFields(resModel); + return fieldService.loadFields(resModel); }; - const loadChain = async (resModel, fieldName) => { - const fieldNameChain = fieldName.length ? fieldName.split(".") : []; - let currentNode = { - resModel, - field: null, - }; - const chain = [currentNode]; - for (const fieldName of fieldNameChain) { - const fieldsInfo = await loadModelFields(currentNode.resModel); - Object.assign(currentNode, { - field: { ...fieldsInfo[fieldName], name: fieldName }, - }); - if (fieldsInfo[fieldName].relation) { - currentNode = { - resModel: fieldsInfo[fieldName].relation, - field: null, - }; - chain.push(currentNode); - } + const loadChain = async (resModel, path) => { + if ("01".includes(path.toString())) { + return [{ resModel, field: null }]; + } + if (typeof path !== "string" || !path) { + return [{ resModel, field: null }]; + } + const { isInvalid, names, modelsInfo } = await fieldService.loadPath(resModel, path); + if (isInvalid) { + return [{ resModel, field: null }]; + } + const chain = zipWith(names, modelsInfo, (name, { resModel, fieldDefs }) => { + return { resModel, field: fieldDefs[name] }; + }); + const lastField = chain.at(-1)?.field; + if (lastField.relation) { + chain.push({ resModel: lastField.relation, field: null }); } return chain; }; diff --git a/addons/web/static/src/views/view_service.js b/addons/web/static/src/views/view_service.js index 4c00ee5fa03..3d36c9a5e6d 100644 --- a/addons/web/static/src/views/view_service.js +++ b/addons/web/static/src/views/view_service.js @@ -41,12 +41,6 @@ import { UPDATE_METHODS } from "@web/core/orm_service"; * @property {boolean} loadIrFilters */ -/** - * @typedef {Object} LoadFieldsOptions - * @property {string[] | false} [fieldNames] - * @property {string[]} [attributes] - */ - export const viewService = { dependencies: ["orm"], start(env, { orm }) { @@ -69,31 +63,6 @@ export const viewService = { } }); - /** - * Loads fields information - * - * @param {string} resModel - * @param {LoadFieldsOptions} [options] - * @returns {Promise} - */ - async function loadFields(resModel, options = {}) { - const key = JSON.stringify([ - "fields", - resModel, - options.fieldNames, - options.attributes, - ]); - if (!cache[key]) { - cache[key] = orm - .call(resModel, "fields_get", [options.fieldNames, options.attributes]) - .catch((error) => { - delete cache[key]; - return Promise.reject(error); - }); - } - return cache[key]; - } - /** * Loads various information concerning views: fields_view for each view, * fields of the corresponding model, and optionally the filters. @@ -128,10 +97,6 @@ export const viewService = { relatedModels: models, views: {}, }; - for (const [resModel, fields] of Object.entries(modelsCopy)) { - const key = JSON.stringify(["fields", resModel, undefined, undefined]); - cache[key] = Promise.resolve(fields); - } for (const viewType in views) { const { arch, toolbar, id, filters, custom_view_id } = views[viewType]; const viewDescription = { arch, id, custom_view_id }; @@ -152,7 +117,7 @@ export const viewService = { } return cache[key]; } - return { loadViews, loadFields }; + return { loadViews }; }, }; diff --git a/addons/web/static/tests/core/domain_selector_tests.js b/addons/web/static/tests/core/domain_selector_tests.js index 5727789fd05..e2373592c52 100644 --- a/addons/web/static/tests/core/domain_selector_tests.js +++ b/addons/web/static/tests/core/domain_selector_tests.js @@ -3,13 +3,13 @@ import { Component, xml } from "@odoo/owl"; import { DomainSelector } from "@web/core/domain_selector/domain_selector"; import { OPERATOR_DESCRIPTIONS } from "@web/core/domain_selector/domain_selector_operators"; +import { fieldService } from "@web/core/field_service"; import { hotkeyService } from "@web/core/hotkeys/hotkey_service"; import { MainComponentsContainer } from "@web/core/main_components_container"; import { ormService } from "@web/core/orm_service"; import { popoverService } from "@web/core/popover/popover_service"; import { registry } from "@web/core/registry"; import { uiService } from "@web/core/ui/ui_service"; -import { viewService } from "@web/views/view_service"; import { registerCleanup } from "../helpers/cleanup"; import { makeTestEnv } from "../helpers/mock_env"; import { makeFakeLocalizationService } from "../helpers/mock_services"; @@ -64,7 +64,7 @@ QUnit.module("Components", (hooks) => { registry.category("services").add("ui", uiService); registry.category("services").add("hotkey", hotkeyService); registry.category("services").add("localization", makeFakeLocalizationService()); - registry.category("services").add("view", viewService); + registry.category("services").add("field", fieldService); target = getFixture(); }); diff --git a/addons/web/static/tests/core/field_service_tests.js b/addons/web/static/tests/core/field_service_tests.js new file mode 100644 index 00000000000..5e85c52bdad --- /dev/null +++ b/addons/web/static/tests/core/field_service_tests.js @@ -0,0 +1,187 @@ +/** @odoo-module **/ + +import { fieldService } from "@web/core/field_service"; +import { makeTestEnv } from "@web/../tests/helpers/mock_env"; +import { registry } from "@web/core/registry"; + +const serviceRegistry = registry.category("services"); + +function getModelInfo(resModel) { + return { + resModel, + fieldDefs: serverData.models[resModel].fields, + }; +} + +let serverData; +QUnit.module("Field Service", { + async beforeEach() { + serverData = { + models: { + tortoise: { + fields: { + id: { string: "ID", type: "integer" }, + display_name: { string: "Display Name", type: "char" }, + name: { string: "Name", type: "char", default: "name" }, + write_date: { string: "Last Modified on", type: "datetime" }, + age: { type: "integer", string: "Age" }, + location_id: { type: "many2one", string: "Location", relation: "location" }, + }, + }, + location: { + fields: { + id: { string: "ID", type: "integer" }, + display_name: { string: "Display Name", type: "char" }, + name: { string: "Name", type: "char", default: "name" }, + write_date: { string: "Last Modified on", type: "datetime" }, + tortoise_ids: { type: "one2many", string: "Turtles", relation: "tortoise" }, + }, + }, + }, + }; + serviceRegistry.add("field", fieldService); + }, +}); + +QUnit.test("loadPath", async (assert) => { + const toTest = [ + { + resModel: "tortoise", + path: "*", + expectedResult: { + names: ["*"], + modelsInfo: [getModelInfo("tortoise")], + }, + }, + { + resModel: "tortoise", + path: "*.a", + expectedResult: { + isInvalid: "path", + names: ["*", "a"], + modelsInfo: [getModelInfo("tortoise")], + }, + }, + { + resModel: "tortoise", + path: "location_id.*", + expectedResult: { + names: ["location_id", "*"], + modelsInfo: [getModelInfo("tortoise"), getModelInfo("location")], + }, + }, + { + resModel: "tortoise", + path: "age", + expectedResult: { + names: ["age"], + modelsInfo: [getModelInfo("tortoise")], + }, + }, + { + resModel: "tortoise", + path: "location_id", + expectedResult: { + names: ["location_id"], + modelsInfo: [getModelInfo("tortoise")], + }, + }, + { + resModel: "tortoise", + path: "location_id.tortoise_ids", + expectedResult: { + names: ["location_id", "tortoise_ids"], + modelsInfo: [getModelInfo("tortoise"), getModelInfo("location")], + }, + }, + { + resModel: "tortoise", + path: "location_id.tortoise_ids.age", + expectedResult: { + names: ["location_id", "tortoise_ids", "age"], + modelsInfo: [ + getModelInfo("tortoise"), + getModelInfo("location"), + getModelInfo("tortoise"), + ], + }, + }, + { + resModel: "tortoise", + path: "location_id.tortoise_ids.age", + expectedResult: { + names: ["location_id", "tortoise_ids", "age"], + modelsInfo: [ + getModelInfo("tortoise"), + getModelInfo("location"), + getModelInfo("tortoise"), + ], + }, + }, + ]; + + const env = await makeTestEnv({ serverData }); + + for (const { resModel, path, expectedResult } of toTest) { + const result = await env.services.field.loadPath(resModel, path); + assert.deepEqual(result, expectedResult); + } + + const errorToTest = [ + { resModel: "notAModel" }, + { resModel: "tortoise", path: {} }, + { resModel: "tortoise", path: "" }, + ]; + + for (const { resModel, path } of errorToTest) { + try { + await env.services.field.loadPath(resModel, path); + } catch { + assert.step("error"); + } + } + assert.verifySteps(errorToTest.map(() => "error")); +}); + +QUnit.test("store loadFields calls in cache in success", async (assert) => { + assert.expect(2); + + const mockRPC = (route) => { + if (route.includes("fields_get")) { + assert.step("fields_get"); + } + }; + + const env = await makeTestEnv({ serverData, mockRPC }); + + await env.services.field.loadFields("tortoise"); + await env.services.field.loadFields("tortoise"); + + assert.verifySteps(["fields_get"]); +}); + +QUnit.test("does not store loadFields calls in cache when failed", async (assert) => { + assert.expect(5); + + const mockRPC = (route) => { + if (route.includes("fields_get")) { + assert.step("fields_get"); + return Promise.reject("my little error"); + } + }; + + const env = await makeTestEnv({ serverData, mockRPC }); + + try { + await env.services.field.loadFields("take.five"); + } catch (error) { + assert.strictEqual(error, "my little error"); + } + try { + await env.services.field.loadFields("take.five"); + } catch (error) { + assert.strictEqual(error, "my little error"); + } + + assert.verifySteps(["fields_get", "fields_get"]); +}); diff --git a/addons/web/static/tests/core/model_field_selector_tests.js b/addons/web/static/tests/core/model_field_selector_tests.js index 539f666c59c..2c0739848d2 100644 --- a/addons/web/static/tests/core/model_field_selector_tests.js +++ b/addons/web/static/tests/core/model_field_selector_tests.js @@ -6,12 +6,12 @@ import { ormService } from "@web/core/orm_service"; import { popoverService } from "@web/core/popover/popover_service"; import { registry } from "@web/core/registry"; import { uiService } from "@web/core/ui/ui_service"; -import { viewService } from "@web/views/view_service"; import { makeTestEnv } from "../helpers/mock_env"; import { click, getFixture, triggerEvent, mount, editInput } from "../helpers/utils"; import { makeFakeLocalizationService } from "../helpers/mock_services"; import { Component, xml } from "@odoo/owl"; +import { fieldService } from "@web/core/field_service"; let target; let serverData; @@ -60,7 +60,7 @@ QUnit.module("Components", (hooks) => { registry.category("services").add("orm", ormService); registry.category("services").add("localization", makeFakeLocalizationService()); registry.category("services").add("ui", uiService); - registry.category("services").add("view", viewService); + registry.category("services").add("field", fieldService); target = getFixture(); }); diff --git a/addons/web/static/tests/views/fields/domain_field_tests.js b/addons/web/static/tests/views/fields/domain_field_tests.js index cba2ad071be..149160f9515 100644 --- a/addons/web/static/tests/views/fields/domain_field_tests.js +++ b/addons/web/static/tests/views/fields/domain_field_tests.js @@ -13,6 +13,8 @@ import { triggerEvent, } from "@web/../tests/helpers/utils"; import { createWebClient, doAction } from "@web/../tests/webclient/helpers"; +import { fieldService } from "@web/core/field_service"; +import { registry } from "@web/core/registry"; let serverData; let target; @@ -91,6 +93,7 @@ QUnit.module("Fields", (hooks) => { }; setupViewRegistries(); + registry.category("services").add("field", fieldService); }); QUnit.module("DomainField"); @@ -815,7 +818,7 @@ QUnit.module("Fields", (hooks) => { assert.verifySteps(["/web/webclient/load_menus"]); await doAction(webClient, 1); - assert.verifySteps(["/web/action/load", "get_views", "read", "search_count"]); + assert.verifySteps(["/web/action/load", "get_views", "read", "search_count", "fields_get"]); assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain); assert.containsOnce(target, ".o_datepicker", "there should be a datepicker"); diff --git a/addons/web/static/tests/views/fields/properties_field_tests.js b/addons/web/static/tests/views/fields/properties_field_tests.js index bc29b06559c..17ea6f40304 100644 --- a/addons/web/static/tests/views/fields/properties_field_tests.js +++ b/addons/web/static/tests/views/fields/properties_field_tests.js @@ -12,6 +12,8 @@ import { } from "@web/../tests/helpers/utils"; import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; import { browser } from "@web/core/browser/browser"; +import { fieldService } from "@web/core/field_service"; +import { registry } from "@web/core/registry"; let serverData; let target; @@ -133,10 +135,10 @@ QUnit.module("Fields", (hooks) => { id: 3, display_name: "third partner", properties: [ - {name: "property_1", type: "char"}, - {name: "property_2", type: "char", definition_deleted: true}, - {name: "property_3", type: "char", definition_changed: true}, - {name: "property_4", type: "char"}, + { name: "property_1", type: "char" }, + { name: "property_2", type: "char", definition_deleted: true }, + { name: "property_3", type: "char", definition_changed: true }, + { name: "property_4", type: "char" }, ], company_id: 37, }, @@ -144,8 +146,8 @@ QUnit.module("Fields", (hooks) => { id: 4, display_name: "fourth partner", properties: [ - {name: "property_2", type: "char", definition_deleted: true}, - {name: "property_3", type: "char", definition_deleted: true}, + { name: "property_2", type: "char", definition_deleted: true }, + { name: "property_3", type: "char", definition_deleted: true }, ], company_id: 37, }, @@ -220,6 +222,7 @@ QUnit.module("Fields", (hooks) => { }; setupViewRegistries(); + registry.category("services").add("field", fieldService); patchWithCleanup(browser, { setTimeout: (fn) => fn(), @@ -1308,18 +1311,20 @@ QUnit.module("Fields", (hooks) => { * Check the behavior of the domain (properies with "definition_deleted" should be ignored). * In that case, some properties start without the flag "definition_deleted". */ - QUnit.test("properties: form view and falsy domain, properties are not empty", async function (assert) { - async function mockRPC(route, { method, model, kwargs }) { - if (method === "check_access_rights") { - return true; + QUnit.test( + "properties: form view and falsy domain, properties are not empty", + async function (assert) { + async function mockRPC(route, { method, model, kwargs }) { + if (method === "check_access_rights") { + return true; + } } - } - await makeView({ - type: "form", - resModel: "partner", - resId: 3, - serverData, - arch: ` + await makeView({ + type: "form", + resModel: "partner", + resId: 3, + serverData, + arch: `
@@ -1332,45 +1337,48 @@ QUnit.module("Fields", (hooks) => {
`, - mockRPC, - }); - assert.ok(target.querySelector(".o_test_properties_not_empty")); + mockRPC, + }); + assert.ok(target.querySelector(".o_test_properties_not_empty")); - // delete a property, 2 properties left - await click(target, ".o_property_field:first-child .o_field_property_open_popover"); - await click(target, ".o_field_property_definition_delete"); - await click(target, ".modal-content .btn-primary"); - assert.ok(target.querySelector(".o_test_properties_not_empty")); + // delete a property, 2 properties left + await click(target, ".o_property_field:first-child .o_field_property_open_popover"); + await click(target, ".o_field_property_definition_delete"); + await click(target, ".modal-content .btn-primary"); + assert.ok(target.querySelector(".o_test_properties_not_empty")); - // delete a property, 1 property left - await click(target, ".o_property_field:first-child .o_field_property_open_popover"); - await click(target, ".o_field_property_definition_delete"); - await click(target, ".modal-content .btn-primary"); - assert.ok(target.querySelector(".o_test_properties_not_empty")); + // delete a property, 1 property left + await click(target, ".o_property_field:first-child .o_field_property_open_popover"); + await click(target, ".o_field_property_definition_delete"); + await click(target, ".modal-content .btn-primary"); + assert.ok(target.querySelector(".o_test_properties_not_empty")); - // delete a property, no property left - await click(target, ".o_property_field:first-child .o_field_property_open_popover"); - await click(target, ".o_field_property_definition_delete"); - await click(target, ".modal-content .btn-primary"); - assert.notOk(target.querySelector(".o_test_properties_not_empty")); - }); + // delete a property, no property left + await click(target, ".o_property_field:first-child .o_field_property_open_popover"); + await click(target, ".o_field_property_definition_delete"); + await click(target, ".modal-content .btn-primary"); + assert.notOk(target.querySelector(".o_test_properties_not_empty")); + } + ); /** * Check the behavior of the domain (properties with "definition_deleted" should be ignored). * In that case, all properties start with the flag "definition_deleted". */ - QUnit.test("properties: form view and falsy domain, properties are empty", async function (assert) { - async function mockRPC(route, { method, model, kwargs }) { - if (method === "check_access_rights") { - return true; + QUnit.test( + "properties: form view and falsy domain, properties are empty", + async function (assert) { + async function mockRPC(route, { method, model, kwargs }) { + if (method === "check_access_rights") { + return true; + } } - } - await makeView({ - type: "form", - resModel: "partner", - resId: 4, - serverData, - arch: ` + await makeView({ + type: "form", + resModel: "partner", + resId: 4, + serverData, + arch: `
@@ -1383,12 +1391,13 @@ QUnit.module("Fields", (hooks) => {
`, - mockRPC, - }); - assert.notOk(target.querySelector(".o_test_properties_not_empty")); + mockRPC, + }); + assert.notOk(target.querySelector(".o_test_properties_not_empty")); - // create the first property - await click(target, ".o_field_property_add button"); - assert.ok(target.querySelector(".o_test_properties_not_empty")); - }); + // create the first property + await click(target, ".o_field_property_add button"); + assert.ok(target.querySelector(".o_test_properties_not_empty")); + } + ); }); diff --git a/addons/web/static/tests/views/view_service_tests.js b/addons/web/static/tests/views/view_service_tests.js index ca3c91c9f91..eb1208a18a3 100644 --- a/addons/web/static/tests/views/view_service_tests.js +++ b/addons/web/static/tests/views/view_service_tests.js @@ -19,7 +19,8 @@ QUnit.module("View service", (hooks) => { records: [], }, "ir.ui.view": { - fields: {}, records: [] + fields: {}, + records: [], }, }; @@ -142,77 +143,4 @@ QUnit.module("View service", (hooks) => { await loadView(); assert.verifySteps([]); // cache was not invalidated }); - - QUnit.test("loadViews stores fields in cache", async (assert) => { - assert.expect(2); - - const mockRPC = (route, args) => { - if (route.includes("get_views")) { - assert.step("get_views"); - } - if (route.includes("fields_get")) { - assert.step("fields_get"); - } - }; - - await makeMockServer(serverData, mockRPC); - const env = await makeTestEnv(); - - await env.services.views.loadViews( - { - resModel: "take.five", - views: [[99, "list"]], - context: { default_field_value: 1 }, - }, - {} - ); - await env.services.views.loadFields("take.five"); - - assert.verifySteps(["get_views"]); - }); - - QUnit.test("store loadFields calls in cache in success", async (assert) => { - assert.expect(2); - - const mockRPC = (route, args) => { - if (route.includes("fields_get")) { - assert.step("fields_get"); - } - }; - - await makeMockServer(serverData, mockRPC); - const env = await makeTestEnv(); - - await env.services.views.loadFields("take.five"); - await env.services.views.loadFields("take.five"); - - assert.verifySteps(["fields_get"]); - }); - - QUnit.test("store loadFields calls in cache when failed", async (assert) => { - assert.expect(5); - - const mockRPC = (route, args) => { - if (route.includes("fields_get")) { - assert.step("fields_get"); - return Promise.reject("my little error"); - } - }; - - await makeMockServer(serverData, mockRPC); - const env = await makeTestEnv(); - - try { - await env.services.views.loadFields("take.five"); - } catch (error) { - assert.strictEqual(error, "my little error"); - } - try { - await env.services.views.loadFields("take.five"); - } catch (error) { - assert.strictEqual(error, "my little error"); - } - - assert.verifySteps(["fields_get", "fields_get"]); - }); }); diff --git a/addons/web/tooling/types/services.d.ts b/addons/web/tooling/types/services.d.ts index 5d20d7d32ee..ad6df81dbac 100644 --- a/addons/web/tooling/types/services.d.ts +++ b/addons/web/tooling/types/services.d.ts @@ -5,6 +5,7 @@ declare module "services" { import { cookieService } from "@web/core/browser/cookie_service"; import { dialogService } from "@web/core/dialog/dialog_service"; import { effectService } from "@web/core/effects/effect_service"; + import { fieldService } from "@web/core/field_service"; import { hotkeyService } from "@web/core/hotkeys/hotkey_service"; import { httpService } from "@web/core/network/http_service"; import { menuService } from "@web/webclient/menus/menu_service"; @@ -25,6 +26,7 @@ declare module "services" { cookie: ReturnType; dialog: ReturnType; effect: ReturnType; + field: ReturnType; hotkey: ReturnType; http: ReturnType; menu: Awaited>;