diff --git a/addons/web/static/src/core/tooltip/tooltip_hook.js b/addons/web/static/src/core/tooltip/tooltip_hook.js deleted file mode 100644 index 24d9d05de06..00000000000 --- a/addons/web/static/src/core/tooltip/tooltip_hook.js +++ /dev/null @@ -1,120 +0,0 @@ -/** @odoo-module **/ - -import { browser } from "@web/core/browser/browser"; -import { useEffect, useListener } from "@web/core/utils/hooks"; -import { usePopover } from "@web/core/popover/popover_hook"; -import { Tooltip } from "./tooltip"; - -/** - * The useTooltip hook allows to display custom tooltips on every elements with - * a "data-tooltip" attribute inside the Component using the hook. This attribute - * can be set on elements for which we prefer a custom tooltip instead of the - * native one displaying the value of the "title" attribute. - * - * Usage (with a parent somewhere using the useTooltip hook): - * - * - * The ideal position of the tooltip can be specified thanks to the attribute - * "data-tooltip-position": - * - * - * For advanced tooltips containing dynamic and/or html content, the - * "data-tooltip-template" and "data-tooltip-info" attributes can be used. - * For example, let's suppose the following qweb template: - * - * - * - * This template can then be used in a tooltip as follows: - * - * with "info" being a stringified object with two keys "x" and "y". - */ -const OPEN_DELAY = 400; -const CLOSE_DELAY = 200; -export function useTooltip() { - const popover = usePopover(); - let openTooltipTimeout; - let closeTooltip; - let target = null; - - /** - * Closes the currently opened tooltip if any, or prevent it from opening. - */ - function cleanup() { - browser.clearTimeout(openTooltipTimeout); - if (closeTooltip) { - closeTooltip(); - } - } - - /** - * Opens the tooltip after a delay, if the event's target has a data-tooltip - * attribute. - * - * @param {MouseEvent} ev a "mouseenter" event - */ - function onMouseEnter(ev) { - const dataset = ev.target.dataset; - const tooltip = dataset.tooltip; - let template, info; - if ("tooltipTemplate" in dataset) { - template = dataset.tooltipTemplate; - info = dataset.tooltipInfo ? JSON.parse(dataset.tooltipInfo) : null; - } - if (tooltip || template) { - cleanup(); - target = ev.target; - openTooltipTimeout = browser.setTimeout(() => { - const position = dataset.tooltipPosition; - closeTooltip = popover.add( - target, - Tooltip, - { tooltip, template, info }, - { position } - ); - }, OPEN_DELAY); - } - } - - // Track mouse position to be able to detect that we are no longer hovering - // the target, thus that we should close the tooltip - let positionX; - let positionY; - useListener("mousemove", (ev) => { - positionX = ev.x; - positionY = ev.y; - }); - - // Regularly check that the target is still in the DOM and we're still - // hovering it, because if not, we have to close the tooltip - useEffect( - () => { - const interval = browser.setInterval(() => { - if (!target) { - return; - } - if (!document.body.contains(target)) { - return cleanup(); // target is no longer in the DOM - } - const targetRect = target.getBoundingClientRect(); - if ( - positionX < targetRect.left || - positionX > targetRect.right || - positionY < targetRect.top || - positionY > targetRect.bottom - ) { - return cleanup(); // mouse is no longer hovering the target - } - }, CLOSE_DELAY); - return () => browser.clearInterval(interval); - }, - () => [] - ); - - // Listen (using event delegation) to "mouseenter" events on all nodes with - // the "data-tooltip" or "data-tooltip-template" attribute, to open the tooltip. - const selector = "[data-tooltip], [data-tooltip-template]"; - useListener("mouseenter", selector, onMouseEnter, { capture: true }); -} diff --git a/addons/web/static/src/core/tooltip/tooltip_service.js b/addons/web/static/src/core/tooltip/tooltip_service.js new file mode 100644 index 00000000000..bc591385fc8 --- /dev/null +++ b/addons/web/static/src/core/tooltip/tooltip_service.js @@ -0,0 +1,125 @@ +/** @odoo-module **/ + +import { browser } from "@web/core/browser/browser"; +import { registry } from "@web/core/registry"; +import { Tooltip } from "./tooltip"; + +const { whenReady } = owl; + +/** + * The tooltip service allows to display custom tooltips on every elements with + * a "data-tooltip" attribute. This attribute can be set on elements for which + * we prefer a custom tooltip instead of the native one displaying the value of + * the "title" attribute. + * + * Usage: + * + * + * The ideal position of the tooltip can be specified thanks to the attribute + * "data-tooltip-position": + * + * + * For advanced tooltips containing dynamic and/or html content, the + * "data-tooltip-template" and "data-tooltip-info" attributes can be used. + * For example, let's suppose the following qweb template: + * + * + * + * This template can then be used in a tooltip as follows: + * + * with "info" being a stringified object with two keys "x" and "y". + */ + +const OPEN_DELAY = 400; +const CLOSE_DELAY = 200; + +export const tooltipService = { + dependencies: ["popover"], + start(env, { popover }) { + let openTooltipTimeout; + let closeTooltip; + let target = null; + + let positionX; + let positionY; + + /** + * Closes the currently opened tooltip if any, or prevent it from opening. + */ + function cleanup() { + browser.clearTimeout(openTooltipTimeout); + if (closeTooltip) { + closeTooltip(); + } + } + + /** + * Opens the tooltip after a delay, if the event's target has a data-tooltip + * attribute. + * + * @param {MouseEvent} ev a "mouseenter" event + */ + function onMouseenter(ev) { + let el = ev.target; + if (!el.matches("[data-tooltip], [data-tooltip-template]")) { + return; + } + target = el; + const dataset = el.dataset; + const tooltip = dataset.tooltip; + let template, info; + if ("tooltipTemplate" in dataset) { + template = dataset.tooltipTemplate; + info = dataset.tooltipInfo ? JSON.parse(dataset.tooltipInfo) : null; + } + cleanup(); + openTooltipTimeout = browser.setTimeout(() => { + const position = dataset.tooltipPosition; + closeTooltip = popover.add( + target, + Tooltip, + { tooltip, template, info }, + { position } + ); + }, OPEN_DELAY); + } + + whenReady(() => { + // Regularly check that the target is still in the DOM and we're still + // hovering it, because if not, we have to close the tooltipd + browser.setInterval(() => { + if (!target) { + return; + } + if (!document.body.contains(target)) { + return cleanup(); // target is no longer in the DOM + } + const targetRect = target.getBoundingClientRect(); + if ( + positionX < targetRect.left || + positionX > targetRect.right || + positionY < targetRect.top || + positionY > targetRect.bottom + ) { + return cleanup(); // mouse is no longer hovering the target + } + }, CLOSE_DELAY); + + // Track mouse position to be able to detect that we are no longer hovering + // the target, thus that we should close the tooltip + document.body.addEventListener("mousemove", (ev) => { + positionX = ev.x; + positionY = ev.y; + }); + + // Listen (using event delegation) to "mouseenter" events on all nodes with + // the "data-tooltip" or "data-tooltip-template" attribute, to open the tooltip. + document.body.addEventListener("mouseenter", onMouseenter, { capture: true }); + }); + }, +}; + +registry.category("services").add("tooltip", tooltipService); diff --git a/addons/web/static/src/webclient/webclient.js b/addons/web/static/src/webclient/webclient.js index 8efe92e4a6e..cf0260f1dd7 100644 --- a/addons/web/static/src/webclient/webclient.js +++ b/addons/web/static/src/webclient/webclient.js @@ -3,7 +3,6 @@ import { ActionContainer } from "./actions/action_container"; import { NavBar } from "./navbar/navbar"; import { useBus, useEffect, useService } from "@web/core/utils/hooks"; -import { useTooltip } from "@web/core/tooltip/tooltip_hook"; import { NotUpdatable } from "../core/utils/components"; import { MainComponentsContainer } from "../core/main_components_container"; import { useOwnDebugContext } from "../core/debug/debug_context"; @@ -46,7 +45,6 @@ export class WebClient extends Component { () => [] ); useExternalListener(window, "click", this.onGlobalClick, { capture: true }); - useTooltip(); } mounted() { diff --git a/addons/web/static/tests/core/tooltip/tooltip_hook_tests.js b/addons/web/static/tests/core/tooltip/tooltip_hook_tests.js deleted file mode 100644 index feb25e7217d..00000000000 --- a/addons/web/static/tests/core/tooltip/tooltip_hook_tests.js +++ /dev/null @@ -1,287 +0,0 @@ -/** @odoo-module **/ - -import { browser } from "@web/core/browser/browser"; -import { PopoverContainer } from "@web/core/popover/popover_container"; -import { popoverService } from "@web/core/popover/popover_service"; -import { useTooltip } from "@web/core/tooltip/tooltip_hook"; -import { registry } from "@web/core/registry"; -import { registerCleanup } from "../../helpers/cleanup"; -import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env"; -import { getFixture, nextTick, patchWithCleanup, triggerEvent } from "../../helpers/utils"; - -const { Component, mount, useState, xml } = owl; - -const mainComponents = registry.category("main_components"); - -/** - * Creates and mounts a parent component that use the "useTooltip" hook. - * - * @param {Component} Child a child Component that contains nodes with "data-tooltip" attribute - * @param {Object} [options] - * @param {function} options.mockSetTimeout the mocked setTimeout to use (by default, calls the - * callback directly) - * @param {function} options.mockSetInterval the mocked setInterval to use (by default, calls the - * callback directly) - * @param {function} options.mockClearTimeout the mocked clearTimeout to use (by default, does nothing) - * @param {function} options.mockClearInterval the mocked clearInterval to use (by default, does nothing) - * @returns {Promise} - */ -async function makeParent(Child, options = {}) { - const fixture = getFixture(); - - // add the popover service to the registry -> will add the PopoverContainer - // to the mainComponentRegistry - clearRegistryWithCleanup(mainComponents); - registry.category("services").add("popover", popoverService); - const env = await makeTestEnv(); - - class Parent extends Component { - setup() { - this.Components = mainComponents.getEntries(); - useTooltip(); - } - } - Parent.template = xml` -
- -
- - - -
-
`; - Parent.components = { PopoverContainer, Child }; - - patchWithCleanup(browser, { - setTimeout: options.mockSetTimeout || ((fn) => fn()), - clearTimeout: options.mockClearTimeout || (() => {}), - setInterval: options.mockSetInterval || ((fn) => fn()), - clearInterval: options.mockClearInterval || (() => {}), - }); - - const comp = await mount(Parent, { env, target: fixture }); - registerCleanup(() => comp.destroy()); - return comp; -} - -QUnit.module("Tooltip hook", () => { - QUnit.test("basic rendering", async (assert) => { - class MyComponent extends Component {} - MyComponent.template = xml``; - let simulateTimeout; - const mockSetTimeout = (fn) => { - simulateTimeout = fn; - }; - let simulateInterval; - const mockSetInterval = (fn) => { - simulateInterval = fn; - }; - const parent = await makeParent(MyComponent, { mockSetTimeout, mockSetInterval }); - - assert.containsNone(parent, ".o_popover_container .o_popover"); - parent.el.querySelector("button").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsNone(parent, ".o_popover_container .o_popover"); - - simulateTimeout(); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual( - parent.el.querySelector(".o_popover_container .o_popover").innerText, - "hello" - ); - - const buttonRect = parent.el.querySelector("button").getBoundingClientRect(); - const x = buttonRect.right + 10; - const y = buttonRect.bottom + 10; - await triggerEvent(parent.el, "button", "mousemove", { - pageX: x, - layerX: x, - screenX: x, - pageY: y, - layerY: y, - screenY: y, - }); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - simulateInterval(); - await nextTick(); - assert.containsNone(parent, ".o_popover_container .o_popover"); - }); - - QUnit.test("remove element with opened tooltip", async (assert) => { - let compState; - class MyComponent extends Component { - setup() { - this.state = useState({ visible: true }); - compState = this.state; - } - } - MyComponent.template = xml` -
- -
`; - let simulateInterval; - const mockSetInterval = (fn) => { - simulateInterval = fn; - }; - const parent = await makeParent(MyComponent, { mockSetInterval }); - - assert.containsOnce(parent, "button"); - assert.containsNone(parent, ".o_popover_container .o_popover"); - parent.el.querySelector("button").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - - compState.visible = false; - await nextTick(); - assert.containsNone(parent, "button"); - simulateInterval(); - await nextTick(); - assert.containsNone(parent, ".o_popover_container .o_popover"); - }); - - QUnit.test("rendering with several tooltips", async (assert) => { - class MyComponent extends Component {} - MyComponent.template = xml` -
- - -
`; - const parent = await makeParent(MyComponent); - - assert.containsNone(parent, ".o_popover_container .o_popover"); - parent.el.querySelector("button.button_1").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "tooltip 1"); - parent.el.querySelector("button.button_1").dispatchEvent(new Event("mouseleave")); - parent.el.querySelector("button.button_2").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "tooltip 2"); - }); - - QUnit.test("positionning", async (assert) => { - class MyComponent extends Component {} - MyComponent.template = xml` -
- - - - - -
`; - const parent = await makeParent(MyComponent); - - // default - parent.el.querySelector("button.default").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "default"); - assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--bm"); - - // top - parent.el.querySelector("button.top").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "top"); - assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--tm"); - - // right - parent.el.querySelector("button.right").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "right"); - assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--rm"); - - // bottom - parent.el.querySelector("button.bottom").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "bottom"); - assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--bm"); - - // left - parent.el.querySelector("button.left").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "left"); - assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--lm"); - }); - - QUnit.test("tooltip with a template, no info", async (assert) => { - class MyComponent extends Component {} - MyComponent.template = xml` - - `; - const parent = await makeParent(MyComponent); - parent.env.qweb.addTemplate("my_tooltip_template", "tooltip"); - - assert.containsNone(parent, ".o_popover_container .o-tooltip"); - parent.el.querySelector("button").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o-tooltip"); - assert.strictEqual(parent.el.querySelector(".o-tooltip").innerHTML, "tooltip"); - }); - - QUnit.test("tooltip with a template and info", async (assert) => { - class MyComponent extends Component { - get info() { - return JSON.stringify({ x: 3, y: "abc" }); - } - } - MyComponent.template = xml` - - `; - const parent = await makeParent(MyComponent); - parent.env.qweb.addTemplate( - "my_tooltip_template", - ` - - ` - ); - - assert.containsNone(parent, ".o_popover_container .o-tooltip"); - parent.el.querySelector("button").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o-tooltip"); - assert.strictEqual( - parent.el.querySelector(".o-tooltip").innerHTML, - "" - ); - }); - - QUnit.test("destroy a component that called useTooltip", async (assert) => { - class MyComponent extends Component {} - MyComponent.template = xml` - - `; - const parent = await makeParent(MyComponent, { - mockSetInterval() { - assert.step("setInterval"); - }, - mockClearInterval() { - assert.step("clearInterval"); - }, - }); - - assert.verifySteps(["setInterval"]); - assert.containsNone(parent, ".o_popover_container .o-tooltip"); - - parent.el.querySelector("button").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o-tooltip"); - assert.strictEqual(parent.el.querySelector(".o-tooltip").innerText, "tooltip"); - - parent.destroy(); - - assert.verifySteps(["clearInterval"]); - }); -}); diff --git a/addons/web/static/tests/core/tooltip/tooltip_service_tests.js b/addons/web/static/tests/core/tooltip/tooltip_service_tests.js new file mode 100644 index 00000000000..d7756b910c1 --- /dev/null +++ b/addons/web/static/tests/core/tooltip/tooltip_service_tests.js @@ -0,0 +1,318 @@ +/** @odoo-module **/ + +import { browser } from "@web/core/browser/browser"; +import { PopoverContainer } from "@web/core/popover/popover_container"; +import { popoverService } from "@web/core/popover/popover_service"; +import { tooltipService } from "@web/core/tooltip/tooltip_service"; +import { registry } from "@web/core/registry"; +import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env"; +import { getFixture, nextTick, patchWithCleanup, triggerEvent } from "../../helpers/utils"; +import { registerCleanup } from "../../helpers/cleanup"; + +const { Component, mount, useState, xml } = owl; + +const mainComponents = registry.category("main_components"); + +/** + * Creates and mounts a parent component that use the "useTooltip" hook. + * + * @param {Component} Child a child Component that contains nodes with "data-tooltip" attribute + * @param {Object} [options] + * @param {function} [options.mockSetTimeout] the mocked setTimeout to use (by default, calls the + * callback directly) + * @param {function} [options.mockSetInterval] the mocked setInterval to use (by default, calls the + * callback directly) + * @param {function} [options.mockClearTimeout] the mocked clearTimeout to use (by default, does nothing) + * @param {function} [options.mockClearInterval] the mocked clearInterval to use (by default, does nothing) + * @param {{[templateName:string]: string}} [options.templates] additional templates + * @returns {Promise} + */ +async function makeParent(Child, options = {}) { + const target = getFixture(); + + // add the popover service to the registry -> will add the PopoverContainer + // to the mainComponentRegistry + clearRegistryWithCleanup(mainComponents); + + patchWithCleanup(browser, { + setTimeout: options.mockSetTimeout || ((fn) => fn()), + clearTimeout: options.mockClearTimeout || (() => {}), + setInterval: options.mockSetInterval || ((fn) => fn()), + clearInterval: options.mockClearInterval || (() => {}), + }); + + registry.category("services").add("popover", popoverService); + registry.category("services").add("tooltip", tooltipService); + const env = await makeTestEnv(); + + class Parent extends Component { + setup() { + this.Components = mainComponents.getEntries(); + } + } + Parent.template = xml` +
+ +
+ + + +
+
`; + Parent.components = { PopoverContainer, Child }; + + const parent = await mount(Parent, { env, target }); + registerCleanup(() => parent.destroy()); + return parent; +} + +let target; +QUnit.module( + "Tooltip service", + { + beforeEach() { + target = getFixture(); + }, + }, + () => { + QUnit.test("basic rendering", async (assert) => { + class MyComponent extends Component {} + MyComponent.template = xml``; + let simulateTimeout; + const mockSetTimeout = (fn) => { + simulateTimeout = fn; + }; + let simulateInterval; + const mockSetInterval = (fn) => { + simulateInterval = fn; + }; + const parent = await makeParent(MyComponent, { mockSetTimeout, mockSetInterval }); + + assert.containsNone(parent, ".o_popover_container .o_popover"); + target.querySelector("button").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsNone(parent, ".o_popover_container .o_popover"); + + simulateTimeout(); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual( + target.querySelector(".o_popover_container .o_popover").innerText, + "hello" + ); + + const buttonRect = target.querySelector("button").getBoundingClientRect(); + const x = buttonRect.right + 10; + const y = buttonRect.bottom + 10; + await triggerEvent(target, "button", "mousemove", { + pageX: x, + layerX: x, + screenX: x, + pageY: y, + layerY: y, + screenY: y, + }); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + simulateInterval(); + await nextTick(); + assert.containsNone(parent, ".o_popover_container .o_popover"); + }); + + QUnit.test("basic rendering 2", async (assert) => { + class MyComponent extends Component {} + MyComponent.template = xml`Action`; + let simulateTimeout; + const mockSetTimeout = (fn) => { + simulateTimeout = fn; + }; + let simulateInterval; + const mockSetInterval = (fn) => { + simulateInterval = fn; + }; + const parent = await makeParent(MyComponent, { mockSetTimeout, mockSetInterval }); + + assert.containsNone(parent, ".o_popover_container .o_popover"); + let [outerSpan, innerSpan] = target.querySelectorAll("span.our_span"); + outerSpan.dispatchEvent(new Event("mouseenter")); + innerSpan.dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsNone(parent, ".o_popover_container .o_popover"); + + simulateTimeout(); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual( + target.querySelector(".o_popover_container .o_popover").innerText, + "hello" + ); + + const outerSpanRect = outerSpan.getBoundingClientRect(); + const x = outerSpanRect.right + 10; + const y = outerSpanRect.bottom + 10; + await triggerEvent(target, 'span[data-tooltip="hello"]', "mousemove", { + pageX: x, + layerX: x, + screenX: x, + pageY: y, + layerY: y, + screenY: y, + }); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + simulateInterval(); + await nextTick(); + assert.containsNone(parent, ".o_popover_container .o_popover"); + }); + + QUnit.test("remove element with opened tooltip", async (assert) => { + let compState; + class MyComponent extends Component { + setup() { + this.state = useState({ visible: true }); + compState = this.state; + } + } + MyComponent.template = xml` +
+ +
`; + let simulateInterval; + const mockSetInterval = (fn) => { + simulateInterval = fn; + }; + const parent = await makeParent(MyComponent, { mockSetInterval }); + + assert.containsOnce(parent, "button"); + assert.containsNone(parent, ".o_popover_container .o_popover"); + target.querySelector("button").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + + compState.visible = false; + await nextTick(); + assert.containsNone(parent, "button"); + simulateInterval(); + await nextTick(); + assert.containsNone(parent, ".o_popover_container .o_popover"); + }); + + QUnit.test("rendering with several tooltips", async (assert) => { + class MyComponent extends Component {} + MyComponent.template = xml` +
+ + +
`; + const parent = await makeParent(MyComponent); + + assert.containsNone(parent, ".o_popover_container .o_popover"); + target.querySelector("button.button_1").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual(target.querySelector(".o_popover").innerText, "tooltip 1"); + target.querySelector("button.button_1").dispatchEvent(new Event("mouseleave")); + target.querySelector("button.button_2").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual(target.querySelector(".o_popover").innerText, "tooltip 2"); + }); + + QUnit.test("positionning", async (assert) => { + class MyComponent extends Component {} + MyComponent.template = xml` +
+ + + + + +
`; + const parent = await makeParent(MyComponent); + + // default + target.querySelector("button.default").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual(target.querySelector(".o_popover").innerText, "default"); + assert.hasClass(target.querySelector(".o_popover"), "o-popper-position--bm"); + + // top + target.querySelector("button.top").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual(target.querySelector(".o_popover").innerText, "top"); + assert.hasClass(target.querySelector(".o_popover"), "o-popper-position--tm"); + + // right + target.querySelector("button.right").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual(target.querySelector(".o_popover").innerText, "right"); + assert.hasClass(target.querySelector(".o_popover"), "o-popper-position--rm"); + + // bottom + target.querySelector("button.bottom").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual(target.querySelector(".o_popover").innerText, "bottom"); + assert.hasClass(target.querySelector(".o_popover"), "o-popper-position--bm"); + + // left + target.querySelector("button.left").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual(target.querySelector(".o_popover").innerText, "left"); + assert.hasClass(target.querySelector(".o_popover"), "o-popper-position--lm"); + }); + + QUnit.test("tooltip with a template, no info", async (assert) => { + class MyComponent extends Component {} + MyComponent.template = xml` + + `; + + const parent = await makeParent(MyComponent); + parent.env.qweb.addTemplate("my_tooltip_template", "tooltip"); + + assert.containsNone(parent, ".o_popover_container .o-tooltip"); + target.querySelector("button").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o-tooltip"); + assert.strictEqual(target.querySelector(".o-tooltip").innerHTML, "tooltip"); + }); + + QUnit.test("tooltip with a template and info", async (assert) => { + class MyComponent extends Component { + get info() { + return JSON.stringify({ x: 3, y: "abc" }); + } + } + MyComponent.template = xml` + + `; + + const parent = await makeParent(MyComponent); + parent.env.qweb.addTemplate( + "my_tooltip_template", + ` + + ` + ); + + assert.containsNone(parent, ".o_popover_container .o-tooltip"); + target.querySelector("button").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o-tooltip"); + assert.strictEqual( + target.querySelector(".o-tooltip").innerHTML, + "" + ); + }); + } +);