mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-18 22:52:08 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
|
|
@ -0,0 +1,572 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
triggerEvents,
|
||||
} from "../helpers/utils";
|
||||
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let env;
|
||||
let target;
|
||||
|
||||
QUnit.module("Components", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
env = await makeTestEnv();
|
||||
target = getFixture();
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("AutoComplete");
|
||||
|
||||
QUnit.test("can be rendered", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<AutoComplete
|
||||
value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
onSelect="() => {}"
|
||||
/>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o-autocomplete");
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
|
||||
await click(target, ".o-autocomplete--input");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
|
||||
|
||||
const options = [...target.querySelectorAll(".o-autocomplete--dropdown-item")];
|
||||
assert.deepEqual(
|
||||
options.map((el) => el.textContent),
|
||||
["World", "Hello"]
|
||||
);
|
||||
|
||||
const optionItems = [...target.querySelectorAll(".dropdown-item")];
|
||||
assert.deepEqual(
|
||||
optionItems.map((el) => ({
|
||||
id: el.id,
|
||||
role: el.getAttribute("role"),
|
||||
"aria-selected": el.getAttribute("aria-selected"),
|
||||
})),
|
||||
[
|
||||
{ id: "autocomplete_0_0", role: "option", "aria-selected": "true" },
|
||||
{ id: "autocomplete_0_1", role: "option", "aria-selected": "false" },
|
||||
]
|
||||
);
|
||||
|
||||
const input = target.querySelector(".o-autocomplete--input");
|
||||
assert.strictEqual(input.getAttribute("aria-activedescendant"), optionItems[0].id);
|
||||
});
|
||||
|
||||
QUnit.test("select option", async (assert) => {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({
|
||||
value: "Hello",
|
||||
});
|
||||
}
|
||||
get sources() {
|
||||
return [
|
||||
{
|
||||
options: [{ label: "World" }, { label: "Hello" }],
|
||||
},
|
||||
];
|
||||
}
|
||||
onSelect(option) {
|
||||
this.state.value = option.label;
|
||||
assert.step(option.label);
|
||||
}
|
||||
}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<AutoComplete
|
||||
value="state.value"
|
||||
sources="sources"
|
||||
onSelect="(option) => this.onSelect(option)"
|
||||
/>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello");
|
||||
|
||||
await click(target, ".o-autocomplete--input");
|
||||
await click(target.querySelectorAll(".o-autocomplete--dropdown-item")[0]);
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "World");
|
||||
assert.verifySteps(["World"]);
|
||||
|
||||
await click(target, ".o-autocomplete--input");
|
||||
await click(target.querySelectorAll(".o-autocomplete--dropdown-item")[1]);
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello");
|
||||
assert.verifySteps(["Hello"]);
|
||||
});
|
||||
|
||||
QUnit.test("open dropdown on input", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<AutoComplete
|
||||
value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
onSelect="() => {}"
|
||||
/>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
await triggerEvent(target, ".o-autocomplete--input", "input");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
|
||||
});
|
||||
|
||||
QUnit.test("cancel result on escape keydown", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<AutoComplete
|
||||
value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
onSelect="() => {}"
|
||||
autoSelect="true"
|
||||
/>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello");
|
||||
|
||||
await triggerEvents(target, ".o-autocomplete--input", ["focus", "click"]);
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
|
||||
await editInput(target, ".o-autocomplete--input", "H");
|
||||
|
||||
await triggerEvent(target, ".o-autocomplete--input", "keydown", { key: "Escape" });
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello");
|
||||
});
|
||||
|
||||
QUnit.test("scroll outside should cancel result", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<AutoComplete
|
||||
value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
onSelect="() => {}"
|
||||
autoSelect="true"
|
||||
/>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello");
|
||||
|
||||
await click(target, ".o-autocomplete--input");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
|
||||
await editInput(target, ".o-autocomplete--input", "H");
|
||||
|
||||
await triggerEvent(target, null, "scroll");
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello");
|
||||
});
|
||||
|
||||
QUnit.test("scroll inside should keep dropdown open", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<AutoComplete
|
||||
value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
onSelect="() => {}"
|
||||
/>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
|
||||
await click(target, ".o-autocomplete--input");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
|
||||
|
||||
await triggerEvent(target, ".o-autocomplete--dropdown-menu", "scroll");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
|
||||
});
|
||||
|
||||
QUnit.test("losing focus should cancel result", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<AutoComplete
|
||||
value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
onSelect="() => {}"
|
||||
autoSelect="true"
|
||||
/>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello");
|
||||
|
||||
await triggerEvents(target, ".o-autocomplete--input", ["focus", "click"]);
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
|
||||
await editInput(target, ".o-autocomplete--input", "H");
|
||||
|
||||
await triggerEvent(target, "", "pointerdown");
|
||||
await triggerEvent(target, ".o-autocomplete--input", "blur");
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello");
|
||||
});
|
||||
|
||||
QUnit.test("click out after clearing input", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<AutoComplete
|
||||
value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
onSelect="() => {}"
|
||||
/>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello");
|
||||
|
||||
await triggerEvents(target, ".o-autocomplete--input", ["focus", "click"]);
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
|
||||
await editInput(target, ".o-autocomplete--input", "");
|
||||
|
||||
await triggerEvent(target, "", "pointerdown");
|
||||
await triggerEvent(target, ".o-autocomplete--input", "blur");
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "");
|
||||
});
|
||||
|
||||
QUnit.test("open twice should not display previous results", async (assert) => {
|
||||
let def = makeDeferred();
|
||||
class Parent extends Component {
|
||||
get sources() {
|
||||
return [
|
||||
{
|
||||
async options(search) {
|
||||
await def;
|
||||
if (search === "A") {
|
||||
return [{ label: "AB" }, { label: "AC" }];
|
||||
}
|
||||
return [{ label: "AB" }, { label: "AC" }, { label: "BC" }];
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<AutoComplete value="''" sources="sources" onSelect="() => {}"/>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
|
||||
await triggerEvent(target, ".o-autocomplete--input", "click");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-item");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-item .fa-spin"); // loading
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.containsN(target, ".o-autocomplete--dropdown-item", 3);
|
||||
assert.containsNone(target, ".fa-spin");
|
||||
|
||||
def = makeDeferred();
|
||||
target.querySelector(".o-autocomplete--input").value = "A";
|
||||
await triggerEvent(target, ".o-autocomplete--input", "input");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-item");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-item .fa-spin"); // loading
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.containsN(target, ".o-autocomplete--dropdown-item", 2);
|
||||
assert.containsNone(target, ".fa-spin");
|
||||
|
||||
await click(target.querySelector(".o-autocomplete--dropdown-item"));
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
|
||||
// re-open the dropdown -> should not display the previous results
|
||||
def = makeDeferred();
|
||||
await triggerEvent(target, ".o-autocomplete--input", "click");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-item");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-item .fa-spin"); // loading
|
||||
});
|
||||
|
||||
QUnit.test("press enter on autocomplete with empty source", async (assert) => {
|
||||
class Parent extends Component {
|
||||
get sources() {
|
||||
return [{ options: [] }];
|
||||
}
|
||||
onSelect() {}
|
||||
}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`<AutoComplete value="''" sources="sources" onSelect="onSelect"/>`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o-autocomplete--input");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "");
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
|
||||
// click inside the input and press "enter", because why not
|
||||
await click(target, ".o-autocomplete--input");
|
||||
await triggerEvent(target, ".o-autocomplete--input", "keydown", { key: "Enter" });
|
||||
|
||||
assert.containsOnce(target, ".o-autocomplete--input");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "");
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
});
|
||||
|
||||
QUnit.test("press enter on autocomplete with empty source (2)", async (assert) => {
|
||||
// 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 {
|
||||
get sources() {
|
||||
const options = (val) => {
|
||||
if (val.length > 2) {
|
||||
return [{ label: "test A" }, { label: "test B" }, { label: "test C" }];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
return [{ options }];
|
||||
}
|
||||
onSelect() {}
|
||||
}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`<AutoComplete value="''" sources="sources" onSelect="onSelect"/>`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o-autocomplete--input");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "");
|
||||
|
||||
// click inside the input and press "enter", because why not
|
||||
await editInput(target, ".o-autocomplete--input", "test");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
|
||||
assert.containsN(
|
||||
target,
|
||||
".o-autocomplete--dropdown-menu .o-autocomplete--dropdown-item",
|
||||
3
|
||||
);
|
||||
|
||||
await editInput(target, ".o-autocomplete--input", "t");
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
|
||||
await triggerEvent(target, ".o-autocomplete--input", "keydown", { key: "Enter" });
|
||||
assert.containsOnce(target, ".o-autocomplete--input");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "t");
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
});
|
||||
|
||||
QUnit.test("correct sequence of blur, focus and select [REQUIRE FOCUS]", async (assert) => {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({
|
||||
value: "",
|
||||
});
|
||||
}
|
||||
get sources() {
|
||||
return [
|
||||
{
|
||||
options: [{ label: "World" }, { label: "Hello" }],
|
||||
},
|
||||
];
|
||||
}
|
||||
onChange() {
|
||||
assert.step("change");
|
||||
}
|
||||
onSelect(option) {
|
||||
target.querySelector(".o-autocomplete--input").value = option.label;
|
||||
assert.step("select " + option.label);
|
||||
}
|
||||
onBlur() {
|
||||
assert.step("blur");
|
||||
}
|
||||
}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<AutoComplete
|
||||
value="state.value"
|
||||
sources="sources"
|
||||
onSelect.bind="onSelect"
|
||||
onBlur.bind="onBlur"
|
||||
onChange.bind="onChange"
|
||||
autoSelect="true"
|
||||
/>
|
||||
`;
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o-autocomplete--input");
|
||||
const input = target.querySelector(".o-autocomplete--input");
|
||||
await click(input);
|
||||
input.focus();
|
||||
|
||||
// Navigate suggestions using arrow keys
|
||||
const optionItems = [...target.querySelectorAll(".dropdown-item")];
|
||||
assert.deepEqual(
|
||||
optionItems.map((el) => ({
|
||||
id: el.id,
|
||||
role: el.getAttribute("role"),
|
||||
"aria-selected": el.getAttribute("aria-selected"),
|
||||
})),
|
||||
[
|
||||
{ id: "autocomplete_0_0", role: "option", "aria-selected": "true" },
|
||||
{ id: "autocomplete_0_1", role: "option", "aria-selected": "false" },
|
||||
]
|
||||
);
|
||||
assert.strictEqual(input.getAttribute("aria-activedescendant"), optionItems[0].id);
|
||||
await triggerEvent(target, ".o-autocomplete--input", "keydown", { key: "arrowdown" });
|
||||
assert.deepEqual(
|
||||
optionItems.map((el) => ({
|
||||
id: el.id,
|
||||
role: el.getAttribute("role"),
|
||||
"aria-selected": el.getAttribute("aria-selected"),
|
||||
})),
|
||||
[
|
||||
{ id: "autocomplete_0_0", role: "option", "aria-selected": "false" },
|
||||
{ id: "autocomplete_0_1", role: "option", "aria-selected": "true" },
|
||||
]
|
||||
);
|
||||
assert.strictEqual(input.getAttribute("aria-activedescendant"), optionItems[1].id);
|
||||
|
||||
// Start typing hello and click on the result
|
||||
await triggerEvent(target, ".o-autocomplete--input", "keydown", { key: "h" });
|
||||
input.value = "h";
|
||||
await triggerEvent(input, "", "input");
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
|
||||
const pointerdownEvent = await triggerEvent(
|
||||
target.querySelectorAll(".o-autocomplete--dropdown-item")[1],
|
||||
"",
|
||||
"pointerdown"
|
||||
);
|
||||
assert.strictEqual(pointerdownEvent.defaultPrevented, false);
|
||||
const mousedownEvent = await triggerEvent(
|
||||
target.querySelectorAll(".o-autocomplete--dropdown-item")[1],
|
||||
"",
|
||||
"mousedown"
|
||||
);
|
||||
assert.strictEqual(mousedownEvent.defaultPrevented, false);
|
||||
await triggerEvent(input, "", "change");
|
||||
await triggerEvent(input, "", "blur");
|
||||
await click(target.querySelectorAll(".o-autocomplete--dropdown-item")[1], "");
|
||||
assert.verifySteps(["change", "select Hello"]);
|
||||
assert.strictEqual(input, document.activeElement);
|
||||
|
||||
// Clear input and focus out
|
||||
await triggerEvent(input, "", "keydown", { key: "Backspace" });
|
||||
input.value = "";
|
||||
await triggerEvent(input, "", "input");
|
||||
await triggerEvent(target, "", "pointerdown");
|
||||
await triggerEvent(input, "", "change");
|
||||
input.blur();
|
||||
await click(target, "");
|
||||
assert.verifySteps(["change", "blur"]);
|
||||
});
|
||||
|
||||
QUnit.test("autocomplete always closes on click away [REQUIRE FOCUS]", async (assert) => {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({
|
||||
value: "",
|
||||
});
|
||||
}
|
||||
get sources() {
|
||||
return [
|
||||
{
|
||||
options: [{ label: "World" }, { label: "Hello" }],
|
||||
},
|
||||
];
|
||||
}
|
||||
onSelect(option) {
|
||||
target.querySelector(".o-autocomplete--input").value = option.label;
|
||||
}
|
||||
}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<AutoComplete
|
||||
value="state.value"
|
||||
sources="sources"
|
||||
onSelect.bind="onSelect"
|
||||
autoSelect="true"
|
||||
/>
|
||||
`;
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o-autocomplete--input");
|
||||
const input = target.querySelector(".o-autocomplete--input");
|
||||
await click(input);
|
||||
assert.containsN(target, ".o-autocomplete--dropdown-item", 2);
|
||||
const pointerdownEvent = await triggerEvent(
|
||||
target.querySelectorAll(".o-autocomplete--dropdown-item")[1],
|
||||
"",
|
||||
"pointerdown"
|
||||
);
|
||||
assert.strictEqual(pointerdownEvent.defaultPrevented, false);
|
||||
const mousedownEvent = await triggerEvent(
|
||||
target.querySelectorAll(".o-autocomplete--dropdown-item")[1],
|
||||
"",
|
||||
"mousedown"
|
||||
);
|
||||
assert.strictEqual(mousedownEvent.defaultPrevented, false);
|
||||
await triggerEvent(input, "", "blur");
|
||||
await triggerEvent(target, "", "pointerup");
|
||||
await triggerEvent(target, "", "mouseup");
|
||||
assert.containsN(target, ".o-autocomplete--dropdown-item", 2);
|
||||
await triggerEvent(target, "", "pointerdown");
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-item");
|
||||
});
|
||||
|
||||
QUnit.test("autocomplete trim spaces for search", async (assert) => {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({
|
||||
value: " World",
|
||||
});
|
||||
}
|
||||
get sources() {
|
||||
return [
|
||||
{
|
||||
options(search) {
|
||||
return [{ label: "World" }, { label: "Hello" }].filter(({ label }) =>
|
||||
label.startsWith(search)
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<AutoComplete value="state.value" sources="sources" onSelect="() => {}"/>
|
||||
`;
|
||||
Parent.props = ["*"];
|
||||
Parent.components = { AutoComplete };
|
||||
await mount(Parent, target, { env });
|
||||
await click(target, `.o-autocomplete input`);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(`.o-autocomplete--dropdown-item`)].map(
|
||||
(el) => el.textContent
|
||||
),
|
||||
["World", "Hello"]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { titleService } from "@web/core/browser/title_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Tests
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let env;
|
||||
let title;
|
||||
|
||||
QUnit.module("Title", {
|
||||
async beforeEach() {
|
||||
title = document.title;
|
||||
registry.category("services").add("title", titleService);
|
||||
env = await makeTestEnv();
|
||||
},
|
||||
afterEach() {
|
||||
document.title = title;
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("simple title", async (assert) => {
|
||||
assert.expect(1);
|
||||
env.services.title.setParts({ zopenerp: "Odoo" });
|
||||
assert.strictEqual(env.services.title.current, "Odoo");
|
||||
});
|
||||
|
||||
QUnit.test("add title part", async (assert) => {
|
||||
assert.expect(2);
|
||||
env.services.title.setParts({ zopenerp: "Odoo", chat: null });
|
||||
assert.strictEqual(env.services.title.current, "Odoo");
|
||||
env.services.title.setParts({ action: "Import" });
|
||||
assert.strictEqual(env.services.title.current, "Odoo - Import");
|
||||
});
|
||||
|
||||
QUnit.test("modify title part", async (assert) => {
|
||||
assert.expect(2);
|
||||
env.services.title.setParts({ zopenerp: "Odoo" });
|
||||
assert.strictEqual(env.services.title.current, "Odoo");
|
||||
env.services.title.setParts({ zopenerp: "Zopenerp" });
|
||||
assert.strictEqual(env.services.title.current, "Zopenerp");
|
||||
});
|
||||
|
||||
QUnit.test("delete title part", async (assert) => {
|
||||
assert.expect(2);
|
||||
env.services.title.setParts({ zopenerp: "Odoo" });
|
||||
assert.strictEqual(env.services.title.current, "Odoo");
|
||||
env.services.title.setParts({ zopenerp: null });
|
||||
assert.strictEqual(env.services.title.current, "");
|
||||
});
|
||||
|
||||
QUnit.test("all at once", async (assert) => {
|
||||
assert.expect(2);
|
||||
env.services.title.setParts({ zopenerp: "Odoo", action: "Import" });
|
||||
assert.strictEqual(env.services.title.current, "Odoo - Import");
|
||||
env.services.title.setParts({ action: null, zopenerp: "Zopenerp", chat: "Sauron" });
|
||||
assert.strictEqual(env.services.title.current, "Zopenerp - Sauron");
|
||||
});
|
||||
|
||||
QUnit.test("get title parts", async (assert) => {
|
||||
assert.expect(3);
|
||||
env.services.title.setParts({ zopenerp: "Odoo", action: "Import" });
|
||||
assert.strictEqual(env.services.title.current, "Odoo - Import");
|
||||
const parts = env.services.title.getParts();
|
||||
assert.deepEqual(parts, { zopenerp: "Odoo", action: "Import" });
|
||||
parts.action = "Export";
|
||||
assert.strictEqual(env.services.title.current, "Odoo - Import"); // parts is a copy!
|
||||
});
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CheckBox } from "@web/core/checkbox/checkbox";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { translatedTerms } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
patchWithCleanup,
|
||||
mount,
|
||||
triggerEvent,
|
||||
triggerHotkey,
|
||||
nextTick,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
|
||||
QUnit.module("Components", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
target = getFixture();
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
});
|
||||
|
||||
QUnit.module("CheckBox");
|
||||
|
||||
QUnit.test("can be rendered", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
await mount(CheckBox, target, { env, props: {} });
|
||||
assert.containsOnce(target, '.o-checkbox input[type="checkbox"]');
|
||||
});
|
||||
|
||||
QUnit.test("has a slot for translatable text", async (assert) => {
|
||||
patchWithCleanup(translatedTerms, { ragabadabadaba: "rugubudubudubu" });
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
const env = await makeTestEnv();
|
||||
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`<CheckBox>ragabadabadaba</CheckBox>`;
|
||||
Parent.components = { CheckBox };
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, "div.form-check");
|
||||
assert.strictEqual(target.querySelector("div.form-check").textContent, "rugubudubudubu");
|
||||
});
|
||||
|
||||
QUnit.test("call onChange prop when some change occurs", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
|
||||
let value = false;
|
||||
class Parent extends Component {
|
||||
onChange(checked) {
|
||||
value = checked;
|
||||
}
|
||||
}
|
||||
Parent.template = xml`<CheckBox onChange="onChange"/>`;
|
||||
Parent.components = { CheckBox };
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o-checkbox input");
|
||||
await click(target.querySelector("input"));
|
||||
assert.strictEqual(value, true);
|
||||
await click(target.querySelector("input"));
|
||||
assert.strictEqual(value, false);
|
||||
});
|
||||
|
||||
QUnit.test("does not call onChange prop when disabled", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
|
||||
let onChangeCalled = false;
|
||||
class Parent extends Component {
|
||||
onChange(checked) {
|
||||
onChangeCalled = true;
|
||||
}
|
||||
}
|
||||
Parent.template = xml`<CheckBox onChange="onChange" disabled="true"/>`;
|
||||
Parent.components = { CheckBox };
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o-checkbox input");
|
||||
await click(target.querySelector("input"));
|
||||
assert.strictEqual(onChangeCalled, false);
|
||||
});
|
||||
|
||||
QUnit.test("can toggle value by pressing ENTER", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ value: false });
|
||||
}
|
||||
onChange(checked) {
|
||||
this.state.value = checked;
|
||||
}
|
||||
}
|
||||
Parent.template = xml`<CheckBox onChange.bind="onChange" value="state.value"/>`;
|
||||
Parent.components = { CheckBox };
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o-checkbox input");
|
||||
assert.notOk(target.querySelector(".o-checkbox input").checked);
|
||||
await triggerEvent(target, ".o-checkbox input", "keydown", { key: "Enter" });
|
||||
assert.ok(target.querySelector(".o-checkbox input").checked);
|
||||
await triggerEvent(target, ".o-checkbox input", "keydown", { key: "Enter" });
|
||||
assert.notOk(target.querySelector(".o-checkbox input").checked);
|
||||
});
|
||||
|
||||
QUnit.test("toggling through multiple ways", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ value: false });
|
||||
}
|
||||
onChange(checked) {
|
||||
this.state.value = checked;
|
||||
assert.step(`${checked}`);
|
||||
}
|
||||
}
|
||||
Parent.template = xml`<CheckBox onChange.bind="onChange" value="state.value"/>`;
|
||||
Parent.components = { CheckBox };
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o-checkbox input");
|
||||
assert.notOk(target.querySelector(".o-checkbox input").checked);
|
||||
|
||||
// Click on div
|
||||
assert.verifySteps([]);
|
||||
await click(target, ".o-checkbox");
|
||||
assert.ok(target.querySelector(".o-checkbox input").checked);
|
||||
assert.verifySteps(["true"]);
|
||||
|
||||
// Click on label
|
||||
assert.verifySteps([]);
|
||||
await click(target, ".o-checkbox > .form-check-label", true);
|
||||
assert.notOk(target.querySelector(".o-checkbox input").checked);
|
||||
assert.verifySteps(["false"]);
|
||||
|
||||
// Click on input (only possible programmatically)
|
||||
assert.verifySteps([]);
|
||||
await click(target, ".o-checkbox input");
|
||||
assert.ok(target.querySelector(".o-checkbox input").checked);
|
||||
assert.verifySteps(["true"]);
|
||||
|
||||
// When somehow applying focus on label, the focus receives it
|
||||
// (this is the default behavior from the label)
|
||||
target.querySelector(".o-checkbox > .form-check-label").focus();
|
||||
await nextTick();
|
||||
assert.strictEqual(document.activeElement, target.querySelector(".o-checkbox input"));
|
||||
|
||||
// Press Enter when focus is on input
|
||||
assert.verifySteps([]);
|
||||
triggerHotkey("Enter");
|
||||
await nextTick();
|
||||
assert.notOk(target.querySelector(".o-checkbox input").checked);
|
||||
assert.verifySteps(["false"]);
|
||||
|
||||
// Pressing Space when focus is on the input is a standard behavior
|
||||
// So we simulate it and verify that it will have its standard behavior.
|
||||
assert.strictEqual(document.activeElement, target.querySelector(".o-checkbox input"));
|
||||
const event = triggerEvent(
|
||||
document.activeElement,
|
||||
null,
|
||||
"keydown",
|
||||
{ key: "Space" },
|
||||
{ fast: true }
|
||||
);
|
||||
assert.ok(!event.defaultPrevented);
|
||||
target.querySelector(".o-checkbox input").checked = true;
|
||||
assert.verifySteps([]);
|
||||
triggerEvent(target, ".o-checkbox input", "change", {}, { fast: true });
|
||||
await nextTick();
|
||||
assert.ok(target.querySelector(".o-checkbox input").checked);
|
||||
assert.verifySteps(["true"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ColorList } from "@web/core/colorlist/colorlist";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { click, getFixture, mount } from "../helpers/utils";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
|
||||
/**
|
||||
* @param {typeof ColorList} Picker
|
||||
* @param {Object} props
|
||||
* @returns {Promise<ColorList>}
|
||||
*/
|
||||
async function mountComponent(Picker, props) {
|
||||
serviceRegistry.add("ui", uiService);
|
||||
target = getFixture();
|
||||
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml/* xml */ `
|
||||
<t t-component="props.Picker" t-props="props.props"/>
|
||||
<div class="outsideDiv">Outside div</div>
|
||||
`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
if (!props.onColorSelected) {
|
||||
props.onColorSelected = () => {};
|
||||
}
|
||||
const parent = await mount(Parent, target, { env, props: { Picker, props } });
|
||||
return parent;
|
||||
}
|
||||
|
||||
QUnit.module("Components", () => {
|
||||
QUnit.module("ColorList");
|
||||
|
||||
QUnit.test("basic rendering with forceExpanded props", async function (assert) {
|
||||
await mountComponent(ColorList, {
|
||||
colors: [0, 9],
|
||||
forceExpanded: true,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_colorlist");
|
||||
assert.containsN(target, ".o_colorlist button", 2, "two buttons are available");
|
||||
const secondBtn = target.querySelectorAll(".o_colorlist button")[1];
|
||||
assert.strictEqual(
|
||||
secondBtn.attributes.title.value,
|
||||
"Fuchsia",
|
||||
"second button color is Fuchsia"
|
||||
);
|
||||
assert.hasClass(
|
||||
secondBtn,
|
||||
"o_colorlist_item_color_9",
|
||||
"second button has the corresponding class"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"color click does not open the list if canToggle props is not given",
|
||||
async function (assert) {
|
||||
const selectedColorId = 0;
|
||||
await mountComponent(ColorList, {
|
||||
colors: [4, 5, 6],
|
||||
selectedColor: selectedColorId,
|
||||
onColorSelected: (colorId) => assert.step("color #" + colorId + " is selected"),
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_colorlist");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"button.o_colorlist_toggler",
|
||||
"only the toggler button is available"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_colorlist button"));
|
||||
|
||||
assert.containsOnce(target, "button.o_colorlist_toggler", "button is still visible");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("open the list of colors if canToggle props is given", async function (assert) {
|
||||
const selectedColorId = 0;
|
||||
await mountComponent(ColorList, {
|
||||
canToggle: true,
|
||||
colors: [4, 5, 6],
|
||||
selectedColor: selectedColorId,
|
||||
onColorSelected: (colorId) => assert.step("color #" + colorId + " is selected"),
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_colorlist");
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_colorlist button"),
|
||||
"o_colorlist_item_color_" + selectedColorId,
|
||||
"toggler has the right class"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_colorlist button"));
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
"button.o_colorlist_toggler",
|
||||
"toggler button is no longer visible"
|
||||
);
|
||||
assert.containsN(target, ".o_colorlist button", 3, "three buttons are available");
|
||||
|
||||
await click(target.querySelector(".outsideDiv"));
|
||||
assert.containsOnce(target, ".o_colorlist button", "only one button is available");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"button.o_colorlist_toggler",
|
||||
"colorlist has been closed and toggler is visible"
|
||||
);
|
||||
|
||||
// reopen the colorlist and select a color
|
||||
await click(target.querySelector(".o_colorlist_toggler"));
|
||||
await click(target.querySelectorAll(".o_colorlist button")[2]);
|
||||
|
||||
assert.verifySteps(["color #6 is selected"]);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,166 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { createWebClient, getActionManagerServerData } from "@web/../tests/webclient/helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { click, getFixture, nextTick, patchWithCleanup, triggerHotkey } from "../../helpers/utils";
|
||||
import { editSearchBar } from "./command_service_tests";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
QUnit.module("Menu Command Provider", {
|
||||
async beforeEach() {
|
||||
patchWithCleanup(browser, {
|
||||
clearTimeout: () => {},
|
||||
setTimeout: (later) => {
|
||||
later();
|
||||
},
|
||||
});
|
||||
const commandCategoryRegistry = registry.category("command_categories");
|
||||
commandCategoryRegistry.add("apps", { namespace: "/" }, { sequence: 10 });
|
||||
commandCategoryRegistry.add("menu_items", { namespace: "/" }, { sequence: 20 });
|
||||
serverData = getActionManagerServerData();
|
||||
serverData.menus = {
|
||||
root: { id: "root", children: [0, 1, 2], name: "root", appID: "root" },
|
||||
0: { id: 0, children: [], name: "UglyHack", appID: 0, xmlid: "menu_0" },
|
||||
1: { id: 1, children: [], name: "Contact", appID: 1, actionID: 1001, xmlid: "menu_1" },
|
||||
2: {
|
||||
id: 2,
|
||||
children: [3, 4],
|
||||
name: "Sales",
|
||||
appID: 2,
|
||||
actionID: 1002,
|
||||
xmlid: "menu_2",
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
children: [],
|
||||
name: "Info",
|
||||
appID: 2,
|
||||
actionID: 1003,
|
||||
xmlid: "menu_3",
|
||||
},
|
||||
4: {
|
||||
id: 4,
|
||||
children: [],
|
||||
name: "Report",
|
||||
appID: 2,
|
||||
actionID: 1004,
|
||||
xmlid: "menu_4",
|
||||
},
|
||||
};
|
||||
serverData.actions[1003] = {
|
||||
id: 1003,
|
||||
tag: "__test__client__action__",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
params: { description: "Info" },
|
||||
};
|
||||
serverData.actions[1004] = {
|
||||
id: 1004,
|
||||
tag: "__test__client__action__",
|
||||
target: "main",
|
||||
type: "ir.actions.client",
|
||||
params: { description: "Report" },
|
||||
};
|
||||
|
||||
target = getFixture();
|
||||
},
|
||||
afterEach() {},
|
||||
});
|
||||
|
||||
QUnit.test("displays only apps if the search value is '/'", async (assert) => {
|
||||
await createWebClient({ serverData });
|
||||
assert.containsNone(target, ".o_menu_brand");
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
await editSearchBar("/");
|
||||
assert.containsOnce(target, ".o_command_palette");
|
||||
assert.containsOnce(target, ".o_command_category");
|
||||
assert.containsN(target, ".o_command", 2);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_command_name")].map((el) => el.textContent),
|
||||
["Contact", "Sales"]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("displays apps and menu items if the search value is not only '/'", async (assert) => {
|
||||
await createWebClient({ serverData });
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
await editSearchBar("/sal");
|
||||
assert.containsOnce(target, ".o_command_palette");
|
||||
assert.containsN(target, ".o_command", 3);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_command_name")].map((el) => el.textContent),
|
||||
["Sales", "Sales / Info", "Sales / Report"]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("opens an app", async (assert) => {
|
||||
await createWebClient({ serverData });
|
||||
assert.containsNone(target, ".o_menu_brand");
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
await editSearchBar("/");
|
||||
assert.containsOnce(target, ".o_command_palette");
|
||||
|
||||
triggerHotkey("enter");
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".o_menu_brand").textContent, "Contact");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".test_client_action").textContent,
|
||||
" ClientAction_Id 1"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("opens a menu items", async (assert) => {
|
||||
await createWebClient({ serverData });
|
||||
assert.containsNone(target, ".o_menu_brand");
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
await editSearchBar("/sal");
|
||||
assert.containsOnce(target, ".o_command_palette");
|
||||
assert.containsN(target, ".o_command_category", 2);
|
||||
|
||||
click(target, "#o_command_2");
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".o_menu_brand").textContent, "Sales");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".test_client_action").textContent,
|
||||
" ClientAction_Report"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("open a menu item when a dialog is displayed", async (assert) => {
|
||||
class CustomDialog extends Component {}
|
||||
CustomDialog.components = { Dialog };
|
||||
CustomDialog.template = xml`<Dialog contentClass="'test'">content</Dialog>`;
|
||||
|
||||
const webclient = await createWebClient({ serverData });
|
||||
assert.containsNone(target, ".o_menu_brand");
|
||||
assert.containsNone(target, ".modal .test");
|
||||
|
||||
webclient.env.services.dialog.add(CustomDialog);
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".modal .test");
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
await editSearchBar("/sal");
|
||||
assert.containsOnce(target, ".o_command_palette");
|
||||
assert.containsOnce(target, ".modal .test");
|
||||
|
||||
await click(target, "#o_command_2");
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".o_menu_brand").textContent, "Sales");
|
||||
assert.containsNone(target, ".modal .test");
|
||||
});
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { click, getFixture, makeDeferred, mount, nextTick, triggerHotkey } from "../helpers/utils";
|
||||
import { makeFakeDialogService } from "../helpers/mock_services";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
let target;
|
||||
|
||||
async function makeDialogTestEnv() {
|
||||
const env = await makeTestEnv();
|
||||
env.dialogData = {
|
||||
isActive: true,
|
||||
close: () => {},
|
||||
};
|
||||
return env;
|
||||
}
|
||||
|
||||
QUnit.module("Components", (hooks) => {
|
||||
hooks.beforeEach(async (assert) => {
|
||||
target = getFixture();
|
||||
async function addDialog(dialogClass, props) {
|
||||
assert.strictEqual(props.body, "Some content");
|
||||
assert.strictEqual(props.title, "Confirmation");
|
||||
}
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("ui", uiService);
|
||||
serviceRegistry.add("dialog", makeFakeDialogService(addDialog), { force: true });
|
||||
});
|
||||
|
||||
QUnit.module("ConfirmationDialog");
|
||||
|
||||
QUnit.test("pressing escape to close the dialog", async function (assert) {
|
||||
const env = await makeDialogTestEnv();
|
||||
await mount(ConfirmationDialog, target, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {
|
||||
assert.step("Close action");
|
||||
},
|
||||
confirm: () => {},
|
||||
cancel: () => {
|
||||
assert.step("Cancel action");
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
triggerHotkey("escape");
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
["Cancel action", "Close action"],
|
||||
"dialog has called its cancel method before its closure"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("clicking on 'Ok'", async function (assert) {
|
||||
const env = await makeDialogTestEnv();
|
||||
await mount(ConfirmationDialog, target, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {
|
||||
assert.step("Close action");
|
||||
},
|
||||
confirm: () => {
|
||||
assert.step("Confirm action");
|
||||
},
|
||||
cancel: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
await click(target, ".modal-footer .btn-primary");
|
||||
assert.verifySteps(["Confirm action", "Close action"]);
|
||||
});
|
||||
|
||||
QUnit.test("clicking on 'Cancel'", async function (assert) {
|
||||
const env = await makeDialogTestEnv();
|
||||
await mount(ConfirmationDialog, target, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {
|
||||
assert.step("Close action");
|
||||
},
|
||||
confirm: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
cancel: () => {
|
||||
assert.step("Cancel action");
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
await click(target, ".modal-footer .btn-secondary");
|
||||
assert.verifySteps(["Cancel action", "Close action"]);
|
||||
});
|
||||
|
||||
QUnit.test("can't click twice on 'Ok'", async function (assert) {
|
||||
const env = await makeDialogTestEnv();
|
||||
await mount(ConfirmationDialog, target, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {},
|
||||
confirm: () => {
|
||||
assert.step("Confirm");
|
||||
},
|
||||
cancel: () => {},
|
||||
},
|
||||
});
|
||||
assert.notOk(target.querySelector(".modal-footer .btn-primary").disabled);
|
||||
assert.notOk(target.querySelector(".modal-footer .btn-secondary").disabled);
|
||||
click(target, ".modal-footer .btn-primary");
|
||||
assert.ok(target.querySelector(".modal-footer .btn-primary").disabled);
|
||||
assert.ok(target.querySelector(".modal-footer .btn-secondary").disabled);
|
||||
assert.verifySteps(["Confirm"]);
|
||||
});
|
||||
|
||||
QUnit.test("can't click twice on 'Cancel'", async function (assert) {
|
||||
const env = await makeDialogTestEnv();
|
||||
await mount(ConfirmationDialog, target, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {},
|
||||
confirm: () => {},
|
||||
cancel: () => {
|
||||
assert.step("Cancel");
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.notOk(target.querySelector(".modal-footer .btn-primary").disabled);
|
||||
assert.notOk(target.querySelector(".modal-footer .btn-secondary").disabled);
|
||||
click(target, ".modal-footer .btn-secondary");
|
||||
assert.ok(target.querySelector(".modal-footer .btn-primary").disabled);
|
||||
assert.ok(target.querySelector(".modal-footer .btn-secondary").disabled);
|
||||
assert.verifySteps(["Cancel"]);
|
||||
});
|
||||
|
||||
QUnit.test("can't cancel (with escape) after confirm", async function (assert) {
|
||||
const def = makeDeferred();
|
||||
const env = await makeDialogTestEnv();
|
||||
await mount(ConfirmationDialog, target, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {
|
||||
assert.step("close");
|
||||
},
|
||||
confirm: () => {
|
||||
assert.step("confirm");
|
||||
return def;
|
||||
},
|
||||
cancel: () => {
|
||||
throw new Error("should not cancel");
|
||||
},
|
||||
},
|
||||
});
|
||||
await click(target, ".modal-footer .btn-primary");
|
||||
assert.verifySteps(["confirm"]);
|
||||
triggerHotkey("escape");
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.verifySteps(["close"]);
|
||||
});
|
||||
|
||||
QUnit.test("wait for confirm callback before closing", async function (assert) {
|
||||
const env = await makeDialogTestEnv();
|
||||
const def = makeDeferred();
|
||||
await mount(ConfirmationDialog, target, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {
|
||||
assert.step("close");
|
||||
},
|
||||
confirm: () => {
|
||||
assert.step("confirm");
|
||||
return def;
|
||||
},
|
||||
},
|
||||
});
|
||||
await click(target, ".modal-footer .btn-primary");
|
||||
assert.verifySteps(["confirm"]);
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.verifySteps(["close"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { makeContext } from "@web/core/context";
|
||||
|
||||
QUnit.module("utils", {}, () => {
|
||||
QUnit.module("makeContext");
|
||||
|
||||
QUnit.test("return empty context", (assert) => {
|
||||
assert.deepEqual(makeContext([]), {});
|
||||
});
|
||||
|
||||
QUnit.test("duplicate a context", (assert) => {
|
||||
const ctx1 = { a: 1 };
|
||||
const ctx2 = makeContext([ctx1]);
|
||||
assert.notStrictEqual(ctx1, ctx2);
|
||||
assert.deepEqual(ctx1, ctx2);
|
||||
});
|
||||
|
||||
QUnit.test("can accept undefined or empty string", (assert) => {
|
||||
assert.deepEqual(makeContext([undefined]), {});
|
||||
assert.deepEqual(makeContext([{ a: 1 }, undefined, { b: 2 }]), { a: 1, b: 2 });
|
||||
assert.deepEqual(makeContext([""]), {});
|
||||
assert.deepEqual(makeContext([{ a: 1 }, "", { b: 2 }]), { a: 1, b: 2 });
|
||||
});
|
||||
|
||||
QUnit.test("evaluate strings", (assert) => {
|
||||
assert.deepEqual(makeContext(["{'a': 33}"]), { a: 33 });
|
||||
});
|
||||
|
||||
QUnit.test("evaluated context is used as evaluation context along the way", (assert) => {
|
||||
assert.deepEqual(makeContext([{ a: 1 }, "{'a': a + 1}"]), { a: 2 });
|
||||
assert.deepEqual(makeContext([{ a: 1 }, "{'b': a + 1}"]), { a: 1, b: 2 });
|
||||
assert.deepEqual(makeContext([{ a: 1 }, "{'b': a + 1}", "{'c': b + 1}"]), {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
});
|
||||
assert.deepEqual(makeContext([{ a: 1 }, "{'b': a + 1}", "{'a': b + 1}"]), { a: 3, b: 2 });
|
||||
});
|
||||
|
||||
QUnit.test("initial evaluation context", (assert) => {
|
||||
assert.deepEqual(makeContext(["{'a': a + 1}"], { a: 1 }), { a: 2 });
|
||||
assert.deepEqual(makeContext(["{'b': a + 1}"], { a: 1 }), { b: 2 });
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,806 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { applyFilter, toggleMenu } from "@web/../tests/search/helpers";
|
||||
import { DatePicker, DateTimePicker } from "@web/core/datepicker/datepicker";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import ActionModel from "web.ActionModel";
|
||||
import CustomFilterItem from "web.CustomFilterItem";
|
||||
import { createComponent } from "web.test_utils";
|
||||
import { editSelect } from "web.test_utils_fields";
|
||||
import { registerCleanup } from "../helpers/cleanup";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { makeFakeLocalizationService } from "../helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
getFixture,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
} from "../helpers/utils";
|
||||
|
||||
const { DateTime, Settings } = luxon;
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
|
||||
/**
|
||||
* @param {typeof DatePicker} Picker
|
||||
* @param {Object} props
|
||||
* @returns {Promise<DatePicker>}
|
||||
*/
|
||||
async function mountPicker(Picker, props) {
|
||||
serviceRegistry
|
||||
.add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({
|
||||
dateFormat: "dd/MM/yyyy",
|
||||
dateTimeFormat: "dd/MM/yyyy HH:mm:ss",
|
||||
})
|
||||
)
|
||||
.add("ui", uiService)
|
||||
.add("hotkey", hotkeyService);
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState(props);
|
||||
}
|
||||
|
||||
onDateChange(date) {
|
||||
if (props.onDateTimeChanged) {
|
||||
props.onDateTimeChanged(date);
|
||||
}
|
||||
this.state.date = date;
|
||||
}
|
||||
}
|
||||
Parent.template = xml/* xml */ `
|
||||
<t t-component="props.Picker" t-props="state" onDateTimeChanged.bind="onDateChange" />
|
||||
`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
return await mount(Parent, target, { env, props: { Picker } });
|
||||
}
|
||||
|
||||
function useFRLocale() {
|
||||
if (!window.moment.locales().includes("fr")) {
|
||||
// Mocks the FR locale if not loaded
|
||||
const originalLocale = window.moment.locale();
|
||||
window.moment.defineLocale("fr", {
|
||||
months: "janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split(
|
||||
"_"
|
||||
),
|
||||
monthsShort: "janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split(
|
||||
"_"
|
||||
),
|
||||
code: "fr",
|
||||
monthsParseExact: true,
|
||||
week: { dow: 1, doy: 4 },
|
||||
});
|
||||
// Moment automatically assigns newly defined locales.
|
||||
window.moment.locale(originalLocale);
|
||||
registerCleanup(() => window.moment.updateLocale("fr", null));
|
||||
}
|
||||
return "fr";
|
||||
}
|
||||
|
||||
var symbolMap = {
|
||||
1: "૧",
|
||||
2: "૨",
|
||||
3: "૩",
|
||||
4: "૪",
|
||||
5: "૫",
|
||||
6: "૬",
|
||||
7: "૭",
|
||||
8: "૮",
|
||||
9: "૯",
|
||||
0: "૦",
|
||||
};
|
||||
var numberMap = {
|
||||
"૧": "1",
|
||||
"૨": "2",
|
||||
"૩": "3",
|
||||
"૪": "4",
|
||||
"૫": "5",
|
||||
"૬": "6",
|
||||
"૭": "7",
|
||||
"૮": "8",
|
||||
"૯": "9",
|
||||
"૦": "0",
|
||||
};
|
||||
|
||||
function useGULocale() {
|
||||
if (!window.moment.locales().includes("gu")) {
|
||||
const originalLocale = window.moment.locale();
|
||||
window.moment.defineLocale("gu", {
|
||||
months: "જાન્યુઆરી_ફેબ્રુઆરી_માર્ચ_એપ્રિલ_મે_જૂન_જુલાઈ_ઑગસ્ટ_સપ્ટેમ્બર_ઑક્ટ્બર_નવેમ્બર_ડિસેમ્બર".split(
|
||||
"_"
|
||||
),
|
||||
monthsShort: "જાન્યુ._ફેબ્રુ._માર્ચ_એપ્રિ._મે_જૂન_જુલા._ઑગ._સપ્ટે._ઑક્ટ્._નવે._ડિસે.".split(
|
||||
"_"
|
||||
),
|
||||
monthsParseExact: true,
|
||||
week: {
|
||||
dow: 0, // Sunday is the first day of the week.
|
||||
doy: 6, // The week that contains Jan 1st is the first week of the year.
|
||||
},
|
||||
preparse: function (string) {
|
||||
return string.replace(/[૧૨૩૪૫૬૭૮૯૦]/g, function (match) {
|
||||
return numberMap[match];
|
||||
});
|
||||
},
|
||||
postformat: function (string) {
|
||||
return string.replace(/\d/g, function (match) {
|
||||
return symbolMap[match];
|
||||
});
|
||||
},
|
||||
});
|
||||
// Moment automatically assigns newly defined locales.
|
||||
window.moment.locale(originalLocale);
|
||||
registerCleanup(() => window.moment.updateLocale("gu", null));
|
||||
}
|
||||
return "gu";
|
||||
}
|
||||
|
||||
function useNOLocale() {
|
||||
if (!window.moment.locales().includes("nb")) {
|
||||
const originalLocale = window.moment.locale();
|
||||
window.moment.defineLocale("nb", {
|
||||
months: "januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split(
|
||||
"_"
|
||||
),
|
||||
monthsShort: "jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.".split("_"),
|
||||
monthsParseExact: true,
|
||||
week: {
|
||||
dow: 1, // Monday is the first day of the week.
|
||||
doy: 4, // The week that contains Jan 4th is the first week of the year.
|
||||
},
|
||||
});
|
||||
// Moment automatically assigns newly defined locales.
|
||||
window.moment.locale(originalLocale);
|
||||
registerCleanup(() => window.moment.updateLocale("nb", null));
|
||||
}
|
||||
return "nb";
|
||||
}
|
||||
|
||||
QUnit.module("Components", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("DatePicker");
|
||||
|
||||
QUnit.test("basic rendering", async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
|
||||
});
|
||||
|
||||
assert.containsOnce(target, "input.o_input.o_datepicker_input");
|
||||
assert.containsOnce(target, "span.o_datepicker_button");
|
||||
assert.containsNone(document.body, "div.bootstrap-datetimepicker-widget");
|
||||
|
||||
const datePicker = target.querySelector(".o_datepicker");
|
||||
const input = datePicker.querySelector("input.o_input.o_datepicker_input");
|
||||
assert.strictEqual(input.value, "09/01/1997", "Value should be the one given");
|
||||
assert.strictEqual(
|
||||
datePicker.dataset.targetInput,
|
||||
`#${datePicker.querySelector("input[type=hidden]").id}`,
|
||||
"DatePicker id should match its input target"
|
||||
);
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.containsOnce(document.body, "div.bootstrap-datetimepicker-widget .datepicker");
|
||||
assert.containsNone(document.body, "div.bootstrap-datetimepicker-widget .timepicker");
|
||||
assert.strictEqual(
|
||||
document.querySelector(".datepicker .day.active").dataset.day,
|
||||
"01/09/1997",
|
||||
"Datepicker should have set the correct day"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"08/02/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
await click(input);
|
||||
await click(document.querySelector(".datepicker th.next")); // next month
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await click(document.querySelectorAll(".datepicker table td")[15]); // previous day
|
||||
|
||||
assert.strictEqual(input.value, "08/02/1997");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date with locale (locale given in props)", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
|
||||
format: "dd MMM, yyyy",
|
||||
locale: useFRLocale(),
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"01/09/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "09 janv., 1997");
|
||||
|
||||
await click(input);
|
||||
await click(document.querySelector(".datepicker .picker-switch")); // month picker
|
||||
await click(document.querySelectorAll(".datepicker .month")[8]); // september
|
||||
await click(document.querySelector(".datepicker .day")); // first day
|
||||
|
||||
assert.strictEqual(input.value, "01 sept., 1997");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date with locale (locale from date props)", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", {
|
||||
zone: "utc",
|
||||
locale: useFRLocale(),
|
||||
}),
|
||||
format: "dd MMM, yyyy",
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"01/09/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "09 janv., 1997");
|
||||
|
||||
await click(input);
|
||||
await click(document.querySelector(".datepicker .picker-switch")); // month picker
|
||||
await click(document.querySelectorAll(".datepicker .month")[8]); // september
|
||||
await click(document.querySelector(".datepicker .day")); // first day
|
||||
|
||||
assert.strictEqual(input.value, "01 sept., 1997");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date with locale (locale with different symbols)", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", {
|
||||
zone: "utc",
|
||||
locale: useGULocale(),
|
||||
}),
|
||||
format: "dd MMM, yyyy",
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"01/09/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "09 જાન્યુ, 1997");
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "09 જાન્યુ, 1997");
|
||||
|
||||
await click(document.querySelector(".datepicker .picker-switch")); // month picker
|
||||
await click(document.querySelectorAll(".datepicker .month")[8]); // september
|
||||
await click(document.querySelectorAll(".datepicker .day")[1]); // first day of september
|
||||
|
||||
assert.strictEqual(input.value, "01 સપ્ટે, 1997");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("enter a date value", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"08/02/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
input.value = "08/02/1997";
|
||||
await triggerEvent(target, ".o_datepicker_input", "change");
|
||||
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(
|
||||
document.querySelector(".datepicker .day.active").dataset.day,
|
||||
"02/08/1997",
|
||||
"Datepicker should have set the correct day"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Date format is correctly set", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
|
||||
format: "yyyy/MM/dd",
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "1997/01/09");
|
||||
|
||||
// Forces an update to assert that the registered format is the correct one
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "1997/01/09");
|
||||
});
|
||||
|
||||
QUnit.test("Validate input date with 'Enter'", async (assert) => {
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
|
||||
format: "dd/MM/yyyy",
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "09/01/1997");
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
|
||||
input.value = "23/03/2022";
|
||||
await triggerEvent(input, null, "keydown", { key: "Enter" });
|
||||
|
||||
assert.strictEqual(input.value, "23/03/2022");
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
});
|
||||
|
||||
QUnit.test("Validate input date with 'Escape'", async (assert) => {
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
|
||||
format: "dd/MM/yyyy",
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "09/01/1997");
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
|
||||
input.value = "23/03/2022";
|
||||
await triggerEvent(input, null, "keydown", { key: "Escape" });
|
||||
|
||||
assert.strictEqual(input.value, "23/03/2022");
|
||||
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
|
||||
});
|
||||
|
||||
QUnit.module("DateTimePicker");
|
||||
|
||||
QUnit.test("basic rendering", async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
});
|
||||
|
||||
assert.containsOnce(target, "input.o_input.o_datepicker_input");
|
||||
assert.containsOnce(target, "span.o_datepicker_button");
|
||||
assert.containsNone(document.body, "div.bootstrap-datetimepicker-widget");
|
||||
|
||||
const datePicker = target.querySelector(".o_datepicker");
|
||||
const input = datePicker.querySelector("input.o_input.o_datepicker_input");
|
||||
assert.strictEqual(input.value, "09/01/1997 12:30:01", "Value should be the one given");
|
||||
assert.strictEqual(
|
||||
datePicker.dataset.targetInput,
|
||||
`#${datePicker.querySelector("input[type=hidden]").id}`,
|
||||
"DateTimePicker id should match its input target"
|
||||
);
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.containsOnce(document.body, "div.bootstrap-datetimepicker-widget .datepicker");
|
||||
assert.containsOnce(document.body, "div.bootstrap-datetimepicker-widget .timepicker");
|
||||
assert.strictEqual(
|
||||
document.querySelector(".datepicker .day.active").dataset.day,
|
||||
"01/09/1997",
|
||||
"Datepicker should have set the correct day"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
document.querySelector(".timepicker .timepicker-hour").innerText.trim(),
|
||||
"12",
|
||||
"Datepicker should have set the correct hour"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector(".timepicker .timepicker-minute").innerText.trim(),
|
||||
"30",
|
||||
"Datepicker should have set the correct minute"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector(".timepicker .timepicker-second").innerText.trim(),
|
||||
"01",
|
||||
"Datepicker should have set the correct second"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date and time", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy HH:mm:ss"),
|
||||
"08/02/1997 15:45:05",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
const input = target.querySelector("input.o_input.o_datepicker_input");
|
||||
|
||||
await click(input);
|
||||
await click(document.querySelector(".datepicker th.next")); // February
|
||||
await click(document.querySelectorAll(".datepicker table td")[15]); // 08
|
||||
await click(document.querySelector('a[title="Select Time"]'));
|
||||
await click(document.querySelector(".timepicker .timepicker-hour"));
|
||||
await click(document.querySelectorAll(".timepicker .hour")[15]); // 15h
|
||||
await click(document.querySelector(".timepicker .timepicker-minute"));
|
||||
await click(document.querySelectorAll(".timepicker .minute")[9]); // 45m
|
||||
await click(document.querySelector(".timepicker .timepicker-second"));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await click(document.querySelectorAll(".timepicker .second")[1]); // 05s
|
||||
|
||||
assert.strictEqual(input.value, "08/02/1997 15:45:05");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date and time with locale", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
format: "dd MMM, yyyy HH:mm:ss",
|
||||
locale: useFRLocale(),
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy HH:mm:ss"),
|
||||
"01/09/1997 15:45:05",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const input = target.querySelector("input.o_input.o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "09 janv., 1997 12:30:01");
|
||||
|
||||
await click(input);
|
||||
|
||||
await click(document.querySelector(".datepicker .picker-switch")); // month picker
|
||||
await click(document.querySelectorAll(".datepicker .month")[8]); // september
|
||||
await click(document.querySelector(".datepicker .day")); // first day
|
||||
|
||||
await click(document.querySelector('a[title="Select Time"]'));
|
||||
await click(document.querySelector(".timepicker .timepicker-hour"));
|
||||
await click(document.querySelectorAll(".timepicker .hour")[15]); // 15h
|
||||
await click(document.querySelector(".timepicker .timepicker-minute"));
|
||||
await click(document.querySelectorAll(".timepicker .minute")[9]); // 45m
|
||||
await click(document.querySelector(".timepicker .timepicker-second"));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await click(document.querySelectorAll(".timepicker .second")[1]); // 05s
|
||||
|
||||
assert.strictEqual(input.value, "01 sept., 1997 15:45:05");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("pick a time with 12 hour format locale", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997 08:30:01", "dd/MM/yyyy hh:mm:ss"),
|
||||
format: "dd/MM/yyyy hh:mm:ss",
|
||||
locale: useFRLocale(),
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy HH:mm:ss"),
|
||||
"09/01/1997 20:30:02",
|
||||
"The new time should be in the afternoon"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const input = target.querySelector("input.o_input.o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "09/01/1997 08:30:01");
|
||||
|
||||
await click(input);
|
||||
|
||||
await click(document.querySelector('a[title="Select Time"]'));
|
||||
await click(document.querySelector('a[title="Increment Second"]'));
|
||||
await click(document.querySelector('button[title="Toggle Period"]'));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await click(document.querySelector('a[title="Close the picker"]'));
|
||||
|
||||
assert.strictEqual(input.value, "09/01/1997 08:30:02");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("enter a datetime value", async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy HH:mm:ss"),
|
||||
"08/02/1997 15:45:05",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
input.value = "08/02/1997 15:45:05";
|
||||
await triggerEvent(target, ".o_datepicker_input", "change");
|
||||
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "08/02/1997 15:45:05");
|
||||
assert.strictEqual(
|
||||
document.querySelector(".datepicker .day.active").dataset.day,
|
||||
"02/08/1997",
|
||||
"Datepicker should have set the correct day"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector(".timepicker .timepicker-hour").innerText.trim(),
|
||||
"15",
|
||||
"Datepicker should have set the correct hour"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector(".timepicker .timepicker-minute").innerText.trim(),
|
||||
"45",
|
||||
"Datepicker should have set the correct minute"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector(".timepicker .timepicker-second").innerText.trim(),
|
||||
"05",
|
||||
"Datepicker should have set the correct second"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Date time format is correctly set", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
format: "HH:mm:ss yyyy/MM/dd",
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "12:30:01 1997/01/09");
|
||||
|
||||
// Forces an update to assert that the registered format is the correct one
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "12:30:01 1997/01/09");
|
||||
});
|
||||
|
||||
QUnit.test("Datepicker works with norwegian locale", async (assert) => {
|
||||
assert.expect(6);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/04/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
format: "dd MMM, yyyy",
|
||||
locale: useNOLocale(),
|
||||
onDateTimeChanged(date) {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"01/04/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "09 apr., 1997");
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "09 apr., 1997");
|
||||
|
||||
const days = [...document.querySelectorAll(".datepicker .day")];
|
||||
await click(days.find((d) => d.innerText.trim() === "1")); // first day of april
|
||||
|
||||
assert.strictEqual(input.value, "01 apr., 1997");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("Datepicker works with dots and commas in format", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
date: DateTime.fromFormat("10/03/2023 13:14:27", "dd/MM/yyyy HH:mm:ss"),
|
||||
format: "dd.MM,yyyy",
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "10.03,2023");
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "10.03,2023");
|
||||
});
|
||||
|
||||
QUnit.test("custom filter date", async function (assert) {
|
||||
assert.expect(3);
|
||||
class MockedSearchModel extends ActionModel {
|
||||
dispatch(method, ...args) {
|
||||
assert.strictEqual(method, "createNewFilters");
|
||||
const preFilters = args[0];
|
||||
const preFilter = preFilters[0];
|
||||
assert.strictEqual(
|
||||
preFilter.description,
|
||||
'A date is equal to "05/05/2005"',
|
||||
"description should be in localized format"
|
||||
);
|
||||
assert.deepEqual(
|
||||
preFilter.domain,
|
||||
'[["date_field","=","2005-05-05"]]',
|
||||
"domain should be in UTC format"
|
||||
);
|
||||
}
|
||||
}
|
||||
const searchModel = new MockedSearchModel();
|
||||
const date_field = { name: "date_field", string: "A date", type: "date", searchable: true };
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: { date_field },
|
||||
},
|
||||
env: { searchModel },
|
||||
});
|
||||
await toggleMenu(target, "Add Custom Filter");
|
||||
await editSelect(target.querySelector(".o_generator_menu_field"), "date_field");
|
||||
const valueInput = target.querySelector(".o_generator_menu_value .o_input");
|
||||
await click(valueInput);
|
||||
await editSelect(valueInput, "05/05/2005");
|
||||
await applyFilter(target);
|
||||
});
|
||||
|
||||
QUnit.test("start with no value", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
onDateTimeChanged(date) {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy HH:mm:ss"),
|
||||
"08/02/1997 15:45:05",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
assert.strictEqual(input.value, "");
|
||||
|
||||
assert.verifySteps([]);
|
||||
input.value = "08/02/1997 15:45:05";
|
||||
await triggerEvent(target, ".o_datepicker_input", "change");
|
||||
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
assert.strictEqual(input.value, "08/02/1997 15:45:05");
|
||||
});
|
||||
|
||||
QUnit.test("arab locale, latin numbering system as input", async (assert) => {
|
||||
const dateFormat = "dd MMM, yyyy";
|
||||
const timeFormat = "hh:mm:ss";
|
||||
const dateTimeFormat = `${dateFormat} ${timeFormat}`;
|
||||
|
||||
patchWithCleanup(localization, { dateFormat, timeFormat, dateTimeFormat });
|
||||
patchWithCleanup(Settings, {
|
||||
defaultLocale: "ar-001",
|
||||
defaultNumberingSystem: "arab",
|
||||
});
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
format: dateTimeFormat,
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
await editInput(input, null, "٠٤ يونيو, ٢٠٢٣ ١١:٣٣:٠٠");
|
||||
|
||||
assert.strictEqual(input.value, "٠٤ يونيو, ٢٠٢٣ ١١:٣٣:٠٠");
|
||||
|
||||
await editInput(input, null, "15 07, 2020 12:30:43");
|
||||
|
||||
assert.strictEqual(input.value, "١٥ يوليو, ٢٠٢٠ ١٢:٣٠:٤٣");
|
||||
});
|
||||
|
||||
QUnit.test("keep date between component and datepicker in sync", async (assert) => {
|
||||
const parent = await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
format: "dd/MM/yyyy",
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
assert.strictEqual(input.value, "09/01/1997");
|
||||
await nextTick();
|
||||
|
||||
await click(input);
|
||||
assert.hasClass(document.querySelector("td.day[data-day='01/09/1997']"), "active");
|
||||
|
||||
// Change the date of the component externally (not through the
|
||||
// datepicker interface)
|
||||
parent.state.date = parent.state.date.plus({ days: 1 });
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(input.value, "10/01/1997");
|
||||
assert.hasClass(document.querySelector("td.day[data-day='01/10/1997']"), "active");
|
||||
|
||||
parent.state.date = false;
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(input.value, "");
|
||||
assert.containsN(document.body, "td.day", 42);
|
||||
assert.containsNone(document.body, "td.day.active");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,884 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { DebugMenu } from "@web/core/debug/debug_menu";
|
||||
import { regenerateAssets, becomeSuperuser } from "@web/core/debug/debug_menu_items";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useDebugCategory, useOwnDebugContext } from "@web/core/debug/debug_context";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { useSetupView } from "@web/views/view_hook";
|
||||
import { ActionDialog } from "@web/webclient/actions/action_dialog";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { makeTestEnv, utils } from "../../helpers/mock_env";
|
||||
import {
|
||||
fakeCompanyService,
|
||||
fakeCommandService,
|
||||
makeFakeDialogService,
|
||||
makeFakeLocalizationService,
|
||||
makeFakeUserService,
|
||||
} from "../../helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
getNodesTextContent,
|
||||
legacyExtraNextTick,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "../../helpers/utils";
|
||||
import { createWebClient, doAction, getActionManagerServerData } from "../../webclient/helpers";
|
||||
import { openViewItem } from "@web/webclient/debug_items";
|
||||
import { editSearchView, editView, setDefaults, viewMetadata } from "@web/views/debug_items";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
const { prepareRegistriesWithCleanup } = utils;
|
||||
|
||||
export class DebugMenuParent extends Component {
|
||||
setup() {
|
||||
useOwnDebugContext({ categories: ["default", "custom"] });
|
||||
}
|
||||
}
|
||||
DebugMenuParent.template = xml`<DebugMenu/>`;
|
||||
DebugMenuParent.components = { DebugMenu };
|
||||
|
||||
const debugRegistry = registry.category("debug");
|
||||
let target;
|
||||
let testConfig;
|
||||
|
||||
QUnit.module("DebugMenu", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
target = getFixture();
|
||||
registry
|
||||
.category("services")
|
||||
.add("hotkey", hotkeyService)
|
||||
.add("ui", uiService)
|
||||
.add("orm", ormService)
|
||||
.add("dialog", makeFakeDialogService())
|
||||
.add("localization", makeFakeLocalizationService())
|
||||
.add("command", fakeCommandService);
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
testConfig = { mockRPC };
|
||||
});
|
||||
QUnit.test("can be rendered", async (assert) => {
|
||||
debugRegistry
|
||||
.category("default")
|
||||
.add("item_1", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 1",
|
||||
callback: () => {
|
||||
assert.step("callback item_1");
|
||||
},
|
||||
sequence: 10,
|
||||
};
|
||||
})
|
||||
.add("item_2", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 2",
|
||||
callback: () => {
|
||||
assert.step("callback item_2");
|
||||
},
|
||||
sequence: 5,
|
||||
};
|
||||
})
|
||||
.add("item_3", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 3",
|
||||
callback: () => {
|
||||
assert.step("callback item_3");
|
||||
},
|
||||
};
|
||||
})
|
||||
.add("separator", () => {
|
||||
return {
|
||||
type: "separator",
|
||||
sequence: 20,
|
||||
};
|
||||
})
|
||||
.add("separator_2", () => {
|
||||
return null;
|
||||
})
|
||||
.add("item_4", () => {
|
||||
return null;
|
||||
});
|
||||
const env = await makeTestEnv(testConfig);
|
||||
await mount(DebugMenuParent, target, { env });
|
||||
await click(target.querySelector("button.dropdown-toggle"));
|
||||
assert.containsN(target, ".dropdown-menu .dropdown-item", 3);
|
||||
assert.containsOnce(target, ".dropdown-divider");
|
||||
const children = [...(target.querySelector(".dropdown-menu").children || [])];
|
||||
assert.deepEqual(
|
||||
children.map((el) => el.tagName),
|
||||
["SPAN", "SPAN", "DIV", "SPAN"]
|
||||
);
|
||||
const items = [...target.querySelectorAll(".dropdown-menu .dropdown-item")] || [];
|
||||
assert.deepEqual(
|
||||
items.map((el) => el.textContent),
|
||||
["Item 2", "Item 1", "Item 3"]
|
||||
);
|
||||
for (const item of items) {
|
||||
click(item);
|
||||
}
|
||||
assert.verifySteps(["callback item_2", "callback item_1", "callback item_3"]);
|
||||
});
|
||||
|
||||
QUnit.test("items are sorted by sequence regardless of category", async (assert) => {
|
||||
debugRegistry
|
||||
.category("default")
|
||||
.add("item_1", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 4",
|
||||
sequence: 4,
|
||||
};
|
||||
})
|
||||
.add("item_2", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 1",
|
||||
sequence: 1,
|
||||
};
|
||||
});
|
||||
debugRegistry
|
||||
.category("custom")
|
||||
.add("item_1", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 3",
|
||||
sequence: 3,
|
||||
};
|
||||
})
|
||||
.add("item_2", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 2",
|
||||
sequence: 2,
|
||||
};
|
||||
});
|
||||
const env = await makeTestEnv(testConfig);
|
||||
await mount(DebugMenuParent, target, { env });
|
||||
await click(target.querySelector("button.dropdown-toggle"));
|
||||
const items = [...target.querySelectorAll(".dropdown-menu .dropdown-item")];
|
||||
assert.deepEqual(
|
||||
items.map((el) => el.textContent),
|
||||
["Item 1", "Item 2", "Item 3", "Item 4"]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Don't display the DebugMenu if debug mode is disabled", async (assert) => {
|
||||
const env = await makeTestEnv(testConfig);
|
||||
env.dialogData = {
|
||||
isActive: true,
|
||||
close() {},
|
||||
};
|
||||
await mount(ActionDialog, target, {
|
||||
env,
|
||||
props: { close: () => {} },
|
||||
});
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.containsNone(target, ".o_dialog .o_debug_manager .fa-bug");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"Display the DebugMenu correctly in a ActionDialog if debug mode is enabled",
|
||||
async (assert) => {
|
||||
assert.expect(8);
|
||||
debugRegistry.category("default").add("global", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Global 1",
|
||||
callback: () => {
|
||||
assert.step("callback global_1");
|
||||
},
|
||||
sequence: 0,
|
||||
};
|
||||
});
|
||||
debugRegistry
|
||||
.category("custom")
|
||||
.add("item1", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 1",
|
||||
callback: () => {
|
||||
assert.step("callback item_1");
|
||||
},
|
||||
sequence: 10,
|
||||
};
|
||||
})
|
||||
.add("item2", ({ customKey }) => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 2",
|
||||
callback: () => {
|
||||
assert.step("callback item_2");
|
||||
assert.strictEqual(customKey, "abc");
|
||||
},
|
||||
sequence: 20,
|
||||
};
|
||||
});
|
||||
class WithCustom extends ActionDialog {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
useDebugCategory("custom", { customKey: "abc" });
|
||||
}
|
||||
}
|
||||
patchWithCleanup(odoo, { debug: "1" });
|
||||
const env = await makeTestEnv(testConfig);
|
||||
env.dialogData = {
|
||||
isActive: true,
|
||||
close() {},
|
||||
};
|
||||
await mount(WithCustom, target, {
|
||||
env,
|
||||
props: { close: () => {} },
|
||||
});
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.containsOnce(target, ".o_dialog .o_debug_manager .fa-bug");
|
||||
await click(target, ".o_dialog .o_debug_manager button");
|
||||
const debugManagerEl = target.querySelector(".o_debug_manager");
|
||||
assert.containsN(debugManagerEl, ".dropdown-menu .dropdown-item", 2);
|
||||
// Check that global debugManager elements are not displayed (global_1)
|
||||
const items =
|
||||
[...debugManagerEl.querySelectorAll(".dropdown-menu .dropdown-item")] || [];
|
||||
assert.deepEqual(
|
||||
items.map((el) => el.textContent),
|
||||
["Item 1", "Item 2"]
|
||||
);
|
||||
for (const item of items) {
|
||||
click(item);
|
||||
}
|
||||
assert.verifySteps(["callback item_1", "callback item_2"]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("can regenerate assets bundles", async (assert) => {
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/ir.attachment/regenerate_assets_bundles") {
|
||||
assert.step("ir.attachment/regenerate_assets_bundles");
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
testConfig = { mockRPC };
|
||||
patchWithCleanup(browser, {
|
||||
location: {
|
||||
reload: () => assert.step("reloadPage"),
|
||||
},
|
||||
});
|
||||
debugRegistry.category("default").add("regenerateAssets", regenerateAssets);
|
||||
const env = await makeTestEnv(testConfig);
|
||||
await mount(DebugMenuParent, target, { env });
|
||||
await click(target.querySelector("button.dropdown-toggle"));
|
||||
assert.containsOnce(target, ".dropdown-menu .dropdown-item");
|
||||
const item = target.querySelector(".dropdown-menu .dropdown-item");
|
||||
assert.strictEqual(item.textContent, "Regenerate Assets Bundles");
|
||||
await click(item);
|
||||
assert.verifySteps(["ir.attachment/regenerate_assets_bundles", "reloadPage"]);
|
||||
});
|
||||
|
||||
QUnit.test("cannot acess the Become superuser menu if not admin", async (assert) => {
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
debugRegistry.category("default").add("becomeSuperuser", becomeSuperuser);
|
||||
|
||||
testConfig = { mockRPC };
|
||||
const env = await makeTestEnv(testConfig);
|
||||
env.services.user.isAdmin = false;
|
||||
await mount(DebugMenuParent, target, { env });
|
||||
|
||||
await click(target.querySelector("button.dropdown-toggle"));
|
||||
assert.containsNone(target, ".dropdown-menu .dropdown-item");
|
||||
});
|
||||
|
||||
QUnit.test("can open a view", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
prepareRegistriesWithCleanup();
|
||||
registry.category("services").add("company", fakeCompanyService);
|
||||
|
||||
patchWithCleanup(odoo, {
|
||||
debug: true,
|
||||
});
|
||||
|
||||
registry.category("debug").category("default").add("openViewItem", openViewItem);
|
||||
|
||||
const serverData = getActionManagerServerData();
|
||||
Object.assign(serverData.models, {
|
||||
"ir.ui.view": {
|
||||
fields: {
|
||||
model: { type: "char" },
|
||||
name: { type: "char" },
|
||||
type: { type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: "formView",
|
||||
model: "partner",
|
||||
type: "form",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
Object.assign(serverData.views, {
|
||||
"ir.ui.view,false,list": `<list><field name="name"/><field name="type"/></list>`,
|
||||
"ir.ui.view,false,search": `<search/>`,
|
||||
"partner,1,form": `<form><div class="some_view"/></form>`,
|
||||
});
|
||||
|
||||
await createWebClient({ serverData, mockRPC });
|
||||
await click(target.querySelector(".o_debug_manager button"));
|
||||
await click(target.querySelector(".o_debug_manager .dropdown-item"));
|
||||
assert.containsOnce(target, ".modal .o_list_view");
|
||||
|
||||
await click(target.querySelector(".modal .o_list_view .o_data_row td"));
|
||||
assert.containsNone(target, ".modal");
|
||||
assert.containsOnce(target, ".some_view");
|
||||
});
|
||||
|
||||
QUnit.test("can edit a pivot view", async (assert) => {
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
prepareRegistriesWithCleanup();
|
||||
|
||||
patchWithCleanup(odoo, {
|
||||
debug: true,
|
||||
});
|
||||
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
registry.category("debug").category("view").add("editViewItem", editView);
|
||||
|
||||
const serverData = getActionManagerServerData();
|
||||
serverData.actions[1234] = {
|
||||
id: 1234,
|
||||
xml_id: "action_1234",
|
||||
name: "Reporting Ponies",
|
||||
res_model: "pony",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[18, "pivot"]],
|
||||
};
|
||||
serverData.views["pony,18,pivot"] = "<pivot></pivot>";
|
||||
serverData.models["ir.ui.view"] = {
|
||||
fields: {},
|
||||
records: [{ id: 18 }],
|
||||
};
|
||||
serverData.views["ir.ui.view,false,form"] = `<form><field name="id"/></form>`;
|
||||
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 1234);
|
||||
await click(target.querySelector(".o_debug_manager button"));
|
||||
await click(target.querySelector(".o_debug_manager .dropdown-item"));
|
||||
assert.containsOnce(target, ".modal .o_form_view");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".modal .o_form_view .o_field_widget[name=id] input").value,
|
||||
"18"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("can edit a search view", async (assert) => {
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
prepareRegistriesWithCleanup();
|
||||
registry.category("services").add("company", fakeCompanyService);
|
||||
|
||||
patchWithCleanup(odoo, {
|
||||
debug: true,
|
||||
});
|
||||
|
||||
registry.category("debug").category("view").add("editSearchViewItem", editSearchView);
|
||||
|
||||
const serverData = getActionManagerServerData();
|
||||
|
||||
serverData.views["partner,293,search"] = "<search></search>";
|
||||
serverData.actions[1].search_view_id = [293, "some_search_view"];
|
||||
serverData.models["ir.ui.view"] = {
|
||||
fields: {},
|
||||
records: [{ id: 293 }],
|
||||
};
|
||||
serverData.views["ir.ui.view,false,form"] = `<form><field name="id"/></form>`;
|
||||
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 1);
|
||||
await click(target.querySelector(".o_debug_manager button"));
|
||||
await click(target.querySelector(".o_debug_manager .dropdown-item"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(target, ".modal .o_form_view");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".modal .o_form_view .o_field_widget[name=id] input").value,
|
||||
"293"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("edit search view on action without search_view_id", async (assert) => {
|
||||
// When the kanban view will be converted to Owl, this test could be simplified by
|
||||
// removing the toy view and using the kanban view directly
|
||||
prepareRegistriesWithCleanup();
|
||||
|
||||
class ToyController extends Component {
|
||||
setup() {
|
||||
useSetupView();
|
||||
}
|
||||
}
|
||||
ToyController.template = xml`<div class="o-toy-view"/>`;
|
||||
|
||||
registry.category("views").add("toy", {
|
||||
type: "toy",
|
||||
display_name: "toy view",
|
||||
Controller: ToyController,
|
||||
});
|
||||
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
|
||||
patchWithCleanup(odoo, {
|
||||
debug: true,
|
||||
});
|
||||
|
||||
registry.category("debug").category("view").add("editSearchViewItem", editSearchView);
|
||||
|
||||
const serverData = getActionManagerServerData();
|
||||
serverData.actions[1] = {
|
||||
id: 1,
|
||||
xml_id: "action_1",
|
||||
name: "Partners Action 1",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "toy"]],
|
||||
search_view_id: false,
|
||||
};
|
||||
serverData.models["ir.ui.view"] = {
|
||||
fields: {},
|
||||
records: [{ id: 293 }],
|
||||
};
|
||||
serverData.views = {};
|
||||
serverData.views["ir.ui.view,false,form"] = `<form><field name="id"/></form>`;
|
||||
serverData.views["partner,false,toy"] = `<toy></toy>`;
|
||||
serverData.views["partner,293,search"] = `<search></search>`;
|
||||
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 1);
|
||||
assert.containsOnce(target, ".o-toy-view");
|
||||
|
||||
await click(target.querySelector(".o_debug_manager button"));
|
||||
await click(target.querySelector(".o_debug_manager .dropdown-item"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(target, ".modal .o_form_view");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".modal .o_form_view .o_field_widget[name=id] input").value,
|
||||
"293"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"cannot edit the control panel of a form view contained in a dialog without control panel.",
|
||||
async (assert) => {
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
prepareRegistriesWithCleanup();
|
||||
|
||||
patchWithCleanup(odoo, {
|
||||
debug: true,
|
||||
});
|
||||
registry.category("debug").category("view").add("editSearchViewItem", editSearchView);
|
||||
|
||||
const serverData = getActionManagerServerData();
|
||||
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
// opens a form view in a dialog without a control panel.
|
||||
await doAction(webClient, 5);
|
||||
await click(target.querySelector(".o_dialog .o_debug_manager button"));
|
||||
assert.containsNone(target, ".o_debug_manager .dropdown-item");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("set defaults: basic rendering", async (assert) => {
|
||||
prepareRegistriesWithCleanup();
|
||||
patchWithCleanup(odoo, {
|
||||
debug: true,
|
||||
});
|
||||
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
registry.category("debug").category("form").add("setDefaults", setDefaults);
|
||||
|
||||
const serverData = getActionManagerServerData();
|
||||
serverData.actions[1234] = {
|
||||
id: 1234,
|
||||
xml_id: "action_1234",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
res_id: 1,
|
||||
type: "ir.actions.act_window",
|
||||
views: [[18, "form"]],
|
||||
};
|
||||
serverData.views["partner,18,form"] = `
|
||||
<form>
|
||||
<field name="m2o"/>
|
||||
<field name="foo"/>
|
||||
<field name="o2m"/>
|
||||
</form>`;
|
||||
serverData.models["ir.ui.view"] = {
|
||||
fields: {},
|
||||
records: [{ id: 18 }],
|
||||
};
|
||||
serverData.models.partner.records = [{ id: 1, display_name: "p1", foo: "hello" }];
|
||||
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 1234);
|
||||
await click(target.querySelector(".o_debug_manager button"));
|
||||
await click(target.querySelector(".o_debug_manager .dropdown-item"));
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.containsOnce(target, ".modal select#formview_default_fields");
|
||||
assert.containsN(target.querySelector(".modal #formview_default_fields"), "option", 2);
|
||||
const options = target.querySelectorAll(".modal #formview_default_fields option");
|
||||
assert.strictEqual(options[0].value, "");
|
||||
assert.strictEqual(options[1].value, "foo");
|
||||
});
|
||||
|
||||
QUnit.test("set defaults: click close", async (assert) => {
|
||||
prepareRegistriesWithCleanup();
|
||||
patchWithCleanup(odoo, {
|
||||
debug: true,
|
||||
});
|
||||
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
registry.category("debug").category("form").add("setDefaults", setDefaults);
|
||||
|
||||
const serverData = getActionManagerServerData();
|
||||
serverData.actions[1234] = {
|
||||
id: 1234,
|
||||
xml_id: "action_1234",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
res_id: 1,
|
||||
type: "ir.actions.act_window",
|
||||
views: [[18, "form"]],
|
||||
};
|
||||
serverData.views["partner,18,form"] = `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
</form>`;
|
||||
serverData.models["ir.ui.view"] = {
|
||||
fields: {},
|
||||
records: [{ id: 18 }],
|
||||
};
|
||||
serverData.models.partner.records = [{ id: 1, display_name: "p1", foo: "hello" }];
|
||||
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "set" && args.model === "ir.default") {
|
||||
throw new Error("should not create a default");
|
||||
}
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 1234);
|
||||
await click(target.querySelector(".o_debug_manager button"));
|
||||
await click(target.querySelector(".o_debug_manager .dropdown-item"));
|
||||
assert.containsOnce(target, ".modal");
|
||||
|
||||
await click(target.querySelector(".modal .modal-footer button"));
|
||||
assert.containsNone(target, ".modal");
|
||||
});
|
||||
|
||||
QUnit.test("set defaults: select and save", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
prepareRegistriesWithCleanup();
|
||||
patchWithCleanup(odoo, {
|
||||
debug: true,
|
||||
});
|
||||
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
registry.category("debug").category("form").add("setDefaults", setDefaults);
|
||||
|
||||
const serverData = getActionManagerServerData();
|
||||
serverData.actions[1234] = {
|
||||
id: 1234,
|
||||
xml_id: "action_1234",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
res_id: 1,
|
||||
type: "ir.actions.act_window",
|
||||
views: [[18, "form"]],
|
||||
};
|
||||
serverData.views["partner,18,form"] = `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
</form>`;
|
||||
serverData.models["ir.ui.view"] = {
|
||||
fields: {},
|
||||
records: [{ id: 18 }],
|
||||
};
|
||||
serverData.models.partner.records = [{ id: 1, display_name: "p1", foo: "hello" }];
|
||||
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
if (args.method === "set" && args.model === "ir.default") {
|
||||
assert.deepEqual(args.args, ["partner", "foo", "hello", true, true, false]);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 1234);
|
||||
await click(target.querySelector(".o_debug_manager button"));
|
||||
await click(target.querySelector(".o_debug_manager .dropdown-item"));
|
||||
assert.containsOnce(target, ".modal");
|
||||
|
||||
const select = target.querySelector(".modal #formview_default_fields");
|
||||
select.value = "foo";
|
||||
select.dispatchEvent(new Event("change"));
|
||||
await nextTick();
|
||||
await click(target.querySelectorAll(".modal .modal-footer button")[1]);
|
||||
assert.containsNone(target, ".modal");
|
||||
});
|
||||
|
||||
QUnit.test("view metadata: basic rendering", async (assert) => {
|
||||
prepareRegistriesWithCleanup();
|
||||
patchWithCleanup(odoo, {
|
||||
debug: true,
|
||||
});
|
||||
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
registry.category("debug").category("form").add("viewMetadata", viewMetadata);
|
||||
|
||||
const serverData = getActionManagerServerData();
|
||||
serverData.actions[1234] = {
|
||||
id: 1234,
|
||||
xml_id: "action_1234",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
res_id: 27,
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "form"]],
|
||||
};
|
||||
serverData.models.partner.records = [{ id: 27, display_name: "p1" }];
|
||||
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
if (args.method === "get_metadata") {
|
||||
return [
|
||||
{
|
||||
create_date: "2023-01-26 14:12:10",
|
||||
create_uid: [4, "Some user"],
|
||||
id: 27,
|
||||
noupdate: false,
|
||||
write_date: "2023-01-26 14:13:31",
|
||||
write_uid: [6, "Another User"],
|
||||
xmlid: "abc.partner_16",
|
||||
xmlids: [{ xmlid: "abc.partner_16", noupdate: false }],
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 1234);
|
||||
await click(target.querySelector(".o_debug_manager button"));
|
||||
await click(target.querySelector(".o_debug_manager .dropdown-item"));
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(
|
||||
target.querySelectorAll(".modal-body table tr th, .modal-body table tr td")
|
||||
),
|
||||
[
|
||||
"ID:",
|
||||
"27",
|
||||
"XML ID:",
|
||||
"abc.partner_16",
|
||||
"No Update:",
|
||||
"false (change)",
|
||||
"Creation User:",
|
||||
"Some user",
|
||||
"Creation Date:",
|
||||
"01/26/2023 15:12:10",
|
||||
"Latest Modification by:",
|
||||
"Another User",
|
||||
"Latest Modification Date:",
|
||||
"01/26/2023 15:13:31",
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("set defaults: setting default value for datetime field", async (assert) => {
|
||||
assert.expect(7);
|
||||
|
||||
prepareRegistriesWithCleanup();
|
||||
patchWithCleanup(odoo, {
|
||||
debug: true,
|
||||
});
|
||||
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
registry.category("debug").category("form").add("setDefaults", setDefaults);
|
||||
|
||||
const serverData = getActionManagerServerData();
|
||||
serverData.actions[1234] = {
|
||||
id: 1234,
|
||||
xml_id: "action_1234",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
res_id: 1,
|
||||
type: "ir.actions.act_window",
|
||||
views: [[18, "form"]],
|
||||
};
|
||||
serverData.models.partner.fields.datetime = {string: 'Datetime', type: 'datetime'}
|
||||
serverData.models.partner.fields.reference = {string: 'Reference', type: 'reference', selection: [["pony", "Pony"]]}
|
||||
serverData.views["partner,18,form"] = `
|
||||
<form>
|
||||
<field name="datetime"/>
|
||||
<field name="reference"/>
|
||||
<field name="m2o"/>
|
||||
</form>`;
|
||||
serverData.models["ir.ui.view"] = {
|
||||
fields: {},
|
||||
records: [{ id: 18 }],
|
||||
};
|
||||
serverData.models.pony.records = [{
|
||||
id: 1,
|
||||
name: "Test"
|
||||
}];
|
||||
serverData.models.partner.records = [{
|
||||
id: 1,
|
||||
display_name: "p1",
|
||||
datetime: "2024-01-24 16:46:16",
|
||||
reference: 'pony,1',
|
||||
m2o: 1
|
||||
}];
|
||||
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
if (args.method === "set" && args.model === "ir.default") {
|
||||
arg_steps.push(args.args)
|
||||
return true;
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({serverData, mockRPC});
|
||||
let arg_steps = [];
|
||||
for (const field_name of ['datetime', 'reference', 'm2o']) {
|
||||
await doAction(webClient, 1234);
|
||||
await click(target.querySelector(".o_debug_manager button"));
|
||||
await click(target.querySelector(".o_debug_manager .dropdown-item"));
|
||||
assert.containsOnce(target, ".modal");
|
||||
|
||||
const select = target.querySelector(".modal #formview_default_fields");
|
||||
select.value = field_name;
|
||||
select.dispatchEvent(new Event("change"));
|
||||
await nextTick();
|
||||
await click(target.querySelectorAll(".modal .modal-footer button")[1]);
|
||||
assert.containsNone(target, ".modal");
|
||||
}
|
||||
assert.deepEqual(arg_steps, [
|
||||
["partner", "datetime", "2024-01-24 16:46:16", true, true, false],
|
||||
["partner", "reference", {"displayName": "Test", "resId": 1, "resModel": "pony"}, true, true, false],
|
||||
["partner", "m2o", 1, true, true, false],
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("set defaults: settings default value for a very long value", async (assert) => {
|
||||
prepareRegistriesWithCleanup();
|
||||
patchWithCleanup(odoo, {
|
||||
debug: true,
|
||||
});
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
registry.category("debug").category("form").add("setDefaults", setDefaults);
|
||||
|
||||
const serverData = getActionManagerServerData();
|
||||
serverData.models.partner.fields.description = { string: "Description", type: "html" };
|
||||
serverData.actions[1234] = {
|
||||
id: 1234,
|
||||
xml_id: "action_1234",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
res_id: 1,
|
||||
type: "ir.actions.act_window",
|
||||
views: [[18, "form"]],
|
||||
};
|
||||
const fooValue = "12".repeat(250);
|
||||
serverData.views["partner,18,form"] = `
|
||||
<form>
|
||||
<group>
|
||||
<field name="display_name"/>
|
||||
<field name="description"/>
|
||||
<field name="foo"/>
|
||||
</group>
|
||||
</form>
|
||||
`;
|
||||
serverData.models.partner.records[0].foo = fooValue;
|
||||
serverData.models.partner.records[0].description = fooValue;
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
if (args.method === "set" && args.model === "ir.default") {
|
||||
assert.step("setting default");
|
||||
assert.deepEqual(args.args, ["partner", "foo", fooValue, true, true, false]);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 1234);
|
||||
await click(target.querySelector(".o_debug_manager button"));
|
||||
await click(target.querySelector(".o_debug_manager .dropdown-item"));
|
||||
const select = target.querySelector(".modal #formview_default_fields");
|
||||
|
||||
const options = Object.fromEntries(
|
||||
Array.from(select.querySelectorAll("option")).map((option) => [
|
||||
option.value,
|
||||
option.textContent,
|
||||
])
|
||||
);
|
||||
assert.deepEqual(options, {
|
||||
"": "",
|
||||
display_name: "Display Name = First record",
|
||||
foo: "Foo = 121212121212121212121212121212121212121212121212121212121...",
|
||||
description:
|
||||
"Description = 121212121212121212121212121212121212121212121212121212121...",
|
||||
});
|
||||
|
||||
select.value = "foo";
|
||||
select.dispatchEvent(new Event("change"));
|
||||
await nextTick();
|
||||
await click(target.querySelectorAll(".modal .modal-footer button")[1]);
|
||||
assert.verifySteps(["setting default"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Debug > Profiling QWeb", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
const qweb = JSON.stringify([
|
||||
{
|
||||
exec_context: [],
|
||||
results: {
|
||||
archs: {
|
||||
1: `<t name="Test1" t-name="test">
|
||||
<t t-call-assets="web.assets_tests"/>
|
||||
</t>`,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
delay: 0.1,
|
||||
directive: 't-call-assets="web.assets_tests"',
|
||||
query: 9,
|
||||
view_id: 1,
|
||||
xpath: "/t/t",
|
||||
},
|
||||
],
|
||||
},
|
||||
stack: [],
|
||||
start: 42,
|
||||
},
|
||||
]);
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
qweb: {
|
||||
string: "QWeb",
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
records: [{ qweb }],
|
||||
},
|
||||
"ir.ui.view": {
|
||||
fields: {
|
||||
model: { type: "char" },
|
||||
name: { type: "char" },
|
||||
type: { type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: "formView",
|
||||
model: "partner",
|
||||
type: "form",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
|
||||
});
|
||||
|
||||
QUnit.test("profiling qweb view field renders delay and query", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qweb" widget="profiling_qweb_view" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsN(target, "[name='qweb'] .ace_gutter .ace_gutter-cell", 3);
|
||||
assert.containsN(target, "[name='qweb'] .ace_gutter .ace_gutter-cell .o_info", 1);
|
||||
const infoEl = target.querySelector("[name='qweb'] .ace_gutter .ace_gutter-cell .o_info");
|
||||
assert.strictEqual(infoEl.querySelector(".o_delay").textContent, "0.1");
|
||||
assert.strictEqual(infoEl.querySelector(".o_query").textContent, "9");
|
||||
|
||||
const header = target.querySelector("[name='qweb'] .o_select_view_profiling");
|
||||
assert.strictEqual(header.querySelector(".o_delay").textContent, "0.1 ms");
|
||||
assert.strictEqual(header.querySelector(".o_query").textContent, "9 query");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { ErrorDialog } from "@web/core/errors/error_dialogs";
|
||||
import { errorService } from "@web/core/errors/error_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { registerCleanup } from "../helpers/cleanup";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "../helpers/mock_env";
|
||||
import { makeFakeLocalizationService, makeFakeRPCService } from "../helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "../helpers/utils";
|
||||
import { Dialog } from "../../src/core/dialog/dialog";
|
||||
|
||||
import { Component, onMounted, xml } from "@odoo/owl";
|
||||
|
||||
let env;
|
||||
let target;
|
||||
const serviceRegistry = registry.category("services");
|
||||
const mainComponentRegistry = registry.category("main_components");
|
||||
|
||||
class PseudoWebClient extends Component {
|
||||
setup() {
|
||||
this.Components = mainComponentRegistry.getEntries();
|
||||
}
|
||||
}
|
||||
PseudoWebClient.template = xml`
|
||||
<div>
|
||||
<div>
|
||||
<t t-foreach="Components" t-as="C" t-key="C[0]">
|
||||
<t t-component="C[1].Component" t-props="C[1].props"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
QUnit.module("DialogManager", {
|
||||
async beforeEach() {
|
||||
target = getFixture();
|
||||
clearRegistryWithCleanup(mainComponentRegistry);
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
serviceRegistry.add("ui", uiService);
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("l10n", makeFakeLocalizationService());
|
||||
|
||||
env = await makeTestEnv();
|
||||
},
|
||||
});
|
||||
QUnit.test("Simple rendering with a single dialog", async (assert) => {
|
||||
assert.expect(4);
|
||||
class CustomDialog extends Component {}
|
||||
CustomDialog.components = { Dialog };
|
||||
CustomDialog.template = xml`<Dialog title="'Welcome'">content</Dialog>`;
|
||||
await mount(PseudoWebClient, target, { env });
|
||||
assert.containsNone(target, ".o_dialog_container .o_dialog");
|
||||
env.services.dialog.add(CustomDialog);
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_dialog_container .o_dialog");
|
||||
assert.strictEqual(target.querySelector("header .modal-title").textContent, "Welcome");
|
||||
await click(target.querySelector(".o_dialog_container .o_dialog footer button"));
|
||||
assert.containsNone(target, ".o_dialog_container .o_dialog");
|
||||
});
|
||||
|
||||
QUnit.test("Simple rendering and close a single dialog", async (assert) => {
|
||||
assert.expect(4);
|
||||
|
||||
class CustomDialog extends Component {}
|
||||
CustomDialog.components = { Dialog };
|
||||
CustomDialog.template = xml`<Dialog title="'Welcome'">content</Dialog>`;
|
||||
|
||||
await mount(PseudoWebClient, target, { env });
|
||||
assert.containsNone(target, ".o_dialog_container .o_dialog");
|
||||
|
||||
const removeDialog = env.services.dialog.add(CustomDialog);
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_dialog_container .o_dialog");
|
||||
assert.strictEqual(target.querySelector("header .modal-title").textContent, "Welcome");
|
||||
|
||||
removeDialog();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_dialog_container .o_dialog");
|
||||
|
||||
// Call a second time, the close on the dialog.
|
||||
// As the dialog is already close, this call is just ignored. No error should be raised.
|
||||
removeDialog();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
QUnit.test("rendering with two dialogs", async (assert) => {
|
||||
assert.expect(7);
|
||||
class CustomDialog extends Component {}
|
||||
CustomDialog.components = { Dialog };
|
||||
CustomDialog.template = xml`<Dialog title="props.title">content</Dialog>`;
|
||||
|
||||
await mount(PseudoWebClient, target, { env });
|
||||
assert.containsNone(target, ".o_dialog_container .o_dialog");
|
||||
env.services.dialog.add(CustomDialog, { title: "Hello" });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_dialog_container .o_dialog");
|
||||
assert.strictEqual(target.querySelector("header .modal-title").textContent, "Hello");
|
||||
env.services.dialog.add(CustomDialog, { title: "Sauron" });
|
||||
await nextTick();
|
||||
assert.containsN(target, ".o_dialog_container .o_dialog", 2);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("header .modal-title")].map((el) => el.textContent),
|
||||
["Hello", "Sauron"]
|
||||
);
|
||||
await click(target.querySelector(".o_dialog_container .o_dialog footer button"));
|
||||
assert.containsOnce(target, ".o_dialog_container .o_dialog");
|
||||
assert.strictEqual(target.querySelector("header .modal-title").textContent, "Sauron");
|
||||
});
|
||||
|
||||
QUnit.test("multiple dialogs can become the UI active element", async (assert) => {
|
||||
assert.expect(3);
|
||||
class CustomDialog extends Component {}
|
||||
CustomDialog.components = { Dialog };
|
||||
CustomDialog.template = xml`<Dialog title="props.title">content</Dialog>`;
|
||||
await mount(PseudoWebClient, target, { env });
|
||||
|
||||
env.services.dialog.add(CustomDialog, { title: "Hello" });
|
||||
await nextTick();
|
||||
let dialogModal = target.querySelector(
|
||||
".o_dialog_container .o_dialog .modal:not(.o_inactive_modal)"
|
||||
);
|
||||
|
||||
assert.strictEqual(dialogModal, env.services.ui.activeElement);
|
||||
|
||||
env.services.dialog.add(CustomDialog, { title: "Sauron" });
|
||||
await nextTick();
|
||||
dialogModal = target.querySelector(
|
||||
".o_dialog_container .o_dialog .modal:not(.o_inactive_modal)"
|
||||
);
|
||||
|
||||
assert.strictEqual(dialogModal, env.services.ui.activeElement);
|
||||
|
||||
env.services.dialog.add(CustomDialog, { title: "Rafiki" });
|
||||
await nextTick();
|
||||
dialogModal = target.querySelector(
|
||||
".o_dialog_container .o_dialog .modal:not(.o_inactive_modal)"
|
||||
);
|
||||
|
||||
assert.strictEqual(dialogModal, env.services.ui.activeElement);
|
||||
});
|
||||
|
||||
QUnit.test("Interactions between multiple dialogs", async (assert) => {
|
||||
assert.expect(14);
|
||||
function activity(modals) {
|
||||
const active = [];
|
||||
const names = [];
|
||||
for (let i = 0; i < modals.length; i++) {
|
||||
active[i] = !modals[i].classList.contains("o_inactive_modal");
|
||||
names[i] = modals[i].querySelector(".modal-title").textContent;
|
||||
}
|
||||
return { active, names };
|
||||
}
|
||||
|
||||
class CustomDialog extends Component {}
|
||||
CustomDialog.components = { Dialog };
|
||||
CustomDialog.template = xml`<Dialog title="props.title">content</Dialog>`;
|
||||
await mount(PseudoWebClient, target, { env });
|
||||
|
||||
env.services.dialog.add(CustomDialog, { title: "Hello" });
|
||||
await nextTick();
|
||||
env.services.dialog.add(CustomDialog, { title: "Sauron" });
|
||||
await nextTick();
|
||||
env.services.dialog.add(CustomDialog, { title: "Rafiki" });
|
||||
await nextTick();
|
||||
|
||||
let modals = document.querySelectorAll(".modal");
|
||||
assert.containsN(target, ".o_dialog", 3);
|
||||
let res = activity(modals);
|
||||
assert.deepEqual(res.active, [false, false, true]);
|
||||
assert.deepEqual(res.names, ["Hello", "Sauron", "Rafiki"]);
|
||||
assert.hasClass(target.querySelector(".o_dialog_container"), "modal-open");
|
||||
|
||||
let lastDialog = modals[modals.length - 1];
|
||||
lastDialog.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Escape" }));
|
||||
await nextTick();
|
||||
modals = document.querySelectorAll(".modal");
|
||||
assert.containsN(target, ".o_dialog", 2);
|
||||
res = activity(modals);
|
||||
assert.deepEqual(res.active, [false, true]);
|
||||
assert.deepEqual(res.names, ["Hello", "Sauron"]);
|
||||
assert.hasClass(target.querySelector(".o_dialog_container"), "modal-open");
|
||||
|
||||
lastDialog = modals[modals.length - 1];
|
||||
await click(lastDialog, "footer button");
|
||||
modals = document.querySelectorAll(".modal");
|
||||
assert.containsN(target, ".o_dialog", 1);
|
||||
res = activity(modals);
|
||||
assert.deepEqual(res.active, [true]);
|
||||
assert.deepEqual(res.names, ["Hello"]);
|
||||
assert.hasClass(target.querySelector(".o_dialog_container"), "modal-open");
|
||||
|
||||
lastDialog = modals[modals.length - 1];
|
||||
await click(lastDialog, "footer button");
|
||||
assert.containsNone(target, ".o_dialog_container .modal");
|
||||
assert.containsOnce(target, ".o_dialog_container");
|
||||
});
|
||||
|
||||
QUnit.test("dialog component crashes", async (assert) => {
|
||||
assert.expect(4);
|
||||
|
||||
class FailingDialog extends Component {
|
||||
setup() {
|
||||
throw new Error("Some Error");
|
||||
}
|
||||
}
|
||||
FailingDialog.components = { Dialog };
|
||||
FailingDialog.template = xml`<Dialog title="'Error'">content</Dialog>`;
|
||||
|
||||
const prom = makeDeferred();
|
||||
patchWithCleanup(ErrorDialog.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
onMounted(() => {
|
||||
prom.resolve();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handler = (ev) => {
|
||||
assert.step("error");
|
||||
// need to preventDefault to remove error from console (so python test pass)
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
window.addEventListener("unhandledrejection", handler);
|
||||
registerCleanup(() => window.removeEventListener("unhandledrejection", handler));
|
||||
patchWithCleanup(QUnit, {
|
||||
onUnhandledRejection: () => {},
|
||||
});
|
||||
|
||||
const rpc = makeFakeRPCService();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
serviceRegistry.add("notification", notificationService);
|
||||
serviceRegistry.add("error", errorService);
|
||||
|
||||
await mount(PseudoWebClient, target, { env });
|
||||
|
||||
env.services.dialog.add(FailingDialog);
|
||||
await prom;
|
||||
|
||||
assert.verifySteps(["error"]);
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.containsOnce(target, ".modal .o_dialog_error");
|
||||
});
|
||||
299
odoo-bringout-oca-ocb-web/web/static/tests/core/dialog_tests.js
Normal file
299
odoo-bringout-oca-ocb-web/web/static/tests/core/dialog_tests.js
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { click, destroy, getFixture, mount } from "../helpers/utils";
|
||||
import { makeFakeDialogService } from "../helpers/mock_services";
|
||||
|
||||
import { Component, useState, onMounted, xml } from "@odoo/owl";
|
||||
const serviceRegistry = registry.category("services");
|
||||
let parent;
|
||||
let target;
|
||||
|
||||
async function makeDialogTestEnv() {
|
||||
const env = await makeTestEnv();
|
||||
env.dialogData = {
|
||||
isActive: true,
|
||||
close: () => {},
|
||||
};
|
||||
return env;
|
||||
}
|
||||
|
||||
QUnit.module("Components", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
target = getFixture();
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("ui", uiService);
|
||||
serviceRegistry.add("dialog", makeFakeDialogService());
|
||||
});
|
||||
hooks.afterEach(() => {
|
||||
if (parent) {
|
||||
parent = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.module("Dialog");
|
||||
|
||||
QUnit.test("simple rendering", async function (assert) {
|
||||
assert.expect(8);
|
||||
class Parent extends Component {}
|
||||
Parent.components = { Dialog };
|
||||
Parent.template = xml`
|
||||
<Dialog title="'Wow(l) Effect'">
|
||||
Hello!
|
||||
</Dialog>
|
||||
`;
|
||||
|
||||
const env = await makeDialogTestEnv();
|
||||
parent = await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_dialog header .modal-title",
|
||||
"the header is rendered by default"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("header .modal-title").textContent,
|
||||
"Wow(l) Effect"
|
||||
);
|
||||
assert.containsOnce(target, ".o_dialog main", "a dialog has always a main node");
|
||||
assert.strictEqual(target.querySelector("main").textContent, " Hello! ");
|
||||
assert.containsOnce(target, ".o_dialog footer", "the footer is rendered by default");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_dialog footer button",
|
||||
"the footer is rendered with a single button 'Ok' by default"
|
||||
);
|
||||
assert.strictEqual(target.querySelector("footer button").textContent, "Ok");
|
||||
});
|
||||
|
||||
QUnit.test("simple rendering with two dialogs", async function (assert) {
|
||||
assert.expect(3);
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<Dialog title="'First Title'">
|
||||
Hello!
|
||||
</Dialog>
|
||||
<Dialog title="'Second Title'">
|
||||
Hello again!
|
||||
</Dialog>
|
||||
</div>
|
||||
`;
|
||||
Parent.components = { Dialog };
|
||||
const env = await makeDialogTestEnv();
|
||||
parent = await mount(Parent, target, { env });
|
||||
assert.containsN(target, ".o_dialog", 2);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("header .modal-title")].map((el) => el.textContent),
|
||||
["First Title", "Second Title"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_dialog .modal-body")].map((el) => el.textContent),
|
||||
[" Hello! ", " Hello again! "]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("click on the button x triggers the service close", async function (assert) {
|
||||
assert.expect(3);
|
||||
const env = await makeDialogTestEnv();
|
||||
env.dialogData.close = () => assert.step("close");
|
||||
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`
|
||||
<Dialog>
|
||||
Hello!
|
||||
</Dialog>
|
||||
`;
|
||||
Parent.components = { Dialog };
|
||||
parent = await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
await click(target, ".o_dialog header button.btn-close");
|
||||
assert.verifySteps(["close"]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"click on the default footer button triggers the service close",
|
||||
async function (assert) {
|
||||
const env = await makeDialogTestEnv();
|
||||
env.dialogData.close = () => assert.step("close");
|
||||
assert.expect(3);
|
||||
class Parent extends Component {}
|
||||
|
||||
Parent.template = xml`
|
||||
<Dialog>
|
||||
Hello!
|
||||
</Dialog>
|
||||
`;
|
||||
Parent.components = { Dialog };
|
||||
parent = await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
await click(target, ".o_dialog footer button");
|
||||
assert.verifySteps(["close"]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("render custom footer buttons is possible", async function (assert) {
|
||||
assert.expect(2);
|
||||
class SimpleButtonsDialog extends Component {}
|
||||
SimpleButtonsDialog.components = { Dialog };
|
||||
SimpleButtonsDialog.template = xml`
|
||||
<Dialog>
|
||||
content
|
||||
<t t-set-slot="footer">
|
||||
<div>
|
||||
<button class="btn btn-primary">The First Button</button>
|
||||
<button class="btn btn-primary">The Second Button</button>
|
||||
</div>
|
||||
</t>
|
||||
</Dialog>
|
||||
`;
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = useState({
|
||||
displayDialog: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<SimpleButtonsDialog/>
|
||||
</div>
|
||||
`;
|
||||
Parent.components = { SimpleButtonsDialog };
|
||||
const env = await makeDialogTestEnv();
|
||||
parent = await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.containsN(target, ".o_dialog footer button", 2);
|
||||
});
|
||||
|
||||
QUnit.test("embed an arbitrary component in a dialog is possible", async function (assert) {
|
||||
assert.expect(6);
|
||||
class SubComponent extends Component {
|
||||
_onClick() {
|
||||
assert.step("subcomponent-clicked");
|
||||
this.props.onClicked();
|
||||
}
|
||||
}
|
||||
SubComponent.template = xml`
|
||||
<div class="o_subcomponent" t-esc="props.text" t-on-click="_onClick"/>
|
||||
`;
|
||||
class Parent extends Component {
|
||||
_onSubcomponentClicked() {
|
||||
assert.step("message received by parent");
|
||||
}
|
||||
}
|
||||
Parent.components = { Dialog, SubComponent };
|
||||
Parent.template = xml`
|
||||
<Dialog>
|
||||
<SubComponent text="'Wow(l) Effect'" onClicked="_onSubcomponentClicked"/>
|
||||
</Dialog>
|
||||
`;
|
||||
const env = await makeDialogTestEnv();
|
||||
parent = await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.containsOnce(target, ".o_dialog main .o_subcomponent");
|
||||
assert.strictEqual(target.querySelector(".o_subcomponent").textContent, "Wow(l) Effect");
|
||||
await click(target.querySelector(".o_subcomponent"));
|
||||
assert.verifySteps(["subcomponent-clicked", "message received by parent"]);
|
||||
});
|
||||
|
||||
QUnit.test("dialog without header/footer", async function (assert) {
|
||||
assert.expect(4);
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`
|
||||
<Dialog header="false" footer="false">content</Dialog>
|
||||
`;
|
||||
const env = await makeDialogTestEnv();
|
||||
Parent.components = { Dialog };
|
||||
parent = await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.containsNone(target, ".o_dialog header");
|
||||
assert.containsOnce(target, "main", "a dialog has always a main node");
|
||||
assert.containsNone(target, ".o_dialog footer");
|
||||
});
|
||||
|
||||
QUnit.test("dialog size can be chosen", async function (assert) {
|
||||
assert.expect(5);
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<Dialog contentClass="'xl'" size="'xl'">content</Dialog>
|
||||
<Dialog contentClass="'lg'">content</Dialog>
|
||||
<Dialog contentClass="'md'" size="'md'">content</Dialog>
|
||||
<Dialog contentClass="'sm'" size="'sm'">content</Dialog>
|
||||
</div>`;
|
||||
Parent.components = { Dialog };
|
||||
const env = await makeDialogTestEnv();
|
||||
parent = await mount(Parent, target, { env });
|
||||
assert.containsN(target, ".o_dialog", 4);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
target.querySelectorAll(".o_dialog .modal-dialog.modal-xl .xl")
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
target.querySelectorAll(".o_dialog .modal-dialog.modal-lg .lg")
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
target.querySelectorAll(".o_dialog .modal-dialog.modal-md .md")
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
target.querySelectorAll(".o_dialog .modal-dialog.modal-sm .sm")
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("dialog can be rendered on fullscreen", async function (assert) {
|
||||
assert.expect(2);
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`
|
||||
<Dialog fullscreen="true">content</Dialog>
|
||||
`;
|
||||
Parent.components = { Dialog };
|
||||
const env = await makeDialogTestEnv();
|
||||
parent = await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.hasClass(target.querySelector(".o_dialog .modal"), "o_modal_full");
|
||||
});
|
||||
|
||||
QUnit.test("can be the UI active element", async function (assert) {
|
||||
assert.expect(4);
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.ui = useService("ui");
|
||||
assert.strictEqual(
|
||||
this.ui.activeElement,
|
||||
document,
|
||||
"UI active element should be the default (document) as Parent is not mounted yet"
|
||||
);
|
||||
onMounted(() => {
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.strictEqual(
|
||||
this.ui.activeElement,
|
||||
target.querySelector(".modal"),
|
||||
"UI active element should be the dialog modal"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
const env = await makeDialogTestEnv();
|
||||
Parent.template = xml`<Dialog>content</Dialog>`;
|
||||
Parent.components = { Dialog };
|
||||
|
||||
const parent = await mount(Parent, target, { env });
|
||||
destroy(parent);
|
||||
|
||||
assert.strictEqual(
|
||||
env.services.ui.activeElement,
|
||||
document,
|
||||
"UI owner should be reset to the default (document)"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,650 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { DomainSelector } from "@web/core/domain_selector/domain_selector";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { click, editInput, editSelect, getFixture, mount, triggerEvent } from "../helpers/utils";
|
||||
import { makeFakeLocalizationService } from "../helpers/mock_services";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
async function mountComponent(Component, params = {}) {
|
||||
const env = await makeTestEnv({ serverData, mockRPC: params.mockRPC });
|
||||
await mount(MainComponentsContainer, target, { env });
|
||||
return mount(Component, target, { env, props: params.props || {} });
|
||||
}
|
||||
|
||||
QUnit.module("Components", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: { string: "Foo", type: "char", searchable: true },
|
||||
bar: { string: "Bar", type: "boolean", searchable: true },
|
||||
product_id: {
|
||||
string: "Product",
|
||||
type: "many2one",
|
||||
relation: "product",
|
||||
searchable: true,
|
||||
},
|
||||
datetime: { string: "Date Time", type: "datetime", searchable: true },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, foo: "yop", bar: true, product_id: 37 },
|
||||
{ id: 2, foo: "blip", bar: true, product_id: false },
|
||||
{ id: 4, foo: "abc", bar: false, product_id: 41 },
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: { string: "Product Name", type: "char", searchable: true },
|
||||
},
|
||||
records: [
|
||||
{ id: 37, display_name: "xphone" },
|
||||
{ id: 41, display_name: "xpad" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("popover", popoverService);
|
||||
registry.category("services").add("orm", ormService);
|
||||
registry.category("services").add("ui", uiService);
|
||||
registry.category("services").add("hotkey", hotkeyService);
|
||||
registry.category("services").add("localization", makeFakeLocalizationService());
|
||||
registry.category("services").add("view", viewService);
|
||||
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("DomainSelector");
|
||||
|
||||
QUnit.test("creating a domain from scratch", async (assert) => {
|
||||
assert.expect(12);
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.value = "[]";
|
||||
}
|
||||
onUpdate(newValue) {
|
||||
this.value = newValue;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
Parent.components = { DomainSelector };
|
||||
Parent.template = xml`
|
||||
<DomainSelector
|
||||
resModel="'partner'"
|
||||
value="value"
|
||||
readonly="false"
|
||||
isDebugMode="true"
|
||||
update="(newValue) => this.onUpdate(newValue)"
|
||||
/>
|
||||
`;
|
||||
|
||||
// Create the domain selector and its mock environment
|
||||
await mountComponent(Parent);
|
||||
|
||||
// As we gave an empty domain, there should be a visible button to add
|
||||
// the first domain part
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_domain_add_first_node_button",
|
||||
"there should be a button to create first domain element"
|
||||
);
|
||||
|
||||
// Clicking on the button should add a visible field selector in the
|
||||
// widget so that the user can change the field chain
|
||||
await click(target, ".o_domain_add_first_node_button");
|
||||
assert.containsOnce(target, ".o_field_selector", "there should be a field selector");
|
||||
|
||||
// Focusing the field selector input should open a field selector popover
|
||||
await click(target, ".o_field_selector");
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
".o_field_selector_popover",
|
||||
"field selector popover should be visible"
|
||||
);
|
||||
|
||||
// The field selector popover should contain the list of "partner"
|
||||
// fields. "Bar" should be among them. "Bar" result li will display the
|
||||
// name of the field and some debug info.
|
||||
assert.strictEqual(
|
||||
document.body.querySelector(".o_field_selector_popover li").textContent,
|
||||
"Barbar (boolean)",
|
||||
"field selector popover should contain the 'Bar' field"
|
||||
);
|
||||
|
||||
// Clicking the "Bar" field should change the internal domain and this
|
||||
// should be displayed in the debug textarea
|
||||
await click(document.body.querySelector(".o_field_selector_popover li"));
|
||||
assert.containsOnce(target, "textarea.o_domain_debug_input");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_debug_input").value,
|
||||
`[("bar", "=", True)]`,
|
||||
"the domain input should contain a domain with 'bar'"
|
||||
);
|
||||
|
||||
// There should be a "+" button to add a domain part; clicking on it
|
||||
// should add the default "['id', '=', 1]" domain
|
||||
assert.containsOnce(target, ".fa-plus-circle", "there should be a '+' button");
|
||||
await click(target, ".fa-plus-circle");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_debug_input").value,
|
||||
`["&", ("bar", "=", True), ("id", "=", 1)]`,
|
||||
"the domain input should contain a domain with 'bar' and 'id'"
|
||||
);
|
||||
|
||||
// There should be two "..." buttons to add a domain group; clicking on
|
||||
// the first one, should add this group with defaults "['id', '=', 1]"
|
||||
// domains and the "|" operator
|
||||
assert.containsN(target, ".fa-ellipsis-h", 2, "there should be two '...' buttons");
|
||||
|
||||
await click(target.querySelector(".fa-ellipsis-h"));
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_debug_input").value,
|
||||
`["&", ("bar", "=", True), "&", "|", ("id", "=", 1), ("id", "=", 1), ("id", "=", 1)]`,
|
||||
"the domain input should contain a domain with 'bar', 'id' and a subgroup"
|
||||
);
|
||||
|
||||
// There should be five "-" buttons to remove domain part; clicking on
|
||||
// the two last ones, should leave a domain with only the "bar" and
|
||||
// "foo" fields, with the initial "&" operator
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_domain_delete_node_button",
|
||||
5,
|
||||
"there should be five 'x' buttons"
|
||||
);
|
||||
let buttons = target.querySelectorAll(".o_domain_delete_node_button");
|
||||
await click(buttons[buttons.length - 1]);
|
||||
buttons = target.querySelectorAll(".o_domain_delete_node_button");
|
||||
await click(buttons[buttons.length - 1]);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_debug_input").value,
|
||||
`["&", ("bar", "=", True), ("id", "=", 1)]`,
|
||||
"the domain input should contain a domain with 'bar' and 'id'"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("building a domain with a datetime", async (assert) => {
|
||||
assert.expect(4);
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.value = `[("datetime", "=", "2017-03-27 15:42:00")]`;
|
||||
}
|
||||
onUpdate(newValue) {
|
||||
assert.strictEqual(
|
||||
newValue,
|
||||
`[("datetime", "=", "2017-02-26 15:42:00")]`,
|
||||
"datepicker value should have changed"
|
||||
);
|
||||
this.value = newValue;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
Parent.components = { DomainSelector };
|
||||
Parent.template = xml`
|
||||
<DomainSelector
|
||||
resModel="'partner'"
|
||||
value="value"
|
||||
readonly="false"
|
||||
isDebugMode="true"
|
||||
update="(newValue) => this.onUpdate(newValue)"
|
||||
/>
|
||||
`;
|
||||
|
||||
// Create the domain selector and its mock environment
|
||||
await mountComponent(Parent);
|
||||
|
||||
// Check that there is a datepicker to choose the date
|
||||
assert.containsOnce(target, ".o_datepicker", "there should be a datepicker");
|
||||
// The input field should display the date and time in the user's timezone
|
||||
assert.equal(target.querySelector(".o_datepicker_input").value, "03/27/2017 16:42:00");
|
||||
|
||||
// Change the date in the datepicker
|
||||
await click(target, ".o_datepicker_input");
|
||||
await click(
|
||||
document.body.querySelector(
|
||||
`.bootstrap-datetimepicker-widget :not(.today)[data-action="selectDay"]`
|
||||
)
|
||||
); // => February 26th
|
||||
await click(
|
||||
document.body.querySelector(`.bootstrap-datetimepicker-widget a[data-action="close"]`)
|
||||
);
|
||||
|
||||
// The input field should display the date and time in the user's timezone
|
||||
assert.equal(target.querySelector(".o_datepicker_input").value, "02/26/2017 16:42:00");
|
||||
});
|
||||
|
||||
QUnit.test("building a domain with a datetime: context_today()", async (assert) => {
|
||||
// Create the domain selector and its mock environment
|
||||
await mountComponent(DomainSelector, {
|
||||
props: {
|
||||
resModel: "partner",
|
||||
value: `[("datetime", "=", context_today())]`,
|
||||
readonly: false,
|
||||
update: () => {
|
||||
assert.step("SHOULD NEVER BE CALLED");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check that there is a datepicker to choose the date
|
||||
assert.containsOnce(target, ".o_datepicker", "there should be a datepicker");
|
||||
// The input field should display that the date is invalid
|
||||
assert.equal(target.querySelector(".o_datepicker_input").value, "Invalid DateTime");
|
||||
|
||||
// Open and close the datepicker
|
||||
await click(target, ".o_datepicker_input");
|
||||
await click(
|
||||
document.body.querySelector(`.bootstrap-datetimepicker-widget [data-action=close]`)
|
||||
);
|
||||
|
||||
// The input field should continue displaying 'Invalid DateTime'.
|
||||
// The value is still invalid.
|
||||
assert.equal(target.querySelector(".o_datepicker_input").value, "Invalid DateTime");
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("building a domain with a m2o without following the relation", async (assert) => {
|
||||
assert.expect(1);
|
||||
|
||||
// Create the domain selector and its mock environment
|
||||
await mountComponent(DomainSelector, {
|
||||
props: {
|
||||
resModel: "partner",
|
||||
value: `[("product_id", "ilike", 1)]`,
|
||||
readonly: false,
|
||||
isDebugMode: true,
|
||||
update: (newValue) => {
|
||||
assert.strictEqual(
|
||||
newValue,
|
||||
`[("product_id", "ilike", "pad")]`,
|
||||
"string should have been allowed as m2o value"
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_domain_leaf_value_input");
|
||||
input.value = "pad";
|
||||
await triggerEvent(input, null, "change");
|
||||
});
|
||||
|
||||
QUnit.test("editing a domain with `parent` key", async (assert) => {
|
||||
// Create the domain selector and its mock environment
|
||||
await mountComponent(DomainSelector, {
|
||||
props: {
|
||||
resModel: "product",
|
||||
value: `[("name", "=", parent.foo)]`,
|
||||
readonly: false,
|
||||
isDebugMode: true,
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.lastElementChild.textContent,
|
||||
" This domain is not supported. Reset domain",
|
||||
"an error message should be displayed because of the `parent` key"
|
||||
);
|
||||
assert.containsOnce(target, "button:contains(Reset domain)");
|
||||
});
|
||||
|
||||
QUnit.test("creating a domain with a default option", async (assert) => {
|
||||
assert.expect(1);
|
||||
|
||||
// Create the domain selector and its mock environment
|
||||
await mountComponent(DomainSelector, {
|
||||
props: {
|
||||
resModel: "partner",
|
||||
value: "[]",
|
||||
readonly: false,
|
||||
isDebugMode: true,
|
||||
defaultLeafValue: ["foo", "=", "kikou"],
|
||||
update: (newValue) => {
|
||||
assert.strictEqual(
|
||||
newValue,
|
||||
`[("foo", "=", "kikou")]`,
|
||||
"the domain input should contain the default domain"
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Clicking on the button should add a visible field selector in the
|
||||
// widget so that the user can change the field chain
|
||||
await click(target, ".o_domain_add_first_node_button");
|
||||
});
|
||||
|
||||
QUnit.test("edit a domain with the debug textarea", async (assert) => {
|
||||
assert.expect(5);
|
||||
|
||||
let newValue;
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.value = `[("product_id", "ilike", 1)]`;
|
||||
}
|
||||
onUpdate(value, fromDebug) {
|
||||
assert.strictEqual(value, newValue);
|
||||
assert.ok(fromDebug);
|
||||
}
|
||||
}
|
||||
Parent.components = { DomainSelector };
|
||||
Parent.template = xml`
|
||||
<DomainSelector
|
||||
value="value"
|
||||
resModel="'partner'"
|
||||
readonly="false"
|
||||
isDebugMode="true"
|
||||
update="(...args) => this.onUpdate(...args)"
|
||||
/>
|
||||
`;
|
||||
// Create the domain selector and its mock environment
|
||||
await mountComponent(Parent);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_domain_node.o_domain_leaf",
|
||||
"should have a single domain node"
|
||||
);
|
||||
newValue = `
|
||||
[
|
||||
['product_id', 'ilike', 1],
|
||||
['id', '=', 0]
|
||||
]`;
|
||||
const input = target.querySelector(".o_domain_debug_input");
|
||||
input.value = newValue;
|
||||
await triggerEvent(input, null, "change");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_debug_input").value,
|
||||
newValue,
|
||||
"the domain should not have been formatted"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_domain_node.o_domain_leaf",
|
||||
"should still have a single domain node"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"set [(1, '=', 1)] or [(0, '=', 1)] as domain with the debug textarea",
|
||||
async (assert) => {
|
||||
assert.expect(15);
|
||||
|
||||
let newValue;
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.value = `[("product_id", "ilike", 1)]`;
|
||||
}
|
||||
onUpdate(value, fromDebug) {
|
||||
this.value = value;
|
||||
assert.strictEqual(value, newValue);
|
||||
assert.ok(fromDebug);
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
Parent.components = { DomainSelector };
|
||||
Parent.template = xml`
|
||||
<DomainSelector
|
||||
value="value"
|
||||
resModel="'partner'"
|
||||
readonly="false"
|
||||
isDebugMode="true"
|
||||
update="(...args) => this.onUpdate(...args)"
|
||||
/>
|
||||
`;
|
||||
// Create the domain selector and its mock environment
|
||||
await mountComponent(Parent);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_domain_node.o_domain_leaf",
|
||||
"should have a single domain node"
|
||||
);
|
||||
newValue = `[(1, "=", 1)]`;
|
||||
let input = target.querySelector(".o_domain_debug_input");
|
||||
input.value = newValue;
|
||||
await triggerEvent(input, null, "change");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_debug_input").value,
|
||||
newValue,
|
||||
"the domain should not have been formatted"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_domain_node.o_domain_leaf",
|
||||
"should still have a single domain node"
|
||||
);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_selector_chain_part").innerText, "1");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "0"); // option "="
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_value_input").value, "1");
|
||||
|
||||
newValue = `[(0, "=", 1)]`;
|
||||
input = target.querySelector(".o_domain_debug_input");
|
||||
input.value = newValue;
|
||||
await triggerEvent(input, null, "change");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_debug_input").value,
|
||||
newValue,
|
||||
"the domain should not have been formatted"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_domain_node.o_domain_leaf",
|
||||
"should still have a single domain node"
|
||||
);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_selector_chain_part").innerText, "0");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "0"); // option "="
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_value_input").value, "1");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("operator fallback", async (assert) => {
|
||||
await mountComponent(DomainSelector, {
|
||||
props: {
|
||||
resModel: "partner",
|
||||
value: "[['foo', 'like', 'kikou']]",
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf").textContent, `Foo like "kikou"`);
|
||||
});
|
||||
|
||||
QUnit.test("operator fallback in edit mode", async (assert) => {
|
||||
registry.category("domain_selector/operator").add("test", {
|
||||
category: "test",
|
||||
label: "test",
|
||||
value: "test",
|
||||
onDidChange: () => null,
|
||||
matches: ({ operator }) => operator === "test",
|
||||
hideValue: true,
|
||||
});
|
||||
|
||||
await mountComponent(DomainSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
resModel: "partner",
|
||||
value: "[['foo', 'test', 'kikou']]",
|
||||
},
|
||||
});
|
||||
|
||||
// check that the DomainSelector does not crash
|
||||
assert.containsOnce(target, ".o_domain_selector");
|
||||
assert.containsN(target, ".o_domain_leaf_edition > div", 2, "value should be hidden");
|
||||
});
|
||||
|
||||
QUnit.test("cache fields_get", async (assert) => {
|
||||
await mountComponent(DomainSelector, {
|
||||
mockRPC(route, { method }) {
|
||||
if (method === "fields_get") {
|
||||
assert.step("fields_get");
|
||||
}
|
||||
},
|
||||
props: {
|
||||
readonly: false,
|
||||
resModel: "partner",
|
||||
value: "['&', ['foo', '=', 'kikou'], ['bar', '=', 'true']]",
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps(["fields_get"]);
|
||||
});
|
||||
|
||||
QUnit.test("selection field with operator change from 'is set' to '='", async (assert) => {
|
||||
serverData.models.partner.fields.state = {
|
||||
string: "State",
|
||||
type: "selection",
|
||||
selection: [
|
||||
["abc", "ABC"],
|
||||
["def", "DEF"],
|
||||
["ghi", "GHI"],
|
||||
],
|
||||
};
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.value = `[['state', '!=', false]]`;
|
||||
}
|
||||
onUpdate(newValue) {
|
||||
this.value = newValue;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
Parent.components = { DomainSelector };
|
||||
Parent.template = xml`
|
||||
<DomainSelector
|
||||
resModel="'partner'"
|
||||
value="value"
|
||||
readonly="false"
|
||||
update="(newValue) => this.onUpdate(newValue)"
|
||||
/>
|
||||
`;
|
||||
|
||||
// Create the domain selector and its mock environment
|
||||
await mountComponent(Parent);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_selector_chain_part").innerText, "State");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "2"); // option "!="
|
||||
|
||||
await editSelect(target, ".o_domain_leaf_operator_select", 0);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_selector_chain_part").innerText, "State");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "0"); // option "="
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_value_input").value, "abc");
|
||||
});
|
||||
|
||||
QUnit.test("show correct operator", async (assert) => {
|
||||
serverData.models.partner.fields.state = {
|
||||
string: "State",
|
||||
type: "selection",
|
||||
selection: [
|
||||
["abc", "ABC"],
|
||||
["def", "DEF"],
|
||||
["ghi", "GHI"],
|
||||
],
|
||||
};
|
||||
|
||||
await mountComponent(DomainSelector, {
|
||||
props: {
|
||||
resModel: "partner",
|
||||
value: `[['state', 'in', ['abc']]]`,
|
||||
readonly: false,
|
||||
},
|
||||
});
|
||||
|
||||
const select = target.querySelector(".o_domain_leaf_operator_select");
|
||||
assert.strictEqual(select.options[select.options.selectedIndex].text, "in");
|
||||
});
|
||||
|
||||
QUnit.test("multi selection", async (assert) => {
|
||||
serverData.models.partner.fields.state = {
|
||||
string: "State",
|
||||
type: "selection",
|
||||
selection: [
|
||||
["a", "A"],
|
||||
["b", "B"],
|
||||
["c", "C"],
|
||||
],
|
||||
};
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.value = `[("state", "in", ["a", "b", "c"])]`;
|
||||
}
|
||||
onUpdate(newValue) {
|
||||
this.value = newValue;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
Parent.components = { DomainSelector };
|
||||
Parent.template = xml`
|
||||
<DomainSelector
|
||||
resModel="'partner'"
|
||||
value="value"
|
||||
readonly="false"
|
||||
update="(newValue) => this.onUpdate(newValue)"
|
||||
/>
|
||||
`;
|
||||
|
||||
// Create the domain selector and its mock environment
|
||||
const comp = await mountComponent(Parent);
|
||||
|
||||
assert.containsOnce(target, ".o_domain_leaf_value_input");
|
||||
assert.strictEqual(comp.value, `[("state", "in", ["a", "b", "c"])]`);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_leaf_value_input").value,
|
||||
`["a", "b", "c"]`
|
||||
);
|
||||
|
||||
await editInput(target, ".o_domain_leaf_value_input", `[]`);
|
||||
assert.strictEqual(comp.value, `[("state", "in", [])]`);
|
||||
|
||||
await editInput(target, ".o_domain_leaf_value_input", `["b"]`);
|
||||
assert.strictEqual(comp.value, `[("state", "in", ["b"])]`);
|
||||
});
|
||||
|
||||
QUnit.test("updating path should also update operator if invalid", async (assert) => {
|
||||
await mountComponent(DomainSelector, {
|
||||
props: {
|
||||
resModel: "partner",
|
||||
value: `[("id", "<", 0)]`,
|
||||
readonly: false,
|
||||
update: (domain) => {
|
||||
assert.strictEqual(domain, `[("foo", "=", "")]`);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_field_selector");
|
||||
await click(target, ".o_field_selector_popover .o_field_selector_item[data-name=foo]");
|
||||
});
|
||||
|
||||
QUnit.test("do not crash with connector '!'", async (assert) => {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.domain = `["!", ("foo", "=", "abc")]`;
|
||||
}
|
||||
}
|
||||
Parent.components = { DomainSelector };
|
||||
Parent.template = xml`<DomainSelector resModel="'partner'" value="domain" readonly="false"/>`;
|
||||
await mountComponent(Parent);
|
||||
assert.containsOnce(target, ".o_domain_node.o_domain_leaf");
|
||||
});
|
||||
});
|
||||
562
odoo-bringout-oca-ocb-web/web/static/tests/core/domain_tests.js
Normal file
562
odoo-bringout-oca-ocb-web/web/static/tests/core/domain_tests.js
Normal file
|
|
@ -0,0 +1,562 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { PyDate } from "../../src/core/py_js/py_date";
|
||||
import { patchWithCleanup } from "../helpers/utils";
|
||||
|
||||
QUnit.module("domain", {}, () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Basic properties
|
||||
// ---------------------------------------------------------------------------
|
||||
QUnit.module("Basic Properties");
|
||||
|
||||
QUnit.test("empty", function (assert) {
|
||||
assert.ok(new Domain([]).contains({}));
|
||||
assert.strictEqual(new Domain([]).toString(), "[]");
|
||||
assert.deepEqual(new Domain([]).toList(), []);
|
||||
});
|
||||
|
||||
QUnit.test("undefined domain", function (assert) {
|
||||
assert.ok(new Domain(undefined).contains({}));
|
||||
assert.strictEqual(new Domain(undefined).toString(), "[]");
|
||||
assert.deepEqual(new Domain(undefined).toList(), []);
|
||||
});
|
||||
|
||||
QUnit.test("simple condition", function (assert) {
|
||||
assert.ok(new Domain([["a", "=", 3]]).contains({ a: 3 }));
|
||||
assert.notOk(new Domain([["a", "=", 3]]).contains({ a: 5 }));
|
||||
assert.strictEqual(new Domain([["a", "=", 3]]).toString(), `[("a", "=", 3)]`);
|
||||
assert.deepEqual(new Domain([["a", "=", 3]]).toList(), [["a", "=", 3]]);
|
||||
});
|
||||
|
||||
QUnit.test("can be created from domain", function (assert) {
|
||||
const domain = new Domain([["a", "=", 3]]);
|
||||
assert.strictEqual(new Domain(domain).toString(), `[("a", "=", 3)]`);
|
||||
});
|
||||
|
||||
QUnit.test("basic", function (assert) {
|
||||
const record = {
|
||||
a: 3,
|
||||
group_method: "line",
|
||||
select1: "day",
|
||||
rrule_type: "monthly",
|
||||
};
|
||||
assert.ok(new Domain([["a", "=", 3]]).contains(record));
|
||||
assert.notOk(new Domain([["a", "=", 5]]).contains(record));
|
||||
assert.ok(new Domain([["group_method", "!=", "count"]]).contains(record));
|
||||
assert.ok(
|
||||
new Domain([
|
||||
["select1", "=", "day"],
|
||||
["rrule_type", "=", "monthly"],
|
||||
]).contains(record)
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("support of '=?' operator", function (assert) {
|
||||
const record = { a: 3 };
|
||||
assert.ok(new Domain([["a", "=?", null]]).contains(record));
|
||||
assert.ok(new Domain([["a", "=?", false]]).contains(record));
|
||||
assert.notOk(new Domain(["!", ["a", "=?", false]]).contains(record));
|
||||
assert.notOk(new Domain([["a", "=?", 1]]).contains(record));
|
||||
assert.ok(new Domain([["a", "=?", 3]]).contains(record));
|
||||
assert.notOk(new Domain(["!", ["a", "=?", 3]]).contains(record));
|
||||
});
|
||||
|
||||
QUnit.test("or", function (assert) {
|
||||
const currentDomain = [
|
||||
"|",
|
||||
["section_id", "=", 42],
|
||||
"|",
|
||||
["user_id", "=", 3],
|
||||
["member_ids", "in", [3]],
|
||||
];
|
||||
const record = {
|
||||
section_id: null,
|
||||
user_id: null,
|
||||
member_ids: null,
|
||||
};
|
||||
assert.ok(new Domain(currentDomain).contains({ ...record, section_id: 42 }));
|
||||
assert.ok(new Domain(currentDomain).contains({ ...record, user_id: 3 }));
|
||||
assert.ok(new Domain(currentDomain).contains({ ...record, member_ids: 3 }));
|
||||
});
|
||||
|
||||
QUnit.test("and", function (assert) {
|
||||
const domain = new Domain(["&", "&", ["a", "=", 1], ["b", "=", 2], ["c", "=", 3]]);
|
||||
|
||||
assert.ok(domain.contains({ a: 1, b: 2, c: 3 }));
|
||||
assert.notOk(domain.contains({ a: -1, b: 2, c: 3 }));
|
||||
assert.notOk(domain.contains({ a: 1, b: -1, c: 3 }));
|
||||
assert.notOk(domain.contains({ a: 1, b: 2, c: -1 }));
|
||||
});
|
||||
|
||||
QUnit.test("not", function (assert) {
|
||||
const record = {
|
||||
a: 5,
|
||||
group_method: "line",
|
||||
};
|
||||
assert.ok(new Domain(["!", ["a", "=", 3]]).contains(record));
|
||||
assert.ok(new Domain(["!", ["group_method", "=", "count"]]).contains(record));
|
||||
});
|
||||
|
||||
QUnit.test("like, =like, ilike and =ilike", function (assert) {
|
||||
assert.expect(16);
|
||||
|
||||
assert.ok(new Domain([["a", "like", "value"]]).contains({ a: "value" }));
|
||||
assert.ok(new Domain([["a", "like", "value"]]).contains({ a: "some value" }));
|
||||
assert.notOk(new Domain([["a", "like", "value"]]).contains({ a: "Some Value" }));
|
||||
assert.notOk(new Domain([["a", "like", "value"]]).contains({ a: false }));
|
||||
|
||||
assert.ok(new Domain([["a", "=like", "%value"]]).contains({ a: "value" }));
|
||||
assert.ok(new Domain([["a", "=like", "%value"]]).contains({ a: "some value" }));
|
||||
assert.notOk(new Domain([["a", "=like", "%value"]]).contains({ a: "Some Value" }));
|
||||
assert.notOk(new Domain([["a", "=like", "%value"]]).contains({ a: false }));
|
||||
|
||||
assert.ok(new Domain([["a", "ilike", "value"]]).contains({ a: "value" }));
|
||||
assert.ok(new Domain([["a", "ilike", "value"]]).contains({ a: "some value" }));
|
||||
assert.ok(new Domain([["a", "ilike", "value"]]).contains({ a: "Some Value" }));
|
||||
assert.notOk(new Domain([["a", "ilike", "value"]]).contains({ a: false }));
|
||||
|
||||
assert.ok(new Domain([["a", "=ilike", "%value"]]).contains({ a: "value" }));
|
||||
assert.ok(new Domain([["a", "=ilike", "%value"]]).contains({ a: "some value" }));
|
||||
assert.ok(new Domain([["a", "=ilike", "%value"]]).contains({ a: "Some Value" }));
|
||||
assert.notOk(new Domain([["a", "=ilike", "%value"]]).contains({ a: false }));
|
||||
});
|
||||
|
||||
QUnit.test("complex domain", function (assert) {
|
||||
const domain = new Domain(["&", "!", ["a", "=", 1], "|", ["a", "=", 2], ["a", "=", 3]]);
|
||||
|
||||
assert.notOk(domain.contains({ a: 1 }));
|
||||
assert.ok(domain.contains({ a: 2 }));
|
||||
assert.ok(domain.contains({ a: 3 }));
|
||||
assert.notOk(domain.contains({ a: 4 }));
|
||||
});
|
||||
|
||||
QUnit.test("toList", function (assert) {
|
||||
assert.deepEqual(new Domain([]).toList(), []);
|
||||
assert.deepEqual(new Domain([["a", "=", 3]]).toList(), [["a", "=", 3]]);
|
||||
assert.deepEqual(
|
||||
new Domain([
|
||||
["a", "=", 3],
|
||||
["b", "!=", "4"],
|
||||
]).toList(),
|
||||
["&", ["a", "=", 3], ["b", "!=", "4"]]
|
||||
);
|
||||
assert.deepEqual(new Domain(["!", ["a", "=", 3]]).toList(), ["!", ["a", "=", 3]]);
|
||||
});
|
||||
|
||||
QUnit.test("toString", function (assert) {
|
||||
assert.strictEqual(new Domain([]).toString(), `[]`);
|
||||
assert.strictEqual(new Domain([["a", "=", 3]]).toString(), `[("a", "=", 3)]`);
|
||||
assert.strictEqual(
|
||||
new Domain([
|
||||
["a", "=", 3],
|
||||
["b", "!=", "4"],
|
||||
]).toString(),
|
||||
`["&", ("a", "=", 3), ("b", "!=", "4")]`
|
||||
);
|
||||
assert.strictEqual(new Domain(["!", ["a", "=", 3]]).toString(), `["!", ("a", "=", 3)]`);
|
||||
assert.strictEqual(new Domain([["name", "=", null]]).toString(), '[("name", "=", None)]');
|
||||
assert.strictEqual(new Domain([["name", "=", false]]).toString(), '[("name", "=", False)]');
|
||||
assert.strictEqual(new Domain([["name", "=", true]]).toString(), '[("name", "=", True)]');
|
||||
assert.strictEqual(
|
||||
new Domain([["name", "=", "null"]]).toString(),
|
||||
'[("name", "=", "null")]'
|
||||
);
|
||||
assert.strictEqual(
|
||||
new Domain([["name", "=", "false"]]).toString(),
|
||||
'[("name", "=", "false")]'
|
||||
);
|
||||
assert.strictEqual(
|
||||
new Domain([["name", "=", "true"]]).toString(),
|
||||
'[("name", "=", "true")]'
|
||||
);
|
||||
assert.strictEqual(new Domain().toString(), "[]");
|
||||
assert.strictEqual(
|
||||
new Domain([["name", "in", [true, false]]]).toString(),
|
||||
'[("name", "in", [True, False])]'
|
||||
);
|
||||
assert.strictEqual(
|
||||
new Domain([["name", "in", [null]]]).toString(),
|
||||
'[("name", "in", [None])]'
|
||||
);
|
||||
assert.strictEqual(
|
||||
new Domain([["name", "in", ["foo", "bar"]]]).toString(),
|
||||
'[("name", "in", ["foo", "bar"])]'
|
||||
);
|
||||
assert.strictEqual(
|
||||
new Domain([["name", "in", [1, 2]]]).toString(),
|
||||
'[("name", "in", [1, 2])]'
|
||||
);
|
||||
assert.strictEqual(
|
||||
new Domain(["&", ["name", "=", "foo"], ["type", "=", "bar"]]).toString(),
|
||||
'["&", ("name", "=", "foo"), ("type", "=", "bar")]'
|
||||
);
|
||||
assert.strictEqual(
|
||||
new Domain(["|", ["name", "=", "foo"], ["type", "=", "bar"]]).toString(),
|
||||
'["|", ("name", "=", "foo"), ("type", "=", "bar")]'
|
||||
);
|
||||
assert.strictEqual(new Domain().toString(), "[]");
|
||||
|
||||
// string domains are only reformatted
|
||||
assert.strictEqual(
|
||||
new Domain('[("name","ilike","foo")]').toString(),
|
||||
'[("name", "ilike", "foo")]'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("implicit &", function (assert) {
|
||||
const domain = new Domain([
|
||||
["a", "=", 3],
|
||||
["b", "=", 4],
|
||||
]);
|
||||
assert.notOk(domain.contains({}));
|
||||
assert.ok(domain.contains({ a: 3, b: 4 }));
|
||||
assert.notOk(domain.contains({ a: 3, b: 5 }));
|
||||
});
|
||||
|
||||
QUnit.test("comparison operators", function (assert) {
|
||||
assert.ok(new Domain([["a", "=", 3]]).contains({ a: 3 }));
|
||||
assert.notOk(new Domain([["a", "=", 3]]).contains({ a: 4 }));
|
||||
assert.strictEqual(new Domain([["a", "=", 3]]).toString(), `[("a", "=", 3)]`);
|
||||
assert.ok(new Domain([["a", "==", 3]]).contains({ a: 3 }));
|
||||
assert.notOk(new Domain([["a", "==", 3]]).contains({ a: 4 }));
|
||||
assert.strictEqual(new Domain([["a", "==", 3]]).toString(), `[("a", "==", 3)]`);
|
||||
assert.notOk(new Domain([["a", "!=", 3]]).contains({ a: 3 }));
|
||||
assert.ok(new Domain([["a", "!=", 3]]).contains({ a: 4 }));
|
||||
assert.strictEqual(new Domain([["a", "!=", 3]]).toString(), `[("a", "!=", 3)]`);
|
||||
assert.notOk(new Domain([["a", "<>", 3]]).contains({ a: 3 }));
|
||||
assert.ok(new Domain([["a", "<>", 3]]).contains({ a: 4 }));
|
||||
assert.strictEqual(new Domain([["a", "<>", 3]]).toString(), `[("a", "<>", 3)]`);
|
||||
assert.notOk(new Domain([["a", "<", 3]]).contains({ a: 5 }));
|
||||
assert.notOk(new Domain([["a", "<", 3]]).contains({ a: 3 }));
|
||||
assert.ok(new Domain([["a", "<", 3]]).contains({ a: 2 }));
|
||||
assert.strictEqual(new Domain([["a", "<", 3]]).toString(), `[("a", "<", 3)]`);
|
||||
assert.notOk(new Domain([["a", "<=", 3]]).contains({ a: 5 }));
|
||||
assert.ok(new Domain([["a", "<=", 3]]).contains({ a: 3 }));
|
||||
assert.ok(new Domain([["a", "<=", 3]]).contains({ a: 2 }));
|
||||
assert.strictEqual(new Domain([["a", "<=", 3]]).toString(), `[("a", "<=", 3)]`);
|
||||
assert.ok(new Domain([["a", ">", 3]]).contains({ a: 5 }));
|
||||
assert.notOk(new Domain([["a", ">", 3]]).contains({ a: 3 }));
|
||||
assert.notOk(new Domain([["a", ">", 3]]).contains({ a: 2 }));
|
||||
assert.strictEqual(new Domain([["a", ">", 3]]).toString(), `[("a", ">", 3)]`);
|
||||
assert.ok(new Domain([["a", ">=", 3]]).contains({ a: 5 }));
|
||||
assert.ok(new Domain([["a", ">=", 3]]).contains({ a: 3 }));
|
||||
assert.notOk(new Domain([["a", ">=", 3]]).contains({ a: 2 }));
|
||||
assert.strictEqual(new Domain([["a", ">=", 3]]).toString(), `[("a", ">=", 3)]`);
|
||||
});
|
||||
|
||||
QUnit.test("other operators", function (assert) {
|
||||
assert.ok(new Domain([["a", "in", 3]]).contains({ a: 3 }));
|
||||
assert.ok(new Domain([["a", "in", [1, 2, 3]]]).contains({ a: 3 }));
|
||||
assert.ok(new Domain([["a", "in", [1, 2, 3]]]).contains({ a: [3] }));
|
||||
assert.notOk(new Domain([["a", "in", 3]]).contains({ a: 5 }));
|
||||
assert.notOk(new Domain([["a", "in", [1, 2, 3]]]).contains({ a: 5 }));
|
||||
assert.notOk(new Domain([["a", "in", [1, 2, 3]]]).contains({ a: [5] }));
|
||||
assert.notOk(new Domain([["a", "not in", 3]]).contains({ a: 3 }));
|
||||
assert.notOk(new Domain([["a", "not in", [1, 2, 3]]]).contains({ a: 3 }));
|
||||
assert.notOk(new Domain([["a", "not in", [1, 2, 3]]]).contains({ a: [3] }));
|
||||
assert.ok(new Domain([["a", "not in", 3]]).contains({ a: 5 }));
|
||||
assert.ok(new Domain([["a", "not in", [1, 2, 3]]]).contains({ a: 5 }));
|
||||
assert.ok(new Domain([["a", "not in", [1, 2, 3]]]).contains({ a: [5] }));
|
||||
assert.ok(new Domain([["a", "like", "abc"]]).contains({ a: "abc" }));
|
||||
assert.notOk(new Domain([["a", "like", "abc"]]).contains({ a: "def" }));
|
||||
assert.ok(new Domain([["a", "=like", "abc"]]).contains({ a: "abc" }));
|
||||
assert.notOk(new Domain([["a", "=like", "abc"]]).contains({ a: "def" }));
|
||||
assert.ok(new Domain([["a", "ilike", "abc"]]).contains({ a: "abc" }));
|
||||
assert.notOk(new Domain([["a", "ilike", "abc"]]).contains({ a: "def" }));
|
||||
assert.ok(new Domain([["a", "=ilike", "abc"]]).contains({ a: "abc" }));
|
||||
assert.notOk(new Domain([["a", "=ilike", "abc"]]).contains({ a: "def" }));
|
||||
});
|
||||
|
||||
QUnit.test("creating a domain with a string expression", function (assert) {
|
||||
assert.strictEqual(new Domain(`[('a', '>=', 3)]`).toString(), `[("a", ">=", 3)]`);
|
||||
assert.ok(new Domain(`[('a', '>=', 3)]`).contains({ a: 5 }));
|
||||
});
|
||||
|
||||
QUnit.test("can evaluate a python expression", function (assert) {
|
||||
assert.deepEqual(new Domain(`[('date', '!=', False)]`).toList(), [["date", "!=", false]]);
|
||||
assert.deepEqual(new Domain(`[('date', '!=', False)]`).toList(), [["date", "!=", false]]);
|
||||
assert.deepEqual(
|
||||
new Domain(`[('date', '!=', 1 + 2)]`).toString(),
|
||||
`[("date", "!=", 1 + 2)]`
|
||||
);
|
||||
assert.deepEqual(new Domain(`[('date', '!=', 1 + 2)]`).toList(), [["date", "!=", 3]]);
|
||||
assert.ok(new Domain(`[('a', '==', 1 + 2)]`).contains({ a: 3 }));
|
||||
assert.notOk(new Domain(`[('a', '==', 1 + 2)]`).contains({ a: 2 }));
|
||||
});
|
||||
|
||||
QUnit.test("some expression with date stuff", function (assert) {
|
||||
patchWithCleanup(PyDate, {
|
||||
today() {
|
||||
return new PyDate(2013, 4, 24);
|
||||
},
|
||||
});
|
||||
let domainStr =
|
||||
"[('date','>=', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]";
|
||||
assert.deepEqual(new Domain(domainStr).toList(), [["date", ">=", "2013-03-25"]]);
|
||||
domainStr = "[('date', '>=', context_today() - relativedelta(days=30))]";
|
||||
const domainList = new Domain(domainStr).toList(); // domain creation using `parseExpr` function since the parameter is a string.
|
||||
assert.deepEqual(
|
||||
domainList[0][2],
|
||||
PyDate.create({ day: 25, month: 3, year: 2013 }),
|
||||
"The right item in the rule in the domain should be a PyDate object"
|
||||
);
|
||||
assert.deepEqual(JSON.stringify(domainList), '[["date",">=","2013-03-25"]]');
|
||||
const domainList2 = new Domain(domainList).toList(); // domain creation using `toAST` function since the parameter is a list.
|
||||
assert.deepEqual(
|
||||
domainList2[0][2],
|
||||
PyDate.create({ day: 25, month: 3, year: 2013 }),
|
||||
"The right item in the rule in the domain should be a PyDate object"
|
||||
);
|
||||
assert.deepEqual(JSON.stringify(domainList2), '[["date",">=","2013-03-25"]]');
|
||||
});
|
||||
|
||||
QUnit.test("Check that there is no dependency between two domains", function (assert) {
|
||||
// The purpose of this test is to verify that a domain created on the basis
|
||||
// of another one does not share any dependency.
|
||||
const domain1 = new Domain(`[('date', '!=', False)]`);
|
||||
const domain2 = new Domain(domain1);
|
||||
assert.strictEqual(domain1.toString(), domain2.toString());
|
||||
|
||||
domain2.ast.value.unshift({ type: 1, value: "!" });
|
||||
assert.notEqual(domain1.toString(), domain2.toString());
|
||||
});
|
||||
|
||||
QUnit.test("TRUE and FALSE Domain", function (assert) {
|
||||
assert.ok(Domain.TRUE.contains({}));
|
||||
assert.notOk(Domain.FALSE.contains({}));
|
||||
|
||||
assert.ok(Domain.and([Domain.TRUE, new Domain([["a", "=", 3]])]).contains({ a: 3 }));
|
||||
assert.notOk(Domain.and([Domain.FALSE, new Domain([["a", "=", 3]])]).contains({ a: 3 }));
|
||||
});
|
||||
|
||||
QUnit.test("invalid domains should not succeed", function (assert) {
|
||||
assert.throws(
|
||||
() => new Domain(["|", ["hr_presence_state", "=", "absent"]]),
|
||||
/invalid domain .* \(missing 1 segment/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
new Domain([
|
||||
"|",
|
||||
"|",
|
||||
["hr_presence_state", "=", "absent"],
|
||||
["attendance_state", "=", "checked_in"],
|
||||
]),
|
||||
/invalid domain .* \(missing 1 segment/
|
||||
);
|
||||
assert.throws(
|
||||
() => new Domain(["|", "|", ["hr_presence_state", "=", "absent"]]),
|
||||
/invalid domain .* \(missing 2 segment\(s\)/
|
||||
);
|
||||
assert.throws(
|
||||
() => new Domain(["&", ["composition_mode", "!=", "mass_post"]]),
|
||||
/invalid domain .* \(missing 1 segment/
|
||||
);
|
||||
assert.throws(() => new Domain(["!"]), /invalid domain .* \(missing 1 segment/);
|
||||
});
|
||||
|
||||
QUnit.test("follow relations", function (assert) {
|
||||
assert.ok(
|
||||
new Domain([["partner.city", "ilike", "Bru"]]).contains({
|
||||
name: "Lucas",
|
||||
partner: {
|
||||
city: "Bruxelles",
|
||||
},
|
||||
})
|
||||
);
|
||||
assert.ok(
|
||||
new Domain([["partner.city.name", "ilike", "Bru"]]).contains({
|
||||
name: "Lucas",
|
||||
partner: {
|
||||
city: {
|
||||
name: "Bruxelles",
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Arrays comparison", (assert) => {
|
||||
const domain = new Domain(["&", ["a", "==", []], ["b", "!=", []]]);
|
||||
|
||||
assert.ok(domain.contains({ a: [] }));
|
||||
assert.ok(domain.contains({ a: [], b: [4] }));
|
||||
assert.notOk(domain.contains({ a: [1] }));
|
||||
assert.notOk(domain.contains({ b: [] }));
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Normalization
|
||||
// ---------------------------------------------------------------------------
|
||||
QUnit.module("Normalization");
|
||||
|
||||
QUnit.test("return simple (normalized) domains", function (assert) {
|
||||
const domains = ["[]", `[("a", "=", 1)]`, `["!", ("a", "=", 1)]`];
|
||||
for (const domain of domains) {
|
||||
assert.strictEqual(new Domain(domain).toString(), domain);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("properly add the & in a non normalized domain", function (assert) {
|
||||
assert.strictEqual(
|
||||
new Domain(`[("a", "=", 1), ("b", "=", 2)]`).toString(),
|
||||
`["&", ("a", "=", 1), ("b", "=", 2)]`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("normalize domain with ! operator", function (assert) {
|
||||
assert.strictEqual(
|
||||
new Domain(`["!", ("a", "=", 1), ("b", "=", 2)]`).toString(),
|
||||
`["&", "!", ("a", "=", 1), ("b", "=", 2)]`
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Combining domains
|
||||
// ---------------------------------------------------------------------------
|
||||
QUnit.module("Combining domains");
|
||||
|
||||
QUnit.test("combining zero domain", function (assert) {
|
||||
assert.strictEqual(Domain.combine([], "AND").toString(), "[]");
|
||||
assert.strictEqual(Domain.combine([], "OR").toString(), "[]");
|
||||
assert.ok(Domain.combine([], "AND").contains({ a: 1, b: 2 }));
|
||||
});
|
||||
|
||||
QUnit.test("combining one domain", function (assert) {
|
||||
assert.strictEqual(
|
||||
Domain.combine([`[("a", "=", 1)]`], "AND").toString(),
|
||||
`[("a", "=", 1)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.combine([`[("user_id", "=", uid)]`], "AND").toString(),
|
||||
`[("user_id", "=", uid)]`
|
||||
);
|
||||
assert.strictEqual(Domain.combine([[["a", "=", 1]]], "AND").toString(), `[("a", "=", 1)]`);
|
||||
assert.strictEqual(
|
||||
Domain.combine(["[('a', '=', '1'), ('b', '!=', 2)]"], "AND").toString(),
|
||||
`["&", ("a", "=", "1"), ("b", "!=", 2)]`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("combining two domains", function (assert) {
|
||||
assert.strictEqual(
|
||||
Domain.combine([`[("a", "=", 1)]`, "[]"], "AND").toString(),
|
||||
`[("a", "=", 1)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.combine([`[("a", "=", 1)]`, []], "AND").toString(),
|
||||
`[("a", "=", 1)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.combine([new Domain(`[("a", "=", 1)]`), "[]"], "AND").toString(),
|
||||
`[("a", "=", 1)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.combine([new Domain(`[("a", "=", 1)]`), "[]"], "OR").toString(),
|
||||
`[("a", "=", 1)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.combine([[["a", "=", 1]], "[('uid', '<=', uid)]"], "AND").toString(),
|
||||
`["&", ("a", "=", 1), ("uid", "<=", uid)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.combine([[["a", "=", 1]], "[('b', '<=', 3)]"], "OR").toString(),
|
||||
`["|", ("a", "=", 1), ("b", "<=", 3)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.combine(
|
||||
["[('a', '=', '1'), ('c', 'in', [4, 5])]", "[('b', '<=', 3)]"],
|
||||
"OR"
|
||||
).toString(),
|
||||
`["|", "&", ("a", "=", "1"), ("c", "in", [4, 5]), ("b", "<=", 3)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.combine(
|
||||
[new Domain("[('a', '=', '1'), ('c', 'in', [4, 5])]"), "[('b', '<=', 3)]"],
|
||||
"OR"
|
||||
).toString(),
|
||||
`["|", "&", ("a", "=", "1"), ("c", "in", [4, 5]), ("b", "<=", 3)]`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("combining three domains", function (assert) {
|
||||
assert.strictEqual(
|
||||
Domain.combine(
|
||||
[
|
||||
new Domain("[('a', '=', '1'), ('c', 'in', [4, 5])]"),
|
||||
[["b", "<=", 3]],
|
||||
`['!', ('uid', '=', uid)]`,
|
||||
],
|
||||
"OR"
|
||||
).toString(),
|
||||
`["|", "&", ("a", "=", "1"), ("c", "in", [4, 5]), "|", ("b", "<=", 3), "!", ("uid", "=", uid)]`
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OPERATOR AND / OR / NOT
|
||||
// ---------------------------------------------------------------------------
|
||||
QUnit.module("Operator and/or/not");
|
||||
QUnit.test("combining two domains with and/or", function (assert) {
|
||||
assert.strictEqual(Domain.and([`[("a", "=", 1)]`, "[]"]).toString(), `[("a", "=", 1)]`);
|
||||
assert.strictEqual(Domain.and([`[("a", "=", 1)]`, []]).toString(), `[("a", "=", 1)]`);
|
||||
assert.strictEqual(
|
||||
Domain.and([new Domain(`[("a", "=", 1)]`), "[]"]).toString(),
|
||||
`[("a", "=", 1)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.or([new Domain(`[("a", "=", 1)]`), "[]"]).toString(),
|
||||
`[("a", "=", 1)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.and([[["a", "=", 1]], "[('uid', '<=', uid)]"]).toString(),
|
||||
`["&", ("a", "=", 1), ("uid", "<=", uid)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.or([[["a", "=", 1]], "[('b', '<=', 3)]"]).toString(),
|
||||
`["|", ("a", "=", 1), ("b", "<=", 3)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.or(["[('a', '=', '1'), ('c', 'in', [4, 5])]", "[('b', '<=', 3)]"]).toString(),
|
||||
`["|", "&", ("a", "=", "1"), ("c", "in", [4, 5]), ("b", "<=", 3)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.or([
|
||||
new Domain("[('a', '=', '1'), ('c', 'in', [4, 5])]"),
|
||||
"[('b', '<=', 3)]",
|
||||
]).toString(),
|
||||
`["|", "&", ("a", "=", "1"), ("c", "in", [4, 5]), ("b", "<=", 3)]`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("apply `NOT` on a Domain", function (assert) {
|
||||
assert.strictEqual(Domain.not("[('a', '=', 1)]").toString(), `["!", ("a", "=", 1)]`);
|
||||
assert.strictEqual(
|
||||
Domain.not('[("uid", "<=", uid)]').toString(),
|
||||
`["!", ("uid", "<=", uid)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.not(new Domain("[('a', '=', 1)]")).toString(),
|
||||
`["!", ("a", "=", 1)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Domain.not(new Domain([["a", "=", 1]])).toString(),
|
||||
`["!", ("a", "=", 1)]`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("tuple are supported", (assert) => {
|
||||
assert.deepEqual(
|
||||
new Domain(`(("field", "like", "string"), ("field", "like", "strOng"))`).toList(),
|
||||
["&", ["field", "like", "string"], ["field", "like", "strOng"]]
|
||||
);
|
||||
assert.deepEqual(new Domain(`("!",("field", "like", "string"))`).toList(), [
|
||||
"!",
|
||||
["field", "like", "string"],
|
||||
]);
|
||||
assert.throws(() => new Domain(`(("field", "like", "string"))`), /Invalid domain AST/);
|
||||
assert.throws(() => new Domain(`("&", "&", "|")`), /Invalid domain AST/);
|
||||
assert.throws(() => new Domain(`("&", "&", 3)`), /Invalid domain AST/);
|
||||
});
|
||||
});
|
||||
1227
odoo-bringout-oca-ocb-web/web/static/tests/core/dropdown_tests.js
Normal file
1227
odoo-bringout-oca-ocb-web/web/static/tests/core/dropdown_tests.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,149 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { effectService } from "@web/core/effects/effect_service";
|
||||
import { userService } from "@web/core/user_service";
|
||||
import { session } from "@web/session";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
import { makeFakeLocalizationService } from "../../helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
mockTimeout,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "../../helpers/utils";
|
||||
|
||||
import { Component, markup, xml } from "@odoo/owl";
|
||||
const serviceRegistry = registry.category("services");
|
||||
const mainComponentRegistry = registry.category("main_components");
|
||||
|
||||
let target;
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.EffectContainer = mainComponentRegistry.get("EffectContainer");
|
||||
this.NotificationContainer = mainComponentRegistry.get("NotificationContainer");
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<t t-component="EffectContainer.Component" t-props="EffectContainer.props" />
|
||||
<t t-component="NotificationContainer.Component" t-props="NotificationContainer.props" />
|
||||
</div>
|
||||
`;
|
||||
|
||||
async function makeParent() {
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const parent = await mount(Parent, target, { env });
|
||||
return parent;
|
||||
}
|
||||
|
||||
QUnit.module("Effect Service", (hooks) => {
|
||||
let effectParams;
|
||||
let execRegisteredTimeouts;
|
||||
hooks.beforeEach(() => {
|
||||
effectParams = {
|
||||
message: markup("<div>Congrats!</div>"),
|
||||
};
|
||||
|
||||
execRegisteredTimeouts = mockTimeout().execRegisteredTimeouts;
|
||||
patchWithCleanup(session, { show_effect: true }); // enable effects
|
||||
|
||||
serviceRegistry.add("user", userService);
|
||||
serviceRegistry.add("effect", effectService);
|
||||
serviceRegistry.add("notification", notificationService);
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.test("effect service displays a rainbowman by default", async function (assert) {
|
||||
const parent = await makeParent();
|
||||
parent.env.services.effect.add();
|
||||
await nextTick();
|
||||
execRegisteredTimeouts();
|
||||
|
||||
assert.containsOnce(target, ".o_reward");
|
||||
assert.strictEqual(target.querySelector(".o_reward").innerText, "Well Done!");
|
||||
});
|
||||
|
||||
QUnit.test("rainbowman effect with show_effect: false", async function (assert) {
|
||||
patchWithCleanup(session, { show_effect: false });
|
||||
|
||||
const parent = await makeParent();
|
||||
parent.env.services.effect.add();
|
||||
await nextTick();
|
||||
execRegisteredTimeouts();
|
||||
|
||||
assert.containsNone(target, ".o_reward");
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
});
|
||||
|
||||
QUnit.test("rendering a rainbowman destroy after animation", async function (assert) {
|
||||
const parent = await makeParent();
|
||||
parent.env.services.effect.add(effectParams);
|
||||
await nextTick();
|
||||
execRegisteredTimeouts();
|
||||
|
||||
assert.containsOnce(target, ".o_reward");
|
||||
assert.containsOnce(target, ".o_reward_rainbow");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_reward_msg_content").innerHTML,
|
||||
"<div>Congrats!</div>"
|
||||
);
|
||||
|
||||
const ev = new AnimationEvent("animationend", { animationName: "reward-fading-reverse" });
|
||||
target.querySelector(".o_reward").dispatchEvent(ev);
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_reward");
|
||||
});
|
||||
|
||||
QUnit.test("rendering a rainbowman destroy on click", async function (assert) {
|
||||
const parent = await makeParent();
|
||||
|
||||
parent.env.services.effect.add(effectParams);
|
||||
await nextTick();
|
||||
execRegisteredTimeouts();
|
||||
|
||||
assert.containsOnce(target, ".o_reward");
|
||||
assert.containsOnce(target, ".o_reward_rainbow");
|
||||
|
||||
await click(target);
|
||||
assert.containsNone(target, ".o_reward");
|
||||
});
|
||||
|
||||
QUnit.test("rendering a rainbowman with an escaped message", async function (assert) {
|
||||
const parent = await makeParent();
|
||||
|
||||
parent.env.services.effect.add(effectParams);
|
||||
await nextTick();
|
||||
execRegisteredTimeouts();
|
||||
|
||||
assert.containsOnce(target, ".o_reward");
|
||||
assert.containsOnce(target, ".o_reward_rainbow");
|
||||
assert.strictEqual(target.querySelector(".o_reward_msg_content").textContent, "Congrats!");
|
||||
});
|
||||
|
||||
QUnit.test("rendering a rainbowman with a custom component", async function (assert) {
|
||||
assert.expect(2);
|
||||
const props = { foo: "bar" };
|
||||
class Custom extends Component {
|
||||
setup() {
|
||||
assert.deepEqual(this.props, props, "should have received these props");
|
||||
}
|
||||
}
|
||||
Custom.template = xml`<div class="custom">foo is <t t-esc="props.foo"/></div>`;
|
||||
|
||||
const parent = await makeParent();
|
||||
parent.env.services.effect.add({ Component: Custom, props });
|
||||
await nextTick();
|
||||
execRegisteredTimeouts();
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_reward_msg_content").innerHTML,
|
||||
`<div class="custom">foo is bar</div>`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import {
|
||||
ClientErrorDialog,
|
||||
Error504Dialog,
|
||||
ErrorDialog,
|
||||
RedirectWarningDialog,
|
||||
SessionExpiredDialog,
|
||||
WarningDialog,
|
||||
} from "@web/core/errors/error_dialogs";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
import { makeFakeDialogService, makeFakeLocalizationService } from "../../helpers/mock_services";
|
||||
import { click, getFixture, mount, nextTick, patchWithCleanup } from "../../helpers/utils";
|
||||
|
||||
let target;
|
||||
let env;
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
async function makeDialogTestEnv() {
|
||||
const env = await makeTestEnv();
|
||||
env.dialogData = {
|
||||
isActive: true,
|
||||
close() {},
|
||||
};
|
||||
return env;
|
||||
}
|
||||
|
||||
QUnit.module("Error dialogs", {
|
||||
async beforeEach() {
|
||||
target = getFixture();
|
||||
serviceRegistry.add("ui", uiService);
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
serviceRegistry.add("dialog", makeFakeDialogService());
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("ErrorDialog with traceback", async (assert) => {
|
||||
assert.expect(11);
|
||||
assert.containsNone(target, ".o_dialog");
|
||||
env = await makeDialogTestEnv();
|
||||
await mount(ErrorDialog, target, {
|
||||
env,
|
||||
props: {
|
||||
message: "Something bad happened",
|
||||
data: { debug: "Some strange unreadable stack" },
|
||||
name: "ERROR_NAME",
|
||||
traceback: "This is a tracback string",
|
||||
close() {},
|
||||
},
|
||||
});
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.strictEqual(target.querySelector("header .modal-title").textContent, "Odoo Error");
|
||||
const mainButtons = target.querySelectorAll("main button");
|
||||
assert.deepEqual(
|
||||
[...mainButtons].map((el) => el.textContent),
|
||||
["Copy the full error to clipboard", "See details"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("main .clearfix p")].map((el) => el.textContent),
|
||||
[
|
||||
"An error occurred",
|
||||
"Please use the copy button to report the error to your support service.",
|
||||
]
|
||||
);
|
||||
assert.containsNone(target, "div.o_error_detail");
|
||||
assert.strictEqual(target.querySelector(".o_dialog footer button").textContent, "Ok");
|
||||
click(mainButtons[1]);
|
||||
await nextTick();
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("main .clearfix p")].map((el) => el.textContent),
|
||||
[
|
||||
"An error occurred",
|
||||
"Please use the copy button to report the error to your support service.",
|
||||
"Something bad happened",
|
||||
]
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("main .clearfix code")].map((el) => el.textContent),
|
||||
["ERROR_NAME"]
|
||||
);
|
||||
assert.containsOnce(target, "div.o_error_detail");
|
||||
assert.strictEqual(
|
||||
target.querySelector("div.o_error_detail").textContent,
|
||||
"This is a tracback string"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Client ErrorDialog with traceback", async (assert) => {
|
||||
assert.expect(11);
|
||||
assert.containsNone(target, ".o_dialog");
|
||||
env = await makeDialogTestEnv();
|
||||
await mount(ClientErrorDialog, target, {
|
||||
env,
|
||||
props: {
|
||||
message: "Something bad happened",
|
||||
data: { debug: "Some strange unreadable stack" },
|
||||
name: "ERROR_NAME",
|
||||
traceback: "This is a traceback string",
|
||||
close() {},
|
||||
},
|
||||
});
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.strictEqual(
|
||||
target.querySelector("header .modal-title").textContent,
|
||||
"Odoo Client Error"
|
||||
);
|
||||
const mainButtons = target.querySelectorAll("main button");
|
||||
assert.deepEqual(
|
||||
[...mainButtons].map((el) => el.textContent),
|
||||
["Copy the full error to clipboard", "See details"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("main .clearfix p")].map((el) => el.textContent),
|
||||
[
|
||||
"An error occurred",
|
||||
"Please use the copy button to report the error to your support service.",
|
||||
]
|
||||
);
|
||||
assert.containsNone(target, "div.o_error_detail");
|
||||
assert.strictEqual(target.querySelector(".o_dialog footer button").textContent, "Ok");
|
||||
click(mainButtons[1]);
|
||||
await nextTick();
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("main .clearfix p")].map((el) => el.textContent),
|
||||
[
|
||||
"An error occurred",
|
||||
"Please use the copy button to report the error to your support service.",
|
||||
"Something bad happened",
|
||||
]
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("main .clearfix code")].map((el) => el.textContent),
|
||||
["ERROR_NAME"]
|
||||
);
|
||||
assert.containsOnce(target, "div.o_error_detail");
|
||||
assert.strictEqual(
|
||||
target.querySelector("div.o_error_detail").textContent,
|
||||
"This is a traceback string"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("button clipboard copy error traceback", async (assert) => {
|
||||
assert.expect(1);
|
||||
const error = new Error();
|
||||
error.name = "ERROR_NAME";
|
||||
error.message = "This is the message";
|
||||
error.traceback = "This is a traceback";
|
||||
patchWithCleanup(browser, {
|
||||
navigator: {
|
||||
clipboard: {
|
||||
writeText: (value) => {
|
||||
assert.strictEqual(
|
||||
value,
|
||||
`${error.name}\n${error.message}\n${error.traceback}`
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
env = await makeDialogTestEnv();
|
||||
await mount(ErrorDialog, target, {
|
||||
env,
|
||||
props: {
|
||||
message: error.message,
|
||||
name: "ERROR_NAME",
|
||||
traceback: "This is a traceback",
|
||||
close() {},
|
||||
},
|
||||
});
|
||||
const clipboardButton = target.querySelector(".fa-clipboard");
|
||||
click(clipboardButton);
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
QUnit.test("WarningDialog", async (assert) => {
|
||||
assert.expect(6);
|
||||
assert.containsNone(target, ".o_dialog");
|
||||
env = await makeDialogTestEnv();
|
||||
await mount(WarningDialog, target, {
|
||||
env,
|
||||
props: {
|
||||
exceptionName: "odoo.exceptions.UserError",
|
||||
message: "...",
|
||||
data: { arguments: ["Some strange unreadable message"] },
|
||||
close() {},
|
||||
},
|
||||
});
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.strictEqual(target.querySelector("header .modal-title").textContent, "User Error");
|
||||
assert.containsOnce(target, "main .o_dialog_warning");
|
||||
assert.strictEqual(target.querySelector("main").textContent, "Some strange unreadable message");
|
||||
assert.strictEqual(target.querySelector(".o_dialog footer button").textContent, "Ok");
|
||||
});
|
||||
|
||||
QUnit.test("RedirectWarningDialog", async (assert) => {
|
||||
assert.expect(10);
|
||||
const faceActionService = {
|
||||
name: "action",
|
||||
start() {
|
||||
return {
|
||||
doAction(actionId) {
|
||||
assert.step(actionId);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("action", faceActionService);
|
||||
env = await makeDialogTestEnv();
|
||||
assert.containsNone(target, ".o_dialog");
|
||||
await mount(RedirectWarningDialog, target, {
|
||||
env,
|
||||
props: {
|
||||
data: {
|
||||
arguments: [
|
||||
"Some strange unreadable message",
|
||||
"buy_action_id",
|
||||
"Buy book on cryptography",
|
||||
],
|
||||
},
|
||||
close() {
|
||||
assert.step("dialog-closed");
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.strictEqual(target.querySelector("header .modal-title").textContent, "Odoo Warning");
|
||||
assert.strictEqual(target.querySelector("main").textContent, "Some strange unreadable message");
|
||||
const footerButtons = target.querySelectorAll("footer button");
|
||||
assert.deepEqual(
|
||||
[...footerButtons].map((el) => el.textContent),
|
||||
["Buy book on cryptography", "Cancel"]
|
||||
);
|
||||
await click(footerButtons[0]); // click on "Buy book on cryptography"
|
||||
assert.verifySteps(["buy_action_id", "dialog-closed"]);
|
||||
|
||||
await click(footerButtons[1]); // click on "Cancel"
|
||||
assert.verifySteps(["dialog-closed"]);
|
||||
});
|
||||
|
||||
QUnit.test("Error504Dialog", async (assert) => {
|
||||
assert.expect(5);
|
||||
assert.containsNone(target, ".o_dialog");
|
||||
env = await makeDialogTestEnv();
|
||||
await mount(Error504Dialog, target, { env, props: { close() {} } });
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.strictEqual(target.querySelector("header .modal-title").textContent, "Request timeout");
|
||||
assert.strictEqual(
|
||||
target.querySelector("main p").textContent,
|
||||
" The operation was interrupted. This usually means that the current operation is taking too much time. "
|
||||
);
|
||||
assert.strictEqual(target.querySelector(".o_dialog footer button").textContent, "Ok");
|
||||
});
|
||||
|
||||
QUnit.test("SessionExpiredDialog", async (assert) => {
|
||||
assert.expect(7);
|
||||
patchWithCleanup(browser, {
|
||||
location: {
|
||||
reload() {
|
||||
assert.step("location reload");
|
||||
},
|
||||
},
|
||||
});
|
||||
env = await makeDialogTestEnv();
|
||||
assert.containsNone(target, ".o_dialog");
|
||||
await mount(SessionExpiredDialog, target, { env, props: { close() {} } });
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.strictEqual(
|
||||
target.querySelector("header .modal-title").textContent,
|
||||
"Odoo Session Expired"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("main p").textContent,
|
||||
" Your Odoo session expired. The current page is about to be refreshed. "
|
||||
);
|
||||
const footerButton = target.querySelector(".o_dialog footer button");
|
||||
assert.strictEqual(footerButton.textContent, "Ok");
|
||||
click(footerButton);
|
||||
assert.verifySteps(["location reload"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,541 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { ClientErrorDialog, RPCErrorDialog } from "@web/core/errors/error_dialogs";
|
||||
import { errorService, UncaughtPromiseError } from "@web/core/errors/error_service";
|
||||
import { ConnectionLostError, RPCError } from "@web/core/network/rpc_service";
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
import {
|
||||
makeFakeDialogService,
|
||||
makeFakeLocalizationService,
|
||||
makeFakeNotificationService,
|
||||
makeFakeRPCService,
|
||||
} from "../../helpers/mock_services";
|
||||
import { getFixture, makeDeferred, mount, nextTick, patchWithCleanup } from "../../helpers/utils";
|
||||
|
||||
import { Component, xml, onError, OwlError, onWillStart } from "@odoo/owl";
|
||||
const errorDialogRegistry = registry.category("error_dialogs");
|
||||
const errorHandlerRegistry = registry.category("error_handlers");
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let errorCb;
|
||||
let unhandledRejectionCb;
|
||||
|
||||
QUnit.module("Error Service", {
|
||||
async beforeEach() {
|
||||
serviceRegistry.add("error", errorService);
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
serviceRegistry.add("notification", notificationService);
|
||||
serviceRegistry.add("rpc", makeFakeRPCService());
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
serviceRegistry.add("ui", uiService);
|
||||
const windowAddEventListener = browser.addEventListener;
|
||||
browser.addEventListener = (type, cb) => {
|
||||
if (type === "unhandledrejection") {
|
||||
unhandledRejectionCb = (ev) => {
|
||||
ev.preventDefault();
|
||||
cb(ev);
|
||||
};
|
||||
}
|
||||
if (type === "error") {
|
||||
errorCb = (ev) => {
|
||||
ev.preventDefault();
|
||||
cb(ev);
|
||||
};
|
||||
}
|
||||
};
|
||||
registerCleanup(() => {
|
||||
browser.addEventListener = windowAddEventListener;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("can handle rejected promise errors with a string as reason", async (assert) => {
|
||||
assert.expect(1);
|
||||
|
||||
errorHandlerRegistry.add(
|
||||
"__test_handler__",
|
||||
(env, err, originalError) => {
|
||||
assert.strictEqual(originalError, "-- something went wrong --");
|
||||
},
|
||||
{ sequence: 0 }
|
||||
);
|
||||
await makeTestEnv();
|
||||
const errorEvent = new PromiseRejectionEvent("error", {
|
||||
reason: "-- something went wrong --",
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
unhandledRejectionCb(errorEvent);
|
||||
});
|
||||
|
||||
QUnit.test("handle RPC_ERROR of type='server' and no associated dialog class", async (assert) => {
|
||||
assert.expect(4);
|
||||
const error = new RPCError();
|
||||
error.code = 701;
|
||||
error.message = "Some strange error occured";
|
||||
error.data = { debug: "somewhere" };
|
||||
error.subType = "strange_error";
|
||||
function addDialog(dialogClass, props) {
|
||||
assert.strictEqual(dialogClass, RPCErrorDialog);
|
||||
assert.deepEqual(_.omit(props, "traceback"), {
|
||||
name: "RPC_ERROR",
|
||||
type: "server",
|
||||
code: 701,
|
||||
data: {
|
||||
debug: "somewhere",
|
||||
},
|
||||
subType: "strange_error",
|
||||
message: "Some strange error occured",
|
||||
exceptionName: null,
|
||||
});
|
||||
assert.ok(props.traceback.indexOf("RPC_ERROR")>= 0);
|
||||
assert.ok(props.traceback.indexOf("Some strange error occured")>= 0);
|
||||
}
|
||||
serviceRegistry.add("dialog", makeFakeDialogService(addDialog), { force: true });
|
||||
await makeTestEnv();
|
||||
const errorEvent = new PromiseRejectionEvent("error", {
|
||||
reason: error,
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"handle custom RPC_ERROR of type='server' and associated custom dialog class",
|
||||
async (assert) => {
|
||||
assert.expect(4);
|
||||
class CustomDialog extends Component {}
|
||||
CustomDialog.template = xml`<RPCErrorDialog title="'Strange Error'"/>`;
|
||||
CustomDialog.components = { RPCErrorDialog };
|
||||
const error = new RPCError();
|
||||
error.code = 701;
|
||||
error.message = "Some strange error occured";
|
||||
const errorData = {
|
||||
context: { exception_class: "strange_error" },
|
||||
name: "strange_error",
|
||||
};
|
||||
error.data = errorData;
|
||||
function addDialog(dialogClass, props) {
|
||||
assert.strictEqual(dialogClass, CustomDialog);
|
||||
assert.deepEqual(_.omit(props, "traceback"), {
|
||||
name: "RPC_ERROR",
|
||||
type: "server",
|
||||
code: 701,
|
||||
data: errorData,
|
||||
subType: null,
|
||||
message: "Some strange error occured",
|
||||
exceptionName: null,
|
||||
});
|
||||
assert.ok(props.traceback.indexOf("RPC_ERROR")>= 0);
|
||||
assert.ok(props.traceback.indexOf("Some strange error occured")>= 0);
|
||||
}
|
||||
serviceRegistry.add("dialog", makeFakeDialogService(addDialog), { force: true });
|
||||
await makeTestEnv();
|
||||
errorDialogRegistry.add("strange_error", CustomDialog);
|
||||
const errorEvent = new PromiseRejectionEvent("error", {
|
||||
reason: error,
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"handle normal RPC_ERROR of type='server' and associated custom dialog class",
|
||||
async (assert) => {
|
||||
assert.expect(4);
|
||||
class CustomDialog extends Component {}
|
||||
CustomDialog.template = xml`<RPCErrorDialog title="'Strange Error'"/>`;
|
||||
CustomDialog.components = { RPCErrorDialog };
|
||||
class NormalDialog extends Component {}
|
||||
NormalDialog.template = xml`<RPCErrorDialog title="'Normal Error'"/>`;
|
||||
NormalDialog.components = { RPCErrorDialog };
|
||||
const error = new RPCError();
|
||||
error.code = 701;
|
||||
error.message = "A normal error occured";
|
||||
const errorData = {
|
||||
context: { exception_class: "strange_error" },
|
||||
};
|
||||
error.exceptionName = "normal_error";
|
||||
error.data = errorData;
|
||||
function addDialog(dialogClass, props) {
|
||||
assert.strictEqual(dialogClass, NormalDialog);
|
||||
assert.deepEqual(_.omit(props, "traceback"), {
|
||||
name: "RPC_ERROR",
|
||||
type: "server",
|
||||
code: 701,
|
||||
data: errorData,
|
||||
subType: null,
|
||||
message: "A normal error occured",
|
||||
exceptionName: "normal_error",
|
||||
});
|
||||
assert.ok(props.traceback.indexOf("RPC_ERROR")>= 0);
|
||||
assert.ok(props.traceback.indexOf("A normal error occured")>= 0);
|
||||
}
|
||||
serviceRegistry.add("dialog", makeFakeDialogService(addDialog), { force: true });
|
||||
await makeTestEnv();
|
||||
errorDialogRegistry.add("strange_error", CustomDialog);
|
||||
errorDialogRegistry.add("normal_error", NormalDialog);
|
||||
const errorEvent = new PromiseRejectionEvent("error", {
|
||||
reason: error,
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("handle CONNECTION_LOST_ERROR", async (assert) => {
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (callback, delay) => {
|
||||
assert.step(`set timeout (${delay === 2000 ? delay : ">2000"})`);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
const mock = (message) => {
|
||||
assert.step(`create (${message})`);
|
||||
return () => {
|
||||
assert.step(`close`);
|
||||
};
|
||||
};
|
||||
serviceRegistry.add("notification", makeFakeNotificationService(mock), {
|
||||
force: true,
|
||||
});
|
||||
const values = [false, true]; // simulate the 'back online status' after 2 'version_info' calls
|
||||
const mockRPC = async (route) => {
|
||||
if (route === "/web/webclient/version_info") {
|
||||
assert.step("version_info");
|
||||
const online = values.shift();
|
||||
if (online) {
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
}
|
||||
};
|
||||
await makeTestEnv({ mockRPC });
|
||||
const error = new ConnectionLostError();
|
||||
const errorEvent = new PromiseRejectionEvent("error", {
|
||||
reason: error,
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
await nextTick(); // wait for mocked RPCs
|
||||
assert.verifySteps([
|
||||
"create (Connection lost. Trying to reconnect...)",
|
||||
"set timeout (2000)",
|
||||
"version_info",
|
||||
"set timeout (>2000)",
|
||||
"version_info",
|
||||
"close",
|
||||
"create (Connection restored. You are back online.)",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("will let handlers from the registry handle errors first", async (assert) => {
|
||||
errorHandlerRegistry.add("__test_handler__", (env, err, originalError) => {
|
||||
assert.strictEqual(originalError, error);
|
||||
assert.strictEqual(env.someValue, 14);
|
||||
assert.step("in handler");
|
||||
});
|
||||
const testEnv = await makeTestEnv();
|
||||
testEnv.someValue = 14;
|
||||
const error = new Error();
|
||||
error.name = "boom";
|
||||
const errorEvent = new PromiseRejectionEvent("error", {
|
||||
reason: error,
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
assert.verifySteps(["in handler"]);
|
||||
});
|
||||
|
||||
QUnit.test("originalError is the root cause of the error chain", async (assert) => {
|
||||
errorHandlerRegistry.add("__test_handler__", (env, err, originalError) => {
|
||||
assert.ok(err instanceof UncaughtPromiseError); // Wrapped by error service
|
||||
assert.ok(err.cause instanceof OwlError); // Wrapped by owl
|
||||
assert.strictEqual(err.cause.cause, originalError); // original error
|
||||
assert.step("in handler");
|
||||
});
|
||||
const testEnv = await makeTestEnv();
|
||||
testEnv.someValue = 14;
|
||||
const error = new Error();
|
||||
error.name = "boom";
|
||||
|
||||
class ErrHandler extends Component {
|
||||
setup() {
|
||||
onError(async (err) => {
|
||||
await unhandledRejectionCb(
|
||||
new PromiseRejectionEvent("error", {
|
||||
reason: err,
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
})
|
||||
);
|
||||
prom.resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
ErrHandler.template = xml`<t t-component="props.comp"/>`;
|
||||
class ThrowInSetup extends Component {
|
||||
setup() {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
ThrowInSetup.template = xml``;
|
||||
let prom = makeDeferred();
|
||||
mount(ErrHandler, getFixture(), { props: { comp: ThrowInSetup } });
|
||||
await prom;
|
||||
assert.verifySteps(["in handler"]);
|
||||
|
||||
class ThrowInWillStart extends Component {
|
||||
setup() {
|
||||
onWillStart(() => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
ThrowInWillStart.template = xml``;
|
||||
prom = makeDeferred();
|
||||
mount(ErrHandler, getFixture(), { props: { comp: ThrowInWillStart } });
|
||||
await prom;
|
||||
assert.verifySteps(["in handler"]);
|
||||
});
|
||||
|
||||
QUnit.test("handle uncaught promise errors", async (assert) => {
|
||||
class TestError extends Error {}
|
||||
const error = new TestError();
|
||||
error.message = "This is an error test";
|
||||
error.name = "TestError";
|
||||
|
||||
function addDialog(dialogClass, props) {
|
||||
assert.strictEqual(dialogClass, ClientErrorDialog);
|
||||
assert.deepEqual(_.omit(props, "traceback"), {
|
||||
name: "UncaughtPromiseError > TestError",
|
||||
message: "Uncaught Promise > This is an error test",
|
||||
});
|
||||
assert.ok(props.traceback.indexOf("TestError")>= 0);
|
||||
assert.ok(props.traceback.indexOf("This is an error test")>= 0);
|
||||
}
|
||||
serviceRegistry.add("dialog", makeFakeDialogService(addDialog), { force: true });
|
||||
await makeTestEnv();
|
||||
|
||||
const errorEvent = new PromiseRejectionEvent("error", {
|
||||
reason: error,
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
});
|
||||
|
||||
QUnit.test("handle uncaught client errors", async (assert) => {
|
||||
class TestError extends Error {}
|
||||
const error = new TestError();
|
||||
error.message = "This is an error test";
|
||||
error.name = "TestError";
|
||||
|
||||
function addDialog(dialogClass, props) {
|
||||
assert.strictEqual(dialogClass, ClientErrorDialog);
|
||||
assert.strictEqual(props.name, "UncaughtClientError > TestError");
|
||||
assert.strictEqual(props.message, "Uncaught Javascript Error > This is an error test");
|
||||
}
|
||||
serviceRegistry.add("dialog", makeFakeDialogService(addDialog), { force: true });
|
||||
await makeTestEnv();
|
||||
|
||||
const errorEvent = new ErrorEvent("error", {
|
||||
error,
|
||||
colno: 1,
|
||||
lineno: 1,
|
||||
filename: "test",
|
||||
cancelable: true,
|
||||
});
|
||||
await errorCb(errorEvent);
|
||||
});
|
||||
|
||||
QUnit.test("don't show dialog for errors in third-party scripts", async (assert) => {
|
||||
class TestError extends Error {}
|
||||
const error = new TestError();
|
||||
error.message = "Script error.";
|
||||
error.name = "Script error.";
|
||||
|
||||
function addDialog(_dialogClass, props) {
|
||||
assert.step(props.message);
|
||||
}
|
||||
serviceRegistry.add("dialog", makeFakeDialogService(addDialog), { force: true });
|
||||
await makeTestEnv();
|
||||
|
||||
// Error events from errors in third-party scripts hav no colno, no lineno and no filename
|
||||
// because of CORS.
|
||||
const errorEvent = new ErrorEvent("error", { error, cancelable: true });
|
||||
await errorCb(errorEvent);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("show dialog for errors in third-party scripts in debug mode", async (assert) => {
|
||||
class TestError extends Error {}
|
||||
const error = new TestError();
|
||||
error.message = "Script error.";
|
||||
error.name = "Script error.";
|
||||
patchWithCleanup(odoo, { debug: true });
|
||||
|
||||
function addDialog(_dialogClass, props) {
|
||||
assert.step(props.message);
|
||||
}
|
||||
serviceRegistry.add("dialog", makeFakeDialogService(addDialog), { force: true });
|
||||
await makeTestEnv();
|
||||
|
||||
// Error events from errors in third-party scripts hav no colno, no lineno and no filename
|
||||
// because of CORS.
|
||||
const errorEvent = new ErrorEvent("error", { error, cancelable: true });
|
||||
await errorCb(errorEvent);
|
||||
assert.verifySteps(["Uncaught CORS Error"]);
|
||||
});
|
||||
|
||||
QUnit.test("check retry", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
errorHandlerRegistry.add("__test_handler__", () => {
|
||||
assert.step("dispatched");
|
||||
});
|
||||
|
||||
const def = makeDeferred();
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout(fn) {
|
||||
def.then(fn);
|
||||
},
|
||||
});
|
||||
|
||||
serviceRegistry.remove("dialog");
|
||||
await makeTestEnv();
|
||||
|
||||
class TestError extends Error {}
|
||||
const error = new TestError();
|
||||
error.message = "This is an error test";
|
||||
error.name = "TestError";
|
||||
|
||||
const errorEvent = new PromiseRejectionEvent("error", {
|
||||
reason: error,
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
await nextTick();
|
||||
|
||||
await def.resolve();
|
||||
assert.verifySteps(["dispatched"]);
|
||||
});
|
||||
|
||||
QUnit.test("lazy loaded handlers", async (assert) => {
|
||||
await makeTestEnv();
|
||||
const errorEvent = new PromiseRejectionEvent("error", {
|
||||
reason: new Error(),
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
assert.verifySteps([]);
|
||||
|
||||
errorHandlerRegistry.add("__test_handler__", () => {
|
||||
assert.step("in handler");
|
||||
});
|
||||
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
assert.verifySteps(["in handler"]);
|
||||
});
|
||||
|
||||
// The following test(s) do not want the preventDefault to be done automatically.
|
||||
QUnit.module("Error Service", {
|
||||
beforeEach() {
|
||||
serviceRegistry.add("error", errorService);
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
serviceRegistry.add("notification", notificationService);
|
||||
serviceRegistry.add("rpc", makeFakeRPCService());
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
serviceRegistry.add("ui", uiService);
|
||||
const windowAddEventListener = browser.addEventListener;
|
||||
browser.addEventListener = (type, cb) => {
|
||||
if (type === "unhandledrejection") {
|
||||
unhandledRejectionCb = cb;
|
||||
} else if (type === "error") {
|
||||
errorCb = cb;
|
||||
}
|
||||
};
|
||||
registerCleanup(() => {
|
||||
browser.addEventListener = windowAddEventListener;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("logs the traceback of the full error chain for unhandledrejection", async (assert) => {
|
||||
assert.expect(2);
|
||||
const regexParts = [
|
||||
/^.*This is a wrapper error/,
|
||||
/Caused by:.*This is a second wrapper error/,
|
||||
/Caused by:.*This is the original error/,
|
||||
];
|
||||
const errorRegex = new RegExp(regexParts.map((re) => re.source).join(/[\s\S]*/.source));
|
||||
patchWithCleanup(console, {
|
||||
error(errorMessage) {
|
||||
assert.ok(errorRegex.test(errorMessage));
|
||||
},
|
||||
});
|
||||
|
||||
const error = new Error("This is a wrapper error");
|
||||
error.cause = new Error("This is a second wrapper error");
|
||||
error.cause.cause = new Error("This is the original error");
|
||||
|
||||
// start the services
|
||||
await makeTestEnv();
|
||||
const errorEvent = new PromiseRejectionEvent("unhandledrejection", {
|
||||
reason: error,
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
assert.ok(errorEvent.defaultPrevented);
|
||||
});
|
||||
|
||||
QUnit.test("logs the traceback of the full error chain for uncaughterror", async (assert) => {
|
||||
assert.expect(2);
|
||||
const regexParts = [
|
||||
/^.*This is a wrapper error/,
|
||||
/Caused by:.*This is a second wrapper error/,
|
||||
/Caused by:.*This is the original error/,
|
||||
];
|
||||
const errorRegex = new RegExp(regexParts.map((re) => re.source).join(/[\s\S]*/.source));
|
||||
patchWithCleanup(console, {
|
||||
error(errorMessage) {
|
||||
assert.ok(errorRegex.test(errorMessage));
|
||||
},
|
||||
});
|
||||
|
||||
const error = new Error("This is a wrapper error");
|
||||
error.cause = new Error("This is a second wrapper error");
|
||||
error.cause.cause = new Error("This is the original error");
|
||||
|
||||
// start the services
|
||||
await makeTestEnv();
|
||||
const errorEvent = new Event("error", {
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
errorEvent.error = error;
|
||||
errorEvent.filename = "dummy_file.js"; // needed to not be treated as a CORS error
|
||||
await errorCb(errorEvent);
|
||||
assert.ok(errorEvent.defaultPrevented);
|
||||
});
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import {
|
||||
editInput,
|
||||
getFixture,
|
||||
mount,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { FileInput } from "@web/core/file_input/file_input";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { session } from "@web/session";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
async function createFileInput({ mockPost, mockAdd, props }) {
|
||||
serviceRegistry.add("notification", {
|
||||
start: () => ({
|
||||
add: mockAdd || (() => {}),
|
||||
}),
|
||||
});
|
||||
serviceRegistry.add("http", {
|
||||
start: () => ({
|
||||
post: mockPost || (() => {}),
|
||||
}),
|
||||
});
|
||||
const env = await makeTestEnv();
|
||||
await mount(FileInput, target, { env, props });
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Tests
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
QUnit.module("Components", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
patchWithCleanup(odoo, { csrf_token: "dummy" });
|
||||
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
// This module cannot be tested as thoroughly as we want it to be:
|
||||
// browsers do not let scripts programmatically assign values to inputs
|
||||
// of type file
|
||||
QUnit.module("FileInput");
|
||||
|
||||
QUnit.test("Upload a file: default props", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
await createFileInput({
|
||||
mockPost: (route, params) => {
|
||||
assert.deepEqual(params, {
|
||||
csrf_token: "dummy",
|
||||
ufile: [],
|
||||
});
|
||||
assert.step(route);
|
||||
return "[]";
|
||||
},
|
||||
props: {},
|
||||
});
|
||||
const input = target.querySelector(".o_file_input input");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_file_input").innerText.trim().toUpperCase(),
|
||||
"CHOOSE FILE",
|
||||
"File input total text should match its given inner element's text"
|
||||
);
|
||||
assert.strictEqual(input.accept, "*", "Input should accept all files by default");
|
||||
|
||||
await triggerEvent(input, null, "change", {}, { skipVisibilityCheck: true });
|
||||
|
||||
assert.notOk(input.multiple, "'multiple' attribute should not be set");
|
||||
assert.verifySteps(["/web/binary/upload_attachment"]);
|
||||
});
|
||||
|
||||
QUnit.test("Upload a file: custom attachment", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
await createFileInput({
|
||||
props: {
|
||||
acceptedFileExtensions: ".png",
|
||||
multiUpload: true,
|
||||
resId: 5,
|
||||
resModel: "res.model",
|
||||
route: "/web/binary/upload",
|
||||
onUpload(files) {
|
||||
assert.strictEqual(
|
||||
files.length,
|
||||
0,
|
||||
"'files' property should be an empty array"
|
||||
);
|
||||
},
|
||||
},
|
||||
mockPost: (route, params) => {
|
||||
assert.deepEqual(params, {
|
||||
id: 5,
|
||||
model: "res.model",
|
||||
csrf_token: "dummy",
|
||||
ufile: [],
|
||||
});
|
||||
assert.step(route);
|
||||
return "[]";
|
||||
},
|
||||
});
|
||||
const input = target.querySelector(".o_file_input input");
|
||||
|
||||
assert.strictEqual(input.accept, ".png", "Input should now only accept pngs");
|
||||
|
||||
await triggerEvent(input, null, "change", {}, { skipVisibilityCheck: true });
|
||||
|
||||
assert.ok(input.multiple, "'multiple' attribute should be set");
|
||||
assert.verifySteps(["/web/binary/upload"]);
|
||||
});
|
||||
|
||||
QUnit.test("Hidden file input", async (assert) => {
|
||||
assert.expect(1);
|
||||
|
||||
await createFileInput({
|
||||
props: { hidden: true },
|
||||
});
|
||||
|
||||
assert.isNotVisible(target.querySelector(".o_file_input"));
|
||||
});
|
||||
|
||||
QUnit.test("uploading a file that is too heavy will send a notification", async (assert) => {
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
patchWithCleanup(session, { max_file_upload_size: 2 });
|
||||
await createFileInput({
|
||||
props: {
|
||||
onUpload(files) {
|
||||
// This code should be unreachable in this case
|
||||
assert.step(files[0].name);
|
||||
},
|
||||
},
|
||||
mockPost: (route, params) => {
|
||||
return JSON.stringify([{ name: params.ufile[0].name }]);
|
||||
},
|
||||
mockAdd: (message) => {
|
||||
assert.step("notification");
|
||||
// Message is a bit weird because values (2 and 4 bytes) are simplified to 2 decimals in regards to megabytes
|
||||
assert.strictEqual(
|
||||
message,
|
||||
"The selected file (4B) is over the maximum allowed file size (2B)."
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await editInput(target, ".o_file_input input", file);
|
||||
assert.verifySteps(
|
||||
["notification"],
|
||||
"Only the notification will be triggered and the file won't be uploaded."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { fileUploadService } from "@web/core/file_upload/file_upload_service";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { FileUploadProgressContainer } from "@web/core/file_upload/file_upload_progress_container";
|
||||
import { FileUploadProgressRecord } from "@web/core/file_upload/file_upload_progress_record";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { click, getFixture, mount, nextTick, patchWithCleanup } from "../helpers/utils";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
class FileUploadProgressTestRecord extends FileUploadProgressRecord {}
|
||||
FileUploadProgressTestRecord.template = xml`
|
||||
<t t-set="progressTexts" t-value="getProgressTexts()"/>
|
||||
<div class="file_upload">
|
||||
<div class="file_upload_progress_text_left" t-esc="progressTexts.left"/>
|
||||
<div class="file_upload_progress_text_right" t-esc="progressTexts.right"/>
|
||||
<FileUploadProgressBar fileUpload="props.fileUpload"/>
|
||||
</div>
|
||||
`;
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.fileUploadService = useService("file_upload");
|
||||
this.FileUploadProgressTestRecord = FileUploadProgressTestRecord;
|
||||
}
|
||||
}
|
||||
Parent.components = {
|
||||
FileUploadProgressContainer,
|
||||
};
|
||||
Parent.template = xml`
|
||||
<div class="parent">
|
||||
<FileUploadProgressContainer fileUploads="fileUploadService.uploads" shouldDisplay="props.shouldDisplay" Component="FileUploadProgressTestRecord"/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let env;
|
||||
let target;
|
||||
let patchUpload;
|
||||
|
||||
QUnit.module("Components", ({ beforeEach }) => {
|
||||
beforeEach(async () => {
|
||||
serviceRegistry.add("file_upload", fileUploadService);
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
patchUpload = (customSend) => {
|
||||
patchWithCleanup(fileUploadService, {
|
||||
createXhr() {
|
||||
const xhr = new window.EventTarget();
|
||||
Object.assign(xhr, {
|
||||
upload: new window.EventTarget(),
|
||||
open() {},
|
||||
send(data) { customSend && customSend(data); },
|
||||
});
|
||||
return xhr;
|
||||
},
|
||||
});
|
||||
};
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("FileUploadProgressContainer");
|
||||
|
||||
QUnit.test("can be rendered", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".parent");
|
||||
assert.containsNone(target, ".file_upload");
|
||||
});
|
||||
|
||||
QUnit.test("upload renders new component(s)", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
patchUpload();
|
||||
const fileUploadService = env.services.file_upload;
|
||||
fileUploadService.upload("/test/", []);
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".file_upload");
|
||||
fileUploadService.upload("/test/", []);
|
||||
await nextTick();
|
||||
assert.containsN(target, ".file_upload", 2);
|
||||
});
|
||||
|
||||
QUnit.test("upload end removes component", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
patchUpload();
|
||||
const fileUploadService = env.services.file_upload;
|
||||
fileUploadService.upload("/test/", []);
|
||||
await nextTick();
|
||||
fileUploadService.uploads[1].xhr.dispatchEvent(new Event("load"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".file_upload");
|
||||
});
|
||||
|
||||
QUnit.test("upload error removes component", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
patchUpload();
|
||||
const fileUploadService = env.services.file_upload;
|
||||
fileUploadService.upload("/test/", []);
|
||||
await nextTick();
|
||||
fileUploadService.uploads[1].xhr.dispatchEvent(new Event("error"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".file_upload");
|
||||
});
|
||||
|
||||
QUnit.test("upload abort removes component", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
patchUpload();
|
||||
const fileUploadService = env.services.file_upload;
|
||||
fileUploadService.upload("/test/", []);
|
||||
await nextTick();
|
||||
fileUploadService.uploads[1].xhr.dispatchEvent(new Event("abort"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".file_upload");
|
||||
});
|
||||
|
||||
QUnit.test("upload can be aborted by clicking on cross", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
patchUpload();
|
||||
const fileUploadService = env.services.file_upload;
|
||||
fileUploadService.upload("/test/", []);
|
||||
await nextTick();
|
||||
patchWithCleanup(env.services.dialog, {
|
||||
add: () => {
|
||||
fileUploadService.uploads[1].xhr.dispatchEvent(new Event("abort"));
|
||||
},
|
||||
});
|
||||
await click(target, ".o-file-upload-progress-bar-abort", true);
|
||||
assert.containsNone(target, ".file_upload");
|
||||
});
|
||||
|
||||
QUnit.test("upload updates on progress", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
patchUpload();
|
||||
const fileUploadService = env.services.file_upload;
|
||||
fileUploadService.upload("/test/", []);
|
||||
await nextTick();
|
||||
|
||||
const progressEvent = new Event("progress", { bubbles: true });
|
||||
progressEvent.loaded = 250000000;
|
||||
progressEvent.total = 500000000;
|
||||
fileUploadService.uploads[1].xhr.upload.dispatchEvent(progressEvent);
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".file_upload_progress_text_left").textContent, "Uploading... (50%)");
|
||||
progressEvent.loaded = 350000000;
|
||||
fileUploadService.uploads[1].xhr.upload.dispatchEvent(progressEvent);
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".file_upload_progress_text_right").textContent, "(350/500MB)");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
1160
odoo-bringout-oca-ocb-web/web/static/tests/core/l10n/dates_tests.js
Normal file
1160
odoo-bringout-oca-ocb-web/web/static/tests/core/l10n/dates_tests.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,217 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { getFixture, mount, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { localizationService } from "@web/core/l10n/localization_service";
|
||||
import { translatedTerms, _lt } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { patch, unpatch } from "@web/core/utils/patch";
|
||||
import { session } from "@web/session";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
const { DateTime, Settings } = luxon;
|
||||
|
||||
const terms = { Hello: "Bonjour" };
|
||||
const serviceRegistry = registry.category("services");
|
||||
class TestComponent extends Component {}
|
||||
|
||||
/**
|
||||
* Patches the 'lang' of the user session and context.
|
||||
*
|
||||
* @param {string} lang
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function patchLang(lang) {
|
||||
const { defaultLocale, defaultNumberingSystem } = Settings;
|
||||
registerCleanup(() => {
|
||||
Settings.defaultLocale = defaultLocale;
|
||||
Settings.defaultNumberingSystem = defaultNumberingSystem;
|
||||
});
|
||||
patchWithCleanup(session.user_context, { lang });
|
||||
patchWithCleanup(browser, {
|
||||
fetch: async () => ({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
modules: {},
|
||||
lang_parameters: {
|
||||
direction: "ltr",
|
||||
date_format: "%d/%m/%Y",
|
||||
time_format: "%H:%M:%S",
|
||||
grouping: "[3,0]",
|
||||
decimal_point: ",",
|
||||
thousands_sep: ".",
|
||||
week_start: 1,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
serviceRegistry.add("localization", localizationService);
|
||||
await makeTestEnv();
|
||||
}
|
||||
|
||||
QUnit.module("Translations");
|
||||
|
||||
QUnit.test("can translate a text node", async (assert) => {
|
||||
assert.expect(1);
|
||||
TestComponent.template = xml`<div>Hello</div>`;
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
const env = await makeTestEnv();
|
||||
patch(translatedTerms, "add translations", terms);
|
||||
const target = getFixture();
|
||||
await mount(TestComponent, target, { env });
|
||||
assert.strictEqual(target.innerText, "Bonjour");
|
||||
unpatch(translatedTerms, "add translations");
|
||||
});
|
||||
|
||||
QUnit.test("can lazy translate", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
TestComponent.template = xml`<div><t t-esc="constructor.someLazyText" /></div>`;
|
||||
TestComponent.someLazyText = _lt("Hello");
|
||||
assert.strictEqual(TestComponent.someLazyText.toString(), "Hello");
|
||||
assert.strictEqual(TestComponent.someLazyText.valueOf(), "Hello");
|
||||
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
const env = await makeTestEnv();
|
||||
patch(translatedTerms, "add translations", terms);
|
||||
const target = getFixture();
|
||||
await mount(TestComponent, target, { env });
|
||||
assert.strictEqual(target.innerText, "Bonjour");
|
||||
unpatch(translatedTerms, "add translations");
|
||||
});
|
||||
|
||||
QUnit.test("_t is in env", async (assert) => {
|
||||
assert.expect(1);
|
||||
TestComponent.template = xml`<div><t t-esc="env._t('Hello')"/></div>`;
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
const env = await makeTestEnv();
|
||||
patch(translatedTerms, "add translations", terms);
|
||||
const target = getFixture();
|
||||
await mount(TestComponent, target, { env });
|
||||
assert.strictEqual(target.innerText, "Bonjour");
|
||||
unpatch(translatedTerms, "add translations");
|
||||
});
|
||||
|
||||
QUnit.test("luxon is configured in the correct lang", async (assert) => {
|
||||
await patchLang("fr_BE");
|
||||
assert.strictEqual(DateTime.utc(2021, 12, 10).toFormat("MMMM"), "décembre");
|
||||
});
|
||||
|
||||
QUnit.test("Mismatched locale sr_RS is correctly converted", async (assert) => {
|
||||
patchLang("sr_RS");
|
||||
await makeTestEnv();
|
||||
assert.strictEqual(DateTime.local().loc.locale, "sr-cyrl");
|
||||
});
|
||||
|
||||
QUnit.test("Mismatched locale sr@latin is correctly converted", async (assert) => {
|
||||
patchLang("sr@latin");
|
||||
await makeTestEnv();
|
||||
assert.strictEqual(DateTime.local().loc.locale, "sr-Latn-RS");
|
||||
});
|
||||
|
||||
QUnit.test("lang is given by an attribute on the DOM root node", async (assert) => {
|
||||
assert.expect(1);
|
||||
patchWithCleanup(session.user_context, { lang: null });
|
||||
document.documentElement.setAttribute("lang", "fr-FR");
|
||||
registerCleanup(() => {
|
||||
document.documentElement.removeAttribute("lang");
|
||||
});
|
||||
patchWithCleanup(session, {
|
||||
cache_hashes: { translations: 1 },
|
||||
})
|
||||
serviceRegistry.add("localization", localizationService);
|
||||
await makeTestEnv({
|
||||
mockRPC(route, params) {
|
||||
assert.strictEqual(route, "/web/webclient/translations/1?lang=fr_FR");
|
||||
return {
|
||||
modules: {},
|
||||
lang_parameters: {
|
||||
direction: "ltr",
|
||||
date_format: "%d/%m/%Y",
|
||||
time_format: "%H:%M:%S",
|
||||
grouping: "[3,0]",
|
||||
decimal_point: ",",
|
||||
thousands_sep: ".",
|
||||
week_start: 1,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("Numbering system");
|
||||
|
||||
QUnit.test("arabic has the correct numbering system (generic)", async (assert) => {
|
||||
await patchLang("ar_001");
|
||||
assert.strictEqual(
|
||||
DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss"),
|
||||
"١٠/١٢/٢٠٢١ ١٢:٠٠:٠٠"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("arabic has the correct numbering system (Algeria)", async (assert) => {
|
||||
await patchLang("ar_DZ");
|
||||
assert.strictEqual(
|
||||
DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss"),
|
||||
"10/12/2021 12:00:00"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("arabic has the correct numbering system (Lybia)", async (assert) => {
|
||||
await patchLang("ar_LY");
|
||||
assert.strictEqual(
|
||||
DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss"),
|
||||
"10/12/2021 12:00:00"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("arabic has the correct numbering system (Morocco)", async (assert) => {
|
||||
await patchLang("ar_MA");
|
||||
assert.strictEqual(
|
||||
DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss"),
|
||||
"10/12/2021 12:00:00"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("arabic has the correct numbering system (Saudi Arabia)", async (assert) => {
|
||||
await patchLang("ar_SA");
|
||||
assert.strictEqual(
|
||||
DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss"),
|
||||
"١٠/١٢/٢٠٢١ ١٢:٠٠:٠٠"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("arabic has the correct numbering system (Tunisia)", async (assert) => {
|
||||
await patchLang("ar_TN");
|
||||
assert.strictEqual(
|
||||
DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss"),
|
||||
"10/12/2021 12:00:00"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("bengalese has the correct numbering system", async (assert) => {
|
||||
await patchLang("bn");
|
||||
assert.strictEqual(
|
||||
DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss"),
|
||||
"১০/১২/২০২১ ১২:০০:০০"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("punjabi (gurmukhi) has the correct numbering system", async (assert) => {
|
||||
await patchLang("pa_in");
|
||||
assert.strictEqual(
|
||||
DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss"),
|
||||
"੧੦/੧੨/੨੦੨੧ ੧੨:੦੦:੦੦"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("tamil has the correct numbering system", async (assert) => {
|
||||
await patchLang("ta");
|
||||
assert.strictEqual(
|
||||
DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss"),
|
||||
"௧௦/௧௨/௨௦௨௧ ௧௨:௦௦:௦௦"
|
||||
);
|
||||
});
|
||||
228
odoo-bringout-oca-ocb-web/web/static/tests/core/macro_tests.js
Normal file
228
odoo-bringout-oca-ocb-web/web/static/tests/core/macro_tests.js
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { MacroEngine } from "@web/core/macro";
|
||||
import { getFixture, mockTimeout } from "../helpers/utils";
|
||||
|
||||
import { Component, xml, useState, mount } from "@odoo/owl";
|
||||
|
||||
let target, engine, mock;
|
||||
|
||||
QUnit.module(
|
||||
"macros",
|
||||
{
|
||||
beforeEach() {
|
||||
target = getFixture();
|
||||
engine = new MacroEngine(target);
|
||||
mock = mockTimeout();
|
||||
},
|
||||
afterEach() {
|
||||
if (engine.macros.size !== 0) {
|
||||
throw new Error("Some macro is still running after a test");
|
||||
}
|
||||
},
|
||||
},
|
||||
() => {
|
||||
class TestComponent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
}
|
||||
TestComponent.template = xml`
|
||||
<div class="counter">
|
||||
<button class="inc" t-on-click="() => this.state.value++">increment</button>
|
||||
<button class="dec" t-on-click="() => this.state.value--">decrement</button>
|
||||
<button class="double" t-on-click="() => this.state.value = 2*this.state.value">double</button>
|
||||
<span class="value"><t t-esc="state.value"/></span>
|
||||
<input />
|
||||
</div>`;
|
||||
|
||||
QUnit.test("simple use", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
|
||||
const span = target.querySelector("span.value");
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
|
||||
engine.activate({
|
||||
name: "test",
|
||||
steps: [
|
||||
{
|
||||
trigger: "button.inc",
|
||||
action: "click",
|
||||
},
|
||||
],
|
||||
});
|
||||
// default interval is 500
|
||||
await mock.advanceTime(300);
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
await mock.advanceTime(300);
|
||||
assert.strictEqual(span.textContent, "1");
|
||||
});
|
||||
|
||||
QUnit.test("multiple steps", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
|
||||
const span = target.querySelector("span.value");
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
|
||||
engine.activate({
|
||||
name: "test",
|
||||
steps: [
|
||||
{
|
||||
trigger: "button.inc",
|
||||
action: "click",
|
||||
},
|
||||
{
|
||||
trigger: () => {
|
||||
return span.textContent === "1" ? span : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
trigger: "button.inc",
|
||||
action: "click",
|
||||
},
|
||||
],
|
||||
});
|
||||
await mock.advanceTime(500);
|
||||
assert.strictEqual(span.textContent, "1");
|
||||
await mock.advanceTime(500);
|
||||
assert.strictEqual(span.textContent, "2");
|
||||
await mock.advanceTime(500);
|
||||
assert.strictEqual(span.textContent, "2");
|
||||
});
|
||||
|
||||
QUnit.test("can use a function as action", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
let flag = false;
|
||||
engine.activate({
|
||||
name: "test",
|
||||
steps: [
|
||||
{
|
||||
trigger: "button.inc",
|
||||
action: () => (flag = true),
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.strictEqual(flag, false);
|
||||
await mock.advanceTime(600);
|
||||
assert.strictEqual(flag, true);
|
||||
});
|
||||
|
||||
QUnit.test("can input values", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
const input = target.querySelector("input");
|
||||
|
||||
engine.activate({
|
||||
name: "test",
|
||||
steps: [
|
||||
{
|
||||
trigger: "div.counter input",
|
||||
action: "text",
|
||||
value: "aaron",
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.strictEqual(input.value, "");
|
||||
await mock.advanceTime(600);
|
||||
assert.strictEqual(input.value, "aaron");
|
||||
});
|
||||
|
||||
QUnit.test("a step can have no trigger", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
const input = target.querySelector("input");
|
||||
|
||||
engine.activate({
|
||||
name: "test",
|
||||
steps: [
|
||||
{ action: () => assert.step("1") },
|
||||
{ action: () => assert.step("2") },
|
||||
{
|
||||
trigger: "div.counter input",
|
||||
action: "text",
|
||||
value: "aaron",
|
||||
},
|
||||
{ action: () => assert.step("3") },
|
||||
],
|
||||
});
|
||||
assert.strictEqual(input.value, "");
|
||||
await mock.advanceTime(600);
|
||||
assert.strictEqual(input.value, "aaron");
|
||||
assert.verifySteps(["1", "2", "3"]);
|
||||
});
|
||||
|
||||
QUnit.test("onStep function is called at each step", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
|
||||
const span = target.querySelector("span.value");
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
|
||||
engine.activate({
|
||||
name: "test",
|
||||
onStep: (el, step) => {
|
||||
assert.step(step.info);
|
||||
},
|
||||
steps: [
|
||||
{ info: "1" },
|
||||
{
|
||||
info: "2",
|
||||
trigger: "button.inc",
|
||||
action: "click",
|
||||
},
|
||||
],
|
||||
});
|
||||
// default interval is 500
|
||||
await mock.advanceTime(600);
|
||||
assert.strictEqual(span.textContent, "1");
|
||||
assert.verifySteps(["1", "2"]);
|
||||
});
|
||||
|
||||
QUnit.test("trigger can be a function returning an htmlelement", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
|
||||
const span = target.querySelector("span.value");
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
|
||||
engine.activate({
|
||||
name: "test",
|
||||
steps: [
|
||||
{
|
||||
trigger: () => target.querySelector("button.inc"),
|
||||
action: "click",
|
||||
},
|
||||
],
|
||||
});
|
||||
// default interval is 500
|
||||
await mock.advanceTime(300);
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
await mock.advanceTime(300);
|
||||
assert.strictEqual(span.textContent, "1");
|
||||
});
|
||||
|
||||
QUnit.test("macro does not click on invisible element", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
|
||||
const span = target.querySelector("span.value");
|
||||
const button = target.querySelector("button.inc");
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
|
||||
engine.activate({
|
||||
name: "test",
|
||||
steps: [
|
||||
{
|
||||
trigger: "button.inc",
|
||||
action: "click",
|
||||
},
|
||||
],
|
||||
});
|
||||
button.classList.add("d-none");
|
||||
|
||||
await mock.advanceTime(500);
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
await mock.advanceTime(500);
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
button.classList.remove("d-none");
|
||||
await mock.advanceTime(500);
|
||||
|
||||
assert.strictEqual(span.textContent, "1");
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
/** @odoo-module **/
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "../helpers/mock_env";
|
||||
import { patch, unpatch } from "@web/core/utils/patch";
|
||||
import { getFixture, mount, nextTick } from "../helpers/utils";
|
||||
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
const mainComponentsRegistry = registry.category("main_components");
|
||||
|
||||
let target;
|
||||
|
||||
QUnit.module("Components", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
target = getFixture();
|
||||
clearRegistryWithCleanup(mainComponentsRegistry);
|
||||
});
|
||||
|
||||
QUnit.module("MainComponentsContainer");
|
||||
|
||||
QUnit.test("simple rendering", async function (assert) {
|
||||
const env = await makeTestEnv();
|
||||
|
||||
class MainComponentA extends Component {}
|
||||
MainComponentA.template = xml`<span>MainComponentA</span>`;
|
||||
|
||||
class MainComponentB extends Component {}
|
||||
MainComponentB.template = xml`<span>MainComponentB</span>`;
|
||||
|
||||
mainComponentsRegistry.add("MainComponentA", { Component: MainComponentA, props: {} });
|
||||
mainComponentsRegistry.add("MainComponentB", { Component: MainComponentB, props: {} });
|
||||
await mount(MainComponentsContainer, target, { env, props: {} });
|
||||
assert.containsOnce(target, "div.o-main-components-container");
|
||||
assert.equal(
|
||||
target.querySelector(".o-main-components-container").innerHTML,
|
||||
"<span>MainComponentA</span><span>MainComponentB</span>"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("unmounts erroring main component", async function (assert) {
|
||||
const env = await makeTestEnv();
|
||||
|
||||
let compA;
|
||||
class MainComponentA extends Component {
|
||||
setup() {
|
||||
compA = this;
|
||||
this.state = useState({ shouldThrow: false });
|
||||
}
|
||||
get error() {
|
||||
throw new Error("BOOM");
|
||||
}
|
||||
}
|
||||
MainComponentA.template = xml`<span><t t-if="state.shouldThrow" t-esc="error"/>MainComponentA</span>`;
|
||||
|
||||
class MainComponentB extends Component {}
|
||||
MainComponentB.template = xml`<span>MainComponentB</span>`;
|
||||
|
||||
mainComponentsRegistry.add("MainComponentA", { Component: MainComponentA, props: {} });
|
||||
mainComponentsRegistry.add("MainComponentB", { Component: MainComponentB, props: {} });
|
||||
await mount(MainComponentsContainer, target, { env, props: {} });
|
||||
assert.containsOnce(target, "div.o-main-components-container");
|
||||
assert.equal(
|
||||
target.querySelector(".o-main-components-container").innerHTML,
|
||||
"<span>MainComponentA</span><span>MainComponentB</span>"
|
||||
);
|
||||
|
||||
const handler = (ev) => {
|
||||
assert.step(ev.reason.message);
|
||||
assert.step(ev.reason.cause.message);
|
||||
// need to preventDefault to remove error from console (so python test pass)
|
||||
ev.preventDefault();
|
||||
};
|
||||
window.addEventListener("unhandledrejection", handler);
|
||||
// fake error service so that the odoo qunit handlers don't think that they need to handle the error
|
||||
registry.category("services").add("error", { start: () => {} });
|
||||
patch(QUnit, "MainComponentsContainer QUnit patch", {
|
||||
onUnhandledRejection: () => {},
|
||||
});
|
||||
compA.state.shouldThrow = true;
|
||||
await nextTick();
|
||||
window.removeEventListener("unhandledrejection", handler);
|
||||
// unpatch QUnit asap so any other errors can be caught by it
|
||||
unpatch(QUnit, "MainComponentsContainer QUnit patch");
|
||||
assert.verifySteps([
|
||||
'An error occured in the owl lifecycle (see this Error\'s "cause" property)',
|
||||
"BOOM",
|
||||
]);
|
||||
|
||||
assert.equal(
|
||||
target.querySelector(".o-main-components-container").innerHTML,
|
||||
"<span>MainComponentB</span>"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("unmounts erroring main component: variation", async function (assert) {
|
||||
const env = await makeTestEnv();
|
||||
|
||||
class MainComponentA extends Component {}
|
||||
MainComponentA.template = xml`<span>MainComponentA</span>`;
|
||||
|
||||
let compB;
|
||||
class MainComponentB extends Component {
|
||||
setup() {
|
||||
compB = this;
|
||||
this.state = useState({ shouldThrow: false });
|
||||
}
|
||||
get error() {
|
||||
throw new Error("BOOM");
|
||||
}
|
||||
}
|
||||
MainComponentB.template = xml`<span><t t-if="state.shouldThrow" t-esc="error"/>MainComponentB</span>`;
|
||||
|
||||
mainComponentsRegistry.add("MainComponentA", { Component: MainComponentA, props: {} });
|
||||
mainComponentsRegistry.add("MainComponentB", { Component: MainComponentB, props: {} });
|
||||
await mount(MainComponentsContainer, target, { env, props: {} });
|
||||
assert.containsOnce(target, "div.o-main-components-container");
|
||||
assert.equal(
|
||||
target.querySelector(".o-main-components-container").innerHTML,
|
||||
"<span>MainComponentA</span><span>MainComponentB</span>"
|
||||
);
|
||||
|
||||
const handler = (ev) => {
|
||||
assert.step(ev.reason.message);
|
||||
assert.step(ev.reason.cause.message);
|
||||
// need to preventDefault to remove error from console (so python test pass)
|
||||
ev.preventDefault();
|
||||
};
|
||||
window.addEventListener("unhandledrejection", handler);
|
||||
// fake error service so that the odoo qunit handlers don't think that they need to handle the error
|
||||
registry.category("services").add("error", { start: () => {} });
|
||||
patch(QUnit, "MainComponentsContainer QUnit patch", {
|
||||
onUnhandledRejection: () => {},
|
||||
});
|
||||
compB.state.shouldThrow = true;
|
||||
await nextTick();
|
||||
window.removeEventListener("unhandledrejection", handler);
|
||||
// unpatch QUnit asap so any other errors can be caught by it
|
||||
unpatch(QUnit, "MainComponentsContainer QUnit patch");
|
||||
assert.verifySteps([
|
||||
'An error occured in the owl lifecycle (see this Error\'s "cause" property)',
|
||||
"BOOM",
|
||||
]);
|
||||
assert.equal(
|
||||
target.querySelector(".o-main-components-container").innerHTML,
|
||||
"<span>MainComponentA</span>"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("MainComponentsContainer re-renders when the registry changes", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
await mount(MainComponentsContainer, target, { env, props: {} });
|
||||
|
||||
assert.containsNone(target, ".myMainComponent");
|
||||
class MyMainComponent extends Component {}
|
||||
MyMainComponent.template = xml`<div class="myMainComponent" />`;
|
||||
mainComponentsRegistry.add("myMainComponent", { Component: MyMainComponent });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".myMainComponent");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,570 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { click, getFixture, triggerEvent, mount, editInput } from "../helpers/utils";
|
||||
import { makeFakeLocalizationService } from "../helpers/mock_services";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
async function mountComponent(Component, params = {}) {
|
||||
const env = await makeTestEnv({ serverData, mockRPC: params.mockRPC });
|
||||
await mount(MainComponentsContainer, target, { env });
|
||||
return mount(Component, target, { env, props: params.props || {} });
|
||||
}
|
||||
|
||||
QUnit.module("Components", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: { string: "Foo", type: "char", searchable: true },
|
||||
bar: { string: "Bar", type: "boolean", searchable: true },
|
||||
product_id: {
|
||||
string: "Product",
|
||||
type: "many2one",
|
||||
relation: "product",
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, foo: "yop", bar: true, product_id: 37 },
|
||||
{ id: 2, foo: "blip", bar: true, product_id: false },
|
||||
{ id: 4, foo: "abc", bar: false, product_id: 41 },
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: { string: "Product Name", type: "char", searchable: true },
|
||||
},
|
||||
records: [
|
||||
{ id: 37, display_name: "xphone" },
|
||||
{ id: 41, display_name: "xpad" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("popover", popoverService);
|
||||
registry.category("services").add("orm", ormService);
|
||||
registry.category("services").add("localization", makeFakeLocalizationService());
|
||||
registry.category("services").add("ui", uiService);
|
||||
registry.category("services").add("view", viewService);
|
||||
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("ModelFieldSelector");
|
||||
|
||||
QUnit.test("creating a field chain from scratch", async (assert) => {
|
||||
function getValueFromDOM(el) {
|
||||
return [...el.querySelectorAll(".o_field_selector_chain_part")]
|
||||
.map((part) => part.textContent.trim())
|
||||
.join(" -> ");
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.fieldName = "";
|
||||
}
|
||||
onUpdate(value) {
|
||||
this.fieldName = value;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
Parent.components = { ModelFieldSelector };
|
||||
Parent.template = xml`
|
||||
<ModelFieldSelector
|
||||
readonly="false"
|
||||
resModel="'partner'"
|
||||
fieldName="fieldName"
|
||||
isDebugMode="false"
|
||||
update="(value) => this.onUpdate(value)"
|
||||
/>
|
||||
`;
|
||||
|
||||
// Create the field selector and its mock environment
|
||||
const fieldSelector = await mountComponent(Parent);
|
||||
|
||||
// Focusing the field selector input should open a field selector popover
|
||||
await click(target, ".o_field_selector");
|
||||
assert.strictEqual(
|
||||
target.querySelector("input.o_input[placeholder='Search...']"),
|
||||
document.activeElement,
|
||||
"the field selector input should be focused"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_selector_popover",
|
||||
"field selector popover should be visible"
|
||||
);
|
||||
|
||||
// The field selector popover should contain the list of "partner"
|
||||
// fields. "Bar" should be among them.
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_selector_popover .o_field_selector_item").textContent,
|
||||
"Bar",
|
||||
"field selector popover should contain the 'Bar' field"
|
||||
);
|
||||
|
||||
// Clicking the "Bar" field should close the popover and set the field
|
||||
// chain to "bar" as it is a basic field
|
||||
await click(target.querySelector(".o_field_selector_popover .o_field_selector_item"));
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_selector_popover",
|
||||
"field selector popover should be closed now"
|
||||
);
|
||||
assert.strictEqual(
|
||||
getValueFromDOM(target),
|
||||
"Bar",
|
||||
"field selector value should be displayed with a 'Bar' tag"
|
||||
);
|
||||
assert.strictEqual(
|
||||
fieldSelector.fieldName,
|
||||
"bar",
|
||||
"the selected field should be correctly set"
|
||||
);
|
||||
|
||||
// Focusing the input again should open the same popover
|
||||
await click(target, ".o_field_selector");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_selector_popover",
|
||||
"field selector popover should be visible"
|
||||
);
|
||||
|
||||
// The field selector popover should contain the list of "partner"
|
||||
// fields. "Product" should be among them.
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_selector_popover .o_field_selector_relation_icon",
|
||||
"field selector popover should contain the 'Product' field"
|
||||
);
|
||||
|
||||
// Clicking on the "Product" field should update the popover to show
|
||||
// the product fields (so only "Product Name" should be there)
|
||||
await click(
|
||||
target.querySelector(".o_field_selector_popover .o_field_selector_relation_icon")
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_selector_popover .o_field_selector_item",
|
||||
"there should be only one field proposition for 'product' model"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_selector_popover .o_field_selector_item").textContent,
|
||||
"Product Name",
|
||||
"the name of the only suggestion should be 'Product Name'"
|
||||
);
|
||||
|
||||
// Clicking on "Product Name" should close the popover and set the chain
|
||||
// to "product_id.name"
|
||||
await click(target.querySelector(".o_field_selector_popover .o_field_selector_item"));
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_selector_popover",
|
||||
"field selector popover should be closed now"
|
||||
);
|
||||
assert.strictEqual(
|
||||
getValueFromDOM(target),
|
||||
"Product -> Product Name",
|
||||
"field selector value should be displayed with two tags: 'Product' and 'Product Name'"
|
||||
);
|
||||
|
||||
// Remove the current selection and recreate it again
|
||||
await click(target, ".o_field_selector");
|
||||
await click(target, ".o_field_selector_prev_page");
|
||||
await click(target, ".o_field_selector_prev_page");
|
||||
await click(target, ".o_field_selector_close");
|
||||
|
||||
await click(target, ".o_field_selector");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_selector_popover .o_field_selector_relation_icon",
|
||||
"field selector popover should contain the 'Product' field"
|
||||
);
|
||||
|
||||
await click(
|
||||
target.querySelector(".o_field_selector_popover .o_field_selector_relation_icon")
|
||||
);
|
||||
await click(target.querySelector(".o_field_selector_popover .o_field_selector_item"));
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_selector_popover",
|
||||
"field selector popover should be closed now"
|
||||
);
|
||||
assert.strictEqual(
|
||||
getValueFromDOM(target),
|
||||
"Product -> Product Name",
|
||||
"field selector value should be displayed with two tags: 'Product' and 'Product Name'"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("default field chain should set the page data correctly", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
// Create the field selector and its mock environment
|
||||
// passing 'product_id' as a prefilled field-chain
|
||||
await mountComponent(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
fieldName: "product_id",
|
||||
resModel: "partner",
|
||||
isDebugMode: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Focusing the field selector input should open a field selector popover
|
||||
await click(target, ".o_field_selector");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_selector_popover",
|
||||
"field selector popover should be visible"
|
||||
);
|
||||
|
||||
// The field selector popover should contain the list of "product"
|
||||
// fields. "Product Name" should be among them.
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_selector_popover .o_field_selector_item",
|
||||
"there should be only one field proposition for 'product' model"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_selector_popover .o_field_selector_item").textContent,
|
||||
"Product Name",
|
||||
"the name of the only suggestion should be 'Product Name'"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("use the filter option", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
// Create the field selector and its mock environment
|
||||
await mountComponent(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
fieldName: "",
|
||||
resModel: "partner",
|
||||
filter: (field) => field.type === "many2one",
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_field_selector");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_selector_popover .o_field_selector_item",
|
||||
"there should only be one element"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_selector_popover .o_field_selector_page").textContent,
|
||||
"Product",
|
||||
"the available field should be the many2one"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("default `showSearchInput` option", async (assert) => {
|
||||
assert.expect(6);
|
||||
|
||||
// Create the field selector and its mock environment
|
||||
await mountComponent(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
fieldName: "",
|
||||
resModel: "partner",
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_field_selector");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_selector_popover .o_field_selector_search",
|
||||
"there should be a search input"
|
||||
);
|
||||
|
||||
// without search
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_field_selector_popover .o_field_selector_item",
|
||||
3,
|
||||
"there should be three available fields"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_selector_popover .o_field_selector_page").textContent,
|
||||
"BarFooProduct",
|
||||
"the available field should be correct"
|
||||
);
|
||||
|
||||
const input = target.querySelector(
|
||||
".o_field_selector_popover .o_field_selector_search input"
|
||||
);
|
||||
input.value = "xx";
|
||||
await triggerEvent(input, null, "input");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_selector_popover .o_field_selector_item",
|
||||
"there shouldn't be any element"
|
||||
);
|
||||
|
||||
input.value = "Pro";
|
||||
await triggerEvent(input, null, "input");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_selector_popover .o_field_selector_item",
|
||||
"there should only be one element"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_selector_popover .o_field_selector_page").textContent,
|
||||
"Product",
|
||||
"the available field should be the Product"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("false `showSearchInput` option", async (assert) => {
|
||||
assert.expect(1);
|
||||
|
||||
// Create the field selector and its mock environment
|
||||
await mountComponent(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
showSearchInput: false,
|
||||
fieldName: "",
|
||||
resModel: "partner",
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_field_selector");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_selector_popover .o_field_selector_search",
|
||||
"there should be no search input"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("create a field chain with value 1 i.e. TRUE_LEAF", async (assert) => {
|
||||
assert.expect(1);
|
||||
|
||||
//create the field selector with domain value ["1"]
|
||||
await mountComponent(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
showSearchInput: false,
|
||||
fieldName: "1",
|
||||
resModel: "partner",
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_selector_chain_part").textContent.trim(),
|
||||
"1",
|
||||
"field name value should be 1."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("create a field chain with value 0 i.e. FALSE_LEAF", async (assert) => {
|
||||
assert.expect(1);
|
||||
|
||||
//create the field selector with domain value ["0"]
|
||||
await mountComponent(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
showSearchInput: false,
|
||||
fieldName: "0",
|
||||
resModel: "partner",
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_selector_chain_part").textContent.trim(),
|
||||
"0",
|
||||
"field name value should be 0."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("cache fields_get", async (assert) => {
|
||||
serverData.models.partner.fields.partner_id = {
|
||||
string: "Partner",
|
||||
type: "many2one",
|
||||
relation: "partner",
|
||||
searchable: true,
|
||||
};
|
||||
|
||||
await mountComponent(ModelFieldSelector, {
|
||||
mockRPC(route, { method }) {
|
||||
if (method === "fields_get") {
|
||||
assert.step("fields_get");
|
||||
}
|
||||
},
|
||||
props: {
|
||||
readonly: false,
|
||||
fieldName: "partner_id.partner_id.partner_id.foo",
|
||||
resModel: "partner",
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps(["fields_get"]);
|
||||
});
|
||||
|
||||
QUnit.test("Using back button in popover", async (assert) => {
|
||||
serverData.models.partner.fields.partner_id = {
|
||||
string: "Partner",
|
||||
type: "many2one",
|
||||
relation: "partner",
|
||||
searchable: true,
|
||||
};
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.fieldName = "partner_id.foo";
|
||||
}
|
||||
onUpdate(value) {
|
||||
this.fieldName = value;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
Parent.components = { ModelFieldSelector };
|
||||
Parent.template = xml`
|
||||
<ModelFieldSelector
|
||||
readonly="false"
|
||||
resModel="'partner'"
|
||||
fieldName="fieldName"
|
||||
update="(value) => this.onUpdate(value)"
|
||||
/>
|
||||
`;
|
||||
|
||||
await mountComponent(Parent);
|
||||
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_field_selector_value span")].map((el) => el.innerText),
|
||||
["Partner", "Foo"]
|
||||
);
|
||||
assert.containsNone(target, ".o_field_selector i.o_field_selector_warning");
|
||||
|
||||
await click(target, ".o_field_selector");
|
||||
await click(target, ".o_field_selector_prev_page");
|
||||
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_field_selector_value span")].map((el) => el.innerText),
|
||||
["Partner"]
|
||||
);
|
||||
assert.containsNone(target, ".o_field_selector i.o_field_selector_warning");
|
||||
|
||||
await click(target, ".o_field_selector_prev_page");
|
||||
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_field_selector_value span")].map((el) => el.innerText),
|
||||
[""]
|
||||
);
|
||||
assert.containsOnce(target, ".o_field_selector i.o_field_selector_warning");
|
||||
|
||||
await click(target, ".o_field_selector_popover .o_field_selector_item:nth-child(1)");
|
||||
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_field_selector_value span")].map((el) => el.innerText),
|
||||
["Bar"]
|
||||
);
|
||||
|
||||
assert.containsNone(target, ".o_field_selector_popover");
|
||||
});
|
||||
|
||||
QUnit.test("can follow relations", async (assert) => {
|
||||
await mountComponent(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
fieldName: "",
|
||||
resModel: "partner",
|
||||
followRelations: true, // default
|
||||
update(value) {
|
||||
assert.strictEqual(value, "product_id");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_field_selector");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_selector_item:last-child .o_field_selector_relation_icon"
|
||||
);
|
||||
await click(target, ".o_field_selector_item:last-child .o_field_selector_relation_icon");
|
||||
assert.containsOnce(target, ".o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("cannot follow relations", async (assert) => {
|
||||
await mountComponent(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
fieldName: "",
|
||||
resModel: "partner",
|
||||
followRelations: false,
|
||||
update(value) {
|
||||
assert.strictEqual(value, "product_id");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_field_selector");
|
||||
assert.containsNone(target, ".o_field_selector_relation_icon");
|
||||
await click(target, ".o_field_selector_item:last-child");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("Edit path in popover debug input", async (assert) => {
|
||||
serverData.models.partner.fields.partner_id = {
|
||||
string: "Partner",
|
||||
type: "many2one",
|
||||
relation: "partner",
|
||||
searchable: true,
|
||||
};
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.fieldName = "foo";
|
||||
}
|
||||
onUpdate(value) {
|
||||
this.fieldName = value;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
Parent.components = { ModelFieldSelector };
|
||||
Parent.template = xml`
|
||||
<ModelFieldSelector
|
||||
readonly="false"
|
||||
resModel="'partner'"
|
||||
fieldName="fieldName"
|
||||
isDebugMode="true"
|
||||
update="(value) => this.onUpdate(value)"
|
||||
/>
|
||||
`;
|
||||
|
||||
await mountComponent(Parent);
|
||||
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_field_selector_value span")].map((el) => el.innerText),
|
||||
["Foo"]
|
||||
);
|
||||
|
||||
await click(target, ".o_field_selector");
|
||||
|
||||
await editInput(
|
||||
target,
|
||||
".o_field_selector_popover .o_field_selector_debug",
|
||||
"partner_id.bar"
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_field_selector_value span")].map((el) => el.innerText),
|
||||
["Partner", "Bar"]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { ModelSelector } from "@web/core/model_selector/model_selector";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { click, editInput, getFixture, mount, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
|
||||
registry.category("mock_server").add("ir.model/display_name_for", function (route, args) {
|
||||
const models = args.args[0];
|
||||
const records = this.models["ir.model"].records.filter((record) =>
|
||||
models.includes(record.model)
|
||||
);
|
||||
return records.map((record) => ({
|
||||
model: record.model,
|
||||
display_name: record.name,
|
||||
}));
|
||||
});
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let env;
|
||||
let fixture;
|
||||
|
||||
async function mountModelSelector(models = [], value = undefined, onModelSelected = () => {}) {
|
||||
await mount(ModelSelector, fixture, {
|
||||
env,
|
||||
props: {
|
||||
models,
|
||||
value,
|
||||
onModelSelected,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function openAutocomplete(search = undefined) {
|
||||
await click(fixture, ".o-autocomplete--input");
|
||||
}
|
||||
|
||||
async function beforeEach() {
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("orm", ormService);
|
||||
env = await makeTestEnv({
|
||||
serverData: {
|
||||
models: {
|
||||
"ir.model": {
|
||||
fields: {
|
||||
name: { string: "Model Name", type: "char" },
|
||||
model: { string: "Model", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: "Model 1",
|
||||
model: "model_1",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Model 2",
|
||||
model: "model_2",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Model 3",
|
||||
model: "model_3",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Model 4",
|
||||
model: "model_4",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Model 5",
|
||||
model: "model_5",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Model 6",
|
||||
model: "model_6",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Model 7",
|
||||
model: "model_7",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Model 8",
|
||||
model: "model_8",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Model 9",
|
||||
model: "model_9",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Model 10",
|
||||
model: "model_10",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
fixture = getFixture();
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.module("web > model_selector", { beforeEach });
|
||||
|
||||
QUnit.test("model_selector: with no model", async function (assert) {
|
||||
await mountModelSelector();
|
||||
await openAutocomplete();
|
||||
assert.containsOnce(fixture, "li.o-autocomplete--dropdown-item");
|
||||
assert.strictEqual(
|
||||
fixture.querySelector("li.o-autocomplete--dropdown-item").innerText,
|
||||
"No records"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("model_selector: displays model display names", async function (assert) {
|
||||
await mountModelSelector(["model_1", "model_2", "model_3"]);
|
||||
await openAutocomplete();
|
||||
assert.containsN(fixture, "li.o-autocomplete--dropdown-item", 3);
|
||||
const items = fixture.querySelectorAll("li.o-autocomplete--dropdown-item");
|
||||
assert.strictEqual(items[0].innerText, "Model 1");
|
||||
assert.strictEqual(items[1].innerText, "Model 2");
|
||||
assert.strictEqual(items[2].innerText, "Model 3");
|
||||
});
|
||||
|
||||
QUnit.test("model_selector: with 8 models", async function (assert) {
|
||||
await mountModelSelector([
|
||||
"model_1",
|
||||
"model_2",
|
||||
"model_3",
|
||||
"model_4",
|
||||
"model_5",
|
||||
"model_6",
|
||||
"model_7",
|
||||
"model_8",
|
||||
]);
|
||||
await openAutocomplete();
|
||||
assert.containsN(fixture, "li.o-autocomplete--dropdown-item", 8);
|
||||
});
|
||||
|
||||
QUnit.test("model_selector: with more than 8 models", async function (assert) {
|
||||
await mountModelSelector([
|
||||
"model_1",
|
||||
"model_2",
|
||||
"model_3",
|
||||
"model_4",
|
||||
"model_5",
|
||||
"model_6",
|
||||
"model_7",
|
||||
"model_8",
|
||||
"model_9",
|
||||
"model_10",
|
||||
]);
|
||||
await openAutocomplete();
|
||||
assert.containsN(fixture, "li.o-autocomplete--dropdown-item", 8);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"model_selector: search content is not applied when opening the autocomplete",
|
||||
async function (assert) {
|
||||
await mountModelSelector(["model_1", "model_2"], "_2");
|
||||
await openAutocomplete();
|
||||
assert.containsN(fixture, "li.o-autocomplete--dropdown-item", 2);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"model_selector: with search matching some records on technical name",
|
||||
async function (assert) {
|
||||
await mountModelSelector(["model_1", "model_2"]);
|
||||
await openAutocomplete();
|
||||
await editInput(fixture, ".o-autocomplete--input", "_2");
|
||||
assert.containsOnce(fixture, "li.o-autocomplete--dropdown-item");
|
||||
assert.strictEqual(
|
||||
fixture.querySelector("li.o-autocomplete--dropdown-item").innerText,
|
||||
"Model 2"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"model_selector: with search matching some records on business name",
|
||||
async function (assert) {
|
||||
await mountModelSelector(["model_1", "model_2"]);
|
||||
await openAutocomplete();
|
||||
await editInput(fixture, ".o-autocomplete--input", " 2");
|
||||
assert.containsOnce(fixture, "li.o-autocomplete--dropdown-item");
|
||||
assert.strictEqual(
|
||||
fixture.querySelector("li.o-autocomplete--dropdown-item").innerText,
|
||||
"Model 2"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("model_selector: with search matching no record", async function (assert) {
|
||||
await mountModelSelector(["model_1", "model_2"]);
|
||||
await openAutocomplete("a random search query");
|
||||
await editInput(fixture, ".o-autocomplete--input", "a random search query");
|
||||
assert.containsOnce(fixture, "li.o-autocomplete--dropdown-item");
|
||||
assert.strictEqual(
|
||||
fixture.querySelector("li.o-autocomplete--dropdown-item").innerText,
|
||||
"No records"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("model_selector: select a model", async function (assert) {
|
||||
await mountModelSelector(["model_1", "model_2", "model_3"], "Model 1", (selected) => {
|
||||
assert.step("model selected");
|
||||
assert.deepEqual(selected, {
|
||||
label: "Model 2",
|
||||
technical: "model_2",
|
||||
});
|
||||
});
|
||||
await openAutocomplete();
|
||||
await click(fixture.querySelector(".o_model_selector_model_2"));
|
||||
assert.verifySteps(["model selected"]);
|
||||
});
|
||||
|
||||
QUnit.test("model_selector: with an initial value", async function (assert) {
|
||||
await mountModelSelector(["model_1", "model_2", "model_3"], "Model 1");
|
||||
assert.equal(fixture.querySelector(".o-autocomplete--input").value, "Model 1");
|
||||
});
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { makeFakeRPCService } from "@web/../tests/helpers/mock_services";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { NameAndSignature } from "@web/core/signature/name_and_signature";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { click, editInput, getFixture, mount, nextTick } from "../helpers/utils";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let env;
|
||||
let target;
|
||||
let props;
|
||||
|
||||
QUnit.module("Components", ({ beforeEach }) => {
|
||||
beforeEach(async () => {
|
||||
const mockRPC = async (route, args) => {
|
||||
if (route === "/web/sign/get_fonts/") {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("rpc", makeFakeRPCService(mockRPC));
|
||||
|
||||
target = getFixture();
|
||||
const signature = {};
|
||||
props = {
|
||||
signature,
|
||||
};
|
||||
env = await makeTestEnv();
|
||||
});
|
||||
|
||||
QUnit.module("NameAndSignature");
|
||||
|
||||
QUnit.test("test name_and_signature widget", async (assert) => {
|
||||
const defaultName = "Don Toliver";
|
||||
props.signature.name = defaultName;
|
||||
await mount(NameAndSignature, target, { env, props });
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".card-header .col-auto")].map((el) =>
|
||||
el.textContent.trim()
|
||||
),
|
||||
["Auto", "Draw", "Load", "Style"]
|
||||
);
|
||||
assert.containsOnce(target, ".card-header .active");
|
||||
assert.strictEqual(target.querySelector(".card-header .active").textContent.trim(), "Auto");
|
||||
assert.containsOnce(target, ".o_web_sign_name_group input");
|
||||
assert.strictEqual(target.querySelector(".o_web_sign_name_group input").value, defaultName);
|
||||
|
||||
await click(target, ".o_web_sign_draw_button");
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".card-header .col-auto")].map((el) =>
|
||||
el.textContent.trim()
|
||||
),
|
||||
["Auto", "Draw", "Load", "Clear"]
|
||||
);
|
||||
assert.containsOnce(target, ".card-header .active");
|
||||
assert.strictEqual(target.querySelector(".card-header .active").textContent.trim(), "Draw");
|
||||
|
||||
await click(target, ".o_web_sign_load_button");
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".card-header .col-auto")].map((el) =>
|
||||
el.textContent.trim()
|
||||
),
|
||||
["Auto", "Draw", "Load", ""]
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelectorAll(".card-header .col-auto")[3],
|
||||
"o_web_sign_load_file"
|
||||
);
|
||||
assert.containsOnce(target, ".card-header .active");
|
||||
assert.strictEqual(target.querySelector(".card-header .active").textContent.trim(), "Load");
|
||||
});
|
||||
|
||||
QUnit.test("test name_and_signature widget without name", async (assert) => {
|
||||
await mount(NameAndSignature, target, { env, props });
|
||||
assert.containsNone(target, ".card-header");
|
||||
assert.containsOnce(target, ".o_web_sign_name_group input");
|
||||
assert.strictEqual(target.querySelector(".o_web_sign_name_group input").value, "");
|
||||
|
||||
await editInput(target, ".o_web_sign_name_group input", "plop");
|
||||
await nextTick();
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".card-header .col-auto")].map((el) =>
|
||||
el.textContent.trim()
|
||||
),
|
||||
["Auto", "Draw", "Load", "Style"]
|
||||
);
|
||||
assert.strictEqual(target.querySelector(".card-header .active").textContent.trim(), "Auto");
|
||||
assert.containsOnce(target, ".o_web_sign_name_group input");
|
||||
assert.strictEqual(target.querySelector(".o_web_sign_name_group input").value, "plop");
|
||||
|
||||
await click(target, ".o_web_sign_draw_button");
|
||||
assert.containsOnce(target, ".card-header .active");
|
||||
assert.strictEqual(target.querySelector(".card-header .active").textContent.trim(), "Draw");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"test name_and_signature widget with noInputName and default name",
|
||||
async function (assert) {
|
||||
const defaultName = "Don Toliver";
|
||||
props = {
|
||||
...props,
|
||||
noInputName: true,
|
||||
};
|
||||
props.signature.name = defaultName;
|
||||
await mount(NameAndSignature, target, { env, props });
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".card-header .col-auto")].map((el) =>
|
||||
el.textContent.trim()
|
||||
),
|
||||
["Auto", "Draw", "Load", "Style"]
|
||||
);
|
||||
assert.containsOnce(target, ".card-header .active");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".card-header .active").textContent.trim(),
|
||||
"Auto"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"test name_and_signature widget with noInputName and without name",
|
||||
async function (assert) {
|
||||
props = {
|
||||
...props,
|
||||
noInputName: true,
|
||||
};
|
||||
await mount(NameAndSignature, target, { env, props });
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".card-header .col-auto")].map((el) =>
|
||||
el.textContent.trim()
|
||||
),
|
||||
["Draw", "Load", "Clear"]
|
||||
);
|
||||
assert.containsOnce(target, ".card-header .active");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".card-header .active").textContent.trim(),
|
||||
"Draw"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
/** @odoo-module */
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { makeDeferred, patchWithCleanup } from "../../helpers/utils";
|
||||
import { download } from "@web/core/network/download";
|
||||
import { makeMockXHR } from "../../helpers/mock_services";
|
||||
import { ConnectionLostError, RPCError } from "@web/core/network/rpc_service";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
|
||||
QUnit.module("download", (hooks) => {
|
||||
QUnit.test("handles connection error when behind a server", async (assert) => {
|
||||
assert.expect(1);
|
||||
|
||||
function send() {
|
||||
this.status = 502;
|
||||
this.response = {
|
||||
type: "text/html",
|
||||
};
|
||||
}
|
||||
const MockXHR = makeMockXHR("", send);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
|
||||
let error;
|
||||
try {
|
||||
await download({
|
||||
data: {},
|
||||
url: "/some_url",
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
assert.ok(error instanceof ConnectionLostError);
|
||||
});
|
||||
|
||||
QUnit.test("handles connection error when network unavailable", async (assert) => {
|
||||
assert.expect(1);
|
||||
|
||||
async function send() {
|
||||
return Promise.reject();
|
||||
}
|
||||
const MockXHR = makeMockXHR("", send);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
|
||||
let error;
|
||||
try {
|
||||
await download({
|
||||
data: {},
|
||||
url: "/some_url",
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
assert.ok(error instanceof ConnectionLostError);
|
||||
});
|
||||
|
||||
QUnit.test("handles business error from server", async (assert) => {
|
||||
assert.expect(4);
|
||||
|
||||
const serverError = {
|
||||
code: 200,
|
||||
data: {
|
||||
name: "odoo.exceptions.RedirectWarning",
|
||||
arguments: ["Business Error Message", "someArg"],
|
||||
message: "Business Error Message",
|
||||
},
|
||||
message: "Odoo Server Error",
|
||||
};
|
||||
|
||||
async function send() {
|
||||
this.status = 200;
|
||||
this.response = new Blob([JSON.stringify(serverError)], { type: "text/html" });
|
||||
}
|
||||
const MockXHR = makeMockXHR("", send);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
|
||||
let error;
|
||||
try {
|
||||
await download({
|
||||
data: {},
|
||||
url: "/some_url",
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
assert.ok(error instanceof RPCError);
|
||||
assert.strictEqual(error.data.name, serverError.data.name);
|
||||
assert.strictEqual(error.data.message, serverError.data.message);
|
||||
assert.deepEqual(error.data.arguments, serverError.data.arguments);
|
||||
});
|
||||
|
||||
QUnit.test("handles arbitrary error", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
const serverError = /* xml */ `<html><body><div>HTML error message</div></body></html>`;
|
||||
|
||||
async function send() {
|
||||
this.status = 200;
|
||||
this.response = new Blob([JSON.stringify(serverError)], { type: "text/html" });
|
||||
}
|
||||
const MockXHR = makeMockXHR("", send);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
|
||||
let error;
|
||||
try {
|
||||
await download({
|
||||
data: {},
|
||||
url: "/some_url",
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
assert.ok(error instanceof RPCError);
|
||||
assert.strictEqual(error.message, "Arbitrary Uncaught Python Exception");
|
||||
assert.strictEqual(error.data.debug.trim(), `200` + `\n` + `HTML error message`);
|
||||
});
|
||||
|
||||
QUnit.test("handles success download", async (assert) => {
|
||||
// This test relies on a implementation detail of the lowest layer of download
|
||||
// That is, a link will be created with the download attribute
|
||||
assert.expect(8);
|
||||
|
||||
async function send(data) {
|
||||
assert.ok(data instanceof FormData);
|
||||
assert.strictEqual(data.get("someKey"), "someValue");
|
||||
assert.ok(data.has("token"));
|
||||
assert.ok(data.has("csrf_token"));
|
||||
|
||||
this.status = 200;
|
||||
this.response = new Blob(["some plain text file"], { type: "text/plain" });
|
||||
}
|
||||
const MockXHR = makeMockXHR("", send);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
|
||||
assert.containsNone(document.body, "a[download]");
|
||||
|
||||
const prom = makeDeferred();
|
||||
|
||||
// This part asserts the implementation detail in question
|
||||
const downloadOnClick = (ev) => {
|
||||
const target = ev.target;
|
||||
if (target.tagName === "A" && "download" in target.attributes) {
|
||||
ev.preventDefault();
|
||||
assert.ok(target.href.startsWith("blob:"));
|
||||
assert.step("file downloaded");
|
||||
document.removeEventListener("click", downloadOnClick);
|
||||
prom.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", downloadOnClick);
|
||||
// safety first: do not pollute window
|
||||
registerCleanup(() => document.removeEventListener("click", downloadOnClick));
|
||||
download({ data: { someKey: "someValue" }, url: "/some_url" });
|
||||
await prom;
|
||||
assert.verifySteps(["file downloaded"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { ConnectionAbortedError, ConnectionLostError, rpcService } from "@web/core/network/rpc_service";
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { patch, unpatch } from "@web/core/utils/patch";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
|
||||
import { makeMockXHR } from "../../helpers/mock_services";
|
||||
import {
|
||||
destroy,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "../../helpers/utils";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
let isXHRMocked = false;
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let isDeployed = false;
|
||||
async function testRPC(route, params) {
|
||||
let url = "";
|
||||
let request;
|
||||
const MockXHR = makeMockXHR({ test: true }, function (data) {
|
||||
request = data;
|
||||
url = this.url;
|
||||
});
|
||||
if (isXHRMocked) {
|
||||
unpatch(browser, "mock.xhr");
|
||||
}
|
||||
patch(
|
||||
browser,
|
||||
"mock.xhr",
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
isXHRMocked = true;
|
||||
|
||||
if (isDeployed) {
|
||||
clearRegistryWithCleanup(registry.category("main_components"));
|
||||
}
|
||||
const env = await makeTestEnv({
|
||||
serviceRegistry,
|
||||
// browser: { XMLHttpRequest: MockXHR },
|
||||
});
|
||||
isDeployed = true;
|
||||
registerCleanup(() => (isDeployed = false));
|
||||
await env.services.rpc(route, params);
|
||||
return { url, request };
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Tests
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
QUnit.module("RPC", {
|
||||
beforeEach() {
|
||||
serviceRegistry.add("notification", notificationService);
|
||||
serviceRegistry.add("rpc", rpcService);
|
||||
},
|
||||
afterEach() {
|
||||
if (isXHRMocked) {
|
||||
unpatch(browser, "mock.xhr");
|
||||
isXHRMocked = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("can perform a simple rpc", async (assert) => {
|
||||
assert.expect(4);
|
||||
const MockXHR = makeMockXHR({ result: { action_id: 123 } }, (request) => {
|
||||
assert.strictEqual(request.jsonrpc, "2.0");
|
||||
assert.strictEqual(request.method, "call");
|
||||
assert.ok(typeof request.id === "number");
|
||||
});
|
||||
|
||||
patch(browser, "mock.xhr", { XMLHttpRequest: MockXHR }, { pure: true });
|
||||
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const result = await env.services.rpc("/test/");
|
||||
assert.deepEqual(result, { action_id: 123 });
|
||||
unpatch(browser, "mock.xhr");
|
||||
});
|
||||
|
||||
QUnit.test("trigger an error when response has 'error' key", async (assert) => {
|
||||
assert.expect(1);
|
||||
const error = {
|
||||
message: "message",
|
||||
code: 12,
|
||||
data: {
|
||||
debug: "data_debug",
|
||||
message: "data_message",
|
||||
},
|
||||
};
|
||||
const MockXHR = makeMockXHR({ error });
|
||||
patch(browser, "mock.xhr", { XMLHttpRequest: MockXHR }, { pure: true });
|
||||
|
||||
const env = await makeTestEnv({
|
||||
serviceRegistry,
|
||||
});
|
||||
try {
|
||||
await env.services.rpc("/test/");
|
||||
} catch (_error) {
|
||||
assert.ok(true);
|
||||
}
|
||||
unpatch(browser, "mock.xhr");
|
||||
});
|
||||
|
||||
QUnit.test("rpc with simple routes", async (assert) => {
|
||||
const info1 = await testRPC("/my/route");
|
||||
assert.strictEqual(info1.url, "/my/route");
|
||||
const info2 = await testRPC("/my/route", { hey: "there", model: "test" });
|
||||
assert.deepEqual(info2.request.params, {
|
||||
hey: "there",
|
||||
model: "test",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("rpc coming from destroyed components are left pending", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.rpc = useService("rpc");
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<div/>`;
|
||||
const def = makeDeferred();
|
||||
const MockXHR = makeMockXHR({ result: "1" }, () => {}, def);
|
||||
patch(browser, "mock.xhr", { XMLHttpRequest: MockXHR }, { pure: true });
|
||||
|
||||
const env = await makeTestEnv({
|
||||
serviceRegistry,
|
||||
});
|
||||
const target = getFixture();
|
||||
const component = await mount(MyComponent, target, { env });
|
||||
let isResolved = false;
|
||||
let isFailed = false;
|
||||
component
|
||||
.rpc("/my/route")
|
||||
.then(() => {
|
||||
isResolved = true;
|
||||
})
|
||||
.catch(() => {
|
||||
isFailed = true;
|
||||
});
|
||||
assert.strictEqual(isResolved, false);
|
||||
assert.strictEqual(isFailed, false);
|
||||
destroy(component);
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.strictEqual(isResolved, false);
|
||||
assert.strictEqual(isFailed, false);
|
||||
unpatch(browser, "mock.xhr");
|
||||
});
|
||||
|
||||
QUnit.test("rpc initiated from destroyed components throw exception", async (assert) => {
|
||||
assert.expect(1);
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.rpc = useService("rpc");
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<div/>`;
|
||||
const env = await makeTestEnv({
|
||||
serviceRegistry,
|
||||
});
|
||||
const target = getFixture();
|
||||
const component = await mount(MyComponent, target, { env });
|
||||
destroy(component);
|
||||
try {
|
||||
await component.rpc("/my/route");
|
||||
} catch (e) {
|
||||
assert.strictEqual(e.message, "Component is destroyed");
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("check trigger RPC:REQUEST and RPC:RESPONSE for a simple rpc", async (assert) => {
|
||||
const MockXHR = makeMockXHR({ test: true }, () => 1);
|
||||
patch(browser, "mock.xhr", { XMLHttpRequest: MockXHR }, { pure: true });
|
||||
|
||||
const env = await makeTestEnv({
|
||||
serviceRegistry,
|
||||
});
|
||||
const rpcIdsRequest = [];
|
||||
const rpcIdsResponse = [];
|
||||
env.bus.addEventListener("RPC:REQUEST", (rpcId) => {
|
||||
rpcIdsRequest.push(rpcId);
|
||||
assert.step("RPC:REQUEST");
|
||||
});
|
||||
env.bus.addEventListener("RPC:RESPONSE", (rpcId) => {
|
||||
rpcIdsResponse.push(rpcId);
|
||||
assert.step("RPC:RESPONSE");
|
||||
});
|
||||
await env.services.rpc("/test/");
|
||||
assert.strictEqual(rpcIdsRequest.toString(), rpcIdsResponse.toString());
|
||||
assert.verifySteps(["RPC:REQUEST", "RPC:RESPONSE"]);
|
||||
|
||||
await env.services.rpc("/test/", {}, { silent: true });
|
||||
assert.verifySteps([]);
|
||||
|
||||
unpatch(browser, "mock.xhr");
|
||||
});
|
||||
|
||||
QUnit.test("check trigger RPC:REQUEST and RPC:RESPONSE for a rpc with an error", async (assert) => {
|
||||
const error = {
|
||||
message: "message",
|
||||
code: 12,
|
||||
data: {
|
||||
debug: "data_debug",
|
||||
message: "data_message",
|
||||
},
|
||||
};
|
||||
const MockXHR = makeMockXHR({ error });
|
||||
patch(browser, "mock.xhr", { XMLHttpRequest: MockXHR }, { pure: true });
|
||||
const env = await makeTestEnv({
|
||||
serviceRegistry,
|
||||
});
|
||||
const rpcIdsRequest = [];
|
||||
const rpcIdsResponse = [];
|
||||
env.bus.addEventListener("RPC:REQUEST", (rpcId) => {
|
||||
rpcIdsRequest.push(rpcId);
|
||||
assert.step("RPC:REQUEST");
|
||||
});
|
||||
env.bus.addEventListener("RPC:RESPONSE", (rpcId) => {
|
||||
rpcIdsResponse.push(rpcId);
|
||||
assert.step("RPC:RESPONSE");
|
||||
});
|
||||
try {
|
||||
await env.services.rpc("/test/");
|
||||
} catch (_e) {
|
||||
assert.ok(true);
|
||||
}
|
||||
assert.strictEqual(rpcIdsRequest.toString(), rpcIdsResponse.toString());
|
||||
assert.verifySteps(["RPC:REQUEST", "RPC:RESPONSE"]);
|
||||
unpatch(browser, "mock.xhr");
|
||||
});
|
||||
|
||||
QUnit.test("check connection aborted", async (assert) => {
|
||||
const def = makeDeferred();
|
||||
const MockXHR = makeMockXHR({}, () => {}, def);
|
||||
patchWithCleanup(browser, { XMLHttpRequest: MockXHR }, { pure: true });
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
env.bus.addEventListener("RPC:REQUEST", () => {
|
||||
assert.step("RPC:REQUEST");
|
||||
});
|
||||
env.bus.addEventListener("RPC:RESPONSE", () => {
|
||||
assert.step("RPC:RESPONSE");
|
||||
});
|
||||
|
||||
const connection = env.services.rpc();
|
||||
connection.abort();
|
||||
assert.rejects(connection, ConnectionAbortedError);
|
||||
assert.verifySteps(["RPC:REQUEST", "RPC:RESPONSE"]);
|
||||
});
|
||||
|
||||
QUnit.test("trigger a ConnectionLostError when response isn't json parsable", async (assert) => {
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
|
||||
const MockXHR = makeMockXHR({}, () => {});
|
||||
const request = new MockXHR();
|
||||
request.response = "<h...";
|
||||
request.status = "500";
|
||||
|
||||
try {
|
||||
await env.services.rpc("/test/", null, { xhr: request });
|
||||
} catch (e) {
|
||||
assert.ok(e instanceof ConnectionLostError);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("rpc can send additional headers", async (assert) => {
|
||||
assert.expect(1);
|
||||
const MockXHR = makeMockXHR(null, function () {
|
||||
assert.deepEqual(this._requestHeaders, {
|
||||
"Content-Type": "application/json",
|
||||
Hello: "World",
|
||||
});
|
||||
});
|
||||
function HeaderCollectingMockXHR() {
|
||||
const ret = MockXHR();
|
||||
ret._requestHeaders = {};
|
||||
ret.setRequestHeader = function(header, value) {
|
||||
ret._requestHeaders[header] = value;
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
patchWithCleanup(browser, { XMLHttpRequest: HeaderCollectingMockXHR }, { pure: true });
|
||||
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
await env.services.rpc("/test/", null, { headers: { Hello: 'World' } });
|
||||
});
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Notebook } from "@web/core/notebook/notebook";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { click, getFixture, mount, nextTick } from "@web/../tests/helpers/utils";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
let target;
|
||||
|
||||
QUnit.module("Components", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("Notebook");
|
||||
|
||||
QUnit.test("not rendered if empty slots", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
await mount(Notebook, target, { env, props: {} });
|
||||
assert.containsNone(target, "div.o_notebook");
|
||||
});
|
||||
|
||||
QUnit.test("notebook with multiple pages given as slots", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`<Notebook>
|
||||
<t t-set-slot="page_about" title="'About'" isVisible="true">
|
||||
<h3>About the bird</h3>
|
||||
<p>Owls are birds from the order Strigiformes which includes over
|
||||
200 species of mostly solitary and nocturnal birds of prey typified by an upright stance, ...</p>
|
||||
</t>
|
||||
<t t-set-slot="page_hunting" title="'Owl Activities'" isVisible="true">
|
||||
<h3>Their favorite activity: hunting</h3>
|
||||
<p>Owls are called raptors, or birds of prey, which means they use sharp talons and curved bills to hunt, kill, and eat other animals.</p>
|
||||
</t>
|
||||
<t t-set-slot="page_secret" title="'Secret about OWLs'" isVisible="false">
|
||||
<p>TODO find a great secret about OWLs.</p>
|
||||
</t>
|
||||
</Notebook>`;
|
||||
Parent.components = { Notebook };
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, "div.o_notebook");
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_notebook"),
|
||||
"horizontal",
|
||||
"default orientation is set as horizontal"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".nav"),
|
||||
"flex-row",
|
||||
"navigation container uses the right class to display as horizontal tabs"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_notebook_headers a.nav-link",
|
||||
2,
|
||||
"navigation link is present for each visible page"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_notebook_headers .nav-item:first-child a"),
|
||||
"active",
|
||||
"first page is selected by default"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".tab-pane.active").firstElementChild.textContent,
|
||||
"About the bird",
|
||||
"first page content is displayed by the notebook"
|
||||
);
|
||||
|
||||
await click(target, ".o_notebook_headers .nav-item:nth-child(2) a");
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_notebook_headers .nav-item:nth-child(2) a"),
|
||||
"active",
|
||||
"second page is now selected"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".tab-pane.active").firstElementChild.textContent,
|
||||
"Their favorite activity: hunting",
|
||||
"second page content is displayed by the notebook"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("notebook with defaultPage props", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`<Notebook defaultPage="'page_hunting'">
|
||||
<t t-set-slot="page_about" title="'About'" isVisible="true">
|
||||
<h3>About the bird</h3>
|
||||
<p>Owls are birds from the order Strigiformes which includes over
|
||||
200 species of mostly solitary and nocturnal birds of prey typified by an upright stance, ...</p>
|
||||
</t>
|
||||
<t t-set-slot="page_hunting" title="'Owl Activities'" isVisible="true">
|
||||
<h3>Their favorite activity: hunting</h3>
|
||||
<p>Owls are called raptors, or birds of prey, which means they use sharp talons and curved bills to hunt, kill, and eat other animals.</p>
|
||||
</t>
|
||||
<t t-set-slot="page_secret" title="'Secret about OWLs'" isVisible="false">
|
||||
<p>TODO find a great secret about OWLs.</p>
|
||||
</t>
|
||||
</Notebook>`;
|
||||
Parent.components = { Notebook };
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, "div.o_notebook");
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_notebook_headers .nav-item:nth-child(2) a"),
|
||||
"active",
|
||||
"second page is selected by default"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".tab-pane.active").firstElementChild.textContent,
|
||||
"Their favorite activity: hunting",
|
||||
"second page content is displayed by the notebook"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("notebook with defaultPage set on invisible page", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`<Notebook defaultPage="'page_secret'">
|
||||
<t t-set-slot="page_about" title="'About'" isVisible="true">
|
||||
<h3>About the bird</h3>
|
||||
<p>Owls are birds from the order Strigiformes which includes over
|
||||
200 species of mostly solitary and nocturnal birds of prey typified by an upright stance, ...</p>
|
||||
</t>
|
||||
<t t-set-slot="page_hunting" title="'Owl Activities'" isVisible="true">
|
||||
<h3>Their favorite activity: hunting</h3>
|
||||
<p>Owls are called raptors, or birds of prey, which means they use sharp talons and curved bills to hunt, kill, and eat other animals.</p>
|
||||
</t>
|
||||
<t t-set-slot="page_secret" title="'Secret about OWLs'" isVisible="false">
|
||||
<h3>Oooops</h3>
|
||||
<p>TODO find a great secret to reveal about OWLs.</p>
|
||||
</t>
|
||||
</Notebook>`;
|
||||
Parent.components = { Notebook };
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, "div.o_notebook");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notebook_headers .nav-item a.active").textContent,
|
||||
"About",
|
||||
"The first page is selected"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_notebook_headers a.nav-link",
|
||||
2,
|
||||
"navigation link is only present for visible pages"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".tab-pane.active").firstElementChild.textContent,
|
||||
"About the bird",
|
||||
"third page content is displayed by the notebook"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("notebook set vertically", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`<Notebook orientation="'vertical'">
|
||||
<t t-set-slot="page_about" title="'About'" isVisible="true">
|
||||
<h3>About the bird</h3>
|
||||
<p>Owls are birds from the order Strigiformes which includes over
|
||||
200 species of mostly solitary and nocturnal birds of prey typified by an upright stance, ...</p>
|
||||
</t>
|
||||
<t t-set-slot="page_hunting" title="'Owl Activities'" isVisible="true">
|
||||
<h3>Their favorite activity: hunting</h3>
|
||||
<p>Owls are called raptors, or birds of prey, which means they use sharp talons and curved bills to hunt, kill, and eat other animals.</p>
|
||||
</t>
|
||||
</Notebook>`;
|
||||
Parent.components = { Notebook };
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, "div.o_notebook");
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_notebook"),
|
||||
"vertical",
|
||||
"orientation is set as vertical"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".nav"),
|
||||
"flex-column",
|
||||
"navigation container uses the right class to display as vertical buttons"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("notebook pages rendered by a template component", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
|
||||
class NotebookPageRenderer extends Component {}
|
||||
NotebookPageRenderer.template = xml`
|
||||
<h3 t-esc="props.heading"></h3>
|
||||
<p t-esc="props.text" />
|
||||
`;
|
||||
NotebookPageRenderer.props = {
|
||||
heading: String,
|
||||
text: String,
|
||||
};
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.pages = [
|
||||
{
|
||||
Component: NotebookPageRenderer,
|
||||
index: 1,
|
||||
title: "Page 2",
|
||||
props: {
|
||||
heading: "Page 2",
|
||||
text: "Second page rendered by a template component",
|
||||
},
|
||||
},
|
||||
{
|
||||
Component: NotebookPageRenderer,
|
||||
id: "page_three", // required to be set as default page
|
||||
index: 2,
|
||||
title: "Page 3",
|
||||
props: {
|
||||
heading: "Page 3",
|
||||
text: "Third page rendered by a template component",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
Parent.template = xml`<Notebook defaultPage="'page_three'" pages="pages">
|
||||
<t t-set-slot="page_one" title="'Page 1'" isVisible="true">
|
||||
<h3>Page 1</h3>
|
||||
<p>First page set directly as a slot</p>
|
||||
</t>
|
||||
<t t-set-slot="page_four" title="'Page 4'" isVisible="true">
|
||||
<h3>Page 4</h3>
|
||||
</t>
|
||||
</Notebook>`;
|
||||
Parent.components = { Notebook };
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
|
||||
assert.containsOnce(target, "div.o_notebook");
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_notebook_headers .nav-item:nth-child(3) a"),
|
||||
"active",
|
||||
"third page is selected by default"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_notebook_headers .nav-item:nth-child(2) a"));
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_notebook_content p").textContent,
|
||||
"Second page rendered by a template component",
|
||||
"displayed content corresponds to the current page"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("each page is different", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
|
||||
class Page extends Component {}
|
||||
Page.template = xml`<h3>Coucou</h3>`;
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.pages = [
|
||||
{
|
||||
Component: Page,
|
||||
index: 1,
|
||||
title: "Page 1",
|
||||
},
|
||||
{
|
||||
Component: Page,
|
||||
index: 2,
|
||||
title: "Page 2",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
Parent.template = xml`<Notebook pages="pages"/>`;
|
||||
Parent.components = { Notebook };
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
|
||||
const firstPage = target.querySelector("h3");
|
||||
assert.ok(firstPage instanceof HTMLElement);
|
||||
|
||||
await click(target.querySelector(".o_notebook_headers .nav-item:nth-child(2) a"));
|
||||
|
||||
const secondPage = target.querySelector("h3");
|
||||
assert.ok(firstPage instanceof HTMLElement);
|
||||
|
||||
assert.notEqual(firstPage, secondPage);
|
||||
});
|
||||
|
||||
QUnit.test("defaultPage recomputed when isVisible is dynamic", async (assert) => {
|
||||
let defaultPageVisible = false;
|
||||
class Parent extends Component {
|
||||
get defaultPageVisible() {
|
||||
return defaultPageVisible;
|
||||
}
|
||||
}
|
||||
Parent.components = { Notebook };
|
||||
Parent.template = xml`
|
||||
<Notebook defaultPage="'3'">
|
||||
<t t-set-slot="1" title="'page1'" isVisible="true">
|
||||
<div class="page1" />
|
||||
</t>
|
||||
<t t-set-slot="2" title="'page2'" isVisible="true">
|
||||
<div class="page2" />
|
||||
</t>
|
||||
<t t-set-slot="3" title="'page3'" isVisible="defaultPageVisible">
|
||||
<div class="page3" />
|
||||
</t>
|
||||
</Notebook>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const parent = await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".page1");
|
||||
assert.strictEqual(target.querySelector(".nav-link.active").textContent, "page1");
|
||||
|
||||
defaultPageVisible = true;
|
||||
parent.render(true);
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".page3");
|
||||
assert.strictEqual(target.querySelector(".nav-link.active").textContent, "page3");
|
||||
|
||||
await click(target.querySelectorAll(".nav-link")[1]);
|
||||
assert.containsOnce(target, ".page2");
|
||||
assert.strictEqual(target.querySelector(".nav-link.active").textContent, "page2");
|
||||
|
||||
parent.render(true);
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".page2");
|
||||
assert.strictEqual(target.querySelector(".nav-link.active").textContent, "page2");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,365 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
import { click, getFixture, mount, nextTick, patchWithCleanup } from "../../helpers/utils";
|
||||
|
||||
import { markup } from "@odoo/owl";
|
||||
|
||||
let target;
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("Notifications", {
|
||||
async beforeEach() {
|
||||
target = getFixture();
|
||||
serviceRegistry.add("notification", notificationService);
|
||||
patchWithCleanup(browser, { setTimeout: () => 1 });
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("can display a basic notification", async (assert) => {
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
await mount(NotificationContainer, target, { env, props });
|
||||
|
||||
notifService.add("I'm a basic notification");
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
const notif = target.querySelector(".o_notification");
|
||||
assert.strictEqual(
|
||||
notif.querySelector(".o_notification_content").textContent,
|
||||
"I'm a basic notification"
|
||||
);
|
||||
assert.hasClass(notif, "border-warning");
|
||||
});
|
||||
|
||||
QUnit.test("can display a notification with a className", async (assert) => {
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
await mount(NotificationContainer, target, { env, props });
|
||||
|
||||
notifService.add("I'm a basic notification", { className: "abc" });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification.abc");
|
||||
});
|
||||
|
||||
QUnit.test("title and message are escaped by default", async (assert) => {
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
await mount(NotificationContainer, target, { env, props });
|
||||
|
||||
notifService.add("<i>Some message</i>", { title: "<b>Some title</b>" });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
const notif = target.querySelector(".o_notification");
|
||||
assert.strictEqual(
|
||||
notif.querySelector(".o_notification_title").textContent,
|
||||
"<b>Some title</b>"
|
||||
);
|
||||
assert.strictEqual(
|
||||
notif.querySelector(".o_notification_content").textContent,
|
||||
"<i>Some message</i>"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("can display a notification with markup content", async (assert) => {
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
await mount(NotificationContainer, target, { env, props });
|
||||
|
||||
notifService.add(markup("<b>I'm a <i>markup</i> notification</b>"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
const notif = target.querySelector(".o_notification");
|
||||
assert.strictEqual(
|
||||
notif.querySelector(".o_notification_content").innerHTML,
|
||||
"<b>I'm a <i>markup</i> notification</b>"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("can display a notification of type danger", async (assert) => {
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
await mount(NotificationContainer, target, { env, props });
|
||||
|
||||
notifService.add("I'm a danger notification", { type: "danger" });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
const notif = target.querySelector(".o_notification");
|
||||
assert.strictEqual(
|
||||
notif.querySelector(".o_notification_content").textContent,
|
||||
"I'm a danger notification"
|
||||
);
|
||||
assert.hasClass(notif, "border-danger");
|
||||
});
|
||||
|
||||
QUnit.test("can display a danger notification with a title", async (assert) => {
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
await mount(NotificationContainer, target, { env, props });
|
||||
|
||||
notifService.add("I'm a danger notification", { title: "Some title", type: "danger" });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
const notif = target.querySelector(".o_notification");
|
||||
assert.strictEqual(notif.querySelector(".o_notification_title").textContent, "Some title");
|
||||
assert.strictEqual(
|
||||
notif.querySelector(".o_notification_content").textContent,
|
||||
"I'm a danger notification"
|
||||
);
|
||||
assert.hasClass(notif, "border-danger");
|
||||
});
|
||||
|
||||
QUnit.test("can display a notification with a button", async (assert) => {
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
await mount(NotificationContainer, target, { env, props });
|
||||
|
||||
notifService.add("I'm a notification with button", {
|
||||
buttons: [
|
||||
{
|
||||
name: "I'm a button",
|
||||
primary: true,
|
||||
onClick: () => {
|
||||
assert.step("Button clicked");
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
const notif = target.querySelector(".o_notification");
|
||||
assert.strictEqual(notif.querySelector(".o_notification_buttons").textContent, "I'm a button");
|
||||
await click(notif, ".btn-primary");
|
||||
assert.verifySteps(["Button clicked"]);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_notification",
|
||||
"Clicking on a button shouldn't close automatically the notification"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("can display a notification with a callback when closed", async (assert) => {
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
await mount(NotificationContainer, target, { env, props });
|
||||
|
||||
notifService.add("I'm a sticky notification", {
|
||||
sticky: true,
|
||||
onClose: () => {
|
||||
assert.step("Notification closed");
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
|
||||
// close by clicking on the close icon
|
||||
await click(target, ".o_notification .o_notification_close");
|
||||
assert.verifySteps(["Notification closed"]);
|
||||
assert.containsNone(target, ".o_notification");
|
||||
});
|
||||
|
||||
QUnit.test("notifications aren't sticky by default", async (assert) => {
|
||||
let timeoutCB;
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (cb) => {
|
||||
timeoutCB = cb;
|
||||
return 1;
|
||||
},
|
||||
});
|
||||
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
await mount(NotificationContainer, target, { env, props });
|
||||
|
||||
notifService.add("I'm a notification");
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
timeoutCB(); // should close the notification
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_notification");
|
||||
});
|
||||
|
||||
QUnit.test("can display a sticky notification", async (assert) => {
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: () => {
|
||||
throw new Error("Should not register a callback for sticky notifications");
|
||||
},
|
||||
});
|
||||
const env = await makeTestEnv({ browser, serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
await mount(NotificationContainer, target, { env, props });
|
||||
|
||||
notifService.add("I'm a sticky notification", { sticky: true });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
});
|
||||
|
||||
QUnit.test("can close sticky notification", async (assert) => {
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
await mount(NotificationContainer, target, { env, props });
|
||||
|
||||
const closeNotif = notifService.add("I'm a sticky notification", { sticky: true });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
|
||||
// close programmatically
|
||||
closeNotif();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_notification");
|
||||
|
||||
notifService.add("I'm a sticky notification", { sticky: true });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
|
||||
// close by clicking on the close icon
|
||||
await click(target, ".o_notification .o_notification_close");
|
||||
assert.containsNone(target, ".o_notification");
|
||||
});
|
||||
|
||||
// The timeout have to be done by the one that uses the notification service
|
||||
QUnit.skip("can close sticky notification with wait", async (assert) => {
|
||||
let timeoutCB;
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (cb, t) => {
|
||||
timeoutCB = cb;
|
||||
assert.step("time: " + t);
|
||||
return 1;
|
||||
},
|
||||
});
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
await mount(NotificationContainer, target, { env, props });
|
||||
|
||||
const id = notifService.create("I'm a sticky notification", { sticky: true });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
|
||||
// close programmatically
|
||||
notifService.close(id, 3000);
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
// simulate end of timeout
|
||||
timeoutCB();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_notification");
|
||||
assert.verifySteps(["time: 3000"]);
|
||||
});
|
||||
|
||||
QUnit.test("can close a non-sticky notification", async (assert) => {
|
||||
let timeoutCB;
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (cb) => {
|
||||
timeoutCB = cb;
|
||||
return 1;
|
||||
},
|
||||
});
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
await mount(NotificationContainer, target, { env, props });
|
||||
|
||||
const closeNotif = notifService.add("I'm a sticky notification");
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
|
||||
// close the notification
|
||||
closeNotif();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_notification");
|
||||
|
||||
// simulate end of timeout, which should try to close the notification as well
|
||||
timeoutCB();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_notification");
|
||||
});
|
||||
|
||||
QUnit.test("close a non-sticky notification while another one remains", async (assert) => {
|
||||
let timeoutCB;
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (cb) => {
|
||||
timeoutCB = cb;
|
||||
return 1;
|
||||
},
|
||||
});
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
await mount(NotificationContainer, target, { env, props });
|
||||
|
||||
const closeNotif1 = notifService.add("I'm a non-sticky notification");
|
||||
const closeNotif2 = notifService.add("I'm a sticky notification", { sticky: true });
|
||||
await nextTick();
|
||||
assert.containsN(target, ".o_notification", 2);
|
||||
|
||||
// close the non sticky notification
|
||||
closeNotif1();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
|
||||
// simulate end of timeout, which should try to close notification 1 as well
|
||||
timeoutCB();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
|
||||
// close the non sticky notification
|
||||
closeNotif2();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_notification");
|
||||
});
|
||||
|
||||
QUnit.test("notification coming when NotificationManager not mounted yet", async (assert) => {
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
const notifService = env.services.notification;
|
||||
mount(NotificationContainer, target, { env, props });
|
||||
|
||||
notifService.add("I'm a non-sticky notification");
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_notification");
|
||||
});
|
||||
|
|
@ -0,0 +1,418 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { getFixture, mount } from "../helpers/utils";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("ORM Service", {
|
||||
async beforeEach() {
|
||||
serviceRegistry.add("orm", ormService);
|
||||
},
|
||||
});
|
||||
|
||||
function makeFakeRPC() {
|
||||
const query = { route: null, params: null };
|
||||
const rpc = {
|
||||
start() {
|
||||
return async (route, params) => {
|
||||
query.route = route;
|
||||
query.params = params;
|
||||
};
|
||||
},
|
||||
};
|
||||
return [query, rpc];
|
||||
}
|
||||
|
||||
QUnit.test("add user context to a simple read request", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
await env.services.orm.read("my.model", [3], ["id", "descr"]);
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/my.model/read");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [[3], ["id", "descr"]],
|
||||
kwargs: {
|
||||
context: {
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "read",
|
||||
model: "my.model",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("context is combined with user context in read request", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
const context = { earth: "isfucked" };
|
||||
await env.services.orm.read("my.model", [3], ["id", "descr"], { context });
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/my.model/read");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [[3], ["id", "descr"]],
|
||||
kwargs: {
|
||||
context: {
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
earth: "isfucked",
|
||||
},
|
||||
},
|
||||
method: "read",
|
||||
model: "my.model",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("basic method call of model", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
await env.services.orm.call("partner", "test", [], { context: { a: 1 } });
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/partner/test");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [],
|
||||
kwargs: {
|
||||
context: {
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
a: 1,
|
||||
},
|
||||
},
|
||||
method: "test",
|
||||
model: "partner",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("create method", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
await env.services.orm.create("partner", [{ color: "red" }]);
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/partner/create");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [
|
||||
{
|
||||
color: "red",
|
||||
},
|
||||
],
|
||||
kwargs: {
|
||||
context: {
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "create",
|
||||
model: "partner",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("nameGet method", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
const context = { complete: true };
|
||||
await env.services.orm.nameGet("sale.order", [2, 5], { context });
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/sale.order/name_get");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [[2, 5]],
|
||||
kwargs: {
|
||||
context: {
|
||||
complete: true,
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "name_get",
|
||||
model: "sale.order",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("read method", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
const context = { abc: 3 };
|
||||
await env.services.orm.read("sale.order", [2, 5], ["name", "amount"], {
|
||||
load: "none",
|
||||
context,
|
||||
});
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/sale.order/read");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [
|
||||
[2, 5],
|
||||
["name", "amount"],
|
||||
],
|
||||
kwargs: {
|
||||
load: "none",
|
||||
context: {
|
||||
abc: 3,
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "read",
|
||||
model: "sale.order",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("unlink method", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
await env.services.orm.unlink("partner", [43]);
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/partner/unlink");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [[43]],
|
||||
kwargs: {
|
||||
context: {
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "unlink",
|
||||
model: "partner",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("write method", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
await env.services.orm.write("partner", [43, 14], { active: false });
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/partner/write");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [[43, 14], { active: false }],
|
||||
kwargs: {
|
||||
context: {
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "write",
|
||||
model: "partner",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("webReadGroup method", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
await env.services.orm.webReadGroup(
|
||||
"sale.order",
|
||||
[["user_id", "=", 2]],
|
||||
["amount_total:sum"],
|
||||
["date_order"],
|
||||
{ offset: 1 }
|
||||
);
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/sale.order/web_read_group");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [],
|
||||
kwargs: {
|
||||
domain: [["user_id", "=", 2]],
|
||||
fields: ["amount_total:sum"],
|
||||
groupby: ["date_order"],
|
||||
context: {
|
||||
lang: "en",
|
||||
uid: 7,
|
||||
tz: "taht",
|
||||
},
|
||||
offset: 1,
|
||||
},
|
||||
method: "web_read_group",
|
||||
model: "sale.order",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("readGroup method", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
await env.services.orm.readGroup(
|
||||
"sale.order",
|
||||
[["user_id", "=", 2]],
|
||||
["amount_total:sum"],
|
||||
["date_order"],
|
||||
{ offset: 1 }
|
||||
);
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/sale.order/read_group");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [],
|
||||
kwargs: {
|
||||
domain: [["user_id", "=", 2]],
|
||||
fields: ["amount_total:sum"],
|
||||
groupby: ["date_order"],
|
||||
context: {
|
||||
lang: "en",
|
||||
uid: 7,
|
||||
tz: "taht",
|
||||
},
|
||||
offset: 1,
|
||||
},
|
||||
method: "read_group",
|
||||
model: "sale.order",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("searchRead method", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
await env.services.orm.searchRead("sale.order", [["user_id", "=", 2]], ["amount_total"]);
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/sale.order/search_read");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [],
|
||||
kwargs: {
|
||||
context: {
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
domain: [["user_id", "=", 2]],
|
||||
fields: ["amount_total"],
|
||||
},
|
||||
method: "search_read",
|
||||
model: "sale.order",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("searchCount method", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
await env.services.orm.searchCount("sale.order", [["user_id", "=", 2]]);
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/sale.order/search_count");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [[["user_id", "=", 2]]],
|
||||
kwargs: {
|
||||
context: {
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "search_count",
|
||||
model: "sale.order",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("webSearchRead method", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
await env.services.orm.webSearchRead("sale.order", [["user_id", "=", 2]], ["amount_total"]);
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/sale.order/web_search_read");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [],
|
||||
kwargs: {
|
||||
context: {
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
domain: [["user_id", "=", 2]],
|
||||
fields: ["amount_total"],
|
||||
},
|
||||
method: "web_search_read",
|
||||
model: "sale.order",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("useModel is specialized for component", async (assert) => {
|
||||
const [, /* query */ rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.rpc = useService("rpc");
|
||||
this.orm = useService("orm");
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<div/>`;
|
||||
|
||||
const target = getFixture();
|
||||
const component = await mount(MyComponent, target, { env });
|
||||
assert.notStrictEqual(component.orm, env.services.orm);
|
||||
});
|
||||
|
||||
QUnit.test("silent mode", async (assert) => {
|
||||
serviceRegistry.add("rpc", {
|
||||
start() {
|
||||
return async (route, params, settings) => {
|
||||
assert.step(`${route}${settings.silent ? " (silent)" : ""}`);
|
||||
};
|
||||
},
|
||||
});
|
||||
const env = await makeTestEnv();
|
||||
const orm = env.services.orm;
|
||||
|
||||
orm.call("my_model", "my_method");
|
||||
orm.silent.call("my_model", "my_method");
|
||||
orm.call("my_model", "my_method");
|
||||
orm.read("my_model", [1], []);
|
||||
orm.silent.read("my_model", [1], []);
|
||||
orm.read("my_model", [1], []);
|
||||
|
||||
assert.verifySteps([
|
||||
"/web/dataset/call_kw/my_model/my_method",
|
||||
"/web/dataset/call_kw/my_model/my_method (silent)",
|
||||
"/web/dataset/call_kw/my_model/my_method",
|
||||
"/web/dataset/call_kw/my_model/read",
|
||||
"/web/dataset/call_kw/my_model/read (silent)",
|
||||
"/web/dataset/call_kw/my_model/read",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("validate some obviously wrong calls", async (assert) => {
|
||||
assert.expect(2);
|
||||
const [, /* query*/ rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
try {
|
||||
await env.services.orm.read(false, [3], ["id", "descr"]);
|
||||
} catch (error) {
|
||||
assert.strictEqual(error.message, "Invalid model name: false");
|
||||
}
|
||||
try {
|
||||
await env.services.orm.read("res.partner", false, ["id", "descr"]);
|
||||
} catch (error) {
|
||||
assert.strictEqual(error.message, "Invalid ids list: false");
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("optimize read and unlink if no ids", async (assert) => {
|
||||
serviceRegistry.add("rpc", {
|
||||
start() {
|
||||
return async (route) => {
|
||||
assert.step(route);
|
||||
};
|
||||
},
|
||||
});
|
||||
const env = await makeTestEnv();
|
||||
const orm = env.services.orm;
|
||||
|
||||
await orm.read("my_model", [1], []);
|
||||
assert.verifySteps(["/web/dataset/call_kw/my_model/read"]);
|
||||
|
||||
await orm.read("my_model", [], []);
|
||||
assert.verifySteps([]);
|
||||
|
||||
await orm.unlink("my_model", [1], {});
|
||||
assert.verifySteps(["/web/dataset/call_kw/my_model/unlink"]);
|
||||
|
||||
await orm.unlink("my_model", [], {});
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
411
odoo-bringout-oca-ocb-web/web/static/tests/core/pager_tests.js
Normal file
411
odoo-bringout-oca-ocb-web/web/static/tests/core/pager_tests.js
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Pager } from "@web/core/pager/pager";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import {
|
||||
click,
|
||||
triggerEvent,
|
||||
makeDeferred,
|
||||
mount,
|
||||
nextTick,
|
||||
getFixture,
|
||||
triggerEvents,
|
||||
} from "../helpers/utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
|
||||
class PagerController extends Component {
|
||||
setup() {
|
||||
this.state = useState({ ...this.props });
|
||||
}
|
||||
async updateProps(nextProps) {
|
||||
Object.assign(this.state, nextProps);
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
PagerController.template = xml`<Pager t-props="state" />`;
|
||||
PagerController.components = { Pager };
|
||||
|
||||
async function makePager(props) {
|
||||
serviceRegistry.add("ui", uiService);
|
||||
const env = await makeTestEnv();
|
||||
const pager = await mount(PagerController, target, { env, props });
|
||||
return pager;
|
||||
}
|
||||
|
||||
QUnit.module("Components", ({ beforeEach }) => {
|
||||
QUnit.module("Pager");
|
||||
|
||||
beforeEach(() => {
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.test("basic interactions", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const pager = await makePager({
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
onUpdate(data) {
|
||||
pager.updateProps(data);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(`.o_pager_counter .o_pager_value`).textContent.trim(),
|
||||
"1-4",
|
||||
"currentMinimum should be set to 1"
|
||||
);
|
||||
|
||||
await click(target.querySelector(`.o_pager button.o_pager_next`));
|
||||
assert.strictEqual(
|
||||
target.querySelector(`.o_pager_counter .o_pager_value`).textContent.trim(),
|
||||
"5-8",
|
||||
"currentMinimum should now be 5"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("edit the pager", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const pager = await makePager({
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
onUpdate(data) {
|
||||
pager.updateProps(data);
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_pager_value");
|
||||
|
||||
assert.containsOnce(target, "input", "the pager should contain an input");
|
||||
assert.strictEqual(
|
||||
target.querySelector(`.o_pager_counter .o_pager_value`).value,
|
||||
"1-4",
|
||||
"the input should have correct value"
|
||||
);
|
||||
|
||||
// change the limit
|
||||
const input = target.querySelector(`.o_pager_counter input.o_pager_value`);
|
||||
input.value = "1-6";
|
||||
await triggerEvents(input, null, ["change", "blur"]);
|
||||
|
||||
assert.containsNone(target, "input", "the pager should not contain an input anymore");
|
||||
assert.strictEqual(
|
||||
target.querySelector(`.o_pager_counter .o_pager_value`).textContent.trim(),
|
||||
"1-6",
|
||||
"the limit should have been updated"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("keydown on pager with same value", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
await makePager({
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
onUpdate() {
|
||||
assert.step("pager-changed");
|
||||
},
|
||||
});
|
||||
|
||||
// Enter edit mode
|
||||
await click(target, ".o_pager_value");
|
||||
|
||||
assert.containsOnce(target, "input");
|
||||
assert.strictEqual(target.querySelector(`.o_pager_counter .o_pager_value`).value, "1-4");
|
||||
assert.verifySteps([]);
|
||||
|
||||
// Exit edit mode
|
||||
await triggerEvent(target, "input", "keydown", { key: "Enter" });
|
||||
|
||||
assert.containsNone(target, "input");
|
||||
assert.strictEqual(
|
||||
target.querySelector(`.o_pager_counter .o_pager_value`).textContent.trim(),
|
||||
"1-4"
|
||||
);
|
||||
assert.verifySteps(["pager-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("pager value formatting", async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const pager = await makePager({
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
onUpdate(data) {
|
||||
pager.updateProps(data);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(`.o_pager_counter .o_pager_value`).textContent.trim(),
|
||||
"1-4",
|
||||
"Initial value should be correct"
|
||||
);
|
||||
|
||||
async function inputAndAssert(inputValue, expected, reason) {
|
||||
await click(target.querySelector(`.o_pager_counter .o_pager_value`));
|
||||
const inputEl = target.querySelector(`.o_pager_counter input.o_pager_value`);
|
||||
inputEl.value = inputValue;
|
||||
await triggerEvents(inputEl, null, ["change", "blur"]);
|
||||
assert.strictEqual(
|
||||
target.querySelector(`.o_pager_counter .o_pager_value`).textContent.trim(),
|
||||
expected,
|
||||
`Pager value should be "${expected}" when given "${inputValue}": ${reason}`
|
||||
);
|
||||
}
|
||||
|
||||
await inputAndAssert("4-4", "4", "values are squashed when minimum = maximum");
|
||||
await inputAndAssert("1-11", "1-10", "maximum is floored to total when out of range");
|
||||
await inputAndAssert("20-15", "10", "combination of the 2 assertions above");
|
||||
await inputAndAssert("6-5", "10", "fallback to previous value when minimum > maximum");
|
||||
await inputAndAssert(
|
||||
"definitelyValidNumber",
|
||||
"10",
|
||||
"fallback to previous value if not a number"
|
||||
);
|
||||
await inputAndAssert(
|
||||
" 1 , 2 ",
|
||||
"1-2",
|
||||
"value is normalized and accepts several separators"
|
||||
);
|
||||
await inputAndAssert("3 8", "3-8", "value accepts whitespace(s) as a separator");
|
||||
});
|
||||
|
||||
QUnit.test("pager disabling", async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
const reloadPromise = makeDeferred();
|
||||
|
||||
const pager = await makePager({
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
// The goal here is to test the reactivity of the pager; in a
|
||||
// typical views, we disable the pager after switching page
|
||||
// to avoid switching twice with the same action (double click).
|
||||
async onUpdate(data) {
|
||||
// 1. Simulate a (long) server action
|
||||
await reloadPromise;
|
||||
// 2. Update the view with loaded data
|
||||
pager.updateProps(data);
|
||||
},
|
||||
});
|
||||
const pagerButtons = target.querySelectorAll("button");
|
||||
|
||||
// Click twice
|
||||
await click(target.querySelector(`.o_pager button.o_pager_next`));
|
||||
await click(target.querySelector(`.o_pager button.o_pager_next`));
|
||||
// Try to edit the pager value
|
||||
await click(target, ".o_pager_value");
|
||||
|
||||
assert.strictEqual(pagerButtons.length, 2, "the two buttons should be displayed");
|
||||
assert.ok(pagerButtons[0].disabled, "'previous' is disabled");
|
||||
assert.ok(pagerButtons[1].disabled, "'next' is disabled");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_pager_value").tagName,
|
||||
"SPAN",
|
||||
"pager edition is prevented"
|
||||
);
|
||||
|
||||
// Server action is done
|
||||
reloadPromise.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(pagerButtons.length, 2, "the two buttons should be displayed");
|
||||
assert.notOk(pagerButtons[0].disabled, "'previous' is enabled");
|
||||
assert.notOk(pagerButtons[1].disabled, "'next' is enabled");
|
||||
assert.strictEqual(
|
||||
target.querySelector(`.o_pager_counter .o_pager_value`).textContent.trim(),
|
||||
"5-8",
|
||||
"value has been updated"
|
||||
);
|
||||
|
||||
await click(target, ".o_pager_value");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_pager_value").tagName,
|
||||
"INPUT",
|
||||
"pager edition is re-enabled"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("input interaction", async function (assert) {
|
||||
const pager = await makePager({
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
onUpdate(data) {
|
||||
pager.updateProps(data);
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_pager_value");
|
||||
assert.containsOnce(target, "input", "the pager should contain an input");
|
||||
assert.strictEqual(
|
||||
target.querySelector("input"),
|
||||
document.activeElement,
|
||||
"pager input is focused"
|
||||
);
|
||||
|
||||
await triggerEvent(target, null, "mousedown");
|
||||
assert.containsNone(target, "input", "the pager should not contain an input");
|
||||
});
|
||||
|
||||
QUnit.test("updateTotal props: click on total", async function (assert) {
|
||||
const pager = await makePager({
|
||||
offset: 0,
|
||||
limit: 5,
|
||||
total: 10,
|
||||
onUpdate() {},
|
||||
updateTotal() {
|
||||
pager.updateProps({ total: 25, updateTotal: undefined });
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-5");
|
||||
assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "10+");
|
||||
assert.hasClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch");
|
||||
|
||||
await click(target, ".o_pager_limit_fetch");
|
||||
assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-5");
|
||||
assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "25");
|
||||
assert.doesNotHaveClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch");
|
||||
});
|
||||
|
||||
QUnit.test("updateTotal props: click next", async function (assert) {
|
||||
let tempTotal = 10;
|
||||
const realTotal = 18;
|
||||
const pager = await makePager({
|
||||
offset: 0,
|
||||
limit: 5,
|
||||
total: tempTotal,
|
||||
onUpdate(data) {
|
||||
tempTotal = Math.min(realTotal, Math.max(tempTotal, data.offset + data.limit));
|
||||
const nextProps = { ...data, total: tempTotal };
|
||||
if (tempTotal === realTotal) {
|
||||
nextProps.updateTotal = undefined;
|
||||
}
|
||||
pager.updateProps(nextProps);
|
||||
},
|
||||
async updateTotal() {},
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-5");
|
||||
assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "10+");
|
||||
assert.hasClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch");
|
||||
|
||||
await click(target, ".o_pager_next");
|
||||
assert.strictEqual(target.querySelector(".o_pager_value").innerText, "6-10");
|
||||
assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "10+");
|
||||
assert.hasClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch");
|
||||
|
||||
await click(target, ".o_pager_next");
|
||||
assert.strictEqual(target.querySelector(".o_pager_value").innerText, "11-15");
|
||||
assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "15+");
|
||||
assert.hasClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch");
|
||||
|
||||
await click(target, ".o_pager_next");
|
||||
assert.strictEqual(target.querySelector(".o_pager_value").innerText, "16-18");
|
||||
assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "18");
|
||||
assert.doesNotHaveClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch");
|
||||
});
|
||||
|
||||
QUnit.test("updateTotal props: edit input", async function (assert) {
|
||||
let tempTotal = 10;
|
||||
const realTotal = 18;
|
||||
const pager = await makePager({
|
||||
offset: 0,
|
||||
limit: 5,
|
||||
total: tempTotal,
|
||||
onUpdate(data) {
|
||||
tempTotal = Math.min(realTotal, Math.max(tempTotal, data.offset + data.limit));
|
||||
const nextProps = { ...data, total: tempTotal };
|
||||
if (tempTotal === realTotal) {
|
||||
nextProps.updateTotal = undefined;
|
||||
}
|
||||
pager.updateProps(nextProps);
|
||||
},
|
||||
async updateTotal() {},
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-5");
|
||||
assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "10+");
|
||||
assert.hasClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch");
|
||||
|
||||
await click(target, ".o_pager_value");
|
||||
let input = target.querySelector(".o_pager_counter input.o_pager_value");
|
||||
input.value = "3-8";
|
||||
await triggerEvents(input, null, ["change", "blur"]);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_pager_value").innerText, "3-8");
|
||||
assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "10+");
|
||||
assert.hasClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch");
|
||||
|
||||
await click(target, ".o_pager_value");
|
||||
input = target.querySelector(".o_pager_counter input.o_pager_value");
|
||||
input.value = "3-20";
|
||||
await triggerEvents(input, null, ["change", "blur"]);
|
||||
assert.strictEqual(target.querySelector(".o_pager_value").innerText, "3-18");
|
||||
assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "18");
|
||||
assert.doesNotHaveClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch");
|
||||
});
|
||||
|
||||
QUnit.test("updateTotal props: can use next even if single page", async function (assert) {
|
||||
const pager = await makePager({
|
||||
offset: 0,
|
||||
limit: 5,
|
||||
total: 5,
|
||||
onUpdate(data) {
|
||||
pager.updateProps({ ...data, total: 10 });
|
||||
},
|
||||
async updateTotal() {},
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-5");
|
||||
assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "5+");
|
||||
assert.hasClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch");
|
||||
|
||||
await click(target, ".o_pager_next");
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_pager_value").innerText, "6-10");
|
||||
assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "10+");
|
||||
assert.hasClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch");
|
||||
});
|
||||
|
||||
QUnit.test("updateTotal props: click previous", async function (assert) {
|
||||
const pager = await makePager({
|
||||
offset: 0,
|
||||
limit: 5,
|
||||
total: 10,
|
||||
onUpdate(data) {
|
||||
pager.updateProps(data);
|
||||
},
|
||||
async updateTotal() {
|
||||
const total = 23;
|
||||
pager.updateProps({ total, updateTotal: undefined });
|
||||
return total;
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_pager_value").innerText, "1-5");
|
||||
assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "10+");
|
||||
assert.hasClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch");
|
||||
|
||||
await click(target, ".o_pager_previous");
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_pager_value").innerText, "21-23");
|
||||
assert.strictEqual(target.querySelector(".o_pager_limit").innerText, "23");
|
||||
assert.doesNotHaveClass(target.querySelector(".o_pager_limit"), "o_pager_limit_fetch");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
|
||||
import { destroy, getFixture, mount, nextTick } from "../../helpers/utils";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
let env;
|
||||
let target;
|
||||
let popoverTarget;
|
||||
|
||||
const mainComponents = registry.category("main_components");
|
||||
|
||||
class PseudoWebClient extends Component {
|
||||
setup() {
|
||||
this.Components = mainComponents.getEntries();
|
||||
}
|
||||
}
|
||||
PseudoWebClient.template = xml`
|
||||
<div>
|
||||
<div id="anchor">Anchor</div>
|
||||
<div id="close">Close</div>
|
||||
<div>
|
||||
<t t-foreach="Components" t-as="Component" t-key="Component[0]">
|
||||
<t t-component="Component[1].Component" t-props="Component[1].props"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
QUnit.module("Popover hook", {
|
||||
async beforeEach() {
|
||||
clearRegistryWithCleanup(mainComponents);
|
||||
registry.category("services").add("popover", popoverService);
|
||||
target = getFixture();
|
||||
env = await makeTestEnv();
|
||||
await mount(PseudoWebClient, target, { env });
|
||||
popoverTarget = target.querySelector("#anchor");
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("close popover when component is unmounted", async (assert) => {
|
||||
class Comp extends Component {}
|
||||
Comp.template = xml`<div t-att-id="props.id">in popover</div>`;
|
||||
|
||||
class CompWithPopover extends Component {
|
||||
setup() {
|
||||
this.popover = usePopover();
|
||||
}
|
||||
}
|
||||
CompWithPopover.template = xml`<div />`;
|
||||
|
||||
const comp1 = await mount(CompWithPopover, target, { env });
|
||||
comp1.popover.add(popoverTarget, Comp, { id: "comp1" });
|
||||
await nextTick();
|
||||
|
||||
const comp2 = await mount(CompWithPopover, target, { env });
|
||||
comp2.popover.add(popoverTarget, Comp, { id: "comp2" });
|
||||
await nextTick();
|
||||
|
||||
assert.containsN(target, ".o_popover", 2);
|
||||
assert.containsOnce(target, ".o_popover #comp1");
|
||||
assert.containsOnce(target, ".o_popover #comp2");
|
||||
|
||||
destroy(comp1);
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_popover");
|
||||
assert.containsNone(target, ".o_popover #comp1");
|
||||
assert.containsOnce(target, ".o_popover #comp2");
|
||||
|
||||
destroy(comp2);
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(target, ".o_popover");
|
||||
assert.containsNone(target, ".o_popover #comp1");
|
||||
assert.containsNone(target, ".o_popover #comp2");
|
||||
});
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
|
||||
import { click, getFixture, mount, nextTick } from "../../helpers/utils";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
let env;
|
||||
let fixture;
|
||||
let popovers;
|
||||
let popoverTarget;
|
||||
|
||||
const mainComponents = registry.category("main_components");
|
||||
|
||||
class PseudoWebClient extends Component {
|
||||
setup() {
|
||||
this.Components = mainComponents.getEntries();
|
||||
}
|
||||
}
|
||||
PseudoWebClient.template = xml`
|
||||
<div>
|
||||
<div id="anchor">Anchor</div>
|
||||
<div id="close">Close</div>
|
||||
<div id="sibling">Sibling</div>
|
||||
<div>
|
||||
<t t-foreach="Components" t-as="C" t-key="C[0]">
|
||||
<t t-component="C[1].Component" t-props="C[1].props"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
QUnit.module("Popover service", {
|
||||
async beforeEach() {
|
||||
clearRegistryWithCleanup(mainComponents);
|
||||
registry.category("services").add("popover", popoverService);
|
||||
|
||||
fixture = getFixture();
|
||||
env = await makeTestEnv();
|
||||
await mount(PseudoWebClient, fixture, { env });
|
||||
popovers = env.services.popover;
|
||||
popoverTarget = fixture.querySelector("#anchor");
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("simple use", async (assert) => {
|
||||
assert.containsOnce(fixture, ".o_popover_container");
|
||||
|
||||
class Comp extends Component {}
|
||||
Comp.template = xml`<div id="comp">in popover</div>`;
|
||||
|
||||
assert.containsNone(fixture, ".o_popover");
|
||||
|
||||
const remove = popovers.add(popoverTarget, Comp, {});
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(fixture, ".o_popover");
|
||||
assert.containsOnce(fixture, ".o_popover #comp");
|
||||
|
||||
remove();
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(fixture, ".o_popover");
|
||||
assert.containsNone(fixture, ".o_popover #comp");
|
||||
});
|
||||
|
||||
QUnit.test("close on click away", async (assert) => {
|
||||
assert.containsOnce(fixture, ".o_popover_container");
|
||||
|
||||
class Comp extends Component {}
|
||||
Comp.template = xml`<div id="comp">in popover</div>`;
|
||||
|
||||
popovers.add(popoverTarget, Comp, {});
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(fixture, ".o_popover");
|
||||
assert.containsOnce(fixture, ".o_popover #comp");
|
||||
|
||||
await click(fixture, "#close");
|
||||
|
||||
assert.containsNone(fixture, ".o_popover");
|
||||
assert.containsNone(fixture, ".o_popover #comp");
|
||||
});
|
||||
|
||||
QUnit.test("do not close on click away", async (assert) => {
|
||||
assert.containsOnce(fixture, ".o_popover_container");
|
||||
|
||||
class Comp extends Component {}
|
||||
Comp.template = xml`<div id="comp">in popover</div>`;
|
||||
|
||||
const remove = popovers.add(popoverTarget, Comp, {}, { closeOnClickAway: false });
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(fixture, ".o_popover");
|
||||
assert.containsOnce(fixture, ".o_popover #comp");
|
||||
|
||||
await click(fixture, "#close");
|
||||
|
||||
assert.containsOnce(fixture, ".o_popover");
|
||||
assert.containsOnce(fixture, ".o_popover #comp");
|
||||
|
||||
remove();
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(fixture, ".o_popover");
|
||||
assert.containsNone(fixture, ".o_popover #comp");
|
||||
});
|
||||
|
||||
QUnit.test("close callback", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
assert.containsOnce(fixture, ".o_popover_container");
|
||||
|
||||
class Comp extends Component {}
|
||||
Comp.template = xml`<div id="comp">in popover</div>`;
|
||||
|
||||
function onClose() {
|
||||
assert.step("close");
|
||||
}
|
||||
|
||||
popovers.add(popoverTarget, Comp, {}, { onClose });
|
||||
await nextTick();
|
||||
|
||||
await click(fixture, "#close");
|
||||
|
||||
assert.verifySteps(["close"]);
|
||||
});
|
||||
|
||||
QUnit.test("sub component triggers close", async (assert) => {
|
||||
assert.containsOnce(fixture, ".o_popover_container");
|
||||
|
||||
class Comp extends Component {}
|
||||
Comp.template = xml`<div id="comp" t-on-click="() => this.props.close()">in popover</div>`;
|
||||
|
||||
popovers.add(popoverTarget, Comp, {});
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(fixture, ".o_popover");
|
||||
assert.containsOnce(fixture, ".o_popover #comp");
|
||||
|
||||
await click(fixture, "#comp");
|
||||
|
||||
assert.containsNone(fixture, ".o_popover");
|
||||
assert.containsNone(fixture, ".o_popover #comp");
|
||||
});
|
||||
|
||||
QUnit.test("close popover if target is removed", async (assert) => {
|
||||
assert.containsOnce(fixture, ".o_popover_container");
|
||||
|
||||
class Comp extends Component {}
|
||||
Comp.template = xml`<div id="comp">in popover</div>`;
|
||||
|
||||
popovers.add(popoverTarget, Comp, {});
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(fixture, ".o_popover");
|
||||
assert.containsOnce(fixture, ".o_popover #comp");
|
||||
|
||||
popoverTarget.remove();
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(fixture, ".o_popover");
|
||||
assert.containsNone(fixture, ".o_popover #comp");
|
||||
});
|
||||
|
||||
QUnit.test("close and do not crash if target parent does not exist", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
// This target does not have any parent, it simulates the case where the element disappeared
|
||||
// from the DOM before the setup of the component
|
||||
const dissapearedTarget = document.createElement("div");
|
||||
|
||||
assert.containsOnce(fixture, ".o_popover_container");
|
||||
|
||||
class Comp extends Component {}
|
||||
Comp.template = xml`<div id="comp">in popover</div>`;
|
||||
|
||||
function onClose() {
|
||||
assert.step("close");
|
||||
}
|
||||
|
||||
popovers.add(dissapearedTarget, Comp, {}, { onClose });
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["close"]);
|
||||
});
|
||||
|
||||
QUnit.test("keep popover if target sibling is removed", async (assert) => {
|
||||
assert.containsOnce(fixture, ".o_popover_container");
|
||||
|
||||
class Comp extends Component {}
|
||||
Comp.template = xml`<div id="comp">in popover</div>`;
|
||||
|
||||
popovers.add(popoverTarget, Comp, {});
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(fixture, ".o_popover");
|
||||
assert.containsOnce(fixture, ".o_popover #comp");
|
||||
|
||||
fixture.querySelector("#sibling").remove();
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(fixture, ".o_popover");
|
||||
assert.containsOnce(fixture, ".o_popover #comp");
|
||||
});
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Popover } from "@web/core/popover/popover";
|
||||
import { usePosition } from "@web/core/position_hook";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
import { getFixture, makeDeferred, mount, nextTick, triggerEvent } from "../../helpers/utils";
|
||||
|
||||
let fixture;
|
||||
let popoverTarget;
|
||||
|
||||
QUnit.module("Popover", {
|
||||
async beforeEach() {
|
||||
fixture = getFixture();
|
||||
|
||||
popoverTarget = document.createElement("div");
|
||||
popoverTarget.id = "target";
|
||||
fixture.appendChild(popoverTarget);
|
||||
|
||||
registerCleanup(() => {
|
||||
popoverTarget.remove();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("popover can have custom class", async (assert) => {
|
||||
await mount(Popover, fixture, {
|
||||
props: { target: popoverTarget, popoverClass: "custom-popover" },
|
||||
});
|
||||
|
||||
assert.containsOnce(fixture, ".o_popover.custom-popover");
|
||||
});
|
||||
|
||||
QUnit.test("popover can have more than one custom class", async (assert) => {
|
||||
await mount(Popover, fixture, {
|
||||
props: { target: popoverTarget, popoverClass: "custom-popover popover-custom" },
|
||||
});
|
||||
|
||||
assert.containsOnce(fixture, ".o_popover.custom-popover.popover-custom");
|
||||
});
|
||||
|
||||
QUnit.test("popover is rendered nearby target (default)", async (assert) => {
|
||||
assert.expect(1);
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction }) {
|
||||
assert.equal(direction, "bottom");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
props: { target: popoverTarget },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("popover is rendered nearby target (bottom)", async (assert) => {
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction }) {
|
||||
assert.equal(direction, "bottom");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
props: { target: popoverTarget, position: "bottom" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("popover is rendered nearby target (top)", async (assert) => {
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction }) {
|
||||
assert.equal(direction, "top");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
props: { target: popoverTarget, position: "top" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("popover is rendered nearby target (left)", async (assert) => {
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction }) {
|
||||
assert.equal(direction, "left");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
props: { target: popoverTarget, position: "left" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("popover is rendered nearby target (right)", async (assert) => {
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction }) {
|
||||
assert.equal(direction, "right");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
props: { target: popoverTarget, position: "right" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("reposition popover should properly change classNames", async (assert) => {
|
||||
// Force some style, to make this test independent of screen size
|
||||
const container = document.createElement("div");
|
||||
container.id = "container";
|
||||
container.style.backgroundColor = "pink";
|
||||
container.style.height = "450px";
|
||||
container.style.width = "450px";
|
||||
container.style.display = "flex";
|
||||
container.style.alignItems = "center";
|
||||
container.style.justifyContent = "center";
|
||||
popoverTarget.style.backgroundColor = "yellow";
|
||||
popoverTarget.style.height = "50px";
|
||||
popoverTarget.style.width = "50px";
|
||||
container.appendChild(popoverTarget);
|
||||
const sheet = document.createElement("style");
|
||||
sheet.textContent = `
|
||||
[role=tooltip] {
|
||||
background-color: cyan;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
`;
|
||||
fixture.appendChild(container);
|
||||
document.head.appendChild(sheet);
|
||||
registerCleanup(() => {
|
||||
container.remove();
|
||||
sheet.remove();
|
||||
});
|
||||
|
||||
const TestPopover = class extends Popover {
|
||||
setup() {
|
||||
// Don't call super.setup() in order to replace the use of usePosition hook...
|
||||
usePosition(this.props.target, {
|
||||
container,
|
||||
onPositioned: this.onPositioned.bind(this),
|
||||
position: this.props.position,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
await mount(TestPopover, container, { props: { target: popoverTarget } });
|
||||
const popover = container.querySelector("[role=tooltip]");
|
||||
const arrow = popover.firstElementChild;
|
||||
|
||||
// Should have classes for a "bottom-middle" placement
|
||||
assert.strictEqual(
|
||||
popover.className,
|
||||
"o_popover popover mw-25 shadow-sm bs-popover-bottom o-popover-bottom o-popover--bm"
|
||||
);
|
||||
assert.strictEqual(arrow.className, "popover-arrow start-0 end-0 mx-auto");
|
||||
|
||||
// Change container style and force update
|
||||
container.style.height = "125px"; // height of popper + 1/2 reference
|
||||
container.style.alignItems = "flex-end";
|
||||
triggerEvent(document, null, "scroll");
|
||||
await nextTick();
|
||||
|
||||
// Should have classes for a "right-end" placement
|
||||
assert.strictEqual(
|
||||
popover.className,
|
||||
"o_popover popover mw-25 shadow-sm bs-popover-end o-popover-right o-popover--re"
|
||||
);
|
||||
assert.strictEqual(arrow.className, "popover-arrow top-auto");
|
||||
});
|
||||
|
||||
QUnit.test("within iframe", async (assert) => {
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.style.height = "200px";
|
||||
iframe.srcdoc = `<div id="target" style="height:400px;">Within iframe</div>`;
|
||||
const def = makeDeferred();
|
||||
iframe.onload = def.resolve;
|
||||
fixture.appendChild(iframe);
|
||||
await def;
|
||||
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction }) {
|
||||
assert.step(direction);
|
||||
}
|
||||
};
|
||||
|
||||
popoverTarget = iframe.contentDocument.getElementById("target");
|
||||
await mount(TestPopover, fixture, {
|
||||
props: { target: popoverTarget },
|
||||
});
|
||||
assert.verifySteps(["bottom"]);
|
||||
|
||||
// The popover should be rendered outside the iframe
|
||||
assert.containsOnce(fixture, ".o_popover");
|
||||
assert.strictEqual(
|
||||
iframe.contentDocument.documentElement.querySelectorAll(".o_popover").length,
|
||||
0
|
||||
);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,476 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
import { patchDate, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { PyDate, PyTimeDelta } from "@web/core/py_js/py_date";
|
||||
|
||||
QUnit.module("py", {}, () => {
|
||||
QUnit.module("date stuff", () => {
|
||||
QUnit.module("time");
|
||||
|
||||
function check(expr, fn) {
|
||||
const d0 = new Date();
|
||||
const result = evaluateExpr(expr);
|
||||
const d1 = new Date();
|
||||
return fn(d0) <= result && result <= fn(d1);
|
||||
}
|
||||
const format = (n) => String(n).padStart(2, "0");
|
||||
const formatDate = (d) => {
|
||||
const year = d.getFullYear();
|
||||
const month = format(d.getMonth() + 1);
|
||||
const day = format(d.getDate());
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
const formatDateTime = (d) => {
|
||||
const h = format(d.getHours());
|
||||
const m = format(d.getMinutes());
|
||||
const s = format(d.getSeconds());
|
||||
return `${formatDate(d)} ${h}:${m}:${s}`;
|
||||
};
|
||||
|
||||
QUnit.test("strftime", (assert) => {
|
||||
assert.ok(check("time.strftime('%Y')", (d) => String(d.getFullYear())));
|
||||
assert.ok(
|
||||
check("time.strftime('%Y') + '-01-30'", (d) => String(d.getFullYear()) + "-01-30")
|
||||
);
|
||||
assert.ok(check("time.strftime('%Y-%m-%d %H:%M:%S')", formatDateTime));
|
||||
});
|
||||
|
||||
QUnit.module("datetime.datetime");
|
||||
|
||||
QUnit.test("datetime.datetime.now", (assert) => {
|
||||
assert.ok(check("datetime.datetime.now().year", (d) => d.getFullYear()));
|
||||
assert.ok(check("datetime.datetime.now().month", (d) => d.getMonth() + 1));
|
||||
assert.ok(check("datetime.datetime.now().day", (d) => d.getDate()));
|
||||
assert.ok(check("datetime.datetime.now().hour", (d) => d.getHours()));
|
||||
assert.ok(check("datetime.datetime.now().minute", (d) => d.getMinutes()));
|
||||
assert.ok(check("datetime.datetime.now().second", (d) => d.getSeconds()));
|
||||
});
|
||||
|
||||
QUnit.test("various operations", (assert) => {
|
||||
const expr1 = "datetime.datetime(day=3,month=4,year=2001).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr1), "2001-04-03");
|
||||
const expr2 = "datetime.datetime(2001, 4, 3).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr2), "2001-04-03");
|
||||
const expr3 =
|
||||
"datetime.datetime(day=3,month=4,second=12, year=2001,minute=32).strftime('%Y-%m-%d %H:%M:%S')";
|
||||
assert.strictEqual(evaluateExpr(expr3), "2001-04-03 00:32:12");
|
||||
});
|
||||
|
||||
QUnit.test("to_utc", (assert) => {
|
||||
patchDate(2021, 8, 17, 10, 0, 0);
|
||||
patchWithCleanup(Date.prototype, {
|
||||
getTimezoneOffset() {
|
||||
return -360;
|
||||
},
|
||||
});
|
||||
|
||||
const expr =
|
||||
"datetime.datetime.combine(context_today(), datetime.time(0,0,0)).to_utc()";
|
||||
|
||||
assert.strictEqual(JSON.stringify(evaluateExpr(expr)), `"2021-09-16 18:00:00"`);
|
||||
});
|
||||
|
||||
QUnit.test("to_utc in october with winter/summer change", (assert) => {
|
||||
patchDate(2021, 9, 17, 10, 0, 0);
|
||||
patchWithCleanup(Date.prototype, {
|
||||
getTimezoneOffset() {
|
||||
const month = this.getMonth() // starts at 0;
|
||||
if (10 <= month || month <= 2) {
|
||||
//rough approximation
|
||||
return -60;
|
||||
} else {
|
||||
return -120;
|
||||
}
|
||||
},
|
||||
});
|
||||
const expr =
|
||||
"datetime.datetime(2022, 10, 17).to_utc()";
|
||||
assert.strictEqual(JSON.stringify(evaluateExpr(expr)), `"2022-10-16 22:00:00"`);
|
||||
});
|
||||
|
||||
QUnit.test("datetime.datetime.combine", (assert) => {
|
||||
const expr =
|
||||
"datetime.datetime.combine(context_today(), datetime.time(23,59,59)).strftime('%Y-%m-%d %H:%M:%S')";
|
||||
assert.ok(
|
||||
check(expr, (d) => {
|
||||
return formatDate(d) + " 23:59:59";
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("datetime.datetime.toJSON", (assert) => {
|
||||
assert.strictEqual(
|
||||
JSON.stringify(evaluateExpr("datetime.datetime(day=3,month=4,year=2001,hour=10)")),
|
||||
`"2001-04-03 10:00:00"`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("datetime + timedelta", function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
assert.strictEqual(
|
||||
evaluateExpr(
|
||||
"(datetime.datetime(2017, 2, 15, 1, 7, 31) + datetime.timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"
|
||||
),
|
||||
"2017-02-16 01:07:31"
|
||||
);
|
||||
assert.strictEqual(
|
||||
evaluateExpr(
|
||||
"(datetime.datetime(2012, 2, 15, 1, 7, 31) - datetime.timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')"
|
||||
),
|
||||
"2012-02-15 00:07:31"
|
||||
);
|
||||
assert.strictEqual(
|
||||
evaluateExpr(
|
||||
"(datetime.datetime(2012, 2, 15, 1, 7, 31) + datetime.timedelta(hours=-1)).strftime('%Y-%m-%d %H:%M:%S')"
|
||||
),
|
||||
"2012-02-15 00:07:31"
|
||||
);
|
||||
assert.strictEqual(
|
||||
evaluateExpr(
|
||||
"(datetime.datetime(2012, 2, 15, 1, 7, 31) + datetime.timedelta(minutes=100)).strftime('%Y-%m-%d %H:%M:%S')"
|
||||
),
|
||||
"2012-02-15 02:47:31"
|
||||
);
|
||||
assert.strictEqual(
|
||||
evaluateExpr(
|
||||
"(datetime.date(day=3,month=4,year=2001) + datetime.timedelta(days=-1)).strftime('%Y-%m-%d')"
|
||||
),
|
||||
"2001-04-02"
|
||||
);
|
||||
assert.strictEqual(
|
||||
evaluateExpr(
|
||||
"(datetime.timedelta(days=-1) + datetime.date(day=3,month=4,year=2001)).strftime('%Y-%m-%d')"
|
||||
),
|
||||
"2001-04-02"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.module("datetime.date");
|
||||
|
||||
QUnit.test("datetime.date.today", (assert) => {
|
||||
assert.ok(check("(datetime.date.today()).strftime('%Y-%m-%d')", formatDate));
|
||||
});
|
||||
|
||||
QUnit.test("various operations", (assert) => {
|
||||
const expr1 = "datetime.date(day=3,month=4,year=2001).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr1), "2001-04-03");
|
||||
const expr2 = "datetime.date(2001, 4, 3).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr2), "2001-04-03");
|
||||
});
|
||||
|
||||
QUnit.test("datetime.date.toJSON", (assert) => {
|
||||
assert.strictEqual(
|
||||
JSON.stringify(evaluateExpr("datetime.date(year=1997,month=5,day=18)")),
|
||||
`"1997-05-18"`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("basic operations with dates", function (assert) {
|
||||
assert.expect(19);
|
||||
|
||||
let ctx = {
|
||||
d1: PyDate.create(2002, 1, 31),
|
||||
d2: PyDate.create(1956, 1, 31),
|
||||
};
|
||||
|
||||
assert.strictEqual(evaluateExpr("(d1 - d2).days", ctx), 46 * 365 + 12);
|
||||
assert.strictEqual(evaluateExpr("(d1 - d2).seconds", ctx), 0);
|
||||
assert.strictEqual(evaluateExpr("(d1 - d2).microseconds", ctx), 0);
|
||||
|
||||
ctx = {
|
||||
a: PyDate.create(2002, 3, 2),
|
||||
day: PyTimeDelta.create({ days: 1 }),
|
||||
week: PyTimeDelta.create({ days: 7 }),
|
||||
date: PyDate,
|
||||
};
|
||||
|
||||
assert.ok(evaluateExpr("a + day == date(2002, 3, 3)", ctx));
|
||||
assert.ok(evaluateExpr("day + a == date(2002, 3, 3)", ctx)); // 5
|
||||
assert.ok(evaluateExpr("a - day == date(2002, 3, 1)", ctx));
|
||||
assert.ok(evaluateExpr("-day + a == date(2002, 3, 1)", ctx));
|
||||
assert.ok(evaluateExpr("a + week == date(2002, 3, 9)", ctx));
|
||||
assert.ok(evaluateExpr("a - week == date(2002, 2, 23)", ctx));
|
||||
assert.ok(evaluateExpr("a + 52*week == date(2003, 3, 1)", ctx)); // 10
|
||||
assert.ok(evaluateExpr("a - 52*week == date(2001, 3, 3)", ctx));
|
||||
assert.ok(evaluateExpr("(a + week) - a == week", ctx));
|
||||
assert.ok(evaluateExpr("(a + day) - a == day", ctx));
|
||||
assert.ok(evaluateExpr("(a - week) - a == -week", ctx));
|
||||
assert.ok(evaluateExpr("(a - day) - a == -day", ctx)); // 15
|
||||
assert.ok(evaluateExpr("a - (a + week) == -week", ctx));
|
||||
assert.ok(evaluateExpr("a - (a + day) == -day", ctx));
|
||||
assert.ok(evaluateExpr("a - (a - week) == week", ctx));
|
||||
assert.ok(evaluateExpr("a - (a - day) == day", ctx));
|
||||
|
||||
// assert.throws(function () {
|
||||
// evaluateExpr("a + 1", ctx);
|
||||
// }, /^Error: TypeError:/); //20
|
||||
// assert.throws(function () {
|
||||
// evaluateExpr("a - 1", ctx);
|
||||
// }, /^Error: TypeError:/);
|
||||
// assert.throws(function () {
|
||||
// evaluateExpr("1 + a", ctx);
|
||||
// }, /^Error: TypeError:/);
|
||||
// assert.throws(function () {
|
||||
// evaluateExpr("1 - a", ctx);
|
||||
// }, /^Error: TypeError:/);
|
||||
|
||||
// // delta - date is senseless.
|
||||
// assert.throws(function () {
|
||||
// evaluateExpr("day - a", ctx);
|
||||
// }, /^Error: TypeError:/);
|
||||
// // mixing date and (delta or date) via * or // is senseless
|
||||
// assert.throws(function () {
|
||||
// evaluateExpr("day * a", ctx);
|
||||
// }, /^Error: TypeError:/); // 25
|
||||
// assert.throws(function () {
|
||||
// evaluateExpr("a * day", ctx);
|
||||
// }, /^Error: TypeError:/);
|
||||
// assert.throws(function () {
|
||||
// evaluateExpr("day // a", ctx);
|
||||
// }, /^Error: TypeError:/);
|
||||
// assert.throws(function () {
|
||||
// evaluateExpr("a // day", ctx);
|
||||
// }, /^Error: TypeError:/);
|
||||
// assert.throws(function () {
|
||||
// evaluateExpr("a * a", ctx);
|
||||
// }, /^Error: TypeError:/);
|
||||
// assert.throws(function () {
|
||||
// evaluateExpr("a // a", ctx);
|
||||
// }, /^Error: TypeError:/); // 30
|
||||
// // date + date is senseless
|
||||
// assert.throws(function () {
|
||||
// evaluateExpr("a + a", ctx);
|
||||
// }, /^Error: TypeError:/);
|
||||
});
|
||||
|
||||
QUnit.module("datetime.time");
|
||||
|
||||
QUnit.test("various operations", (assert) => {
|
||||
const expr1 = "datetime.time(hour=3,minute=2. second=1).strftime('%H:%M:%S')";
|
||||
assert.strictEqual(evaluateExpr(expr1), "03:02:01");
|
||||
});
|
||||
|
||||
QUnit.test("attributes", (assert) => {
|
||||
const expr1 = "datetime.time(hour=3,minute=2. second=1).hour";
|
||||
assert.strictEqual(evaluateExpr(expr1), 3);
|
||||
const expr2 = "datetime.time(hour=3,minute=2. second=1).minute";
|
||||
assert.strictEqual(evaluateExpr(expr2), 2);
|
||||
const expr3 = "datetime.time(hour=3,minute=2. second=1).second";
|
||||
assert.strictEqual(evaluateExpr(expr3), 1);
|
||||
});
|
||||
|
||||
QUnit.test("datetime.time.toJSON", (assert) => {
|
||||
assert.strictEqual(
|
||||
JSON.stringify(evaluateExpr("datetime.time(hour=11,minute=45,second=15)")),
|
||||
`"11:45:15"`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.module("relativedelta relative : period is plural", () => {
|
||||
QUnit.test("adding date and relative delta", (assert) => {
|
||||
const expr1 =
|
||||
"(datetime.date(day=3,month=4,year=2001) + relativedelta(days=-1)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr1), "2001-04-02");
|
||||
const expr2 =
|
||||
"(datetime.date(day=3,month=4,year=2001) + relativedelta(weeks=-1)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr2), "2001-03-27");
|
||||
});
|
||||
|
||||
QUnit.test("adding relative delta and date", (assert) => {
|
||||
const expr =
|
||||
"(relativedelta(days=-1) + datetime.date(day=3,month=4,year=2001)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr), "2001-04-02");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"adding/substracting relative delta and date -- shifts order of magnitude",
|
||||
(assert) => {
|
||||
const expr =
|
||||
"(relativedelta(hours=14) + datetime.datetime(hour=15,day=3,month=4,year=2001)).strftime('%Y-%m-%d %H:%M:%S')";
|
||||
assert.strictEqual(evaluateExpr(expr), "2001-04-04 05:00:00");
|
||||
|
||||
const expr2 =
|
||||
"(relativedelta(days=32) + datetime.date(day=3,month=4,year=2001)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr2), "2001-05-05");
|
||||
|
||||
const expr3 =
|
||||
"(relativedelta(months=14) + datetime.date(day=3,month=4,year=2001)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr3), "2002-06-03");
|
||||
|
||||
const expr4 =
|
||||
"(datetime.datetime(hour=13,day=3,month=4,year=2001) - relativedelta(hours=14)).strftime('%Y-%m-%d %H:%M:%S')";
|
||||
assert.strictEqual(evaluateExpr(expr4), "2001-04-02 23:00:00");
|
||||
|
||||
const expr5 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(days=4)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr5), "2001-03-30");
|
||||
|
||||
const expr6 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(months=5)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr6), "2000-11-03");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("substracting date and relative delta", (assert) => {
|
||||
const expr1 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(days=-1)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr1), "2001-04-04");
|
||||
const expr2 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(weeks=-1)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr2), "2001-04-10");
|
||||
const expr3 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(days=1)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr3), "2001-04-02");
|
||||
const expr4 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(weeks=1)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr4), "2001-03-27");
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("relativedelta absolute : period is singular", () => {
|
||||
QUnit.test("throws when period negative", (assert) => {
|
||||
const matcher = (errorMessage) => {
|
||||
return function match(err) {
|
||||
return err.message === errorMessage;
|
||||
};
|
||||
};
|
||||
|
||||
const expr1 = "relativedelta(day=-1)";
|
||||
assert.throws(() => evaluateExpr(expr1), matcher("day -1 is out of range"));
|
||||
|
||||
const expr2 = "relativedelta(month=-1)";
|
||||
assert.throws(() => evaluateExpr(expr2), matcher("month -1 is out of range"));
|
||||
});
|
||||
|
||||
QUnit.test("adding date and relative delta", (assert) => {
|
||||
const expr1 =
|
||||
"(datetime.date(day=3,month=4,year=2001) + relativedelta(day=1)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr1), "2001-04-01");
|
||||
|
||||
const expr2 =
|
||||
"(datetime.date(day=3,month=4,year=2001) + relativedelta(month=1)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr2), "2001-01-03");
|
||||
|
||||
const expr3 =
|
||||
"(datetime.date(2021,10,1) + relativedelta(hours=12)).strftime('%Y-%m-%d %H:%M:%S')";
|
||||
assert.strictEqual(evaluateExpr(expr3), "2021-10-01 12:00:00");
|
||||
|
||||
const expr4 =
|
||||
"(datetime.date(2021,10,1) + relativedelta(day=15,days=3)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr4), "2021-10-18");
|
||||
|
||||
const expr5 =
|
||||
"(datetime.date(2021,10,1) - relativedelta(day=15,days=3)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr5), "2021-10-12");
|
||||
|
||||
const expr6 =
|
||||
"(datetime.date(2021,10,1) + relativedelta(day=15,days=3,hours=24)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr6), "2021-10-19");
|
||||
});
|
||||
|
||||
QUnit.test("adding relative delta and date", (assert) => {
|
||||
const expr =
|
||||
"(relativedelta(day=1) + datetime.date(day=3,month=4,year=2001)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr), "2001-04-01");
|
||||
});
|
||||
|
||||
QUnit.test("substracting date and relative delta", (assert) => {
|
||||
const expr1 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(day=1)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr1), "2001-04-01");
|
||||
|
||||
const expr3 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(day=1)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr3), "2001-04-01");
|
||||
});
|
||||
|
||||
QUnit.test("type of date + relative delta", (assert) => {
|
||||
const expr1 = "(datetime.date(2021,10,1) + relativedelta(day=15,days=3,hours=24))";
|
||||
assert.ok(evaluateExpr(expr1) instanceof PyDate);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("relative delta weekday", () => {
|
||||
QUnit.test("add or substract weekday", (assert) => {
|
||||
const expr1 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(day=1, weekday=3)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr1), "2001-04-05");
|
||||
|
||||
const expr2 =
|
||||
"(datetime.date(day=29,month=4,year=2001) - relativedelta(weekday=4)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr2), "2001-05-04");
|
||||
|
||||
const expr3 =
|
||||
"(datetime.date(day=6,month=4,year=2001) - relativedelta(weekday=0)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr3), "2001-04-09");
|
||||
|
||||
const expr4 =
|
||||
"(datetime.date(day=1,month=4,year=2001) + relativedelta(weekday=-2)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr4), "2001-04-07");
|
||||
|
||||
const expr5 =
|
||||
"(datetime.date(day=11,month=4,year=2001) + relativedelta(weekday=2)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr5), "2001-04-11");
|
||||
|
||||
const expr6 =
|
||||
"(datetime.date(day=11,month=4,year=2001) + relativedelta(weekday=-2)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr6), "2001-04-14");
|
||||
|
||||
const expr7 =
|
||||
"(datetime.date(day=11,month=4,year=2001) + relativedelta(weekday=0)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr7), "2001-04-16");
|
||||
|
||||
const expr8 =
|
||||
"(datetime.date(day=11,month=4,year=2001) + relativedelta(weekday=1)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr8), "2001-04-17");
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("relative delta yearday nlyearday", () => {
|
||||
QUnit.test("yearday", (assert) => {
|
||||
const expr1 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(year=2000, yearday=60)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr1), "2000-02-29");
|
||||
|
||||
const expr2 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(yearday=60)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr2), "2001-03-01");
|
||||
|
||||
const expr3 =
|
||||
"(datetime.date(1999,12,31) + relativedelta(days=1, yearday=60)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr3), "1999-03-02");
|
||||
});
|
||||
|
||||
QUnit.test("nlyearday", (assert) => {
|
||||
const expr1 =
|
||||
"(datetime.date(day=3,month=4,year=2001) + relativedelta(year=2000, nlyearday=60)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr1), "2000-03-01");
|
||||
|
||||
const expr2 =
|
||||
"(datetime.date(day=3,month=4,year=2001) + relativedelta(nlyearday=60)).strftime('%Y-%m-%d')";
|
||||
assert.strictEqual(evaluateExpr(expr2), "2001-03-01");
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("misc");
|
||||
|
||||
QUnit.test("context_today", (assert) => {
|
||||
assert.ok(check("context_today().strftime('%Y-%m-%d')", formatDate));
|
||||
});
|
||||
|
||||
QUnit.test("today", (assert) => {
|
||||
assert.ok(check("today", formatDate));
|
||||
});
|
||||
|
||||
QUnit.test("now", (assert) => {
|
||||
assert.ok(check("now", formatDateTime));
|
||||
});
|
||||
|
||||
QUnit.test("current_date", (assert) => {
|
||||
patchDate(2021, 8, 20, 10, 0, 0);
|
||||
assert.deepEqual(evaluateExpr("current_date"), "2021-09-20");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
|
||||
QUnit.module("py", {}, () => {
|
||||
QUnit.module("interpreter", () => {
|
||||
QUnit.module("basic values");
|
||||
|
||||
QUnit.test("evaluate simple values", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("12"), 12);
|
||||
assert.strictEqual(evaluateExpr('"foo"'), "foo");
|
||||
});
|
||||
|
||||
QUnit.test("empty expression", (assert) => {
|
||||
assert.throws(() => evaluateExpr(""), /Error: Missing token/);
|
||||
});
|
||||
|
||||
QUnit.test("numbers", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("1.2"), 1.2);
|
||||
assert.strictEqual(evaluateExpr(".12"), 0.12);
|
||||
assert.strictEqual(evaluateExpr("0"), 0);
|
||||
assert.strictEqual(evaluateExpr("1.0"), 1);
|
||||
assert.strictEqual(evaluateExpr("-1.2"), -1.2);
|
||||
assert.strictEqual(evaluateExpr("-12"), -12);
|
||||
assert.strictEqual(evaluateExpr("+12"), 12);
|
||||
});
|
||||
|
||||
QUnit.test("strings", (assert) => {
|
||||
assert.strictEqual(evaluateExpr('""'), "");
|
||||
assert.strictEqual(evaluateExpr('"foo"'), "foo");
|
||||
assert.strictEqual(evaluateExpr("'foo'"), "foo");
|
||||
});
|
||||
|
||||
QUnit.test("boolean", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("True"), true);
|
||||
assert.strictEqual(evaluateExpr("False"), false);
|
||||
});
|
||||
|
||||
QUnit.test("lists", (assert) => {
|
||||
assert.deepEqual(evaluateExpr("[]"), []);
|
||||
assert.deepEqual(evaluateExpr("[1]"), [1]);
|
||||
assert.deepEqual(evaluateExpr("[1,2]"), [1, 2]);
|
||||
assert.deepEqual(evaluateExpr("[1,False, None, 'foo']"), [1, false, null, "foo"]);
|
||||
assert.deepEqual(evaluateExpr("[1,2 + 3]"), [1, 5]);
|
||||
assert.deepEqual(evaluateExpr("[1,2, 3][1]"), 2);
|
||||
});
|
||||
|
||||
QUnit.test("None", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("None"), null);
|
||||
});
|
||||
|
||||
QUnit.test("Tuples", (assert) => {
|
||||
assert.deepEqual(evaluateExpr("()"), []);
|
||||
assert.deepEqual(evaluateExpr("(1,)"), [1]);
|
||||
assert.deepEqual(evaluateExpr("(1,2)"), [1, 2]);
|
||||
});
|
||||
|
||||
QUnit.test("strings can be concatenated", (assert) => {
|
||||
assert.strictEqual(evaluateExpr('"foo" + "bar"'), "foobar");
|
||||
});
|
||||
|
||||
QUnit.module("number properties");
|
||||
|
||||
QUnit.test("number arithmetic", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("1 + 2"), 3);
|
||||
assert.strictEqual(evaluateExpr("4 - 2"), 2);
|
||||
assert.strictEqual(evaluateExpr("4 * 2"), 8);
|
||||
assert.strictEqual(evaluateExpr("1.5 + 2"), 3.5);
|
||||
assert.strictEqual(evaluateExpr("1 + -1"), 0);
|
||||
assert.strictEqual(evaluateExpr("1 - 1"), 0);
|
||||
assert.strictEqual(evaluateExpr("1.5 - 2"), -0.5);
|
||||
assert.strictEqual(evaluateExpr("0 * 5"), 0);
|
||||
assert.strictEqual(evaluateExpr("1 + 3 * 5"), 16);
|
||||
assert.strictEqual(evaluateExpr("42 * -2"), -84);
|
||||
assert.strictEqual(evaluateExpr("1 / 2"), 0.5);
|
||||
assert.strictEqual(evaluateExpr("2 / 1"), 2);
|
||||
assert.strictEqual(evaluateExpr("42 % 5"), 2);
|
||||
assert.strictEqual(evaluateExpr("2 ** 3"), 8);
|
||||
assert.strictEqual(evaluateExpr("a + b", { a: 1, b: 41 }), 42);
|
||||
});
|
||||
|
||||
QUnit.test("// operator", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("1 // 2"), 0);
|
||||
assert.strictEqual(evaluateExpr("1 // -2"), -1);
|
||||
assert.strictEqual(evaluateExpr("-1 // 2"), -1);
|
||||
assert.strictEqual(evaluateExpr("6 // 2"), 3);
|
||||
});
|
||||
|
||||
QUnit.module("boolean properties");
|
||||
|
||||
QUnit.test("boolean arithmetic", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("True and False"), false);
|
||||
assert.strictEqual(evaluateExpr("True or False"), true);
|
||||
assert.strictEqual(evaluateExpr("True and (False or True)"), true);
|
||||
assert.strictEqual(evaluateExpr("not True"), false);
|
||||
assert.strictEqual(evaluateExpr("not False"), true);
|
||||
assert.strictEqual(evaluateExpr("not foo", { foo: false }), true);
|
||||
assert.strictEqual(evaluateExpr("not None"), true);
|
||||
assert.strictEqual(evaluateExpr("True == False or True == True"), true);
|
||||
assert.strictEqual(evaluateExpr("False == True and False"), false);
|
||||
});
|
||||
|
||||
QUnit.test("get value from context", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("foo == 'foo' or foo == 'bar'", { foo: "bar" }), true);
|
||||
assert.strictEqual(
|
||||
evaluateExpr("foo == 'foo' and bar == 'bar'", { foo: "foo", bar: "bar" }),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("should be lazy", (assert) => {
|
||||
// second clause should nameerror if evaluated
|
||||
assert.throws(() => evaluateExpr("foo == 'foo' and bar == 'bar'", { foo: "foo" }));
|
||||
assert.strictEqual(
|
||||
evaluateExpr("foo == 'foo' and bar == 'bar'", { foo: "bar" }),
|
||||
false
|
||||
);
|
||||
assert.strictEqual(evaluateExpr("foo == 'foo' or bar == 'bar'", { foo: "foo" }), true);
|
||||
});
|
||||
|
||||
QUnit.test("should return the actual object", (assert) => {
|
||||
assert.strictEqual(evaluateExpr('"foo" or "bar"'), "foo");
|
||||
assert.strictEqual(evaluateExpr('None or "bar"'), "bar");
|
||||
assert.strictEqual(evaluateExpr("False or None"), null);
|
||||
assert.strictEqual(evaluateExpr("0 or 1"), 1);
|
||||
});
|
||||
|
||||
QUnit.module("values from context");
|
||||
|
||||
QUnit.test("free variable", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("a", { a: 3 }), 3);
|
||||
assert.strictEqual(evaluateExpr("a + b", { a: 3, b: 5 }), 8);
|
||||
assert.strictEqual(evaluateExpr("a", { a: true }), true);
|
||||
assert.strictEqual(evaluateExpr("a", { a: false }), false);
|
||||
assert.strictEqual(evaluateExpr("a", { a: null }), null);
|
||||
assert.strictEqual(evaluateExpr("a", { a: "bar" }), "bar");
|
||||
assert.deepEqual(evaluateExpr("foo", { foo: [1, 2, 3] }), [1, 2, 3]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"special case for context: the eval context can be accessed as 'context'",
|
||||
(assert) => {
|
||||
assert.strictEqual(evaluateExpr("context.get('b', 54)", { b: 3 }), 3);
|
||||
assert.strictEqual(evaluateExpr("context.get('c', 54)", { b: 3 }), 54);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("true and false available in context", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("true"), true);
|
||||
assert.strictEqual(evaluateExpr("false"), false);
|
||||
});
|
||||
|
||||
QUnit.test("throw error if name is not defined", (assert) => {
|
||||
assert.throws(() => evaluateExpr("a"));
|
||||
});
|
||||
|
||||
QUnit.module("comparisons");
|
||||
|
||||
QUnit.test("equality", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("1 == 1"), true);
|
||||
assert.strictEqual(evaluateExpr('"foo" == "foo"'), true);
|
||||
assert.strictEqual(evaluateExpr('"foo" == "bar"'), false);
|
||||
assert.strictEqual(evaluateExpr("1 == True"), true);
|
||||
assert.strictEqual(evaluateExpr("True == 1"), true);
|
||||
assert.strictEqual(evaluateExpr("1 == False"), false);
|
||||
assert.strictEqual(evaluateExpr("False == 1"), false);
|
||||
assert.strictEqual(evaluateExpr("0 == False"), true);
|
||||
assert.strictEqual(evaluateExpr("False == 0"), true);
|
||||
assert.strictEqual(evaluateExpr("None == None"), true);
|
||||
assert.strictEqual(evaluateExpr("None == False"), false);
|
||||
});
|
||||
|
||||
QUnit.test("equality should work with free variables", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("1 == a", { a: 1 }), true);
|
||||
assert.strictEqual(evaluateExpr('foo == "bar"', { foo: "bar" }), true);
|
||||
assert.strictEqual(evaluateExpr('foo == "bar"', { foo: "qux" }), false);
|
||||
});
|
||||
|
||||
QUnit.test("inequality", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("1 != 2"), true);
|
||||
assert.strictEqual(evaluateExpr('"foo" != "foo"'), false);
|
||||
assert.strictEqual(evaluateExpr('"foo" != "bar"'), true);
|
||||
});
|
||||
|
||||
QUnit.test("inequality should work with free variables", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("1 != a", { a: 42 }), true);
|
||||
assert.strictEqual(evaluateExpr('foo != "bar"', { foo: "bar" }), false);
|
||||
assert.strictEqual(evaluateExpr('foo != "bar"', { foo: "qux" }), true);
|
||||
assert.strictEqual(evaluateExpr("foo != bar", { foo: "qux", bar: "quux" }), true);
|
||||
});
|
||||
|
||||
QUnit.test("should accept deprecated form", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("1 <> 2"), true);
|
||||
assert.strictEqual(evaluateExpr('"foo" <> "foo"'), false);
|
||||
assert.strictEqual(evaluateExpr('"foo" <> "bar"'), true);
|
||||
});
|
||||
|
||||
QUnit.test("comparing numbers", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("3 < 5"), true);
|
||||
assert.strictEqual(evaluateExpr("3 > 5"), false);
|
||||
assert.strictEqual(evaluateExpr("5 >= 3"), true);
|
||||
assert.strictEqual(evaluateExpr("3 >= 3"), true);
|
||||
assert.strictEqual(evaluateExpr("3 <= 5"), true);
|
||||
assert.strictEqual(evaluateExpr("5 <= 3"), false);
|
||||
});
|
||||
|
||||
QUnit.test("should support comparison chains", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("1 < 3 < 5"), true);
|
||||
assert.strictEqual(evaluateExpr("5 > 3 > 1"), true);
|
||||
assert.strictEqual(evaluateExpr("1 < 3 > 2 == 2 > -2"), true);
|
||||
assert.strictEqual(evaluateExpr("1 < 2 < 3 < 4 < 5 < 6"), true);
|
||||
});
|
||||
|
||||
QUnit.test("should compare strings", (assert) => {
|
||||
assert.strictEqual(
|
||||
evaluateExpr("date >= current", { date: "2010-06-08", current: "2010-06-05" }),
|
||||
true
|
||||
);
|
||||
assert.strictEqual(evaluateExpr('state >= "cancel"', { state: "cancel" }), true);
|
||||
assert.strictEqual(evaluateExpr('state >= "cancel"', { state: "open" }), true);
|
||||
});
|
||||
|
||||
QUnit.test("mixed types comparisons", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("None < 42"), true);
|
||||
assert.strictEqual(evaluateExpr("None > 42"), false);
|
||||
assert.strictEqual(evaluateExpr("42 > None"), true);
|
||||
assert.strictEqual(evaluateExpr("None < False"), true);
|
||||
assert.strictEqual(evaluateExpr("None < True"), true);
|
||||
assert.strictEqual(evaluateExpr("False > None"), true);
|
||||
assert.strictEqual(evaluateExpr("True > None"), true);
|
||||
assert.strictEqual(evaluateExpr("None > False"), false);
|
||||
assert.strictEqual(evaluateExpr("None > True"), false);
|
||||
assert.strictEqual(evaluateExpr("0 > True"), false);
|
||||
assert.strictEqual(evaluateExpr("0 < True"), true);
|
||||
assert.strictEqual(evaluateExpr("1 <= True"), true);
|
||||
assert.strictEqual(evaluateExpr('False < ""'), true);
|
||||
assert.strictEqual(evaluateExpr('"" > False'), true);
|
||||
assert.strictEqual(evaluateExpr('False > ""'), false);
|
||||
assert.strictEqual(evaluateExpr('0 < ""'), true);
|
||||
assert.strictEqual(evaluateExpr('"" > 0'), true);
|
||||
assert.strictEqual(evaluateExpr('0 > ""'), false);
|
||||
assert.strictEqual(evaluateExpr("3 < True"), false);
|
||||
assert.strictEqual(evaluateExpr("3 > True"), true);
|
||||
assert.strictEqual(evaluateExpr("{} > None"), true);
|
||||
assert.strictEqual(evaluateExpr("{} < None"), false);
|
||||
assert.strictEqual(evaluateExpr("{} > False"), true);
|
||||
assert.strictEqual(evaluateExpr("{} < False"), false);
|
||||
assert.strictEqual(evaluateExpr("3 < 'foo'"), true);
|
||||
assert.strictEqual(evaluateExpr("'foo' < 4444"), false);
|
||||
assert.strictEqual(evaluateExpr("{} < []"), true);
|
||||
});
|
||||
|
||||
QUnit.module("containment");
|
||||
|
||||
QUnit.test("in tuples", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("'bar' in ('foo', 'bar')"), true);
|
||||
assert.strictEqual(evaluateExpr("'bar' in ('foo', 'qux')"), false);
|
||||
assert.strictEqual(evaluateExpr("1 in (1,2,3,4)"), true);
|
||||
assert.strictEqual(evaluateExpr("1 in (2,3,4)"), false);
|
||||
assert.strictEqual(evaluateExpr("'url' in ('url',)"), true);
|
||||
assert.strictEqual(evaluateExpr("'ur' in ('url',)"), false);
|
||||
assert.strictEqual(evaluateExpr("'url' in ('url', 'foo', 'bar')"), true);
|
||||
});
|
||||
|
||||
QUnit.test("in strings", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("'bar' in 'bar'"), true);
|
||||
assert.strictEqual(evaluateExpr("'bar' in 'foobar'"), true);
|
||||
assert.strictEqual(evaluateExpr("'bar' in 'fooqux'"), false);
|
||||
});
|
||||
|
||||
QUnit.test("in lists", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("'bar' in ['foo', 'bar']"), true);
|
||||
assert.strictEqual(evaluateExpr("'bar' in ['foo', 'qux']"), false);
|
||||
assert.strictEqual(evaluateExpr("3 in [1,2,3]"), true);
|
||||
assert.strictEqual(evaluateExpr("None in [1,'foo',None]"), true);
|
||||
assert.strictEqual(evaluateExpr("not a in b", { a: 3, b: [1, 2, 4, 8] }), true);
|
||||
});
|
||||
|
||||
QUnit.test("not in", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("1 not in (2,3,4)"), true);
|
||||
assert.strictEqual(evaluateExpr('"ur" not in ("url",)'), true);
|
||||
assert.strictEqual(evaluateExpr("-2 not in (1,2,3)"), true);
|
||||
assert.strictEqual(evaluateExpr("-2 not in (1,-2,3)"), false);
|
||||
});
|
||||
|
||||
QUnit.module("conversions");
|
||||
|
||||
QUnit.test("to bool", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("bool('')"), false);
|
||||
assert.strictEqual(evaluateExpr("bool('foo')"), true);
|
||||
assert.strictEqual(
|
||||
evaluateExpr("bool(date_deadline)", { date_deadline: "2008" }),
|
||||
true
|
||||
);
|
||||
assert.strictEqual(evaluateExpr("bool(s)", { s: "" }), false);
|
||||
});
|
||||
|
||||
QUnit.module("callables");
|
||||
|
||||
QUnit.test("should not call function from context", (assert) => {
|
||||
assert.throws(() => evaluateExpr("foo()", { foo: () => 3 }));
|
||||
assert.throws(() => evaluateExpr("1 + foo()", { foo: () => 3 }));
|
||||
});
|
||||
|
||||
QUnit.module("dicts");
|
||||
|
||||
QUnit.test("dict", (assert) => {
|
||||
assert.deepEqual(evaluateExpr("{}"), {});
|
||||
assert.deepEqual(evaluateExpr("{'foo': 1 + 2}"), { foo: 3 });
|
||||
assert.deepEqual(evaluateExpr("{'foo': 1, 'bar': 4}"), { foo: 1, bar: 4 });
|
||||
});
|
||||
|
||||
QUnit.test("lookup and definition", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("{'a': 1}['a']"), 1);
|
||||
assert.strictEqual(evaluateExpr("{1: 2}[1]"), 2);
|
||||
});
|
||||
|
||||
QUnit.test("can get values with get method", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("{'a': 1}.get('a')"), 1);
|
||||
assert.strictEqual(evaluateExpr("{'a': 1}.get('b')"), null);
|
||||
assert.strictEqual(evaluateExpr("{'a': 1}.get('b', 54)"), 54);
|
||||
});
|
||||
|
||||
QUnit.module("objects");
|
||||
|
||||
QUnit.test("can read values from object", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("obj.a", { obj: { a: 123 } }), 123);
|
||||
assert.strictEqual(evaluateExpr("obj.a.b.c", { obj: { a: { b: { c: 321 } } } }), 321);
|
||||
});
|
||||
|
||||
QUnit.test("cannot call function in object", (assert) => {
|
||||
assert.throws(() => evaluateExpr("obj.f(3)", { obj: { f: (n) => n + 1 } }));
|
||||
});
|
||||
|
||||
QUnit.module("if expressions");
|
||||
|
||||
QUnit.test("simple if expressions", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("1 if True else 2"), 1);
|
||||
assert.strictEqual(evaluateExpr("1 if 3 < 2 else 'greater'"), "greater");
|
||||
});
|
||||
|
||||
QUnit.test("only evaluate proper branch", (assert) => {
|
||||
// will throw if evaluate wrong branch => name error
|
||||
assert.strictEqual(evaluateExpr("1 if True else boom"), 1);
|
||||
assert.strictEqual(evaluateExpr("boom if False else 222"), 222);
|
||||
});
|
||||
|
||||
QUnit.module("miscellaneous expressions");
|
||||
|
||||
QUnit.test("tuple in list", (assert) => {
|
||||
assert.deepEqual(evaluateExpr("[(1 + 2,'foo', True)]"), [[3, "foo", true]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,351 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { parseExpr } from "@web/core/py_js/py";
|
||||
|
||||
QUnit.module("py", {}, () => {
|
||||
QUnit.module("parser");
|
||||
|
||||
QUnit.test("can parse basic elements", (assert) => {
|
||||
assert.deepEqual(parseExpr("1"), { type: 0 /* Number */, value: 1 });
|
||||
assert.deepEqual(parseExpr('"foo"'), { type: 1 /* String */, value: "foo" });
|
||||
assert.deepEqual(parseExpr("foo"), { type: 5 /* Name */, value: "foo" });
|
||||
assert.deepEqual(parseExpr("True"), { type: 2 /* Boolean */, value: true });
|
||||
assert.deepEqual(parseExpr("False"), { type: 2 /* Boolean */, value: false });
|
||||
assert.deepEqual(parseExpr("None"), { type: 3 /* None */ });
|
||||
});
|
||||
|
||||
QUnit.test("cannot parse empty string", (assert) => {
|
||||
assert.throws(() => parseExpr(""), /Error: Missing token/);
|
||||
});
|
||||
|
||||
QUnit.test("can parse unary operator -", (assert) => {
|
||||
assert.deepEqual(parseExpr("-1"), {
|
||||
type: 6 /* UnaryOperator */,
|
||||
op: "-",
|
||||
right: { type: 0 /* Number */, value: 1 },
|
||||
});
|
||||
assert.deepEqual(parseExpr("-foo"), {
|
||||
type: 6 /* UnaryOperator */,
|
||||
op: "-",
|
||||
right: { type: 5 /* Name */, value: "foo" },
|
||||
});
|
||||
assert.deepEqual(parseExpr("not True"), {
|
||||
type: 6 /* UnaryOperator */,
|
||||
op: "not",
|
||||
right: { type: 2 /* Boolean */, value: true },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("can parse parenthesis", (assert) => {
|
||||
assert.deepEqual(parseExpr("(1 + 2)"), {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "+",
|
||||
left: { type: 0 /* Number */, value: 1 },
|
||||
right: { type: 0 /* Number */, value: 2 },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("can parse binary operators", (assert) => {
|
||||
assert.deepEqual(parseExpr("1 < 2"), {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "<",
|
||||
left: { type: 0 /* Number */, value: 1 },
|
||||
right: { type: 0 /* Number */, value: 2 },
|
||||
});
|
||||
assert.deepEqual(parseExpr('a + "foo"'), {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "+",
|
||||
left: { type: 5 /* Name */, value: "a" },
|
||||
right: { type: 1 /* String */, value: "foo" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("can parse boolean operators", (assert) => {
|
||||
assert.deepEqual(parseExpr('True and "foo"'), {
|
||||
type: 14 /* BooleanOperator */,
|
||||
op: "and",
|
||||
left: { type: 2 /* Boolean */, value: true },
|
||||
right: { type: 1 /* String */, value: "foo" },
|
||||
});
|
||||
assert.deepEqual(parseExpr('True or "foo"'), {
|
||||
type: 14 /* BooleanOperator */,
|
||||
op: "or",
|
||||
left: { type: 2 /* Boolean */, value: true },
|
||||
right: { type: 1 /* String */, value: "foo" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("expression with == and or", (assert) => {
|
||||
assert.deepEqual(parseExpr("False == True and False"), {
|
||||
type: 14 /* BooleanOperator */,
|
||||
op: "and",
|
||||
left: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "==",
|
||||
left: { type: 2 /* Boolean */, value: false },
|
||||
right: { type: 2 /* Boolean */, value: true },
|
||||
},
|
||||
right: { type: 2 /* Boolean */, value: false },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("expression with + and ==", (assert) => {
|
||||
assert.deepEqual(parseExpr("1 + 2 == 3"), {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "==",
|
||||
left: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "+",
|
||||
left: { type: 0 /* Number */, value: 1 },
|
||||
right: { type: 0 /* Number */, value: 2 },
|
||||
},
|
||||
right: { type: 0 /* Number */, value: 3 },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("can parse chained comparisons", (assert) => {
|
||||
assert.deepEqual(parseExpr("1 < 2 <= 3"), {
|
||||
type: 14 /* BooleanOperator */,
|
||||
op: "and",
|
||||
left: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "<",
|
||||
left: { type: 0 /* Number */, value: 1 },
|
||||
right: { type: 0 /* Number */, value: 2 },
|
||||
},
|
||||
right: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "<=",
|
||||
left: { type: 0 /* Number */, value: 2 },
|
||||
right: { type: 0 /* Number */, value: 3 },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(parseExpr("1 < 2 <= 3 > 33"), {
|
||||
type: 14 /* BooleanOperator */,
|
||||
op: "and",
|
||||
left: {
|
||||
type: 14 /* BooleanOperator */,
|
||||
op: "and",
|
||||
left: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "<",
|
||||
left: { type: 0 /* Number */, value: 1 },
|
||||
right: { type: 0 /* Number */, value: 2 },
|
||||
},
|
||||
right: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "<=",
|
||||
left: { type: 0 /* Number */, value: 2 },
|
||||
right: { type: 0 /* Number */, value: 3 },
|
||||
},
|
||||
},
|
||||
right: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: ">",
|
||||
left: { type: 0 /* Number */, value: 3 },
|
||||
right: { type: 0 /* Number */, value: 33 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("can parse lists", (assert) => {
|
||||
assert.deepEqual(parseExpr("[]"), {
|
||||
type: 4 /* List */,
|
||||
value: [],
|
||||
});
|
||||
assert.deepEqual(parseExpr("[1]"), {
|
||||
type: 4 /* List */,
|
||||
value: [{ type: 0 /* Number */, value: 1 }],
|
||||
});
|
||||
assert.deepEqual(parseExpr("[1,]"), {
|
||||
type: 4 /* List */,
|
||||
value: [{ type: 0 /* Number */, value: 1 }],
|
||||
});
|
||||
assert.deepEqual(parseExpr("[1, 4]"), {
|
||||
type: 4 /* List */,
|
||||
value: [
|
||||
{ type: 0 /* Number */, value: 1 },
|
||||
{ type: 0 /* Number */, value: 4 },
|
||||
],
|
||||
});
|
||||
assert.throws(() => parseExpr("[1 1]"));
|
||||
});
|
||||
|
||||
QUnit.test("can parse lists lookup", (assert) => {
|
||||
assert.deepEqual(parseExpr("[1,2][1]"), {
|
||||
type: 12 /* Lookup */,
|
||||
target: {
|
||||
type: 4 /* List */,
|
||||
value: [
|
||||
{ type: 0 /* Number */, value: 1 },
|
||||
{ type: 0 /* Number */, value: 2 },
|
||||
],
|
||||
},
|
||||
key: { type: 0 /* Number */, value: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("can parse tuples", (assert) => {
|
||||
assert.deepEqual(parseExpr("()"), {
|
||||
type: 10 /* Tuple */,
|
||||
value: [],
|
||||
});
|
||||
assert.deepEqual(parseExpr("(1,)"), {
|
||||
type: 10 /* Tuple */,
|
||||
value: [{ type: 0 /* Number */, value: 1 }],
|
||||
});
|
||||
assert.deepEqual(parseExpr("(1,4)"), {
|
||||
type: 10 /* Tuple */,
|
||||
value: [
|
||||
{ type: 0 /* Number */, value: 1 },
|
||||
{ type: 0 /* Number */, value: 4 },
|
||||
],
|
||||
});
|
||||
assert.throws(() => parseExpr("(1 1)"));
|
||||
});
|
||||
|
||||
QUnit.test("can parse dictionary", (assert) => {
|
||||
assert.deepEqual(parseExpr("{}"), {
|
||||
type: 11 /* Dictionary */,
|
||||
value: {},
|
||||
});
|
||||
assert.deepEqual(parseExpr("{'foo': 1}"), {
|
||||
type: 11 /* Dictionary */,
|
||||
value: { foo: { type: 0 /* Number */, value: 1 } },
|
||||
});
|
||||
assert.deepEqual(parseExpr("{'foo': 1, 'bar': 3}"), {
|
||||
type: 11 /* Dictionary */,
|
||||
value: {
|
||||
foo: { type: 0 /* Number */, value: 1 },
|
||||
bar: { type: 0 /* Number */, value: 3 },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(parseExpr("{1: 2}"), {
|
||||
type: 11 /* Dictionary */,
|
||||
value: { 1: { type: 0 /* Number */, value: 2 } },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("can parse dictionary lookup", (assert) => {
|
||||
assert.deepEqual(parseExpr("{}['a']"), {
|
||||
type: 12 /* Lookup */,
|
||||
target: { type: 11 /* Dictionary */, value: {} },
|
||||
key: { type: 1 /* String */, value: "a" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("can parse assignment", (assert) => {
|
||||
assert.deepEqual(parseExpr("a=1"), {
|
||||
type: 9 /* Assignment */,
|
||||
name: { type: 5 /* Name */, value: "a" },
|
||||
value: { type: 0 /* Number */, value: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("can parse function calls", (assert) => {
|
||||
assert.deepEqual(parseExpr("f()"), {
|
||||
type: 8 /* FunctionCall */,
|
||||
fn: { type: 5 /* Name */, value: "f" },
|
||||
args: [],
|
||||
kwargs: {},
|
||||
});
|
||||
assert.deepEqual(parseExpr("f() + 2"), {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "+",
|
||||
left: {
|
||||
type: 8 /* FunctionCall */,
|
||||
fn: { type: 5 /* Name */, value: "f" },
|
||||
args: [],
|
||||
kwargs: {},
|
||||
},
|
||||
right: { type: 0 /* Number */, value: 2 },
|
||||
});
|
||||
assert.deepEqual(parseExpr("f(1)"), {
|
||||
type: 8 /* FunctionCall */,
|
||||
fn: { type: 5 /* Name */, value: "f" },
|
||||
args: [{ type: 0 /* Number */, value: 1 }],
|
||||
kwargs: {},
|
||||
});
|
||||
assert.deepEqual(parseExpr("f(1, 2)"), {
|
||||
type: 8 /* FunctionCall */,
|
||||
fn: { type: 5 /* Name */, value: "f" },
|
||||
args: [
|
||||
{ type: 0 /* Number */, value: 1 },
|
||||
{ type: 0 /* Number */, value: 2 },
|
||||
],
|
||||
kwargs: {},
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("can parse function calls with kwargs", (assert) => {
|
||||
assert.deepEqual(parseExpr("f(a = 1)"), {
|
||||
type: 8 /* FunctionCall */,
|
||||
fn: { type: 5 /* Name */, value: "f" },
|
||||
args: [],
|
||||
kwargs: { a: { type: 0 /* Number */, value: 1 } },
|
||||
});
|
||||
assert.deepEqual(parseExpr("f(3, a = 1)"), {
|
||||
type: 8 /* FunctionCall */,
|
||||
fn: { type: 5 /* Name */, value: "f" },
|
||||
args: [{ type: 0 /* Number */, value: 3 }],
|
||||
kwargs: { a: { type: 0 /* Number */, value: 1 } },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("can parse not a in b", (assert) => {
|
||||
assert.deepEqual(parseExpr("not a in b"), {
|
||||
type: 6 /* UnaryOperator */,
|
||||
op: "not",
|
||||
right: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "in",
|
||||
left: { type: 5 /* Name */, value: "a" },
|
||||
right: { type: 5 /* Name */, value: "b" },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(parseExpr("a.b.c"), {
|
||||
type: 15 /* ObjLookup */,
|
||||
obj: {
|
||||
type: 15 /* ObjLookup */,
|
||||
obj: { type: 5 /* Name */, value: "a" },
|
||||
key: "b",
|
||||
},
|
||||
key: "c",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("can parse if statement", (assert) => {
|
||||
assert.deepEqual(parseExpr("1 if True else 2"), {
|
||||
type: 13 /* If */,
|
||||
condition: { type: 2 /* Boolean */, value: true },
|
||||
ifTrue: { type: 0 /* Number */, value: 1 },
|
||||
ifFalse: { type: 0 /* Number */, value: 2 },
|
||||
});
|
||||
assert.deepEqual(parseExpr("1 + 1 if True else 2"), {
|
||||
type: 13 /* If */,
|
||||
condition: { type: 2 /* Boolean */, value: true },
|
||||
ifTrue: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "+",
|
||||
left: { type: 0 /* Number */, value: 1 },
|
||||
right: { type: 0 /* Number */, value: 1 },
|
||||
},
|
||||
ifFalse: { type: 0 /* Number */, value: 2 },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("tuple in list", (assert) => {
|
||||
assert.deepEqual(parseExpr("[(1,2)]"), {
|
||||
type: 4 /* List */,
|
||||
value: [
|
||||
{
|
||||
type: 10 /* Tuple */,
|
||||
value: [
|
||||
{ type: 0 /* Number */, value: 1 },
|
||||
{ type: 0 /* Number */, value: 2 },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
import { PyTimeDelta } from "@web/core/py_js/py_date";
|
||||
|
||||
function testDelta(assert, expr, res) {
|
||||
const timedelta = evaluateExpr(expr, { td: PyTimeDelta });
|
||||
assert.strictEqual(`${timedelta.days}, ${timedelta.seconds}, ${timedelta.microseconds}`, res);
|
||||
}
|
||||
|
||||
function testEquality(assert, expr1, expr2, ctx) {
|
||||
const equality = `${expr1} == ${expr2}`;
|
||||
assert.ok(
|
||||
evaluateExpr(equality, Object.assign({ td: PyTimeDelta }, ctx)),
|
||||
`evaluating ${equality}`
|
||||
);
|
||||
}
|
||||
|
||||
QUnit.module("py", {}, () => {
|
||||
QUnit.module("datetime.timedelta");
|
||||
|
||||
QUnit.test("create", (assert) => {
|
||||
testDelta(assert, "td(weeks=1)", "7, 0, 0");
|
||||
testDelta(assert, "td(days=1)", "1, 0, 0");
|
||||
testDelta(assert, "td(hours=1)", "0, 3600, 0");
|
||||
testDelta(assert, "td(minutes=1)", "0, 60, 0");
|
||||
testDelta(assert, "td(seconds=1)", "0, 1, 0");
|
||||
testDelta(assert, "td(milliseconds=1)", "0, 0, 1000");
|
||||
testDelta(assert, "td(microseconds=1)", "0, 0, 1");
|
||||
|
||||
testDelta(assert, "td(days=-1.25)", "-2, 64800, 0");
|
||||
testDelta(assert, "td(seconds=129600.4)", "1, 43200, 400000");
|
||||
testDelta(assert, "td(hours=24.5,milliseconds=1400)", "1, 1801, 400000");
|
||||
|
||||
testEquality(
|
||||
assert,
|
||||
"td()",
|
||||
"td(weeks=0, days=0, hours=0, minutes=0, seconds=0, milliseconds=0, microseconds=0)"
|
||||
);
|
||||
testEquality(assert, "td(1)", "td(days=1)");
|
||||
testEquality(assert, "td(0, 1)", "td(seconds=1)");
|
||||
testEquality(assert, "td(0, 0, 1)", "td(microseconds=1)");
|
||||
testEquality(assert, "td(weeks=1)", "td(days=7)");
|
||||
testEquality(assert, "td(days=1)", "td(hours=24)");
|
||||
testEquality(assert, "td(hours=1)", "td(minutes=60)");
|
||||
testEquality(assert, "td(minutes=1)", "td(seconds=60)");
|
||||
testEquality(assert, "td(seconds=1)", "td(milliseconds=1000)");
|
||||
testEquality(assert, "td(milliseconds=1)", "td(microseconds=1000)");
|
||||
|
||||
testEquality(assert, "td(weeks=1.0/7)", "td(days=1)");
|
||||
testEquality(assert, "td(days=1.0/24)", "td(hours=1)");
|
||||
testEquality(assert, "td(hours=1.0/60)", "td(minutes=1)");
|
||||
testEquality(assert, "td(minutes=1.0/60)", "td(seconds=1)");
|
||||
testEquality(assert, "td(seconds=0.001)", "td(milliseconds=1)");
|
||||
testEquality(assert, "td(milliseconds=0.001)", "td(microseconds=1)");
|
||||
});
|
||||
|
||||
QUnit.test("massive normalization", function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const td = PyTimeDelta.create({ microseconds: -1 });
|
||||
|
||||
assert.strictEqual(td.days, -1);
|
||||
assert.strictEqual(td.seconds, 24 * 3600 - 1);
|
||||
assert.strictEqual(td.microseconds, 999999);
|
||||
});
|
||||
|
||||
QUnit.test("attributes", function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const ctx = { td: PyTimeDelta };
|
||||
|
||||
assert.strictEqual(evaluateExpr("td(1, 7, 31).days", ctx), 1);
|
||||
assert.strictEqual(evaluateExpr("td(1, 7, 31).seconds", ctx), 7);
|
||||
assert.strictEqual(evaluateExpr("td(1, 7, 31).microseconds", ctx), 31);
|
||||
});
|
||||
|
||||
QUnit.test("basic operations: +, -, *, //", function (assert) {
|
||||
assert.expect(28);
|
||||
|
||||
const ctx = {
|
||||
a: new PyTimeDelta(7, 0, 0),
|
||||
b: new PyTimeDelta(0, 60, 0),
|
||||
c: new PyTimeDelta(0, 0, 1000),
|
||||
};
|
||||
|
||||
testEquality(assert, "a+b+c", "td(7, 60, 1000)", ctx);
|
||||
testEquality(assert, "a-b", "td(6, 24*3600 - 60)", ctx);
|
||||
testEquality(assert, "-a", "td(-7)", ctx);
|
||||
testEquality(assert, "+a", "td(7)", ctx);
|
||||
testEquality(assert, "-b", "td(-1, 24*3600 - 60)", ctx);
|
||||
testEquality(assert, "-c", "td(-1, 24*3600 - 1, 999000)", ctx);
|
||||
testEquality(assert, "td(6, 24*3600)", "a", ctx);
|
||||
testEquality(assert, "td(0, 0, 60*1000000)", "b", ctx);
|
||||
testEquality(assert, "a*10", "td(70)", ctx);
|
||||
testEquality(assert, "a*10", "10*a", ctx);
|
||||
// testEquality(assert, 'a*10L', '10*a', ctx);
|
||||
testEquality(assert, "b*10", "td(0, 600)", ctx);
|
||||
testEquality(assert, "10*b", "td(0, 600)", ctx);
|
||||
// testEquality(assert, 'b*10L', 'td(0, 600)', ctx);
|
||||
testEquality(assert, "c*10", "td(0, 0, 10000)", ctx);
|
||||
testEquality(assert, "10*c", "td(0, 0, 10000)", ctx);
|
||||
// testEquality(assert, 'c*10L', 'td(0, 0, 10000)', ctx);
|
||||
testEquality(assert, "a*-1", "-a", ctx);
|
||||
testEquality(assert, "b*-2", "-b-b", ctx);
|
||||
testEquality(assert, "c*-2", "-c+-c", ctx);
|
||||
testEquality(assert, "b*(60*24)", "(b*60)*24", ctx);
|
||||
testEquality(assert, "b*(60*24)", "(60*b)*24", ctx);
|
||||
testEquality(assert, "c*1000", "td(0, 1)", ctx);
|
||||
testEquality(assert, "1000*c", "td(0, 1)", ctx);
|
||||
testEquality(assert, "a//7", "td(1)", ctx);
|
||||
testEquality(assert, "b//10", "td(0, 6)", ctx);
|
||||
testEquality(assert, "c//1000", "td(0, 0, 1)", ctx);
|
||||
testEquality(assert, "a//10", "td(0, 7*24*360)", ctx);
|
||||
testEquality(assert, "a//3600000", "td(0, 0, 7*24*1000)", ctx);
|
||||
testEquality(
|
||||
assert,
|
||||
"td(999999999, 86399, 999999) - td(999999999, 86399, 999998)",
|
||||
"td(0, 0, 1)"
|
||||
);
|
||||
testEquality(assert, "td(999999999, 1, 1) - td(999999999, 1, 0)", "td(0, 0, 1)");
|
||||
});
|
||||
|
||||
QUnit.test("total_seconds", function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const ctx = { td: PyTimeDelta };
|
||||
|
||||
assert.strictEqual(evaluateExpr("td(365).total_seconds()", ctx), 31536000);
|
||||
assert.strictEqual(
|
||||
evaluateExpr("td(seconds=123456.789012).total_seconds()", ctx),
|
||||
123456.789012
|
||||
);
|
||||
assert.strictEqual(
|
||||
evaluateExpr("td(seconds=-123456.789012).total_seconds()", ctx),
|
||||
-123456.789012
|
||||
);
|
||||
assert.strictEqual(evaluateExpr("td(seconds=0.123456).total_seconds()", ctx), 0.123456);
|
||||
assert.strictEqual(evaluateExpr("td().total_seconds()", ctx), 0);
|
||||
assert.strictEqual(evaluateExpr("td(seconds=1000000).total_seconds()", ctx), 1e6);
|
||||
});
|
||||
|
||||
QUnit.test("bool", function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const ctx = { td: PyTimeDelta };
|
||||
|
||||
assert.ok(evaluateExpr("bool(td(1))", ctx));
|
||||
assert.ok(evaluateExpr("bool(td(0, 1))", ctx));
|
||||
assert.ok(evaluateExpr("bool(td(0, 0, 1))", ctx));
|
||||
assert.ok(evaluateExpr("bool(td(microseconds=1))", ctx));
|
||||
assert.ok(evaluateExpr("bool(not td(0))", ctx));
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { tokenize } from "@web/core/py_js/py";
|
||||
|
||||
QUnit.module("py", {}, () => {
|
||||
QUnit.module("tokenizer");
|
||||
|
||||
QUnit.test("can tokenize simple expressions with spaces", (assert) => {
|
||||
assert.deepEqual(tokenize("1"), [{ type: 0 /* Number */, value: 1 }]);
|
||||
assert.deepEqual(tokenize(" 1"), [{ type: 0 /* Number */, value: 1 }]);
|
||||
assert.deepEqual(tokenize(" 1 "), [{ type: 0 /* Number */, value: 1 }]);
|
||||
});
|
||||
|
||||
QUnit.test("can tokenize numbers", (assert) => {
|
||||
/* Without exponent */
|
||||
assert.deepEqual(tokenize("1"), [{ type: 0 /* Number */, value: 1 }]);
|
||||
assert.deepEqual(tokenize("13"), [{ type: 0 /* Number */, value: 13 }]);
|
||||
assert.deepEqual(tokenize("-1"), [
|
||||
{ type: 2 /* Symbol */, value: "-" },
|
||||
{ type: 0 /* Number */, value: 1 },
|
||||
]);
|
||||
|
||||
/* With exponent */
|
||||
assert.deepEqual(tokenize("1e2"), [{ type: 0 /* Number */, value: 100 }]);
|
||||
assert.deepEqual(tokenize("13E+02"), [{ type: 0 /* Number */, value: 1300 }]);
|
||||
assert.deepEqual(tokenize("15E-2"), [{ type: 0 /* Number */, value: 0.15 }]);
|
||||
assert.deepEqual(tokenize("-30e+002"), [
|
||||
{ type: 2 /* Symbol */, value: "-" },
|
||||
{ type: 0 /* Number */, value: 3000 },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("can tokenize floats", (assert) => {
|
||||
/* Without exponent */
|
||||
assert.deepEqual(tokenize("12.0"), [{ type: 0 /* Number */, value: 12 }]);
|
||||
assert.deepEqual(tokenize("1.2"), [{ type: 0 /* Number */, value: 1.2 }]);
|
||||
assert.deepEqual(tokenize(".42"), [{ type: 0 /* Number */, value: 0.42 }]);
|
||||
assert.deepEqual(tokenize("12."), [{type: 0 /* Number */, value: 12}]);
|
||||
assert.deepEqual(tokenize("-1.23"), [
|
||||
{ type: 2 /* Symbol */, value: "-" },
|
||||
{ type: 0 /* Number */, value: 1.23 },
|
||||
]);
|
||||
|
||||
/* With exponent */
|
||||
assert.deepEqual(tokenize("1234e-3"), [{type: 0 /* Number */, value: 1.234}]);
|
||||
assert.deepEqual(tokenize("1.23E-03"), [{type: 0 /* Number */, value: 0.00123}]);
|
||||
assert.deepEqual(tokenize('.23e-3'), [{type: 0 /* Number */, value: 0.00023}]);
|
||||
assert.deepEqual(tokenize('23.e-03'), [{type: 0 /* Number */, value: 0.023}]);
|
||||
|
||||
assert.deepEqual(tokenize("12.1E2"), [{type: 0 /* Number */, value: 1210}]);
|
||||
assert.deepEqual(tokenize("1.23e+03"), [{type: 0 /* Number */, value: 1230}]);
|
||||
assert.deepEqual(tokenize('.23e2'), [{type: 0 /* Number */, value: 23}]);
|
||||
assert.deepEqual(tokenize('15.E+02'), [{type: 0 /* Number */, value: 1500}]);
|
||||
|
||||
assert.deepEqual(tokenize("-23E02"), [
|
||||
{type: 2 /* Symbol */, value: "-"},
|
||||
{type: 0 /* Number */, value: 2300}
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
QUnit.test("can tokenize strings", (assert) => {
|
||||
assert.deepEqual(tokenize('"foo"'), [{ type: 1 /* String */, value: "foo" }]);
|
||||
});
|
||||
|
||||
QUnit.test("can tokenize bare names", (assert) => {
|
||||
assert.deepEqual(tokenize("foo"), [{ type: 3 /* Name */, value: "foo" }]);
|
||||
});
|
||||
|
||||
QUnit.test("can tokenize misc operators", (assert) => {
|
||||
assert.deepEqual(tokenize("in"), [{ type: 2 /* Symbol */, value: "in" }]);
|
||||
assert.deepEqual(tokenize("not in"), [{ type: 2 /* Symbol */, value: "not in" }]);
|
||||
assert.deepEqual(tokenize("3 ** 2")[1], { type: 2 /* Symbol */, value: "**" });
|
||||
});
|
||||
|
||||
QUnit.test("can tokenize constants", (assert) => {
|
||||
assert.deepEqual(tokenize("None"), [{ type: 4 /* Constant */, value: "None" }]);
|
||||
assert.deepEqual(tokenize("True"), [{ type: 4 /* Constant */, value: "True" }]);
|
||||
assert.deepEqual(tokenize("False"), [{ type: 4 /* Constant */, value: "False" }]);
|
||||
});
|
||||
|
||||
QUnit.test("can tokenize parenthesis", (assert) => {
|
||||
assert.deepEqual(tokenize("()"), [
|
||||
{ type: 2 /* Symbol */, value: "(" },
|
||||
{ type: 2 /* Symbol */, value: ")" },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("can tokenize function with kwargs", (assert) => {
|
||||
assert.deepEqual(tokenize('foo(bar=3, qux="4")'), [
|
||||
{ type: 3 /* Name */, value: "foo" },
|
||||
{ type: 2 /* Symbol */, value: "(" },
|
||||
{ type: 3 /* Name */, value: "bar" },
|
||||
{ type: 2 /* Symbol */, value: "=" },
|
||||
{ type: 0 /* Number */, value: 3 },
|
||||
{ type: 2 /* Symbol */, value: "," },
|
||||
{ type: 3 /* Name */, value: "qux" },
|
||||
{ type: 2 /* Symbol */, value: "=" },
|
||||
{ type: 1 /* String */, value: "4" },
|
||||
{ type: 2 /* Symbol */, value: ")" },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("can tokenize if statement", (assert) => {
|
||||
assert.deepEqual(tokenize("1 if True else 2"), [
|
||||
{ type: 0 /* Number */, value: 1 },
|
||||
{ type: 2 /* Symbol */, value: "if" },
|
||||
{ type: 4 /* Constant */, value: "True" },
|
||||
{ type: 2 /* Symbol */, value: "else" },
|
||||
{ type: 0 /* Number */, value: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("sanity check: throw some errors", (assert) => {
|
||||
assert.throws(() => tokenize("'asdf"));
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { evaluateExpr, formatAST, parseExpr } from "@web/core/py_js/py";
|
||||
import { toPyValue } from "@web/core/py_js/py_utils";
|
||||
import { PyDate, PyDateTime } from "@web/core/py_js/py_date";
|
||||
|
||||
QUnit.module("py", {}, () => {
|
||||
QUnit.module("formatAST");
|
||||
function checkAST(expr, message = expr) {
|
||||
const ast = parseExpr(expr);
|
||||
const str = formatAST(ast);
|
||||
if (str !== expr) {
|
||||
throw new Error(`Mismatch: ${str} !== ${expr} (${message});`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QUnit.test("basic values", function (assert) {
|
||||
assert.ok(checkAST("1", "integer value"));
|
||||
assert.ok(checkAST("1.4", "float value"));
|
||||
assert.ok(checkAST("-12", "negative integer value"));
|
||||
assert.ok(checkAST("True", "boolean"));
|
||||
assert.ok(checkAST(`"some string"`, "a string"));
|
||||
assert.ok(checkAST("None", "None"));
|
||||
});
|
||||
|
||||
QUnit.test("dictionary", function (assert) {
|
||||
assert.ok(checkAST("{}", "empty dictionary"));
|
||||
assert.ok(checkAST(`{"a": 1}`, "dictionary with a single key"));
|
||||
assert.ok(checkAST(`d["a"]`, "get a value in a dictionary"));
|
||||
});
|
||||
|
||||
QUnit.test("list", function (assert) {
|
||||
assert.ok(checkAST("[]", "empty list"));
|
||||
assert.ok(checkAST("[1]", "list with one value"));
|
||||
assert.ok(checkAST("[1, 2]", "list with two values"));
|
||||
});
|
||||
|
||||
QUnit.test("tuple", function (assert) {
|
||||
assert.ok(checkAST("()", "empty tuple"));
|
||||
assert.ok(checkAST("(1, 2)", "basic tuple"));
|
||||
});
|
||||
|
||||
QUnit.test("simple arithmetic", function (assert) {
|
||||
assert.ok(checkAST("1 + 2", "addition"));
|
||||
assert.ok(checkAST("+(1 + 2)", "other addition, prefix"));
|
||||
assert.ok(checkAST("1 - 2", "substraction"));
|
||||
assert.ok(checkAST("-1 - 2", "other substraction"));
|
||||
assert.ok(checkAST("-(1 + 2)", "other substraction"));
|
||||
assert.ok(checkAST("1 + 2 + 3", "addition of 3 integers"));
|
||||
assert.ok(checkAST("a + b", "addition of two variables"));
|
||||
assert.ok(checkAST("42 % 5", "modulo operator"));
|
||||
assert.ok(checkAST("a * 10", "multiplication"));
|
||||
assert.ok(checkAST("a ** 10", "**"));
|
||||
assert.ok(checkAST("~10", "bitwise not"));
|
||||
assert.ok(checkAST("~(10 + 3)", "bitwise not"));
|
||||
assert.ok(checkAST("a * (1 + 2)", "multiplication and addition"));
|
||||
assert.ok(checkAST("(a + b) * 43", "addition and multiplication"));
|
||||
assert.ok(checkAST("a // 10", "integer division"));
|
||||
});
|
||||
|
||||
QUnit.test("boolean operators", function (assert) {
|
||||
assert.ok(checkAST("True and False", "boolean operator"));
|
||||
assert.ok(checkAST("True or False", "boolean operator or"));
|
||||
assert.ok(checkAST("(True or False) and False", "boolean operators and and or"));
|
||||
assert.ok(checkAST("not False", "not prefix"));
|
||||
assert.ok(checkAST("not foo", "not prefix with variable"));
|
||||
assert.ok(checkAST("not a in b", "not prefix with expression"));
|
||||
});
|
||||
|
||||
QUnit.test("conditional expression", function (assert) {
|
||||
assert.ok(checkAST("1 if a else 2"));
|
||||
assert.ok(checkAST("[] if a else 2"));
|
||||
});
|
||||
|
||||
QUnit.test("other operators", function (assert) {
|
||||
assert.ok(checkAST("x == y", "== operator"));
|
||||
assert.ok(checkAST("x != y", "!= operator"));
|
||||
assert.ok(checkAST("x < y", "< operator"));
|
||||
assert.ok(checkAST("x is y", "is operator"));
|
||||
assert.ok(checkAST("x is not y", "is and not operator"));
|
||||
assert.ok(checkAST("x in y", "in operator"));
|
||||
assert.ok(checkAST("x not in y", "not in operator"));
|
||||
});
|
||||
|
||||
QUnit.test("equality", function (assert) {
|
||||
assert.ok(checkAST("a == b", "simple equality"));
|
||||
});
|
||||
|
||||
QUnit.test("strftime", function (assert) {
|
||||
assert.ok(checkAST(`time.strftime("%Y")`, "strftime with year"));
|
||||
assert.ok(checkAST(`time.strftime("%Y") + "-01-30"`, "strftime with year"));
|
||||
assert.ok(checkAST(`time.strftime("%Y-%m-%d %H:%M:%S")`, "strftime with year"));
|
||||
});
|
||||
|
||||
QUnit.test("context_today", function (assert) {
|
||||
assert.ok(checkAST(`context_today().strftime("%Y-%m-%d")`, "context today call"));
|
||||
});
|
||||
|
||||
QUnit.test("function call", function (assert) {
|
||||
assert.ok(checkAST("td()", "simple call"));
|
||||
assert.ok(checkAST("td(a, b, c)", "simple call with args"));
|
||||
assert.ok(checkAST("td(days = 1)", "simple call with kwargs"));
|
||||
assert.ok(checkAST("f(1, 2, days = 1)", "mixing args and kwargs"));
|
||||
assert.ok(checkAST("str(td(2))", "function call in function call"));
|
||||
});
|
||||
|
||||
QUnit.test("various expressions", function (assert) {
|
||||
assert.ok(checkAST("(a - b).days", "substraction and .days"));
|
||||
assert.ok(checkAST("a + day == date(2002, 3, 3)"));
|
||||
const expr = `[("type", "=", "in"), ("day", "<=", time.strftime("%Y-%m-%d")), ("day", ">", (context_today() - datetime.timedelta(days = 15)).strftime("%Y-%m-%d"))]`;
|
||||
assert.ok(checkAST(expr));
|
||||
});
|
||||
|
||||
QUnit.test("escaping support", function (assert) {
|
||||
assert.strictEqual(evaluateExpr(String.raw`"\x61"`), "a", "hex escapes");
|
||||
assert.strictEqual(
|
||||
evaluateExpr(String.raw`"\\abc"`),
|
||||
String.raw`\abc`,
|
||||
"escaped backslash"
|
||||
);
|
||||
assert.ok(checkAST(String.raw`"\\abc"`, "escaped backslash AST check"));
|
||||
const a = String.raw`'foo\\abc"\''`;
|
||||
const b = formatAST(parseExpr(formatAST(parseExpr(a))));
|
||||
// Our repr uses JSON.stringify which always uses double quotes,
|
||||
// whereas Python's repr is single-quote-biased: strings are repr'd
|
||||
// using single quote delimiters *unless* they contain single quotes and
|
||||
// no double quotes, then they're delimited with double quotes.
|
||||
assert.strictEqual(b, String.raw`"foo\\abc\"'"`);
|
||||
});
|
||||
|
||||
QUnit.test("null value", function (assert) {
|
||||
assert.strictEqual(formatAST(toPyValue(null)), "None");
|
||||
});
|
||||
|
||||
QUnit.module("toPyValue");
|
||||
|
||||
QUnit.test("toPyValue a string", function (assert) {
|
||||
const ast = toPyValue("test");
|
||||
assert.strictEqual(ast.type, 1);
|
||||
assert.strictEqual(ast.value, "test");
|
||||
assert.strictEqual(formatAST(ast), '"test"');
|
||||
});
|
||||
|
||||
QUnit.test("toPyValue a number", function (assert) {
|
||||
const ast = toPyValue(1);
|
||||
assert.strictEqual(ast.type, 0);
|
||||
assert.strictEqual(ast.value, 1);
|
||||
assert.strictEqual(formatAST(ast), "1");
|
||||
});
|
||||
|
||||
QUnit.test("toPyValue a boolean", function (assert) {
|
||||
let ast = toPyValue(true);
|
||||
assert.strictEqual(ast.type, 2);
|
||||
assert.strictEqual(ast.value, true);
|
||||
assert.strictEqual(formatAST(ast), "True");
|
||||
|
||||
ast = toPyValue(false);
|
||||
assert.strictEqual(ast.type, 2);
|
||||
assert.strictEqual(ast.value, false);
|
||||
assert.strictEqual(formatAST(ast), "False");
|
||||
});
|
||||
|
||||
QUnit.test("toPyValue a object", function (assert) {
|
||||
const ast = toPyValue({ a: 1 });
|
||||
assert.strictEqual(ast.type, 11);
|
||||
assert.ok("a" in ast.value);
|
||||
assert.ok(["type", "value"].every((prop) => prop in ast.value.a));
|
||||
assert.strictEqual(ast.value.a.type, 0);
|
||||
assert.strictEqual(ast.value.a.value, 1);
|
||||
assert.strictEqual(formatAST(ast), '{"a": 1}');
|
||||
});
|
||||
|
||||
QUnit.test("toPyValue a date", function (assert) {
|
||||
const date = new Date(Date.UTC(2000, 0, 1));
|
||||
const ast = toPyValue(date);
|
||||
assert.strictEqual(ast.type, 1);
|
||||
const expectedValue = PyDateTime.convertDate(date);
|
||||
assert.ok(ast.value.isEqual(expectedValue));
|
||||
assert.strictEqual(formatAST(ast), JSON.stringify(expectedValue));
|
||||
});
|
||||
|
||||
QUnit.test("toPyValue a dateime", function (assert) {
|
||||
const datetime = new Date(Date.UTC(2000, 0, 1, 1, 0, 0, 0));
|
||||
const ast = toPyValue(datetime);
|
||||
assert.strictEqual(ast.type, 1);
|
||||
const expectedValue = PyDateTime.convertDate(datetime);
|
||||
assert.ok(ast.value.isEqual(expectedValue));
|
||||
assert.strictEqual(formatAST(ast), JSON.stringify(expectedValue));
|
||||
});
|
||||
|
||||
QUnit.test("toPyValue a PyDate", function (assert) {
|
||||
const value = new PyDate(2000, 1, 1);
|
||||
const ast = toPyValue(value);
|
||||
assert.strictEqual(ast.type, 1);
|
||||
assert.strictEqual(ast.value, value);
|
||||
assert.strictEqual(formatAST(ast), JSON.stringify(value));
|
||||
});
|
||||
|
||||
QUnit.test("toPyValue a PyDateTime", function (assert) {
|
||||
const value = new PyDateTime(2000, 1, 1, 1, 0, 0, 0);
|
||||
const ast = toPyValue(value);
|
||||
assert.strictEqual(ast.type, 1);
|
||||
assert.strictEqual(ast.value, value);
|
||||
assert.strictEqual(formatAST(ast), JSON.stringify(value));
|
||||
});
|
||||
});
|
||||
159
odoo-bringout-oca-ocb-web/web/static/tests/core/registry_test.js
Normal file
159
odoo-bringout-oca-ocb-web/web/static/tests/core/registry_test.js
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Registry } from "@web/core/registry";
|
||||
|
||||
QUnit.module("Registry");
|
||||
|
||||
QUnit.test("key set and get", function (assert) {
|
||||
const registry = new Registry();
|
||||
const foo = {};
|
||||
|
||||
registry.add("foo", foo);
|
||||
|
||||
assert.strictEqual(registry.get("foo"), foo);
|
||||
});
|
||||
|
||||
QUnit.test("can set and get falsy values", function (assert) {
|
||||
const registry = new Registry();
|
||||
registry.add("foo1", false);
|
||||
registry.add("foo2", 0);
|
||||
registry.add("foo3", "");
|
||||
registry.add("foo4", undefined);
|
||||
registry.add("foo5", null);
|
||||
assert.strictEqual(registry.get("foo1"), false);
|
||||
assert.strictEqual(registry.get("foo2"), 0);
|
||||
assert.strictEqual(registry.get("foo3"), "");
|
||||
assert.strictEqual(registry.get("foo4"), undefined);
|
||||
assert.strictEqual(registry.get("foo5"), null);
|
||||
});
|
||||
|
||||
QUnit.test("can set and get falsy values with default value", function (assert) {
|
||||
const registry = new Registry();
|
||||
registry.add("foo1", false);
|
||||
registry.add("foo2", 0);
|
||||
registry.add("foo3", "");
|
||||
registry.add("foo4", undefined);
|
||||
registry.add("foo5", null);
|
||||
assert.strictEqual(registry.get("foo1", 1), false);
|
||||
assert.strictEqual(registry.get("foo2", 1), 0);
|
||||
assert.strictEqual(registry.get("foo3", 1), "");
|
||||
assert.strictEqual(registry.get("foo4", 1), undefined);
|
||||
assert.strictEqual(registry.get("foo5", 1), null);
|
||||
});
|
||||
|
||||
QUnit.test("can get a default value when missing key", function (assert) {
|
||||
const registry = new Registry();
|
||||
assert.strictEqual(registry.get("missing", "default"), "default");
|
||||
assert.strictEqual(registry.get("missing", null), null);
|
||||
assert.strictEqual(registry.get("missing", false), false);
|
||||
});
|
||||
|
||||
QUnit.test("throws if key is missing", function (assert) {
|
||||
const registry = new Registry();
|
||||
assert.throws(() => registry.get("missing"));
|
||||
});
|
||||
|
||||
QUnit.test("contains method", function (assert) {
|
||||
const registry = new Registry();
|
||||
|
||||
registry.add("foo", 1);
|
||||
|
||||
assert.ok(registry.contains("foo"));
|
||||
assert.notOk(registry.contains("bar"));
|
||||
});
|
||||
|
||||
QUnit.test("can set and get a value, with an order arg", function (assert) {
|
||||
const registry = new Registry();
|
||||
const foo = {};
|
||||
|
||||
registry.add("foo", foo, { sequence: 24 });
|
||||
|
||||
assert.strictEqual(registry.get("foo"), foo);
|
||||
});
|
||||
|
||||
QUnit.test("can get ordered list of elements", function (assert) {
|
||||
const registry = new Registry();
|
||||
|
||||
registry
|
||||
.add("foo1", "foo1", { sequence: 1 })
|
||||
.add("foo2", "foo2", { sequence: 2 })
|
||||
.add("foo5", "foo5", { sequence: 5 })
|
||||
.add("foo3", "foo3", { sequence: 3 });
|
||||
|
||||
assert.deepEqual(registry.getAll(), ["foo1", "foo2", "foo3", "foo5"]);
|
||||
});
|
||||
|
||||
QUnit.test("can get ordered list of entries", function (assert) {
|
||||
const registry = new Registry();
|
||||
|
||||
registry
|
||||
.add("foo1", "foo1", { sequence: 1 })
|
||||
.add("foo2", "foo2", { sequence: 2 })
|
||||
.add("foo5", "foo5", { sequence: 5 })
|
||||
.add("foo3", "foo3", { sequence: 3 });
|
||||
|
||||
assert.deepEqual(registry.getEntries(), [
|
||||
["foo1", "foo1"],
|
||||
["foo2", "foo2"],
|
||||
["foo3", "foo3"],
|
||||
["foo5", "foo5"],
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("getAll and getEntries returns shallow copies", function (assert) {
|
||||
const registry = new Registry();
|
||||
|
||||
registry.add("foo1", "foo1");
|
||||
|
||||
const all = registry.getAll();
|
||||
const entries = registry.getEntries();
|
||||
|
||||
assert.deepEqual(all, ["foo1"]);
|
||||
assert.deepEqual(entries, [["foo1", "foo1"]]);
|
||||
|
||||
all.push("foo2");
|
||||
entries.push(["foo2", "foo2"]);
|
||||
|
||||
assert.deepEqual(all, ["foo1", "foo2"]);
|
||||
assert.deepEqual(entries, [
|
||||
["foo1", "foo1"],
|
||||
["foo2", "foo2"],
|
||||
]);
|
||||
assert.deepEqual(registry.getAll(), ["foo1"]);
|
||||
assert.deepEqual(registry.getEntries(), [["foo1", "foo1"]]);
|
||||
});
|
||||
|
||||
QUnit.test("can override element with sequence", function (assert) {
|
||||
const registry = new Registry();
|
||||
|
||||
registry
|
||||
.add("foo1", "foo1", { sequence: 1 })
|
||||
.add("foo2", "foo2", { sequence: 2 })
|
||||
.add("foo1", "foo3", { force: true });
|
||||
|
||||
assert.deepEqual(registry.getEntries(), [
|
||||
["foo1", "foo3"],
|
||||
["foo2", "foo2"],
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("can override element with sequence 2 ", function (assert) {
|
||||
const registry = new Registry();
|
||||
|
||||
registry
|
||||
.add("foo1", "foo1", { sequence: 1 })
|
||||
.add("foo2", "foo2", { sequence: 2 })
|
||||
.add("foo1", "foo3", { force: true, sequence: 3 });
|
||||
|
||||
assert.deepEqual(registry.getEntries(), [
|
||||
["foo2", "foo2"],
|
||||
["foo1", "foo3"],
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("can recursively open sub registry", function (assert) {
|
||||
const registry = new Registry();
|
||||
|
||||
registry.category("sub").add("a", "b");
|
||||
assert.deepEqual(registry.category("sub").get("a"), "b");
|
||||
});
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { parseHash, parseSearchQuery, routeToUrl } from "@web/core/browser/router_service";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { makeFakeRouterService } from "../helpers/mock_services";
|
||||
import { nextTick, patchWithCleanup } from "../helpers/utils";
|
||||
|
||||
import { EventBus } from "@odoo/owl";
|
||||
|
||||
async function createRouter(params = {}) {
|
||||
const env = params.env || {};
|
||||
env.bus = env.bus || new EventBus();
|
||||
if (params.onPushState) {
|
||||
const originalPushState = browser.history.pushState;
|
||||
const onPushState = params.onPushState;
|
||||
delete params.onPushState;
|
||||
patchWithCleanup(browser, {
|
||||
history: Object.assign({}, browser.history, {
|
||||
pushState() {
|
||||
originalPushState(...arguments);
|
||||
onPushState(...arguments);
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
const router = await makeFakeRouterService(params).start(env);
|
||||
return router;
|
||||
}
|
||||
|
||||
QUnit.module("Router");
|
||||
|
||||
QUnit.test("can parse an empty hash", (assert) => {
|
||||
assert.deepEqual(parseHash(""), {});
|
||||
});
|
||||
|
||||
QUnit.test("can parse an single hash", (assert) => {
|
||||
assert.deepEqual(parseHash("#"), {});
|
||||
});
|
||||
|
||||
QUnit.test("can parse a hash with a single key/value pair", (assert) => {
|
||||
const hash = "#action=114";
|
||||
assert.deepEqual(parseHash(hash), { action: 114 });
|
||||
});
|
||||
|
||||
QUnit.test("can parse a hash with 2 key/value pairs", (assert) => {
|
||||
const hash = "#action=114&active_id=mail.box_inbox";
|
||||
assert.deepEqual(parseHash(hash), { action: 114, active_id: "mail.box_inbox" });
|
||||
});
|
||||
|
||||
QUnit.test("a missing value is encoded as an empty string", (assert) => {
|
||||
const hash = "#action";
|
||||
assert.deepEqual(parseHash(hash), { action: "" });
|
||||
});
|
||||
|
||||
QUnit.test("a missing value is encoded as an empty string -- 2", (assert) => {
|
||||
const hash = "#action=";
|
||||
assert.deepEqual(parseHash(hash), { action: "" });
|
||||
});
|
||||
|
||||
QUnit.test("can parse a realistic hash", (assert) => {
|
||||
const hash = "#action=114&active_id=mail.box_inbox&cids=1&menu_id=91";
|
||||
const expected = {
|
||||
action: 114,
|
||||
active_id: "mail.box_inbox",
|
||||
cids: 1,
|
||||
menu_id: 91,
|
||||
};
|
||||
assert.deepEqual(parseHash(hash), expected);
|
||||
});
|
||||
|
||||
QUnit.test("can parse an empty search", (assert) => {
|
||||
assert.deepEqual(parseSearchQuery(""), {});
|
||||
});
|
||||
|
||||
QUnit.test("can parse an simple search with no value", (assert) => {
|
||||
assert.deepEqual(parseSearchQuery("?a"), { a: "" });
|
||||
});
|
||||
|
||||
QUnit.test("can parse an simple search with a value", (assert) => {
|
||||
assert.deepEqual(parseSearchQuery("?a=1"), { a: 1 });
|
||||
});
|
||||
|
||||
QUnit.test("can parse an search with 2 key/value pairs", (assert) => {
|
||||
assert.deepEqual(parseSearchQuery("?a=1&b=2"), { a: 1, b: 2 });
|
||||
});
|
||||
|
||||
QUnit.test("can parse URI encoded strings", (assert) => {
|
||||
assert.deepEqual(parseSearchQuery("?space=this%20is"), { space: "this is" });
|
||||
assert.deepEqual(parseHash("#comma=that%2Cis"), { comma: "that,is" });
|
||||
});
|
||||
|
||||
QUnit.test("routeToUrl encodes URI compatible strings", (assert) => {
|
||||
const route = { pathname: "/asf", search: {}, hash: {} };
|
||||
assert.strictEqual(routeToUrl(route), "/asf");
|
||||
|
||||
route.search = { a: "11", g: "summer wine" };
|
||||
assert.strictEqual(routeToUrl(route), "/asf?a=11&g=summer%20wine");
|
||||
|
||||
route.hash = { b: "2", c: "", e: "kloug,gloubi" };
|
||||
assert.strictEqual(routeToUrl(route), "/asf?a=11&g=summer%20wine#b=2&c=&e=kloug%2Cgloubi");
|
||||
});
|
||||
|
||||
QUnit.test("can redirect an URL", async (assert) => {
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout(handler, delay) {
|
||||
handler();
|
||||
assert.step(`timeout: ${delay}`);
|
||||
},
|
||||
});
|
||||
let firstCheckServer = true;
|
||||
const env = await makeTestEnv({
|
||||
async mockRPC(route) {
|
||||
if (route === "/web/webclient/version_info") {
|
||||
if (firstCheckServer) {
|
||||
firstCheckServer = false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
const onRedirect = (url) => assert.step(url);
|
||||
const router = await createRouter({ env, onRedirect });
|
||||
|
||||
router.redirect("/my/test/url");
|
||||
await nextTick();
|
||||
assert.verifySteps(["/my/test/url"]);
|
||||
|
||||
router.redirect("/my/test/url/2", true);
|
||||
await nextTick();
|
||||
assert.verifySteps(["timeout: 1000", "timeout: 250", "/my/test/url/2"]);
|
||||
});
|
||||
|
||||
QUnit.module("Router: Push state");
|
||||
|
||||
QUnit.test("can push in same timeout", async (assert) => {
|
||||
const router = await createRouter();
|
||||
|
||||
assert.deepEqual(router.current.hash, {});
|
||||
|
||||
router.pushState({ k1: 2 });
|
||||
assert.deepEqual(router.current.hash, {});
|
||||
|
||||
router.pushState({ k1: 3 });
|
||||
assert.deepEqual(router.current.hash, {});
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 3 });
|
||||
});
|
||||
|
||||
QUnit.test("can lock keys", async (assert) => {
|
||||
const router = await createRouter();
|
||||
|
||||
router.pushState({ k1: 2 }, { lock: true });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 2 });
|
||||
|
||||
router.pushState({ k1: 3 });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 2 });
|
||||
|
||||
router.pushState({ k1: 4 }, { lock: true });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 4 });
|
||||
|
||||
router.pushState({ k1: 3 });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 4 });
|
||||
});
|
||||
|
||||
QUnit.test("can re-lock keys in same final call", async (assert) => {
|
||||
const router = await createRouter();
|
||||
|
||||
router.pushState({ k1: 2 }, { lock: true });
|
||||
await nextTick();
|
||||
router.pushState({ k1: 1 }, { lock: true });
|
||||
router.pushState({ k1: 4 });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 1 });
|
||||
});
|
||||
|
||||
QUnit.test("can unlock keys", async (assert) => {
|
||||
const router = await createRouter();
|
||||
|
||||
router.pushState({ k1: 2 }, { lock: true });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 2 });
|
||||
|
||||
router.pushState({ k1: 3 });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 2 });
|
||||
|
||||
router.pushState({ k1: 4 }, { lock: false });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 4 });
|
||||
|
||||
router.pushState({ k1: 3 });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 3 });
|
||||
});
|
||||
|
||||
QUnit.test("can replace hash", async (assert) => {
|
||||
const router = await createRouter();
|
||||
|
||||
router.pushState({ k1: 2 });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 2 });
|
||||
|
||||
router.pushState({ k2: 3 }, { replace: true });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k2: 3 });
|
||||
});
|
||||
|
||||
QUnit.test("can replace hash with locked keys", async (assert) => {
|
||||
const router = await createRouter();
|
||||
|
||||
router.pushState({ k1: 2 }, { lock: true });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 2 });
|
||||
|
||||
router.pushState({ k2: 3 }, { replace: true });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 2, k2: 3 });
|
||||
});
|
||||
|
||||
QUnit.test("can merge hash", async (assert) => {
|
||||
const router = await createRouter();
|
||||
|
||||
router.pushState({ k1: 2 });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 2 });
|
||||
|
||||
router.pushState({ k2: 3 });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 2, k2: 3 });
|
||||
});
|
||||
|
||||
QUnit.test("undefined keys are not pushed", async (assert) => {
|
||||
const onPushState = () => assert.step("pushed state");
|
||||
const router = await createRouter({ onPushState });
|
||||
|
||||
router.pushState({ k1: undefined });
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
assert.deepEqual(router.current.hash, {});
|
||||
});
|
||||
|
||||
QUnit.test("undefined keys destroy previous non locked keys", async (assert) => {
|
||||
const router = await createRouter();
|
||||
|
||||
router.pushState({ k1: 1 });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, { k1: 1 });
|
||||
|
||||
router.pushState({ k1: undefined });
|
||||
await nextTick();
|
||||
assert.deepEqual(router.current.hash, {});
|
||||
});
|
||||
|
||||
QUnit.test("do not re-push when hash is same", async (assert) => {
|
||||
const onPushState = () => assert.step("pushed state");
|
||||
const router = await createRouter({ onPushState });
|
||||
|
||||
router.pushState({ k1: 1, k2: 2 });
|
||||
await nextTick();
|
||||
assert.verifySteps(["pushed state"]);
|
||||
|
||||
router.pushState({ k2: 2, k1: 1 });
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("do not re-push when hash is same (with integers as strings)", async (assert) => {
|
||||
const onPushState = () => assert.step("pushed state");
|
||||
const router = await createRouter({ onPushState });
|
||||
|
||||
router.pushState({ k1: 1, k2: "2" });
|
||||
await nextTick();
|
||||
assert.verifySteps(["pushed state"]);
|
||||
|
||||
router.pushState({ k2: 2, k1: "1" });
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
|
@ -0,0 +1,396 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { scrollerService } from "@web/core/scroller_service";
|
||||
import { scrollTo } from "@web/core/utils/scrolling";
|
||||
import { registerCleanup } from "../helpers/cleanup";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { click, getFixture, mount, nextTick } from "../helpers/utils";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let env;
|
||||
let target;
|
||||
|
||||
QUnit.module("ScrollerService", {
|
||||
async beforeEach() {
|
||||
serviceRegistry.add("scroller", scrollerService);
|
||||
env = await makeTestEnv();
|
||||
target = getFixture();
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("Ignore empty hrefs", async (assert) => {
|
||||
assert.expect(1);
|
||||
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml/* xml */ `
|
||||
<div class="my_component">
|
||||
<a href="#" class="inactive_link">This link does nothing</a>
|
||||
<button class="btn btn-secondary">
|
||||
<a href="#">
|
||||
<i class="fa fa-trash"/>
|
||||
</a>
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
await mount(MyComponent, target, { env });
|
||||
|
||||
/**
|
||||
* To determine whether the hash changed we need to use a custom hash for
|
||||
* this test. Note that changing the hash does not reload the page and is
|
||||
* rollbacked after the test so it should not not interfere with the test suite.
|
||||
*/
|
||||
const initialHash = location.hash;
|
||||
const testHash = initialHash ? `${initialHash}&testscroller` : "#testscroller";
|
||||
location.hash = testHash;
|
||||
registerCleanup(() => (location.hash = initialHash));
|
||||
|
||||
target.querySelector(".inactive_link").click();
|
||||
await nextTick();
|
||||
|
||||
target.querySelector(".fa.fa-trash").click();
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(location.hash, testHash);
|
||||
});
|
||||
|
||||
QUnit.test("Simple rendering with a scroll", async (assert) => {
|
||||
assert.expect(2);
|
||||
const scrollableParent = document.createElement("div");
|
||||
scrollableParent.style.overflow = "scroll";
|
||||
scrollableParent.style.height = "150px";
|
||||
scrollableParent.style.width = "400px";
|
||||
target.append(scrollableParent);
|
||||
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml/* xml */ `
|
||||
<div class="o_content">
|
||||
<a href="#scrollToHere" class="btn btn-primary">sroll to ...</a>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.
|
||||
Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,
|
||||
dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper
|
||||
congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est
|
||||
eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu
|
||||
massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut
|
||||
in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent
|
||||
egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue
|
||||
blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices
|
||||
posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque
|
||||
fermentum. Maecenas adipiscing ante non diam sodales hendrerit.
|
||||
</p>
|
||||
<p>
|
||||
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa
|
||||
suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc
|
||||
turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus
|
||||
nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis
|
||||
metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing
|
||||
elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor
|
||||
tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis
|
||||
vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus,
|
||||
et tristique ligula justo vitae magna.
|
||||
</p>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus,
|
||||
felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
|
||||
</p>
|
||||
<div id="scrollToHere">sroll here!</div>
|
||||
</div>
|
||||
`;
|
||||
await mount(MyComponent, scrollableParent, { env });
|
||||
|
||||
assert.strictEqual(scrollableParent.scrollTop, 0);
|
||||
await click(scrollableParent, ".btn.btn-primary");
|
||||
assert.ok(scrollableParent.scrollTop !== 0);
|
||||
});
|
||||
|
||||
QUnit.test("Rendering with multiple anchors and scrolls", async (assert) => {
|
||||
assert.expect(4);
|
||||
const scrollableParent = document.createElement("div");
|
||||
scrollableParent.style.overflow = "scroll";
|
||||
scrollableParent.style.height = "150px";
|
||||
scrollableParent.style.width = "400px";
|
||||
target.append(scrollableParent);
|
||||
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml/* xml */ `
|
||||
<div class="o_content">
|
||||
<h2 id="anchor3">ANCHOR 3</h2>
|
||||
<a href="#anchor1" class="link1">sroll to ...</a>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.
|
||||
Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,
|
||||
dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper
|
||||
congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est
|
||||
eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu
|
||||
massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut
|
||||
in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent
|
||||
egestas leo in pede. Praesent blandit odio eu enim.
|
||||
</p>
|
||||
<p>
|
||||
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa
|
||||
suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc
|
||||
turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus
|
||||
nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis
|
||||
metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing
|
||||
elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor
|
||||
tellus, aliquam faucibus, convallis id, congue eu, quam.
|
||||
</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h2 id="anchor2">ANCHOR 2</h2>
|
||||
<a href="#anchor3" class="link3">TO ANCHOR 3</a>
|
||||
<p>
|
||||
The table forces you to get the precise position of the element.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue.
|
||||
</p>
|
||||
<div id="anchor1">sroll here!</div>
|
||||
<a href="#anchor2" class="link2">TO ANCHOR 2</a>
|
||||
</div>
|
||||
`;
|
||||
await mount(MyComponent, scrollableParent, { env });
|
||||
assert.strictEqual(scrollableParent.scrollTop, 0);
|
||||
await click(scrollableParent, ".link1");
|
||||
|
||||
// The element must be contained in the scrollable parent (top and bottom)
|
||||
const isVisible = (el) => {
|
||||
return (
|
||||
el.getBoundingClientRect().bottom <= scrollableParent.getBoundingClientRect().bottom &&
|
||||
el.getBoundingClientRect().top >= scrollableParent.getBoundingClientRect().top
|
||||
);
|
||||
};
|
||||
assert.ok(isVisible(scrollableParent.querySelector("#anchor1")));
|
||||
await click(scrollableParent, ".link2");
|
||||
assert.ok(isVisible(scrollableParent.querySelector("#anchor2")));
|
||||
await click(scrollableParent, ".link3");
|
||||
assert.ok(isVisible(scrollableParent.querySelector("#anchor3")));
|
||||
});
|
||||
|
||||
QUnit.test("clicking anchor when no scrollable", async (assert) => {
|
||||
assert.expect(3);
|
||||
const scrollableParent = document.createElement("div");
|
||||
scrollableParent.style.overflow = "auto";
|
||||
scrollableParent.style.height = "150px";
|
||||
scrollableParent.style.width = "400px";
|
||||
target.append(scrollableParent);
|
||||
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml/* xml */ `
|
||||
<div class="o_content">
|
||||
<a href="#scrollToHere" class="btn btn-primary">scroll to ...</a>
|
||||
<div class="active-container">
|
||||
<p>There is no scrollable with only the height of this element</p>
|
||||
</div>
|
||||
<div class="inactive-container" style="max-height: 0">
|
||||
<h2>There should be no scrollable if this element has 0 height</h2>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus,
|
||||
felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
|
||||
</p>
|
||||
<div id="scrollToHere">should try to scroll here only if scrollable!</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
await mount(MyComponent, scrollableParent, { env });
|
||||
assert.strictEqual(scrollableParent.scrollTop, 0);
|
||||
await click(scrollableParent, ".btn.btn-primary");
|
||||
assert.ok(scrollableParent.scrollTop === 0, "no scroll happened");
|
||||
scrollableParent.querySelector(".inactive-container").style.maxHeight = "unset";
|
||||
await click(scrollableParent, ".btn.btn-primary");
|
||||
assert.ok(scrollableParent.scrollTop !== 0, "a scroll happened");
|
||||
});
|
||||
|
||||
QUnit.test("clicking anchor when multi levels scrollables", async (assert) => {
|
||||
assert.expect(4);
|
||||
const scrollableParent = document.createElement("div");
|
||||
scrollableParent.style.overflow = "auto";
|
||||
scrollableParent.style.height = "150px";
|
||||
scrollableParent.style.width = "400px";
|
||||
target.append(scrollableParent);
|
||||
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml/* xml */ `
|
||||
<div class="o_content scrollable-1">
|
||||
<a href="#scroll1" class="btn1 btn btn-primary">go to level 2 anchor</a>
|
||||
<div>
|
||||
<p>This is some content</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.
|
||||
Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,
|
||||
dolor. Cras elementum ultrices diam. Maecenas
|
||||
</p>
|
||||
</div>
|
||||
<div class="scrollable-2" style="background: green; overflow: auto; height: 100px;">
|
||||
<h2>This is level 1 of scrollable</h2>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus,
|
||||
felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
|
||||
</p>
|
||||
<div style="background: lime;">
|
||||
<h2>This is level 2 of scrollable</h2>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus,
|
||||
felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
|
||||
</p>
|
||||
<div id="scroll1" style="background: orange;">this element is contained in a scrollable metaverse!</div>
|
||||
<a href="#scroll2" class="btn2 btn btn-primary">go to level 1 anchor</a>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.
|
||||
Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,
|
||||
dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper
|
||||
congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est
|
||||
eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu
|
||||
massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut
|
||||
in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent
|
||||
egestas leo in pede. Praesent blandit odio eu enim.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="scroll2" style="background: orange;">this is an anchor at level 1!</div>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus,
|
||||
felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
await mount(MyComponent, scrollableParent, { env });
|
||||
|
||||
const border = (el) => {
|
||||
// Returns the state of the element in relation to the borders
|
||||
const element = el.getBoundingClientRect();
|
||||
const scrollable = scrollableParent.getBoundingClientRect();
|
||||
return {
|
||||
top: parseInt(element.top - scrollable.top) < 10,
|
||||
bottom: parseInt(scrollable.bottom - element.bottom) < 10,
|
||||
};
|
||||
};
|
||||
|
||||
assert.strictEqual(scrollableParent.scrollTop, 0);
|
||||
await click(scrollableParent, ".btn1");
|
||||
assert.ok(
|
||||
border(scrollableParent.querySelector("#scroll1")).top,
|
||||
"the element must be near the top border"
|
||||
);
|
||||
assert.ok(
|
||||
border(scrollableParent.querySelector("#scroll1")).top,
|
||||
"the scrollable inside level 1 must be near the top border"
|
||||
);
|
||||
await click(scrollableParent, ".btn2");
|
||||
assert.ok(
|
||||
border(scrollableParent.querySelector("#scroll2")).top,
|
||||
"the element must be near the top border"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Simple scroll to HTML elements", async (assert) => {
|
||||
assert.expect(6);
|
||||
const scrollableParent = document.createElement("div");
|
||||
scrollableParent.style.overflow = "scroll";
|
||||
scrollableParent.style.height = "150px";
|
||||
scrollableParent.style.width = "400px";
|
||||
target.append(scrollableParent);
|
||||
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml/* xml */ `
|
||||
<div class="o_content">
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus.
|
||||
</p>
|
||||
<div id="o-div-1">A div is an HTML element</div>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.
|
||||
Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,
|
||||
dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper
|
||||
congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est
|
||||
eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu
|
||||
massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut
|
||||
in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent
|
||||
egestas leo in pede. Praesent blandit odio eu enim.
|
||||
</p>
|
||||
<p>
|
||||
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa
|
||||
suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc
|
||||
turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus
|
||||
nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis
|
||||
metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing
|
||||
elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor
|
||||
tellus, aliquam faucibus, convallis id, congue eu, quam.
|
||||
</p>
|
||||
<div id="o-div-2">A div is an HTML element</div>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
await mount(MyComponent, scrollableParent, { env });
|
||||
assert.strictEqual(scrollableParent.scrollTop, 0);
|
||||
|
||||
// The element must be contained in the scrollable parent (top and bottom)
|
||||
const isVisible = (el) => {
|
||||
return (
|
||||
el.getBoundingClientRect().bottom <= scrollableParent.getBoundingClientRect().bottom &&
|
||||
el.getBoundingClientRect().top >= scrollableParent.getBoundingClientRect().top
|
||||
);
|
||||
};
|
||||
|
||||
const border = (el) => {
|
||||
// Returns the state of the element in relation to the borders
|
||||
const element = el.getBoundingClientRect();
|
||||
const scrollable = scrollableParent.getBoundingClientRect();
|
||||
return {
|
||||
top: parseInt(element.top - scrollable.top) < 10,
|
||||
bottom: parseInt(scrollable.bottom - element.bottom) < 10,
|
||||
};
|
||||
};
|
||||
|
||||
// When using scrollTo to an element, this should just scroll
|
||||
// until the element is visible in the scrollable parent
|
||||
const div_1 = scrollableParent.querySelector("#o-div-1");
|
||||
const div_2 = scrollableParent.querySelector("#o-div-2");
|
||||
assert.ok(isVisible(div_1) && !isVisible(div_2), "only the first div is visible");
|
||||
assert.ok(!border(div_1).top, "the element is not at the top border");
|
||||
scrollTo(div_2);
|
||||
assert.ok(!isVisible(div_1) && isVisible(div_2), "only the second div is visible");
|
||||
assert.ok(border(div_2).bottom, "the element must be at the bottom border");
|
||||
scrollTo(div_1);
|
||||
assert.ok(border(div_1).top, "the element must be at the top border");
|
||||
});
|
||||
|
|
@ -0,0 +1,464 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { PopoverContainer } from "@web/core/popover/popover_container";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { tooltipService } from "@web/core/tooltip/tooltip_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
|
||||
import { getFixture, nextTick, patchWithCleanup, triggerEvent } from "../../helpers/utils";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
import { makeFakeLocalizationService } from "../../helpers/mock_services";
|
||||
import { templates } from "@web/core/assets";
|
||||
|
||||
import { App, Component, useState, xml } from "@odoo/owl";
|
||||
|
||||
const mainComponents = registry.category("main_components");
|
||||
|
||||
/**
|
||||
* Creates and mounts a parent component that use the "useTooltip" hook.
|
||||
*
|
||||
* @param {Component} Child a child Component that contains nodes with "data-tooltip" attribute
|
||||
* @param {Object} [options]
|
||||
* @param {function} [options.mockSetTimeout] the mocked setTimeout to use (by default, calls the
|
||||
* callback directly)
|
||||
* @param {function} [options.mockSetInterval] the mocked setInterval to use (by default, calls the
|
||||
* callback directly)
|
||||
* @param {function} [options.mockClearTimeout] the mocked clearTimeout to use (by default, does nothing)
|
||||
* @param {function} [options.mockClearInterval] the mocked clearInterval to use (by default, does nothing)
|
||||
* @param {function} [options.onPopoverAdded] use this callback to check what is being passed to the popover service
|
||||
* @param {Object} [options.extraEnv] an object whose keys should be added to the env
|
||||
* @param {{[templateName:string]: string}} [options.templates] additional templates
|
||||
* @returns {Promise<Component>}
|
||||
*/
|
||||
export async function makeParent(Child, options = {}) {
|
||||
const target = getFixture();
|
||||
|
||||
// add the popover service to the registry -> will add the PopoverContainer
|
||||
// to the mainComponentRegistry
|
||||
clearRegistryWithCleanup(mainComponents);
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: options.mockSetTimeout || ((fn) => fn()),
|
||||
clearTimeout: options.mockClearTimeout || (() => {}),
|
||||
setInterval: options.mockSetInterval || ((fn) => fn()),
|
||||
clearInterval: options.mockClearInterval || (() => {}),
|
||||
ontouchstart: options.mockOnTouchStart || undefined,
|
||||
});
|
||||
|
||||
registry.category("services").add("popover", popoverService);
|
||||
registry.category("services").add("tooltip", tooltipService);
|
||||
registry.category("services").add("localization", makeFakeLocalizationService());
|
||||
let env = await makeTestEnv();
|
||||
if (options.extraEnv) {
|
||||
env = Object.create(env, Object.getOwnPropertyDescriptors(options.extraEnv));
|
||||
}
|
||||
|
||||
patchWithCleanup(env.services.popover, {
|
||||
add(...args) {
|
||||
const result = this._super(...args);
|
||||
if (options.onPopoverAdded) {
|
||||
options.onPopoverAdded(...args);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.Components = mainComponents.getEntries();
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<Child/>
|
||||
<div>
|
||||
<t t-foreach="Components" t-as="Component" t-key="Component[0]">
|
||||
<t t-component="Component[1].Component" t-props="Component[1].props"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>`;
|
||||
Parent.components = { PopoverContainer, Child };
|
||||
|
||||
const app = new App(Parent, {
|
||||
env,
|
||||
target,
|
||||
templates,
|
||||
test: true,
|
||||
});
|
||||
registerCleanup(() => app.destroy());
|
||||
for (const [name, template] of Object.entries(options.templates || {})) {
|
||||
app.addTemplate(name, template);
|
||||
}
|
||||
|
||||
return app.mount(target);
|
||||
}
|
||||
|
||||
let target;
|
||||
QUnit.module("Tooltip service", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.test("basic rendering", async (assert) => {
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`<button class="mybtn" data-tooltip="hello">Action</button>`;
|
||||
let simulateTimeout;
|
||||
const mockSetTimeout = (fn) => {
|
||||
simulateTimeout = fn;
|
||||
};
|
||||
await makeParent(MyComponent, { mockSetTimeout });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
target.querySelector(".mybtn").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
|
||||
simulateTimeout();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_popover_container .o_popover").innerText,
|
||||
"hello"
|
||||
);
|
||||
|
||||
target.querySelector(".mybtn").dispatchEvent(new Event("mouseleave"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("basic rendering 2", async (assert) => {
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`<span data-tooltip="hello" class="our_span"><span class="our_span">Action</span></span>`;
|
||||
let simulateTimeout;
|
||||
const mockSetTimeout = (fn) => {
|
||||
simulateTimeout = fn;
|
||||
};
|
||||
await makeParent(MyComponent, { mockSetTimeout });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
const [outerSpan, innerSpan] = target.querySelectorAll("span.our_span");
|
||||
outerSpan.dispatchEvent(new Event("mouseenter"));
|
||||
innerSpan.dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
|
||||
simulateTimeout();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_popover_container .o_popover").innerText,
|
||||
"hello"
|
||||
);
|
||||
|
||||
innerSpan.dispatchEvent(new Event("mouseleave"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
|
||||
outerSpan.dispatchEvent(new Event("mouseleave"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("remove element with opened tooltip", async (assert) => {
|
||||
let compState;
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ visible: true });
|
||||
compState = this.state;
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<div>
|
||||
<button t-if="state.visible" data-tooltip="hello">Action</button>
|
||||
</div>`;
|
||||
let simulateInterval;
|
||||
const mockSetInterval = (fn) => {
|
||||
simulateInterval = fn;
|
||||
};
|
||||
await makeParent(MyComponent, { mockSetInterval });
|
||||
|
||||
assert.containsOnce(target, "button");
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
target.querySelector("button").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
|
||||
compState.visible = false;
|
||||
await nextTick();
|
||||
assert.containsNone(target, "button");
|
||||
simulateInterval();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("rendering with several tooltips", async (assert) => {
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`
|
||||
<div>
|
||||
<button class="button_1" data-tooltip="tooltip 1">Action 1</button>
|
||||
<button class="button_2" data-tooltip="tooltip 2">Action 2</button>
|
||||
</div>`;
|
||||
await makeParent(MyComponent);
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
target.querySelector("button.button_1").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "tooltip 1");
|
||||
target.querySelector("button.button_1").dispatchEvent(new Event("mouseleave"));
|
||||
target.querySelector("button.button_2").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "tooltip 2");
|
||||
});
|
||||
|
||||
QUnit.test("positioning", async (assert) => {
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`
|
||||
<div style="height: 400px; padding: 40px">
|
||||
<button class="default" data-tooltip="default">Default</button>
|
||||
<button class="top" data-tooltip="top" data-tooltip-position="top">Top</button>
|
||||
<button class="right" data-tooltip="right" data-tooltip-position="right">Right</button>
|
||||
<button class="bottom" data-tooltip="bottom" data-tooltip-position="bottom">Bottom</button>
|
||||
<button class="left" data-tooltip="left" data-tooltip-position="left">Left</button>
|
||||
</div>`;
|
||||
await makeParent(MyComponent, {
|
||||
onPopoverAdded(...args) {
|
||||
const { position } = args[3];
|
||||
if (position) {
|
||||
assert.step(`popover added with position: ${position}`);
|
||||
} else {
|
||||
assert.step(`popover added with default positioning`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// default
|
||||
target.querySelector("button.default").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "default");
|
||||
assert.verifySteps(["popover added with default positioning"]);
|
||||
|
||||
// top
|
||||
target.querySelector("button.top").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "top");
|
||||
assert.verifySteps(["popover added with position: top"]);
|
||||
|
||||
// right
|
||||
target.querySelector("button.right").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "right");
|
||||
assert.verifySteps(["popover added with position: right"]);
|
||||
|
||||
// bottom
|
||||
target.querySelector("button.bottom").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "bottom");
|
||||
assert.verifySteps(["popover added with position: bottom"]);
|
||||
|
||||
// left
|
||||
target.querySelector("button.left").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "left");
|
||||
assert.verifySteps(["popover added with position: left"]);
|
||||
});
|
||||
|
||||
QUnit.test("tooltip with a template, no info", async (assert) => {
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`
|
||||
<button data-tooltip-template="my_tooltip_template">Action</button>
|
||||
`;
|
||||
|
||||
const templates = {
|
||||
my_tooltip_template: "<i t-esc='env.tooltip_text'/>",
|
||||
};
|
||||
await makeParent(MyComponent, { templates, extraEnv: { tooltip_text: "tooltip" } });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o-tooltip");
|
||||
target.querySelector("button").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o-tooltip");
|
||||
assert.strictEqual(target.querySelector(".o-tooltip").innerHTML, "<i>tooltip</i>");
|
||||
});
|
||||
|
||||
QUnit.test("tooltip with a template and info", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
get info() {
|
||||
return JSON.stringify({ x: 3, y: "abc" });
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<button
|
||||
data-tooltip-template="my_tooltip_template"
|
||||
t-att-data-tooltip-info="info">
|
||||
Action
|
||||
</button>
|
||||
`;
|
||||
|
||||
const templates = {
|
||||
my_tooltip_template: `
|
||||
<ul>
|
||||
<li>X: <t t-esc="x"/></li>
|
||||
<li>Y: <t t-esc="y"/></li>
|
||||
</ul>
|
||||
`,
|
||||
};
|
||||
await makeParent(MyComponent, { templates });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o-tooltip");
|
||||
target.querySelector("button").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o-tooltip");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o-tooltip").innerHTML,
|
||||
"<ul><li>X: 3</li><li>Y: abc</li></ul>"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("empty tooltip, no template", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
get tooltip() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<button t-att-data-tooltip="tooltip">Action</button>`;
|
||||
let simulateTimeout = () => {};
|
||||
const mockSetTimeout = (fn) => {
|
||||
simulateTimeout = fn;
|
||||
};
|
||||
await makeParent(MyComponent, { mockSetTimeout });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
target.querySelector("button").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
simulateTimeout();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("tooltip with no delay (default delay)", async (assert) => {
|
||||
assert.expect(1);
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`<button class="myBtn" data-tooltip="'helpful tooltip'">Action</button>`;
|
||||
const mockSetTimeout = (fn, delay) => {
|
||||
assert.strictEqual(delay, 400);
|
||||
};
|
||||
await makeParent(MyComponent, { mockSetTimeout });
|
||||
target.querySelector("button.myBtn").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
QUnit.test("tooltip with a delay", async (assert) => {
|
||||
assert.expect(1);
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`<button class="myBtn" data-tooltip="'helpful tooltip'" data-tooltip-delay="2000">Action</button>`;
|
||||
const mockSetTimeout = (fn, delay) => {
|
||||
assert.strictEqual(delay, 2000);
|
||||
};
|
||||
await makeParent(MyComponent, { mockSetTimeout });
|
||||
target.querySelector("button.myBtn").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
QUnit.test("touch rendering - hold-to-show", async (assert) => {
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`<button data-tooltip="hello">Action</button>`;
|
||||
let simulateTimeout;
|
||||
const mockSetTimeout = (fn) => {
|
||||
simulateTimeout = fn;
|
||||
};
|
||||
let simulateInterval;
|
||||
const mockSetInterval = (fn) => {
|
||||
simulateInterval = fn;
|
||||
};
|
||||
const mockOnTouchStart = () => {};
|
||||
await makeParent(MyComponent, { mockSetTimeout, mockSetInterval, mockOnTouchStart });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
await triggerEvent(target, "button", "touchstart");
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
|
||||
simulateTimeout();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_popover_container .o_popover").innerText,
|
||||
"hello"
|
||||
);
|
||||
|
||||
await triggerEvent(target, "button", "touchend");
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
simulateInterval();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("touch rendering - tap-to-show", async (assert) => {
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`<button data-tooltip="hello" data-tooltip-touch-tap-to-show="true">Action</button>`;
|
||||
let simulateTimeout;
|
||||
const mockSetTimeout = (fn) => {
|
||||
simulateTimeout = fn;
|
||||
};
|
||||
let simulateInterval;
|
||||
const mockSetInterval = (fn) => {
|
||||
simulateInterval = fn;
|
||||
};
|
||||
const mockOnTouchStart = () => {};
|
||||
await makeParent(MyComponent, { mockSetTimeout, mockSetInterval, mockOnTouchStart });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
await triggerEvent(target, "button[data-tooltip]", "touchstart");
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
|
||||
simulateTimeout();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_popover_container .o_popover").innerText,
|
||||
"hello"
|
||||
);
|
||||
|
||||
await triggerEvent(target, "button[data-tooltip]", "touchend");
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
simulateInterval();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
|
||||
await triggerEvent(target, "button[data-tooltip]", "touchstart");
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("tooltip does not crash with disappearing target", async (assert) => {
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`<button class="mybtn" data-tooltip="hello">Action</button>`;
|
||||
let simulateTimeout;
|
||||
const mockSetTimeout = async (fn) => {
|
||||
simulateTimeout = fn;
|
||||
};
|
||||
await makeParent(MyComponent, { mockSetTimeout });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
target.querySelector(".mybtn").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
|
||||
// the element disappeared from the DOM during the setTimeout
|
||||
target.querySelector(".mybtn").remove();
|
||||
|
||||
simulateTimeout();
|
||||
await nextTick();
|
||||
|
||||
// tooltip did not crash and is not shown
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Transition, useTransition, config as transitionConfig } from "@web/core/transition";
|
||||
import { getFixture, mockTimeout, mount, nextTick, patchWithCleanup } from "../helpers/utils";
|
||||
|
||||
import { Component, xml, useState } from "@odoo/owl";
|
||||
|
||||
QUnit.module("Transition");
|
||||
|
||||
QUnit.test("useTransition hook", async (assert) => {
|
||||
patchWithCleanup(transitionConfig, {
|
||||
disabled: false,
|
||||
});
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.transition = useTransition({
|
||||
name: "test",
|
||||
onLeave: () => assert.step("leave"),
|
||||
});
|
||||
}
|
||||
}
|
||||
Parent.template = xml`<div t-if="transition.shouldMount" t-att-class="transition.className"/>`;
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
|
||||
const target = getFixture();
|
||||
const parent = await mount(Parent, target);
|
||||
// Mounted with -enter but not -enter-active
|
||||
assert.containsOnce(target, ".test.test-enter:not(.test-enter-active)");
|
||||
await nextTick();
|
||||
// No longer has -enter class but now has -enter-active
|
||||
assert.containsOnce(target, ".test.test-enter-active:not(.test-enter)");
|
||||
parent.transition.shouldMount = false;
|
||||
await nextTick();
|
||||
// Leaving: -leave but not -enter-active
|
||||
assert.containsOnce(target, ".test.test-leave:not(.test-enter-active)");
|
||||
assert.verifySteps([]);
|
||||
execRegisteredTimeouts();
|
||||
assert.verifySteps(["leave"]);
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".test");
|
||||
});
|
||||
|
||||
QUnit.test("Transition HOC", async (assert) => {
|
||||
patchWithCleanup(transitionConfig, {
|
||||
disabled: false,
|
||||
});
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ show: true });
|
||||
}
|
||||
onLeave() {
|
||||
assert.step("leave");
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<Transition name="'test'" visible="state.show" t-slot-scope="transition" onLeave="onLeave">
|
||||
<div t-att-class="transition.className"/>
|
||||
</Transition>
|
||||
`;
|
||||
Parent.components = { Transition };
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
|
||||
const target = getFixture();
|
||||
const parent = await mount(Parent, target);
|
||||
// Mounted with -enter but not -enter-active
|
||||
assert.containsOnce(target, ".test.test-enter:not(.test-enter-active)");
|
||||
await nextTick();
|
||||
// No longer has -enter class but now has -enter-active
|
||||
assert.containsOnce(target, ".test.test-enter-active:not(.test-enter)");
|
||||
parent.state.show = false;
|
||||
await nextTick();
|
||||
// Leaving: -leave but not -enter-active
|
||||
assert.containsOnce(target, ".test.test-leave:not(.test-enter-active)");
|
||||
assert.verifySteps([]);
|
||||
execRegisteredTimeouts();
|
||||
assert.verifySteps(["leave"]);
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".test");
|
||||
});
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService, useActiveElement } from "@web/core/ui/ui_service";
|
||||
import { useAutofocus } from "@web/core/utils/hooks";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { makeFakeLocalizationService } from "../helpers/mock_services";
|
||||
import { getFixture, mount, nextTick, triggerEvent } from "../helpers/utils";
|
||||
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
let browser;
|
||||
let baseConfig;
|
||||
let BlockUI, props;
|
||||
|
||||
QUnit.module("UI service", {
|
||||
async beforeEach() {
|
||||
target = getFixture();
|
||||
serviceRegistry.add("ui", uiService);
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
browser = { setTimeout: () => 1 };
|
||||
baseConfig = { browser };
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("block and unblock once ui with ui service", async (assert) => {
|
||||
const env = await makeTestEnv({ ...baseConfig });
|
||||
({ Component: BlockUI, props } = registry.category("main_components").get("BlockUI"));
|
||||
const ui = env.services.ui;
|
||||
await mount(BlockUI, target, { env, props });
|
||||
let blockUI = target.querySelector(".o_blockUI");
|
||||
assert.strictEqual(blockUI, null, "ui should not be blocked");
|
||||
ui.block();
|
||||
await nextTick();
|
||||
blockUI = target.querySelector(".o_blockUI");
|
||||
assert.notStrictEqual(blockUI, null, "ui should be blocked");
|
||||
ui.unblock();
|
||||
await nextTick();
|
||||
blockUI = target.querySelector(".o_blockUI");
|
||||
assert.strictEqual(blockUI, null, "ui should not be blocked");
|
||||
});
|
||||
|
||||
QUnit.test("use block and unblock several times to block ui with ui service", async (assert) => {
|
||||
const env = await makeTestEnv({ ...baseConfig });
|
||||
({ Component: BlockUI, props } = registry.category("main_components").get("BlockUI"));
|
||||
const ui = env.services.ui;
|
||||
await mount(BlockUI, target, { env, props });
|
||||
let blockUI = target.querySelector(".o_blockUI");
|
||||
assert.strictEqual(blockUI, null, "ui should not be blocked");
|
||||
ui.block();
|
||||
ui.block();
|
||||
ui.block();
|
||||
await nextTick();
|
||||
blockUI = target.querySelector(".o_blockUI");
|
||||
assert.notStrictEqual(blockUI, null, "ui should be blocked");
|
||||
ui.unblock();
|
||||
ui.unblock();
|
||||
await nextTick();
|
||||
blockUI = target.querySelector(".o_blockUI");
|
||||
assert.notStrictEqual(blockUI, null, "ui should be blocked");
|
||||
ui.unblock();
|
||||
await nextTick();
|
||||
blockUI = target.querySelector(".o_blockUI");
|
||||
assert.strictEqual(blockUI, null, "ui should not be blocked");
|
||||
});
|
||||
|
||||
QUnit.test("a component can be the UI active element: with t-ref delegation", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useActiveElement("delegatedRef");
|
||||
this.hasRef = true;
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<div>
|
||||
<h1>My Component</h1>
|
||||
<div t-if="hasRef" id="owner" t-ref="delegatedRef"/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const env = await makeTestEnv({ ...baseConfig });
|
||||
const ui = env.services.ui;
|
||||
assert.deepEqual(ui.activeElement, document);
|
||||
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
assert.deepEqual(ui.activeElement, document.getElementById("owner"));
|
||||
comp.hasRef = false;
|
||||
comp.render();
|
||||
await nextTick();
|
||||
|
||||
assert.deepEqual(ui.activeElement, document);
|
||||
});
|
||||
|
||||
QUnit.test("UI active element: trap focus", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useActiveElement("delegatedRef");
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<div>
|
||||
<h1>My Component</h1>
|
||||
<input type="text" placeholder="outerUIActiveElement"/>
|
||||
<div t-ref="delegatedRef">
|
||||
<input type="text" placeholder="withFocus"/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const env = await makeTestEnv({ ...baseConfig });
|
||||
await mount(MyComponent, target, { env });
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector("input[placeholder=withFocus]"),
|
||||
"The focus is on the first 'focusable' element of the UI active element"
|
||||
);
|
||||
|
||||
// Pressing 'Tab'
|
||||
let event = triggerEvent(
|
||||
document.activeElement,
|
||||
null,
|
||||
"keydown",
|
||||
{ key: "Tab" },
|
||||
{ fast: true }
|
||||
);
|
||||
assert.strictEqual(event.defaultPrevented, true);
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector("input[placeholder=withFocus]")
|
||||
);
|
||||
|
||||
// Pressing 'Shift + Tab'
|
||||
event = triggerEvent(
|
||||
document.activeElement,
|
||||
null,
|
||||
"keydown",
|
||||
{ key: "Tab", shiftKey: true },
|
||||
{ fast: true }
|
||||
);
|
||||
assert.strictEqual(event.defaultPrevented, true);
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector("input[placeholder=withFocus]")
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("UI active element: trap focus - default focus with autofocus", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useActiveElement("delegatedRef");
|
||||
useAutofocus();
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<div>
|
||||
<h1>My Component</h1>
|
||||
<input type="text" placeholder="outerUIActiveElement"/>
|
||||
<div t-ref="delegatedRef">
|
||||
<input type="text" placeholder="withoutFocus"/>
|
||||
<input type="text" t-ref="autofocus" placeholder="withAutoFocus"/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const env = await makeTestEnv({ ...baseConfig });
|
||||
await mount(MyComponent, target, { env });
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector("input[placeholder=withAutoFocus]"),
|
||||
"The focus is on the autofocus element of the UI active element"
|
||||
);
|
||||
|
||||
// Pressing 'Tab'
|
||||
let event = triggerEvent(
|
||||
document.activeElement,
|
||||
null,
|
||||
"keydown",
|
||||
{ key: "Tab" },
|
||||
{ fast: true }
|
||||
);
|
||||
assert.strictEqual(event.defaultPrevented, true);
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector("input[placeholder=withoutFocus]")
|
||||
);
|
||||
|
||||
// Pressing 'Shift + Tab'
|
||||
event = triggerEvent(
|
||||
document.activeElement,
|
||||
null,
|
||||
"keydown",
|
||||
{ key: "Tab", shiftKey: true },
|
||||
{ fast: true }
|
||||
);
|
||||
assert.strictEqual(event.defaultPrevented, true);
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector("input[placeholder=withAutoFocus]")
|
||||
);
|
||||
|
||||
// Pressing 'Shift + Tab' (default)
|
||||
event = triggerEvent(
|
||||
document.activeElement,
|
||||
null,
|
||||
"keydown",
|
||||
{ key: "Tab", shiftKey: true },
|
||||
{ fast: true }
|
||||
);
|
||||
assert.strictEqual(event.defaultPrevented, false);
|
||||
});
|
||||
|
||||
QUnit.test("UI active element: trap focus - no focus element", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useActiveElement("delegatedRef");
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<div>
|
||||
<h1>My Component</h1>
|
||||
<input type="text" placeholder="outerUIActiveElement"/>
|
||||
<div id="idActiveElement" t-ref="delegatedRef">
|
||||
<div>
|
||||
<span> No focus element </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const env = await makeTestEnv({ ...baseConfig });
|
||||
await mount(MyComponent, target, { env });
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector("div[id=idActiveElement]"),
|
||||
"when there is not other element, the focus is on the UI active element itself"
|
||||
);
|
||||
// Pressing 'Tab'
|
||||
let event = triggerEvent(
|
||||
document.activeElement,
|
||||
null,
|
||||
"keydown",
|
||||
{ key: "Tab" },
|
||||
{ fast: true }
|
||||
);
|
||||
assert.strictEqual(event.defaultPrevented, true);
|
||||
await nextTick();
|
||||
assert.strictEqual(document.activeElement, target.querySelector("div[id=idActiveElement]"));
|
||||
|
||||
// Pressing 'Shift + Tab'
|
||||
event = triggerEvent(
|
||||
document.activeElement,
|
||||
null,
|
||||
"keydown",
|
||||
{ key: "Tab", shiftKey: true },
|
||||
{ fast: true }
|
||||
);
|
||||
assert.strictEqual(event.defaultPrevented, true);
|
||||
await nextTick();
|
||||
assert.strictEqual(document.activeElement, target.querySelector("div[id=idActiveElement]"));
|
||||
});
|
||||
|
||||
QUnit.test("UI active element: trap focus - first or last tabable changes", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.show = useState({ a: true, c: false });
|
||||
useActiveElement("delegatedRef");
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<div>
|
||||
<h1>My Component</h1>
|
||||
<input type="text" name="outer"/>
|
||||
<div id="idActiveElement" t-ref="delegatedRef">
|
||||
<div>
|
||||
<input type="text" name="a" t-if="show.a"/>
|
||||
<input type="text" name="b"/>
|
||||
<input type="text" name="c" t-if="show.c"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const env = await makeTestEnv({ ...baseConfig });
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
|
||||
assert.strictEqual(document.activeElement, target.querySelector("input[name=a]"));
|
||||
// Pressing 'Shift + Tab'
|
||||
let event = await triggerEvent(document.activeElement, null, "keydown", {
|
||||
key: "Tab",
|
||||
shiftKey: true,
|
||||
});
|
||||
assert.strictEqual(event.defaultPrevented, true);
|
||||
assert.strictEqual(document.activeElement, target.querySelector("input[name=b]"));
|
||||
|
||||
comp.show.a = false;
|
||||
comp.show.c = true;
|
||||
await nextTick();
|
||||
assert.strictEqual(document.activeElement, target.querySelector("input[name=b]"));
|
||||
|
||||
// Pressing 'Shift + Tab'
|
||||
event = await triggerEvent(document.activeElement, null, "keydown", {
|
||||
key: "Tab",
|
||||
shiftKey: true,
|
||||
});
|
||||
assert.strictEqual(event.defaultPrevented, true);
|
||||
assert.strictEqual(document.activeElement, target.querySelector("input[name=c]"));
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"UI active element: trap focus is not bypassed using invisible elements",
|
||||
async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useActiveElement("delegatedRef");
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<div>
|
||||
<h1>My Component</h1>
|
||||
<input type="text" placeholder="outerUIActiveElement"/>
|
||||
<div t-ref="delegatedRef">
|
||||
<input type="text" placeholder="withFocus"/>
|
||||
<input class="d-none" type="text" placeholder="withFocusNotDisplayed"/>
|
||||
<div class="d-none">
|
||||
<input type="text" placeholder="withFocusNotDisplayedToo"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const env = await makeTestEnv({ ...baseConfig });
|
||||
await mount(MyComponent, target, { env });
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector("input[placeholder=withFocus]"),
|
||||
"The focus is on the first 'focusable' element of the UI active element"
|
||||
);
|
||||
|
||||
// Pressing 'Tab'
|
||||
let event = await triggerEvent(
|
||||
document.activeElement,
|
||||
null,
|
||||
"keydown",
|
||||
{ key: "Tab" },
|
||||
{ fast: true }
|
||||
);
|
||||
|
||||
// No other visible element is found
|
||||
assert.strictEqual(event.defaultPrevented, true);
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector("input[placeholder=withFocus]")
|
||||
);
|
||||
|
||||
// Pressing 'Shift + Tab'
|
||||
event = triggerEvent(
|
||||
document.activeElement,
|
||||
null,
|
||||
"keydown",
|
||||
{ key: "Tab", shiftKey: true },
|
||||
{ fast: true }
|
||||
);
|
||||
assert.strictEqual(event.defaultPrevented, true);
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector("input[placeholder=withFocus]")
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { userService } from "@web/core/user_service";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("UserService");
|
||||
|
||||
QUnit.test("successive calls to hasGroup", async (assert) => {
|
||||
serviceRegistry.add("user", userService);
|
||||
const groups = ["x"];
|
||||
const mockRPC = (route, args) => {
|
||||
assert.step(`${args.model}/${args.method}/${args.args[0]}`);
|
||||
return groups.includes(args.args[0]);
|
||||
};
|
||||
const env = await makeTestEnv({ mockRPC });
|
||||
const hasGroupX = await env.services.user.hasGroup("x");
|
||||
const hasGroupY = await env.services.user.hasGroup("y");
|
||||
assert.strictEqual(hasGroupX, true);
|
||||
assert.strictEqual(hasGroupY, false);
|
||||
const hasGroupXAgain = await env.services.user.hasGroup("x");
|
||||
assert.strictEqual(hasGroupXAgain, true);
|
||||
|
||||
assert.verifySteps(["res.users/has_group/x", "res.users/has_group/y"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
cartesian,
|
||||
groupBy,
|
||||
intersection,
|
||||
shallowEqual,
|
||||
sortBy,
|
||||
unique,
|
||||
} from "@web/core/utils/arrays";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("Arrays");
|
||||
|
||||
QUnit.test("groupBy parameter validations", function (assert) {
|
||||
// Safari: TypeError: undefined is not a function
|
||||
// Other navigator: array is not iterable
|
||||
assert.throws(
|
||||
() => groupBy({}),
|
||||
/array is not iterable|TypeError: undefined is not a function/
|
||||
);
|
||||
assert.throws(
|
||||
() => groupBy([], true),
|
||||
/Expected criterion of type 'string' or 'function' and got 'boolean'/
|
||||
);
|
||||
assert.throws(
|
||||
() => groupBy([], 3),
|
||||
/Expected criterion of type 'string' or 'function' and got 'number'/
|
||||
);
|
||||
assert.throws(
|
||||
() => groupBy([], {}),
|
||||
/Expected criterion of type 'string' or 'function' and got 'object'/
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("groupBy (no criterion)", function (assert) {
|
||||
// criterion = default
|
||||
assert.deepEqual(groupBy(["a", "b", 1, true]), {
|
||||
1: [1],
|
||||
a: ["a"],
|
||||
b: ["b"],
|
||||
true: [true],
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("groupBy by property", function (assert) {
|
||||
assert.deepEqual(groupBy([{ x: "a" }, { x: "a" }, { x: "b" }], "x"), {
|
||||
a: [{ x: "a" }, { x: "a" }],
|
||||
b: [{ x: "b" }],
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("groupBy", function (assert) {
|
||||
assert.deepEqual(
|
||||
groupBy(["a", "b", 1, true], (x) => `el${x}`),
|
||||
{
|
||||
ela: ["a"],
|
||||
elb: ["b"],
|
||||
el1: [1],
|
||||
eltrue: [true],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("sortBy parameter validation", function (assert) {
|
||||
assert.throws(() => sortBy({}), /array.slice is not a function/);
|
||||
assert.throws(
|
||||
() => sortBy([Symbol("b"), Symbol("a")]),
|
||||
/(Cannot convert a (Symbol value)|(symbol) to a number)|(can't convert symbol to number)/
|
||||
);
|
||||
assert.throws(
|
||||
() => sortBy([2, 1, 5], true),
|
||||
/Expected criterion of type 'string' or 'function' and got 'boolean'/
|
||||
);
|
||||
assert.throws(
|
||||
() => sortBy([2, 1, 5], 3),
|
||||
/Expected criterion of type 'string' or 'function' and got 'number'/
|
||||
);
|
||||
assert.throws(
|
||||
() => sortBy([2, 1, 5], {}),
|
||||
/Expected criterion of type 'string' or 'function' and got 'object'/
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("sortBy do not sort in place", function (assert) {
|
||||
const toSort = [2, 3, 1];
|
||||
sortBy(toSort);
|
||||
assert.deepEqual(toSort, [2, 3, 1]);
|
||||
});
|
||||
|
||||
QUnit.test("sortBy (no criterion)", function (assert) {
|
||||
assert.deepEqual(sortBy([]), []);
|
||||
assert.deepEqual(sortBy([2, 1, 5]), [1, 2, 5]);
|
||||
assert.deepEqual(sortBy([true, false, true]), [false, true, true]);
|
||||
assert.deepEqual(sortBy(["b", "a", "z"]), ["a", "b", "z"]);
|
||||
assert.deepEqual(sortBy([{ x: true }, { x: false }, { x: true }]), [
|
||||
{ x: true },
|
||||
{ x: false },
|
||||
{ x: true },
|
||||
]);
|
||||
assert.deepEqual(sortBy([{ x: 2 }, { x: 1 }, { x: 5 }]), [{ x: 2 }, { x: 1 }, { x: 5 }]);
|
||||
assert.deepEqual(sortBy([{ x: "b" }, { x: "a" }, { x: "z" }]), [
|
||||
{ x: "b" },
|
||||
{ x: "a" },
|
||||
{ x: "z" },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("sortBy property", function (assert) {
|
||||
assert.deepEqual(sortBy([], "x"), []);
|
||||
assert.deepEqual(sortBy([2, 1, 5], "x"), [2, 1, 5]);
|
||||
assert.deepEqual(sortBy([true, false, true], "x"), [true, false, true]);
|
||||
assert.deepEqual(sortBy(["b", "a", "z"], "x"), ["b", "a", "z"]);
|
||||
assert.deepEqual(sortBy([{ x: true }, { x: false }, { x: true }], "x"), [
|
||||
{ x: false },
|
||||
{ x: true },
|
||||
{ x: true },
|
||||
]);
|
||||
assert.deepEqual(sortBy([{ x: 2 }, { x: 1 }, { x: 5 }], "x"), [
|
||||
{ x: 1 },
|
||||
{ x: 2 },
|
||||
{ x: 5 },
|
||||
]);
|
||||
assert.deepEqual(sortBy([{ x: "b" }, { x: "a" }, { x: "z" }], "x"), [
|
||||
{ x: "a" },
|
||||
{ x: "b" },
|
||||
{ x: "z" },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("sortBy getter", function (assert) {
|
||||
const getter = (obj) => obj.x;
|
||||
assert.deepEqual(sortBy([], getter), []);
|
||||
assert.deepEqual(sortBy([2, 1, 5], getter), [2, 1, 5]);
|
||||
assert.deepEqual(sortBy([true, false, true], getter), [true, false, true]);
|
||||
assert.deepEqual(sortBy(["b", "a", "z"], getter), ["b", "a", "z"]);
|
||||
assert.deepEqual(sortBy([{ x: true }, { x: false }, { x: true }], getter), [
|
||||
{ x: false },
|
||||
{ x: true },
|
||||
{ x: true },
|
||||
]);
|
||||
assert.deepEqual(sortBy([{ x: 2 }, { x: 1 }, { x: 5 }], getter), [
|
||||
{ x: 1 },
|
||||
{ x: 2 },
|
||||
{ x: 5 },
|
||||
]);
|
||||
assert.deepEqual(sortBy([{ x: "b" }, { x: "a" }, { x: "z" }], getter), [
|
||||
{ x: "a" },
|
||||
{ x: "b" },
|
||||
{ x: "z" },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("sortBy descending order", function (assert) {
|
||||
assert.deepEqual(sortBy([2, 1, 5], null, "desc"), [5, 2, 1]);
|
||||
assert.deepEqual(sortBy([{ x: "b" }, { x: "a" }, { x: "z" }], "x", "desc"), [
|
||||
{ x: "z" },
|
||||
{ x: "b" },
|
||||
{ x: "a" },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("intersection of arrays", function (assert) {
|
||||
assert.deepEqual(intersection([], [1, 2]), []);
|
||||
assert.deepEqual(intersection([1, 2], []), []);
|
||||
assert.deepEqual(intersection([1], [2]), []);
|
||||
assert.deepEqual(intersection([1, 2], [2, 3]), [2]);
|
||||
assert.deepEqual(intersection([1, 2, 3], [1, 2, 3]), [1, 2, 3]);
|
||||
});
|
||||
|
||||
QUnit.test("cartesian product of zero arrays", function (assert) {
|
||||
assert.deepEqual(cartesian(), [undefined], "the unit of the product is a singleton");
|
||||
});
|
||||
|
||||
QUnit.test("cartesian product of a single array", function (assert) {
|
||||
assert.deepEqual(cartesian([]), []);
|
||||
assert.deepEqual(cartesian([1]), [1], "we don't want unecessary brackets");
|
||||
assert.deepEqual(cartesian([1, 2]), [1, 2]);
|
||||
assert.deepEqual(
|
||||
cartesian([[1, 2]]),
|
||||
[[1, 2]],
|
||||
"the internal structure of elements should be preserved"
|
||||
);
|
||||
assert.deepEqual(
|
||||
cartesian([
|
||||
[1, 2],
|
||||
[3, [2]],
|
||||
]),
|
||||
[
|
||||
[1, 2],
|
||||
[3, [2]],
|
||||
],
|
||||
"the internal structure of elements should be preserved"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("cartesian product of two arrays", function (assert) {
|
||||
assert.deepEqual(cartesian([], []), []);
|
||||
assert.deepEqual(cartesian([1], []), []);
|
||||
assert.deepEqual(cartesian([1], [2]), [[1, 2]]);
|
||||
assert.deepEqual(cartesian([1, 2], [3]), [
|
||||
[1, 3],
|
||||
[2, 3],
|
||||
]);
|
||||
assert.deepEqual(
|
||||
cartesian([[1], 4], [2, [3]]),
|
||||
[
|
||||
[[1], 2],
|
||||
[[1], [3]],
|
||||
[4, 2],
|
||||
[4, [3]],
|
||||
],
|
||||
"the internal structure of elements should be preserved"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("cartesian product of three arrays", function (assert) {
|
||||
assert.deepEqual(cartesian([], [], []), []);
|
||||
assert.deepEqual(cartesian([1], [], [2, 5]), []);
|
||||
assert.deepEqual(
|
||||
cartesian([1], [2], [3]),
|
||||
[[1, 2, 3]],
|
||||
"we should have no unecessary brackets, we want elements to be 'triples'"
|
||||
);
|
||||
assert.deepEqual(
|
||||
cartesian([[1], 2], [3], [4]),
|
||||
[
|
||||
[[1], 3, 4],
|
||||
[2, 3, 4],
|
||||
],
|
||||
"the internal structure of elements should be preserved"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("cartesian product of four arrays", function (assert) {
|
||||
assert.deepEqual(cartesian([1], [2], [3], [4]), [[1, 2, 3, 4]]);
|
||||
});
|
||||
|
||||
QUnit.test("unique array", function (assert) {
|
||||
assert.deepEqual(unique([1, 2, 3, 2, 4, 3, 1, 4]), [1, 2, 3, 4]);
|
||||
assert.deepEqual(unique("a d c a b c d b".split(" ")), "a d c b".split(" "));
|
||||
});
|
||||
|
||||
QUnit.test("shallowEqual: simple valid cases", function (assert) {
|
||||
assert.ok(shallowEqual([], []));
|
||||
assert.ok(shallowEqual([1], [1]));
|
||||
assert.ok(shallowEqual([1, "a"], [1, "a"]));
|
||||
});
|
||||
|
||||
QUnit.test("shallowEqual: simple invalid cases", function (assert) {
|
||||
assert.notOk(shallowEqual([1], []));
|
||||
assert.notOk(shallowEqual([], [1]));
|
||||
assert.notOk(shallowEqual([1, "b"], [1, "a"]));
|
||||
});
|
||||
|
||||
QUnit.test("shallowEqual: arrays with non primitive values", function (assert) {
|
||||
const obj = { b: 3 };
|
||||
assert.ok(shallowEqual([obj], [obj]));
|
||||
assert.notOk(shallowEqual([{ b: 3 }], [{ b: 3 }]));
|
||||
|
||||
const arr = ["x", "y", "z"];
|
||||
assert.ok(shallowEqual([arr], [arr]));
|
||||
assert.notOk(shallowEqual([["x", "y", "z"]], [["x", "y", "z"]]));
|
||||
|
||||
const fn = () => {};
|
||||
assert.ok(shallowEqual([fn], [fn]));
|
||||
assert.notOk(shallowEqual([() => {}], [() => {}]));
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { assets, loadJS, loadCSS } from "@web/core/assets";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("Assets");
|
||||
|
||||
QUnit.test("loadJS: load invalid JS lib", function (assert) {
|
||||
assert.rejects(
|
||||
loadJS("/some/invalid/file.js"),
|
||||
new RegExp("The loading of /some/invalid/file.js failed"),
|
||||
"Trying to load an invalid file rejects the promise"
|
||||
);
|
||||
assert.ok(
|
||||
document.querySelector("script[src='/some/invalid/file.js']"),
|
||||
"Document contains a script with the src we asked to load"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("loadCSS: load invalid CSS lib", function (assert) {
|
||||
assets.retries = {count: 3, delay: 1, extraDelay: 1}; // Fail fast.
|
||||
assert.rejects(
|
||||
loadCSS("/some/invalid/file.css"),
|
||||
new RegExp("The loading of /some/invalid/file.css failed"),
|
||||
"Trying to load an invalid file rejects the promise"
|
||||
);
|
||||
assert.ok(
|
||||
document.querySelector("link[href='/some/invalid/file.css']"),
|
||||
"Document contains a link with the href we asked to load"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ErrorHandler } from "@web/core/utils/components";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
import { getFixture, mount } from "../../helpers/utils";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("components");
|
||||
|
||||
QUnit.test("ErrorHandler component", async function (assert) {
|
||||
class Boom extends Component {}
|
||||
Boom.template = xml`<div><t t-esc="this.will.throw"/></div>`;
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.flag = true;
|
||||
}
|
||||
handleError() {
|
||||
this.flag = false;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<t t-if="flag">
|
||||
<ErrorHandler onError="() => this.handleError()">
|
||||
<Boom />
|
||||
</ErrorHandler>
|
||||
</t>
|
||||
<t t-else="">
|
||||
not boom
|
||||
</t>
|
||||
</div>`;
|
||||
Parent.components = { Boom, ErrorHandler };
|
||||
|
||||
const target = getFixture();
|
||||
await mount(Parent, target, { env: makeTestEnv() });
|
||||
assert.strictEqual(target.innerHTML, "<div> not boom </div>");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,442 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Deferred, Mutex, KeepLast, Race } from "@web/core/utils/concurrency";
|
||||
import { nextTick, makeDeferred } from "../../helpers/utils";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("Concurrency");
|
||||
|
||||
QUnit.test("Mutex: simple scheduling", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const mutex = new Mutex();
|
||||
const def1 = makeDeferred();
|
||||
const def2 = makeDeferred();
|
||||
|
||||
mutex.exec(() => def1).then(() => assert.step("ok [1]"));
|
||||
mutex.exec(() => def2).then(() => assert.step("ok [2]"));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def1.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok [1]"]);
|
||||
|
||||
def2.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok [2]"]);
|
||||
});
|
||||
|
||||
QUnit.test("Mutex: simple scheduling (2)", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const mutex = new Mutex();
|
||||
const def1 = makeDeferred();
|
||||
const def2 = makeDeferred();
|
||||
|
||||
mutex.exec(() => def1).then(() => assert.step("ok [1]"));
|
||||
mutex.exec(() => def2).then(() => assert.step("ok [2]"));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def2.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def1.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok [1]", "ok [2]"]);
|
||||
});
|
||||
|
||||
QUnit.test("Mutex: reject", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const mutex = new Mutex();
|
||||
const def1 = makeDeferred();
|
||||
const def2 = makeDeferred();
|
||||
const def3 = makeDeferred();
|
||||
|
||||
mutex.exec(() => def1).then(() => assert.step("ok [1]"));
|
||||
mutex.exec(() => def2).catch(() => assert.step("ko [2]"));
|
||||
mutex.exec(() => def3).then(() => assert.step("ok [3]"));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def1.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok [1]"]);
|
||||
|
||||
def2.reject({ name: "sdkjfmqsjdfmsjkdfkljsdq" });
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ko [2]"]);
|
||||
|
||||
def3.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok [3]"]);
|
||||
});
|
||||
|
||||
QUnit.test("Mutex: getUnlockedDef checks", async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
const mutex = new Mutex();
|
||||
const def1 = makeDeferred();
|
||||
const def2 = makeDeferred();
|
||||
|
||||
mutex.getUnlockedDef().then(() => assert.step("mutex unlocked (1)"));
|
||||
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["mutex unlocked (1)"]);
|
||||
|
||||
mutex.exec(() => def1).then(() => assert.step("ok [1]"));
|
||||
await nextTick();
|
||||
|
||||
mutex.getUnlockedDef().then(function () {
|
||||
assert.step("mutex unlocked (2)");
|
||||
});
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
mutex.exec(() => def2).then(() => assert.step("ok [2]"));
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def1.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok [1]"]);
|
||||
|
||||
def2.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["mutex unlocked (2)", "ok [2]"]);
|
||||
});
|
||||
|
||||
QUnit.test("Mutex: error and getUnlockedDef", async function (assert) {
|
||||
const mutex = new Mutex();
|
||||
const action = async () => {
|
||||
await Promise.resolve();
|
||||
throw new Error("boom");
|
||||
};
|
||||
mutex.exec(action).catch(() => assert.step("prom rejected"));
|
||||
await nextTick();
|
||||
assert.verifySteps(["prom rejected"]);
|
||||
|
||||
mutex.getUnlockedDef().then(() => assert.step("mutex unlocked"));
|
||||
await nextTick();
|
||||
assert.verifySteps(["mutex unlocked"]);
|
||||
});
|
||||
|
||||
QUnit.test("KeepLast: basic use", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const keepLast = new KeepLast();
|
||||
const def = makeDeferred();
|
||||
|
||||
keepLast.add(def).then(() => assert.step("ok"));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok"]);
|
||||
});
|
||||
|
||||
QUnit.test("KeepLast: rejected promise", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const keepLast = new KeepLast();
|
||||
const def = makeDeferred();
|
||||
|
||||
keepLast.add(def).catch(() => assert.step("ko"));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def.reject();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ko"]);
|
||||
});
|
||||
|
||||
QUnit.test("KeepLast: two promises resolved in order", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const keepLast = new KeepLast();
|
||||
const def1 = makeDeferred();
|
||||
const def2 = makeDeferred();
|
||||
|
||||
keepLast.add(def1).then(() => {
|
||||
throw new Error("should not be executed");
|
||||
});
|
||||
keepLast.add(def2).then(() => assert.step("ok [2]"));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def1.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def2.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok [2]"]);
|
||||
});
|
||||
|
||||
QUnit.test("KeepLast: two promises resolved in reverse order", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const keepLast = new KeepLast();
|
||||
const def1 = makeDeferred();
|
||||
const def2 = makeDeferred();
|
||||
|
||||
keepLast.add(def1).then(() => {
|
||||
throw new Error("should not be executed");
|
||||
});
|
||||
keepLast.add(def2).then(() => assert.step("ok [2]"));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def2.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok [2]"]);
|
||||
|
||||
def1.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("Race: basic use", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const race = new Race();
|
||||
const def = makeDeferred();
|
||||
|
||||
race.add(def).then((v) => assert.step(`ok (${v})`));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def.resolve(44);
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok (44)"]);
|
||||
});
|
||||
|
||||
QUnit.test("Race: two promises resolved in order", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const race = new Race();
|
||||
const def1 = makeDeferred();
|
||||
const def2 = makeDeferred();
|
||||
|
||||
race.add(def1).then((v) => assert.step(`ok (${v}) [1]`));
|
||||
race.add(def2).then((v) => assert.step(`ok (${v}) [2]`));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def1.resolve(44);
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok (44) [1]", "ok (44) [2]"]);
|
||||
|
||||
def2.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("Race: two promises resolved in reverse order", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const race = new Race();
|
||||
const def1 = makeDeferred();
|
||||
const def2 = makeDeferred();
|
||||
|
||||
race.add(def1).then((v) => assert.step(`ok (${v}) [1]`));
|
||||
race.add(def2).then((v) => assert.step(`ok (${v}) [2]`));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def2.resolve(44);
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok (44) [1]", "ok (44) [2]"]);
|
||||
|
||||
def1.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("Race: multiple resolutions", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const race = new Race();
|
||||
const def1 = makeDeferred();
|
||||
const def2 = makeDeferred();
|
||||
const def3 = makeDeferred();
|
||||
|
||||
race.add(def1).then((v) => assert.step(`ok (${v}) [1]`));
|
||||
def1.resolve(44);
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok (44) [1]"]);
|
||||
|
||||
race.add(def2).then((v) => assert.step(`ok (${v}) [2]`));
|
||||
race.add(def3).then((v) => assert.step(`ok (${v}) [3]`));
|
||||
|
||||
def2.resolve(44);
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok (44) [2]", "ok (44) [3]"]);
|
||||
});
|
||||
|
||||
QUnit.test("Race: catch rejected promise", async function (assert) {
|
||||
const race = new Race();
|
||||
const def = makeDeferred();
|
||||
|
||||
race.add(def).catch((v) => assert.step(`not ok (${v})`));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def.reject(44);
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["not ok (44)"]);
|
||||
});
|
||||
|
||||
QUnit.test("Race: first promise rejects first", async function (assert) {
|
||||
const race = new Race();
|
||||
const def1 = makeDeferred();
|
||||
const def2 = makeDeferred();
|
||||
|
||||
race.add(def1).catch((v) => assert.step(`not ok (${v}) [1]`));
|
||||
race.add(def2).catch((v) => assert.step(`not ok (${v}) [2]`));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def1.reject(44);
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["not ok (44) [1]", "not ok (44) [2]"]);
|
||||
|
||||
def2.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("Race: second promise rejects after", async function (assert) {
|
||||
const race = new Race();
|
||||
const def1 = makeDeferred();
|
||||
const def2 = makeDeferred();
|
||||
|
||||
race.add(def1).then((v) => assert.step(`ok (${v}) [1]`));
|
||||
race.add(def2).then((v) => assert.step(`ok (${v}) [2]`));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def1.resolve(44);
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok (44) [1]", "ok (44) [2]"]);
|
||||
|
||||
def2.reject();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("Race: second promise rejects first", async function (assert) {
|
||||
const race = new Race();
|
||||
const def1 = makeDeferred();
|
||||
const def2 = makeDeferred();
|
||||
|
||||
race.add(def1).catch((v) => assert.step(`not ok (${v}) [1]`));
|
||||
race.add(def2).catch((v) => assert.step(`not ok (${v}) [2]`));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def2.reject(44);
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["not ok (44) [1]", "not ok (44) [2]"]);
|
||||
|
||||
def1.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("Race: first promise rejects after", async function (assert) {
|
||||
const race = new Race();
|
||||
const def1 = makeDeferred();
|
||||
const def2 = makeDeferred();
|
||||
|
||||
race.add(def1).then((v) => assert.step(`ok (${v}) [1]`));
|
||||
race.add(def2).then((v) => assert.step(`ok (${v}) [2]`));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
def2.resolve(44);
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["ok (44) [1]", "ok (44) [2]"]);
|
||||
|
||||
def1.reject();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("Race: getCurrentProm", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const race = new Race();
|
||||
const def1 = makeDeferred();
|
||||
const def2 = makeDeferred();
|
||||
const def3 = makeDeferred();
|
||||
|
||||
assert.strictEqual(race.getCurrentProm(), null);
|
||||
|
||||
race.add(def1);
|
||||
race.getCurrentProm().then((v) => assert.step(`ok (${v})`));
|
||||
def1.resolve(44);
|
||||
await nextTick();
|
||||
assert.verifySteps(["ok (44)"]);
|
||||
assert.strictEqual(race.getCurrentProm(), null);
|
||||
|
||||
race.add(def2);
|
||||
race.getCurrentProm().then((v) => assert.step(`ok (${v})`));
|
||||
race.add(def3);
|
||||
def3.resolve(44);
|
||||
await nextTick();
|
||||
assert.verifySteps(["ok (44)"]);
|
||||
assert.strictEqual(race.getCurrentProm(), null);
|
||||
});
|
||||
|
||||
QUnit.test("Deferred: basic use", async function (assert) {
|
||||
const def1 = new Deferred();
|
||||
def1.then((v) => assert.step(`ok (${v})`));
|
||||
def1.resolve(44);
|
||||
await nextTick();
|
||||
assert.verifySteps(["ok (44)"]);
|
||||
|
||||
const def2 = new Deferred();
|
||||
def2.catch((v) => assert.step(`ko (${v})`));
|
||||
def2.reject(44);
|
||||
await nextTick();
|
||||
assert.verifySteps(["ko (44)"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { memoize } from "@web/core/utils/functions";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("Functions");
|
||||
|
||||
QUnit.test("memoize", function (assert) {
|
||||
let callCount = 0;
|
||||
let lastReceivedArgs;
|
||||
const func = function () {
|
||||
lastReceivedArgs = [...arguments];
|
||||
return callCount++;
|
||||
};
|
||||
const memoized = memoize(func);
|
||||
const firstValue = memoized("first");
|
||||
assert.equal(callCount, 1, "Memoized function was called once to fill the cache");
|
||||
assert.equal(lastReceivedArgs, "first", "Memoized function received the correct argument");
|
||||
const secondValue = memoized("first");
|
||||
assert.equal(
|
||||
callCount,
|
||||
1,
|
||||
"Subsequent calls to memoized function with the same argument do not call the original function again"
|
||||
);
|
||||
assert.equal(
|
||||
firstValue,
|
||||
secondValue,
|
||||
"Subsequent call to memoized function with the same argument returns the same value"
|
||||
);
|
||||
|
||||
const thirdValue = memoized();
|
||||
assert.equal(
|
||||
callCount,
|
||||
2,
|
||||
"Subsequent calls to memoized function with a different argument call the original function again"
|
||||
);
|
||||
const fourthValue = memoized();
|
||||
assert.equal(
|
||||
thirdValue,
|
||||
fourthValue,
|
||||
"Memoization also works with no first argument as a key"
|
||||
);
|
||||
assert.equal(
|
||||
callCount,
|
||||
2,
|
||||
"Subsequent calls to memoized function with no first argument do not call the original function again"
|
||||
);
|
||||
|
||||
memoized(1, 2, 3);
|
||||
assert.equal(callCount, 3);
|
||||
assert.deepEqual(
|
||||
lastReceivedArgs,
|
||||
[1, 2, 3],
|
||||
"Arguments after the first one are passed through correctly"
|
||||
);
|
||||
memoized(1, 20, 30);
|
||||
assert.equal(
|
||||
callCount,
|
||||
3,
|
||||
"Subsequent calls to memoized function with more than one argument do not call the original function again even if the arguments other than the first have changed"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("memoized function inherit function name if possible", function (assert) {
|
||||
const memoized1 = memoize(function test() {});
|
||||
assert.strictEqual(memoized1.name, "test (memoized)");
|
||||
|
||||
const memoized2 = memoize(function () {});
|
||||
assert.strictEqual(memoized2.name, "memoized");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,490 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { useAutofocus, useBus, useChildRef, useForwardRefToParent, useListener, useService } from "@web/core/utils/hooks";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import {
|
||||
click,
|
||||
destroy,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
mount,
|
||||
nextTick,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { LegacyComponent } from "@web/legacy/legacy_component";
|
||||
|
||||
import { Component, onMounted, useState, xml } from "@odoo/owl";
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("Hooks", () => {
|
||||
QUnit.module("useAutofocus");
|
||||
|
||||
QUnit.test("useAutofocus: simple usecase", async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus();
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<span>
|
||||
<input type="text" t-ref="autofocus" />
|
||||
</span>
|
||||
`;
|
||||
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(document.activeElement, comp.inputRef.el);
|
||||
|
||||
comp.render();
|
||||
await nextTick();
|
||||
assert.strictEqual(document.activeElement, comp.inputRef.el);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"useAutofocus: simple usecase when input type is number",
|
||||
async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus();
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<span>
|
||||
<input type="number" t-ref="autofocus" />
|
||||
</span>
|
||||
`;
|
||||
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
|
||||
assert.strictEqual(document.activeElement, comp.inputRef.el);
|
||||
|
||||
comp.render();
|
||||
await nextTick();
|
||||
assert.strictEqual(document.activeElement, comp.inputRef.el);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("useAutofocus: conditional autofocus", async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus();
|
||||
this.showInput = true;
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<span>
|
||||
<input t-if="showInput" type="text" t-ref="autofocus" />
|
||||
</span>
|
||||
`;
|
||||
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(document.activeElement, comp.inputRef.el);
|
||||
|
||||
comp.showInput = false;
|
||||
comp.render();
|
||||
await nextTick();
|
||||
assert.notStrictEqual(document.activeElement, comp.inputRef.el);
|
||||
|
||||
comp.showInput = true;
|
||||
comp.render();
|
||||
await nextTick();
|
||||
assert.strictEqual(document.activeElement, comp.inputRef.el);
|
||||
});
|
||||
|
||||
QUnit.test("useAutofocus returns also a ref when isSmall is true", async function (assert) {
|
||||
assert.expect(2);
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus();
|
||||
assert.ok(this.env.isSmall);
|
||||
onMounted(() => {
|
||||
assert.ok(this.inputRef.el);
|
||||
});
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<span>
|
||||
<input type="text" t-ref="autofocus" />
|
||||
</span>
|
||||
`;
|
||||
|
||||
const fakeUIService = {
|
||||
start(env) {
|
||||
const ui = {};
|
||||
Object.defineProperty(env, "isSmall", {
|
||||
get() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
return ui;
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("ui", fakeUIService);
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
await mount(MyComponent, target, { env });
|
||||
});
|
||||
|
||||
QUnit.test("supports different ref names", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.secondRef = useAutofocus({ refName: "second" });
|
||||
this.firstRef = useAutofocus({ refName: "first" });
|
||||
|
||||
this.state = useState({ showSecond: true });
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<span>
|
||||
<input type="text" t-ref="first" />
|
||||
<input t-if="state.showSecond" type="text" t-ref="second" />
|
||||
</span>
|
||||
`;
|
||||
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
await nextTick();
|
||||
|
||||
// "first" is focused first since it has the last call to "useAutofocus"
|
||||
assert.strictEqual(document.activeElement, comp.firstRef.el);
|
||||
|
||||
comp.state.showSecond = false;
|
||||
await nextTick();
|
||||
comp.state.showSecond = true;
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(document.activeElement, comp.secondRef.el);
|
||||
});
|
||||
|
||||
QUnit.test("can select an entire text", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus({ selectAll: true });
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<span>
|
||||
<input type="text" value="abcdefghij" t-ref="autofocus" />
|
||||
</span>
|
||||
`;
|
||||
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(document.activeElement, comp.inputRef.el);
|
||||
assert.strictEqual(comp.inputRef.el.selectionStart, 0);
|
||||
assert.strictEqual(comp.inputRef.el.selectionEnd, 10);
|
||||
});
|
||||
|
||||
QUnit.module("useBus");
|
||||
|
||||
QUnit.test("useBus hook: simple usecase", async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useBus(this.env.bus, "test-event", this.myCallback);
|
||||
}
|
||||
myCallback() {
|
||||
assert.step("callback");
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<div/>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
env.bus.trigger("test-event");
|
||||
await nextTick();
|
||||
assert.verifySteps(["callback"]);
|
||||
|
||||
destroy(comp);
|
||||
env.bus.trigger("test-event");
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.module("useListener");
|
||||
|
||||
QUnit.test("useListener: simple usecase", async function (assert) {
|
||||
class MyComponent extends LegacyComponent {
|
||||
setup() {
|
||||
useListener("click", () => assert.step("click"));
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<button class="root">Click Me</button>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
await mount(MyComponent, target, { env });
|
||||
|
||||
await click(target.querySelector(".root"));
|
||||
assert.verifySteps(["click"]);
|
||||
});
|
||||
|
||||
QUnit.test("useListener: event delegation", async function (assert) {
|
||||
class MyComponent extends LegacyComponent {
|
||||
setup() {
|
||||
this.flag = true;
|
||||
useListener("click", "button", () => assert.step("click"));
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<div class="root">
|
||||
<button t-if="flag">Click Here</button>
|
||||
<button t-else="">
|
||||
<span>or Here</span>
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
|
||||
await click(target.querySelector(".root"));
|
||||
assert.verifySteps([]);
|
||||
await click(target.querySelector("button"));
|
||||
assert.verifySteps(["click"]);
|
||||
|
||||
comp.flag = false;
|
||||
comp.render();
|
||||
await nextTick();
|
||||
await click(target.querySelector("button span"));
|
||||
assert.verifySteps(["click"]);
|
||||
});
|
||||
|
||||
QUnit.test("useListener: event delegation with capture option", async function (assert) {
|
||||
class MyComponent extends LegacyComponent {
|
||||
setup() {
|
||||
this.flag = false;
|
||||
useListener("click", "button", () => assert.step("click"), { capture: true });
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<div class="root">
|
||||
<button t-if="flag">Click Here</button>
|
||||
<button t-else="">
|
||||
<span>or Here</span>
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
|
||||
await click(target.querySelector(".root"));
|
||||
assert.verifySteps([]);
|
||||
await click(target.querySelector("button"));
|
||||
assert.verifySteps(["click"]);
|
||||
|
||||
comp.flag = false;
|
||||
await comp.render();
|
||||
await click(target.querySelector("button span"));
|
||||
assert.verifySteps(["click"]);
|
||||
});
|
||||
|
||||
QUnit.module("useService");
|
||||
|
||||
QUnit.test("useService: unavailable service", async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useService("toy_service");
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<div/>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
try {
|
||||
await mount(MyComponent, target, { env });
|
||||
} catch (e) {
|
||||
assert.strictEqual(e.message, "Service toy_service is not available");
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("useService: service that returns null", async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.toyService = useService("toy_service");
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<div/>`;
|
||||
|
||||
serviceRegistry.add("toy_service", {
|
||||
name: "toy_service",
|
||||
start: () => {
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
assert.strictEqual(comp.toyService, null);
|
||||
});
|
||||
|
||||
QUnit.test("useService: async service with protected methods", async function (assert) {
|
||||
let nbCalls = 0;
|
||||
let def = makeDeferred();
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.objectService = useService("object_service");
|
||||
this.functionService = useService("function_service");
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<div/>`;
|
||||
|
||||
serviceRegistry.add("object_service", {
|
||||
name: "object_service",
|
||||
async: ["asyncMethod"],
|
||||
start() {
|
||||
return {
|
||||
async asyncMethod() {
|
||||
nbCalls++;
|
||||
await def;
|
||||
return this;
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
serviceRegistry.add("function_service", {
|
||||
name: "function_service",
|
||||
async: true,
|
||||
start() {
|
||||
return async function asyncFunc() {
|
||||
nbCalls++;
|
||||
await def;
|
||||
return this;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
// Functions and methods have the correct this
|
||||
def.resolve();
|
||||
assert.deepEqual(await comp.objectService.asyncMethod(), comp.objectService);
|
||||
assert.deepEqual(await comp.objectService.asyncMethod.call("boundThis"), "boundThis");
|
||||
assert.deepEqual(await comp.functionService(), comp);
|
||||
assert.deepEqual(await comp.functionService.call("boundThis"), "boundThis");
|
||||
assert.strictEqual(nbCalls, 4);
|
||||
// Functions that were called before the component is destroyed but resolved after never resolve
|
||||
let nbResolvedProms = 0;
|
||||
def = makeDeferred();
|
||||
comp.objectService.asyncMethod().then(() => nbResolvedProms++);
|
||||
comp.objectService.asyncMethod.call("boundThis").then(() => nbResolvedProms++);
|
||||
comp.functionService().then(() => nbResolvedProms++);
|
||||
comp.functionService.call("boundThis").then(() => nbResolvedProms++);
|
||||
assert.strictEqual(nbCalls, 8);
|
||||
comp.__owl__.app.destroy();
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
nbResolvedProms,
|
||||
0,
|
||||
"The promises returned by the calls should never resolve"
|
||||
);
|
||||
// Calling the functions after the destruction rejects the promise
|
||||
assert.rejects(comp.objectService.asyncMethod(), "Component is destroyed");
|
||||
assert.rejects(
|
||||
comp.objectService.asyncMethod.call("boundThis"),
|
||||
"Component is destroyed"
|
||||
);
|
||||
assert.rejects(comp.functionService(), "Component is destroyed");
|
||||
assert.rejects(comp.functionService.call("boundThis"), "Component is destroyed");
|
||||
assert.strictEqual(nbCalls, 8);
|
||||
});
|
||||
|
||||
QUnit.module("useChildRef / useForwardRefToParent");
|
||||
|
||||
QUnit.test("simple usecase", async function (assert) {
|
||||
let childRef;
|
||||
let parentRef;
|
||||
class Child extends Component {
|
||||
setup() {
|
||||
childRef = useForwardRefToParent("someRef");
|
||||
}
|
||||
}
|
||||
Child.template = xml`<span t-ref="someRef" class="my_span">Hello</span>`;
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.someRef = useChildRef();
|
||||
parentRef = this.someRef;
|
||||
}
|
||||
}
|
||||
Parent.template = xml`<div><Child someRef="someRef"/></div>`;
|
||||
Parent.components = { Child };
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.strictEqual(childRef.el, target.querySelector(".my_span"));
|
||||
assert.strictEqual(parentRef.el, target.querySelector(".my_span"));
|
||||
});
|
||||
|
||||
QUnit.test("useForwardRefToParent in a conditional child", async function (assert) {
|
||||
class Child extends Component {
|
||||
setup() {
|
||||
useForwardRefToParent("someRef");
|
||||
}
|
||||
}
|
||||
Child.template = xml`<span t-ref="someRef" class="my_span">Hello</span>`;
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.someRef = useChildRef();
|
||||
this.state = useState({ hasChild: true });
|
||||
}
|
||||
}
|
||||
Parent.template = xml`<div><Child t-if="state.hasChild" someRef="someRef"/></div>`;
|
||||
Parent.components = { Child };
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
|
||||
const parent = await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".my_span");
|
||||
assert.strictEqual(parent.someRef.el, target.querySelector(".my_span"));
|
||||
|
||||
parent.state.hasChild = false;
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(target, ".my_span");
|
||||
assert.strictEqual(parent.someRef.el, null);
|
||||
|
||||
parent.state.hasChild = true;
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".my_span");
|
||||
assert.strictEqual(parent.someRef.el, target.querySelector(".my_span"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { omit, pick, shallowEqual } from "@web/core/utils/objects";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("Objects");
|
||||
|
||||
QUnit.test("omit", function (assert) {
|
||||
assert.deepEqual(omit({}), {});
|
||||
assert.deepEqual(omit({}, "a"), {});
|
||||
assert.deepEqual(omit({ a: 1 }), { a: 1 });
|
||||
assert.deepEqual(omit({ a: 1 }, "a"), {});
|
||||
assert.deepEqual(omit({ a: 1, b: 2 }, "c", "a"), { b: 2 });
|
||||
assert.deepEqual(omit({ a: 1, b: 2 }, "b", "c"), { a: 1 });
|
||||
});
|
||||
|
||||
QUnit.test("pick", function (assert) {
|
||||
assert.deepEqual(pick({}), {});
|
||||
assert.deepEqual(pick({}, "a"), {});
|
||||
assert.deepEqual(pick({ a: 3, b: "a", c: [] }, "a"), { a: 3 });
|
||||
assert.deepEqual(pick({ a: 3, b: "a", c: [] }, "a", "c"), { a: 3, c: [] });
|
||||
assert.deepEqual(pick({ a: 3, b: "a", c: [] }, "a", "b", "c"), { a: 3, b: "a", c: [] });
|
||||
|
||||
// Non enumerable property
|
||||
class MyClass {
|
||||
get a() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
const myClass = new MyClass();
|
||||
Object.defineProperty(myClass, "b", { enumerable: false, value: 2 });
|
||||
assert.deepEqual(pick(myClass, "a", "b"), { a: 1, b: 2 });
|
||||
});
|
||||
|
||||
QUnit.test("shallowEqual: simple valid cases", function (assert) {
|
||||
assert.ok(shallowEqual({}, {}));
|
||||
assert.ok(shallowEqual({ a: 1 }, { a: 1 }));
|
||||
assert.ok(shallowEqual({ a: 1, b: "x" }, { b: "x", a: 1 }));
|
||||
});
|
||||
|
||||
QUnit.test("shallowEqual: simple invalid cases", function (assert) {
|
||||
assert.notOk(shallowEqual({ a: 1 }, { a: 2 }));
|
||||
assert.notOk(shallowEqual({}, { a: 2 }));
|
||||
assert.notOk(shallowEqual({ a: 1 }, {}));
|
||||
});
|
||||
|
||||
QUnit.test("shallowEqual: objects with non primitive values", function (assert) {
|
||||
const obj = { x: "y" };
|
||||
assert.ok(shallowEqual({ a: obj }, { a: obj }));
|
||||
assert.notOk(shallowEqual({ a: { x: "y" } }, { a: { x: "y" } }));
|
||||
|
||||
const arr = ["x", "y", "z"];
|
||||
assert.ok(shallowEqual({ a: arr }, { a: arr }));
|
||||
assert.notOk(shallowEqual({ a: ["x", "y", "z"] }, { a: ["x", "y", "z"] }));
|
||||
|
||||
const fn = () => {};
|
||||
assert.ok(shallowEqual({ a: fn }, { a: fn }));
|
||||
assert.notOk(shallowEqual({ a: () => {} }, { a: () => {} }));
|
||||
});
|
||||
});
|
||||
1205
odoo-bringout-oca-ocb-web/web/static/tests/core/utils/patch_tests.js
Normal file
1205
odoo-bringout-oca-ocb-web/web/static/tests/core/utils/patch_tests.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,54 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { fuzzyLookup, fuzzyTest } from "@web/core/utils/search";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("Fuzzy Search");
|
||||
|
||||
QUnit.test("fuzzyLookup", function (assert) {
|
||||
const data = [
|
||||
{ name: "Abby White" },
|
||||
{ name: "Robert Black" },
|
||||
{ name: "Jane Yellow" },
|
||||
{ name: "Brandon Green" },
|
||||
{ name: "Jérémy Red" },
|
||||
];
|
||||
assert.deepEqual(
|
||||
fuzzyLookup("ba", data, (d) => d.name),
|
||||
[{ name: "Brandon Green" }, { name: "Robert Black" }]
|
||||
);
|
||||
assert.deepEqual(
|
||||
fuzzyLookup("g", data, (d) => d.name),
|
||||
[{ name: "Brandon Green" }]
|
||||
);
|
||||
assert.deepEqual(
|
||||
fuzzyLookup("z", data, (d) => d.name),
|
||||
[]
|
||||
);
|
||||
assert.deepEqual(
|
||||
fuzzyLookup("brand", data, (d) => d.name),
|
||||
[{ name: "Brandon Green" }]
|
||||
);
|
||||
assert.deepEqual(
|
||||
fuzzyLookup("jâ", data, (d) => d.name),
|
||||
[{ name: "Jane Yellow" }]
|
||||
);
|
||||
assert.deepEqual(
|
||||
fuzzyLookup("je", data, (d) => d.name),
|
||||
[{ name: "Jérémy Red" }, { name: "Jane Yellow" }]
|
||||
);
|
||||
assert.deepEqual(
|
||||
fuzzyLookup("", data, (d) => d.name),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("fuzzyTest", function (assert) {
|
||||
assert.ok(fuzzyTest("a", "Abby White"));
|
||||
assert.ok(fuzzyTest("ba", "Brandon Green"));
|
||||
assert.ok(fuzzyTest("je", "Jérémy red"));
|
||||
assert.ok(fuzzyTest("jé", "Jeremy red"));
|
||||
assert.notOk(fuzzyTest("z", "Abby White"));
|
||||
assert.notOk(fuzzyTest("ba", "Abby White"));
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { escapeRegExp, intersperse, sprintf } from "@web/core/utils/strings";
|
||||
import { _lt, translatedTerms } from "@web/core/l10n/translation";
|
||||
import { patchWithCleanup } from "../../helpers/utils";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("strings");
|
||||
|
||||
QUnit.test("escapeRegExp", (assert) => {
|
||||
assert.deepEqual(escapeRegExp(""), "");
|
||||
assert.deepEqual(escapeRegExp("wowl"), "wowl");
|
||||
assert.deepEqual(escapeRegExp("[wowl]"), "\\[wowl\\]");
|
||||
assert.deepEqual(escapeRegExp("[wowl.odoo]"), "\\[wowl\\.odoo\\]");
|
||||
assert.deepEqual(
|
||||
escapeRegExp("^odoo.define([.]*)$"),
|
||||
"\\^odoo\\.define\\(\\[\\.\\]\\*\\)\\$"
|
||||
);
|
||||
assert.deepEqual(
|
||||
escapeRegExp("[.*+?^${}()|[]\\"),
|
||||
"\\[\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("intersperse", (assert) => {
|
||||
assert.deepEqual(intersperse("", []), "");
|
||||
assert.deepEqual(intersperse("0", []), "0");
|
||||
assert.deepEqual(intersperse("012", []), "012");
|
||||
assert.deepEqual(intersperse("1", []), "1");
|
||||
assert.deepEqual(intersperse("12", []), "12");
|
||||
assert.deepEqual(intersperse("123", []), "123");
|
||||
assert.deepEqual(intersperse("1234", []), "1234");
|
||||
assert.deepEqual(intersperse("123456789", []), "123456789");
|
||||
assert.deepEqual(intersperse("&ab%#@1", []), "&ab%#@1");
|
||||
assert.deepEqual(intersperse("0", []), "0");
|
||||
assert.deepEqual(intersperse("0", [1]), "0");
|
||||
assert.deepEqual(intersperse("0", [2]), "0");
|
||||
assert.deepEqual(intersperse("0", [200]), "0");
|
||||
assert.deepEqual(intersperse("12345678", [0], "."), "12345678");
|
||||
assert.deepEqual(intersperse("", [1], "."), "");
|
||||
assert.deepEqual(intersperse("12345678", [1], "."), "1234567.8");
|
||||
assert.deepEqual(intersperse("12345678", [1], "."), "1234567.8");
|
||||
assert.deepEqual(intersperse("12345678", [2], "."), "123456.78");
|
||||
assert.deepEqual(intersperse("12345678", [2, 1], "."), "12345.6.78");
|
||||
assert.deepEqual(intersperse("12345678", [2, 0], "."), "12.34.56.78");
|
||||
assert.deepEqual(intersperse("12345678", [-1, 2], "."), "12345678");
|
||||
assert.deepEqual(intersperse("12345678", [2, -1], "."), "123456.78");
|
||||
assert.deepEqual(intersperse("12345678", [2, 0, 1], "."), "12.34.56.78");
|
||||
assert.deepEqual(intersperse("12345678", [2, 0, 0], "."), "12.34.56.78");
|
||||
assert.deepEqual(intersperse("12345678", [2, 0, -1], "."), "12.34.56.78");
|
||||
assert.deepEqual(intersperse("12345678", [3, 3, 3, 3], "."), "12.345.678");
|
||||
assert.deepEqual(intersperse("12345678", [3, 0], "."), "12.345.678");
|
||||
});
|
||||
|
||||
QUnit.test("sprintf properly formats strings", (assert) => {
|
||||
assert.deepEqual(sprintf("Hello %s!", "ged"), "Hello ged!");
|
||||
assert.deepEqual(sprintf("Hello %s and %s!", "ged", "lpe"), "Hello ged and lpe!");
|
||||
assert.deepEqual(sprintf("Hello %(x)s!", { x: "ged" }), "Hello ged!");
|
||||
assert.deepEqual(
|
||||
sprintf("Hello %(x)s and %(y)s!", { x: "ged", y: "lpe" }),
|
||||
"Hello ged and lpe!"
|
||||
);
|
||||
assert.deepEqual(sprintf("Hello!"), "Hello!");
|
||||
assert.deepEqual(sprintf("Hello %s!"), "Hello %s!");
|
||||
assert.deepEqual(sprintf("Hello %(value)s!"), "Hello %(value)s!");
|
||||
});
|
||||
|
||||
QUnit.test("sprintf properly formats numbers", (assert) => {
|
||||
assert.deepEqual(sprintf("Hello %s!", 5), "Hello 5!");
|
||||
assert.deepEqual(sprintf("Hello %s and %s!", 9, 10), "Hello 9 and 10!");
|
||||
assert.deepEqual(sprintf("Hello %(x)s!", { x: 11 }), "Hello 11!");
|
||||
assert.deepEqual(sprintf("Hello %(x)s and %(y)s!", { x: 12, y: 13 }), "Hello 12 and 13!");
|
||||
});
|
||||
|
||||
QUnit.test("sprintf set behavior when value is an Array", (assert) => {
|
||||
assert.deepEqual(sprintf("Hello %s!", ["inarray"]), "Hello inarray!");
|
||||
assert.deepEqual(sprintf("Hello %s and %s!", [9, "10"], [11]), "Hello 9,10 and 11!");
|
||||
assert.deepEqual(sprintf("Hello %(x)s!", { x: [11] }), "Hello 11!");
|
||||
assert.deepEqual(
|
||||
sprintf("Hello %(x)s and %(y)s!", { x: [12], y: ["13"] }),
|
||||
"Hello 12 and 13!"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("sprintf supports lazy translated string", (assert) => {
|
||||
patchWithCleanup(translatedTerms, {
|
||||
one: "en",
|
||||
two: "två",
|
||||
});
|
||||
|
||||
assert.deepEqual(sprintf("Hello %s", _lt("one")), "Hello en");
|
||||
assert.deepEqual(sprintf("Hello %s %s", _lt("one"), _lt("two")), "Hello en två");
|
||||
|
||||
const vals = {
|
||||
one: _lt("one"),
|
||||
two: _lt("two"),
|
||||
};
|
||||
assert.deepEqual(sprintf("Hello %(two)s %(one)s", vals), "Hello två en");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { debounce, throttleForAnimation } from "@web/core/utils/timing";
|
||||
import {
|
||||
makeDeferred,
|
||||
patchWithCleanup,
|
||||
mockTimeout,
|
||||
mockAnimationFrame,
|
||||
} from "../../helpers/utils";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("timing");
|
||||
|
||||
QUnit.test("debounce on an async function", async function (assert) {
|
||||
let callback;
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (later) => {
|
||||
callback = later;
|
||||
},
|
||||
});
|
||||
const imSearchDef = makeDeferred();
|
||||
const myFunc = () => {
|
||||
assert.step("myFunc");
|
||||
return imSearchDef;
|
||||
};
|
||||
const myDebouncedFunc = debounce(myFunc, 3000);
|
||||
myDebouncedFunc().then(() => {
|
||||
throw new Error("Should never be resolved");
|
||||
});
|
||||
myDebouncedFunc().then((x) => {
|
||||
assert.step("resolved " + x);
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
callback();
|
||||
assert.verifySteps(["myFunc"]);
|
||||
imSearchDef.resolve(42);
|
||||
await Promise.resolve(); // wait for promise returned by myFunc
|
||||
await Promise.resolve(); // wait for promise returned by debounce
|
||||
|
||||
assert.verifySteps(["resolved 42"]);
|
||||
});
|
||||
|
||||
QUnit.test("debounce on a sync function", async function (assert) {
|
||||
let callback;
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (later) => {
|
||||
callback = later;
|
||||
},
|
||||
});
|
||||
const myFunc = () => {
|
||||
assert.step("myFunc");
|
||||
return 42;
|
||||
};
|
||||
const myDebouncedFunc = debounce(myFunc, 3000);
|
||||
myDebouncedFunc().then(() => {
|
||||
throw new Error("Should never be resolved");
|
||||
});
|
||||
myDebouncedFunc().then((x) => {
|
||||
assert.step("resolved " + x);
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
callback();
|
||||
assert.verifySteps(["myFunc"]);
|
||||
await Promise.resolve(); // wait for promise returned by myFunc
|
||||
await Promise.resolve(); // wait for promise returned by debounce
|
||||
|
||||
assert.verifySteps(["resolved 42"]);
|
||||
});
|
||||
|
||||
QUnit.test("debounce with immediate", async function (assert) {
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
const myFunc = () => {
|
||||
assert.step("myFunc");
|
||||
return 42;
|
||||
};
|
||||
const myDebouncedFunc = debounce(myFunc, 3000, { immediate: true });
|
||||
myDebouncedFunc().then((x) => {
|
||||
assert.step("resolved " + x);
|
||||
});
|
||||
assert.verifySteps(["myFunc"]);
|
||||
await Promise.resolve(); // wait for promise returned by debounce
|
||||
await Promise.resolve(); // wait for promise returned chained onto it (step resolved x)
|
||||
assert.verifySteps(["resolved 42"]);
|
||||
|
||||
myDebouncedFunc().then((x) => {
|
||||
assert.step("resolved " + x);
|
||||
});
|
||||
await execRegisteredTimeouts();
|
||||
assert.verifySteps([]); // not called 3000ms did not elapse between the previous call and the first
|
||||
|
||||
myDebouncedFunc().then((x) => {
|
||||
assert.step("resolved " + x);
|
||||
});
|
||||
assert.verifySteps(["myFunc"]);
|
||||
await Promise.resolve(); // wait for promise returned by debounce
|
||||
await Promise.resolve(); // wait for promise returned chained onto it (step resolved x)
|
||||
assert.verifySteps(["resolved 42"]);
|
||||
});
|
||||
|
||||
QUnit.test("debounce with 'animationFrame' delay", async function (assert) {
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
const execRegisteredAnimationFrames = mockAnimationFrame();
|
||||
const myFunc = () => {
|
||||
assert.step("myFunc");
|
||||
};
|
||||
debounce(myFunc, "animationFrame")();
|
||||
assert.verifySteps([]);
|
||||
|
||||
execRegisteredTimeouts(); // should have no effect as we wait for the animation frame
|
||||
assert.verifySteps([]);
|
||||
|
||||
execRegisteredAnimationFrames(); // should call the function
|
||||
assert.verifySteps(["myFunc"]);
|
||||
});
|
||||
|
||||
QUnit.test("debounced call can be cancelled", async function (assert) {
|
||||
assert.expect(3);
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
const myFunc = () => {
|
||||
assert.step("myFunc");
|
||||
};
|
||||
const myDebouncedFunc = debounce(myFunc, 3000);
|
||||
myDebouncedFunc();
|
||||
myDebouncedFunc.cancel();
|
||||
execRegisteredTimeouts();
|
||||
assert.verifySteps([], "Debounced call was cancelled");
|
||||
|
||||
myDebouncedFunc();
|
||||
execRegisteredTimeouts();
|
||||
assert.verifySteps(["myFunc"], "Debounced call was not cancelled");
|
||||
});
|
||||
|
||||
QUnit.test("throttleForAnimation", async (assert) => {
|
||||
assert.expect(4);
|
||||
const execAnimationFrameCallbacks = mockAnimationFrame();
|
||||
const throttledFn = throttleForAnimation((val) => {
|
||||
assert.step(`throttled function called with ${val}`);
|
||||
});
|
||||
|
||||
throttledFn(0);
|
||||
throttledFn(1);
|
||||
assert.verifySteps([], "throttled function hasn't been called yet");
|
||||
execAnimationFrameCallbacks();
|
||||
assert.verifySteps(
|
||||
["throttled function called with 1"],
|
||||
"only the last queued call was executed"
|
||||
);
|
||||
throttledFn(2);
|
||||
throttledFn(3);
|
||||
throttledFn.cancel();
|
||||
execAnimationFrameCallbacks();
|
||||
assert.verifySteps([], "queued throttled function calls were cancelled correctly");
|
||||
});
|
||||
|
||||
QUnit.test("throttleForAnimationScrollEvent", async (assert) => {
|
||||
assert.expect(5);
|
||||
const execAnimationFrameCallbacks = mockAnimationFrame();
|
||||
let resolveThrottled;
|
||||
const throttled = new Promise(resolve => resolveThrottled = resolve);
|
||||
const throttledFn = throttleForAnimation((val, targetEl) => {
|
||||
// In Chrome, the currentTarget of scroll events is lost after the
|
||||
// event was handled, it is therefore null here.
|
||||
// Because of this, if it is needed, it must be included in the
|
||||
// callback signature.
|
||||
const nodeName = val && val.currentTarget && val.currentTarget.nodeName;
|
||||
const targetName = targetEl && targetEl.nodeName;
|
||||
assert.step(`throttled function called with ${nodeName} in event, but ${targetName} in parameter`);
|
||||
resolveThrottled();
|
||||
});
|
||||
|
||||
const el = document.createElement("div");
|
||||
el.style = "position: absolute; overflow: scroll; height: 100px; width: 100px;";
|
||||
const childEl = document.createElement("div");
|
||||
childEl.style = "height: 200px; width: 200px;";
|
||||
let resolveScrolled;
|
||||
const scrolled = new Promise(resolve => resolveScrolled = resolve);
|
||||
el.appendChild(childEl);
|
||||
el.addEventListener("scroll", (ev) => {
|
||||
assert.step("before scroll");
|
||||
throttledFn(ev, ev.currentTarget);
|
||||
assert.step("after scroll");
|
||||
resolveScrolled();
|
||||
});
|
||||
document.body.appendChild(el);
|
||||
el.scrollBy(1, 1);
|
||||
el.scrollBy(2, 2);
|
||||
el.remove();
|
||||
await scrolled;
|
||||
|
||||
assert.verifySteps([
|
||||
"before scroll",
|
||||
"after scroll",
|
||||
], "scroll happened but throttled function hasn't been called yet");
|
||||
setTimeout(execAnimationFrameCallbacks);
|
||||
await throttled;
|
||||
assert.verifySteps(
|
||||
["throttled function called with null in event, but DIV in parameter"],
|
||||
"currentTarget was not available in throttled function's event"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,485 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
drag,
|
||||
dragAndDrop,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
triggerHotkey
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { useSortable } from "@web/core/utils/sortable";
|
||||
|
||||
import { Component, reactive, useRef, useState, xml } from "@odoo/owl";
|
||||
|
||||
let target;
|
||||
QUnit.module("UI", ({ beforeEach }) => {
|
||||
beforeEach(() => (target = getFixture()));
|
||||
|
||||
QUnit.module("Sortable hook");
|
||||
|
||||
QUnit.test("Parameters error handling", async (assert) => {
|
||||
assert.expect(8);
|
||||
|
||||
const mountListAndAssert = async (setupList, shouldThrow) => {
|
||||
class List extends Component {
|
||||
setup() {
|
||||
setupList();
|
||||
}
|
||||
}
|
||||
|
||||
List.template = xml`
|
||||
<div t-ref="root" class="root">
|
||||
<ul class="list">
|
||||
<li t-foreach="[1, 2, 3]" t-as="i" t-key="i" t-esc="i" class="item" />
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
let err;
|
||||
await mount(List, target).catch((e) => (err = e));
|
||||
|
||||
assert.ok(
|
||||
shouldThrow ? err : !err,
|
||||
`An error should${shouldThrow ? "" : "n't"} have been thrown when mounting.`
|
||||
);
|
||||
};
|
||||
|
||||
// Incorrect params
|
||||
await mountListAndAssert(() => {
|
||||
useSortable({});
|
||||
}, true);
|
||||
await mountListAndAssert(() => {
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
});
|
||||
}, true);
|
||||
await mountListAndAssert(() => {
|
||||
useSortable({
|
||||
elements: ".item",
|
||||
});
|
||||
}, true);
|
||||
await mountListAndAssert(() => {
|
||||
useSortable({
|
||||
elements: ".item",
|
||||
groups: ".list",
|
||||
});
|
||||
}, true);
|
||||
await mountListAndAssert(() => {
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
setup: () => ({ elements: ".item" }),
|
||||
});
|
||||
}, true);
|
||||
await mountListAndAssert(() => {
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
elements: () => ".item",
|
||||
});
|
||||
}, true);
|
||||
|
||||
// Correct params
|
||||
await mountListAndAssert(() => {
|
||||
useSortable({
|
||||
ref: {},
|
||||
elements: ".item",
|
||||
enable: false,
|
||||
});
|
||||
}, false);
|
||||
await mountListAndAssert(() => {
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
connectGroups: () => true,
|
||||
});
|
||||
}, false);
|
||||
});
|
||||
|
||||
QUnit.test("Simple sorting in single group", async (assert) => {
|
||||
assert.expect(19);
|
||||
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
onDragStart({ element, group }) {
|
||||
assert.step("start");
|
||||
assert.notOk(group);
|
||||
assert.strictEqual(element.innerText, "1");
|
||||
},
|
||||
onElementEnter({ element }) {
|
||||
assert.step("elemententer");
|
||||
assert.strictEqual(element.innerText, "2");
|
||||
},
|
||||
onDragEnd({ element, group }) {
|
||||
assert.step("stop");
|
||||
assert.notOk(group);
|
||||
assert.strictEqual(element.innerText, "1");
|
||||
assert.containsN(target, ".item", 4);
|
||||
},
|
||||
onDrop({ element, group, previous, next, parent }) {
|
||||
assert.step("drop");
|
||||
assert.notOk(group);
|
||||
assert.strictEqual(element.innerText, "1");
|
||||
assert.strictEqual(previous.innerText, "2");
|
||||
assert.strictEqual(next.innerText, "3");
|
||||
assert.notOk(parent);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List.template = xml`
|
||||
<div t-ref="root" class="root">
|
||||
<ul class="list">
|
||||
<li t-foreach="[1, 2, 3]" t-as="i" t-key="i" t-esc="i" class="item" />
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
await mount(List, target);
|
||||
|
||||
assert.containsN(target, ".item", 3);
|
||||
assert.verifySteps([]);
|
||||
|
||||
// First item after 2nd item
|
||||
await dragAndDrop(".item:first-child", ".item:nth-child(2)");
|
||||
|
||||
assert.containsN(target, ".item", 3);
|
||||
assert.verifySteps(["start", "elemententer", "stop", "drop"]);
|
||||
});
|
||||
|
||||
QUnit.test("Simple sorting in multiple groups", async (assert) => {
|
||||
assert.expect(20);
|
||||
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
groups: ".list",
|
||||
connectGroups: true,
|
||||
onDragStart({ element, group }) {
|
||||
assert.step("start");
|
||||
assert.hasClass(group, "list2");
|
||||
assert.strictEqual(element.innerText, "2 1");
|
||||
},
|
||||
onGroupEnter({ group }) {
|
||||
assert.step("groupenter");
|
||||
assert.hasClass(group, "list1");
|
||||
},
|
||||
onDragEnd({ element, group }) {
|
||||
assert.step("stop");
|
||||
assert.hasClass(group, "list2");
|
||||
assert.strictEqual(element.innerText, "2 1");
|
||||
},
|
||||
onDrop({ element, group, previous, next, parent }) {
|
||||
assert.step("drop");
|
||||
assert.hasClass(group, "list2");
|
||||
assert.strictEqual(element.innerText, "2 1");
|
||||
assert.strictEqual(previous.innerText, "1 3");
|
||||
assert.notOk(next);
|
||||
assert.hasClass(parent, "list1");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List.template = xml`
|
||||
<div t-ref="root" class="root">
|
||||
<ul t-foreach="[1, 2, 3]" t-as="l" t-key="l" t-attf-class="list p-3 list{{ l }}">
|
||||
<li t-foreach="[1, 2, 3]" t-as="i" t-key="i" t-esc="l + ' ' + i" class="item" />
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
await mount(List, target);
|
||||
|
||||
assert.containsN(target, ".list", 3);
|
||||
assert.containsN(target, ".item", 9);
|
||||
assert.verifySteps([]);
|
||||
|
||||
// First item of 2nd list appended to first list
|
||||
await dragAndDrop(".list2 .item:first-child", ".list1");
|
||||
|
||||
assert.containsN(target, ".list", 3);
|
||||
assert.containsN(target, ".item", 9);
|
||||
assert.verifySteps(["start", "groupenter", "stop", "drop"]);
|
||||
});
|
||||
|
||||
QUnit.test("Sorting in groups with distinct per-axis scrolling", async (assert) => {
|
||||
const nextAnimationFrame = async (timeDelta) => {
|
||||
timeStamp += timeDelta;
|
||||
animationFrameDef.resolve();
|
||||
animationFrameDef = makeDeferred();
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
let animationFrameDef = makeDeferred();
|
||||
let timeStamp = 0;
|
||||
let handlers = new Set();
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
async requestAnimationFrame(handler) {
|
||||
await animationFrameDef;
|
||||
// Prevent setRecurringAnimationFrame from being recursive
|
||||
// for better test control (only the first iteration/movement
|
||||
// is needed to check that the scrolling works).
|
||||
if (!handlers.has(handler)) {
|
||||
handler(timeStamp);
|
||||
handlers.add(handler);
|
||||
}
|
||||
},
|
||||
performance: { now: () => timeStamp },
|
||||
});
|
||||
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
groups: ".list",
|
||||
connectGroups: true,
|
||||
edgeScrolling: { speed: 16, threshold: 25 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List.template = xml`
|
||||
<div class="scroll_parent_y" style="max-width: 150px; max-height: 200px; overflow-y: scroll; overflow-x: hidden;">
|
||||
<div class="spacer_before" style="min-height: 50px;"></div>
|
||||
<div class="spacer_horizontal" style="min-height: 50px;"></div>
|
||||
<div t-ref="root" class="root d-flex align-items-end" style="overflow-x: scroll;">
|
||||
<div class="d-flex">
|
||||
<div style="padding-left: 20px;"
|
||||
t-foreach="[1, 2, 3]" t-as="c" t-key="c" t-attf-class="list m-0 list{{ c }}">
|
||||
<div style="min-width: 50px; min-height: 50px; padding-top: 20px;"
|
||||
t-foreach="[1, 2, 3]" t-as="l" t-key="l" t-esc="'item' + l + '' + c" t-attf-class="item item{{ l + '' + c }}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer_after" style="min-height: 150px;"></div>
|
||||
</div>
|
||||
`;
|
||||
await mount(List, target);
|
||||
|
||||
assert.containsN(target, ".list", 3);
|
||||
assert.containsN(target, ".item", 9);
|
||||
|
||||
const scrollParentX = target.querySelector(".root");
|
||||
const scrollParentY = target.querySelector(".scroll_parent_y");
|
||||
const assertScrolling = (top, left) => {
|
||||
assert.strictEqual(scrollParentY.scrollTop, top);
|
||||
assert.strictEqual(scrollParentX.scrollLeft, left);
|
||||
}
|
||||
const cancelDrag = async () => {
|
||||
triggerHotkey("Escape");
|
||||
await nextTick();
|
||||
scrollParentY.scrollTop = 0;
|
||||
scrollParentX.scrollLeft = 0;
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_dragged");
|
||||
}
|
||||
assert.containsNone(target, ".o_dragged");
|
||||
|
||||
// Negative horizontal scrolling.
|
||||
target.querySelector(".spacer_horizontal").scrollIntoView();
|
||||
scrollParentX.scrollLeft = 16;
|
||||
await nextTick();
|
||||
assertScrolling(50, 16);
|
||||
await drag(".item12", ".item11", "left");
|
||||
await nextAnimationFrame(16);
|
||||
assertScrolling(50, 0);
|
||||
await cancelDrag();
|
||||
|
||||
// Positive horizontal scrolling.
|
||||
target.querySelector(".spacer_horizontal").scrollIntoView();
|
||||
await nextTick();
|
||||
assertScrolling(50, 0);
|
||||
await drag(".item11", ".item12", "right");
|
||||
await nextAnimationFrame(16);
|
||||
assertScrolling(50, 16);
|
||||
await cancelDrag();
|
||||
|
||||
// Negative vertical scrolling.
|
||||
target.querySelector(".root").scrollIntoView();
|
||||
await nextTick();
|
||||
assertScrolling(100, 0);
|
||||
await drag(".item11", ".item11", "top");
|
||||
await nextAnimationFrame(16);
|
||||
assertScrolling(84, 0);
|
||||
await cancelDrag();
|
||||
|
||||
// Positive vertical scrolling.
|
||||
target.querySelector(".spacer_before").scrollIntoView();
|
||||
await nextTick();
|
||||
assertScrolling(0, 0);
|
||||
await drag(".item21", ".item21", "bottom");
|
||||
await nextAnimationFrame(16);
|
||||
assertScrolling(16, 0);
|
||||
await cancelDrag();
|
||||
});
|
||||
|
||||
QUnit.test("Dynamically disable sortable feature", async (assert) => {
|
||||
assert.expect(4);
|
||||
|
||||
const state = reactive({ enableSortable: true });
|
||||
class List extends Component {
|
||||
setup() {
|
||||
this.state = useState(state);
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
enable: () => this.state.enableSortable,
|
||||
onDragStart() {
|
||||
assert.step("start");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List.template = xml`
|
||||
<div t-ref="root" class="root">
|
||||
<ul class="list">
|
||||
<li t-foreach="[1, 2, 3]" t-as="i" t-key="i" t-esc="i" class="item" />
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
await mount(List, target);
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
// First item before last item
|
||||
await dragAndDrop(".item:first-child", ".item:last-child");
|
||||
|
||||
// Drag should have occurred
|
||||
assert.verifySteps(["start"]);
|
||||
|
||||
state.enableSortable = false;
|
||||
await nextTick();
|
||||
|
||||
// First item before last item
|
||||
await dragAndDrop(".item:first-child", ".item:last-child");
|
||||
|
||||
// Drag shouldn't have occurred
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("Disabled in small environment", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
onDragStart() {
|
||||
throw new Error("Shouldn't start the sortable feature.");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List.template = xml`
|
||||
<div t-ref="root" class="root">
|
||||
<ul class="list">
|
||||
<li t-foreach="[1, 2, 3]" t-as="i" t-key="i" t-esc="i" class="item" />
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
await mount(List, target, { env: { isSmall: true } });
|
||||
|
||||
assert.containsN(target, ".item", 3);
|
||||
|
||||
// First item after 2nd item
|
||||
await dragAndDrop(".item:first-child", ".item:nth-child(2)");
|
||||
|
||||
assert.ok(true, "No drag sequence should have been initiated");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"Drag has a default tolerance of 10 pixels before initiating the dragging",
|
||||
async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
onDragStart() {
|
||||
assert.step("Initation of the drag sequence");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List.template = xml`
|
||||
<div t-ref="root" class="root">
|
||||
<ul class="list">
|
||||
<li t-foreach="[1, 2, 3]" t-as="i" t-key="i" t-esc="i" class="item" />
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
await mount(List, target);
|
||||
|
||||
// Move the element from only 5 pixels
|
||||
await dragAndDrop(".item:first-child", ".item:first-child", { x: 5, y: 5 });
|
||||
assert.verifySteps([], "No drag sequence should have been initiated");
|
||||
|
||||
// Move the element from more than 10 pixels
|
||||
await dragAndDrop(".item:first-child", ".item:first-child", { x: 10, y: 10 });
|
||||
assert.verifySteps(
|
||||
["Initation of the drag sequence"],
|
||||
"A drag sequence should have been initiated"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("Ignore specified elements", async (assert) => {
|
||||
assert.expect(6);
|
||||
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
ignore: ".ignored",
|
||||
onDragStart() {
|
||||
assert.step("drag");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List.template = xml`
|
||||
<div t-ref="root" class="root">
|
||||
<ul class="list">
|
||||
<li t-foreach="[1, 2, 3]" t-as="i" t-key="i" class="item">
|
||||
<span class="ignored" t-esc="i" />
|
||||
<span class="not-ignored" t-esc="i" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
await mount(List, target);
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
// Drag root item element
|
||||
await dragAndDrop(".item:first-child", ".item:nth-child(2)");
|
||||
|
||||
assert.verifySteps(["drag"]);
|
||||
|
||||
// Drag ignored element
|
||||
await dragAndDrop(".item:first-child .not-ignored", ".item:nth-child(2)");
|
||||
|
||||
assert.verifySteps(["drag"]);
|
||||
|
||||
// Drag non-ignored element
|
||||
await dragAndDrop(".item:first-child .ignored", ".item:nth-child(2)");
|
||||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { getDataURLFromFile, getOrigin, url } from "@web/core/utils/urls";
|
||||
import { patchWithCleanup } from "../../helpers/utils";
|
||||
|
||||
QUnit.module("URLS", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
patchWithCleanup(browser, {
|
||||
location: { protocol: "http:", host: "testhost" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("urls.getOrigin", (assert) => {
|
||||
assert.strictEqual(getOrigin(), "http://testhost");
|
||||
assert.strictEqual(getOrigin("protocol://host"), "protocol://host");
|
||||
});
|
||||
|
||||
QUnit.test("can return current origin", (assert) => {
|
||||
patchWithCleanup(browser, {
|
||||
location: { protocol: "testprotocol:", host: "testhost" },
|
||||
});
|
||||
|
||||
const testUrl = url();
|
||||
assert.strictEqual(testUrl, "testprotocol://testhost");
|
||||
});
|
||||
|
||||
QUnit.test("can return custom origin", (assert) => {
|
||||
const testUrl = url(null, null, { origin: "customProtocol://customHost/" });
|
||||
assert.strictEqual(testUrl, "customProtocol://customHost");
|
||||
});
|
||||
|
||||
QUnit.test("can return custom origin with route", (assert) => {
|
||||
const testUrl = url("/my_route", null, { origin: "customProtocol://customHost/" });
|
||||
assert.strictEqual(testUrl, "customProtocol://customHost/my_route");
|
||||
});
|
||||
|
||||
QUnit.test("can return full route", (assert) => {
|
||||
const testUrl = url("/my_route");
|
||||
assert.strictEqual(testUrl, "http://testhost/my_route");
|
||||
});
|
||||
|
||||
QUnit.test("can return full route with params", (assert) => {
|
||||
const testUrl = url("/my_route", {
|
||||
my_param: [1, 2],
|
||||
other: 9,
|
||||
});
|
||||
assert.strictEqual(testUrl, "http://testhost/my_route?my_param=1%2C2&other=9");
|
||||
});
|
||||
|
||||
QUnit.test("can return cors urls", (assert) => {
|
||||
const testUrl = url("https://cors_server/cors_route/");
|
||||
assert.strictEqual(testUrl, "https://cors_server/cors_route/");
|
||||
});
|
||||
|
||||
QUnit.test("can be used for cors urls", (assert) => {
|
||||
const testUrl = url("https://cors_server/cors_route/", {
|
||||
my_param: [1, 2],
|
||||
});
|
||||
assert.strictEqual(testUrl, "https://cors_server/cors_route/?my_param=1%2C2");
|
||||
});
|
||||
|
||||
QUnit.test("getDataURLFromFile handles empty file", async (assert) => {
|
||||
const emptyFile = new File([""], "empty.txt", { type: "text/plain" });
|
||||
const dataUrl = await getDataURLFromFile(emptyFile);
|
||||
assert.strictEqual(dataUrl, "data:text/plain;base64,", "dataURL for empty file is not proper");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/** @odoo-module **/
|
||||
import { XMLParser } from "@web/core/utils/xml";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("xml");
|
||||
|
||||
QUnit.test("parse error throws an exception", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
const parser = new XMLParser();
|
||||
let XMLToParse = "<invalid'>";
|
||||
try {
|
||||
parser.parseXML(XMLToParse);
|
||||
assert.step("no error");
|
||||
} catch (e) {
|
||||
if (e.message.includes("error occured while parsing")) {
|
||||
assert.step("error");
|
||||
}
|
||||
}
|
||||
|
||||
XMLToParse = "<div><div>Valid</div><div><Invalid</div></div>";
|
||||
try {
|
||||
parser.parseXML(XMLToParse);
|
||||
assert.step("no error");
|
||||
} catch (e) {
|
||||
if (e.message.includes("error occured while parsing")) {
|
||||
assert.step("error");
|
||||
}
|
||||
}
|
||||
|
||||
assert.verifySteps(["error", "error"]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue