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

676 lines
23 KiB
JavaScript

import { expect, test } from "@odoo/hoot";
import {
pointerDown,
pointerUp,
queryAllAttributes,
queryAllTexts,
queryFirst,
queryOne,
} from "@odoo/hoot-dom";
import { Deferred, animationFrame, runAllTimers } from "@odoo/hoot-mock";
import { Component, useState, xml } from "@odoo/owl";
import { contains, mountWithCleanup } from "@web/../tests/web_test_helpers";
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
test("can be rendered", async () => {
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<AutoComplete
value="'Hello'"
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
onSelect="() => {}"
/>
`;
static props = {};
}
await mountWithCleanup(Parent);
expect(".o-autocomplete").toHaveCount(1);
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
await contains(".o-autocomplete input").click();
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
expect(queryAllTexts(".o-autocomplete--dropdown-item")).toEqual(["World", "Hello"]);
const dropdownItemIds = queryAllAttributes(".dropdown-item", "id");
expect(dropdownItemIds).toEqual(["autocomplete_0_0", "autocomplete_0_1"]);
expect(queryAllAttributes(".dropdown-item", "role")).toEqual(["option", "option"]);
expect(queryAllAttributes(".dropdown-item", "aria-selected")).toEqual(["true", "false"]);
expect(".o-autocomplete--input").toHaveAttribute("aria-activedescendant", dropdownItemIds[0]);
});
test("select option", async () => {
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<AutoComplete
value="state.value"
sources="sources"
onSelect="(option) => this.onSelect(option)"
/>
`;
static props = {};
setup() {
this.state = useState({
value: "Hello",
});
}
get sources() {
return [
{
options: [{ label: "World" }, { label: "Hello" }],
},
];
}
onSelect(option) {
this.state.value = option.label;
expect.step(option.label);
}
}
await mountWithCleanup(Parent);
expect(".o-autocomplete input").toHaveValue("Hello");
await contains(".o-autocomplete input").click();
await contains(queryFirst(".o-autocomplete--dropdown-item")).click();
expect(".o-autocomplete input").toHaveValue("World");
expect.verifySteps(["World"]);
await contains(".o-autocomplete input").click();
await contains(".o-autocomplete--dropdown-item:last").click();
expect(".o-autocomplete input").toHaveValue("Hello");
expect.verifySteps(["Hello"]);
});
test("autocomplete with resetOnSelect='true'", async () => {
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<div>
<div class= "test_value" t-esc="state.value"/>
<AutoComplete
value="''"
sources="sources"
onSelect="(option) => this.onSelect(option)"
resetOnSelect="true"
/>
</div>
`;
static props = {};
setup() {
this.state = useState({
value: "Hello",
});
}
get sources() {
return [
{
options: [{ label: "World" }, { label: "Hello" }],
},
];
}
onSelect(option) {
this.state.value = option.label;
expect.step(option.label);
}
}
await mountWithCleanup(Parent);
expect(".test_value").toHaveText("Hello");
expect(".o-autocomplete input").toHaveValue("");
await contains(".o-autocomplete input").edit("Blip", { confirm: false });
await runAllTimers();
await contains(".o-autocomplete--dropdown-item:last").click();
expect(".test_value").toHaveText("Hello");
expect(".o-autocomplete input").toHaveValue("");
expect.verifySteps(["Hello"]);
});
test("open dropdown on input", async () => {
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<AutoComplete
value="'Hello'"
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
onSelect="() => {}"
/>
`;
static props = {};
}
await mountWithCleanup(Parent);
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
await contains(".o-autocomplete input").fill("a", { confirm: false });
await runAllTimers();
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
});
test("cancel result on escape keydown", async () => {
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<AutoComplete
value="'Hello'"
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
onSelect="() => {}"
autoSelect="true"
/>
`;
static props = {};
}
await mountWithCleanup(Parent);
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
expect(".o-autocomplete input").toHaveValue("Hello");
await contains(".o-autocomplete input").click();
await contains(".o-autocomplete input").edit("H", { confirm: false });
await runAllTimers();
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
await contains(".o-autocomplete input").press("Escape");
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
expect(".o-autocomplete input").toHaveValue("Hello");
});
test("select input text on first focus", async () => {
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<AutoComplete value="'Bar'" sources="[{ options: [{ label: 'Bar' }] }]" onSelect="() => {}"/>
`;
static props = {};
}
await mountWithCleanup(Parent);
await contains(".o-autocomplete input").click();
await runAllTimers();
expect(getSelection().toString()).toBe("Bar");
});
test("scroll outside should cancel result", async () => {
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<div class="autocomplete_container overflow-auto" style="max-height: 100px;">
<div style="height: 1000px;">
<AutoComplete
value="'Hello'"
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
onSelect="() => {}"
autoSelect="true"
/>
</div>
</div>
`;
static props = {};
}
await mountWithCleanup(Parent);
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
expect(".o-autocomplete input").toHaveValue("Hello");
await contains(".o-autocomplete input").click();
await contains(".o-autocomplete input").edit("H", { confirm: false });
await runAllTimers();
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
await contains(".autocomplete_container").scroll({ top: 10 });
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
expect(".o-autocomplete input").toHaveValue("Hello");
});
test("scroll inside should keep dropdown open", async () => {
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<div class="autocomplete_container overflow-auto" style="max-height: 100px;">
<div style="height: 1000px;">
<AutoComplete
value="'Hello'"
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
onSelect="() => {}"
/>
</div>
</div>
`;
static props = {};
}
await mountWithCleanup(Parent);
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
await contains(".o-autocomplete input").click();
await runAllTimers();
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
await contains(".o-autocomplete .dropdown-menu").scroll({ top: 10 });
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
});
test("losing focus should cancel result", async () => {
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<AutoComplete
value="'Hello'"
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
onSelect="() => {}"
autoSelect="true"
/>
`;
static props = {};
}
await mountWithCleanup(Parent);
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
expect(".o-autocomplete input").toHaveValue("Hello");
await contains(".o-autocomplete input").click();
await contains(".o-autocomplete input").edit("H", { confirm: false });
await runAllTimers();
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
await contains(document.body).click();
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
expect(".o-autocomplete input").toHaveValue("Hello");
});
test("click out after clearing input", async () => {
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<AutoComplete
value="'Hello'"
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
onSelect="() => {}"
/>
`;
static props = {};
}
await mountWithCleanup(Parent);
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
expect(".o-autocomplete input").toHaveValue("Hello");
await contains(".o-autocomplete input").click();
await contains(".o-autocomplete input").clear({ confirm: false });
await runAllTimers();
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
await contains(document.body).click();
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
expect(".o-autocomplete input").toHaveValue("");
});
test("open twice should not display previous results", async () => {
let def = new Deferred();
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<AutoComplete value="''" sources="sources" onSelect="() => {}"/>
`;
static props = {};
get sources() {
return [
{
async options(search) {
await def;
if (search === "A") {
return [{ label: "AB" }, { label: "AC" }];
}
return [{ label: "AB" }, { label: "AC" }, { label: "BC" }];
},
},
];
}
}
await mountWithCleanup(Parent);
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
await contains(".o-autocomplete input").click();
await runAllTimers();
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
expect(".o-autocomplete--dropdown-item").toHaveCount(1);
expect(".o-autocomplete--dropdown-item .fa-spin").toHaveCount(1); // loading
def.resolve();
await animationFrame();
expect(".o-autocomplete--dropdown-item").toHaveCount(3);
expect(".fa-spin").toHaveCount(0);
def = new Deferred();
await contains(".o-autocomplete input").fill("A", { confirm: false });
await runAllTimers();
expect(".o-autocomplete--dropdown-item").toHaveCount(1);
expect(".o-autocomplete--dropdown-item .fa-spin").toHaveCount(1); // loading
def.resolve();
await runAllTimers();
expect(".o-autocomplete--dropdown-item").toHaveCount(2);
expect(".fa-spin").toHaveCount(0);
await contains(queryFirst(".o-autocomplete--dropdown-item")).click();
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
// re-open the dropdown -> should not display the previous results
def = new Deferred();
await contains(".o-autocomplete input").click();
await runAllTimers();
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
expect(".o-autocomplete--dropdown-item").toHaveCount(1);
expect(".o-autocomplete--dropdown-item .fa-spin").toHaveCount(1); // loading
});
test("press enter on autocomplete with empty source", async () => {
class Parent extends Component {
static components = { AutoComplete };
static template = xml`<AutoComplete value="''" sources="sources" onSelect="onSelect"/>`;
static props = {};
get sources() {
return [{ options: [] }];
}
onSelect() {}
}
await mountWithCleanup(Parent);
expect(".o-autocomplete input").toHaveCount(1);
expect(".o-autocomplete input").toHaveValue("");
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
// click inside the input and press "enter", because why not
await contains(".o-autocomplete input").click();
await runAllTimers();
await contains(".o-autocomplete input").press("Enter");
expect(".o-autocomplete input").toHaveCount(1);
expect(".o-autocomplete input").toHaveValue("");
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
});
test("press enter on autocomplete with empty source (2)", async () => {
// in this test, the source isn't empty at some point, but becomes empty as the user
// updates the input's value.
class Parent extends Component {
static components = { AutoComplete };
static template = xml`<AutoComplete value="''" sources="sources" onSelect="onSelect"/>`;
static props = {};
get sources() {
const options = (val) => {
if (val.length > 2) {
return [{ label: "test A" }, { label: "test B" }, { label: "test C" }];
}
return [];
};
return [{ options }];
}
onSelect() {}
}
await mountWithCleanup(Parent);
expect(".o-autocomplete input").toHaveCount(1);
expect(".o-autocomplete input").toHaveValue("");
await contains(".o-autocomplete input").edit("test", { confirm: false });
await runAllTimers();
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
expect(".o-autocomplete .dropdown-menu .o-autocomplete--dropdown-item").toHaveCount(3);
await contains(".o-autocomplete input").edit("t", { confirm: false });
await runAllTimers();
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
await contains(".o-autocomplete input").press("Enter");
expect(".o-autocomplete input").toHaveCount(1);
expect(".o-autocomplete input").toHaveValue("t");
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
});
test.tags("desktop");
test("autofocus=true option work as expected", async () => {
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<AutoComplete value="'Hello'"
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
autofocus="true"
onSelect="() => {}"
/>
`;
static props = {};
}
await mountWithCleanup(Parent);
expect(".o-autocomplete input").toBeFocused();
});
test.tags("desktop");
test("autocomplete in edition keep edited value before select option", async () => {
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<button class="myButton" t-on-mouseover="onHover">My button</button>
<AutoComplete value="this.state.value"
sources="[{ options: [{ label: 'My Selection' }] }]"
onSelect.bind="onSelect"
/>
`;
static props = {};
setup() {
this.state = useState({ value: "Hello" });
}
onHover() {
this.state.value = "My Click";
}
onSelect() {
this.state.value = "My Selection";
}
}
await mountWithCleanup(Parent);
await contains(".o-autocomplete input").edit("Yolo", { confirm: false });
await runAllTimers();
expect(".o-autocomplete input").toHaveValue("Yolo");
// We want to simulate an external value edition (like a delayed onChange)
await contains(".myButton").hover();
expect(".o-autocomplete input").toHaveValue("Yolo");
// Leave inEdition mode when selecting an option
await contains(".o-autocomplete input").click();
await runAllTimers();
await contains(queryFirst(".o-autocomplete--dropdown-item")).click();
expect(".o-autocomplete input").toHaveValue("My Selection");
// Will also trigger the hover event
await contains(".myButton").click();
expect(".o-autocomplete input").toHaveValue("My Click");
});
test.tags("desktop");
test("autocomplete in edition keep edited value before blur", async () => {
let count = 0;
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<button class="myButton" t-on-mouseover="onHover">My button</button>
<AutoComplete value="this.state.value"
sources="[]"
onSelect="() => {}"
/>
`;
static props = {};
setup() {
this.state = useState({ value: "Hello" });
}
onHover() {
this.state.value = `My Click ${count++}`;
}
}
await mountWithCleanup(Parent);
await contains(".o-autocomplete input").edit("", { confirm: false });
await runAllTimers();
expect(".o-autocomplete input").toHaveValue("");
// We want to simulate an external value edition (like a delayed onChange)
await contains(".myButton").hover();
expect(".o-autocomplete input").toHaveValue("");
// Leave inEdition mode when blur the input
await contains(document.body).click();
expect(".o-autocomplete input").toHaveValue("");
// Will also trigger the hover event
await contains(".myButton").click();
expect(".o-autocomplete input").toHaveValue("My Click 1");
});
test("correct sequence of blur, focus and select", async () => {
class Parent extends Component {
static components = { AutoComplete };
static template = xml`
<AutoComplete
value="state.value"
sources="sources"
onSelect.bind="onSelect"
onBlur.bind="onBlur"
onChange.bind="onChange"
autoSelect="true"
/>
`;
static props = {};
setup() {
this.state = useState({
value: "",
});
}
get sources() {
return [
{
options: [{ label: "World" }, { label: "Hello" }],
},
];
}
onChange() {
expect.step("change");
}
onSelect(option, params) {
queryOne(".o-autocomplete input").value = option.label;
expect.step("select " + option.label);
expect(params.triggeredOnBlur).not.toBe(true);
}
onBlur() {
expect.step("blur");
}
}
await mountWithCleanup(Parent);
expect(".o-autocomplete input").toHaveCount(1);
await contains(".o-autocomplete input").click();
// Navigate suggestions using arrow keys
let dropdownItemIds = queryAllAttributes(".dropdown-item", "id");
expect(dropdownItemIds).toEqual(["autocomplete_0_0", "autocomplete_0_1"]);
expect(queryAllAttributes(".dropdown-item", "role")).toEqual(["option", "option"]);
expect(queryAllAttributes(".dropdown-item", "aria-selected")).toEqual(["true", "false"]);
expect(".o-autocomplete--input").toHaveAttribute("aria-activedescendant", dropdownItemIds[0]);
await contains(".o-autocomplete--input").press("ArrowDown");
dropdownItemIds = queryAllAttributes(".dropdown-item", "id");
expect(dropdownItemIds).toEqual(["autocomplete_0_0", "autocomplete_0_1"]);
expect(queryAllAttributes(".dropdown-item", "role")).toEqual(["option", "option"]);
expect(queryAllAttributes(".dropdown-item", "aria-selected")).toEqual(["false", "true"]);
expect(".o-autocomplete--input").toHaveAttribute("aria-activedescendant", dropdownItemIds[1]);
// Start typing hello and click on the result
await contains(".o-autocomplete input").edit("h", { confirm: false });
await runAllTimers();
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
await contains(".o-autocomplete--dropdown-item:last").click();
expect.verifySteps(["change", "select Hello"]);
expect(".o-autocomplete input").toBeFocused();
// Clear input and focus out
await contains(".o-autocomplete input").edit("", { confirm: false });
await runAllTimers();
await contains(document.body).click();
expect.verifySteps(["blur", "change"]);
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
});
test("autocomplete always closes on click away", async () => {
class Parent extends Component {
static template = xml`
<AutoComplete
value="state.value"
sources="sources"
onSelect.bind="onSelect"
autoSelect="true"
/>
`;
static props = ["*"];
static components = { AutoComplete };
setup() {
this.state = useState({
value: "",
});
}
get sources() {
return [
{
options: [{ label: "World" }, { label: "Hello" }],
},
];
}
onSelect(option) {
queryOne(".o-autocomplete--input").value = option.label;
}
}
await mountWithCleanup(Parent);
expect(".o-autocomplete input").toHaveCount(1);
await contains(".o-autocomplete input").click();
expect(".o-autocomplete--dropdown-item").toHaveCount(2);
await pointerDown(".o-autocomplete--dropdown-item:last");
await pointerUp(document.body);
expect(".o-autocomplete--dropdown-item").toHaveCount(2);
await contains(document.body).click();
expect(".o-autocomplete--dropdown-item").toHaveCount(0);
});
test("autocomplete trim spaces for search", async () => {
class Parent extends Component {
static template = xml`
<AutoComplete value="state.value" sources="sources" onSelect="() => {}"/>
`;
static props = ["*"];
static components = { AutoComplete };
setup() {
this.state = useState({ value: " World" });
}
get sources() {
return [
{
options(search) {
return [{ label: "World" }, { label: "Hello" }].filter(({ label }) =>
label.startsWith(search)
);
},
},
];
}
}
await mountWithCleanup(Parent);
await contains(`.o-autocomplete input`).click();
expect(queryAllTexts(`.o-autocomplete--dropdown-item`)).toEqual(["World", "Hello"]);
});