19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -228,6 +228,20 @@ describe(parseUrl(import.meta.url), () => {
expect(testResult.events.map(({ label }) => label)).toEqual(matchers.map(([name]) => name));
});
test("'expect' error handling", async () => {
const [customExpect, hooks] = makeExpect({ headless: true });
hooks.before();
expect(() => customExpect(undefined).toInclude("3")).toThrow(
"expected received value to be of type string, any[] or object, got undefined"
);
const testResult = hooks.after();
expect(testResult.pass).toBe(false);
});
test("assertions are prevented after an error", async () => {
const [customExpect, hooks] = makeExpect({ headless: true });
@ -414,7 +428,12 @@ describe(parseUrl(import.meta.url), () => {
});
test("verifyErrors", async () => {
expect.assertions(1);
expect.assertions(2);
expect(() => expect.verifyErrors(["event", "promise", "timeout"])).toThrow(
"cannot call `expect.verifyErrors()` without calling `expect.errors()` beforehand"
);
expect.errors(3);
const boom = (msg) => {

View file

@ -1,11 +1,13 @@
/** @odoo-module */
import { describe, expect, getFixture, test } from "@odoo/hoot";
import {
animationFrame,
click,
describe,
expect,
formatXml,
getActiveElement,
getFixture,
getFocusableElements,
getNextFocusableElement,
getPreviousFocusableElement,
@ -14,16 +16,17 @@ import {
isFocusable,
isInDOM,
isVisible,
mockTouch,
queryAll,
queryAllRects,
queryAllTexts,
queryFirst,
queryOne,
queryRect,
test,
waitFor,
waitForNone,
} from "@odoo/hoot-dom";
import { mockTouch } from "@odoo/hoot-mock";
} from "@odoo/hoot";
import { getParentFrame } from "@web/../lib/hoot-dom/helpers/dom";
import { mountForTest, parseUrl } from "../local_helpers";
@ -466,12 +469,16 @@ describe(parseUrl(import.meta.url), () => {
expectSelector(".title:eq('1')").toEqualNodes(".title", { index: 1 });
expectSelector('.title:eq("1")').toEqualNodes(".title", { index: 1 });
// :contains (text)
// :contains
expectSelector("main > .text:contains(ipsum)").toEqualNodes("p");
expectSelector(".text:contains(/\\bL\\w+\\b\\sipsum/)").toEqualNodes("p");
expectSelector(".text:contains(item)").toEqualNodes("li");
// :contains (value)
// :text
expectSelector(".text:text(item)").toEqualNodes("");
expectSelector(".text:text(first item)").toEqualNodes("li:first-of-type");
// :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]");
@ -493,6 +500,17 @@ describe(parseUrl(import.meta.url), () => {
});
});
test("query options", async () => {
await mountForTest(FULL_HTML_TEMPLATE);
expect($$("input", { count: 2 })).toHaveLength(2);
expect(() => $$("input", { count: 1 })).toThrow();
expect($$("option", { count: 6 })).toHaveLength(6);
expect($$("option", { count: 3, root: "[name=title]" })).toHaveLength(3);
expect(() => $$("option", { count: 6, root: "[name=title]" })).toThrow();
});
test("advanced use cases", async () => {
await mountForTest(FULL_HTML_TEMPLATE);
@ -596,9 +614,9 @@ describe(parseUrl(import.meta.url), () => {
<div>PA4: PAV41</div>
</span>
`);
expectSelector(
`span:contains("Matrix (PAV11, PAV22, PAV31)\nPA4: PAV41")`
).toEqualNodes("span");
expectSelector(`span:contains("Matrix (PAV11, PAV22, PAV31) PA4: PAV41")`).toEqualNodes(
"span"
);
});
test(":has(...):first", async () => {
@ -730,7 +748,7 @@ describe(parseUrl(import.meta.url), () => {
`);
expectSelector(
`.o_content:has(.o_field_widget[name=messages]):has(td:contains(/^bbb$/)):has(td:contains(/^\\[test_trigger\\] Mitchell Admin$/))`
`.o_content:has(.o_field_widget[name=messages]):has(td:text(bbb)):has(td:contains(/^\\[test_trigger\\] Mitchell Admin/))`
).toEqualNodes(".o_content");
});
@ -861,7 +879,7 @@ describe(parseUrl(import.meta.url), () => {
expect($1(".title:first")).toBe(getFixture().querySelector("header .title"));
expect(() => $1(".title")).toThrow();
expect(() => $1(".title", { exact: 2 })).toThrow();
expect(() => $1(".title", { count: 2 })).toThrow();
});
test("queryRect", async () => {
@ -899,10 +917,10 @@ describe(parseUrl(import.meta.url), () => {
// queryOne error messages
expect(() => $1()).toThrow(`found 0 elements instead of 1`);
expect(() => $$([], { exact: 18 })).toThrow(`found 0 elements instead of 18`);
expect(() => $$([], { count: 18 })).toThrow(`found 0 elements instead of 18`);
expect(() => $1("")).toThrow(`found 0 elements instead of 1: 0 matching ""`);
expect(() => $$(".tralalero", { exact: -20 })).toThrow(
`found 1 element instead of -20: 1 matching ".tralalero"`
expect(() => $$(".tralalero", { count: -20 })).toThrow(
`invalid 'count' option: should be a positive integer`
);
expect(() => $1`.tralalero:contains(Tralala):visible:scrollable:first`).toThrow(
`found 0 elements instead of 1: 0 matching ".tralalero:contains(Tralala):visible:scrollable:first" (1 element with text "Tralala" > 1 visible element > 0 scrollable elements)`

View file

@ -1,20 +1,26 @@
/** @odoo-module */
import { after, describe, expect, getFixture, test } from "@odoo/hoot";
import {
advanceTime,
after,
animationFrame,
clear,
click,
dblclick,
describe,
drag,
edit,
expect,
fill,
getFixture,
hover,
keyDown,
keyUp,
leave,
middleClick,
mockFetch,
mockTouch,
mockUserAgent,
on,
pointerDown,
pointerUp,
@ -26,9 +32,9 @@ import {
select,
setInputFiles,
setInputRange,
test,
uncheck,
} from "@odoo/hoot-dom";
import { mockFetch, mockTouch, mockUserAgent } from "@odoo/hoot-mock";
} from "@odoo/hoot";
import { Component, xml } from "@odoo/owl";
import { EventList } from "@web/../lib/hoot-dom/helpers/events";
import { mountForTest, parseUrl } from "../local_helpers";
@ -1230,8 +1236,8 @@ describe(parseUrl(import.meta.url), () => {
"pointerleave:0@input",
"mouseleave:0@input",
// Change
"blur@input",
"change@input",
"blur@input",
"focusout@input",
]);
});
@ -1428,6 +1434,24 @@ describe(parseUrl(import.meta.url), () => {
expect("input").toHaveValue(/file\.txt/);
});
test("setInputFiles: shadow root", async () => {
await mountForTest(/* xml */ `
<div class="container" />
`);
const shadow = queryOne(".container").attachShadow({
mode: "open",
});
const input = document.createElement("input");
input.type = "file";
shadow.appendChild(input);
await click(".container:shadow input");
await setInputFiles(new File([""], "file.txt"));
expect(".container:shadow input").toHaveValue(/file\.txt/);
});
test("setInputRange: basic case and events", async () => {
await mountForTest(/* xml */ `<input type="range" min="10" max="40" />`);
@ -1735,6 +1759,8 @@ describe(parseUrl(import.meta.url), () => {
"focus@input",
"focusin@input",
"focusin@form",
"keyup:Tab@input",
"keyup:Tab@form",
// Enter
"keydown:Enter@input",
"keydown:Enter@form",
@ -1766,6 +1792,8 @@ describe(parseUrl(import.meta.url), () => {
"focus@button",
"focusin@button",
"focusin@form",
"keyup:Tab@button",
"keyup:Tab@form",
// Enter
"keydown:Enter@button",
"keydown:Enter@form",
@ -1798,6 +1826,8 @@ describe(parseUrl(import.meta.url), () => {
"focus@button",
"focusin@button",
"focusin@form",
"keyup:Tab@button",
"keyup:Tab@form",
// Enter
"keydown:Enter@button",
"keydown:Enter@form",
@ -1817,10 +1847,11 @@ describe(parseUrl(import.meta.url), () => {
</form>
`);
mockFetch((url, { body, method }) => {
mockFetch((url, { body, headers, method }) => {
expect.step(new URL(url).pathname);
expect(method).toBe("post");
expect(method).toBe("POST");
expect(headers).toEqual(new Headers([["Content-Type", "multipart/form-data"]]));
expect(body).toBeInstanceOf(FormData);
expect(body.get("csrf_token")).toBe("CSRF_TOKEN_VALUE");
expect(body.get("name")).toBe("Pierre");

View file

@ -1,15 +1,17 @@
/** @odoo-module */
import { describe, expect, test } from "@odoo/hoot";
import {
Deferred,
advanceTime,
animationFrame,
describe,
expect,
microTick,
runAllTimers,
test,
tick,
waitUntil,
} from "@odoo/hoot-dom";
} from "@odoo/hoot";
import { parseUrl } from "../local_helpers";
// timeout of 1 second to ensure all timeouts are actually mocked

View file

@ -4,6 +4,7 @@ import { describe, expect, test } from "@odoo/hoot";
import { queryOne } from "@odoo/hoot-dom";
import { isInstanceOf, isIterable } from "@web/../lib/hoot-dom/hoot_dom_utils";
import {
deepCopy,
deepEqual,
formatHumanReadable,
formatTechnical,
@ -12,16 +13,30 @@ import {
lookup,
match,
parseQuery,
S_CIRCULAR,
title,
toExplicitString,
} from "../hoot_utils";
import { mountForTest, parseUrl } from "./local_helpers";
describe(parseUrl(import.meta.url), () => {
test("deepEqual", () => {
const recursive = {};
recursive.self = recursive;
const recursive = {};
recursive.self = recursive;
describe(parseUrl(import.meta.url), () => {
test("deepCopy", () => {
expect(deepCopy(true)).toEqual(true);
expect(deepCopy(false)).toEqual(false);
expect(deepCopy(null)).toEqual(null);
expect(deepCopy(recursive)).toEqual({ self: S_CIRCULAR });
expect(deepCopy(new Date(0))).toEqual(new Date(0));
expect(deepCopy({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 });
expect(deepCopy({ o: { a: [{ b: 1 }] } })).toEqual({ o: { a: [{ b: 1 }] } });
expect(deepCopy(Symbol.for("a"))).toEqual(Symbol.for("a"));
expect(deepCopy(document.createElement("div"))).toEqual(document.createElement("div"));
expect(deepCopy([1, 2, 3])).toEqual([1, 2, 3]);
});
test("deepEqual", () => {
const TRUTHY_CASES = [
[true, true],
[false, false],

View file

@ -10,7 +10,6 @@
{
"imports": {
"@odoo/hoot-dom": "/web/static/lib/hoot-dom/hoot-dom.js",
"@odoo/hoot-mock": "/web/static/lib/hoot/hoot-mock.js",
"@odoo/hoot": "/web/static/lib/hoot/hoot.js",
"@odoo/owl": "/web/static/lib/hoot/tests/hoot-owl-module.js",
"@web/../lib/hoot-dom/helpers/dom": "/web/static/lib/hoot-dom/helpers/dom.js",
@ -34,7 +33,6 @@
"/web/static/lib/hoot/core/test": "/web/static/lib/hoot/core/test.js",
"/web/static/lib/hoot/core/url": "/web/static/lib/hoot/core/url.js",
"/web/static/lib/hoot/hoot_utils": "/web/static/lib/hoot/hoot_utils.js",
"/web/static/lib/hoot/hoot-mock": "/web/static/lib/hoot/hoot-mock.js",
"/web/static/lib/hoot/hoot": "/web/static/lib/hoot/hoot.js",
"/web/static/lib/hoot/lib/diff_match_patch": "/web/static/lib/hoot/lib/diff_match_patch.js",
"/web/static/lib/hoot/main_runner": "/web/static/lib/hoot/main_runner.js",
@ -47,7 +45,6 @@
"/web/static/lib/hoot/mock/network": "/web/static/lib/hoot/mock/network.js",
"/web/static/lib/hoot/mock/notification": "/web/static/lib/hoot/mock/notification.js",
"/web/static/lib/hoot/mock/storage": "/web/static/lib/hoot/mock/storage.js",
"/web/static/lib/hoot/mock/sync_values": "/web/static/lib/hoot/mock/sync_values.js",
"/web/static/lib/hoot/mock/window": "/web/static/lib/hoot/mock/window.js",
"/web/static/lib/hoot/tests/local_helpers": "/web/static/lib/hoot/tests/local_helpers.js",
"/web/static/lib/hoot/ui/hoot_buttons": "/web/static/lib/hoot/ui/hoot_buttons.js",
@ -85,7 +82,6 @@
<!-- Test assets -->
<script src="/web/static/lib/owl/owl.js"></script>
<script src="../hoot.js" type="module" defer></script>
<link rel="stylesheet" href="/web/static/lib/hoot/ui/hoot_style.css" />
<link rel="stylesheet" href="/web/static/src/libs/fontawesome/css/font-awesome.css" />

View file

@ -1,7 +1,6 @@
/** @odoo-module */
import { describe, expect, test } from "@odoo/hoot";
import { mockSendBeacon, mockTouch, mockVibrate } from "@odoo/hoot-mock";
import { describe, expect, mockSendBeacon, mockTouch, mockVibrate, test } from "@odoo/hoot";
import { parseUrl } from "../local_helpers";
/**

View file

@ -1,9 +1,17 @@
/** @odoo-module */
import { describe, expect, test } from "@odoo/hoot";
import { mockFetch } from "@odoo/hoot-mock";
import { after, describe, expect, mockFetch, test } from "@odoo/hoot";
import { parseUrl } from "../local_helpers";
/**
* @param {Blob | MediaSource} obj
*/
function createObjectURL(obj) {
const url = URL.createObjectURL(obj);
after(() => URL.revokeObjectURL(url));
return url;
}
describe(parseUrl(import.meta.url), () => {
test("setup network values", async () => {
expect(document.cookie).toBe("");
@ -20,13 +28,170 @@ describe(parseUrl(import.meta.url), () => {
expect(document.title).toBe("");
});
test("fetch should not mock internal URLs", async () => {
test("fetch with internal URLs works without mocking fetch", async () => {
const blob = new Blob([JSON.stringify({ name: "coucou" })], {
type: "application/json",
});
const blobUrl = createObjectURL(blob);
const blobResponse = await fetch(blobUrl).then((res) => res.json());
const dataResponse = await fetch("data:text/html,<body></body>").then((res) => res.text());
expect(blobResponse).toEqual({ name: "coucou" });
expect(dataResponse).toBe("<body></body>");
await expect(fetch("http://some.url")).rejects.toThrow(/fetch is not mocked/);
});
test("fetch with internal URLs should return default value", async () => {
mockFetch(expect.step);
await fetch("http://some.url");
await fetch("/odoo");
await fetch(URL.createObjectURL(new Blob([""])));
const external = await fetch("http://some.url").then((res) => res.text());
const internal = await fetch("/odoo").then((res) => res.text());
const data = await fetch("data:text/html,<body></body>").then((res) => res.text());
expect.verifySteps(["http://some.url", "/odoo"]);
expect(external).toBe("null");
expect(internal).toBe("null");
expect(data).toBe("<body></body>");
expect.verifySteps(["http://some.url", "/odoo", "data:text/html,<body></body>"]);
});
test("fetch JSON with blob URLs", async () => {
mockFetch(expect.step);
const blob = new Blob([JSON.stringify({ name: "coucou" })], {
type: "application/json",
});
const blobUrl = createObjectURL(blob);
const response = await fetch(blobUrl);
const json = await response.json();
expect(json).toEqual({ name: "coucou" });
expect.verifySteps([blobUrl]);
});
test("fetch with mocked blob URLs", async () => {
mockFetch((input) => {
expect.step(input);
return "Some other content";
});
const blob = new Blob([JSON.stringify({ name: "coucou" })], {
type: "application/json",
});
const blobUrl = createObjectURL(blob);
const response = await fetch(blobUrl);
expect(response.headers).toEqual(new Headers([["Content-Type", "text/plain"]]));
const text = await response.text();
expect(text).toBe("Some other content");
expect.verifySteps([blobUrl]);
});
test("mock response with nested blobs", async () => {
mockFetch(
() =>
new Blob(["some blob", new Blob([" with nested content"], { type: "text/plain" })])
);
const response = await fetch("/nestedBlob");
const blob = await response.blob();
const result = await blob.text();
expect(result).toBe("some blob with nested content");
});
test("mock responses: array buffer", async () => {
mockFetch(() => "some text");
const response = await fetch("/arrayBuffer");
const result = await response.arrayBuffer();
expect(result).toBeInstanceOf(ArrayBuffer);
expect(new TextDecoder("utf-8").decode(result)).toBe("some text");
});
test("mock responses: blob", async () => {
mockFetch(() => "blob content");
const response = await fetch("/blob");
const result = await response.blob();
expect(result).toBeInstanceOf(Blob);
expect(result.size).toBe(12);
const buffer = await result.arrayBuffer();
expect(new TextDecoder("utf-8").decode(buffer)).toBe("blob content");
});
test("mock responses: bytes", async () => {
mockFetch(() => "some text");
const response = await fetch("/bytes");
const result = await response.bytes();
expect(result).toBeInstanceOf(Uint8Array);
expect(new TextDecoder("utf-8").decode(result)).toBe("some text");
});
test("mock responses: formData", async () => {
mockFetch(() => {
const data = new FormData();
data.append("name", "Frodo");
return data;
});
const response = await fetch("/formData");
const result = await response.formData();
expect(result).toBeInstanceOf(FormData);
expect(result.get("name")).toBe("Frodo");
});
test("mock responses: json", async () => {
mockFetch(() => ({ json: "content" }));
const response = await fetch("/json");
const result = await response.json();
expect(result).toEqual({ json: "content" });
});
test("mock responses: text", async () => {
mockFetch(() => "some text");
const response = await fetch("/text");
const result = await response.text();
expect(result).toBe("some text");
});
test("mock responses: error handling after reading body", async () => {
mockFetch(() => "some text");
const response = await fetch("/text");
const responseClone = response.clone();
const result = await response.text(); // read once
expect(result).toBe("some text");
// Rejects for every reader after body is used
await expect(response.arrayBuffer()).rejects.toThrow(TypeError);
await expect(response.blob()).rejects.toThrow(TypeError);
await expect(response.bytes()).rejects.toThrow(TypeError);
await expect(response.formData()).rejects.toThrow(TypeError);
await expect(response.json()).rejects.toThrow(TypeError);
await expect(response.text()).rejects.toThrow(TypeError);
const cloneResult = await responseClone.text(); // read clone
expect(cloneResult).toBe(result);
// Clone rejects reader as well
await expect(responseClone.text()).rejects.toThrow(TypeError);
});
});

View file

@ -1,10 +1,9 @@
/** @odoo-module */
import { after, describe, expect, test } from "@odoo/hoot";
import { after, describe, expect, test, watchListeners } from "@odoo/hoot";
import { queryOne } from "@odoo/hoot-dom";
import { EventBus } from "@odoo/owl";
import { mountForTest, parseUrl } from "../local_helpers";
import { watchListeners } from "@odoo/hoot-mock";
describe(parseUrl(import.meta.url), () => {
class TestBus extends EventBus {