[IMP] web, web_tour: improve isVisible helper
Before this commit, the isVisible helper would most of the time work as expected, except if an element has display: contents. In that case, it has no bounding box, but it may still be visible if one of its child is visible. This is particularly important since we want to set the display property of all field components to "contents". Part-of: odoo/odoo#96865
This commit is contained in:
committed by
Aaron Bohy
parent
b3831d9892
commit
130a7d9ee1
@@ -1,12 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
// rough approximation of a visible element. not perfect (does not take into
|
||||
// account opacity = 0 for example), but good enough for our purpose
|
||||
function isVisible(e) {
|
||||
return e.offsetWidth > 0 || e.offsetHeight > 0;
|
||||
}
|
||||
import { isVisible } from "@web/core/utils/ui";
|
||||
|
||||
export const ACTION_HELPERS = {
|
||||
click(el, _step) {
|
||||
|
||||
@@ -1,5 +1,73 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* rough approximation of a visible element. not perfect (does not take into
|
||||
* account opacity = 0 for example), but good enough for our purpose
|
||||
*
|
||||
* @param {Element} el
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isVisible(el) {
|
||||
if (el === document || el === window) {
|
||||
return true;
|
||||
}
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
let _isVisible = false;
|
||||
if ("offsetWidth" in el && "offsetHeight" in el) {
|
||||
_isVisible = el.offsetWidth > 0 && el.offsetHeight > 0;
|
||||
} else if ("getBoundingClientRect" in el) {
|
||||
// for example, svgelements
|
||||
const rect = el.getBoundingClientRect();
|
||||
_isVisible = rect.width > 0 && rect.height > 0;
|
||||
}
|
||||
if (!_isVisible && getComputedStyle(el).display === "contents") {
|
||||
for (const child of el.children) {
|
||||
if (isVisible(child)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return _isVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function only exists because some tours currently rely on the fact that
|
||||
* we can click on elements with a non null width *xor* height (not both). However,
|
||||
* if one of these is 0, the element is not visible. We thus keep this function
|
||||
* to ease the transition to the more robust "isVisible" helper, which requires
|
||||
* both a non null width *and* height.
|
||||
*
|
||||
* @deprecated use isVisible instead
|
||||
* @param {Element} el
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function _legacyIsVisible(el) {
|
||||
if (el === document || el === window) {
|
||||
return true;
|
||||
}
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
let _isVisible = false;
|
||||
if ("offsetWidth" in el && "offsetHeight" in el) {
|
||||
_isVisible = el.offsetWidth > 0 || el.offsetHeight > 0;
|
||||
} else if ("getBoundingClientRect" in el) {
|
||||
// for example, svgelements
|
||||
const rect = el.getBoundingClientRect();
|
||||
_isVisible = rect.width > 0 || rect.height > 0;
|
||||
}
|
||||
if (!_isVisible && getComputedStyle(el).display === "contents") {
|
||||
for (const child of el.children) {
|
||||
if (isVisible(child)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return _isVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} activeElement
|
||||
* @param {String} selector
|
||||
@@ -10,8 +78,7 @@ export function getVisibleElements(activeElement, selector) {
|
||||
/** @type {NodeListOf<HTMLElement>} */
|
||||
const elements = activeElement.querySelectorAll(selector);
|
||||
for (const el of elements) {
|
||||
const isVisible = el.offsetWidth > 0 && el.offsetHeight > 0;
|
||||
if (isVisible) {
|
||||
if (isVisible(el)) {
|
||||
visibleElements.push(el);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ async function mountComponent(Picker, props) {
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml/* xml */ `
|
||||
<t t-component="props.Picker" t-props="props.props"/>
|
||||
<div class="outsideDiv"/>
|
||||
<div class="outsideDiv">Outside div</div>
|
||||
`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
@@ -38,8 +38,6 @@ QUnit.module("Components", () => {
|
||||
QUnit.module("ColorList");
|
||||
|
||||
QUnit.test("basic rendering", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
await mountComponent(ColorList, {
|
||||
colors: [0, 9],
|
||||
});
|
||||
@@ -60,8 +58,6 @@ QUnit.module("Components", () => {
|
||||
});
|
||||
|
||||
QUnit.test("toggler is available if togglerColor props is given", async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
const togglerColorId = 0;
|
||||
await mountComponent(ColorList, {
|
||||
colors: [4, 5, 6],
|
||||
@@ -88,10 +84,10 @@ QUnit.module("Components", () => {
|
||||
"button.o_colorlist_toggler",
|
||||
"toggler button is no longer visible"
|
||||
);
|
||||
assert.containsN(target, "button", 3, "three buttons are available");
|
||||
assert.containsN(target, ".o_colorlist button", 3, "three buttons are available");
|
||||
|
||||
await click(target.querySelector(".outsideDiv"));
|
||||
assert.containsN(target, "button", 1, "only one button is available");
|
||||
assert.containsOnce(target, ".o_colorlist button", "only one button is available");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"button.o_colorlist_toggler",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { isMacOS } from "@web/core/browser/feature_detection";
|
||||
import { download } from "@web/core/network/download";
|
||||
import { Deferred } from "@web/core/utils/concurrency";
|
||||
import { patch, unpatch } from "@web/core/utils/patch";
|
||||
import { isVisible } from "@web/core/utils/ui";
|
||||
import { registerCleanup } from "./cleanup";
|
||||
|
||||
const { App, onMounted, onPatched, useComponent } = owl;
|
||||
@@ -285,12 +286,7 @@ export function triggerEvent(el, selector, eventType, eventAttrs = {}, options =
|
||||
throw new Error(`Can't find a target to trigger ${eventType} event`);
|
||||
}
|
||||
if (!options.skipVisibilityCheck) {
|
||||
const isVisible =
|
||||
target === document ||
|
||||
target === window ||
|
||||
target.offsetWidth > 0 ||
|
||||
target.offsetHeight > 0;
|
||||
if (!isVisible) {
|
||||
if (!isVisible(target)) {
|
||||
throw new Error(`Called triggerEvent ${eventType} on invisible target`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { setupQUnit } from "./qunit";
|
||||
import { setupTests } from "./setup";
|
||||
|
||||
(async () => {
|
||||
setupQUnit();
|
||||
await setupTests();
|
||||
QUnit.start();
|
||||
})();
|
||||
|
||||
@@ -1,14 +1,37 @@
|
||||
/** @odoo-module */
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
import { isVisible as isElemVisible } from "@web/core/utils/ui";
|
||||
|
||||
function setQUnitDebugMode() {
|
||||
owl.whenReady(() => document.body.classList.add("debug")); // make the test visible to the naked eye
|
||||
QUnit.config.debug = true; // allows for helper functions to behave differently (logging, the HTML element in which the test occurs etc...)
|
||||
QUnit.config.testTimeout = 60 * 60 * 1000;
|
||||
// Allows for interacting with the test when it is over
|
||||
// In fact, this will pause QUnit.
|
||||
// Also, logs useful info in the console.
|
||||
QUnit.testDone(async (...args) => {
|
||||
console.groupCollapsed("Debug Test output");
|
||||
console.log(...args);
|
||||
console.groupEnd();
|
||||
await new Promise(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
// need to do this outside of the setup function so the QUnit.debug is defined when we need it
|
||||
QUnit.debug = (name, cb) => {
|
||||
setQUnitDebugMode();
|
||||
QUnit.only(name, cb);
|
||||
};
|
||||
|
||||
// need to do this outside of the setup function so it is executed quickly
|
||||
QUnit.config.autostart = false;
|
||||
|
||||
export function setupQUnit() {
|
||||
const { Component } = owl;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// QUnit config
|
||||
// -----------------------------------------------------------------------------
|
||||
QUnit.config.autostart = false;
|
||||
QUnit.config.testTimeout = 1 * 60 * 1000;
|
||||
QUnit.config.hidepassed = window.location.href.match(/[?&]testId=/) === null;
|
||||
|
||||
@@ -166,14 +189,8 @@
|
||||
}
|
||||
}
|
||||
msg = msg || `target should ${shouldBeVisible ? "" : "not"} be visible`;
|
||||
let isVisible = el && el.offsetWidth && el.offsetHeight;
|
||||
if (isVisible) {
|
||||
// This computation is a little more heavy and we only want to perform it
|
||||
// if the above assertion has failed.
|
||||
const rect = el.getBoundingClientRect();
|
||||
isVisible = rect.width + rect.height;
|
||||
}
|
||||
const condition = shouldBeVisible ? isVisible : !isVisible;
|
||||
const _isVisible = isElemVisible(el);
|
||||
const condition = shouldBeVisible ? _isVisible : !_isVisible;
|
||||
QUnit.assert.ok(condition, msg);
|
||||
}
|
||||
function isVisible(el, msg) {
|
||||
@@ -330,26 +347,6 @@
|
||||
reRun.parentElement.insertBefore(reRunDebug, reRun.nextSibling);
|
||||
});
|
||||
|
||||
function setQUnitDebugMode() {
|
||||
owl.whenReady(() => document.body.classList.add("debug")); // make the test visible to the naked eye
|
||||
QUnit.config.debug = true; // allows for helper functions to behave differently (logging, the HTML element in which the test occurs etc...)
|
||||
QUnit.config.testTimeout = 60 * 60 * 1000;
|
||||
// Allows for interacting with the test when it is over
|
||||
// In fact, this will pause QUnit.
|
||||
// Also, logs useful info in the console.
|
||||
QUnit.testDone(async (...args) => {
|
||||
console.groupCollapsed("Debug Test output");
|
||||
console.log(...args);
|
||||
console.groupEnd();
|
||||
await new Promise(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.debug = (name, cb) => {
|
||||
setQUnitDebugMode();
|
||||
QUnit.only(name, cb);
|
||||
};
|
||||
|
||||
const debugTestId = new URLSearchParams(location.search).get("debugTestId");
|
||||
if (debugTestId) {
|
||||
QUnit.config.testId = [debugTestId];
|
||||
@@ -513,4 +510,4 @@
|
||||
el.innerText = "details:not([open]) > :not(summary) { display: none; }";
|
||||
document.head.appendChild(el);
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("readonly BooleanToggleField is not disabled in edit mode", async function (assert) {
|
||||
QUnit.test("readonly BooleanToggleField is disabled in edit mode", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
@@ -66,11 +66,8 @@ QUnit.module("Fields", (hooks) => {
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_boolean_toggle input:disabled:checked");
|
||||
|
||||
await click(target, ".o_field_widget[name='bar'] label");
|
||||
|
||||
assert.containsOnce(target, ".o_boolean_toggle input:disabled:checked");
|
||||
assert.containsOnce(target, ".o_form_editable");
|
||||
assert.ok(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
});
|
||||
|
||||
QUnit.test("BooleanToggleField is not disabled in readonly mode", async function (assert) {
|
||||
@@ -82,9 +79,11 @@ QUnit.module("Fields", (hooks) => {
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_form_readonly");
|
||||
assert.containsOnce(target, ".form-check.o_boolean_toggle");
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").checked);
|
||||
await click(target, ".o_field_widget[name='bar'] label");
|
||||
await click(target, ".o_field_widget[name='bar'] input");
|
||||
assert.ok(target.querySelector(".o_boolean_toggle input").checked);
|
||||
});
|
||||
|
||||
@@ -98,10 +97,9 @@ QUnit.module("Fields", (hooks) => {
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".form-check.o_boolean_toggle");
|
||||
assert.ok(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
await click(target.querySelector(".o_form_button_edit"));
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").checked);
|
||||
await click(target, ".o_field_widget[name='bar'] label");
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").checked);
|
||||
assert.ok(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
});
|
||||
|
||||
QUnit.test("BooleanToggleField is enabled in edit mode", async function (assert) {
|
||||
@@ -116,23 +114,23 @@ QUnit.module("Fields", (hooks) => {
|
||||
assert.containsOnce(target, ".form-check.o_boolean_toggle");
|
||||
await click(target.querySelector(".o_form_button_edit"));
|
||||
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").checked);
|
||||
await click(target, ".o_field_widget[name='bar'] label");
|
||||
await click(target, ".o_field_widget[name='bar'] input");
|
||||
assert.notOk(target.querySelector(".o_boolean_toggle input").disabled);
|
||||
assert.ok(target.querySelector(".o_boolean_toggle input").checked);
|
||||
});
|
||||
|
||||
QUnit.test("boolean toggle widget is not disabled in readonly mode", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
serverData,
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" />
|
||||
</form>`,
|
||||
<form>
|
||||
<field name="bar" widget="boolean_toggle" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
|
||||
@@ -9204,7 +9204,7 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.ok(getCard(0).querySelector("[name='bar'] input").checked);
|
||||
assert.ok(getCard(1).querySelector("[name='bar'] input").checked);
|
||||
|
||||
await click(getCard(1), "[name='bar'] label");
|
||||
await click(getCard(1), "[name='bar'] input");
|
||||
assert.ok(getCard(0).querySelector("[name='bar'] input").checked);
|
||||
assert.notOk(getCard(1).querySelector("[name='bar'] input").checked);
|
||||
});
|
||||
|
||||
@@ -3129,7 +3129,7 @@ QUnit.module("Views", (hooks) => {
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.isNotVisible(target.querySelector(".o_field_one2many"));
|
||||
assert.containsNone(target, ".o_field_one2many");
|
||||
|
||||
await click(target.querySelector(".nav-item:last-child .nav-link"));
|
||||
|
||||
@@ -5079,7 +5079,7 @@ QUnit.module("Views", (hooks) => {
|
||||
await toggleMenuItem(target, "Empty List");
|
||||
assert.containsOnce(target, ".o_view_nocontent");
|
||||
|
||||
await click(target, ".o_view_nocontent");
|
||||
await click(target, ".o_nocontent_help");
|
||||
assert.hasClass(target.querySelector(".o_list_button_add"), "o_catch_attention");
|
||||
}
|
||||
);
|
||||
@@ -8649,7 +8649,7 @@ QUnit.module("Views", (hooks) => {
|
||||
await click(target.querySelectorAll(".o_data_row .o_list_record_selector input")[0]);
|
||||
await click(target.querySelectorAll(".o_data_row .o_list_record_selector input")[1]);
|
||||
await click(target.querySelectorAll(".o_data_row .o_list_record_selector input")[2]);
|
||||
await click(target.querySelector(".o_data_row .o_field_boolean label"));
|
||||
await click(target.querySelector(".o_data_row .o_field_boolean input"));
|
||||
assert.containsOnce(target, ".modal");
|
||||
|
||||
await click(target, ".modal .modal-footer .btn-primary");
|
||||
@@ -9471,7 +9471,7 @@ QUnit.module("Views", (hooks) => {
|
||||
const rows = target.querySelectorAll(".o_data_row");
|
||||
await click(rows[0], ".o_list_record_selector input");
|
||||
await click(rows[2], ".o_list_record_selector input");
|
||||
await click(rows[0].querySelector(".o_boolean_toggle label"));
|
||||
await click(rows[0].querySelector(".o_boolean_toggle input"));
|
||||
|
||||
assert.ok(
|
||||
$(".modal").text().includes("Confirmation"),
|
||||
|
||||
@@ -358,7 +358,10 @@ QUnit.module("ActionManager", (hooks) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
ClientAction.template = xml`<div class="my_action" t-on-click="onClick" />`;
|
||||
ClientAction.template = xml`
|
||||
<div class="my_action" t-on-click="onClick">
|
||||
My Action
|
||||
</div>`;
|
||||
registry.category("actions").add("clientAction", ClientAction);
|
||||
|
||||
const errorDialogOpened = makeDeferred();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
odoo.define('web_tour.utils', function(require) {
|
||||
"use strict";
|
||||
|
||||
const { _legacyIsVisible } = require("@web/core/utils/ui");
|
||||
|
||||
function get_step_key(name) {
|
||||
return 'tour_' + name + '_step';
|
||||
}
|
||||
@@ -20,7 +22,7 @@ function get_running_delay_key() {
|
||||
function get_first_visible_element($elements) {
|
||||
for (var i = 0 ; i < $elements.length ; i++) {
|
||||
var $i = $elements.eq(i);
|
||||
if ($i.is(':visible:hasVisibility')) {
|
||||
if (_legacyIsVisible($i[0])) {
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ odoo.define("website_event_booth_exhibitor.tour", function (require) {
|
||||
trigger: 'img[alt="Premium Booth"]',
|
||||
}, {
|
||||
content: 'Choose Booth',
|
||||
trigger: 'div:contains("OpenWood Demonstrator 2") input',
|
||||
trigger: '.o_wbooth_booths div:contains("OpenWood Demonstrator 2") input',
|
||||
}, {
|
||||
content: "Validate attendees details",
|
||||
trigger: 'button:contains("Book my Booths")',
|
||||
|
||||
Reference in New Issue
Block a user