/** @odoo-module **/ import { dialogService } from "@web/core/dialog/dialog_service"; import { notificationService } from "@web/core/notifications/notification_service"; 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 { makeLegacyActionManagerService, makeLegacyNotificationService, mapLegacyEnvToWowlEnv, makeLegacySessionService, } from "@web/legacy/utils"; import { viewService } from "@web/views/view_service"; import { actionService } from "@web/webclient/actions/action_service"; import { effectService } from "@web/webclient/effects/effect_service"; import { hotkeyService } from "@web/core/hotkeys/hotkey_service"; import { menuService } from "@web/webclient/menu_service"; import { WebClient } from "@web/webclient/webclient"; // This import is needed because of it's sideeffects, for exemple : // web.test_utils easyload xml templates at line : 124:130. // Also it set the autocomplete delay time for the field Many2One at 0 for the tests at line : 132:137 import "web.test_legacy"; import AbstractService from "web.AbstractService"; import ActionMenus from "web.ActionMenus"; import basicFields from "web.basic_fields"; import Registry from "web.Registry"; import core from "web.core"; import makeTestEnvironment from "web.test_env"; import { registerCleanup } from "../helpers/cleanup"; import { makeTestEnv } from "../helpers/mock_env"; import { fakeTitleService, makeFakeLocalizationService, makeFakeRouterService, } from "../helpers/mock_services"; import { getFixture, legacyExtraNextTick, nextTick, patchWithCleanup } from "../helpers/utils"; import session from "web.session"; import { ComponentAdapter } from "web.OwlCompatibility"; import LegacyMockServer from "web.MockServer"; import Widget from "web.Widget"; import { userService } from "@web/core/user_service"; import { uiService } from "@web/core/ui/ui_service"; import { ClientActionAdapter, ViewAdapter } from "@web/legacy/action_adapters"; const { Component, mount, tags } = owl; const actionRegistry = registry.category("actions"); const serviceRegistry = registry.category("services"); /** * Builds the service registry for tests using a WebClient with a default version * of each required service. If the registry already contains one of those * services, the existing one is kept (it means it has been added in the test * directly, e.g. to have a custom version of the service). */ export function setupWebClientServiceRegistry() { const services = { action: () => actionService, dialog: () => dialogService, effect: () => effectService, hotkey: () => hotkeyService, legacy_service_provider: () => legacyServiceProvider, localization: () => makeFakeLocalizationService(), menu: () => menuService, notification: () => notificationService, orm: () => ormService, popover: () => popoverService, router: () => makeFakeRouterService(), title: () => fakeTitleService, ui: () => uiService, user: () => userService, view: () => viewService, }; for (let serviceName in services) { if (!serviceRegistry.contains(serviceName)) { serviceRegistry.add(serviceName, services[serviceName]()); } } } /** * Remove this as soon as we drop the legacy support */ function addLegacyMockEnvironment(env, legacyParams = {}) { // setup a legacy env const dataManager = Object.assign( { load_action: (actionID, context) => { return env.services.rpc("/web/action/load", { action_id: actionID, additional_context: context, }); }, load_views: async (params, options) => { const result = await env.services.rpc(`/web/dataset/call_kw/${params.model}`, { args: [], kwargs: { context: params.context, options: options, views: params.views_descr, }, method: "load_views", model: params.model, }); const views = result.fields_views; for (const [, viewType] of params.views_descr) { const fvg = views[viewType]; fvg.viewFields = fvg.fields; fvg.fields = result.fields; } if (params.favoriteFilters && "search" in views) { views.search.favoriteFilters = params.favoriteFilters; } return views; }, load_filters: (params) => { if (QUnit.config.debug) { console.log("[mock] load_filters", params); } return Promise.resolve([]); }, }, legacyParams.dataManager ); // clear the ActionMenus registry to prevent external code from doing unknown rpcs const actionMenusRegistry = ActionMenus.registry; ActionMenus.registry = new Registry(); registerCleanup(() => (ActionMenus.registry = actionMenusRegistry)); let localSession; if (legacyParams && legacyParams.getTZOffset) { patchWithCleanup(session, { getTZOffset: legacyParams.getTZOffset, }); localSession = { getTZOffset: legacyParams.getTZOffset }; } const baseEnv = { dataManager, 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)); // patch DebouncedField delay const debouncedField = basicFields.DebouncedField; const initialDebouncedVal = debouncedField.prototype.DEBOUNCE; debouncedField.prototype.DEBOUNCE = 0; registerCleanup(() => (debouncedField.prototype.DEBOUNCE = initialDebouncedVal)); if (legacyParams.withLegacyMockServer) { const adapter = new ComponentAdapter(null, { Component: owl.Component }); 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; env.services.rpc = async (...args) => { try { return await originalRPC(...args); } catch (e) { if (e.message.includes("Unimplemented")) { return legacyMockServer._performRpc(...args); } else { throw e; } } }; } } /** * This method create a web client instance properly configured. * * Note that the returned web client will be automatically cleaned up after the * end of the test. * * @param {*} params */ export async function createWebClient(params) { setupWebClientServiceRegistry(); // With the compatibility layer, the action manager keeps legacy alive if they // are still acessible from the breacrumbs. They are manually destroyed as soon // as they are no longer referenced in the stack. This works fine in production, // because the webclient is never destroyed. However, at the end of each test, // we destroy the webclient and expect every legacy that has been instantiated // to be destroyed. We thus need to manually destroy them here. const controllers = []; patchWithCleanup(ClientActionAdapter.prototype, { mounted() { this._super(); controllers.push(this.widget); }, }); patchWithCleanup(ViewAdapter.prototype, { mounted() { this._super(); controllers.push(this.widget); }, }); 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, }); addLegacyMockEnvironment(env, legacyParams); const WebClientClass = params.WebClientClass || WebClient; const target = params && params.target ? params.target : getFixture(); const wc = await mount(WebClientClass, { env, target }); registerCleanup(() => { for (const controller of controllers) { if (!controller.isDestroyed()) { controller.destroy(); } } wc.destroy(); }); // Wait for visual changes caused by a potential loadState await nextTick(); return wc; } export async function doAction(env, ...args) { if (env instanceof Component) { env = env.env; } try { await env.services.action.doAction(...args); } finally { await legacyExtraNextTick(); } } export async function loadState(env, state) { if (env instanceof Component) { env = env.env; } env.bus.trigger("test:hashchange", state); // wait the asynchronous hashchange // (the event hashchange must be triggered in a nonBlocking stack) await nextTick(); // wait for the regular rendering await nextTick(); // wait for the legacy rendering below owl layer await legacyExtraNextTick(); } export function getActionManagerServerData() { // additional basic client action class TestClientAction extends Component {} TestClientAction.template = tags.xml`