Odoo18-Base/addons/web/static/lib/hoot/tests/hoot-dom/dom.test.js
2025-01-06 10:57:38 +07:00

946 lines
36 KiB
JavaScript

/** @odoo-module */
import { describe, expect, getFixture, test } from "@odoo/hoot";
import {
click,
formatXml,
getActiveElement,
getFocusableElements,
getNextFocusableElement,
getPreviousFocusableElement,
isDisplayed,
isEditable,
isFocusable,
isInDOM,
isVisible,
queryAll,
queryAllRects,
queryAllTexts,
queryOne,
queryRect,
waitFor,
waitForNone,
} from "@odoo/hoot-dom";
import { animationFrame, mockTouch } from "@odoo/hoot-mock";
import { getParentFrame } from "@web/../lib/hoot-dom/helpers/dom";
import { mountForTest, parseUrl, waitForIframes } from "../local_helpers";
/**
* @param {...string} queryAllSelectors
*/
const expectSelector = (...queryAllSelectors) => {
/**
* @param {string} nativeSelector
*/
const toEqualNodes = (nativeSelector, options) => {
if (typeof nativeSelector !== "string") {
throw new Error(`Invalid selector: ${nativeSelector}`);
}
let root = options?.root || getFixture();
if (typeof root === "string") {
root = getFixture().querySelector(root);
if (root.tagName === "IFRAME") {
root = root.contentDocument;
}
}
let nodes = nativeSelector ? [...root.querySelectorAll(nativeSelector)] : [];
if (Number.isInteger(options?.index)) {
nodes = [nodes.at(options.index)];
}
const selector = queryAllSelectors.join(", ");
const fnNodes = queryAll(selector);
expect(fnNodes).toEqual(queryAll`${selector}`, {
message: (pass, r) => [
queryAll,
r`should return the same result from a tagged template literal`,
],
});
expect(fnNodes).toEqual(nodes, {
message: (pass, r) => [selector, r`should match`, nodes.length, r`nodes`],
});
};
return { toEqualNodes };
};
/**
* @param {Document} document
* @param {HTMLElement} [root]
* @returns {Promise<HTMLIFrameElement>}
*/
const makeIframe = (document, root) =>
new Promise((resolve) => {
const iframe = document.createElement("iframe");
iframe.addEventListener("load", () => resolve(iframe));
iframe.srcdoc = "<body></body>";
(root || document.body).appendChild(iframe);
});
const FULL_HTML_TEMPLATE = /* html */ `
<header>
<h1 class="title">Title</h1>
</header>
<main id="custom-html">
<h5 class="title">List header</h5>
<ul colspan="1" class="overflow-auto" style="max-height: 80px">
<li class="text highlighted">First item</li>
<li class="text">Second item</li>
<li class="text">Last item</li>
</ul>
<p colspan="2" class="text">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur justo
velit, tristique vitae neque a, faucibus mollis dui. Aliquam iaculis
sodales mi id posuere. Proin malesuada bibendum pellentesque. Phasellus
mattis at massa quis gravida. Morbi luctus interdum mi, quis dapibus
augue. Vivamus condimentum nunc mi, vitae suscipit turpis dictum nec.
Sed varius diam dui, eget ultricies ante dictum ac.
</p>
<div class="hidden" style="display: none;">Invisible section</div>
<svg></svg>
<form class="overflow-auto" style="max-width: 100px">
<h5 class="title">Form title</h5>
<input name="name" type="text" value="John Doe (JOD)" />
<input name="email" type="email" value="johndoe@sample.com" />
<select name="title" value="mr">
<option>Select an option</option>
<option value="mr" selected="selected">Mr.</option>
<option value="mrs">Mrs.</option>
</select>
<select name="job">
<option selected="selected">Select an option</option>
<option value="employer">Employer</option>
<option value="employee">Employee</option>
</select>
<button type="submit">Submit</button>
<button type="submit" disabled="disabled">Cancel</button>
</form>
<iframe srcdoc="&lt;p&gt;Iframe text content&lt;/p&gt;"></iframe>
</main>
<footer>
<em>Footer</em>
<button type="button">Back to top</button>
</footer>
`;
customElements.define(
"hoot-test-shadow-root",
class ShadowRoot extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
const p = document.createElement("p");
p.textContent = "Shadow content";
const input = document.createElement("input");
shadow.append(p, input);
}
}
);
describe.tags("ui");
describe(parseUrl(import.meta.url), () => {
test.todo("should crash", async () => {
expect().toBeFalsy();
});
test("formatXml", () => {
expect(formatXml("")).toBe("");
expect(formatXml("<input />")).toBe("<input/>");
expect(
formatXml(/* xml */ `
<div>
A
</div>
`)
).toBe(`<div>\n A\n</div>`);
expect(formatXml(/* xml */ `<div>A</div>`)).toBe(`<div>\n A\n</div>`);
// Inline
expect(
formatXml(
/* xml */ `
<div>
A
</div>
`,
{ keepInlineTextNodes: true }
)
).toBe(`<div>\n A\n</div>`);
expect(formatXml(/* xml */ `<div>A</div>`, { keepInlineTextNodes: true })).toBe(
`<div>A</div>`
);
});
test("getActiveElement", async () => {
await mountForTest(/* xml */ `<iframe srcdoc="&lt;input &gt;"></iframe>`);
await waitForIframes();
expect(":iframe input").not.toBeFocused();
const input = queryOne(":iframe input");
await click(input);
expect(":iframe input").toBeFocused();
expect(getActiveElement()).toBe(input);
});
test("getActiveElement: shadow dom", async () => {
await mountForTest(/* xml */ `<hoot-test-shadow-root />`);
expect("hoot-test-shadow-root:shadow input").not.toBeFocused();
const input = queryOne("hoot-test-shadow-root:shadow input");
await click(input);
expect("hoot-test-shadow-root:shadow input").toBeFocused();
expect(getActiveElement()).toBe(input);
});
test("getFocusableElements", async () => {
await mountForTest(/* xml */ `
<input class="input" />
<div class="div" tabindex="0">aaa</div>
<span class="span" tabindex="-1">aaa</span>
<button class="disabled-button" disabled="disabled">Disabled button</button>
<button class="button" tabindex="1">Button</button>
`);
expect(getFocusableElements().map((el) => el.className)).toEqual([
"button",
"span",
"input",
"div",
]);
expect(getFocusableElements({ tabbable: true }).map((el) => el.className)).toEqual([
"button",
"input",
"div",
]);
});
test("getNextFocusableElement", async () => {
await mountForTest(/* xml */ `
<input class="input" />
<div class="div" tabindex="0">aaa</div>
<button class="disabled-button" disabled="disabled">Disabled button</button>
<button class="button" tabindex="1">Button</button>
`);
await click(".input");
expect(getNextFocusableElement()).toHaveClass("div");
});
test("getParentFrame", async () => {
await mountForTest(/* xml */ `
<div class="root"></div>
`);
const parent = await makeIframe(document, queryOne(".root"));
const child = await makeIframe(parent.contentDocument);
const content = child.contentDocument.createElement("div");
child.contentDocument.body.appendChild(content);
expect(getParentFrame(content)).toBe(child);
expect(getParentFrame(child)).toBe(parent);
expect(getParentFrame(parent)).toBe(null);
});
test("getPreviousFocusableElement", async () => {
await mountForTest(/* xml */ `
<input class="input" />
<div class="div" tabindex="0">aaa</div>
<button class="disabled-button" disabled="disabled">Disabled button</button>
<button class="button" tabindex="1">Button</button>
`);
await click(".input");
expect(getPreviousFocusableElement()).toHaveClass("button");
});
test("isEditable", async () => {
expect(isEditable(document.createElement("input"))).toBe(true);
expect(isEditable(document.createElement("textarea"))).toBe(true);
expect(isEditable(document.createElement("select"))).toBe(false);
const editableDiv = document.createElement("div");
expect(isEditable(editableDiv)).toBe(false);
editableDiv.setAttribute("contenteditable", "true");
expect(isEditable(editableDiv)).toBe(false); // not supported
});
test("isFocusable", async () => {
await mountForTest(FULL_HTML_TEMPLATE);
expect(isFocusable("input:first")).toBe(true);
expect(isFocusable("li:first")).toBe(false);
});
test("isInDom", async () => {
await mountForTest(FULL_HTML_TEMPLATE);
await waitForIframes();
expect(isInDOM(document)).toBe(true);
expect(isInDOM(document.body)).toBe(true);
expect(isInDOM(document.head)).toBe(true);
expect(isInDOM(document.documentElement)).toBe(true);
const form = queryOne`form`;
expect(isInDOM(form)).toBe(true);
form.remove();
expect(isInDOM(form)).toBe(false);
const paragraph = queryOne`:iframe p`;
expect(isInDOM(paragraph)).toBe(true);
paragraph.remove();
expect(isInDOM(paragraph)).toBe(false);
});
test("isDisplayed", async () => {
await mountForTest(FULL_HTML_TEMPLATE);
expect(isDisplayed(document)).toBe(true);
expect(isDisplayed(document.body)).toBe(true);
expect(isDisplayed(document.head)).toBe(true);
expect(isDisplayed(document.documentElement)).toBe(true);
expect(isDisplayed("form")).toBe(true);
expect(isDisplayed(".hidden")).toBe(false);
expect(isDisplayed("body")).toBe(false); // not available from fixture
});
test("isVisible", async () => {
await mountForTest(FULL_HTML_TEMPLATE + "<hoot-test-shadow-root />");
expect(isVisible(document)).toBe(true);
expect(isVisible(document.body)).toBe(true);
expect(isVisible(document.head)).toBe(false);
expect(isVisible(document.documentElement)).toBe(true);
expect(isVisible("form")).toBe(true);
expect(isVisible("hoot-test-shadow-root:shadow input")).toBe(true);
expect(isVisible(".hidden")).toBe(false);
expect(isVisible("body")).toBe(false); // not available from fixture
});
test("matchMedia", async () => {
// Invalid syntax
expect(matchMedia("aaaa").matches).toBe(false);
expect(matchMedia("display-mode: browser").matches).toBe(false);
// Does not exist
expect(matchMedia("(a)").matches).toBe(false);
expect(matchMedia("(a: b)").matches).toBe(false);
// Defaults
expect(matchMedia("(display-mode:browser)").matches).toBe(true);
expect(matchMedia("(display-mode: standalone)").matches).toBe(false);
expect(matchMedia("not (display-mode: standalone)").matches).toBe(true);
expect(matchMedia("(prefers-color-scheme :light)").matches).toBe(true);
expect(matchMedia("(prefers-color-scheme : dark)").matches).toBe(false);
expect(matchMedia("not (prefers-color-scheme: dark)").matches).toBe(true);
expect(matchMedia("(prefers-reduced-motion: reduce)").matches).toBe(true);
expect(matchMedia("(prefers-reduced-motion: no-preference)").matches).toBe(false);
// Touch feature
expect(window.matchMedia("(pointer: coarse)").matches).toBe(false);
expect(window.ontouchstart).toBe(undefined);
mockTouch(true);
expect(window.matchMedia("(pointer: coarse)").matches).toBe(true);
expect(window.ontouchstart).not.toBe(undefined);
});
test("waitFor: already in fixture", async () => {
await mountForTest(FULL_HTML_TEMPLATE);
waitFor(".title").then((el) => {
expect.step(el.className);
return el;
});
expect.verifySteps([]);
await animationFrame();
expect.verifySteps(["title"]);
});
test("waitFor: rejects", async () => {
await expect(waitFor("never", { timeout: 1 })).rejects.toThrow(
`Could not find elements matching "never" within 1 milliseconds`
);
});
test("waitFor: add new element", async () => {
const el1 = document.createElement("div");
el1.className = "new-element";
const el2 = document.createElement("div");
el2.className = "new-element";
const promise = waitFor(".new-element").then((el) => {
expect.step(el.className);
return el;
});
await animationFrame();
expect.verifySteps([]);
getFixture().append(el1, el2);
await expect(promise).resolves.toBe(el1);
expect.verifySteps(["new-element"]);
});
test("waitForNone: DOM empty", async () => {
waitForNone(".title").then(() => expect.step("none"));
expect.verifySteps([]);
await animationFrame();
expect.verifySteps(["none"]);
});
test("waitForNone: rejects", async () => {
await mountForTest(FULL_HTML_TEMPLATE);
await expect(waitForNone(".title", { timeout: 1 })).rejects.toThrow();
});
test("waitForNone: delete elements", async () => {
await mountForTest(FULL_HTML_TEMPLATE);
waitForNone(".title").then(() => expect.step("none"));
expect(".title").toHaveCount(3);
for (const title of queryAll(".title")) {
expect.verifySteps([]);
title.remove();
await animationFrame();
}
expect.verifySteps(["none"]);
});
describe("query", () => {
test("native selectors", async () => {
await mountForTest(FULL_HTML_TEMPLATE);
expect(queryAll()).toEqual([]);
for (const selector of [
"main",
`.${"title"}`,
`${"ul"}${" "}${`${"li"}`}`,
".title",
"ul > li",
"form:has(.title:not(.haha)):not(.huhu) input[name='email']:enabled",
"[colspan='1']",
]) {
expectSelector(selector).toEqualNodes(selector);
}
});
test("custom pseudo-classes", async () => {
await mountForTest(FULL_HTML_TEMPLATE);
await waitForIframes();
// :first, :last, :only & :eq
expectSelector(".title:first").toEqualNodes(".title", { index: 0 });
expectSelector(".title:last").toEqualNodes(".title", { index: -1 });
expectSelector(".title:eq(-1)").toEqualNodes(".title", { index: -1 });
expectSelector("main:only").toEqualNodes("main");
expectSelector(".title:only").toEqualNodes("");
expectSelector(".title:eq(1)").toEqualNodes(".title", { index: 1 });
expectSelector(".title:eq('1')").toEqualNodes(".title", { index: 1 });
expectSelector('.title:eq("1")').toEqualNodes(".title", { index: 1 });
// :contains (text)
expectSelector("main > .text:contains(ipsum)").toEqualNodes("p");
expectSelector(".text:contains(/\\bL\\w+\\b\\sipsum/)").toEqualNodes("p");
expectSelector(".text:contains(item)").toEqualNodes("li");
// :contains (value)
expectSelector("input:value(john)").toEqualNodes("[name=name],[name=email]");
expectSelector("input:value(john doe)").toEqualNodes("[name=name]");
expectSelector("input:value('John Doe (JOD)')").toEqualNodes("[name=name]");
expectSelector(`input:value("(JOD)")`).toEqualNodes("[name=name]");
expectSelector("input:value(johndoe)").toEqualNodes("[name=email]");
expectSelector("select:value(mr)").toEqualNodes("[name=title]");
expectSelector("select:value(unknown value)").toEqualNodes("");
// :selected
expectSelector("option:selected").toEqualNodes(
"select[name=title] option[value=mr],select[name=job] option:first-child"
);
// :iframe
expectSelector("iframe p:contains(iframe text content)").toEqualNodes("");
expectSelector("div:iframe p").toEqualNodes("");
expectSelector(":iframe p:contains(iframe text content)").toEqualNodes("p", {
root: "iframe",
});
});
test("advanced use cases", async () => {
await mountForTest(FULL_HTML_TEMPLATE);
// Comma-separated selectors
expectSelector(":has(form:contains('Form title')),p:contains(ipsum)").toEqualNodes(
"p,main"
);
// :has & :not combinations with custom pseudo-classes
expectSelector(`select:has(:contains(Employer))`).toEqualNodes("select[name=job]");
expectSelector(`select:not(:has(:contains(Employer)))`).toEqualNodes(
"select[name=title]"
);
expectSelector(
`main:first-of-type:not(:has(:contains(This text does not exist))):contains('List header') > form:has([name="name"]):contains("Form title"):nth-child(6).overflow-auto:visible select[name=job] option:selected`
).toEqualNodes("select[name=job] option:first-child");
// :contains & commas
expectSelector(`p:contains(velit,)`).toEqualNodes("p");
expectSelector(`p:contains('velit,')`).toEqualNodes("p");
expectSelector(`p:contains(", tristique")`).toEqualNodes("p");
expectSelector(`p:contains(/\\bvelit,/)`).toEqualNodes("p");
});
// Whatever, at this point I'm just copying failing selectors and creating
// fake contexts accordingly as I'm fixing them.
test("comma-separated long selector: no match", async () => {
await mountForTest(/* xml */ `
<div class="o_we_customize_panel">
<we-customizeblock-option class="snippet-option-ImageTools">
<div class="o_we_so_color_palette o_we_widget_opened">
idk
</div>
<we-select data-name="shape_img_opt">
<we-toggler></we-toggler>
</we-select>
</we-customizeblock-option>
</div>
`);
expectSelector(
`.o_we_customize_panel:not(:has(.o_we_so_color_palette.o_we_widget_opened)) we-customizeblock-option[class='snippet-option-ImageTools'] we-select[data-name="shape_img_opt"] we-toggler`,
`.o_we_customize_panel:not(:has(.o_we_so_color_palette.o_we_widget_opened)) we-customizeblock-option[class='snippet-option-ImageTools'] [title='we-select[data-name="shape_img_opt"] we-toggler']`
).toEqualNodes("");
});
test("comma-separated long selector: match first", async () => {
await mountForTest(/* xml */ `
<div class="o_we_customize_panel">
<we-customizeblock-option class="snippet-option-ImageTools">
<we-select data-name="shape_img_opt">
<we-toggler></we-toggler>
</we-select>
</we-customizeblock-option>
</div>
`);
expectSelector(
`.o_we_customize_panel:not(:has(.o_we_so_color_palette.o_we_widget_opened)) we-customizeblock-option[class='snippet-option-ImageTools'] we-select[data-name="shape_img_opt"] we-toggler`,
`.o_we_customize_panel:not(:has(.o_we_so_color_palette.o_we_widget_opened)) we-customizeblock-option[class='snippet-option-ImageTools'] [title='we-select[data-name="shape_img_opt"] we-toggler']`
).toEqualNodes("we-toggler");
});
test("comma-separated long selector: match second", async () => {
await mountForTest(/* xml */ `
<div class="o_we_customize_panel">
<we-customizeblock-option class="snippet-option-ImageTools">
<div title='we-select[data-name="shape_img_opt"] we-toggler'>
idk
</div>
</we-customizeblock-option>
</div>
`);
expectSelector(
`.o_we_customize_panel:not(:has(.o_we_so_color_palette.o_we_widget_opened)) we-customizeblock-option[class='snippet-option-ImageTools'] we-select[data-name="shape_img_opt"] we-toggler`,
`.o_we_customize_panel:not(:has(.o_we_so_color_palette.o_we_widget_opened)) we-customizeblock-option[class='snippet-option-ImageTools'] [title='we-select[data-name="shape_img_opt"] we-toggler']`
).toEqualNodes("div[title]");
});
test("comma-separated :contains", async () => {
await mountForTest(/* xml */ `
<div class="o_menu_sections">
<a class="dropdown-item">Products</a>
</div>
<nav class="o_burger_menu_content">
<ul>
<li data-menu-xmlid="sale.menu_product_template_action">
Products
</li>
</ul>
</nav>
`);
expectSelector(
`.o_menu_sections .dropdown-item:contains('Products'), nav.o_burger_menu_content li[data-menu-xmlid='sale.menu_product_template_action']`
).toEqualNodes(".dropdown-item,li");
});
test(":contains with line return", async () => {
await mountForTest(/* xml */ `
<span>
<div>Matrix (PAV11, PAV22, PAV31)</div>
<div>PA4: PAV41</div>
</span>
`);
expectSelector(
`span:contains("Matrix (PAV11, PAV22, PAV31)\nPA4: PAV41")`
).toEqualNodes("span");
});
test(":has(...):first", async () => {
await mountForTest(/* xml */ `
<a href="/web/event/1"></a>
<a target="" href="/web/event/2">
<span>Conference for Architects TEST</span>
</a>
`);
expectSelector(
`a[href*="/event"]:contains("Conference for Architects TEST")`
).toEqualNodes("[target]");
expectSelector(
`a[href*="/event"]:contains("Conference for Architects TEST"):first`
).toEqualNodes("[target]");
});
test(":eq", async () => {
await mountForTest(/* xml */ `
<ul>
<li>a</li>
<li>b</li>
<li>c</li>
</ul>
`);
expectSelector(`li:first:contains(a)`).toEqualNodes("li:nth-child(1)");
expectSelector(`li:contains(a):first`).toEqualNodes("li:nth-child(1)");
expectSelector(`li:first:contains(b)`).toEqualNodes("");
expectSelector(`li:contains(b):first`).toEqualNodes("li:nth-child(2)");
});
test(":empty", async () => {
await mountForTest(/* xml */ `
<input class="empty" />
<input class="value" value="value" />
`);
expectSelector(`input:empty`).toEqualNodes(".empty");
expectSelector(`input:not(:empty)`).toEqualNodes(".value");
});
test("regular :contains", async () => {
await mountForTest(/* xml */ `
<div class="website_links_click_chart">
<div class="title">
0 clicks
</div>
<div class="title">
1 clicks
</div>
<div class="title">
2 clicks
</div>
</div>
`);
expectSelector(`.website_links_click_chart .title:contains("1 clicks")`).toEqualNodes(
".title:nth-child(2)"
);
});
test("other regular :contains", async () => {
await mountForTest(/* xml */ `
<ul
class="o-autocomplete--dropdown-menu ui-widget show dropdown-menu ui-autocomplete"
style="position: fixed; top: 283.75px; left: 168.938px"
>
<li class="o-autocomplete--dropdown-item ui-menu-item block">
<a
href="#"
class="dropdown-item ui-menu-item-wrapper truncate ui-state-active"
>Account Tax Group Partner</a
>
</li>
<li
class="o-autocomplete--dropdown-item ui-menu-item block o_m2o_dropdown_option o_m2o_dropdown_option_search_more"
>
<a href="#" class="dropdown-item ui-menu-item-wrapper truncate"
>Search More...</a
>
</li>
<li
class="o-autocomplete--dropdown-item ui-menu-item block o_m2o_dropdown_option o_m2o_dropdown_option_create_edit"
>
<a href="#" class="dropdown-item ui-menu-item-wrapper truncate"
>Create and edit...</a
>
</li>
</ul>
`);
expectSelector(`.ui-menu-item a:contains("Account Tax Group Partner")`).toEqualNodes(
"ul li:first-child a"
);
});
test(":iframe", async () => {
await mountForTest(/* xml */ `
<iframe srcdoc="&lt;p&gt;Iframe text content&lt;/p&gt;"></iframe>
`);
await waitForIframes();
expectSelector(`:iframe html`).toEqualNodes("html", { root: "iframe" });
expectSelector(`:iframe body`).toEqualNodes("body", { root: "iframe" });
expectSelector(`:iframe head`).toEqualNodes("head", { root: "iframe" });
});
test(":contains with brackets", async () => {
await mountForTest(/* xml */ `
<div class="o_content">
<div class="o_field_widget" name="messages">
<table class="o_list_view table table-sm table-hover table-striped o_list_view_ungrouped">
<tbody>
<tr class="o_data_row">
<td class="o_list_record_selector">
bbb
</td>
<td class="o_data_cell o_required_modifier">
<span>
[test_trigger] Mitchell Admin
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
`);
expectSelector(
`.o_content:has(.o_field_widget[name=messages]):has(td:contains(/^bbb$/)):has(td:contains(/^\\[test_trigger\\] Mitchell Admin$/))`
).toEqualNodes(".o_content");
});
test(":eq in the middle of a selector", async () => {
await mountForTest(/* xml */ `
<ul>
<li class="oe_overlay o_draggable"></li>
<li class="oe_overlay o_draggable"></li>
<li class="oe_overlay o_draggable oe_active"></li>
<li class="oe_overlay o_draggable"></li>
</ul>
`);
expectSelector(`.oe_overlay.o_draggable:eq(2).oe_active`).toEqualNodes(
"li:nth-child(3)"
);
});
test("combinator +", async () => {
await mountForTest(/* xml */ `
<form class="js_attributes">
<input type="checkbox" />
<label>Steel - Test</label>
</form>
`);
expectSelector(
`form.js_attributes input:not(:checked) + label:contains(Steel - Test)`
).toEqualNodes("label");
});
test("multiple + combinators", async () => {
await mountForTest(/* xml */ `
<div class="s_cover">
<span class="o_text_highlight">
<span class="o_text_highlight_item">
<span class="o_text_highlight_path_underline" />
</span>
<br />
<span class="o_text_highlight_item">
<span class="o_text_highlight_path_underline" />
</span>
</span>
</div>
`);
expectSelector(`
.s_cover span.o_text_highlight:has(
.o_text_highlight_item
+ br
+ .o_text_highlight_item
)
`).toEqualNodes(".o_text_highlight");
});
test(":last", async () => {
await mountForTest(/* xml */ `
<div class="o_field_widget" name="messages">
<table class="o_list_view table table-sm table-hover table-striped o_list_view_ungrouped">
<tbody>
<tr class="o_data_row">
<td class="o_list_record_remove">
<button class="btn">Remove</button>
</td>
</tr>
<tr class="o_data_row">
<td class="o_list_record_remove">
<button class="btn">Remove</button>
</td>
</tr>
</tbody>
</table>
</div>
`);
expectSelector(
`.o_field_widget[name=messages] .o_data_row td.o_list_record_remove button:visible:last`
).toEqualNodes(".o_data_row:last-child button");
});
test("select :contains & :value", async () => {
await mountForTest(/* xml */ `
<select class="configurator_select form-select form-select-lg">
<option value="217" selected="">Metal</option>
<option value="218">Wood</option>
</select>
`);
expectSelector(`.configurator_select:has(option:contains(Metal))`).toEqualNodes(
"select"
);
expectSelector(`.configurator_select:has(option:value(217))`).toEqualNodes("select");
expectSelector(`.configurator_select:has(option:value(218))`).toEqualNodes("select");
expectSelector(`.configurator_select:value(217)`).toEqualNodes("select");
expectSelector(`.configurator_select:value(218)`).toEqualNodes("");
expectSelector(`.configurator_select:value(Metal)`).toEqualNodes("");
});
test("invalid selectors", async () => {
await mountForTest(FULL_HTML_TEMPLATE);
expect(() => queryAll`[colspan=1]`).toThrow(); // missing quotes
expect(() => queryAll`[href=/]`).toThrow(); // missing quotes
expect(
() =>
queryAll`_o_wblog_posts_loop:has(span:has(i.fa-calendar-o):has(a[href="/blog?search=a"])):has(span:has(i.fa-search):has(a[href^="/blog?date_begin"]))`
).toThrow(); // nested :has statements
});
test("queryAllRects", async () => {
await mountForTest(/* xml */ `
<div style="width: 40px; height: 60px;" />
<div style="width: 20px; height: 10px;" />
`);
expect(queryAllRects("div")).toEqual(
queryAll("div").map((el) => el.getBoundingClientRect())
);
expect(queryAllRects("div:first")).toEqual([new DOMRect({ width: 40, height: 60 })]);
expect(queryAllRects("div:last")).toEqual([new DOMRect({ width: 20, height: 10 })]);
});
test("queryAllTexts", async () => {
await mountForTest(FULL_HTML_TEMPLATE);
expect(queryAllTexts(".title")).toEqual(["Title", "List header", "Form title"]);
expect(queryAllTexts("footer")).toEqual(["FooterBack to top"]);
});
test("queryOne", async () => {
await mountForTest(FULL_HTML_TEMPLATE);
expect(queryOne(".title:first")).toBe(getFixture().querySelector("header .title"));
expect(() => queryOne(".title")).toThrow();
expect(() => queryOne(".title", { exact: 2 })).toThrow();
});
test("queryRect", async () => {
await mountForTest(/* xml */ `
<div class="container">
<div class="rect" style="width: 40px; height: 60px;" />
</div>
`);
expect(".rect").toHaveRect(".container"); // same rect as parent
expect(".rect").toHaveRect({ width: 40, height: 60 });
expect(queryRect(".rect")).toEqual(queryOne(".rect").getBoundingClientRect());
expect(queryRect(".rect")).toEqual(new DOMRect({ width: 40, height: 60 }));
});
test("queryRect with trimPadding", async () => {
await mountForTest(/* xml */ `
<div style="width: 50px; height: 70px; padding: 5px; margin: 6px" />
`);
expect("div").toHaveRect({ width: 50, height: 70 }); // with padding
expect("div").toHaveRect({ width: 40, height: 60 }, { trimPadding: true });
});
test.skip("performance against jQuery", async () => {
const jQuery = globalThis.$;
const time = (fn) => {
const start = performance.now();
fn();
return Number((performance.now() - start).toFixed(3));
};
const testCases = [
[
FULL_HTML_TEMPLATE,
`main:first-of-type:not(:has(:contains(This text does not exist))):contains('List header') > form:has([name="name"]):contains("Form title"):nth-child(6).overflow-auto:visible select[name=job] option:selected`,
],
[
/* html */ `
<div class="o_we_customize_panel">
<we-customizeblock-option class="snippet-option-ImageTools">
<we-select data-name="shape_img_opt">
<we-toggler></we-toggler>
</we-select>
</we-customizeblock-option>
</div>
`,
`.o_we_customize_panel:not(:has(.o_we_so_color_palette.o_we_widget_opened)) we-customizeblock-option[class='snippet-option-ImageTools'] we-select[data-name="shape_img_opt"] we-toggler`,
],
];
for (const [template, selector] of testCases) {
const jQueryTimes = [];
const queryAllTimes = [];
for (let i = 0; i < 100; i++) {
mountForTest(template);
jQueryTimes.push(time(() => jQuery(selector)));
queryAllTimes.push(time(() => queryAll(selector)));
}
const jQueryAvg = jQueryTimes.reduce((a, b) => a + b, 0) / jQueryTimes.length;
const queryAllAvg = queryAllTimes.reduce((a, b) => a + b, 0) / queryAllTimes.length;
expect(queryAllAvg).toBeLessThan(jQueryAvg * 1.25); // 25% margin
}
});
});
});