/** @odoo-module */
import {
advanceTime,
after,
animationFrame,
clear,
click,
dblclick,
describe,
drag,
edit,
expect,
fill,
getFixture,
hover,
keyDown,
keyUp,
leave,
middleClick,
mockFetch,
mockTouch,
mockUserAgent,
on,
pointerDown,
pointerUp,
press,
queryOne,
resize,
rightClick,
scroll,
select,
setInputFiles,
setInputRange,
test,
uncheck,
} from "@odoo/hoot";
import { Component, xml } from "@odoo/owl";
import { EventList } from "@web/../lib/hoot-dom/helpers/events";
import { mountForTest, parseUrl } from "../local_helpers";
/**
* @param {Event} ev
*/
const formatEvent = (ev) => {
const { currentTarget, type } = ev;
const id = currentTarget.id ? `#${currentTarget.id}` : currentTarget.tagName.toLowerCase();
let formatted = "";
// Mouse
if (ev.button >= 0) {
formatted += `:${ev.button}`;
}
if (ev.buttons) {
formatted += `(${ev.buttons})`;
}
// Keyboard
if (ev.key) {
formatted += `:${ev.key}`;
}
if (ev.altKey) {
formatted += `.alt`;
}
if (ev.ctrlKey) {
formatted += `.ctrl`;
}
if (ev.metaKey) {
formatted += `.meta`;
}
if (ev.shiftKey) {
formatted += `.shift`;
}
// Input
if (ev.data) {
formatted += `:${ev.data}`;
}
return `${type}${formatted}@${id}`;
};
/**
* @param {import("../../helpers/dom").Target} target
* @param {(ev: Event) => string} [formatStep]
*/
const monitorEvents = (target, formatStep) => {
const handleEvent = (element, type) =>
after(
on(element, type, (ev) => {
const formattedStep = formatStep(ev);
if (formattedStep) {
expect.step(formattedStep);
}
})
);
formatStep ||= formatEvent;
for (const element of document.querySelectorAll(target)) {
for (const prop in element) {
const type = prop.match(/^on(\w+)/)?.[1];
if (!type || BLACK_LISTED_EVENT_TYPES.includes(type)) {
continue;
}
handleEvent(element, type);
}
for (const type of ADDITIONAL_EVENT_TYPES) {
handleEvent(element, type);
}
}
};
const ADDITIONAL_EVENT_TYPES = ["focusin", "focusout"];
const BLACK_LISTED_EVENT_TYPES = ["selectionchange"];
describe(parseUrl(import.meta.url), () => {
test("clear", async () => {
await mountForTest(/* xml */ ``);
expect("input").toHaveValue("Test");
expect.verifySteps([]);
await click("input");
monitorEvents("input");
await clear({ delay: 0 });
expect("input").not.toHaveValue();
expect.verifySteps([
"keydown:a.ctrl@input",
"select@input",
"keyup:a.ctrl@input",
"keydown:Backspace@input",
"beforeinput@input",
"input@input",
"keyup:Backspace@input",
]);
});
test("clear: email", async () => {
await mountForTest(/* xml */ ``);
expect("input").toHaveValue("john@doe.com");
await click("input");
await clear();
expect("input").toHaveValue("");
});
test("clear: number", async () => {
await mountForTest(/* xml */ ``);
expect("input").toHaveValue(421);
await click("input");
await clear();
expect("input").not.toHaveValue();
});
test("clear: files", async () => {
await mountForTest(/* xml */ ``);
const file = new File([""], "file.txt");
expect("input").not.toHaveValue();
await click("input");
await fill(file);
expect("input").toHaveValue([file]);
await clear();
expect("input").not.toHaveValue();
});
test("click", async () => {
mockTouch(false);
await mountForTest(/* xml */ ``);
monitorEvents("button");
const events = await click("button");
const clickEvent = events.get("click");
expect(clickEvent.pointerId).toBeGreaterThan(0);
expect(clickEvent.pointerType).toBe("mouse");
expect.verifySteps([
// Hover
"pointerover:0@button",
"mouseover:0@button",
"pointerenter:0@button",
"mouseenter:0@button",
"pointermove:0@button",
"mousemove:0@button",
// Click
"pointerdown:0(1)@button",
"mousedown:0(1)@button",
"focus@button",
"focusin@button",
"pointerup:0@button",
"mouseup:0@button",
"click:0@button",
]);
});
test("dblclick", async () => {
await mountForTest(/* xml */ ``);
monitorEvents("button");
await dblclick("button");
expect.verifySteps([
// Hover
"pointerover:0@button",
"mouseover:0@button",
"pointerenter:0@button",
"mouseenter:0@button",
"pointermove:0@button",
"mousemove:0@button",
// Click 1
"pointerdown:0(1)@button",
"mousedown:0(1)@button",
"focus@button",
"focusin@button",
"pointerup:0@button",
"mouseup:0@button",
"click:0@button",
// Click 2
"pointerdown:0(1)@button",
"mousedown:0(1)@button",
"pointerup:0@button",
"mouseup:0@button",
"click:0@button",
// Double click event
"dblclick:0@button",
]);
});
test("triple click", async () => {
await mountForTest(/* xml */ ``);
const allEvents = new EventList(
// trigger 3 clicks
await click("button"),
await click("button"),
await click("button")
);
const clickEvents = allEvents.getAll("click");
const mouseDownEvents = allEvents.getAll("mousedown");
const mouseUpEvents = allEvents.getAll("mouseup");
const pointerDownEvents = allEvents.getAll("pointerdown");
const pointerUpEvents = allEvents.getAll("pointerup");
expect(pointerDownEvents).toHaveLength(3);
expect(pointerDownEvents[0].detail).toBe(0);
expect(pointerDownEvents[1].detail).toBe(0);
expect(pointerDownEvents[2].detail).toBe(0);
expect(mouseDownEvents).toHaveLength(3);
expect(mouseDownEvents[0].detail).toBe(1);
expect(mouseDownEvents[1].detail).toBe(2);
expect(mouseDownEvents[2].detail).toBe(3);
expect(pointerUpEvents).toHaveLength(3);
expect(pointerUpEvents[0].detail).toBe(0);
expect(pointerUpEvents[1].detail).toBe(0);
expect(pointerUpEvents[2].detail).toBe(0);
expect(mouseUpEvents).toHaveLength(3);
expect(mouseUpEvents[0].detail).toBe(1);
expect(mouseUpEvents[1].detail).toBe(2);
expect(mouseUpEvents[2].detail).toBe(3);
expect(clickEvents).toHaveLength(3);
expect(clickEvents[0].detail).toBe(1);
expect(clickEvents[1].detail).toBe(2);
expect(clickEvents[2].detail).toBe(3);
expect(allEvents.getAll("dblclick")).toHaveLength(1);
await advanceTime(1_000);
const events = await click("button");
expect(events.get("click").detail).toBe(1);
});
test("auxclick", async () => {
mockTouch(false);
await mountForTest(/* xml */ ``);
await hover("button");
monitorEvents("button");
await middleClick("button");
expect.verifySteps([
"pointerdown:1(4)@button",
"mousedown:1(4)@button",
"focus@button",
"focusin@button",
"pointerup:1@button",
"mouseup:1@button",
"auxclick:1@button",
]);
await rightClick("button");
expect.verifySteps([
"pointerdown:2(2)@button",
"mousedown:2(2)@button",
"contextmenu:2(2)@button",
"pointerup:2@button",
"mouseup:2@button",
"auxclick:2@button",
]);
});
test("click on disabled element", async () => {
await mountForTest(/* xml */ ``);
monitorEvents("button");
await click("button");
expect.verifySteps([
// Hover
"pointerover:0@button",
"mouseover:0@button",
"pointerenter:0@button",
"mouseenter:0@button",
"pointermove:0@button",
"mousemove:0@button",
// Click (mouse events disabled)
"pointerdown:0(1)@button",
"pointerup:0@button",
]);
});
test("click on element allowing or disallowing pointer events", async () => {
await mountForTest(/* xml */ `
`);
const container = queryOne(".container");
const interactiveButton = queryOne(".second");
let events;
// Elements affected by pointer-events: none -> doesn't work
events = await click(".first");
expect(events.get("click").target).toBe(container);
events = await click(".third");
expect(events.get("click").target).toBe(container);
// Allowed button -> does work
events = await click(interactiveButton);
expect(events.get("click").target).toBe(interactiveButton);
expect("button:interactive").toHaveCount(1);
container.style.pointerEvents = "none";
interactiveButton.style.pointerEvents = "none";
// Does not work anymore
events = await click(interactiveButton);
expect(events.get("click").target).toBe(container.parentElement);
expect("button:interactive").not.toHaveCount();
});
test("click on inert element", async () => {
await mountForTest(/* xml */ `
`);
let events = await click(".btn");
expect(events.get("click")).not.toBe(null);
queryOne`.btn`.setAttribute("inert", "");
events = await click(".btn");
expect(events.get("click").target).toBe(queryOne`.container`);
await expect(click(":iframe button")).rejects.toThrow();
});
test("click on common parent", async () => {
await mountForTest(/* xml */ `
`);
monitorEvents(".parent");
monitorEvents(".first");
monitorEvents(".second");
await pointerDown(".first");
await pointerUp(".second");
expect.verifySteps([
// Move to first
"pointerover:0@button",
"pointerover:0@main",
"mouseover:0@button",
"mouseover:0@main",
"pointerenter:0@main",
"pointerenter:0@button",
"mouseenter:0@main",
"mouseenter:0@button",
"pointermove:0@button",
"pointermove:0@main",
"mousemove:0@button",
"mousemove:0@main",
// Pointer down on first
"pointerdown:0(1)@button",
"pointerdown:0(1)@main",
"mousedown:0(1)@button",
"mousedown:0(1)@main",
"focus@button",
"focusin@button",
"focusin@main",
// Move to second
"pointermove:0(1)@button",
"pointermove:0(1)@main",
"mousemove:0(1)@button",
"mousemove:0(1)@main",
"pointerout:0(1)@button",
"pointerout:0(1)@main",
"mouseout:0(1)@button",
"mouseout:0(1)@main",
"pointerleave:0(1)@button",
"mouseleave:0(1)@button",
"pointerover:0(1)@input",
"pointerover:0(1)@main",
"mouseover:0(1)@input",
"mouseover:0(1)@main",
"pointerenter:0(1)@input",
"mouseenter:0(1)@input",
"pointermove:0(1)@input",
"pointermove:0(1)@main",
"mousemove:0(1)@input",
"mousemove:0(1)@main",
// Pointer up on second
"pointerup:0@input",
"pointerup:0@main",
"mouseup:0@input",
"mouseup:0@main",
"click:0@main",
]);
});
test("click can be dispatched with pointer events prevented", async () => {
await mountForTest(/* xml */ ``);
const prevent = (ev) => ev.preventDefault();
on("button", "pointerdown", prevent);
on("button", "mousedown", prevent);
on("button", "pointerup", prevent);
on("button", "mouseup", prevent);
await hover("button");
monitorEvents("button");
await click("button");
expect.verifySteps(["pointerdown:0(1)@button", "pointerup:0@button", "click:0@button"]);
});
test("click: iframe", async () => {
await mountForTest(/* xml */ `
`);
expect("button").toHaveCount(1);
expect(":iframe button").toHaveCount(1);
await click("button");
expect("button").toBeFocused();
expect(":iframe button").not.toBeFocused();
await click(":iframe button");
expect("button").not.toBeFocused();
expect(":iframe button").toBeFocused();
});
test("drag & drop: draggable items", async () => {
await mountForTest(/* xml */ `
`);
monitorEvents("body");
monitorEvents("li");
// Drag & cancel
await (await drag("#first-item")).cancel();
expect.verifySteps([
// Move to first
"pointerover:0@#first-item",
"pointerover:0@body",
"mouseover:0@#first-item",
"mouseover:0@body",
"pointerenter:0@body",
"pointerenter:0@#first-item",
"mouseenter:0@body",
"mouseenter:0@#first-item",
"pointermove:0@#first-item",
"pointermove:0@body",
"mousemove:0@#first-item",
"mousemove:0@body",
// Drag first
"pointerdown:0(1)@#first-item",
"pointerdown:0(1)@body",
"mousedown:0(1)@#first-item",
"mousedown:0(1)@body",
// Cancel
"keydown:Escape@body",
"keyup:Escape@body",
]);
// Drag & drop
await (await drag("#first-item")).drop("#third-item");
expect.verifySteps([
// Drag first
"pointerdown:0(1)@#first-item",
"pointerdown:0(1)@body",
"mousedown:0(1)@#first-item",
"mousedown:0(1)@body",
// Leave first
"dragstart:0@#first-item",
"dragstart:0@body",
"drag:0@#first-item",
"drag:0@body",
"dragover:0@#first-item",
"dragover:0@body",
"dragleave:0@#first-item",
"dragleave:0@body",
// Move to third
"dragenter:0@#third-item",
"dragenter:0@body",
"drag:0@#third-item",
"drag:0@body",
"dragover:0@#third-item",
"dragover:0@body",
// Drop
"dragend:0@#third-item",
"dragend:0@body",
]);
// Drag, move & cancel
await (await (await drag("#first-item")).moveTo("#third-item")).cancel();
expect.verifySteps([
// Leave third
"pointermove:0@#third-item",
"pointermove:0@body",
"mousemove:0@#third-item",
"mousemove:0@body",
"pointerout:0@#third-item",
"pointerout:0@body",
"mouseout:0@#third-item",
"mouseout:0@body",
"pointerleave:0@#third-item",
"mouseleave:0@#third-item",
// Move to first
"pointerover:0@#first-item",
"pointerover:0@body",
"mouseover:0@#first-item",
"mouseover:0@body",
"pointerenter:0@#first-item",
"mouseenter:0@#first-item",
"pointermove:0@#first-item",
"pointermove:0@body",
"mousemove:0@#first-item",
"mousemove:0@body",
// Drag first
"pointerdown:0(1)@#first-item",
"pointerdown:0(1)@body",
"mousedown:0(1)@#first-item",
"mousedown:0(1)@body",
// Leave first
"dragstart:0@#first-item",
"dragstart:0@body",
"drag:0@#first-item",
"drag:0@body",
"dragover:0@#first-item",
"dragover:0@body",
"dragleave:0@#first-item",
"dragleave:0@body",
// Move to third
"dragenter:0@#third-item",
"dragenter:0@body",
"drag:0@#third-item",
"drag:0@body",
"dragover:0@#third-item",
"dragover:0@body",
// Cancel
"keydown:Escape@body",
"keyup:Escape@body",
]);
// Drag, move & drop
await (await (await drag("#first-item")).moveTo("#third-item")).drop();
expect.verifySteps([
// Leave third
"pointermove:0@#third-item",
"pointermove:0@body",
"mousemove:0@#third-item",
"mousemove:0@body",
"pointerout:0@#third-item",
"pointerout:0@body",
"mouseout:0@#third-item",
"mouseout:0@body",
"pointerleave:0@#third-item",
"mouseleave:0@#third-item",
// Move to first
"pointerover:0@#first-item",
"pointerover:0@body",
"mouseover:0@#first-item",
"mouseover:0@body",
"pointerenter:0@#first-item",
"mouseenter:0@#first-item",
"pointermove:0@#first-item",
"pointermove:0@body",
"mousemove:0@#first-item",
"mousemove:0@body",
// Drag first
"pointerdown:0(1)@#first-item",
"pointerdown:0(1)@body",
"mousedown:0(1)@#first-item",
"mousedown:0(1)@body",
// Leave first
"dragstart:0@#first-item",
"dragstart:0@body",
"drag:0@#first-item",
"drag:0@body",
"dragover:0@#first-item",
"dragover:0@body",
"dragleave:0@#first-item",
"dragleave:0@body",
// Move to third
"dragenter:0@#third-item",
"dragenter:0@body",
"drag:0@#third-item",
"drag:0@body",
"dragover:0@#third-item",
"dragover:0@body",
// Drop
"dragend:0@#third-item",
"dragend:0@body",
]);
// Drag, move & drop (different target)
await (await (await drag("#first-item")).moveTo("#second-item")).drop("#third-item");
expect.verifySteps([
// Leave third
"pointermove:0@#third-item",
"pointermove:0@body",
"mousemove:0@#third-item",
"mousemove:0@body",
"pointerout:0@#third-item",
"pointerout:0@body",
"mouseout:0@#third-item",
"mouseout:0@body",
"pointerleave:0@#third-item",
"mouseleave:0@#third-item",
// Move to first
"pointerover:0@#first-item",
"pointerover:0@body",
"mouseover:0@#first-item",
"mouseover:0@body",
"pointerenter:0@#first-item",
"mouseenter:0@#first-item",
"pointermove:0@#first-item",
"pointermove:0@body",
"mousemove:0@#first-item",
"mousemove:0@body",
// Drag first
"pointerdown:0(1)@#first-item",
"pointerdown:0(1)@body",
"mousedown:0(1)@#first-item",
"mousedown:0(1)@body",
// Leave first
"dragstart:0@#first-item",
"dragstart:0@body",
"drag:0@#first-item",
"drag:0@body",
"dragover:0@#first-item",
"dragover:0@body",
"dragleave:0@#first-item",
"dragleave:0@body",
// Move to second
"dragenter:0@#second-item",
"dragenter:0@body",
"drag:0@#second-item",
"drag:0@body",
"dragover:0@#second-item",
"dragover:0@body",
// Leave second
"drag:0@#second-item",
"drag:0@body",
"dragover:0@#second-item",
"dragover:0@body",
"dragleave:0@#second-item",
"dragleave:0@body",
// Move to third
"dragenter:0@#third-item",
"dragenter:0@body",
"drag:0@#third-item",
"drag:0@body",
"dragover:0@#third-item",
"dragover:0@body",
// Drop
"dragend:0@#third-item",
"dragend:0@body",
]);
});
test("drag & drop: draggable items with files", async () => {
await mountForTest(/* xml */ `
`);
const { drop, moveTo } = await drag("#first-item", {
dropEffect: "move",
files: [new File([""], "dragged-file.txt")],
});
await moveTo("#second-item");
const events = await drop("#third-item");
const dragEvents = events.getAll((ev) => ev.type.startsWith("drag"));
const { dataTransfer } = dragEvents[0];
expect(dataTransfer.dropEffect).toBe("move");
expect(dataTransfer.effectAllowed).toBe("all");
expect(dataTransfer.files).toHaveLength(1);
expect(dataTransfer.items).toHaveLength(1);
expect(dataTransfer.types).toEqual(["Files"]);
for (const event of dragEvents) {
expect(event.dataTransfer).toBe(dataTransfer, {
message: `drag event "${event.type}" should share the same dataTransfer object`,
});
}
});
test("drag & drop: draggable items with dataTransfer items", async () => {
await mountForTest(/* xml */ `
`);
const { drop, moveTo } = await drag("#first-item", {
items: [
["abc", "text/plain"],
["