Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

View file

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

View file

@ -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!
});

View file

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

View file

@ -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

View file

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

View file

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

View file

@ -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 });
});
});

View file

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

View file

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

View file

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

View file

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

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

View file

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

View 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/);
});
});

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -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);
});

View file

@ -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."
);
});
});

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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"),
"௧௦/௧௨/௨௦௨௧ ௧௨::"
);
});

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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' } });
});

View file

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

View file

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

View file

@ -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([]);
});

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

View file

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

View file

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

View file

@ -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

View file

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

View file

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

View file

@ -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 },
],
},
],
});
});
});

View file

@ -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));
});
});

View file

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

View file

@ -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));
});
});

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

View file

@ -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([]);
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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([() => {}], [() => {}]));
});
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: () => {} }));
});
});

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -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([]);
});
});

View file

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

View file

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