[REF] web,*: regroup custom hooks in core/utils/hooks

*base_automation,iap,im_livechat,point_of_sale

This commit introduces a new file in core/utils: hooks.js
This file contains custom hooks (useEffect, useService, useBus...).

The useHotkey hook is tightly related to hotkeys, so it has been
moved to core/hotkeys.

closes odoo/odoo#74535

Related: odoo/enterprise#20003
Signed-off-by: Lucas Perais (lpe) <lpe@odoo.com>
This commit is contained in:
Aaron Bohy
2021-08-02 11:13:52 +00:00
parent 5ffdd92034
commit ff6e89052d
49 changed files with 520 additions and 536 deletions
@@ -3,7 +3,7 @@
import { ErrorDialog } from "@web/core/errors/error_dialogs";
import session from "web.session";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/service_hook";
import { useService } from "@web/core/utils/hooks";
export class BaseAutomationErrorDialog extends ErrorDialog {
setup() {
@@ -1,7 +1,7 @@
/** @odoo-module */
import { Dialog } from "@web/core/dialog/dialog";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/service_hook";
import { useService } from "@web/core/utils/hooks";
class InsufficientCreditDialog extends Dialog {
setup() {
+1 -3
View File
@@ -99,9 +99,8 @@ Help your customers with this chat, and analyse their feedback.
'web/static/src/core/browser/browser.js',
'web/static/src/core/browser/feature_detection.js',
'web/static/src/core/dialog/dialog.js',
'web/static/src/core/effect_hook.js',
'web/static/src/core/errors/error_dialogs.js',
'web/static/src/core/hotkey_hook.js',
'web/static/src/core/utils/hooks.js',
'web/static/src/core/hotkeys/hotkey_service.js',
'web/static/src/core/l10n/dates.js',
'web/static/src/core/l10n/localization.js',
@@ -113,7 +112,6 @@ Help your customers with this chat, and analyse their feedback.
'web/static/src/core/notifications/notification_container.js',
'web/static/src/core/notifications/notification_service.js',
'web/static/src/core/registry.js',
'web/static/src/core/service_hook.js',
'web/static/src/core/ui/block_ui.js',
'web/static/src/core/ui/ui_service.js',
'web/static/src/core/user_service.js',
@@ -1,11 +1,11 @@
/** @odoo-module */
import { useService } from "@web/core/service_hook";
import { useService } from "@web/core/utils/hooks";
import Chrome from "point_of_sale.Chrome";
import Registries from "point_of_sale.Registries";
import { configureGui } from "point_of_sale.Gui";
import { useBus } from "@web/core/bus_hook";
import { useBus } from "@web/core/utils/hooks";
import PosComponent from "point_of_sale.PosComponent";
import PopupControllerMixin from "point_of_sale.PopupControllerMixin";
import { registry } from "@web/core/registry";
@@ -1,45 +0,0 @@
/** @odoo-module **/
import { useEffect } from "./effect_hook";
const { useComponent } = owl.hooks;
// -----------------------------------------------------------------------------
// Hook functions
// -----------------------------------------------------------------------------
/**
* Focus a given selector as soon as it appears in the DOM and if it was not
* displayed before. If the selected target is an input|textarea, set the selection
* at the end.
*
* @param {Object} [params]
* @param {string} [params.selector='autofocus'] default: select the first element
* with an `autofocus` attribute.
* @returns {Function} function that forces the focus on the next update if visible.
*/
export function useAutofocus(params = {}) {
const comp = useComponent();
// Prevent autofocus in mobile
if (comp.env.isSmall) {
return () => {};
}
const selector = params.selector || "[autofocus]";
let forceFocusCount = 0;
useEffect(
function autofocus(target) {
if (target) {
target.focus();
if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
const inputEl = target;
inputEl.selectionStart = inputEl.selectionEnd = inputEl.value.length;
}
}
},
() => [comp.el.querySelector(selector), forceFocusCount]
);
return function focusOnUpdate() {
forceFocusCount++; // force the effect to rerun on next patch
};
}
-40
View File
@@ -1,40 +0,0 @@
/** @odoo-module **/
import { useEffect } from "./effect_hook";
const { useComponent } = owl.hooks;
/**
* This file contains various custom hooks.
* Their inner working is rather simple:
* Each custom hook simply hook itself to any number of owl lifecycle hooks.
* You can then use them just like an owl hook in any Component
* e.g.:
* import { useBus } from "@web/core/bus_hook";
* ...
* setup() {
* ...
* useBus(someBus, someEvent, callback)
* ...
* }
*/
// -----------------------------------------------------------------------------
// Hook functions
// -----------------------------------------------------------------------------
/**
* Ensures a bus event listener is attached and cleared the proper way.
*
* @param {EventBus} bus
* @param {string} eventName
* @param {Callback} callback
*/
export function useBus(bus, eventName, callback) {
const component = useComponent();
useEffect(
() => {
bus.on(eventName, component, callback);
return () => bus.off(eventName, component);
},
() => []
);
}
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { registry } from "../registry";
import { memoize } from "../utils/functions";
import { useEffect } from "../effect_hook";
import { useEffect } from "@web/core/utils/hooks";
const { useEnv, useSubEnv } = owl.hooks;
const debugRegistry = registry.category("debug");
@@ -1,5 +1,7 @@
/** @odoo-module **/
import { useCurrentDebugContext } from "./debug_context";
const { Component } = owl;
export class DebugMenu extends Component {
@@ -1,8 +1,7 @@
/** @odoo-module **/
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { useBus } from "@web/core/bus_hook";
import { useService } from "@web/core/service_hook";
import { useBus, useService } from "@web/core/utils/hooks";
const { Component } = owl;
+1 -2
View File
@@ -1,7 +1,6 @@
/** @odoo-module **/
import { useHotkey } from "../hotkey_hook";
import { useService } from "../service_hook";
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
import { useActiveElement } from "../ui/ui_service";
const { Component, hooks } = owl;
@@ -1,8 +1,6 @@
/** @odoo-module **/
import { useBus } from "../bus_hook";
import { useService } from "../service_hook";
import { useEffect } from "../effect_hook";
import { useBus, useEffect, useService } from "@web/core/utils/hooks";
import { scrollTo } from "../utils/scrolling";
import { ParentClosingMode } from "./dropdown_item";
-47
View File
@@ -1,47 +0,0 @@
/** @odoo-module */
const { onMounted, onWillPatch, onPatched, onWillUnmount } = owl.hooks;
const NO_OP = () => {};
/**
* @callback Effect
* @param {...any} dependencies the dependencies computed by computeDependencies
* @returns {void|(()=>void)} a cleanup function that reverses the side
* effects of the effect callback.
*/
/**
* This hook will run a callback when a component is mounted and patched, and
* will run a cleanup function before patching and before unmounting the
* the component.
*
* @param {Effect} effect the effect to run on component mount and/or patch
* @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
* dependencies that will decide if the effect needs to be cleaned up and
* run again. If the dependencies did not change, the effect will not run
* again. The default value returns an array containing only NaN because
* NaN !== NaN, which will cause the effect to rerun on every patch.
*/
export function useEffect(effect, computeDependencies = () => [NaN]) {
let cleanup, dependencies;
onMounted(() => {
dependencies = computeDependencies();
cleanup = effect(...dependencies) || NO_OP;
});
let shouldReapplyOnPatch = false;
onWillPatch(() => {
const newDeps = computeDependencies();
shouldReapplyOnPatch = newDeps.some((val, i) => val !== dependencies[i]);
if (shouldReapplyOnPatch) {
cleanup();
dependencies = newDeps;
}
});
onPatched(() => {
if (shouldReapplyOnPatch) {
cleanup = effect(...dependencies) || NO_OP;
}
});
onWillUnmount(() => cleanup());
}
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { browser } from "../browser/browser";
import { useEffect } from "../effect_hook";
import { browser } from "@web/core/browser/browser";
import { useEffect } from "@web/core/utils/hooks";
const { Component, hooks } = owl;
@@ -4,7 +4,7 @@ import { browser } from "../browser/browser";
import { Dialog } from "../dialog/dialog";
import { _lt } from "../l10n/translation";
import { registry } from "../registry";
import { useService } from "../service_hook";
import { useService } from "@web/core/utils/hooks";
import { capitalize } from "../utils/strings";
const { hooks } = owl;
@@ -20,7 +20,7 @@ export const odooExceptionTitleMap = new Map(
"odoo.exceptions.UserError": _lt("User Error"),
"odoo.exceptions.ValidationError": _lt("Validation Error"),
"odoo.exceptions.AccessError": _lt("Access Error"),
"odoo.exceptions.Warning": _lt("Warning")
"odoo.exceptions.Warning": _lt("Warning"),
})
);
@@ -31,7 +31,7 @@ export class ErrorDialog extends Dialog {
setup() {
super.setup();
this.state = useState({
showTraceback: false
showTraceback: false,
});
}
onClickClipboard() {
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { useService } from "../service_hook";
import { useService } from "@web/core/utils/hooks";
const { Component, hooks, QWeb } = owl;
const { useRef } = hooks;
@@ -1,7 +1,6 @@
/** @odoo-module */
/** @odoo-module **/
import { useService } from "./service_hook";
import { useEffect } from "./effect_hook";
import { useEffect, useService } from "@web/core/utils/hooks";
/**
* This hook will register/unregister the given registration
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { useService } from "../service_hook";
import { useService } from "@web/core/utils/hooks";
const { onWillUnmount, useComponent } = owl.hooks;
@@ -1,43 +0,0 @@
/** @odoo-module **/
import { SERVICES_METADATA } from "../env";
const { useComponent } = owl.hooks;
/**
* Import a service into a component
*
* @param {string} serviceName
* @returns {any}
*/
export function useService(serviceName) {
const component = useComponent();
const { services } = component.env;
if (!(serviceName in services)) {
throw new Error(`Service ${serviceName} is not available`);
}
const service = services[serviceName];
if (serviceName in SERVICES_METADATA) {
if (service instanceof Function) {
return protectMethod(component, null, service);
} else {
const methods = SERVICES_METADATA[serviceName];
const result = Object.create(service);
for (let method of methods) {
result[method] = protectMethod(component, service, service[method]);
}
return result;
}
}
return service;
}
function protectMethod(component, caller, fn) {
return async (...args) => {
if (component.__owl__.status === 5 /* DESTROYED */) {
throw new Error("Component is destroyed");
}
const result = await fn.call(caller, ...args);
return component.__owl__.status === 5 ? new Promise(() => {}) : result;
};
}
+1 -2
View File
@@ -1,9 +1,8 @@
/** @odoo-module **/
import { useService } from "@web/core/service_hook";
import { useEffect, useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { debounce } from "@web/core/utils/timing";
import { useEffect } from "@web/core/effect_hook";
import { BlockUI } from "./block_ui";
const { Component, core, hooks } = owl;
+173
View File
@@ -0,0 +1,173 @@
/** @odoo-module **/
import { SERVICES_METADATA } from "@web/env";
const { onMounted, onWillPatch, onPatched, onWillUnmount, useComponent } = owl.hooks;
/**
* This file contains various custom hooks.
* Their inner working is rather simple:
* Each custom hook simply hooks itself to any number of owl lifecycle hooks.
* You can then use them just like an owl hook in any Component
* e.g.:
* import { useBus } from "@web/core/utils/hooks";
* ...
* setup() {
* ...
* useBus(someBus, someEvent, callback)
* ...
* }
*/
// -----------------------------------------------------------------------------
// useAutofocus
// -----------------------------------------------------------------------------
/**
* Focus a given selector as soon as it appears in the DOM and if it was not
* displayed before. If the selected target is an input|textarea, set the selection
* at the end.
*
* @param {Object} [params]
* @param {string} [params.selector='autofocus'] default: select the first element
* with an `autofocus` attribute.
* @returns {Function} function that forces the focus on the next update if visible.
*/
export function useAutofocus(params = {}) {
const comp = useComponent();
// Prevent autofocus in mobile
if (comp.env.isSmall) {
return () => {};
}
const selector = params.selector || "[autofocus]";
let forceFocusCount = 0;
useEffect(
function autofocus(target) {
if (target) {
target.focus();
if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
const inputEl = target;
inputEl.selectionStart = inputEl.selectionEnd = inputEl.value.length;
}
}
},
() => [comp.el.querySelector(selector), forceFocusCount]
);
return function focusOnUpdate() {
forceFocusCount++; // force the effect to rerun on next patch
};
}
// -----------------------------------------------------------------------------
// useBus
// -----------------------------------------------------------------------------
/**
* Ensures a bus event listener is attached and cleared the proper way.
*
* @param {EventBus} bus
* @param {string} eventName
* @param {Callback} callback
*/
export function useBus(bus, eventName, callback) {
const component = useComponent();
useEffect(
() => {
bus.on(eventName, component, callback);
return () => bus.off(eventName, component);
},
() => []
);
}
// -----------------------------------------------------------------------------
// useEffect
// -----------------------------------------------------------------------------
const NO_OP = () => {};
/**
* @callback Effect
* @param {...any} dependencies the dependencies computed by computeDependencies
* @returns {void|(()=>void)} a cleanup function that reverses the side
* effects of the effect callback.
*/
/**
* This hook will run a callback when a component is mounted and patched, and
* will run a cleanup function before patching and before unmounting the
* the component.
*
* @param {Effect} effect the effect to run on component mount and/or patch
* @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
* dependencies that will decide if the effect needs to be cleaned up and
* run again. If the dependencies did not change, the effect will not run
* again. The default value returns an array containing only NaN because
* NaN !== NaN, which will cause the effect to rerun on every patch.
*/
export function useEffect(effect, computeDependencies = () => [NaN]) {
let cleanup, dependencies;
onMounted(() => {
dependencies = computeDependencies();
cleanup = effect(...dependencies) || NO_OP;
});
let shouldReapplyOnPatch = false;
onWillPatch(() => {
const newDeps = computeDependencies();
shouldReapplyOnPatch = newDeps.some((val, i) => val !== dependencies[i]);
if (shouldReapplyOnPatch) {
cleanup();
dependencies = newDeps;
}
});
onPatched(() => {
if (shouldReapplyOnPatch) {
cleanup = effect(...dependencies) || NO_OP;
}
});
onWillUnmount(() => cleanup());
}
// -----------------------------------------------------------------------------
// useService
// -----------------------------------------------------------------------------
function _protectMethod(component, caller, fn) {
return async (...args) => {
if (component.__owl__.status === 5 /* DESTROYED */) {
throw new Error("Component is destroyed");
}
const result = await fn.call(caller, ...args);
return component.__owl__.status === 5 ? new Promise(() => {}) : result;
};
}
/**
* Import a service into a component
*
* @param {string} serviceName
* @returns {any}
*/
export function useService(serviceName) {
const component = useComponent();
const { services } = component.env;
if (!(serviceName in services)) {
throw new Error(`Service ${serviceName} is not available`);
}
const service = services[serviceName];
if (serviceName in SERVICES_METADATA) {
if (service instanceof Function) {
return _protectMethod(component, null, service);
} else {
const methods = SERVICES_METADATA[serviceName];
const result = Object.create(service);
for (let method of methods) {
result[method] = _protectMethod(component, service, service[method]);
}
return result;
}
}
return service;
}
@@ -6,8 +6,7 @@ import { ComponentAdapter } from "web.OwlCompatibility";
import { objectToQuery } from "../core/browser/router_service";
import { useDebugCategory } from "../core/debug/debug_context";
import { Dialog } from "../core/dialog/dialog";
import { useEffect } from "../core/effect_hook";
import { useService } from "../core/service_hook";
import { useEffect, useService } from "@web/core/utils/hooks";
import { ViewNotFoundError } from "../webclient/actions/action_service";
import { cleanDomFromBootstrap, wrapSuccessOrFail, mapDoActionOptionAPI } from "./utils";
@@ -2,7 +2,7 @@ odoo.define('web.custom_hooks', function (require) {
"use strict";
const { Component } = owl;
const { useEffect } = require("@web/core/effect_hook");
const { useEffect } = require("@web/core/utils/hooks");
/**
* Focus a given selector as soon as it appears in the DOM and if it was not
@@ -3,7 +3,7 @@ odoo.define('web.AbstractFieldOwl', function (require) {
const field_utils = require('web.field_utils');
const { useListener } = require('web.custom_hooks');
const { useEffect } = require('@web/core/effect_hook');
const { useEffect } = require("@web/core/utils/hooks");
/**
* This file defines the Owl version of the AbstractField. Specific fields
@@ -1,6 +1,6 @@
/** @odoo-module alias=web.PivotRenderer **/
import { useEffect } from "@web/core/effect_hook";
import { useEffect } from "@web/core/utils/hooks";
import DropdownMenu from 'web.DropdownMenu';
import DropdownMenuItem from 'web.DropdownMenuItem';
import OwlAbstractRenderer from '../abstract_renderer_owl';
@@ -2,11 +2,9 @@
import { Dialog } from "../core/dialog/dialog";
import { patch } from "@web/core/utils/patch";
import { useEffect } from "@web/core/effect_hook";
import { useEffect } from "@web/core/utils/hooks";
import OwlDialog from "web.OwlDialog";
const { hooks } = owl;
/**
* This is a patch of the new Dialog class.
* Its purpose is to inform the old "active/inactive" mechanism.
+1 -1
View File
@@ -1,7 +1,7 @@
/** @odoo-module **/
const { Component, hooks, tags } = owl;
import { useService } from "../core/service_hook";
import { useService } from "@web/core/utils/hooks";
import { useSetupAction } from "../webclient/actions/action_hook";
import legacyViewRegistry from "web.view_registry";
import { ViewAdapter } from "./action_adapters";
@@ -1,9 +1,9 @@
/** @odoo-module **/
import { Dialog } from "../../core/dialog/dialog";
import { DebugMenu } from "../../core/debug/debug_menu";
import { useOwnDebugContext } from "../../core/debug/debug_context";
import { useEffect } from "../../core/effect_hook";
import { Dialog } from "@web/core/dialog/dialog";
import { DebugMenu } from "@web/core/debug/debug_menu";
import { useOwnDebugContext } from "@web/core/debug/debug_context";
import { useEffect } from "@web/core/utils/hooks";
const { hooks } = owl;
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { getScrollPosition, setScrollPosition } from "../../core/utils/scrolling";
import { useEffect } from "../../core/effect_hook";
import { getScrollPosition, setScrollPosition } from "@web/core/utils/scrolling";
import { useEffect } from "@web/core/utils/hooks";
const { useComponent } = owl.hooks;
@@ -1,14 +1,14 @@
/** @odoo-module **/
import { cleanDomFromBootstrap } from "@web/legacy/utils";
import { browser } from "../../core/browser/browser";
import { useBus } from "../../core/bus_hook";
import { makeContext } from "../../core/context";
import { download } from "../../core/network/download";
import { evaluateExpr } from "../../core/py_js/py";
import { registry } from "../../core/registry";
import { KeepLast } from "../../core/utils/concurrency";
import { sprintf } from "../../core/utils/strings";
import { browser } from "@web/core/browser/browser";
import { useBus } from "@web/core/utils/hooks";
import { makeContext } from "@web/core/context";
import { download } from "@web/core/network/download";
import { evaluateExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
import { KeepLast } from "@web/core/utils/concurrency";
import { sprintf } from "@web/core/utils/strings";
import { ActionDialog } from "./action_dialog";
/** @typedef {number|false} ActionId */
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { useService } from "@web/core/service_hook";
import { useService } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
const { utils, Component } = owl;
@@ -1,7 +1,6 @@
/** @odoo-module **/
import { useEffect } from "@web/core/effect_hook";
import { useService } from "@web/core/service_hook";
import { useEffect, useService } from "@web/core/utils/hooks";
/**
* @typedef {import("./command_service").CommandServiceAddOptions} CommandServiceAddOptions
@@ -1,9 +1,8 @@
/** @odoo-module **/
import { useAutofocus } from "@web/core/autofocus_hook";
import { useHotkey } from "@web/core/hotkey_hook";
import { useAutofocus, useService } from "@web/core/utils/hooks";
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/service_hook";
import { KeepLast } from "@web/core/utils/concurrency";
import { scrollTo } from "@web/core/utils/scrolling";
import { fuzzyLookup } from "@web/core/utils/search";
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { isMacOS } from "@web/core/browser/feature_detection";
import { useHotkey } from "@web/core/hotkey_hook";
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
import { registry } from "@web/core/registry";
import { capitalize } from "@web/core/utils/strings";
import { getVisibleElements } from "@web/core/utils/ui";
@@ -1,8 +1,8 @@
/** @odoo-module **/
import { browser } from "../../core/browser/browser";
import { registry } from "../../core/registry";
import { useService } from "../../core/service_hook";
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
const { Component, useState } = owl;
@@ -1,12 +1,11 @@
/** @odoo-module **/
import { Dropdown } from "../../core/dropdown/dropdown";
import { DropdownItem } from "../../core/dropdown/dropdown_item";
import { useService } from "../../core/service_hook";
import { useEffect } from "../../core/effect_hook";
import { registry } from "../../core/registry";
import { debounce } from "../../core/utils/timing";
import { ErrorHandler, NotUpdatable } from "../../core/utils/components";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { useEffect, useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { debounce } from "@web/core/utils/timing";
import { ErrorHandler, NotUpdatable } from "@web/core/utils/components";
const { Component, hooks } = owl;
const { useExternalListener, useRef } = hooks;
@@ -1,9 +1,9 @@
/** @odoo-module **/
import { useService } from "../../core/service_hook";
import { registry } from "../../core/registry";
import { browser } from "../../core/browser/browser";
import { symmetricalDifference } from "../../core/utils/arrays";
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { browser } from "@web/core/browser/browser";
import { symmetricalDifference } from "@web/core/utils/arrays";
const { Component, hooks } = owl;
const { useState } = hooks;
@@ -1,10 +1,9 @@
/** @odoo-module **/
import { browser } from "../../core/browser/browser";
import { DropdownItem } from "../../core/dropdown/dropdown_item";
import { useService } from "../../core/service_hook";
import { registry } from "../../core/registry";
import { useEffect } from "../../core/effect_hook";
import { browser } from "@web/core/browser/browser";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { registry } from "@web/core/registry";
import { useEffect, useService } from "@web/core/utils/hooks";
const { Component } = owl;
+1 -3
View File
@@ -1,10 +1,8 @@
/** @odoo-module **/
import { useService } from "../core/service_hook";
import { useBus } from "../core/bus_hook";
import { ActionContainer } from "./actions/action_container";
import { NavBar } from "./navbar/navbar";
import { useEffect } from "@web/core/effect_hook";
import { useBus, useEffect, useService } from "@web/core/utils/hooks";
import { NotUpdatable } from "../core/utils/components";
import { MainComponentsContainer } from "../core/main_components_container";
import { useOwnDebugContext } from "../core/debug/debug_context";
@@ -1,35 +0,0 @@
/** @odoo-module **/
import { useBus } from "@web/core/bus_hook";
import { makeTestEnv } from "../helpers/mock_env";
import { getFixture, nextTick } from "../helpers/utils";
const { Component, mount, tags } = owl;
QUnit.module("useBus");
QUnit.test("useBus hook: simple usecase", async function (assert) {
class MyComponent extends Component {
setup() {
useBus(this.env.bus, "test-event", this.myCallback);
}
myCallback() {
assert.step("callback");
}
}
MyComponent.template = tags.xml`<div/>`;
const env = await makeTestEnv();
const target = getFixture();
const comp = await mount(MyComponent, { env, target });
env.bus.trigger("test-event");
await nextTick();
assert.verifySteps(["callback"]);
comp.unmount();
env.bus.trigger("test-event");
await nextTick();
assert.verifySteps([]);
comp.destroy();
});
+1 -1
View File
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { useService } from "@web/core/service_hook";
import { useService } from "@web/core/utils/hooks";
import { uiService } from "@web/core/ui/ui_service";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { Dialog } from "@web/core/dialog/dialog";
@@ -1,183 +0,0 @@
/** @odoo-module **/
import { useEffect } from "@web/core/effect_hook";
import { makeTestEnv } from "../helpers/mock_env";
import { getFixture } from "../helpers/utils";
const { Component, tags, hooks, mount } = owl;
const { useState } = hooks;
QUnit.module("useEffect");
QUnit.test("useEffect: effect runs on mount, is reapplied on patch, and is cleaned up on unmount and before reapplying", async function (assert) {
assert.expect(7);
let cleanupRun = 0;
class MyComponent extends Component {
setup() {
this.state = useState({
value: 0,
});
useEffect(
() => {
assert.step(`value is ${this.state.value}`);
return () => assert.step(`cleaning up for value = ${this.state.value} (cleanup ${cleanupRun++})`);
}
);
}
}
MyComponent.template = tags.xml`<div/>`;
const env = await makeTestEnv();
const target = getFixture();
const component = await mount(MyComponent, { env, target });
assert.step("before state mutation");
component.state.value++;
// Wait for an owl render
await new Promise(resolve => requestAnimationFrame(resolve));
assert.step("after state mutation");
await component.unmount();
assert.verifySteps([
"value is 0",
"before state mutation",
// While one might expect value to be 0 at cleanup, because the value is
// read during cleanup from the state rather than captured by a dependency
// it already has the new value. Having this in business code is a symptom
// of a missing dependency and can lead to bugs.
"cleaning up for value = 1 (cleanup 0)",
"value is 1",
"after state mutation",
"cleaning up for value = 1 (cleanup 1)",
]);
});
QUnit.test("useEffect: dependencies prevent effects from rerunning when unchanged", async function (assert) {
assert.expect(21);
class MyComponent extends Component {
setup() {
this.state = useState({
a: 0,
b: 0,
});
useEffect(
(a) => {
assert.step(`Effect a: ${a}`);
return () => assert.step(`cleaning up for a: ${a}`);
},
() => [this.state.a]
);
useEffect(
(b) => {
assert.step(`Effect b: ${b}`);
return () => assert.step(`cleaning up for b: ${b}`);
},
() => [this.state.b]
);
useEffect(
(a, b) => {
assert.step(`Effect ab: {a: ${a}, b: ${b}}`);
return () => assert.step(`cleaning up for ab: {a: ${a}, b: ${b}}`);
},
() => [this.state.a, this.state.b]
);
}
}
MyComponent.template = tags.xml`<div/>`;
const env = await makeTestEnv();
const target = getFixture();
assert.step("before mount");
const component = await mount(MyComponent, { env, target });
assert.step("after mount");
assert.step("before state mutation: a");
component.state.a++;
// Wait for an owl render
await new Promise(resolve => requestAnimationFrame(resolve));
assert.step("after state mutation: a");
assert.step("before state mutation: b");
component.state.b++;
// Wait for an owl render
await new Promise(resolve => requestAnimationFrame(resolve));
assert.step("after state mutation: b");
await component.unmount();
assert.verifySteps([
// All effects run on mount
"before mount",
"Effect a: 0",
"Effect b: 0",
"Effect ab: {a: 0, b: 0}",
"after mount",
"before state mutation: a",
// Cleanups run in reverse order
"cleaning up for ab: {a: 0, b: 0}",
// Cleanup for b is not run
"cleaning up for a: 0",
"Effect a: 1",
// Effect b is not run
"Effect ab: {a: 1, b: 0}",
"after state mutation: a",
"before state mutation: b",
"cleaning up for ab: {a: 1, b: 0}",
"cleaning up for b: 0",
// Cleanup for a is not run
// Effect a is not run
"Effect b: 1",
"Effect ab: {a: 1, b: 1}",
"after state mutation: b",
// All cleanups run on unmount
"cleaning up for ab: {a: 1, b: 1}",
"cleaning up for b: 1",
"cleaning up for a: 1",
]);
});
QUnit.test("useEffect: effect with empty dependency list never reruns", async function (assert) {
assert.expect(6);
class MyComponent extends Component {
setup() {
this.state = useState({
value: 0,
});
useEffect(
() => {
assert.step(`value is ${this.state.value}`);
return () => assert.step(`cleaning up for ${this.state.value}`);
},
() => []
);
}
}
MyComponent.template = tags.xml`<div t-esc="state.value"/>`;
const env = await makeTestEnv();
const target = getFixture();
const component = await mount(MyComponent, { env, target });
assert.step("before state mutation");
component.state.value++;
// Wait for an owl render
await new Promise(resolve => requestAnimationFrame(resolve));
assert.equal(component.el.textContent, 1, "Value was correctly changed inside the component");
assert.step("after state mutation");
await component.unmount();
assert.verifySteps([
"value is 0",
"before state mutation",
// no cleanup or effect caused by mutation
"after state mutation",
// Value being clean
"cleaning up for 1",
]);
});
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { useHotkey } from "@web/core/hotkey_hook";
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
import { registry } from "@web/core/registry";
import { uiService, useActiveElement } from "@web/core/ui/ui_service";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
@@ -4,7 +4,7 @@ import { browser } from "@web/core/browser/browser";
import { ConnectionAbortedError, rpcService } from "@web/core/network/rpc_service";
import { notificationService } from "@web/core/notifications/notification_service";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/service_hook";
import { useService } from "@web/core/utils/hooks";
import { patch, unpatch } from "@web/core/utils/patch";
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
import { makeMockXHR } from "../../helpers/mock_services";
@@ -2,7 +2,7 @@
import { ormService } from "@web/core/orm_service";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/service_hook";
import { useService } from "@web/core/utils/hooks";
import { makeTestEnv } from "../helpers/mock_env";
import { getFixture } from "../helpers/utils";
@@ -1,51 +0,0 @@
/** @odoo-module **/
import { useService } from "@web/core/service_hook";
import { registry } from "@web/core/registry";
import { makeTestEnv } from "../helpers/mock_env";
import { getFixture } from "../helpers/utils";
const { Component, mount, tags } = owl;
const serviceRegistry = registry.category("services");
QUnit.module("useService");
QUnit.test("useService: unavailable service", async function (assert) {
class MyComponent extends Component {
setup() {
useService("toy_service");
}
}
MyComponent.template = tags.xml`<div/>`;
const env = await makeTestEnv();
const target = getFixture();
try {
await mount(MyComponent, { env, target });
} catch (e) {
assert.strictEqual(e.message, "Service toy_service is not available");
}
});
QUnit.test("useService: service that returns null", async function (assert) {
class MyComponent extends Component {
setup() {
this.toyService = useService("toy_service");
}
}
MyComponent.template = tags.xml`<div/>`;
serviceRegistry.add("toy_service", {
name: "toy_service",
start: () => {
return null;
},
});
const env = await makeTestEnv();
const target = getFixture();
const comp = await mount(MyComponent, { env, target });
assert.strictEqual(comp.toyService, null);
comp.unmount();
});
@@ -0,0 +1,275 @@
/** @odoo-module **/
import { useBus, useEffect, useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { makeTestEnv } from "../../helpers/mock_env";
import { getFixture, nextTick } from "../../helpers/utils";
const { Component, mount, tags, useState } = owl;
const serviceRegistry = registry.category("services");
QUnit.module("utils", () => {
QUnit.module("Hooks", () => {
QUnit.module("useBus");
QUnit.test("useBus hook: simple usecase", async function (assert) {
class MyComponent extends Component {
setup() {
useBus(this.env.bus, "test-event", this.myCallback);
}
myCallback() {
assert.step("callback");
}
}
MyComponent.template = tags.xml`<div/>`;
const env = await makeTestEnv();
const target = getFixture();
const comp = await mount(MyComponent, { env, target });
env.bus.trigger("test-event");
await nextTick();
assert.verifySteps(["callback"]);
comp.unmount();
env.bus.trigger("test-event");
await nextTick();
assert.verifySteps([]);
comp.destroy();
});
QUnit.module("useEffect");
QUnit.test(
"useEffect: effect runs on mount, is reapplied on patch, and is cleaned up on unmount and before reapplying",
async function (assert) {
assert.expect(7);
let cleanupRun = 0;
class MyComponent extends Component {
setup() {
this.state = useState({
value: 0,
});
useEffect(() => {
assert.step(`value is ${this.state.value}`);
return () =>
assert.step(
`cleaning up for value = ${
this.state.value
} (cleanup ${cleanupRun++})`
);
});
}
}
MyComponent.template = tags.xml`<div/>`;
const env = await makeTestEnv();
const target = getFixture();
const component = await mount(MyComponent, { env, target });
assert.step("before state mutation");
component.state.value++;
// Wait for an owl render
await new Promise((resolve) => requestAnimationFrame(resolve));
assert.step("after state mutation");
await component.unmount();
assert.verifySteps([
"value is 0",
"before state mutation",
// While one might expect value to be 0 at cleanup, because the value is
// read during cleanup from the state rather than captured by a dependency
// it already has the new value. Having this in business code is a symptom
// of a missing dependency and can lead to bugs.
"cleaning up for value = 1 (cleanup 0)",
"value is 1",
"after state mutation",
"cleaning up for value = 1 (cleanup 1)",
]);
}
);
QUnit.test(
"useEffect: dependencies prevent effects from rerunning when unchanged",
async function (assert) {
assert.expect(21);
class MyComponent extends Component {
setup() {
this.state = useState({
a: 0,
b: 0,
});
useEffect(
(a) => {
assert.step(`Effect a: ${a}`);
return () => assert.step(`cleaning up for a: ${a}`);
},
() => [this.state.a]
);
useEffect(
(b) => {
assert.step(`Effect b: ${b}`);
return () => assert.step(`cleaning up for b: ${b}`);
},
() => [this.state.b]
);
useEffect(
(a, b) => {
assert.step(`Effect ab: {a: ${a}, b: ${b}}`);
return () => assert.step(`cleaning up for ab: {a: ${a}, b: ${b}}`);
},
() => [this.state.a, this.state.b]
);
}
}
MyComponent.template = tags.xml`<div/>`;
const env = await makeTestEnv();
const target = getFixture();
assert.step("before mount");
const component = await mount(MyComponent, { env, target });
assert.step("after mount");
assert.step("before state mutation: a");
component.state.a++;
// Wait for an owl render
await new Promise((resolve) => requestAnimationFrame(resolve));
assert.step("after state mutation: a");
assert.step("before state mutation: b");
component.state.b++;
// Wait for an owl render
await new Promise((resolve) => requestAnimationFrame(resolve));
assert.step("after state mutation: b");
await component.unmount();
assert.verifySteps([
// All effects run on mount
"before mount",
"Effect a: 0",
"Effect b: 0",
"Effect ab: {a: 0, b: 0}",
"after mount",
"before state mutation: a",
// Cleanups run in reverse order
"cleaning up for ab: {a: 0, b: 0}",
// Cleanup for b is not run
"cleaning up for a: 0",
"Effect a: 1",
// Effect b is not run
"Effect ab: {a: 1, b: 0}",
"after state mutation: a",
"before state mutation: b",
"cleaning up for ab: {a: 1, b: 0}",
"cleaning up for b: 0",
// Cleanup for a is not run
// Effect a is not run
"Effect b: 1",
"Effect ab: {a: 1, b: 1}",
"after state mutation: b",
// All cleanups run on unmount
"cleaning up for ab: {a: 1, b: 1}",
"cleaning up for b: 1",
"cleaning up for a: 1",
]);
}
);
QUnit.test(
"useEffect: effect with empty dependency list never reruns",
async function (assert) {
assert.expect(6);
class MyComponent extends Component {
setup() {
this.state = useState({
value: 0,
});
useEffect(
() => {
assert.step(`value is ${this.state.value}`);
return () => assert.step(`cleaning up for ${this.state.value}`);
},
() => []
);
}
}
MyComponent.template = tags.xml`<div t-esc="state.value"/>`;
const env = await makeTestEnv();
const target = getFixture();
const component = await mount(MyComponent, { env, target });
assert.step("before state mutation");
component.state.value++;
// Wait for an owl render
await new Promise((resolve) => requestAnimationFrame(resolve));
assert.equal(
component.el.textContent,
1,
"Value was correctly changed inside the component"
);
assert.step("after state mutation");
await component.unmount();
assert.verifySteps([
"value is 0",
"before state mutation",
// no cleanup or effect caused by mutation
"after state mutation",
// Value being clean
"cleaning up for 1",
]);
}
);
QUnit.module("useService");
QUnit.test("useService: unavailable service", async function (assert) {
class MyComponent extends Component {
setup() {
useService("toy_service");
}
}
MyComponent.template = tags.xml`<div/>`;
const env = await makeTestEnv();
const target = getFixture();
try {
await mount(MyComponent, { env, target });
} catch (e) {
assert.strictEqual(e.message, "Service toy_service is not available");
}
});
QUnit.test("useService: service that returns null", async function (assert) {
class MyComponent extends Component {
setup() {
this.toyService = useService("toy_service");
}
}
MyComponent.template = tags.xml`<div/>`;
serviceRegistry.add("toy_service", {
name: "toy_service",
start: () => {
return null;
},
});
const env = await makeTestEnv();
const target = getFixture();
const comp = await mount(MyComponent, { env, target });
assert.strictEqual(comp.toyService, null);
comp.unmount();
});
});
});
@@ -2,7 +2,7 @@
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/service_hook";
import { useService } from "@web/core/utils/hooks";
import testUtils from "web.test_utils";
import { click, legacyExtraNextTick, nextTick, patchWithCleanup } from "../../helpers/utils";
import { createWebClient, doAction, getActionManagerServerData } from "./../helpers";
@@ -4,16 +4,11 @@ import testUtils from "web.test_utils";
import core from "web.core";
import AbstractAction from "web.AbstractAction";
import { registry } from "@web/core/registry";
import {
click,
legacyExtraNextTick,
patchWithCleanup,
makeDeferred,
} from "../../helpers/utils";
import { click, legacyExtraNextTick, patchWithCleanup, makeDeferred } from "../../helpers/utils";
import { createWebClient, doAction, getActionManagerServerData } from "./../helpers";
import { registerCleanup } from "../../helpers/cleanup";
import { errorService } from "@web/core/errors/error_service";
import { useService } from "@web/core/service_hook";
import { useService } from "@web/core/utils/hooks";
import { ClientErrorDialog } from "@web/core/errors/error_dialogs";
let serverData;
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { useService } from "@web/core/service_hook";
import { useService } from "@web/core/utils/hooks";
import { Dialog } from "@web/core/dialog/dialog";
import { _lt } from "@web/core/l10n/translation";