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` `; 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` `; 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`
`; 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` `; 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` `; 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` `; 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`
`; 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`
`; 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` `; 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` `; 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` `; 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``; 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``; 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` `; 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` `; 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` `; 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` `; 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` `; 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` `; 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"]); });