mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 00:52:07 +02:00
vanilla 17.0
This commit is contained in:
parent
d72e748793
commit
a9bcec8e91
1986 changed files with 1613876 additions and 568976 deletions
|
|
@ -119,6 +119,49 @@ QUnit.module("Components", (hooks) => {
|
|||
assert.verifySteps(["Hello"]);
|
||||
});
|
||||
|
||||
QUnit.test("autocomplete with resetOnSelect='true'", 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`
|
||||
<div>
|
||||
<div class= "test_value" t-esc="state.value"/>
|
||||
<AutoComplete
|
||||
value="''"
|
||||
sources="sources"
|
||||
onSelect="(option) => this.onSelect(option)"
|
||||
resetOnSelect="true"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.strictEqual(target.querySelector(".test_value").textContent, "Hello");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "");
|
||||
|
||||
await editInput(target, ".o-autocomplete--input", "Blip");
|
||||
await click(target.querySelectorAll(".o-autocomplete--dropdown-item")[1]);
|
||||
assert.strictEqual(target.querySelector(".test_value").textContent, "Hello");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "");
|
||||
assert.verifySteps(["Hello"]);
|
||||
});
|
||||
|
||||
QUnit.test("open dropdown on input", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { AutoComplete };
|
||||
|
|
@ -162,6 +205,19 @@ QUnit.module("Components", (hooks) => {
|
|||
assert.strictEqual(target.querySelector(".o-autocomplete--input").value, "Hello");
|
||||
});
|
||||
|
||||
QUnit.test("select input text on first focus", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<AutoComplete value="'Bar'" sources="[{ options: [{ label: 'Bar' }] }]" onSelect="() => {}"/>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
await triggerEvents(target, ".o-autocomplete--input", ["focus", "click"]);
|
||||
const el = target.querySelector(".o-autocomplete--input");
|
||||
assert.strictEqual(el.value.substring(el.selectionStart, el.selectionEnd), "Bar");
|
||||
});
|
||||
|
||||
QUnit.test("scroll outside should cancel result", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { AutoComplete };
|
||||
|
|
@ -380,6 +436,102 @@ QUnit.module("Components", (hooks) => {
|
|||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
});
|
||||
|
||||
QUnit.test("autofocus=true option work as expected", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<AutoComplete value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
autofocus="true"
|
||||
onSelect="() => {}"
|
||||
/>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete input"), document.activeElement);
|
||||
});
|
||||
|
||||
QUnit.test("autocomplete in edition keep edited value before select option", async (assert) => {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ value: "Hello" });
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.state.value = "My Click";
|
||||
}
|
||||
|
||||
onSelect() {
|
||||
this.state.value = "My Selection";
|
||||
}
|
||||
}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<button class="myButton" t-on-click="onClick" />
|
||||
<AutoComplete value="this.state.value"
|
||||
sources="[{ options: [{ label: 'My Selection' }] }]"
|
||||
onSelect.bind="onSelect"
|
||||
/>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
const input = target.querySelector(".o-autocomplete input");
|
||||
input.value = "Yolo";
|
||||
await triggerEvent(input, null, "input");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "Yolo");
|
||||
|
||||
await click(target, ".myButton");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "Yolo");
|
||||
|
||||
// Leave inEdition mode when selecting an option
|
||||
await click(target.querySelector(".o-autocomplete input"));
|
||||
await click(target.querySelectorAll(".o-autocomplete--dropdown-item")[0]);
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "My Selection");
|
||||
|
||||
await click(target, ".myButton");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "My Click");
|
||||
});
|
||||
|
||||
QUnit.test("autocomplete in edition keep edited value before blur", async (assert) => {
|
||||
let count = 0;
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ value: "Hello" });
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.state.value = `My Click ${count++}`;
|
||||
}
|
||||
}
|
||||
Parent.components = { AutoComplete };
|
||||
Parent.template = xml`
|
||||
<button class="myButton" t-on-click="onClick" />
|
||||
<AutoComplete value="this.state.value"
|
||||
sources="[]"
|
||||
onSelect="() => {}"
|
||||
/>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
let input = target.querySelector(".o-autocomplete input");
|
||||
input.value = "";
|
||||
await triggerEvent(input, null, "input");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "");
|
||||
|
||||
await click(target, ".myButton");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "");
|
||||
|
||||
// Leave inEdition mode when blur the input
|
||||
input = target.querySelector(".o-autocomplete input");
|
||||
await triggerEvent(input, null, "blur");
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "");
|
||||
|
||||
await click(target, ".myButton");
|
||||
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "My Click 1");
|
||||
});
|
||||
|
||||
QUnit.test("correct sequence of blur, focus and select [REQUIRE FOCUS]", async (assert) => {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
|
|
@ -469,7 +621,11 @@ QUnit.module("Components", (hooks) => {
|
|||
assert.strictEqual(mousedownEvent.defaultPrevented, false);
|
||||
await triggerEvent(input, "", "change");
|
||||
await triggerEvent(input, "", "blur");
|
||||
await click(target.querySelectorAll(".o-autocomplete--dropdown-item")[1], "");
|
||||
await triggerEvent(
|
||||
target.querySelectorAll(".o-autocomplete--dropdown-item")[1],
|
||||
"",
|
||||
"click"
|
||||
);
|
||||
assert.verifySteps(["change", "select Hello"]);
|
||||
assert.strictEqual(input, document.activeElement);
|
||||
|
||||
|
|
@ -479,8 +635,8 @@ QUnit.module("Components", (hooks) => {
|
|||
await triggerEvent(input, "", "input");
|
||||
await triggerEvent(target, "", "pointerdown");
|
||||
await triggerEvent(input, "", "change");
|
||||
input.blur();
|
||||
await click(target, "");
|
||||
await triggerEvent(input, "", "blur");
|
||||
await triggerEvent(target, "", "click");
|
||||
assert.verifySteps(["change", "blur"]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Cache } from "@web/core/utils/cache";
|
||||
import { makeDeferred, nextTick } from "../helpers/utils";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("cache");
|
||||
|
||||
QUnit.test("do not call getValue if already cached", async (assert) => {
|
||||
const cache = new Cache((key) => {
|
||||
assert.step(key);
|
||||
return key.toUpperCase();
|
||||
});
|
||||
|
||||
assert.strictEqual(cache.read("a"), "A");
|
||||
assert.strictEqual(cache.read("b"), "B");
|
||||
assert.strictEqual(cache.read("a"), "A");
|
||||
|
||||
assert.verifySteps(["a", "b"]);
|
||||
});
|
||||
|
||||
QUnit.test("multiple cache key", async (assert) => {
|
||||
const cache = new Cache((...keys) => assert.step(keys.join("-")));
|
||||
|
||||
cache.read("a", 1);
|
||||
cache.read("a", 2);
|
||||
cache.read("a", 1);
|
||||
|
||||
assert.verifySteps(["a-1", "a-2"]);
|
||||
});
|
||||
|
||||
QUnit.test("compute key", async (assert) => {
|
||||
const cache = new Cache(
|
||||
(key) => assert.step(key),
|
||||
(key) => key.toLowerCase()
|
||||
);
|
||||
|
||||
cache.read("a");
|
||||
cache.read("A");
|
||||
|
||||
assert.verifySteps(["a"]);
|
||||
});
|
||||
|
||||
QUnit.test("cache promise", async (assert) => {
|
||||
const cache = new Cache((key) => {
|
||||
assert.step(`read ${key}`);
|
||||
return makeDeferred();
|
||||
});
|
||||
|
||||
cache.read("a").then((k) => assert.step(`then ${k}`));
|
||||
cache.read("b").then((k) => assert.step(`then ${k}`));
|
||||
cache.read("a").then((k) => assert.step(`then ${k}`));
|
||||
cache.read("a").resolve("a");
|
||||
cache.read("b").resolve("b");
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps(["read a", "read b", "then a", "then a", "then b"]);
|
||||
});
|
||||
|
||||
QUnit.test("clear cache", async (assert) => {
|
||||
const cache = new Cache((key) => assert.step(key));
|
||||
|
||||
cache.read("a");
|
||||
cache.read("b");
|
||||
assert.verifySteps(["a", "b"]);
|
||||
|
||||
cache.read("a");
|
||||
cache.read("b");
|
||||
assert.verifySteps([]);
|
||||
|
||||
cache.clear("a");
|
||||
cache.read("a");
|
||||
cache.read("b");
|
||||
assert.verifySteps(["a"]);
|
||||
|
||||
cache.clear();
|
||||
cache.read("a");
|
||||
cache.read("b");
|
||||
assert.verifySteps([]);
|
||||
|
||||
cache.invalidate();
|
||||
cache.read("a");
|
||||
cache.read("b");
|
||||
assert.verifySteps(["a", "b"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -69,22 +69,16 @@ QUnit.module("Components", (hooks) => {
|
|||
assert.strictEqual(value, false);
|
||||
});
|
||||
|
||||
QUnit.test("does not call onChange prop when disabled", async (assert) => {
|
||||
QUnit.test("checkbox with props disabled", async (assert) => {
|
||||
const env = await makeTestEnv();
|
||||
|
||||
let onChangeCalled = false;
|
||||
class Parent extends Component {
|
||||
onChange(checked) {
|
||||
onChangeCalled = true;
|
||||
}
|
||||
}
|
||||
class Parent extends Component {}
|
||||
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);
|
||||
assert.ok(target.querySelector(".o-checkbox input").disabled);
|
||||
});
|
||||
|
||||
QUnit.test("can toggle value by pressing ENTER", async (assert) => {
|
||||
|
|
@ -134,7 +128,7 @@ QUnit.module("Components", (hooks) => {
|
|||
|
||||
// Click on label
|
||||
assert.verifySteps([]);
|
||||
await click(target, ".o-checkbox > .form-check-label", true);
|
||||
await click(target, ".o-checkbox > .form-check-label", { skipVisibilityCheck: true });
|
||||
assert.notOk(target.querySelector(".o-checkbox input").checked);
|
||||
assert.verifySteps(["false"]);
|
||||
|
||||
|
|
@ -160,18 +154,11 @@ QUnit.module("Components", (hooks) => {
|
|||
// 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 }
|
||||
);
|
||||
const event = await triggerEvent(document.activeElement, null, "keydown", { key: "Space" });
|
||||
assert.ok(!event.defaultPrevented);
|
||||
target.querySelector(".o-checkbox input").checked = true;
|
||||
assert.verifySteps([]);
|
||||
triggerEvent(target, ".o-checkbox input", "change", {}, { fast: true });
|
||||
await nextTick();
|
||||
await triggerEvent(target, ".o-checkbox input", "change");
|
||||
assert.ok(target.querySelector(".o-checkbox input").checked);
|
||||
assert.verifySteps(["true"]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,300 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import {
|
||||
getFixture,
|
||||
patchWithCleanup,
|
||||
mount,
|
||||
nextTick,
|
||||
editInput,
|
||||
triggerEvents,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
|
||||
import { Component, markup, useState, xml } from "@odoo/owl";
|
||||
import { CodeEditor } from "@web/core/code_editor/code_editor";
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
|
||||
QUnit.module("Web Components", (hooks) => {
|
||||
QUnit.module("Code Editor");
|
||||
|
||||
let env;
|
||||
let target;
|
||||
|
||||
hooks.beforeEach(async () => {
|
||||
env = await makeTestEnv();
|
||||
target = getFixture();
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => Promise.resolve().then(fn),
|
||||
});
|
||||
});
|
||||
|
||||
function getDomValue() {
|
||||
const lines = [...target.querySelectorAll(".ace_line")];
|
||||
return lines
|
||||
.map((line) => {
|
||||
const spans = [...line.querySelectorAll(":scope > span")];
|
||||
return spans.map((span) => span.textContent).join("");
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
async function edit(value) {
|
||||
const textArea = target.querySelector(".ace_editor textarea");
|
||||
await editInput(textArea, null, value);
|
||||
return null;
|
||||
}
|
||||
|
||||
function getFakeAceEditor() {
|
||||
return {
|
||||
session: {
|
||||
on: () => {},
|
||||
setMode: () => {},
|
||||
setUseWorker: () => {},
|
||||
setOptions: () => {},
|
||||
getValue: () => {},
|
||||
setValue: () => {},
|
||||
},
|
||||
renderer: {
|
||||
setOptions: () => {},
|
||||
$cursorLayer: { element: { style: {} } },
|
||||
},
|
||||
setOptions: () => {},
|
||||
setValue: () => {},
|
||||
getValue: () => "",
|
||||
setTheme: () => {},
|
||||
resize: () => {},
|
||||
destroy: () => {},
|
||||
setSession: () => {},
|
||||
getSession() {
|
||||
return this.session;
|
||||
},
|
||||
on: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
QUnit.test("Can be rendered", async (assert) => {
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`<CodeEditor mode="'xml'" />`;
|
||||
}
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".ace_editor", "Code editor is rendered");
|
||||
});
|
||||
|
||||
QUnit.test("CodeEditor shouldn't accepts markup values", async (assert) => {
|
||||
assert.expectErrors();
|
||||
|
||||
const _console = window.console;
|
||||
window.console = Object.assign(Object.create(_console), {
|
||||
warn(msg) {
|
||||
assert.step(msg);
|
||||
},
|
||||
});
|
||||
registerCleanup(() => {
|
||||
window.console = _console;
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`<CodeEditor value="props.value"/>`;
|
||||
}
|
||||
class GrandParent extends Component {
|
||||
static components = { Parent };
|
||||
static template = xml`<Parent value="state.value"/>`;
|
||||
setup() {
|
||||
this.state = useState({ value: `<div>Some Text</div>` });
|
||||
}
|
||||
}
|
||||
|
||||
const codeEditor = await mount(GrandParent, target, { env });
|
||||
const textMarkup = markup`<div>Some Text</div>`;
|
||||
codeEditor.state.value = textMarkup;
|
||||
await nextTick(); // wait for the errorService to be called
|
||||
assert.verifySteps(["[Owl] Unhandled error. Destroying the root component"]);
|
||||
assert.verifyErrors(["Invalid props for component 'CodeEditor': 'value' is not valid"]);
|
||||
});
|
||||
|
||||
QUnit.test("onChange props called when code is edited", async (assert) => {
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`<CodeEditor onChange.bind="onChange" />`;
|
||||
onChange(value) {
|
||||
assert.step(value);
|
||||
}
|
||||
}
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
await edit("Some Text");
|
||||
assert.verifySteps(["Some Text"], "Value properly given to onChange");
|
||||
});
|
||||
|
||||
QUnit.test("onChange props not called when value props is updated", async (assert) => {
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`<CodeEditor value="state.value" onChange.bind="onChange" />`;
|
||||
state = useState({ value: "initial value" });
|
||||
onChange(value) {
|
||||
assert.step(value || "__emptystring__");
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mount(Parent, target, { env });
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".ace_line").textContent, "initial value");
|
||||
|
||||
parent.state.value = "new value";
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".ace_line").textContent, "new value");
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("Default value correctly set and updates", async (assert) => {
|
||||
const textA = "<div>\n<p>A Paragraph</p>\n</div>";
|
||||
const textB = "<div>\n<p>An Other Paragraph</p>\n</div>";
|
||||
const textC = "<div>\n<p>A Paragraph</p>\n</div>\n<p>And More</p>";
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`
|
||||
<CodeEditor
|
||||
mode="'xml'"
|
||||
value="state.value"
|
||||
onChange.bind="onChange"
|
||||
maxLines="200"
|
||||
/>
|
||||
`;
|
||||
setup() {
|
||||
this.state = useState({ value: textA });
|
||||
}
|
||||
onChange(value) {
|
||||
// Changing the value of the textarea manualy triggers an Ace "remove" event
|
||||
// of the whole text (the value is thus empty), then an "add" event with the
|
||||
// actual value, this isn't ideal but we ignore the remove.
|
||||
if (value.length <= 0) {
|
||||
return;
|
||||
}
|
||||
assert.step(value);
|
||||
}
|
||||
changeValue(newValue) {
|
||||
this.state.value = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
const codeEditor = await mount(Parent, target, { env });
|
||||
await nextTick();
|
||||
assert.equal(getDomValue(), textA, "Default value correctly set");
|
||||
const aceEditor = window.ace.edit(target.querySelector(".ace_editor"));
|
||||
aceEditor.selectAll();
|
||||
await edit(textB);
|
||||
assert.equal(
|
||||
getDomValue(),
|
||||
textB,
|
||||
"When the textarea is updated the value is correctly changed in the dom"
|
||||
);
|
||||
|
||||
codeEditor.changeValue(textC);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.equal(
|
||||
getDomValue(),
|
||||
textC,
|
||||
"When the props is updated the value is correctly changed in the dom"
|
||||
);
|
||||
|
||||
assert.verifySteps([textB], "Changes properly given to onChange");
|
||||
});
|
||||
|
||||
QUnit.test("Mode props update imports the mode", async (assert) => {
|
||||
const fakeAceEditor = getFakeAceEditor();
|
||||
fakeAceEditor.session.setMode = (mode) => {
|
||||
assert.step(mode);
|
||||
};
|
||||
|
||||
patchWithCleanup(window.ace, {
|
||||
edit: () => fakeAceEditor,
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`<CodeEditor mode="state.mode" />`;
|
||||
setup() {
|
||||
this.state = useState({ mode: "xml" });
|
||||
}
|
||||
setMode(newMode) {
|
||||
this.state.mode = newMode;
|
||||
}
|
||||
}
|
||||
|
||||
const codeEditor = await mount(Parent, target, { env });
|
||||
assert.verifySteps(["ace/mode/xml"], "XML mode should be loaded");
|
||||
|
||||
await codeEditor.setMode("js");
|
||||
await nextTick();
|
||||
assert.verifySteps(["ace/mode/js"], "JS mode should be loaded");
|
||||
});
|
||||
|
||||
QUnit.test("Theme props updates imports the theme", async (assert) => {
|
||||
const fakeAceEditor = getFakeAceEditor();
|
||||
fakeAceEditor.setTheme = (theme) => {
|
||||
assert.step(theme ? theme : "default");
|
||||
};
|
||||
|
||||
patchWithCleanup(window.ace, {
|
||||
edit: () => fakeAceEditor,
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`<CodeEditor theme="state.theme" />`;
|
||||
setup() {
|
||||
this.state = useState({ theme: "" });
|
||||
}
|
||||
setTheme(newTheme) {
|
||||
this.state.theme = newTheme;
|
||||
}
|
||||
}
|
||||
|
||||
const codeEditor = await mount(Parent, target, { env });
|
||||
assert.verifySteps(["default"], "Default theme should be loaded");
|
||||
|
||||
await codeEditor.setTheme("monokai");
|
||||
await nextTick();
|
||||
assert.verifySteps(["ace/theme/monokai"], "Monokai theme should be loaded");
|
||||
});
|
||||
|
||||
QUnit.test("initial value cannot be undone", async (assert) => {
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`<CodeEditor mode="'xml'" value="'some value'" />`;
|
||||
}
|
||||
await mount(Parent, target, { env });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".ace_editor", "Code editor is rendered");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".ace_editor .ace_content").textContent,
|
||||
"some value"
|
||||
);
|
||||
const editor = window.ace.edit(target.querySelector(".ace_editor"));
|
||||
const undo = editor.session.$undoManager.undo.bind(editor.session.$undoManager);
|
||||
editor.session.$undoManager.undo = (...args) => {
|
||||
assert.step("ace undo");
|
||||
return undo(...args);
|
||||
};
|
||||
await triggerEvents(target, ".ace_editor textarea.ace_text-input", [
|
||||
["keydown", { key: "Control", keyCode: 17, which: 17 }],
|
||||
["keypress", { key: "Control", ctrlKey: true, keyCode: 17, which: 17 }],
|
||||
["keydown", { key: "z", ctrlKey: true, keyCode: 90, which: 90 }],
|
||||
["keypress", { key: "z", ctrlKey: true, keyCode: 90, which: 90 }],
|
||||
["keyup", { key: "z", ctrlKey: true, keyCode: 90, which: 90 }],
|
||||
["keyup", { key: "Control", keyCode: 17, which: 17 }],
|
||||
]);
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
target.querySelector(".ace_editor .ace_content").textContent,
|
||||
"some value"
|
||||
);
|
||||
assert.verifySteps(["ace undo"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -48,8 +48,8 @@ QUnit.module("Components", () => {
|
|||
const secondBtn = target.querySelectorAll(".o_colorlist button")[1];
|
||||
assert.strictEqual(
|
||||
secondBtn.attributes.title.value,
|
||||
"Fuchsia",
|
||||
"second button color is Fuchsia"
|
||||
"Raspberry",
|
||||
"second button color is Raspberry"
|
||||
);
|
||||
assert.hasClass(
|
||||
secondBtn,
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ class FooterComponent extends Component {}
|
|||
FooterComponent.template = xml`<span>My footer</span>`;
|
||||
|
||||
class TestComponent extends Component {
|
||||
get DialogContainer() {
|
||||
return registry.category("main_components").get("DialogContainer");
|
||||
get OverlayContainer() {
|
||||
return registry.category("main_components").get("OverlayContainer");
|
||||
}
|
||||
}
|
||||
TestComponent.template = xml`
|
||||
<div>
|
||||
<div class="o_dialog_container"/>
|
||||
<t t-component="DialogContainer.Component" t-props="DialogContainer.props" />
|
||||
<t t-component="OverlayContainer.Component" t-props="OverlayContainer.props" />
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { dialogService } from "@web/core/dialog/dialog_service";
|
|||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService, useActiveElement } from "@web/core/ui/ui_service";
|
||||
import testUtils from "web.test_utils";
|
||||
import testUtils from "@web/../tests/legacy/helpers/test_utils";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
|
||||
import {
|
||||
click,
|
||||
|
|
@ -42,13 +42,13 @@ export async function backspaceSearchBar() {
|
|||
}
|
||||
|
||||
class TestComponent extends Component {
|
||||
get DialogContainer() {
|
||||
return registry.category("main_components").get("DialogContainer");
|
||||
get OverlayContainer() {
|
||||
return registry.category("main_components").get("OverlayContainer");
|
||||
}
|
||||
}
|
||||
TestComponent.template = xml`
|
||||
<div>
|
||||
<t t-component="DialogContainer.Component" t-props="DialogContainer.props" />
|
||||
<t t-component="OverlayContainer.Component" t-props="OverlayContainer.props" />
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
@ -236,7 +236,7 @@ QUnit.test("global command with hotkey", async (assert) => {
|
|||
useActiveElement("active");
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<div t-ref="active"></div>`;
|
||||
MyComponent.template = xml`<div t-ref="active"><button/></div>`;
|
||||
await mount(MyComponent, target, { env });
|
||||
|
||||
triggerHotkey("a");
|
||||
|
|
@ -246,6 +246,131 @@ QUnit.test("global command with hotkey", async (assert) => {
|
|||
assert.verifySteps([globalHotkey]);
|
||||
});
|
||||
|
||||
QUnit.test("command with hotkey and isAvailable", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
const hotkey = "a";
|
||||
let isAvailable = false;
|
||||
env.services.command.add("test", () => assert.step(hotkey), {
|
||||
hotkey,
|
||||
isAvailable: () => isAvailable,
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
triggerHotkey("a");
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
isAvailable = true;
|
||||
triggerHotkey("a");
|
||||
await nextTick();
|
||||
assert.verifySteps([hotkey]);
|
||||
});
|
||||
|
||||
QUnit.test("useCommand hook with hotkey and hotkeyOptions", async (assert) => {
|
||||
const allowRepeatKey = "a";
|
||||
const disallowRepeatKey = "b";
|
||||
const defaultBehaviourKey = "c";
|
||||
class MyComponent extends TestComponent {
|
||||
setup() {
|
||||
useCommand("Allow repeat key", () => assert.step(allowRepeatKey), {
|
||||
hotkey: allowRepeatKey,
|
||||
hotkeyOptions: {
|
||||
allowRepeat: true,
|
||||
},
|
||||
});
|
||||
useCommand("Disallow repeat key", () => assert.step(disallowRepeatKey), {
|
||||
hotkey: disallowRepeatKey,
|
||||
hotkeyOptions: {
|
||||
allowRepeat: false,
|
||||
},
|
||||
});
|
||||
useCommand("Default repeat key", () => assert.step(defaultBehaviourKey), {
|
||||
hotkey: defaultBehaviourKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
await mount(MyComponent, target, { env });
|
||||
|
||||
// Dispatch the three keys without repeat:
|
||||
triggerHotkey(allowRepeatKey);
|
||||
triggerHotkey(disallowRepeatKey);
|
||||
triggerHotkey(defaultBehaviourKey);
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([allowRepeatKey, disallowRepeatKey, defaultBehaviourKey]);
|
||||
|
||||
// Dispatch the three keys with repeat:
|
||||
triggerHotkey(allowRepeatKey, false, { repeat: true });
|
||||
triggerHotkey(disallowRepeatKey, false, { repeat: true });
|
||||
triggerHotkey(defaultBehaviourKey, false, { repeat: true });
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([allowRepeatKey]);
|
||||
});
|
||||
|
||||
QUnit.test("useCommand hook with hotkey and isAvailable", async (assert) => {
|
||||
const hotkeys = ["a", "b", "c", "d", "e"];
|
||||
class MyComponent extends TestComponent {
|
||||
setup() {
|
||||
useCommand("Command 1", () => assert.step(hotkeys[0]), {
|
||||
hotkey: hotkeys[0],
|
||||
isAvailable: () => true,
|
||||
hotkeyOptions: {
|
||||
allowRepeat: true,
|
||||
isAvailable: () => true,
|
||||
},
|
||||
});
|
||||
useCommand("Command 2", () => assert.step(hotkeys[1]), {
|
||||
hotkey: hotkeys[1],
|
||||
isAvailable: () => true,
|
||||
hotkeyOptions: {
|
||||
allowRepeat: true,
|
||||
isAvailable: () => false,
|
||||
},
|
||||
});
|
||||
useCommand("Command 3", () => assert.step(hotkeys[2]), {
|
||||
hotkey: hotkeys[2],
|
||||
isAvailable: () => false,
|
||||
hotkeyOptions: {
|
||||
allowRepeat: true,
|
||||
isAvailable: () => true,
|
||||
},
|
||||
});
|
||||
useCommand("Command 4", () => assert.step(hotkeys[3]), {
|
||||
hotkey: hotkeys[3],
|
||||
isAvailable: () => true,
|
||||
hotkeyOptions: {
|
||||
allowRepeat: true,
|
||||
},
|
||||
});
|
||||
useCommand("Command 5", () => assert.step(hotkeys[4]), {
|
||||
hotkey: hotkeys[4],
|
||||
isAvailable: () => false,
|
||||
hotkeyOptions: {
|
||||
allowRepeat: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
await mount(MyComponent, target, { env });
|
||||
|
||||
for (const hotkey of hotkeys) {
|
||||
triggerHotkey(hotkey);
|
||||
}
|
||||
await nextTick();
|
||||
assert.verifySteps(["a", "d"]);
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_command_palette");
|
||||
assert.containsN(target, ".o_command", 3);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
|
||||
["Command 1A", "Command 2B", "Command 4D"]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("open command palette with command config", async (assert) => {
|
||||
const hotkey = "alt+a";
|
||||
const action = () => {};
|
||||
|
|
|
|||
|
|
@ -113,6 +113,8 @@ QUnit.test("opens an app", async (assert) => {
|
|||
triggerHotkey("enter");
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
// empty screen for now, wait for actual action to show up
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".o_menu_brand").textContent, "Contact");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".test_client_action").textContent,
|
||||
|
|
@ -133,6 +135,8 @@ QUnit.test("opens a menu items", async (assert) => {
|
|||
click(target, "#o_command_2");
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
// empty screen for now, wait for actual action to show up
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".o_menu_brand").textContent, "Sales");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".test_client_action").textContent,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,293 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getNodesTextContent, editInput, click, editSelect } from "../helpers/utils";
|
||||
import { getModelFieldSelectorValues } from "./model_field_selector_tests";
|
||||
import { fieldService } from "@web/core/field_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { makeFakeLocalizationService } from "../helpers/mock_services";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { nameService } from "@web/core/name_service";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { datetimePickerService } from "@web/core/datetime/datetimepicker_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
|
||||
export function setupConditionTreeEditorServices() {
|
||||
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("field", fieldService);
|
||||
registry.category("services").add("name", nameService);
|
||||
registry.category("services").add("dialog", dialogService);
|
||||
registry.category("services").add("datetime_picker", datetimePickerService);
|
||||
registry.category("services").add("notification", notificationService);
|
||||
}
|
||||
|
||||
export function makeServerData() {
|
||||
const 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,
|
||||
},
|
||||
date: { string: "Date", type: "date", searchable: true },
|
||||
datetime: { string: "Date Time", type: "datetime", searchable: true },
|
||||
int: { string: "Integer", type: "integer", searchable: true },
|
||||
json_field: { string: "Json Field", type: "json", searchable: true },
|
||||
state: {
|
||||
string: "State",
|
||||
type: "selection",
|
||||
selection: [
|
||||
["abc", "ABC"],
|
||||
["def", "DEF"],
|
||||
["ghi", "GHI"],
|
||||
],
|
||||
},
|
||||
},
|
||||
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" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
return serverData;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export const SELECTORS = {
|
||||
node: ".o_tree_editor_node",
|
||||
row: ".o_tree_editor_row",
|
||||
tree: ".o_tree_editor > .o_tree_editor_node",
|
||||
connector: ".o_tree_editor_connector",
|
||||
condition: ".o_tree_editor_condition",
|
||||
addNewRule: ".o_tree_editor_row > a",
|
||||
buttonAddNewRule: ".o_tree_editor_node_control_panel > button:nth-child(1)",
|
||||
buttonAddBranch: ".o_tree_editor_node_control_panel > button:nth-child(2)",
|
||||
buttonDeleteNode: ".o_tree_editor_node_control_panel > button:nth-child(3)",
|
||||
pathEditor: ".o_tree_editor_condition > .o_tree_editor_editor:nth-child(1)",
|
||||
operatorEditor: ".o_tree_editor_condition > .o_tree_editor_editor:nth-child(2)",
|
||||
valueEditor: ".o_tree_editor_condition > .o_tree_editor_editor:nth-child(3)",
|
||||
editor: ".o_tree_editor_editor",
|
||||
clearNotSupported: ".o_input .fa-times",
|
||||
tag: ".o_input .o_tag",
|
||||
toggleArchive: ".form-switch",
|
||||
complexCondition: ".o_tree_editor_complex_condition",
|
||||
complexConditionInput: ".o_tree_editor_complex_condition input",
|
||||
};
|
||||
|
||||
const CHILD_SELECTOR = ["connector", "condition", "complexCondition"]
|
||||
.map((k) => SELECTORS[k])
|
||||
.join(",");
|
||||
export function getTreeEditorContent(target, options = {}) {
|
||||
const content = [];
|
||||
const nodes = target.querySelectorAll(SELECTORS.node);
|
||||
const mapping = new Map();
|
||||
for (const node of nodes) {
|
||||
const parent = node.parentElement.closest(SELECTORS.node);
|
||||
const level = parent ? mapping.get(parent) + 1 : 0;
|
||||
mapping.set(node, level);
|
||||
const nodeValue = { level };
|
||||
const associatedNode = node.querySelector(CHILD_SELECTOR);
|
||||
const className = associatedNode.className;
|
||||
if (className.includes("connector")) {
|
||||
nodeValue.value = getCurrentConnector(node);
|
||||
} else if (className.includes("complex_condition")) {
|
||||
nodeValue.value = getCurrentComplexCondition(node);
|
||||
} else {
|
||||
nodeValue.value = getCurrentCondition(node);
|
||||
}
|
||||
if (options.node) {
|
||||
nodeValue.node = node;
|
||||
}
|
||||
content.push(nodeValue);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
export function get(target, selector, index = 0) {
|
||||
if (index) {
|
||||
return [...target.querySelectorAll(selector)].at(index);
|
||||
}
|
||||
return target.querySelector(selector);
|
||||
}
|
||||
|
||||
function getValue(target) {
|
||||
if (target) {
|
||||
const el = target.querySelector("input,select,span:not(.o_tag)");
|
||||
switch (el.tagName) {
|
||||
case "INPUT":
|
||||
return el.value;
|
||||
case "SELECT":
|
||||
return el.options[el.selectedIndex].label;
|
||||
case "SPAN":
|
||||
return el.innerText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentPath(target, index = 0) {
|
||||
const pathEditor = get(target, SELECTORS.pathEditor, index);
|
||||
if (pathEditor) {
|
||||
if (pathEditor.querySelector(".o_model_field_selector")) {
|
||||
return getModelFieldSelectorValues(pathEditor).join(" > ");
|
||||
}
|
||||
return pathEditor.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentOperator(target, index = 0) {
|
||||
const operatorEditor = get(target, SELECTORS.operatorEditor, index);
|
||||
return getValue(operatorEditor);
|
||||
}
|
||||
|
||||
export function getCurrentValue(target, index) {
|
||||
const valueEditor = get(target, SELECTORS.valueEditor, index);
|
||||
const value = getValue(valueEditor);
|
||||
if (valueEditor) {
|
||||
const tags = [...valueEditor.querySelectorAll(".o_tag")];
|
||||
if (tags.length) {
|
||||
let text = `${tags.map((t) => t.innerText).join(" ")}`;
|
||||
if (value) {
|
||||
text += ` ${value}`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getOperatorOptions(target, index = 0) {
|
||||
const el = get(target, SELECTORS.operatorEditor, index);
|
||||
if (el) {
|
||||
const select = el.querySelector("select");
|
||||
return [...select.options].map((o) => o.label);
|
||||
}
|
||||
}
|
||||
|
||||
export function getValueOptions(target, index = 0) {
|
||||
const el = get(target, SELECTORS.valueEditor, index);
|
||||
if (el) {
|
||||
const select = el.querySelector("select");
|
||||
return [...select.options].map((o) => o.label);
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentComplexCondition(target, index = 0) {
|
||||
const input = get(target, SELECTORS.complexConditionInput, index);
|
||||
return input?.value;
|
||||
}
|
||||
|
||||
export function getConditionText(target, index = 0) {
|
||||
const condition = get(target, SELECTORS.condition, index);
|
||||
if (condition) {
|
||||
const texts = [];
|
||||
for (const t of getNodesTextContent(condition.childNodes)) {
|
||||
const t2 = t.trim();
|
||||
if (t2) {
|
||||
texts.push(t2);
|
||||
}
|
||||
}
|
||||
return texts.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentCondition(target, index = 0) {
|
||||
const values = [getCurrentPath(target, index), getCurrentOperator(target, index)];
|
||||
const valueEditor = get(target, SELECTORS.valueEditor, index);
|
||||
if (valueEditor) {
|
||||
values.push(getCurrentValue(target, index));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function getCurrentConnector(target, index = 0) {
|
||||
const connector = get(
|
||||
target,
|
||||
`${SELECTORS.connector} .dropdown-toggle, ${SELECTORS.connector} > span:nth-child(2), ${SELECTORS.connector} > span > strong`,
|
||||
index
|
||||
);
|
||||
return connector?.textContent.search("all") >= 0 ? "all" : connector?.textContent;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export function isNotSupportedPath(target, index = 0) {
|
||||
const pathEditor = get(target, SELECTORS.pathEditor, index);
|
||||
return Boolean(pathEditor.querySelector(SELECTORS.clearNotSupported));
|
||||
}
|
||||
|
||||
export function isNotSupportedOperator(target, index = 0) {
|
||||
const operatorEditor = get(target, SELECTORS.operatorEditor, index);
|
||||
return Boolean(operatorEditor.querySelector(SELECTORS.clearNotSupported));
|
||||
}
|
||||
|
||||
export function isNotSupportedValue(target, index = 0) {
|
||||
const valueEditor = get(target, SELECTORS.valueEditor, index);
|
||||
return Boolean(valueEditor.querySelector(SELECTORS.clearNotSupported));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export async function selectOperator(target, operator, index = 0) {
|
||||
const el = get(target, SELECTORS.operatorEditor, index);
|
||||
await editSelect(el, "select", JSON.stringify(operator));
|
||||
}
|
||||
|
||||
export async function selectValue(target, value, index = 0) {
|
||||
const el = get(target, SELECTORS.valueEditor, index);
|
||||
await editSelect(el, "select", JSON.stringify(value));
|
||||
}
|
||||
|
||||
export async function editValue(target, value, index = 0) {
|
||||
const el = get(target, SELECTORS.valueEditor, index);
|
||||
await editInput(el, "input", value);
|
||||
}
|
||||
|
||||
export async function clickOnButtonAddNewRule(target, index = 0) {
|
||||
await click(get(target, SELECTORS.buttonAddNewRule, index));
|
||||
}
|
||||
|
||||
export async function clickOnButtonAddBranch(target, index = 0) {
|
||||
await click(get(target, SELECTORS.buttonAddBranch, index));
|
||||
}
|
||||
|
||||
export async function clickOnButtonDeleteNode(target, index = 0) {
|
||||
await click(get(target, SELECTORS.buttonDeleteNode, index));
|
||||
}
|
||||
|
||||
export async function clearNotSupported(target, index = 0) {
|
||||
await click(get(target, SELECTORS.clearNotSupported, index));
|
||||
}
|
||||
|
||||
export async function addNewRule(target) {
|
||||
await click(target, SELECTORS.addNewRule);
|
||||
}
|
||||
|
||||
export async function toggleArchive(target) {
|
||||
await click(target, SELECTORS.toggleArchive);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -0,0 +1,885 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { evaluateBooleanExpr } from "@web/core/py_js/py";
|
||||
import {
|
||||
complexCondition,
|
||||
condition,
|
||||
connector,
|
||||
domainFromExpression,
|
||||
domainFromTree,
|
||||
expression,
|
||||
Expression,
|
||||
expressionFromDomain,
|
||||
expressionFromTree,
|
||||
treeFromDomain,
|
||||
treeFromExpression,
|
||||
} from "@web/core/tree_editor/condition_tree";
|
||||
|
||||
QUnit.module("condition tree", {});
|
||||
|
||||
QUnit.test("domainFromTree", function (assert) {
|
||||
const toTest = [
|
||||
{
|
||||
tree: condition("foo", "=", false),
|
||||
result: `[("foo", "=", False)]`,
|
||||
},
|
||||
{
|
||||
tree: condition("foo", "=", false, true),
|
||||
result: `["!", ("foo", "=", False)]`,
|
||||
},
|
||||
{
|
||||
tree: condition("foo", "=?", false),
|
||||
result: `[("foo", "=?", False)]`,
|
||||
},
|
||||
{
|
||||
tree: condition("foo", "=?", false, true),
|
||||
result: `["!", ("foo", "=?", False)]`,
|
||||
},
|
||||
{
|
||||
tree: condition("foo", "between", [1, 3]),
|
||||
result: `["&", ("foo", ">=", 1), ("foo", "<=", 3)]`,
|
||||
},
|
||||
{
|
||||
tree: condition("foo", "between", [1, expression("uid")], true),
|
||||
result: `["!", "&", ("foo", ">=", 1), ("foo", "<=", uid)]`,
|
||||
},
|
||||
];
|
||||
for (const { tree, result } of toTest) {
|
||||
assert.strictEqual(domainFromTree(tree), result);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("domainFromTree . treeFromDomain", function (assert) {
|
||||
const toTest = [
|
||||
{
|
||||
domain: `[("foo", "=", False)]`,
|
||||
result: `[("foo", "=", False)]`,
|
||||
},
|
||||
{
|
||||
domain: `[("foo", "=", true)]`,
|
||||
result: `[("foo", "=", True)]`,
|
||||
},
|
||||
{
|
||||
domain: `["!", ("foo", "=", False)]`,
|
||||
result: `[("foo", "!=", False)]`,
|
||||
},
|
||||
{
|
||||
domain: `[("foo", "=?", False)]`,
|
||||
result: `[("foo", "=?", False)]`,
|
||||
},
|
||||
{
|
||||
domain: `["!", ("foo", "=?", False)]`,
|
||||
result: `["!", ("foo", "=?", False)]`,
|
||||
},
|
||||
{
|
||||
domain: `["&", ("foo", ">=", 1), ("foo", "<=", 3)]`,
|
||||
result: `["&", ("foo", ">=", 1), ("foo", "<=", 3)]`,
|
||||
},
|
||||
{
|
||||
domain: `["&", ("foo", ">=", 1), ("foo", "<=", uid)]`,
|
||||
result: `["&", ("foo", ">=", 1), ("foo", "<=", uid)]`,
|
||||
},
|
||||
];
|
||||
for (const { domain, result } of toTest) {
|
||||
assert.deepEqual(domainFromTree(treeFromDomain(domain)), result);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("domainFromExpression", function (assert) {
|
||||
const options = {
|
||||
getFieldDef: (name) => {
|
||||
if (["foo", "bar"].includes(name)) {
|
||||
return {}; // any field
|
||||
}
|
||||
if (name === "foo_ids") {
|
||||
return { type: "many2many" };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
const toTest = [
|
||||
{
|
||||
expression: `not foo`,
|
||||
result: `[("foo", "=", False)]`,
|
||||
},
|
||||
{
|
||||
expression: `foo == False`,
|
||||
result: `[("foo", "=", False)]`,
|
||||
},
|
||||
{
|
||||
expression: `foo`,
|
||||
result: `[("foo", "!=", False)]`,
|
||||
},
|
||||
{
|
||||
expression: `foo == True`,
|
||||
result: `[("foo", "=", True)]`,
|
||||
},
|
||||
{
|
||||
expression: `foo is True`,
|
||||
result: `[(bool(foo is True), "=", 1)]`,
|
||||
},
|
||||
{
|
||||
expression: `not (foo == False)`,
|
||||
result: `[("foo", "!=", False)]`,
|
||||
},
|
||||
{
|
||||
expression: `not (not foo)`,
|
||||
result: `[("foo", "!=", False)]`,
|
||||
},
|
||||
{
|
||||
expression: `foo >= 1 and foo <= 3`,
|
||||
result: `["&", ("foo", ">=", 1), ("foo", "<=", 3)]`,
|
||||
},
|
||||
{
|
||||
expression: `foo >= 1 and foo <= uid`,
|
||||
result: `["&", ("foo", ">=", 1), ("foo", "<=", uid)]`,
|
||||
},
|
||||
{
|
||||
expression: `foo >= 1 if foo else foo <= uid`,
|
||||
result: `["|", "&", ("foo", "!=", False), ("foo", ">=", 1), "&", ("foo", "=", False), ("foo", "<=", uid)]`,
|
||||
},
|
||||
{
|
||||
expression: `context.get('toto')`,
|
||||
result: `[(bool(context.get("toto")), "=", 1)]`,
|
||||
},
|
||||
{
|
||||
expression: `foo >= 1 if context.get('toto') else bar == 42`,
|
||||
result: `["|", "&", (bool(context.get("toto")), "=", 1), ("foo", ">=", 1), "&", (not context.get("toto"), "=", 1), ("bar", "=", 42)]`,
|
||||
},
|
||||
{
|
||||
expression: `not context.get('toto')`,
|
||||
result: `[(not context.get("toto"), "=", 1)]`,
|
||||
},
|
||||
{
|
||||
expression: `True`,
|
||||
result: `[(1, "=", 1)]`,
|
||||
},
|
||||
{
|
||||
expression: `False`,
|
||||
result: `[(0, "=", 1)]`,
|
||||
},
|
||||
{
|
||||
expression: `A`,
|
||||
result: `[(bool(A), "=", 1)]`,
|
||||
},
|
||||
{
|
||||
expression: `foo`,
|
||||
result: `[("foo", "!=", False)]`,
|
||||
},
|
||||
{
|
||||
expression: `not A`,
|
||||
result: `[(not A, "=", 1)]`,
|
||||
},
|
||||
{
|
||||
expression: `not not A`,
|
||||
result: `[(bool(A), "=", 1)]`,
|
||||
},
|
||||
{
|
||||
expression: `y == 2`,
|
||||
result: `[(bool(y == 2), "=", 1)]`,
|
||||
},
|
||||
{
|
||||
expression: `not (y == 2)`,
|
||||
result: `[(bool(y != 2), "=", 1)]`,
|
||||
},
|
||||
{
|
||||
expression: `foo == 2`,
|
||||
result: `[("foo", "=", 2)]`,
|
||||
},
|
||||
{
|
||||
expression: `not (foo == 2)`,
|
||||
result: `[("foo", "!=", 2)]`,
|
||||
},
|
||||
{
|
||||
expression: `2 == foo`,
|
||||
result: `[("foo", "=", 2)]`,
|
||||
},
|
||||
{
|
||||
expression: `not (2 == foo)`,
|
||||
result: `[("foo", "!=", 2)]`,
|
||||
},
|
||||
{
|
||||
expression: `foo < 2`,
|
||||
result: `[("foo", "<", 2)]`,
|
||||
},
|
||||
{
|
||||
expression: `not (foo < 2)`,
|
||||
result: `[("foo", ">=", 2)]`,
|
||||
},
|
||||
{
|
||||
expression: `2 < foo`,
|
||||
result: `[("foo", ">", 2)]`,
|
||||
},
|
||||
{
|
||||
expression: `not (2 < foo)`,
|
||||
result: `[("foo", "<=", 2)]`,
|
||||
},
|
||||
{
|
||||
expression: `not(y == 1)`,
|
||||
result: `[(bool(y != 1), "=", 1)]`,
|
||||
},
|
||||
{
|
||||
expression: `A if B else C`,
|
||||
result: `["|", "&", (bool(B), "=", 1), (bool(A), "=", 1), "&", (not B, "=", 1), (bool(C), "=", 1)]`,
|
||||
},
|
||||
{
|
||||
expression: `not bool(A)`,
|
||||
result: `[(not A, "=", 1)]`,
|
||||
},
|
||||
{
|
||||
expression: `not(A and not B)`,
|
||||
result: `["!", "&", (bool(A), "=", 1), (not B, "=", 1)]`,
|
||||
},
|
||||
{
|
||||
expression: `not (A and not B)`,
|
||||
result: `["|", (not A, "=", 1), (bool(B), "=", 1)]`,
|
||||
extraOptions: { distributeNot: true },
|
||||
},
|
||||
];
|
||||
for (const { expression, result, extraOptions } of toTest) {
|
||||
const o = { ...options, ...extraOptions };
|
||||
assert.deepEqual(domainFromExpression(expression, o), result);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("expressionFromTree", function (assert) {
|
||||
const options = {
|
||||
getFieldDef: (name) => {
|
||||
if (["foo", "bar"].includes(name)) {
|
||||
return {}; // any field
|
||||
}
|
||||
if (["foo_ids", "bar_ids"].includes(name)) {
|
||||
return { type: "many2many" };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
const toTest = [
|
||||
{
|
||||
expressionTree: condition("foo", "=", false),
|
||||
result: `not foo`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo", "=", false, true),
|
||||
result: `foo`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo", "!=", false),
|
||||
result: `foo`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo", "!=", false, true),
|
||||
result: `not foo`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("y", "=", false),
|
||||
result: `not "y"`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo", "between", [1, 3]),
|
||||
result: `foo >= 1 and foo <= 3`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo", "between", [1, expression("uid")], true),
|
||||
result: `not ( foo >= 1 and foo <= uid )`,
|
||||
},
|
||||
{
|
||||
expressionTree: complexCondition("uid"),
|
||||
result: `uid`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo_ids", "in", []),
|
||||
result: `set(foo_ids).intersection([])`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo_ids", "in", [1]),
|
||||
result: `set(foo_ids).intersection([1])`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo_ids", "in", 1),
|
||||
result: `set(foo_ids).intersection([1])`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo", "in", []),
|
||||
result: `foo in []`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition(expression("expr"), "in", []),
|
||||
result: `expr in []`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo", "in", [1]),
|
||||
result: `foo in [1]`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo", "in", 1),
|
||||
result: `foo in [1]`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo", "in", expression("expr")),
|
||||
result: `foo in expr`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo_ids", "in", expression("expr")),
|
||||
result: `set(foo_ids).intersection(expr)`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("y", "in", []),
|
||||
result: `"y" in []`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("y", "in", [1]),
|
||||
result: `"y" in [1]`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("y", "in", 1),
|
||||
result: `"y" in [1]`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo_ids", "not in", []),
|
||||
result: `not set(foo_ids).intersection([])`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo_ids", "not in", [1]),
|
||||
result: `not set(foo_ids).intersection([1])`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo_ids", "not in", 1),
|
||||
result: `not set(foo_ids).intersection([1])`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo", "not in", []),
|
||||
result: `foo not in []`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo", "not in", [1]),
|
||||
result: `foo not in [1]`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("foo", "not in", 1),
|
||||
result: `foo not in [1]`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("y", "not in", []),
|
||||
result: `"y" not in []`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("y", "not in", [1]),
|
||||
result: `"y" not in [1]`,
|
||||
},
|
||||
{
|
||||
expressionTree: condition("y", "not in", 1),
|
||||
result: `"y" not in [1]`,
|
||||
},
|
||||
];
|
||||
for (const { expressionTree, result, extraOptions } of toTest) {
|
||||
const o = { ...options, ...extraOptions };
|
||||
assert.strictEqual(expressionFromTree(expressionTree, o), result);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("treeFromExpression", function (assert) {
|
||||
const options = {
|
||||
getFieldDef: (name) => {
|
||||
if (["foo", "bar"].includes(name)) {
|
||||
return {}; // any field
|
||||
}
|
||||
if (["foo_ids", "bar_ids"].includes(name)) {
|
||||
return { type: "many2many" };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
const toTest = [
|
||||
{
|
||||
expression: `not foo`,
|
||||
result: condition("foo", "not_set", false),
|
||||
},
|
||||
{
|
||||
expression: `foo == False`,
|
||||
result: condition("foo", "not_set", false),
|
||||
},
|
||||
{
|
||||
expression: `foo`,
|
||||
result: condition("foo", "set", false),
|
||||
},
|
||||
{
|
||||
expression: `foo == True`,
|
||||
result: condition("foo", "=", true),
|
||||
},
|
||||
{
|
||||
expression: `foo is True`,
|
||||
result: complexCondition(`foo is True`),
|
||||
},
|
||||
{
|
||||
expression: `not (foo == False)`,
|
||||
result: condition("foo", "set", false),
|
||||
},
|
||||
{
|
||||
expression: `not (not foo)`,
|
||||
result: condition("foo", "set", false),
|
||||
},
|
||||
{
|
||||
expression: `foo >= 1 and foo <= 3`,
|
||||
result: condition("foo", "between", [1, 3]),
|
||||
},
|
||||
{
|
||||
expression: `foo >= 1 and foo <= uid`,
|
||||
result: condition("foo", "between", [1, expression("uid")]),
|
||||
},
|
||||
{
|
||||
expression: `foo >= 1 if bar else foo <= uid`,
|
||||
result: connector("|", [
|
||||
connector("&", [condition("bar", "set", false), condition("foo", ">=", 1)]),
|
||||
connector("&", [
|
||||
condition("bar", "not_set", false),
|
||||
condition("foo", "<=", new Expression("uid")),
|
||||
]),
|
||||
]),
|
||||
},
|
||||
{
|
||||
expression: `context.get('toto')`,
|
||||
result: complexCondition(`context.get("toto")`),
|
||||
},
|
||||
{
|
||||
expression: `not context.get('toto')`,
|
||||
result: complexCondition(`not context.get("toto")`),
|
||||
},
|
||||
{
|
||||
expression: `foo >= 1 if context.get('toto') else bar == 42`,
|
||||
result: connector("|", [
|
||||
connector("&", [
|
||||
complexCondition(`context.get("toto")`),
|
||||
condition("foo", ">=", 1),
|
||||
]),
|
||||
connector("&", [
|
||||
complexCondition(`not context.get("toto")`),
|
||||
condition("bar", "=", 42),
|
||||
]),
|
||||
]),
|
||||
},
|
||||
{
|
||||
expression: `set()`,
|
||||
result: complexCondition(`set()`),
|
||||
},
|
||||
{
|
||||
expression: `set([1, 2])`,
|
||||
result: complexCondition(`set([1, 2])`),
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).intersection([1, 2])`,
|
||||
result: condition("foo_ids", "in", [1, 2]),
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).intersection(set([1, 2]))`,
|
||||
result: condition("foo_ids", "in", [1, 2]),
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).intersection(set((1, 2)))`,
|
||||
result: condition("foo_ids", "in", [1, 2]),
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).intersection("ab")`,
|
||||
result: complexCondition(`set(foo_ids).intersection("ab")`),
|
||||
},
|
||||
{
|
||||
expression: `set([1, 2]).intersection(foo_ids)`,
|
||||
result: condition("foo_ids", "in", [1, 2]),
|
||||
},
|
||||
{
|
||||
expression: `set(set([1, 2])).intersection(foo_ids)`,
|
||||
result: condition("foo_ids", "in", [1, 2]),
|
||||
},
|
||||
{
|
||||
expression: `set((1, 2)).intersection(foo_ids)`,
|
||||
result: condition("foo_ids", "in", [1, 2]),
|
||||
},
|
||||
{
|
||||
expression: `set("ab").intersection(foo_ids)`,
|
||||
result: complexCondition(`set("ab").intersection(foo_ids)`),
|
||||
},
|
||||
{
|
||||
expression: `set([2, 3]).intersection([1, 2])`,
|
||||
result: complexCondition(`set([2, 3]).intersection([1, 2])`),
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).intersection(bar_ids)`,
|
||||
result: complexCondition(`set(foo_ids).intersection(bar_ids)`),
|
||||
},
|
||||
{
|
||||
expression: `set().intersection(foo_ids)`,
|
||||
result: condition(0, "=", 1),
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).intersection()`,
|
||||
result: condition("foo_ids", "set", false),
|
||||
},
|
||||
{
|
||||
expression: `not set().intersection(foo_ids)`,
|
||||
result: condition(1, "=", 1),
|
||||
},
|
||||
{
|
||||
expression: `not set(foo_ids).intersection()`,
|
||||
result: condition("foo_ids", "not_set", false),
|
||||
},
|
||||
{
|
||||
expression: `not set(foo_ids).intersection([1, 2])`,
|
||||
result: condition("foo_ids", "not in", [1, 2]),
|
||||
},
|
||||
{
|
||||
expression: `not set(foo_ids).intersection(set([1, 2]))`,
|
||||
result: condition("foo_ids", "not in", [1, 2]),
|
||||
},
|
||||
{
|
||||
expression: `not set(foo_ids).intersection(set((1, 2)))`,
|
||||
result: condition("foo_ids", "not in", [1, 2]),
|
||||
},
|
||||
{
|
||||
expression: `not set(foo_ids).intersection("ab")`,
|
||||
result: complexCondition(`not set(foo_ids).intersection("ab")`),
|
||||
},
|
||||
{
|
||||
expression: `not set([1, 2]).intersection(foo_ids)`,
|
||||
result: condition("foo_ids", "not in", [1, 2]),
|
||||
},
|
||||
{
|
||||
expression: `not set(set([1, 2])).intersection(foo_ids)`,
|
||||
result: condition("foo_ids", "not in", [1, 2]),
|
||||
},
|
||||
{
|
||||
expression: `not set((1, 2)).intersection(foo_ids)`,
|
||||
result: condition("foo_ids", "not in", [1, 2]),
|
||||
},
|
||||
{
|
||||
expression: `not set("ab").intersection(foo_ids)`,
|
||||
result: complexCondition(`not set("ab").intersection(foo_ids)`),
|
||||
},
|
||||
{
|
||||
expression: `not set([2, 3]).intersection([1, 2])`,
|
||||
result: complexCondition(`not set([2, 3]).intersection([1, 2])`),
|
||||
},
|
||||
{
|
||||
expression: `not set(foo_ids).intersection(bar_ids)`,
|
||||
result: complexCondition(`not set(foo_ids).intersection(bar_ids)`),
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).difference([1, 2])`,
|
||||
result: complexCondition(`set(foo_ids).difference([1, 2])`),
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).union([1, 2])`,
|
||||
result: complexCondition(`set(foo_ids).union([1, 2])`),
|
||||
},
|
||||
{
|
||||
expression: `expr in []`,
|
||||
result: complexCondition(`expr in []`),
|
||||
},
|
||||
];
|
||||
for (const { expression, result, extraOptions } of toTest) {
|
||||
const o = { ...options, ...extraOptions };
|
||||
assert.deepEqual(treeFromExpression(expression, o), result);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("expressionFromTree . treeFromExpression", function (assert) {
|
||||
const options = {
|
||||
getFieldDef: (name) => {
|
||||
if (["foo", "bar"].includes(name)) {
|
||||
return {}; // any field
|
||||
}
|
||||
if (name === "foo_ids") {
|
||||
return { type: "many2many" };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
const toTest = [
|
||||
{
|
||||
expression: `not foo`,
|
||||
result: `not foo`,
|
||||
},
|
||||
{
|
||||
expression: `foo == False`,
|
||||
result: `not foo`,
|
||||
},
|
||||
{
|
||||
expression: `foo == None`,
|
||||
result: `foo == None`,
|
||||
},
|
||||
{
|
||||
expression: `foo is None`,
|
||||
result: `foo is None`,
|
||||
},
|
||||
{
|
||||
expression: `foo`,
|
||||
result: `foo`,
|
||||
},
|
||||
{
|
||||
expression: `foo == True`,
|
||||
result: `foo == True`,
|
||||
},
|
||||
{
|
||||
expression: `foo is True`,
|
||||
result: `foo is True`,
|
||||
},
|
||||
{
|
||||
expression: `not (foo == False)`,
|
||||
result: `foo`,
|
||||
},
|
||||
{
|
||||
expression: `not (not foo)`,
|
||||
result: `foo`,
|
||||
},
|
||||
{
|
||||
expression: `foo >= 1 and foo <= 3`,
|
||||
result: `foo >= 1 and foo <= 3`,
|
||||
},
|
||||
{
|
||||
expression: `foo >= 1 and foo <= uid`,
|
||||
result: `foo >= 1 and foo <= uid`,
|
||||
},
|
||||
{
|
||||
expression: `foo >= 1 if glob else foo <= uid`,
|
||||
result: `foo >= 1 if glob else foo <= uid`,
|
||||
},
|
||||
{
|
||||
expression: `context.get("toto")`,
|
||||
result: `context.get("toto")`,
|
||||
},
|
||||
{
|
||||
expression: `foo >= 1 if context.get("toto") else bar == 42`,
|
||||
result: `foo >= 1 if context.get("toto") else bar == 42`,
|
||||
},
|
||||
{
|
||||
expression: `not context.get("toto")`,
|
||||
result: `not context.get("toto")`,
|
||||
},
|
||||
{
|
||||
expression: `set()`,
|
||||
result: `set()`,
|
||||
},
|
||||
{
|
||||
expression: `set([1, 2])`,
|
||||
result: `set([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).intersection([1, 2])`,
|
||||
result: `set(foo_ids).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).intersection(set([1, 2]))`,
|
||||
result: `set(foo_ids).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).intersection(set((1, 2)))`,
|
||||
result: `set(foo_ids).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).intersection("ab")`,
|
||||
result: `set(foo_ids).intersection("ab")`,
|
||||
},
|
||||
{
|
||||
expression: `set([1, 2]).intersection(foo_ids)`,
|
||||
result: `set(foo_ids).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `set(set([1, 2])).intersection(foo_ids)`,
|
||||
result: `set(foo_ids).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `set((1, 2)).intersection(foo_ids)`,
|
||||
result: `set(foo_ids).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `set("ab").intersection(foo_ids)`,
|
||||
result: `set("ab").intersection(foo_ids)`,
|
||||
},
|
||||
{
|
||||
expression: `set([2, 3]).intersection([1, 2])`,
|
||||
result: `set([2, 3]).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).intersection(bar_ids)`,
|
||||
result: `set(foo_ids).intersection(bar_ids)`,
|
||||
},
|
||||
{
|
||||
expression: `set().intersection(foo_ids)`,
|
||||
result: `False`,
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).intersection()`,
|
||||
result: `foo_ids`,
|
||||
},
|
||||
{
|
||||
expression: `not set(foo_ids).intersection([1, 2])`,
|
||||
result: `not set(foo_ids).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `not set(foo_ids).intersection(set([1, 2]))`,
|
||||
result: `not set(foo_ids).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `not set(foo_ids).intersection(set((1, 2)))`,
|
||||
result: `not set(foo_ids).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `not set(foo_ids).intersection("ab")`,
|
||||
result: `not set(foo_ids).intersection("ab")`,
|
||||
},
|
||||
{
|
||||
expression: `not set([1, 2]).intersection(foo_ids)`,
|
||||
result: `not set(foo_ids).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `not set(set([1, 2])).intersection(foo_ids)`,
|
||||
result: `not set(foo_ids).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `not set((1, 2)).intersection(foo_ids)`,
|
||||
result: `not set(foo_ids).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `not set("ab").intersection(foo_ids)`,
|
||||
result: `not set("ab").intersection(foo_ids)`,
|
||||
},
|
||||
{
|
||||
expression: `not set([2, 3]).intersection([1, 2])`,
|
||||
result: `not set([2, 3]).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `not set(foo_ids).intersection(bar_ids)`,
|
||||
result: `not set(foo_ids).intersection(bar_ids)`,
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).difference([1, 2])`,
|
||||
result: `set(foo_ids).difference([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `set(foo_ids).intersection([1, 2])`,
|
||||
result: `set(foo_ids).intersection([1, 2])`,
|
||||
},
|
||||
{
|
||||
expression: `set([foo]).intersection()`,
|
||||
result: `foo`,
|
||||
},
|
||||
{
|
||||
expression: `set([foo]).intersection([1, 2])`,
|
||||
result: `foo in [1, 2]`,
|
||||
},
|
||||
{
|
||||
expression: `set().intersection([foo])`,
|
||||
result: `False`,
|
||||
},
|
||||
{
|
||||
expression: `set([1, 2]).intersection([foo])`,
|
||||
result: `foo in [1, 2]`,
|
||||
},
|
||||
{
|
||||
expression: `not set([foo]).intersection()`,
|
||||
result: `not foo`,
|
||||
},
|
||||
{
|
||||
expression: `not set([foo]).intersection([1, 2])`,
|
||||
result: `foo not in [1, 2]`,
|
||||
},
|
||||
{
|
||||
expression: `not set().intersection([foo])`,
|
||||
result: `True`,
|
||||
},
|
||||
{
|
||||
expression: `not set([1, 2]).intersection([foo])`,
|
||||
result: `foo not in [1, 2]`,
|
||||
},
|
||||
];
|
||||
for (const { expression, result, extraOptions } of toTest) {
|
||||
const o = { ...options, ...extraOptions };
|
||||
assert.deepEqual(expressionFromTree(treeFromExpression(expression, o), o), result);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("expressionFromDomain", function (assert) {
|
||||
const options = {
|
||||
getFieldDef: (name) => (name === "x" ? {} : null),
|
||||
};
|
||||
const toTest = [
|
||||
{
|
||||
domain: `[(1, "=", 1)]`,
|
||||
result: `True`,
|
||||
},
|
||||
{
|
||||
domain: `[(0, "=", 1)]`,
|
||||
result: `False`,
|
||||
},
|
||||
{
|
||||
domain: `[("A", "=", 1)]`,
|
||||
result: `"A" == 1`,
|
||||
},
|
||||
{
|
||||
domain: `[(bool(A), "=", 1)]`,
|
||||
result: `bool(A)`,
|
||||
},
|
||||
{
|
||||
domain: `[("x", "=", 2)]`,
|
||||
result: `x == 2`,
|
||||
},
|
||||
];
|
||||
|
||||
for (const { domain, result, extraOptions } of toTest) {
|
||||
const o = { ...options, ...extraOptions };
|
||||
assert.deepEqual(expressionFromDomain(domain, o), result);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("evaluation . expressionFromTree = contains . domainFromTree", function (assert) {
|
||||
const options = {
|
||||
getFieldDef: (name) => {
|
||||
if (name === "foo") {
|
||||
return {}; // any field
|
||||
}
|
||||
if (name === "foo_ids") {
|
||||
return { type: "many2many" };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const record = { foo: 1, foo_ids: [1, 2], uid: 7, expr: "abc", expr2: [1] };
|
||||
|
||||
const toTest = [
|
||||
condition("foo", "=", false),
|
||||
condition("foo", "=", false, true),
|
||||
condition("foo", "!=", false),
|
||||
condition("foo", "!=", false, true),
|
||||
condition("y", "=", false),
|
||||
condition("foo", "between", [1, 3]),
|
||||
condition("foo", "between", [1, expression("uid")], true),
|
||||
condition("foo_ids", "in", []),
|
||||
condition("foo_ids", "in", [1]),
|
||||
condition("foo_ids", "in", 1),
|
||||
condition("foo", "in", []),
|
||||
condition(expression("expr"), "in", []),
|
||||
condition("foo", "in", [1]),
|
||||
condition("foo", "in", 1),
|
||||
condition("y", "in", []),
|
||||
condition("y", "in", [1]),
|
||||
condition("y", "in", 1),
|
||||
condition("foo_ids", "not in", []),
|
||||
condition("foo_ids", "not in", [1]),
|
||||
condition("foo_ids", "not in", 1),
|
||||
condition("foo", "not in", []),
|
||||
condition("foo", "not in", [1]),
|
||||
condition("foo", "not in", 1),
|
||||
condition("y", "not in", []),
|
||||
condition("y", "not in", [1]),
|
||||
condition("y", "not in", 1),
|
||||
condition("foo", "in", expression("expr2")),
|
||||
condition("foo_ids", "in", expression("expr2")),
|
||||
];
|
||||
for (const tree of toTest) {
|
||||
assert.strictEqual(
|
||||
evaluateBooleanExpr(expressionFromTree(tree, options), record),
|
||||
new Domain(domainFromTree(tree)).contains(record)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -4,22 +4,22 @@ 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 { makeDialogTestEnv } from "../helpers/mock_env";
|
||||
import {
|
||||
click,
|
||||
destroy,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
mount,
|
||||
nextTick,
|
||||
triggerHotkey,
|
||||
} from "../helpers/utils";
|
||||
import { makeFakeDialogService } from "../helpers/mock_services";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
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();
|
||||
|
|
@ -34,30 +34,129 @@ QUnit.module("Components", (hooks) => {
|
|||
|
||||
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");
|
||||
QUnit.test(
|
||||
"Without dismiss callback 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: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
cancel: () => {
|
||||
assert.step("Cancel 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(
|
||||
"With dismiss callback: 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: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
cancel: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
dismiss: () => {
|
||||
assert.step("Dismiss action");
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
triggerHotkey("escape");
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
["Cancel action", "Close action"],
|
||||
"dialog has called its cancel method before its closure"
|
||||
);
|
||||
});
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
triggerHotkey("escape");
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
["Dismiss action", "Close action"],
|
||||
"dialog has called its dismiss method before its closure"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"Without dismiss callback: clicking on 'X' 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: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
cancel: () => {
|
||||
assert.step("Cancel action");
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
await click(target, ".modal-header .btn-close");
|
||||
assert.verifySteps(
|
||||
["Cancel action", "Close action"],
|
||||
"dialog has called its cancel method before its closure"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"With dismiss callback: clicking on 'X' 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: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
cancel: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
dismiss: () => {
|
||||
assert.step("Dismiss action");
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
await click(target, ".modal-header .btn-close");
|
||||
assert.verifySteps(
|
||||
["Dismiss action", "Close action"],
|
||||
"dialog has called its dismiss method before its closure"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("clicking on 'Ok'", async function (assert) {
|
||||
const env = await makeDialogTestEnv();
|
||||
|
|
@ -75,6 +174,9 @@ QUnit.module("Components", (hooks) => {
|
|||
cancel: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
dismiss: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
|
|
@ -98,6 +200,9 @@ QUnit.module("Components", (hooks) => {
|
|||
cancel: () => {
|
||||
assert.step("Cancel action");
|
||||
},
|
||||
dismiss: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
|
|
@ -202,4 +307,42 @@ QUnit.module("Components", (hooks) => {
|
|||
await nextTick();
|
||||
assert.verifySteps(["close"]);
|
||||
});
|
||||
|
||||
QUnit.test("Focus is correctly restored after confirmation", async function (assert) {
|
||||
const env = await makeDialogTestEnv();
|
||||
|
||||
class MyComp extends Component {}
|
||||
MyComp.template = xml`<div class="my-comp"><input type="text" class="my-input"/></div>`;
|
||||
|
||||
await mount(MyComp, target, { env });
|
||||
target.querySelector(".my-input").focus();
|
||||
assert.strictEqual(document.activeElement, target.querySelector(".my-input"));
|
||||
|
||||
const comp = await mount(ConfirmationDialog, target, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
confirm: () => {},
|
||||
close: () => {},
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector(".modal-footer .btn-primary")
|
||||
);
|
||||
await click(target, ".modal-footer .btn-primary");
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
document.body,
|
||||
"As the button is disabled, the focus is now on the body"
|
||||
);
|
||||
destroy(comp);
|
||||
await Promise.resolve();
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector(".my-input"),
|
||||
"After destruction of the dialog, the focus is restored to the input"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { makeContext } from "@web/core/context";
|
||||
import { evalPartialContext, makeContext } from "@web/core/context";
|
||||
|
||||
QUnit.module("utils", {}, () => {
|
||||
QUnit.module("Context", {}, () => {
|
||||
QUnit.module("makeContext");
|
||||
|
||||
QUnit.test("return empty context", (assert) => {
|
||||
|
|
@ -42,4 +42,38 @@ QUnit.module("utils", {}, () => {
|
|||
assert.deepEqual(makeContext(["{'a': a + 1}"], { a: 1 }), { a: 2 });
|
||||
assert.deepEqual(makeContext(["{'b': a + 1}"], { a: 1 }), { b: 2 });
|
||||
});
|
||||
|
||||
QUnit.module("evalPartialContext");
|
||||
|
||||
QUnit.test("static contexts", (assert) => {
|
||||
assert.deepEqual(evalPartialContext("{}", {}), {});
|
||||
assert.deepEqual(evalPartialContext("{'a': 1}", {}), { a: 1 });
|
||||
assert.deepEqual(evalPartialContext("{'a': 'b'}", {}), { a: "b" });
|
||||
assert.deepEqual(evalPartialContext("{'a': true}", {}), { a: true });
|
||||
assert.deepEqual(evalPartialContext("{'a': None}", {}), { a: null });
|
||||
});
|
||||
|
||||
QUnit.test("complete dynamic contexts", (assert) => {
|
||||
assert.deepEqual(evalPartialContext("{'a': a, 'b': 1}", { a: 2 }), { a: 2, b: 1 });
|
||||
});
|
||||
|
||||
QUnit.test("partial dynamic contexts", (assert) => {
|
||||
assert.deepEqual(evalPartialContext("{'a': a}", {}), {});
|
||||
assert.deepEqual(evalPartialContext("{'a': a, 'b': 1}", {}), { b: 1 });
|
||||
assert.deepEqual(evalPartialContext("{'a': a, 'b': b}", { a: 2 }), { a: 2 });
|
||||
});
|
||||
|
||||
QUnit.test("value of type obj (15)", (assert) => {
|
||||
assert.deepEqual(evalPartialContext("{'a': a.b.c}", {}), {});
|
||||
assert.deepEqual(evalPartialContext("{'a': a.b.c}", { a: {} }), {});
|
||||
assert.deepEqual(evalPartialContext("{'a': a.b.c}", { a: { b: { c: 2 } } }), { a: 2 });
|
||||
});
|
||||
|
||||
QUnit.test("value of type op (14)", (assert) => {
|
||||
assert.deepEqual(evalPartialContext("{'a': a + 1}", {}), {});
|
||||
assert.deepEqual(evalPartialContext("{'a': a + b}", {}), {});
|
||||
assert.deepEqual(evalPartialContext("{'a': a + b}", { a: 2 }), {});
|
||||
assert.deepEqual(evalPartialContext("{'a': a + 1}", { a: 2 }), { a: 3 });
|
||||
assert.deepEqual(evalPartialContext("{'a': a + b}", { a: 2, b: 3 }), { a: 5 });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { defaultLocalization } from "@web/../tests/helpers/mock_services";
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
|
||||
import { currencies, formatCurrency } from "@web/core/currency";
|
||||
import { session } from "@web/session";
|
||||
|
||||
QUnit.module("utils", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
|
||||
});
|
||||
|
||||
QUnit.module("Currency");
|
||||
|
||||
QUnit.test("formatCurrency", function (assert) {
|
||||
patchWithCleanup(currencies, {
|
||||
10: {
|
||||
digits: [69, 2],
|
||||
position: "after",
|
||||
symbol: "€",
|
||||
},
|
||||
11: {
|
||||
digits: [69, 2],
|
||||
position: "before",
|
||||
symbol: "$",
|
||||
},
|
||||
12: {
|
||||
digits: [69, 2],
|
||||
position: "after",
|
||||
symbol: "&",
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(formatCurrency(200), "200.00");
|
||||
|
||||
assert.deepEqual(formatCurrency(1234567.654, 10), "1,234,567.65\u00a0€");
|
||||
assert.deepEqual(formatCurrency(1234567.654, 11), "$\u00a01,234,567.65");
|
||||
assert.deepEqual(formatCurrency(1234567.654, 44), "1,234,567.65");
|
||||
assert.deepEqual(
|
||||
formatCurrency(1234567.654, 10, { noSymbol: true }),
|
||||
"1,234,567.65"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatCurrency(8.0, 10, { humanReadable: true }),
|
||||
"8.00\u00a0€"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatCurrency(1234567.654, 10, { humanReadable: true }),
|
||||
"1.23M\u00a0€"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatCurrency(1990000.001, 10, { humanReadable: true }),
|
||||
"1.99M\u00a0€"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatCurrency(1234567.654, 44, { digits: [69, 1] }),
|
||||
"1,234,567.7"
|
||||
);
|
||||
assert.deepEqual(
|
||||
formatCurrency(1234567.654, 11, { digits: [69, 1] }),
|
||||
"$\u00a01,234,567.7",
|
||||
"options digits should take over currency digits when both are defined"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("formatCurrency without currency", function (assert) {
|
||||
patchWithCleanup(session, {
|
||||
currencies: {},
|
||||
});
|
||||
assert.deepEqual(
|
||||
formatCurrency(1234567.654, 10, { humanReadable: true }),
|
||||
"1.23M"
|
||||
);
|
||||
assert.deepEqual(formatCurrency(1234567.654, 10), "1,234,567.65");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,806 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { applyFilter, toggleMenu } from "@web/../tests/search/helpers";
|
||||
import { DatePicker, DateTimePicker } from "@web/core/datepicker/datepicker";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import ActionModel from "web.ActionModel";
|
||||
import CustomFilterItem from "web.CustomFilterItem";
|
||||
import { createComponent } from "web.test_utils";
|
||||
import { editSelect } from "web.test_utils_fields";
|
||||
import { registerCleanup } from "../helpers/cleanup";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { makeFakeLocalizationService } from "../helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
getFixture,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
} from "../helpers/utils";
|
||||
|
||||
const { DateTime, Settings } = luxon;
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let target;
|
||||
|
||||
/**
|
||||
* @param {typeof DatePicker} Picker
|
||||
* @param {Object} props
|
||||
* @returns {Promise<DatePicker>}
|
||||
*/
|
||||
async function mountPicker(Picker, props) {
|
||||
serviceRegistry
|
||||
.add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({
|
||||
dateFormat: "dd/MM/yyyy",
|
||||
dateTimeFormat: "dd/MM/yyyy HH:mm:ss",
|
||||
})
|
||||
)
|
||||
.add("ui", uiService)
|
||||
.add("hotkey", hotkeyService);
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState(props);
|
||||
}
|
||||
|
||||
onDateChange(date) {
|
||||
if (props.onDateTimeChanged) {
|
||||
props.onDateTimeChanged(date);
|
||||
}
|
||||
this.state.date = date;
|
||||
}
|
||||
}
|
||||
Parent.template = xml/* xml */ `
|
||||
<t t-component="props.Picker" t-props="state" onDateTimeChanged.bind="onDateChange" />
|
||||
`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
return await mount(Parent, target, { env, props: { Picker } });
|
||||
}
|
||||
|
||||
function useFRLocale() {
|
||||
if (!window.moment.locales().includes("fr")) {
|
||||
// Mocks the FR locale if not loaded
|
||||
const originalLocale = window.moment.locale();
|
||||
window.moment.defineLocale("fr", {
|
||||
months: "janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split(
|
||||
"_"
|
||||
),
|
||||
monthsShort: "janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split(
|
||||
"_"
|
||||
),
|
||||
code: "fr",
|
||||
monthsParseExact: true,
|
||||
week: { dow: 1, doy: 4 },
|
||||
});
|
||||
// Moment automatically assigns newly defined locales.
|
||||
window.moment.locale(originalLocale);
|
||||
registerCleanup(() => window.moment.updateLocale("fr", null));
|
||||
}
|
||||
return "fr";
|
||||
}
|
||||
|
||||
var symbolMap = {
|
||||
1: "૧",
|
||||
2: "૨",
|
||||
3: "૩",
|
||||
4: "૪",
|
||||
5: "૫",
|
||||
6: "૬",
|
||||
7: "૭",
|
||||
8: "૮",
|
||||
9: "૯",
|
||||
0: "૦",
|
||||
};
|
||||
var numberMap = {
|
||||
"૧": "1",
|
||||
"૨": "2",
|
||||
"૩": "3",
|
||||
"૪": "4",
|
||||
"૫": "5",
|
||||
"૬": "6",
|
||||
"૭": "7",
|
||||
"૮": "8",
|
||||
"૯": "9",
|
||||
"૦": "0",
|
||||
};
|
||||
|
||||
function useGULocale() {
|
||||
if (!window.moment.locales().includes("gu")) {
|
||||
const originalLocale = window.moment.locale();
|
||||
window.moment.defineLocale("gu", {
|
||||
months: "જાન્યુઆરી_ફેબ્રુઆરી_માર્ચ_એપ્રિલ_મે_જૂન_જુલાઈ_ઑગસ્ટ_સપ્ટેમ્બર_ઑક્ટ્બર_નવેમ્બર_ડિસેમ્બર".split(
|
||||
"_"
|
||||
),
|
||||
monthsShort: "જાન્યુ._ફેબ્રુ._માર્ચ_એપ્રિ._મે_જૂન_જુલા._ઑગ._સપ્ટે._ઑક્ટ્._નવે._ડિસે.".split(
|
||||
"_"
|
||||
),
|
||||
monthsParseExact: true,
|
||||
week: {
|
||||
dow: 0, // Sunday is the first day of the week.
|
||||
doy: 6, // The week that contains Jan 1st is the first week of the year.
|
||||
},
|
||||
preparse: function (string) {
|
||||
return string.replace(/[૧૨૩૪૫૬૭૮૯૦]/g, function (match) {
|
||||
return numberMap[match];
|
||||
});
|
||||
},
|
||||
postformat: function (string) {
|
||||
return string.replace(/\d/g, function (match) {
|
||||
return symbolMap[match];
|
||||
});
|
||||
},
|
||||
});
|
||||
// Moment automatically assigns newly defined locales.
|
||||
window.moment.locale(originalLocale);
|
||||
registerCleanup(() => window.moment.updateLocale("gu", null));
|
||||
}
|
||||
return "gu";
|
||||
}
|
||||
|
||||
function useNOLocale() {
|
||||
if (!window.moment.locales().includes("nb")) {
|
||||
const originalLocale = window.moment.locale();
|
||||
window.moment.defineLocale("nb", {
|
||||
months: "januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split(
|
||||
"_"
|
||||
),
|
||||
monthsShort: "jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.".split("_"),
|
||||
monthsParseExact: true,
|
||||
week: {
|
||||
dow: 1, // Monday is the first day of the week.
|
||||
doy: 4, // The week that contains Jan 4th is the first week of the year.
|
||||
},
|
||||
});
|
||||
// Moment automatically assigns newly defined locales.
|
||||
window.moment.locale(originalLocale);
|
||||
registerCleanup(() => window.moment.updateLocale("nb", null));
|
||||
}
|
||||
return "nb";
|
||||
}
|
||||
|
||||
QUnit.module("Components", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("DatePicker");
|
||||
|
||||
QUnit.test("basic rendering", async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
|
||||
});
|
||||
|
||||
assert.containsOnce(target, "input.o_input.o_datepicker_input");
|
||||
assert.containsOnce(target, "span.o_datepicker_button");
|
||||
assert.containsNone(document.body, "div.bootstrap-datetimepicker-widget");
|
||||
|
||||
const datePicker = target.querySelector(".o_datepicker");
|
||||
const input = datePicker.querySelector("input.o_input.o_datepicker_input");
|
||||
assert.strictEqual(input.value, "09/01/1997", "Value should be the one given");
|
||||
assert.strictEqual(
|
||||
datePicker.dataset.targetInput,
|
||||
`#${datePicker.querySelector("input[type=hidden]").id}`,
|
||||
"DatePicker id should match its input target"
|
||||
);
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.containsOnce(document.body, "div.bootstrap-datetimepicker-widget .datepicker");
|
||||
assert.containsNone(document.body, "div.bootstrap-datetimepicker-widget .timepicker");
|
||||
assert.strictEqual(
|
||||
document.querySelector(".datepicker .day.active").dataset.day,
|
||||
"01/09/1997",
|
||||
"Datepicker should have set the correct day"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"08/02/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
await click(input);
|
||||
await click(document.querySelector(".datepicker th.next")); // next month
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await click(document.querySelectorAll(".datepicker table td")[15]); // previous day
|
||||
|
||||
assert.strictEqual(input.value, "08/02/1997");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date with locale (locale given in props)", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
|
||||
format: "dd MMM, yyyy",
|
||||
locale: useFRLocale(),
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"01/09/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "09 janv., 1997");
|
||||
|
||||
await click(input);
|
||||
await click(document.querySelector(".datepicker .picker-switch")); // month picker
|
||||
await click(document.querySelectorAll(".datepicker .month")[8]); // september
|
||||
await click(document.querySelector(".datepicker .day")); // first day
|
||||
|
||||
assert.strictEqual(input.value, "01 sept., 1997");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date with locale (locale from date props)", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", {
|
||||
zone: "utc",
|
||||
locale: useFRLocale(),
|
||||
}),
|
||||
format: "dd MMM, yyyy",
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"01/09/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "09 janv., 1997");
|
||||
|
||||
await click(input);
|
||||
await click(document.querySelector(".datepicker .picker-switch")); // month picker
|
||||
await click(document.querySelectorAll(".datepicker .month")[8]); // september
|
||||
await click(document.querySelector(".datepicker .day")); // first day
|
||||
|
||||
assert.strictEqual(input.value, "01 sept., 1997");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date with locale (locale with different symbols)", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", {
|
||||
zone: "utc",
|
||||
locale: useGULocale(),
|
||||
}),
|
||||
format: "dd MMM, yyyy",
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"01/09/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "09 જાન્યુ, 1997");
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "09 જાન્યુ, 1997");
|
||||
|
||||
await click(document.querySelector(".datepicker .picker-switch")); // month picker
|
||||
await click(document.querySelectorAll(".datepicker .month")[8]); // september
|
||||
await click(document.querySelectorAll(".datepicker .day")[1]); // first day of september
|
||||
|
||||
assert.strictEqual(input.value, "01 સપ્ટે, 1997");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("enter a date value", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"08/02/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
input.value = "08/02/1997";
|
||||
await triggerEvent(target, ".o_datepicker_input", "change");
|
||||
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(
|
||||
document.querySelector(".datepicker .day.active").dataset.day,
|
||||
"02/08/1997",
|
||||
"Datepicker should have set the correct day"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Date format is correctly set", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
|
||||
format: "yyyy/MM/dd",
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "1997/01/09");
|
||||
|
||||
// Forces an update to assert that the registered format is the correct one
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "1997/01/09");
|
||||
});
|
||||
|
||||
QUnit.test("Validate input date with 'Enter'", async (assert) => {
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
|
||||
format: "dd/MM/yyyy",
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "09/01/1997");
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
|
||||
input.value = "23/03/2022";
|
||||
await triggerEvent(input, null, "keydown", { key: "Enter" });
|
||||
|
||||
assert.strictEqual(input.value, "23/03/2022");
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
});
|
||||
|
||||
QUnit.test("Validate input date with 'Escape'", async (assert) => {
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", { zone: "utc" }),
|
||||
format: "dd/MM/yyyy",
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "09/01/1997");
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
|
||||
input.value = "23/03/2022";
|
||||
await triggerEvent(input, null, "keydown", { key: "Escape" });
|
||||
|
||||
assert.strictEqual(input.value, "23/03/2022");
|
||||
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
|
||||
});
|
||||
|
||||
QUnit.module("DateTimePicker");
|
||||
|
||||
QUnit.test("basic rendering", async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
});
|
||||
|
||||
assert.containsOnce(target, "input.o_input.o_datepicker_input");
|
||||
assert.containsOnce(target, "span.o_datepicker_button");
|
||||
assert.containsNone(document.body, "div.bootstrap-datetimepicker-widget");
|
||||
|
||||
const datePicker = target.querySelector(".o_datepicker");
|
||||
const input = datePicker.querySelector("input.o_input.o_datepicker_input");
|
||||
assert.strictEqual(input.value, "09/01/1997 12:30:01", "Value should be the one given");
|
||||
assert.strictEqual(
|
||||
datePicker.dataset.targetInput,
|
||||
`#${datePicker.querySelector("input[type=hidden]").id}`,
|
||||
"DateTimePicker id should match its input target"
|
||||
);
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.containsOnce(document.body, "div.bootstrap-datetimepicker-widget .datepicker");
|
||||
assert.containsOnce(document.body, "div.bootstrap-datetimepicker-widget .timepicker");
|
||||
assert.strictEqual(
|
||||
document.querySelector(".datepicker .day.active").dataset.day,
|
||||
"01/09/1997",
|
||||
"Datepicker should have set the correct day"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
document.querySelector(".timepicker .timepicker-hour").innerText.trim(),
|
||||
"12",
|
||||
"Datepicker should have set the correct hour"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector(".timepicker .timepicker-minute").innerText.trim(),
|
||||
"30",
|
||||
"Datepicker should have set the correct minute"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector(".timepicker .timepicker-second").innerText.trim(),
|
||||
"01",
|
||||
"Datepicker should have set the correct second"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date and time", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy HH:mm:ss"),
|
||||
"08/02/1997 15:45:05",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
const input = target.querySelector("input.o_input.o_datepicker_input");
|
||||
|
||||
await click(input);
|
||||
await click(document.querySelector(".datepicker th.next")); // February
|
||||
await click(document.querySelectorAll(".datepicker table td")[15]); // 08
|
||||
await click(document.querySelector('a[title="Select Time"]'));
|
||||
await click(document.querySelector(".timepicker .timepicker-hour"));
|
||||
await click(document.querySelectorAll(".timepicker .hour")[15]); // 15h
|
||||
await click(document.querySelector(".timepicker .timepicker-minute"));
|
||||
await click(document.querySelectorAll(".timepicker .minute")[9]); // 45m
|
||||
await click(document.querySelector(".timepicker .timepicker-second"));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await click(document.querySelectorAll(".timepicker .second")[1]); // 05s
|
||||
|
||||
assert.strictEqual(input.value, "08/02/1997 15:45:05");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date and time with locale", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
format: "dd MMM, yyyy HH:mm:ss",
|
||||
locale: useFRLocale(),
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy HH:mm:ss"),
|
||||
"01/09/1997 15:45:05",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const input = target.querySelector("input.o_input.o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "09 janv., 1997 12:30:01");
|
||||
|
||||
await click(input);
|
||||
|
||||
await click(document.querySelector(".datepicker .picker-switch")); // month picker
|
||||
await click(document.querySelectorAll(".datepicker .month")[8]); // september
|
||||
await click(document.querySelector(".datepicker .day")); // first day
|
||||
|
||||
await click(document.querySelector('a[title="Select Time"]'));
|
||||
await click(document.querySelector(".timepicker .timepicker-hour"));
|
||||
await click(document.querySelectorAll(".timepicker .hour")[15]); // 15h
|
||||
await click(document.querySelector(".timepicker .timepicker-minute"));
|
||||
await click(document.querySelectorAll(".timepicker .minute")[9]); // 45m
|
||||
await click(document.querySelector(".timepicker .timepicker-second"));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await click(document.querySelectorAll(".timepicker .second")[1]); // 05s
|
||||
|
||||
assert.strictEqual(input.value, "01 sept., 1997 15:45:05");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("pick a time with 12 hour format locale", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997 08:30:01", "dd/MM/yyyy hh:mm:ss"),
|
||||
format: "dd/MM/yyyy hh:mm:ss",
|
||||
locale: useFRLocale(),
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy HH:mm:ss"),
|
||||
"09/01/1997 20:30:02",
|
||||
"The new time should be in the afternoon"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const input = target.querySelector("input.o_input.o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "09/01/1997 08:30:01");
|
||||
|
||||
await click(input);
|
||||
|
||||
await click(document.querySelector('a[title="Select Time"]'));
|
||||
await click(document.querySelector('a[title="Increment Second"]'));
|
||||
await click(document.querySelector('button[title="Toggle Period"]'));
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await click(document.querySelector('a[title="Close the picker"]'));
|
||||
|
||||
assert.strictEqual(input.value, "09/01/1997 08:30:02");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("enter a datetime value", async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy HH:mm:ss"),
|
||||
"08/02/1997 15:45:05",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
input.value = "08/02/1997 15:45:05";
|
||||
await triggerEvent(target, ".o_datepicker_input", "change");
|
||||
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "08/02/1997 15:45:05");
|
||||
assert.strictEqual(
|
||||
document.querySelector(".datepicker .day.active").dataset.day,
|
||||
"02/08/1997",
|
||||
"Datepicker should have set the correct day"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector(".timepicker .timepicker-hour").innerText.trim(),
|
||||
"15",
|
||||
"Datepicker should have set the correct hour"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector(".timepicker .timepicker-minute").innerText.trim(),
|
||||
"45",
|
||||
"Datepicker should have set the correct minute"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector(".timepicker .timepicker-second").innerText.trim(),
|
||||
"05",
|
||||
"Datepicker should have set the correct second"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Date time format is correctly set", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
format: "HH:mm:ss yyyy/MM/dd",
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "12:30:01 1997/01/09");
|
||||
|
||||
// Forces an update to assert that the registered format is the correct one
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "12:30:01 1997/01/09");
|
||||
});
|
||||
|
||||
QUnit.test("Datepicker works with norwegian locale", async (assert) => {
|
||||
assert.expect(6);
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/04/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
format: "dd MMM, yyyy",
|
||||
locale: useNOLocale(),
|
||||
onDateTimeChanged(date) {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"01/04/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "09 apr., 1997");
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "09 apr., 1997");
|
||||
|
||||
const days = [...document.querySelectorAll(".datepicker .day")];
|
||||
await click(days.find((d) => d.innerText.trim() === "1")); // first day of april
|
||||
|
||||
assert.strictEqual(input.value, "01 apr., 1997");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("Datepicker works with dots and commas in format", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
date: DateTime.fromFormat("10/03/2023 13:14:27", "dd/MM/yyyy HH:mm:ss"),
|
||||
format: "dd.MM,yyyy",
|
||||
});
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "10.03,2023");
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "10.03,2023");
|
||||
});
|
||||
|
||||
QUnit.test("custom filter date", async function (assert) {
|
||||
assert.expect(3);
|
||||
class MockedSearchModel extends ActionModel {
|
||||
dispatch(method, ...args) {
|
||||
assert.strictEqual(method, "createNewFilters");
|
||||
const preFilters = args[0];
|
||||
const preFilter = preFilters[0];
|
||||
assert.strictEqual(
|
||||
preFilter.description,
|
||||
'A date is equal to "05/05/2005"',
|
||||
"description should be in localized format"
|
||||
);
|
||||
assert.deepEqual(
|
||||
preFilter.domain,
|
||||
'[["date_field","=","2005-05-05"]]',
|
||||
"domain should be in UTC format"
|
||||
);
|
||||
}
|
||||
}
|
||||
const searchModel = new MockedSearchModel();
|
||||
const date_field = { name: "date_field", string: "A date", type: "date", searchable: true };
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: { date_field },
|
||||
},
|
||||
env: { searchModel },
|
||||
});
|
||||
await toggleMenu(target, "Add Custom Filter");
|
||||
await editSelect(target.querySelector(".o_generator_menu_field"), "date_field");
|
||||
const valueInput = target.querySelector(".o_generator_menu_value .o_input");
|
||||
await click(valueInput);
|
||||
await editSelect(valueInput, "05/05/2005");
|
||||
await applyFilter(target);
|
||||
});
|
||||
|
||||
QUnit.test("start with no value", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
onDateTimeChanged(date) {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy HH:mm:ss"),
|
||||
"08/02/1997 15:45:05",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
assert.strictEqual(input.value, "");
|
||||
|
||||
assert.verifySteps([]);
|
||||
input.value = "08/02/1997 15:45:05";
|
||||
await triggerEvent(target, ".o_datepicker_input", "change");
|
||||
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
assert.strictEqual(input.value, "08/02/1997 15:45:05");
|
||||
});
|
||||
|
||||
QUnit.test("arab locale, latin numbering system as input", async (assert) => {
|
||||
const dateFormat = "dd MMM, yyyy";
|
||||
const timeFormat = "hh:mm:ss";
|
||||
const dateTimeFormat = `${dateFormat} ${timeFormat}`;
|
||||
|
||||
patchWithCleanup(localization, { dateFormat, timeFormat, dateTimeFormat });
|
||||
patchWithCleanup(Settings, {
|
||||
defaultLocale: "ar-001",
|
||||
defaultNumberingSystem: "arab",
|
||||
});
|
||||
|
||||
await mountPicker(DateTimePicker, {
|
||||
format: dateTimeFormat,
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
await editInput(input, null, "٠٤ يونيو, ٢٠٢٣ ١١:٣٣:٠٠");
|
||||
|
||||
assert.strictEqual(input.value, "٠٤ يونيو, ٢٠٢٣ ١١:٣٣:٠٠");
|
||||
|
||||
await editInput(input, null, "15 07, 2020 12:30:43");
|
||||
|
||||
assert.strictEqual(input.value, "١٥ يوليو, ٢٠٢٠ ١٢:٣٠:٤٣");
|
||||
});
|
||||
|
||||
QUnit.test("keep date between component and datepicker in sync", async (assert) => {
|
||||
const parent = await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
format: "dd/MM/yyyy",
|
||||
});
|
||||
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
assert.strictEqual(input.value, "09/01/1997");
|
||||
await nextTick();
|
||||
|
||||
await click(input);
|
||||
assert.hasClass(document.querySelector("td.day[data-day='01/09/1997']"), "active");
|
||||
|
||||
// Change the date of the component externally (not through the
|
||||
// datepicker interface)
|
||||
parent.state.date = parent.state.date.plus({ days: 1 });
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(input.value, "10/01/1997");
|
||||
assert.hasClass(document.querySelector("td.day[data-day='01/10/1997']"), "active");
|
||||
|
||||
parent.state.date = false;
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(input.value, "");
|
||||
assert.containsN(document.body, "td.day", 42);
|
||||
assert.containsNone(document.body, "td.day.active");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, reactive, useState, xml } from "@odoo/owl";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { editInput, getFixture, mount, nextTick } from "@web/../tests/helpers/utils";
|
||||
import { datetimePickerService } from "@web/core/datetime/datetimepicker_service";
|
||||
import { useDateTimePicker } from "@web/core/datetime/datetime_hook";
|
||||
import { DateTimeInput } from "@web/core/datetime/datetime_input";
|
||||
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";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
/**
|
||||
* @param {() => any} setup
|
||||
*/
|
||||
const mountInput = async (setup) => {
|
||||
const env = await makeTestEnv();
|
||||
await mount(Root, getFixture(), { env, props: { setup } });
|
||||
return fixture.querySelector(".datetime_hook_input");
|
||||
};
|
||||
|
||||
class Root extends Component {
|
||||
static components = { DateTimeInput };
|
||||
|
||||
static template = xml`
|
||||
<input type="text" class="datetime_hook_input" t-ref="start-date" />
|
||||
<t t-foreach="mainComponentEntries" t-as="comp" t-key="comp[0]">
|
||||
<t t-component="comp[1].Component" t-props="comp[1].props" />
|
||||
</t>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.mainComponentEntries = mainComponentRegistry.getEntries();
|
||||
this.props.setup();
|
||||
}
|
||||
}
|
||||
|
||||
const mainComponentRegistry = registry.category("main_components");
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let fixture;
|
||||
|
||||
QUnit.module("Components", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
clearRegistryWithCleanup(mainComponentRegistry);
|
||||
|
||||
serviceRegistry
|
||||
.add("hotkey", hotkeyService)
|
||||
.add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({
|
||||
dateFormat: "dd/MM/yyyy",
|
||||
dateTimeFormat: "dd/MM/yyyy HH:mm:ss",
|
||||
})
|
||||
)
|
||||
.add("popover", popoverService)
|
||||
.add("ui", uiService)
|
||||
.add("datetime_picker", datetimePickerService);
|
||||
|
||||
fixture = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("DateTime hook");
|
||||
|
||||
QUnit.test("reactivity: update inert object", async (assert) => {
|
||||
const pickerProps = {
|
||||
value: false,
|
||||
type: "date",
|
||||
};
|
||||
|
||||
const input = await mountInput(() => {
|
||||
useDateTimePicker({ pickerProps });
|
||||
});
|
||||
|
||||
assert.strictEqual(input.value, "");
|
||||
|
||||
pickerProps.value = DateTime.fromSQL("2023-06-06");
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(input.value, "");
|
||||
});
|
||||
|
||||
QUnit.test("reactivity: useState & update getter object", async (assert) => {
|
||||
const pickerProps = reactive({
|
||||
value: false,
|
||||
type: "date",
|
||||
});
|
||||
|
||||
const input = await mountInput(() => {
|
||||
const state = useState(pickerProps);
|
||||
state.value; // artificially subscribe to value
|
||||
|
||||
useDateTimePicker({
|
||||
get pickerProps() {
|
||||
return pickerProps;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
assert.strictEqual(input.value, "");
|
||||
|
||||
pickerProps.value = DateTime.fromSQL("2023-06-06");
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(input.value, "06/06/2023");
|
||||
});
|
||||
|
||||
QUnit.test("reactivity: update reactive object returned by the hook", async (assert) => {
|
||||
let pickerProps;
|
||||
const defaultPickerProps = {
|
||||
value: false,
|
||||
type: "date",
|
||||
};
|
||||
|
||||
const input = await mountInput(() => {
|
||||
pickerProps = useDateTimePicker({ pickerProps: defaultPickerProps }).state;
|
||||
});
|
||||
|
||||
assert.strictEqual(input.value, "");
|
||||
assert.strictEqual(pickerProps.value, false);
|
||||
|
||||
pickerProps.value = DateTime.fromSQL("2023-06-06");
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(input.value, "06/06/2023");
|
||||
});
|
||||
|
||||
QUnit.test("returned value is updated when input has changed", async (assert) => {
|
||||
let pickerProps;
|
||||
const defaultPickerProps = {
|
||||
value: false,
|
||||
type: "date",
|
||||
};
|
||||
|
||||
const input = await mountInput(() => {
|
||||
pickerProps = useDateTimePicker({ pickerProps: defaultPickerProps }).state;
|
||||
});
|
||||
|
||||
assert.strictEqual(input.value, "");
|
||||
assert.strictEqual(pickerProps.value, false);
|
||||
|
||||
await editInput(input, null, "06/06/2023");
|
||||
|
||||
assert.strictEqual(pickerProps.value.toSQL().split(" ")[0], "2023-06-06");
|
||||
});
|
||||
|
||||
QUnit.test("value is not updated if it did not change", async (assert) => {
|
||||
const getShortDate = (date) => date.toSQL().split(" ")[0];
|
||||
|
||||
let pickerProps;
|
||||
const defaultPickerProps = {
|
||||
value: DateTime.fromSQL("2023-06-06"),
|
||||
type: "date",
|
||||
};
|
||||
|
||||
const input = await mountInput(() => {
|
||||
pickerProps = useDateTimePicker({
|
||||
pickerProps: defaultPickerProps,
|
||||
onApply: (value) => {
|
||||
assert.step(getShortDate(value));
|
||||
},
|
||||
}).state;
|
||||
});
|
||||
|
||||
assert.strictEqual(input.value, "06/06/2023");
|
||||
assert.strictEqual(getShortDate(pickerProps.value), "2023-06-06");
|
||||
|
||||
await editInput(input, null, "06/06/2023");
|
||||
|
||||
assert.strictEqual(getShortDate(pickerProps.value), "2023-06-06");
|
||||
assert.verifySteps([]);
|
||||
|
||||
await editInput(input, null, "07/06/2023");
|
||||
|
||||
assert.strictEqual(getShortDate(pickerProps.value), "2023-06-07");
|
||||
assert.verifySteps(["2023-06-07"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,549 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
editSelect,
|
||||
getFixture,
|
||||
mount,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { DateTimeInput } from "@web/core/datetime/datetime_input";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import {
|
||||
assertDateTimePicker,
|
||||
getPickerCell,
|
||||
getTimePickers,
|
||||
zoomOut,
|
||||
} from "./datetime_test_helpers";
|
||||
import { datetimePickerService } from "@web/core/datetime/datetimepicker_service";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
/**
|
||||
* @typedef {import("@web/core/datetime/datetime_input").DateTimeInputProps} DateTimeInputProps
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {DateTimeInputProps} props
|
||||
*/
|
||||
const mountInput = async (props) => {
|
||||
const env = await makeTestEnv();
|
||||
await mount(Root, getFixture(), { env, props });
|
||||
return fixture.querySelector(".o_datetime_input");
|
||||
};
|
||||
|
||||
class Root extends Component {
|
||||
static components = { DateTimeInput };
|
||||
|
||||
static template = xml`
|
||||
<DateTimeInput t-props="props" />
|
||||
<t t-foreach="mainComponentEntries" t-as="comp" t-key="comp[0]">
|
||||
<t t-component="comp[1].Component" t-props="comp[1].props" />
|
||||
</t>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
this.mainComponentEntries = mainComponentRegistry.getEntries();
|
||||
}
|
||||
}
|
||||
|
||||
const mainComponentRegistry = registry.category("main_components");
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let fixture;
|
||||
|
||||
QUnit.module("Components", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
clearRegistryWithCleanup(mainComponentRegistry);
|
||||
|
||||
serviceRegistry
|
||||
.add("hotkey", hotkeyService)
|
||||
.add(
|
||||
"localization",
|
||||
makeFakeLocalizationService({
|
||||
dateFormat: "dd/MM/yyyy",
|
||||
dateTimeFormat: "dd/MM/yyyy HH:mm:ss",
|
||||
})
|
||||
)
|
||||
.add("popover", popoverService)
|
||||
.add("ui", uiService)
|
||||
.add("datetime_picker", datetimePickerService);
|
||||
|
||||
fixture = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("DateTimeInput (date)");
|
||||
|
||||
QUnit.test("basic rendering", async function (assert) {
|
||||
await mountInput({
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
});
|
||||
|
||||
assert.containsOnce(fixture, ".o_datetime_input");
|
||||
assertDateTimePicker(false);
|
||||
|
||||
const input = fixture.querySelector(".o_datetime_input");
|
||||
assert.strictEqual(input.value, "09/01/1997", "Value should be the one given");
|
||||
|
||||
await click(input);
|
||||
|
||||
assertDateTimePicker({
|
||||
title: "January 1997",
|
||||
date: [
|
||||
{
|
||||
cells: [
|
||||
[-29, -30, -31, 1, 2, 3, 4],
|
||||
[5, 6, 7, 8, [9], 10, 11],
|
||||
[12, 13, 14, 15, 16, 17, 18],
|
||||
[19, 20, 21, 22, 23, 24, 25],
|
||||
[26, 27, 28, 29, 30, 31, -1],
|
||||
[-2, -3, -4, -5, -6, -7, -8],
|
||||
],
|
||||
daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
weekNumbers: [1, 2, 3, 4, 5, 6],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("pick a date", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
onChange: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"08/02/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
await click(input);
|
||||
await click(getFixture(), ".o_datetime_picker .o_next"); // next month
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await click(getPickerCell("8").at(0));
|
||||
|
||||
assert.strictEqual(input.value, "08/02/1997");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date with FR locale", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
patchWithCleanup(luxon.Settings, { defaultLocale: "fr-FR" });
|
||||
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
format: "dd MMM, yyyy",
|
||||
onChange: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"01/09/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(input.value, "09 janv., 1997");
|
||||
|
||||
await click(input);
|
||||
await zoomOut();
|
||||
await click(getPickerCell("sept."));
|
||||
await click(getPickerCell("1").at(0));
|
||||
|
||||
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);
|
||||
|
||||
patchWithCleanup(luxon.Settings, { defaultLocale: "gu" });
|
||||
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
format: "dd MMM, yyyy",
|
||||
onChange: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"01/09/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(input.value, "09 જાન્યુ, 1997");
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "09 જાન્યુ, 1997");
|
||||
|
||||
await zoomOut();
|
||||
await click(getPickerCell("સપ્ટે"));
|
||||
await click(getPickerCell("1").at(0));
|
||||
|
||||
assert.strictEqual(input.value, "01 સપ્ટે, 1997");
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
QUnit.test("enter a date value", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
onChange: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"08/02/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
await editInput(input, null, "08/02/1997");
|
||||
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.hasClass(getPickerCell("8").at(0), "o_selected");
|
||||
});
|
||||
|
||||
QUnit.test("Date format is correctly set", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
format: "yyyy/MM/dd",
|
||||
});
|
||||
|
||||
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.module("DateTimeInput (datetime)");
|
||||
|
||||
QUnit.test("basic rendering", async function (assert) {
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
});
|
||||
|
||||
assert.containsOnce(fixture, ".o_datetime_input");
|
||||
assertDateTimePicker(false);
|
||||
|
||||
assert.strictEqual(input.value, "09/01/1997 12:30:01", "Value should be the one given");
|
||||
|
||||
await click(input);
|
||||
|
||||
assertDateTimePicker({
|
||||
title: "January 1997",
|
||||
date: [
|
||||
{
|
||||
cells: [
|
||||
[-29, -30, -31, 1, 2, 3, 4],
|
||||
[5, 6, 7, 8, [9], 10, 11],
|
||||
[12, 13, 14, 15, 16, 17, 18],
|
||||
[19, 20, 21, 22, 23, 24, 25],
|
||||
[26, 27, 28, 29, 30, 31, -1],
|
||||
[-2, -3, -4, -5, -6, -7, -8],
|
||||
],
|
||||
daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
weekNumbers: [1, 2, 3, 4, 5, 6],
|
||||
},
|
||||
],
|
||||
time: [[12, 30]],
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("pick a date and time", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
onChange: (date) => assert.step(date.toSQL().split(".")[0]),
|
||||
});
|
||||
|
||||
assert.strictEqual(input.value, "09/01/1997 12:30:01");
|
||||
|
||||
await click(input);
|
||||
|
||||
// Select February 8th
|
||||
await click(getFixture(), ".o_datetime_picker .o_next");
|
||||
await click(getPickerCell("8").at(0));
|
||||
|
||||
// Select 15:45
|
||||
const [hourSelect, minuteSelect] = getTimePickers().at(0);
|
||||
await editSelect(hourSelect, null, "15");
|
||||
await editSelect(minuteSelect, null, "45");
|
||||
|
||||
assert.strictEqual(input.value, "08/02/1997 15:45:01");
|
||||
assert.verifySteps(["1997-02-08 12:30:01", "1997-02-08 15:30:01", "1997-02-08 15:45:01"]);
|
||||
});
|
||||
|
||||
QUnit.test("pick a date and time with locale", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
patchWithCleanup(luxon.Settings, { defaultLocale: "fr-FR" });
|
||||
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
format: "dd MMM, yyyy HH:mm:ss",
|
||||
onChange: (date) => assert.step(date.toSQL().split(".")[0]),
|
||||
});
|
||||
|
||||
assert.strictEqual(input.value, "09 janv., 1997 12:30:01");
|
||||
|
||||
await click(input);
|
||||
|
||||
// Select September 1st
|
||||
await zoomOut();
|
||||
await click(getPickerCell("sept."));
|
||||
await click(getPickerCell("1").at(0));
|
||||
|
||||
// Select 15:45
|
||||
const [hourSelect, minuteSelect] = getTimePickers().at(0);
|
||||
await editSelect(hourSelect, null, "15");
|
||||
await editSelect(minuteSelect, null, "45");
|
||||
|
||||
assert.strictEqual(input.value, "01 sept., 1997 15:45:01");
|
||||
assert.verifySteps(["1997-09-01 12:30:01", "1997-09-01 15:30:01", "1997-09-01 15:45:01"]);
|
||||
});
|
||||
|
||||
QUnit.test("pick a time with 12 hour format without meridiem", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
patchWithCleanup(localization, {
|
||||
dateFormat: "dd/MM/yyyy",
|
||||
dateTimeFormat: "dd/MM/yyyy hh:mm:ss",
|
||||
timeFormat: "hh:mm:ss",
|
||||
});
|
||||
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("09/01/1997 08:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
onChange: (date) => assert.step(date.toSQL().split(".")[0]),
|
||||
});
|
||||
|
||||
assert.strictEqual(input.value, "09/01/1997 08:30:01");
|
||||
|
||||
await click(input);
|
||||
|
||||
const [, minuteSelect] = getTimePickers().at(0);
|
||||
await editSelect(minuteSelect, null, "15");
|
||||
|
||||
assert.verifySteps(["1997-01-09 08:15:01"]);
|
||||
});
|
||||
|
||||
QUnit.test("enter a datetime value", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
onChange: (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"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
input.value = "08/02/1997 15:45:05";
|
||||
await triggerEvent(fixture, ".o_datetime_input", "change");
|
||||
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "08/02/1997 15:45:05");
|
||||
assert.hasClass(getPickerCell("8").at(0), "o_selected");
|
||||
assert.deepEqual(getTimePickers({ parse: true }).at(0), [15, 45]);
|
||||
});
|
||||
|
||||
QUnit.test("Date time format is correctly set", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
format: "HH:mm:ss yyyy/MM/dd",
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
patchWithCleanup(luxon.Settings, { defaultLocale: "nb-NO" });
|
||||
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("09/04/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
format: "dd MMM, yyyy",
|
||||
onChange(date) {
|
||||
assert.step("datetime-changed");
|
||||
assert.strictEqual(
|
||||
date.toFormat("dd/MM/yyyy"),
|
||||
"01/04/1997",
|
||||
"Event should transmit the correct date"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(input.value, "09 apr., 1997");
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "09 apr., 1997");
|
||||
|
||||
await click(getPickerCell("1").at(0));
|
||||
|
||||
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);
|
||||
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("10/03/2023 13:14:27", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
format: "dd.MM,yyyy",
|
||||
});
|
||||
|
||||
assert.strictEqual(input.value, "10.03,2023");
|
||||
|
||||
await click(input);
|
||||
|
||||
assert.strictEqual(input.value, "10.03,2023");
|
||||
});
|
||||
|
||||
QUnit.test("start with no value", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const input = await mountInput({
|
||||
type: "datetime",
|
||||
onChange(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"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(input.value, "");
|
||||
assert.verifySteps([]);
|
||||
|
||||
await editInput(input, null, "08/02/1997 15:45:05");
|
||||
|
||||
assert.verifySteps(["datetime-changed"]);
|
||||
assert.strictEqual(input.value, "08/02/1997 15:45:05");
|
||||
});
|
||||
|
||||
QUnit.test("Clicking close button closes datetime picker", async function (assert) {
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
format: "dd MMM, yyyy HH:mm:ss",
|
||||
});
|
||||
await click(input);
|
||||
await click(getFixture(), ".o_datetime_picker .o_datetime_buttons .btn-secondary");
|
||||
|
||||
assert.strictEqual(
|
||||
getFixture().querySelector(".o_datetime_picker"),
|
||||
null,
|
||||
"Datetime picker is closed"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("arab locale, latin numbering system as input", async (assert) => {
|
||||
patchWithCleanup(localization, {
|
||||
dateFormat: "dd MMM, yyyy",
|
||||
dateTimeFormat: "dd MMM, yyyy hh:mm:ss",
|
||||
timeFormat: "hh:mm:ss",
|
||||
});
|
||||
patchWithCleanup(luxon.Settings, {
|
||||
defaultLocale: "ar-001",
|
||||
defaultNumberingSystem: "arab",
|
||||
});
|
||||
|
||||
const input = await mountInput();
|
||||
|
||||
await editInput(input, null, "٠٤ يونيو, ٢٠٢٣ ١١:٣٣:٠٠");
|
||||
|
||||
assert.strictEqual(input.value, "٠٤ يونيو, ٢٠٢٣ ١١:٣٣:٠٠");
|
||||
|
||||
await editInput(input, null, "15 07, 2020 12:30:43");
|
||||
|
||||
assert.strictEqual(input.value, "١٥ يوليو, ٢٠٢٠ ١٢:٣٠:٤٣");
|
||||
});
|
||||
|
||||
QUnit.test("check datepicker in localization with textual month format", async function (assert) {
|
||||
assert.expect(3);
|
||||
let onChangeDate;
|
||||
|
||||
Object.assign(localization, {
|
||||
dateFormat: 'MMM/dd/yyyy',
|
||||
timeFormat: 'HH:mm:ss',
|
||||
dateTimeFormat: 'MMM/dd/yyyy HH:mm:ss',
|
||||
});
|
||||
|
||||
const input = await mountInput({
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
onChange: date => onChangeDate = date,
|
||||
});
|
||||
|
||||
assert.strictEqual(input.value, "Jan/09/1997");
|
||||
|
||||
await click(input);
|
||||
await click(getPickerCell("5").at(0));
|
||||
|
||||
assert.strictEqual(input.value, "Jan/05/1997");
|
||||
assert.strictEqual(onChangeDate.toFormat("dd/MM/yyyy"), "05/01/1997");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,216 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { ensureArray } from "@web/core/utils/arrays";
|
||||
import { click, getFixture } from "../../helpers/utils";
|
||||
|
||||
/**
|
||||
* @typedef {import("@web/core/datetime/datetime_picker").DateTimePickerProps} DateTimePickerProps
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {false | {
|
||||
* title?: string | string[],
|
||||
* date?: {
|
||||
* cells: (number | string | [number] | [string])[][],
|
||||
* daysOfWeek?: string[],
|
||||
* weekNumbers?: number[],
|
||||
* }[],
|
||||
* time?: ([number, number] | [number, number, "AM" | "PM"])[],
|
||||
* }} expectedParams
|
||||
*/
|
||||
export function assertDateTimePicker(expectedParams) {
|
||||
const assert = QUnit.assert;
|
||||
const fixture = getFixture();
|
||||
|
||||
// Check for picker in DOM
|
||||
if (expectedParams) {
|
||||
assert.containsOnce(fixture, ".o_datetime_picker");
|
||||
} else {
|
||||
assert.containsNone(fixture, ".o_datetime_picker");
|
||||
return;
|
||||
}
|
||||
|
||||
const { title, date, time } = expectedParams;
|
||||
|
||||
// Title
|
||||
if (title) {
|
||||
const expectedTitle = ensureArray(title);
|
||||
assert.containsOnce(fixture, ".o_datetime_picker_header");
|
||||
assert.deepEqual(
|
||||
getTexts(".o_datetime_picker_header", "strong"),
|
||||
expectedTitle,
|
||||
`title should be "${expectedTitle.join(" - ")}"`
|
||||
);
|
||||
} else {
|
||||
assert.containsNone(fixture, ".o_datetime_picker_header");
|
||||
}
|
||||
|
||||
// Time picker
|
||||
if (time) {
|
||||
assert.containsN(fixture, ".o_time_picker", time.length);
|
||||
const timePickers = select(".o_time_picker");
|
||||
for (let i = 0; i < time.length; i++) {
|
||||
const expectedTime = time[i];
|
||||
const values = select(timePickers[i], ".o_time_picker_select").map((sel) => sel.value);
|
||||
const actual = [...values.slice(0, 2).map(Number), ...values.slice(2)];
|
||||
assert.deepEqual(actual, expectedTime, `time values should be [${expectedTime}]`);
|
||||
}
|
||||
} else {
|
||||
assert.containsNone(fixture, ".o_time_picker");
|
||||
}
|
||||
|
||||
// Date picker
|
||||
const datePickerEls = select(".o_date_picker");
|
||||
assert.containsN(fixture, ".o_date_picker", date.length);
|
||||
|
||||
let selectedCells = 0;
|
||||
let outOfRangeCells = 0;
|
||||
let todayCells = 0;
|
||||
for (let i = 0; i < date.length; i++) {
|
||||
const { cells, daysOfWeek, weekNumbers } = date[i];
|
||||
const datePickerEl = datePickerEls[i];
|
||||
const cellEls = select(datePickerEl, ".o_date_item_cell");
|
||||
|
||||
assert.strictEqual(
|
||||
cellEls.length,
|
||||
PICKER_ROWS * PICKER_COLS,
|
||||
`picker should have ${
|
||||
PICKER_ROWS * PICKER_COLS
|
||||
} cells (${PICKER_ROWS} rows and ${PICKER_COLS} columns)`
|
||||
);
|
||||
|
||||
if (daysOfWeek) {
|
||||
const actualDow = getTexts(datePickerEl, ".o_day_of_week_cell");
|
||||
assert.deepEqual(
|
||||
actualDow,
|
||||
daysOfWeek,
|
||||
`picker should display the days of week: ${daysOfWeek
|
||||
.map((dow) => `"${dow}"`)
|
||||
.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
if (weekNumbers) {
|
||||
assert.deepEqual(
|
||||
getTexts(datePickerEl, ".o_week_number_cell").map(Number),
|
||||
weekNumbers,
|
||||
`picker should display the week numbers (${weekNumbers.join(", ")})`
|
||||
);
|
||||
}
|
||||
|
||||
// Date cells
|
||||
const expectedCells = cells.flatMap((row, rowIndex) =>
|
||||
row.map((cell, colIndex) => {
|
||||
const cellEl = cellEls[rowIndex * PICKER_COLS + colIndex];
|
||||
|
||||
// Check flags
|
||||
let value = cell;
|
||||
const isSelected = Array.isArray(cell);
|
||||
if (isSelected) {
|
||||
value = value[0];
|
||||
}
|
||||
const isToday = typeof value === "string";
|
||||
if (isToday) {
|
||||
value = Number(value);
|
||||
}
|
||||
const isOutOfRange = value < 0;
|
||||
if (isOutOfRange) {
|
||||
value = Math.abs(value);
|
||||
}
|
||||
|
||||
// Assert based on flags
|
||||
if (isSelected) {
|
||||
selectedCells++;
|
||||
assert.hasClass(cellEl, "o_selected");
|
||||
}
|
||||
if (isOutOfRange) {
|
||||
outOfRangeCells++;
|
||||
assert.hasClass(cellEl, "o_out_of_range");
|
||||
}
|
||||
if (isToday) {
|
||||
todayCells++;
|
||||
assert.hasClass(cellEl, "o_today");
|
||||
}
|
||||
|
||||
return value;
|
||||
})
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
cellEls.map((cell) => Number(getTexts(cell)[0])),
|
||||
expectedCells,
|
||||
`cell content should match the expected values: [${expectedCells.join(", ")}]`
|
||||
);
|
||||
}
|
||||
|
||||
assert.containsN(fixture, ".o_selected", selectedCells);
|
||||
assert.containsN(fixture, ".o_out_of_range", outOfRangeCells);
|
||||
assert.containsN(fixture, ".o_today", todayCells);
|
||||
}
|
||||
|
||||
export function getPickerApplyButton() {
|
||||
return select(".o_datetime_picker .o_datetime_buttons .o_apply").at(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RegExp | string} expr
|
||||
*/
|
||||
export function getPickerCell(expr) {
|
||||
const regex = expr instanceof RegExp ? expr : new RegExp(`^${expr}$`, "i");
|
||||
const cells = select(".o_datetime_picker .o_date_item_cell").filter((cell) =>
|
||||
regex.test(getTexts(cell)[0])
|
||||
);
|
||||
return cells.length === 1 ? cells[0] : cells;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {...(string | HTMLElement)} selectors
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function getTexts(...selectors) {
|
||||
return select(...selectors).map((e) => e.innerText.trim().replace(/\s+/g, " "));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} [options={}]
|
||||
* @param {boolean} [options.parse=false] whether to directly return the parsed
|
||||
* values of the select elements
|
||||
* @returns {HTMLSelectElement[] | (number | string)[]}
|
||||
*/
|
||||
export function getTimePickers({ parse = false } = {}) {
|
||||
return select(".o_time_picker").map((timePickerEl) => {
|
||||
const selects = select(timePickerEl, ".o_time_picker_select");
|
||||
if (parse) {
|
||||
return selects.map((sel) => (isNaN(sel.value) ? sel.value : Number(sel.value)));
|
||||
} else {
|
||||
return selects;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {...(string | HTMLElement)} selectors
|
||||
* @returns {HTMLElement[]}
|
||||
*/
|
||||
const select = (...selectors) => {
|
||||
const root = selectors[0] instanceof Element ? selectors.shift() : getFixture();
|
||||
return selectors.length ? [...root.querySelectorAll(selectors.join(" "))] : [root];
|
||||
};
|
||||
|
||||
export function useTwelveHourClockFormat() {
|
||||
const { dateFormat = "dd/MM/yyyy", timeFormat = "HH:mm:ss" } = localization;
|
||||
const twcTimeFormat = `${timeFormat.replace(/H/g, "h")} a`;
|
||||
patchWithCleanup(localization, {
|
||||
dateTimeFormat: `${dateFormat} ${twcTimeFormat}`,
|
||||
timeFormat: twcTimeFormat,
|
||||
});
|
||||
}
|
||||
|
||||
export function zoomOut() {
|
||||
return click(getFixture(), ".o_zoom_out");
|
||||
}
|
||||
|
||||
const PICKER_ROWS = 6;
|
||||
const PICKER_COLS = 7;
|
||||
|
|
@ -10,7 +10,7 @@ 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 { makeTestEnv, prepareRegistriesWithCleanup } from "../../helpers/mock_env";
|
||||
import {
|
||||
fakeCompanyService,
|
||||
fakeCommandService,
|
||||
|
|
@ -22,17 +22,23 @@ 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 {
|
||||
editSearchView,
|
||||
editView,
|
||||
getView,
|
||||
setDefaults,
|
||||
viewMetadata,
|
||||
viewRawRecord,
|
||||
} from "@web/views/debug_items";
|
||||
import { fieldService } from "@web/core/field_service";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
const { prepareRegistriesWithCleanup } = utils;
|
||||
|
||||
export class DebugMenuParent extends Component {
|
||||
setup() {
|
||||
|
|
@ -56,6 +62,7 @@ QUnit.module("DebugMenu", (hooks) => {
|
|||
.add("orm", ormService)
|
||||
.add("dialog", makeFakeDialogService())
|
||||
.add("localization", makeFakeLocalizationService())
|
||||
.add("field", fieldService)
|
||||
.add("command", fakeCommandService);
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
|
|
@ -354,6 +361,41 @@ QUnit.module("DebugMenu", (hooks) => {
|
|||
assert.containsOnce(target, ".some_view");
|
||||
});
|
||||
|
||||
QUnit.test("get view: basic rendering", async (assert) => {
|
||||
prepareRegistriesWithCleanup();
|
||||
patchWithCleanup(odoo, {
|
||||
debug: true,
|
||||
});
|
||||
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
registry.category("debug").category("view").add("getView", getView);
|
||||
|
||||
const serverData = getActionManagerServerData();
|
||||
serverData.actions[1234] = {
|
||||
id: 1234,
|
||||
xml_id: "action_1234",
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
};
|
||||
|
||||
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.strictEqual(
|
||||
target.querySelector(".modal-body").innerText,
|
||||
`<tree><field name="foo" field_id="foo_0"/></tree>`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("can edit a pivot view", async (assert) => {
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "check_access_rights") {
|
||||
|
|
@ -381,18 +423,25 @@ QUnit.module("DebugMenu", (hooks) => {
|
|||
serverData.views["pony,18,pivot"] = "<pivot></pivot>";
|
||||
serverData.models["ir.ui.view"] = {
|
||||
fields: {},
|
||||
records: [{ id: 18 }],
|
||||
records: [{ id: 18, name: "Edit view" }],
|
||||
};
|
||||
serverData.views["ir.ui.view,false,form"] = `<form><field name="id"/></form>`;
|
||||
serverData.views["ir.ui.view,false,search"] = `<search></search>`;
|
||||
|
||||
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.containsOnce(target, ".breadcrumb-item");
|
||||
assert.containsOnce(target, ".o_breadcrumb .active");
|
||||
assert.strictEqual(target.querySelector(".o_breadcrumb .active").textContent, "Edit view");
|
||||
assert.strictEqual(target.querySelector(".o_field_widget[name=id]").textContent, "18");
|
||||
|
||||
await click(target, ".breadcrumb .o_back_button");
|
||||
assert.containsOnce(target, ".o_breadcrumb .active");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".modal .o_form_view .o_field_widget[name=id] input").value,
|
||||
"18"
|
||||
target.querySelector(".o_breadcrumb .active").textContent,
|
||||
"Reporting Ponies"
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -417,20 +466,19 @@ QUnit.module("DebugMenu", (hooks) => {
|
|||
serverData.actions[1].search_view_id = [293, "some_search_view"];
|
||||
serverData.models["ir.ui.view"] = {
|
||||
fields: {},
|
||||
records: [{ id: 293 }],
|
||||
records: [{ id: 293, name: "Edit view" }],
|
||||
};
|
||||
serverData.views["ir.ui.view,false,form"] = `<form><field name="id"/></form>`;
|
||||
serverData.views["ir.ui.view,false,search"] = `<search></search>`;
|
||||
|
||||
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"
|
||||
);
|
||||
assert.containsOnce(target, ".breadcrumb-item");
|
||||
assert.containsOnce(target, ".o_breadcrumb .active");
|
||||
assert.strictEqual(target.querySelector(".o_breadcrumb .active").textContent, "Edit view");
|
||||
assert.strictEqual(target.querySelector(".o_field_widget[name=id]").textContent, "293");
|
||||
});
|
||||
|
||||
QUnit.test("edit search view on action without search_view_id", async (assert) => {
|
||||
|
|
@ -475,10 +523,11 @@ QUnit.module("DebugMenu", (hooks) => {
|
|||
};
|
||||
serverData.models["ir.ui.view"] = {
|
||||
fields: {},
|
||||
records: [{ id: 293 }],
|
||||
records: [{ id: 293, name: "Edit view" }],
|
||||
};
|
||||
serverData.views = {};
|
||||
serverData.views["ir.ui.view,false,form"] = `<form><field name="id"/></form>`;
|
||||
serverData.views["ir.ui.view,false,search"] = `<search></search>`;
|
||||
serverData.views["partner,false,toy"] = `<toy></toy>`;
|
||||
serverData.views["partner,293,search"] = `<search></search>`;
|
||||
|
||||
|
|
@ -488,12 +537,10 @@ QUnit.module("DebugMenu", (hooks) => {
|
|||
|
||||
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"
|
||||
);
|
||||
assert.containsOnce(target, ".breadcrumb-item");
|
||||
assert.containsOnce(target, ".o_breadcrumb .active");
|
||||
assert.strictEqual(target.querySelector(".o_breadcrumb .active").textContent, "Edit view");
|
||||
assert.strictEqual(target.querySelector(".o_field_widget[name=id]").textContent, "293");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
|
|
@ -670,6 +717,47 @@ QUnit.module("DebugMenu", (hooks) => {
|
|||
assert.containsNone(target, ".modal");
|
||||
});
|
||||
|
||||
QUnit.test("fetch raw data: basic rendering", async (assert) => {
|
||||
prepareRegistriesWithCleanup();
|
||||
patchWithCleanup(odoo, {
|
||||
debug: true,
|
||||
});
|
||||
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
registry.category("debug").category("form").add("viewRawRecord", viewRawRecord);
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
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.strictEqual(
|
||||
target.querySelector(".modal-title").textContent,
|
||||
"Raw Record Data: partner(27)"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".modal-body pre").textContent,
|
||||
'{\n "bar": false,\n "display_name": "p1",\n "foo": false,\n "id": 27,\n "m2o": false,\n "name": "name",\n "o2m": [],\n "write_date": false\n}'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("view metadata: basic rendering", async (assert) => {
|
||||
prepareRegistriesWithCleanup();
|
||||
patchWithCleanup(odoo, {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ 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 {
|
||||
|
|
@ -19,7 +18,9 @@ import {
|
|||
patchWithCleanup,
|
||||
} from "../helpers/utils";
|
||||
import { Dialog } from "../../src/core/dialog/dialog";
|
||||
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { useAutofocus } from "@web/core/utils/hooks";
|
||||
import { Component, onMounted, xml } from "@odoo/owl";
|
||||
|
||||
let env;
|
||||
|
|
@ -60,13 +61,13 @@ QUnit.test("Simple rendering with a single dialog", async (assert) => {
|
|||
CustomDialog.components = { Dialog };
|
||||
CustomDialog.template = xml`<Dialog title="'Welcome'">content</Dialog>`;
|
||||
await mount(PseudoWebClient, target, { env });
|
||||
assert.containsNone(target, ".o_dialog_container .o_dialog");
|
||||
assert.containsNone(target, ".o_dialog");
|
||||
env.services.dialog.add(CustomDialog);
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_dialog_container .o_dialog");
|
||||
assert.containsOnce(target, ".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");
|
||||
await click(target.querySelector(".o_dialog footer button"));
|
||||
assert.containsNone(target, ".o_dialog");
|
||||
});
|
||||
|
||||
QUnit.test("Simple rendering and close a single dialog", async (assert) => {
|
||||
|
|
@ -77,16 +78,16 @@ QUnit.test("Simple rendering and close a single dialog", async (assert) => {
|
|||
CustomDialog.template = xml`<Dialog title="'Welcome'">content</Dialog>`;
|
||||
|
||||
await mount(PseudoWebClient, target, { env });
|
||||
assert.containsNone(target, ".o_dialog_container .o_dialog");
|
||||
assert.containsNone(target, ".o_dialog");
|
||||
|
||||
const removeDialog = env.services.dialog.add(CustomDialog);
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_dialog_container .o_dialog");
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.strictEqual(target.querySelector("header .modal-title").textContent, "Welcome");
|
||||
|
||||
removeDialog();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_dialog_container .o_dialog");
|
||||
assert.containsNone(target, ".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.
|
||||
|
|
@ -101,20 +102,20 @@ QUnit.test("rendering with two dialogs", async (assert) => {
|
|||
CustomDialog.template = xml`<Dialog title="props.title">content</Dialog>`;
|
||||
|
||||
await mount(PseudoWebClient, target, { env });
|
||||
assert.containsNone(target, ".o_dialog_container .o_dialog");
|
||||
assert.containsNone(target, ".o_dialog");
|
||||
env.services.dialog.add(CustomDialog, { title: "Hello" });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_dialog_container .o_dialog");
|
||||
assert.containsOnce(target, ".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.containsN(target, ".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");
|
||||
await click(target.querySelector(".o_dialog footer button"));
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
assert.strictEqual(target.querySelector("header .modal-title").textContent, "Sauron");
|
||||
});
|
||||
|
||||
|
|
@ -127,31 +128,63 @@ QUnit.test("multiple dialogs can become the UI active element", async (assert) =
|
|||
|
||||
env.services.dialog.add(CustomDialog, { title: "Hello" });
|
||||
await nextTick();
|
||||
let dialogModal = target.querySelector(
|
||||
".o_dialog_container .o_dialog .modal:not(.o_inactive_modal)"
|
||||
);
|
||||
let dialogModal = target.querySelector(".o_dialog:not(.o_inactive_modal) .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)"
|
||||
);
|
||||
dialogModal = target.querySelector(".o_dialog:not(.o_inactive_modal) .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)"
|
||||
);
|
||||
dialogModal = target.querySelector(".o_dialog:not(.o_inactive_modal) .modal");
|
||||
|
||||
assert.strictEqual(dialogModal, env.services.ui.activeElement);
|
||||
});
|
||||
|
||||
QUnit.test("a popover with an autofocus child can become the UI active element", async (assert) => {
|
||||
class TestPopover extends Component {
|
||||
static template = xml`<input type="text" t-ref="autofocus" />`;
|
||||
setup() {
|
||||
useAutofocus();
|
||||
}
|
||||
}
|
||||
class CustomDialog extends Component {
|
||||
static components = { Dialog };
|
||||
static template = xml`<Dialog title="props.title">
|
||||
<button class="btn test" t-on-click="showPopover">show</button>
|
||||
</Dialog>`;
|
||||
setup() {
|
||||
this.popover = usePopover(TestPopover);
|
||||
}
|
||||
showPopover(event) {
|
||||
this.popover.open(event.target, {});
|
||||
}
|
||||
}
|
||||
serviceRegistry.add("popover", popoverService);
|
||||
await nextTick(); // wait for the popover service to be started
|
||||
await mount(PseudoWebClient, target, { env });
|
||||
assert.strictEqual(env.services.ui.activeElement, document);
|
||||
assert.strictEqual(document.activeElement, document.body);
|
||||
|
||||
env.services.dialog.add(CustomDialog, { title: "Hello" });
|
||||
await nextTick();
|
||||
const dialogModal = target.querySelector(".o_dialog:not(.o_inactive_modal) .modal");
|
||||
assert.strictEqual(env.services.ui.activeElement, dialogModal);
|
||||
assert.strictEqual(document.activeElement, dialogModal.querySelector(".btn.o-default-button"));
|
||||
|
||||
await click(dialogModal, ".btn.test");
|
||||
const popover = target.querySelector(".o_popover");
|
||||
const input = popover.querySelector("input");
|
||||
assert.strictEqual(env.services.ui.activeElement, popover);
|
||||
assert.strictEqual(document.activeElement, input);
|
||||
});
|
||||
|
||||
QUnit.test("Interactions between multiple dialogs", async (assert) => {
|
||||
assert.expect(14);
|
||||
assert.expect(10);
|
||||
function activity(modals) {
|
||||
const active = [];
|
||||
const names = [];
|
||||
|
|
@ -174,40 +207,37 @@ QUnit.test("Interactions between multiple dialogs", async (assert) => {
|
|||
env.services.dialog.add(CustomDialog, { title: "Rafiki" });
|
||||
await nextTick();
|
||||
|
||||
let modals = document.querySelectorAll(".modal");
|
||||
let modals = document.querySelectorAll(".o_dialog");
|
||||
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");
|
||||
modals = document.querySelectorAll(".o_dialog");
|
||||
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");
|
||||
modals = document.querySelectorAll(".o_dialog");
|
||||
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");
|
||||
assert.containsNone(target, ".o_dialog");
|
||||
});
|
||||
|
||||
QUnit.test("dialog component crashes", async (assert) => {
|
||||
assert.expect(4);
|
||||
assert.expect(3);
|
||||
assert.expectErrors();
|
||||
|
||||
class FailingDialog extends Component {
|
||||
setup() {
|
||||
|
|
@ -220,27 +250,15 @@ QUnit.test("dialog component crashes", async (assert) => {
|
|||
const prom = makeDeferred();
|
||||
patchWithCleanup(ErrorDialog.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
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("rpc", rpc, { force: true });
|
||||
serviceRegistry.add("notification", notificationService);
|
||||
serviceRegistry.add("error", errorService);
|
||||
|
||||
|
|
@ -249,7 +267,7 @@ QUnit.test("dialog component crashes", async (assert) => {
|
|||
env.services.dialog.add(FailingDialog);
|
||||
await prom;
|
||||
|
||||
assert.verifySteps(["error"]);
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.containsOnce(target, ".modal .o_dialog_error");
|
||||
assert.containsOnce(target, ".modal .o_error_dialog");
|
||||
assert.verifyErrors(["Some Error"]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,8 +5,17 @@ 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 { makeDialogTestEnv } from "../helpers/mock_env";
|
||||
import {
|
||||
click,
|
||||
destroy,
|
||||
getFixture,
|
||||
mount,
|
||||
triggerEvent,
|
||||
triggerHotkey,
|
||||
dragAndDrop,
|
||||
nextTick,
|
||||
} from "../helpers/utils";
|
||||
import { makeFakeDialogService } from "../helpers/mock_services";
|
||||
|
||||
import { Component, useState, onMounted, xml } from "@odoo/owl";
|
||||
|
|
@ -14,15 +23,6 @@ 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();
|
||||
|
|
@ -71,6 +71,33 @@ QUnit.module("Components", (hooks) => {
|
|||
assert.strictEqual(target.querySelector("footer button").textContent, "Ok");
|
||||
});
|
||||
|
||||
QUnit.test("hotkeys work on dialogs", async function (assert) {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { Dialog };
|
||||
Parent.template = xml`
|
||||
<Dialog title="'Wow(l) Effect'">
|
||||
Hello!
|
||||
</Dialog>
|
||||
`;
|
||||
|
||||
const env = await makeDialogTestEnv();
|
||||
env.dialogData.close = () => assert.step("close");
|
||||
env.dialogData.dismiss = () => assert.step("dismiss");
|
||||
parent = await mount(Parent, target, { env });
|
||||
assert.strictEqual(
|
||||
target.querySelector("header .modal-title").textContent,
|
||||
"Wow(l) Effect"
|
||||
);
|
||||
assert.strictEqual(target.querySelector("footer button").textContent, "Ok");
|
||||
// Same effect as clicking on the x button
|
||||
triggerHotkey("escape");
|
||||
await nextTick();
|
||||
assert.verifySteps(["dismiss", "close"]);
|
||||
// Same effect as clicking on the Ok button
|
||||
triggerHotkey("control+enter");
|
||||
assert.verifySteps(["close"]);
|
||||
});
|
||||
|
||||
QUnit.test("simple rendering with two dialogs", async function (assert) {
|
||||
assert.expect(3);
|
||||
class Parent extends Component {}
|
||||
|
|
@ -99,9 +126,10 @@ QUnit.module("Components", (hooks) => {
|
|||
});
|
||||
|
||||
QUnit.test("click on the button x triggers the service close", async function (assert) {
|
||||
assert.expect(3);
|
||||
assert.expect(4);
|
||||
const env = await makeDialogTestEnv();
|
||||
env.dialogData.close = () => assert.step("close");
|
||||
env.dialogData.dismiss = () => assert.step("dismiss");
|
||||
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`
|
||||
|
|
@ -113,14 +141,43 @@ QUnit.module("Components", (hooks) => {
|
|||
parent = await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
await click(target, ".o_dialog header button.btn-close");
|
||||
assert.verifySteps(["close"]);
|
||||
assert.verifySteps(["dismiss", "close"]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"click on the button x triggers the close and dismiss defined by a Child component",
|
||||
async function (assert) {
|
||||
assert.expect(4);
|
||||
const env = await makeDialogTestEnv();
|
||||
class Child extends Component {
|
||||
static template = xml`<div>Hello</div>`;
|
||||
|
||||
setup() {
|
||||
this.env.dialogData.close = () => assert.step("close");
|
||||
this.env.dialogData.dismiss = () => assert.step("dismiss");
|
||||
}
|
||||
}
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`
|
||||
<Dialog>
|
||||
<Child/>
|
||||
</Dialog>
|
||||
`;
|
||||
Parent.components = { Child, Dialog };
|
||||
parent = await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o_dialog");
|
||||
|
||||
await click(target, ".o_dialog header button.btn-close");
|
||||
assert.verifySteps(["dismiss", "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");
|
||||
env.dialogData.dismiss = () => assert.step("dismiss");
|
||||
assert.expect(3);
|
||||
class Parent extends Component {}
|
||||
|
||||
|
|
@ -289,6 +346,7 @@ QUnit.module("Components", (hooks) => {
|
|||
|
||||
const parent = await mount(Parent, target, { env });
|
||||
destroy(parent);
|
||||
await Promise.resolve();
|
||||
|
||||
assert.strictEqual(
|
||||
env.services.ui.activeElement,
|
||||
|
|
@ -296,4 +354,54 @@ QUnit.module("Components", (hooks) => {
|
|||
"UI owner should be reset to the default (document)"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("dialog can be moved", async (assert) => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<Dialog>content</Dialog>`;
|
||||
static components = { Dialog };
|
||||
}
|
||||
|
||||
await mount(Parent, target, { env: await makeDialogTestEnv() });
|
||||
const content = target.querySelector(".modal-content");
|
||||
assert.strictEqual(content.style.top, "0px");
|
||||
assert.strictEqual(content.style.left, "0px");
|
||||
|
||||
const header = content.querySelector(".modal-header");
|
||||
const headerRect = header.getBoundingClientRect();
|
||||
await dragAndDrop(header, document.body, {
|
||||
// the util function sets the source coordinates at (x; y) + (w/2; h/2)
|
||||
// so we need to move the dialog based on these coordinates.
|
||||
x: headerRect.x + headerRect.width / 2 + 20,
|
||||
y: headerRect.y + headerRect.height / 2 + 50,
|
||||
});
|
||||
assert.strictEqual(content.style.top, "50px");
|
||||
assert.strictEqual(content.style.left, "20px");
|
||||
});
|
||||
|
||||
QUnit.test("dialog's position is reset on resize", async (assert) => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<Dialog>content</Dialog>`;
|
||||
static components = { Dialog };
|
||||
}
|
||||
|
||||
await mount(Parent, target, { env: await makeDialogTestEnv() });
|
||||
const content = target.querySelector(".modal-content");
|
||||
assert.strictEqual(content.style.top, "0px");
|
||||
assert.strictEqual(content.style.left, "0px");
|
||||
|
||||
const header = content.querySelector(".modal-header");
|
||||
const headerRect = header.getBoundingClientRect();
|
||||
await dragAndDrop(header, document.body, {
|
||||
// the util function sets the source coordinates at (x; y) + (w/2; h/2)
|
||||
// so we need to move the dialog based on these coordinates.
|
||||
x: headerRect.x + headerRect.width / 2 + 20,
|
||||
y: headerRect.y + headerRect.height / 2 + 50,
|
||||
});
|
||||
assert.strictEqual(content.style.top, "50px");
|
||||
assert.strictEqual(content.style.left, "20px");
|
||||
|
||||
await triggerEvent(window, null, "resize");
|
||||
assert.strictEqual(content.style.top, "0px");
|
||||
assert.strictEqual(content.style.left, "0px");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { DomainSelectorDialog } from "@web/core/domain_selector_dialog/domain_selector_dialog";
|
||||
import { click, dragAndDrop, getFixture, mount } from "../helpers/utils";
|
||||
import { makeDialogTestEnv } from "../helpers/mock_env";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { makeFakeLocalizationService } from "../helpers/mock_services";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { fieldService } from "@web/core/field_service";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { nameService } from "@web/core/name_service";
|
||||
|
||||
/**
|
||||
* @typedef {Record<keyof DomainSelectorDialog.props, any>} Props
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Partial<Props> & { mockRPC: Function }} [params]
|
||||
*/
|
||||
async function makeDomainSelectorDialog(params = {}) {
|
||||
const props = { ...params };
|
||||
const mockRPC = props.mockRPC;
|
||||
delete props.mockRPC;
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { DomainSelectorDialog };
|
||||
static template = xml`<DomainSelectorDialog t-props="domainSelectorProps"/>`;
|
||||
setup() {
|
||||
this.domainSelectorProps = {
|
||||
resModel: "partner",
|
||||
readonly: false,
|
||||
domain: "[]",
|
||||
close: () => {},
|
||||
onConfirm: () => {},
|
||||
...props,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const env = await makeDialogTestEnv({ serverData, mockRPC });
|
||||
await mount(MainComponentsContainer, fixture, { env });
|
||||
return mount(Parent, fixture, { env, props });
|
||||
}
|
||||
|
||||
/** @type {Element} */
|
||||
let fixture;
|
||||
let serverData;
|
||||
|
||||
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 },
|
||||
int: { string: "Integer", type: "integer", searchable: true },
|
||||
json_field: { string: "Json Field", type: "json", 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("notification", notificationService);
|
||||
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("popover", popoverService);
|
||||
registry.category("services").add("field", fieldService);
|
||||
registry.category("services").add("name", nameService);
|
||||
|
||||
fixture = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("DomainSelectorDialog");
|
||||
|
||||
QUnit.test("a domain with a user context dynamic part is valid", async (assert) => {
|
||||
await makeDomainSelectorDialog({
|
||||
domain: "[('foo', '=', uid)]",
|
||||
onConfirm(domain) {
|
||||
assert.strictEqual(domain, "[('foo', '=', uid)]");
|
||||
assert.step("confirmed");
|
||||
},
|
||||
mockRPC(route) {
|
||||
if (route === "/web/domain/validate") {
|
||||
assert.step("validation");
|
||||
return true;
|
||||
}
|
||||
},
|
||||
});
|
||||
const confirmButton = fixture.querySelector(".o_dialog footer button");
|
||||
await click(confirmButton);
|
||||
assert.verifySteps(["validation", "confirmed"]);
|
||||
});
|
||||
|
||||
QUnit.test("can extend eval context", async (assert) => {
|
||||
await makeDomainSelectorDialog({
|
||||
domain: "['&', ('foo', '=', uid), ('bar', '=', var)]",
|
||||
context: { uid: 99, var: "true" },
|
||||
onConfirm(domain) {
|
||||
assert.strictEqual(domain, "['&', ('foo', '=', uid), ('bar', '=', var)]");
|
||||
assert.step("confirmed");
|
||||
},
|
||||
|
||||
mockRPC(route) {
|
||||
if (route === "/web/domain/validate") {
|
||||
assert.step("validation");
|
||||
return true;
|
||||
}
|
||||
},
|
||||
});
|
||||
const confirmButton = fixture.querySelector(".o_dialog footer button");
|
||||
await click(confirmButton);
|
||||
assert.verifySteps(["validation", "confirmed"]);
|
||||
});
|
||||
|
||||
QUnit.test("a domain with an unknown expression is not valid", async (assert) => {
|
||||
await makeDomainSelectorDialog({
|
||||
domain: "[('foo', '=', unknown)]",
|
||||
onConfirm() {
|
||||
assert.step("confirmed");
|
||||
},
|
||||
mockRPC(route) {
|
||||
if (route === "/web/domain/validate") {
|
||||
assert.step("validation");
|
||||
}
|
||||
},
|
||||
});
|
||||
const confirmButton = fixture.querySelector(".o_dialog footer button");
|
||||
await click(confirmButton);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("model_field_selector should close on dialog drag", async (assert) => {
|
||||
await makeDomainSelectorDialog({
|
||||
domain: "[('foo', '=', unknown)]",
|
||||
});
|
||||
|
||||
assert.containsNone(fixture, ".o_model_field_selector_popover");
|
||||
await click(fixture, ".o_model_field_selector_value");
|
||||
assert.containsOnce(fixture, ".o_model_field_selector_popover");
|
||||
|
||||
const header = fixture.querySelector(".modal-header");
|
||||
const headerRect = header.getBoundingClientRect();
|
||||
await dragAndDrop(header, document.body, {
|
||||
// the util function sets the source coordinates at (x; y) + (w/2; h/2)
|
||||
// so we need to move the dialog based on these coordinates.
|
||||
x: headerRect.x + headerRect.width / 2 + 20,
|
||||
y: headerRect.y + headerRect.height / 2 + 50,
|
||||
});
|
||||
assert.containsNone(fixture, ".o_model_field_selector_popover");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -98,9 +98,11 @@ QUnit.module("domain", {}, () => {
|
|||
assert.ok(new Domain(["!", ["group_method", "=", "count"]]).contains(record));
|
||||
});
|
||||
|
||||
QUnit.test("like, =like, ilike and =ilike", function (assert) {
|
||||
assert.expect(16);
|
||||
QUnit.test("like, =like, ilike, =ilike, not like and not ilike", function (assert) {
|
||||
assert.expect(36);
|
||||
|
||||
assert.ok(new Domain([["a", "like", "test%value"]]).contains({ a: "test value" }));
|
||||
assert.notOk(new Domain([["a", "like", "test%value"]]).contains({ a: "value test" }));
|
||||
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" }));
|
||||
|
|
@ -111,6 +113,8 @@ QUnit.module("domain", {}, () => {
|
|||
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", "test%value"]]).contains({ a: "test value" }));
|
||||
assert.notOk(new Domain([["a", "ilike", "test%value"]]).contains({ a: "value test" }));
|
||||
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" }));
|
||||
|
|
@ -120,6 +124,24 @@ QUnit.module("domain", {}, () => {
|
|||
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.notOk(new Domain([["a", "not like", "test%value"]]).contains({ a: "test value" }));
|
||||
assert.ok(new Domain([["a", "not like", "test%value"]]).contains({ a: "value test" }));
|
||||
assert.notOk(new Domain([["a", "not like", "value"]]).contains({ a: "value" }));
|
||||
assert.notOk(new Domain([["a", "not like", "value"]]).contains({ a: "some value" }));
|
||||
assert.ok(new Domain([["a", "not like", "value"]]).contains({ a: "Some Value" }));
|
||||
assert.ok(new Domain([["a", "not like", "value"]]).contains({ a: "something" }));
|
||||
assert.ok(new Domain([["a", "not like", "value"]]).contains({ a: "Something" }));
|
||||
assert.notOk(new Domain([["a", "not ilike", "test%value"]]).contains({ a: "test value" }));
|
||||
assert.ok(new Domain([["a", "not ilike", "test%value"]]).contains({ a: "value test" }));
|
||||
assert.notOk(new Domain([["a", "not like", "value"]]).contains({ a: false }));
|
||||
|
||||
assert.notOk(new Domain([["a", "not ilike", "value"]]).contains({ a: "value" }));
|
||||
assert.notOk(new Domain([["a", "not ilike", "value"]]).contains({ a: "some value" }));
|
||||
assert.notOk(new Domain([["a", "not ilike", "value"]]).contains({ a: "Some Value" }));
|
||||
assert.ok(new Domain([["a", "not ilike", "value"]]).contains({ a: "something" }));
|
||||
assert.ok(new Domain([["a", "not ilike", "value"]]).contains({ a: "Something" }));
|
||||
assert.notOk(new Domain([["a", "not ilike", "value"]]).contains({ a: false }));
|
||||
});
|
||||
|
||||
QUnit.test("complex domain", function (assert) {
|
||||
|
|
@ -204,6 +226,21 @@ QUnit.module("domain", {}, () => {
|
|||
);
|
||||
});
|
||||
|
||||
QUnit.test("toJson", function (assert) {
|
||||
assert.deepEqual(new Domain([]).toJson(), []);
|
||||
assert.deepEqual(new Domain("[]").toJson(), []);
|
||||
assert.deepEqual(new Domain([["a", "=", 3]]).toJson(), [["a", "=", 3]]);
|
||||
assert.deepEqual(new Domain('[("a", "=", 3)]').toJson(), [["a", "=", 3]]);
|
||||
assert.strictEqual(
|
||||
new Domain('[("user_id", "=", uid)]').toJson(),
|
||||
'[("user_id", "=", uid)]'
|
||||
);
|
||||
assert.strictEqual(
|
||||
new Domain('[("date", "=", context_today())]').toJson(),
|
||||
'[("date", "=", context_today())]'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("implicit &", function (assert) {
|
||||
const domain = new Domain([
|
||||
["a", "=", 3],
|
||||
|
|
@ -354,6 +391,16 @@ QUnit.module("domain", {}, () => {
|
|||
/invalid domain .* \(missing 1 segment/
|
||||
);
|
||||
assert.throws(() => new Domain(["!"]), /invalid domain .* \(missing 1 segment/);
|
||||
assert.throws(() => new Domain(`[(1, 2)]`), /Invalid domain AST/);
|
||||
assert.throws(() => new Domain(`[(1, 2, 3, 4)]`), /Invalid domain AST/);
|
||||
assert.throws(() => new Domain(`["a"]`), /Invalid domain AST/);
|
||||
assert.throws(() => new Domain(`[1]`), /Invalid domain AST/);
|
||||
assert.throws(() => new Domain(`[x]`), /Invalid domain AST/);
|
||||
assert.throws(() => new Domain(`[True]`), /Invalid domain AST/); // will possibly change with CHM work
|
||||
assert.throws(() => new Domain(`[(x.=, "=", 1)]`), /Invalid domain representation/);
|
||||
assert.throws(() => new Domain(`[(+, "=", 1)]`), /Invalid domain representation/);
|
||||
assert.throws(() => new Domain([{}]), /Invalid domain representation/);
|
||||
assert.throws(() => new Domain([1]), /Invalid domain representation/);
|
||||
});
|
||||
|
||||
QUnit.test("follow relations", function (assert) {
|
||||
|
|
@ -559,4 +606,50 @@ QUnit.module("domain", {}, () => {
|
|||
assert.throws(() => new Domain(`("&", "&", "|")`), /Invalid domain AST/);
|
||||
assert.throws(() => new Domain(`("&", "&", 3)`), /Invalid domain AST/);
|
||||
});
|
||||
|
||||
QUnit.module("RemoveDomainLeaf");
|
||||
|
||||
QUnit.test("Remove leaf in domain.", function (assert) {
|
||||
let domain = [
|
||||
["start_datetime", "!=", false],
|
||||
["end_datetime", "!=", false],
|
||||
["sale_line_id", "!=", false],
|
||||
];
|
||||
const keysToRemove = ["start_datetime", "end_datetime"];
|
||||
let newDomain = Domain.removeDomainLeaves(domain, keysToRemove);
|
||||
let expectedDomain = new Domain([
|
||||
"&",
|
||||
...Domain.TRUE.toList({}),
|
||||
...Domain.TRUE.toList({}),
|
||||
["sale_line_id", "!=", false],
|
||||
]);
|
||||
assert.deepEqual(newDomain.toList({}), expectedDomain.toList({}));
|
||||
domain = [
|
||||
"|",
|
||||
["role_id", "=", false],
|
||||
"&",
|
||||
["resource_id", "!=", false],
|
||||
["start_datetime", "=", false],
|
||||
["sale_line_id", "!=", false],
|
||||
];
|
||||
newDomain = Domain.removeDomainLeaves(domain, keysToRemove);
|
||||
expectedDomain = new Domain([
|
||||
"|",
|
||||
["role_id", "=", false],
|
||||
"&",
|
||||
["resource_id", "!=", false],
|
||||
...Domain.TRUE.toList({}),
|
||||
["sale_line_id", "!=", false],
|
||||
]);
|
||||
assert.deepEqual(newDomain.toList({}), expectedDomain.toList({}));
|
||||
domain = [
|
||||
"|",
|
||||
["start_datetime", "=", false],
|
||||
["end_datetime", "=", false],
|
||||
["sale_line_id", "!=", false],
|
||||
];
|
||||
newDomain = Domain.removeDomainLeaves(domain, keysToRemove);
|
||||
expectedDomain = new Domain([...Domain.TRUE.toList({}), ["sale_line_id", "!=", false]]);
|
||||
assert.deepEqual(newDomain.toList({}), expectedDomain.toList({}));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { AccordionItem } from "@web/core/dropdown/accordion_item";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
triggerHotkey,
|
||||
} from "../helpers/utils";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let env;
|
||||
let target;
|
||||
|
||||
QUnit.module("Components", ({ beforeEach }) => {
|
||||
beforeEach(async () => {
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("ui", uiService);
|
||||
target = getFixture();
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
clearTimeout: () => {},
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("Dropdown Accordion Item");
|
||||
|
||||
QUnit.test("accordion can be rendered", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`<AccordionItem description="'Test'" class="'text-primary'" selected="false"><h5>In accordion</h5></AccordionItem>`;
|
||||
Parent.components = { AccordionItem };
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_accordion").outerHTML,
|
||||
`<div class="o_accordion position-relative"><button class="o_menu_item o_accordion_toggle dropdown-item text-primary" tabindex="0" aria-expanded="false">Test</button></div>`
|
||||
);
|
||||
assert.containsOnce(target, "button.o_accordion_toggle");
|
||||
assert.containsNone(target, ".o_accordion_values");
|
||||
|
||||
await click(target, "button.o_accordion_toggle");
|
||||
assert.containsOnce(target, ".o_accordion_values");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_accordion_values").innerHTML,
|
||||
`<h5>In accordion</h5>`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("dropdown with accordion keyboard navigation", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`
|
||||
<Dropdown>
|
||||
<DropdownItem>item 1</DropdownItem>
|
||||
<AccordionItem description="'item 2'" selected="false">
|
||||
<DropdownItem>item 2-1</DropdownItem>
|
||||
<DropdownItem>item 2-2</DropdownItem>
|
||||
</AccordionItem>
|
||||
<DropdownItem>item 3</DropdownItem>
|
||||
</Dropdown>
|
||||
`;
|
||||
Parent.components = { Dropdown, DropdownItem, AccordionItem };
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
await click(target, ".o-dropdown .dropdown-toggle");
|
||||
|
||||
// Navigate with arrows
|
||||
assert.containsNone(
|
||||
target,
|
||||
".dropdown-menu > .focus",
|
||||
"menu should not have any active items"
|
||||
);
|
||||
|
||||
const scenarioSteps = [
|
||||
{ key: "arrowdown", expected: "item 1" },
|
||||
{ key: "arrowdown", expected: "item 2" },
|
||||
{ key: "arrowdown", expected: "item 3" },
|
||||
{ key: "arrowdown", expected: "item 1" },
|
||||
{ key: "tab", expected: "item 2" },
|
||||
{ key: "enter", expected: "item 2" },
|
||||
{ key: "tab", expected: "item 2-1" },
|
||||
{ key: "tab", expected: "item 2-2" },
|
||||
{ key: "tab", expected: "item 3" },
|
||||
{ key: "tab", expected: "item 1" },
|
||||
{ key: "arrowup", expected: "item 3" },
|
||||
{ key: "arrowup", expected: "item 2-2" },
|
||||
{ key: "arrowup", expected: "item 2-1" },
|
||||
{ key: "arrowup", expected: "item 2" },
|
||||
{ key: "enter", expected: "item 2" },
|
||||
{ key: "arrowup", expected: "item 1" },
|
||||
{ key: "shift+tab", expected: "item 3" },
|
||||
{ key: "shift+tab", expected: "item 2" },
|
||||
{ key: "shift+tab", expected: "item 1" },
|
||||
{ key: "end", expected: "item 3" },
|
||||
{ key: "home", expected: "item 1" },
|
||||
];
|
||||
|
||||
for (const step of scenarioSteps) {
|
||||
triggerHotkey(step.key);
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
target.querySelector(".dropdown-menu .focus").innerText,
|
||||
step.expected,
|
||||
`selected menu should be ${step.expected}`
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.activeElement.innerText,
|
||||
step.expected,
|
||||
`document.activeElement should be ${step.expected}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,14 +1,17 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { App, Component, onMounted, onPatched, useRef, useState, xml } from "@odoo/owl";
|
||||
import { templates } from "@web/core/assets";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { DateTimePicker } from "@web/core/datepicker/datepicker";
|
||||
import { DateTimeInput } from "@web/core/datetime/datetime_input";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { CheckboxItem } from "@web/core/dropdown/checkbox_item";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_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 { clearRegistryWithCleanup, makeTestEnv } from "../helpers/mock_env";
|
||||
import { makeFakeLocalizationService } from "../helpers/mock_services";
|
||||
import {
|
||||
click,
|
||||
|
|
@ -17,15 +20,18 @@ import {
|
|||
mockTimeout,
|
||||
mount,
|
||||
mouseEnter,
|
||||
mouseLeave,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
triggerHotkey,
|
||||
} from "../helpers/utils";
|
||||
import { makeParent } from "./tooltip/tooltip_service_tests";
|
||||
import { templates } from "@web/core/assets";
|
||||
import { getPickerCell } from "./datetime/datetime_test_helpers";
|
||||
import { datetimePickerService } from "@web/core/datetime/datetimepicker_service";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
|
||||
import { App, Component, onMounted, onPatched, useRef, useState, xml } from "@odoo/owl";
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let env;
|
||||
|
|
@ -52,7 +58,7 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
await mount(Parent, target, { env });
|
||||
assert.strictEqual(
|
||||
target.querySelector(".dropdown").outerHTML,
|
||||
'<div class="o-dropdown dropdown o-dropdown--no-caret"><button class="dropdown-toggle" tabindex="0" aria-expanded="false"></button></div>'
|
||||
'<div class="o-dropdown dropdown o-dropdown--no-caret"><button type="button" class="dropdown-toggle" tabindex="0" aria-expanded="false"></button></div>'
|
||||
);
|
||||
assert.containsOnce(target, "button.dropdown-toggle");
|
||||
assert.containsNone(target, ".dropdown-menu");
|
||||
|
|
@ -89,7 +95,7 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
patchWithCleanup(DropdownItem.prototype, {
|
||||
onClick(ev) {
|
||||
assert.ok(!ev.defaultPrevented);
|
||||
this._super(...arguments);
|
||||
super.onClick(...arguments);
|
||||
const href = ev.target.getAttribute("href");
|
||||
// defaultPrevented only if props.href is defined
|
||||
assert.ok(href !== null ? ev.defaultPrevented : !ev.defaultPrevented);
|
||||
|
|
@ -184,7 +190,7 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
patchWithCleanup(Dropdown.prototype, {
|
||||
close() {
|
||||
assert.step("dropdown will close");
|
||||
this._super();
|
||||
super.close();
|
||||
},
|
||||
});
|
||||
class Parent extends Component {
|
||||
|
|
@ -225,6 +231,74 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
assert.containsNone(target, ".dropdown-menu");
|
||||
});
|
||||
|
||||
QUnit.test("hold position on hover", async (assert) => {
|
||||
let parentState;
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ filler: false });
|
||||
parentState = this.state;
|
||||
}
|
||||
static template = xml`
|
||||
<div t-if="state.filler" class="filler" style="height: 100px;"/>
|
||||
<Dropdown holdOnHover="true">
|
||||
</Dropdown>
|
||||
`;
|
||||
static components = { Dropdown };
|
||||
}
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsNone(target, ".dropdown-menu");
|
||||
await click(target, "button.dropdown-toggle");
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
const menuBox1 = target.querySelector(".dropdown-menu").getBoundingClientRect();
|
||||
|
||||
// Pointer enter the dropdown menu
|
||||
await mouseEnter(target, ".dropdown-menu");
|
||||
|
||||
// Add a filler to the parent
|
||||
assert.containsNone(target, ".filler");
|
||||
parentState.filler = true;
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".filler");
|
||||
const menuBox2 = target.querySelector(".dropdown-menu").getBoundingClientRect();
|
||||
assert.strictEqual(menuBox2.top - menuBox1.top, 0);
|
||||
|
||||
// Pointer leave the dropdown menu
|
||||
await mouseLeave(target, ".dropdown-menu");
|
||||
const menuBox3 = target.querySelector(".dropdown-menu").getBoundingClientRect();
|
||||
assert.strictEqual(menuBox3.top - menuBox1.top, 100);
|
||||
});
|
||||
|
||||
QUnit.test("unlock position after close", async (assert) => {
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div style="margin-left: 200px;">
|
||||
<Dropdown holdOnHover="true" position="'bottom-end'">
|
||||
</Dropdown>
|
||||
</div>
|
||||
`;
|
||||
static components = { Dropdown };
|
||||
}
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsNone(target, ".dropdown-menu");
|
||||
await click(target, "button.dropdown-toggle");
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
const menuBox1 = target.querySelector(".dropdown-menu").getBoundingClientRect();
|
||||
|
||||
// Pointer enter the dropdown menu to lock the menu
|
||||
await mouseEnter(target, ".dropdown-menu");
|
||||
// close the menu
|
||||
await click(target);
|
||||
assert.containsNone(target, ".dropdown-menu");
|
||||
|
||||
// and reopen it
|
||||
await click(target, "button.dropdown-toggle");
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
const menuBox2 = target.querySelector(".dropdown-menu").getBoundingClientRect();
|
||||
assert.strictEqual(menuBox2.left - menuBox1.left, 0);
|
||||
});
|
||||
|
||||
QUnit.test("payload received on item selection", async (assert) => {
|
||||
class Parent extends Component {
|
||||
onItemSelected(value) {
|
||||
|
|
@ -560,13 +634,14 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
}
|
||||
);
|
||||
|
||||
QUnit.test("siblings dropdowns with manualOnly props", async (assert) => {
|
||||
assert.expect(7);
|
||||
QUnit.test("siblings dropdowns with autoOpen", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<Dropdown class="'one'" manualOnly="true"/>
|
||||
<Dropdown class="'two'" manualOnly="true"/>
|
||||
<Dropdown class="'one'" autoOpen="false"/>
|
||||
<Dropdown class="'two'" autoOpen="false"/>
|
||||
<Dropdown class="'three'"/>
|
||||
<Dropdown class="'four'"/>
|
||||
<div class="outside">OUTSIDE</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -576,22 +651,37 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
// Click on one
|
||||
await click(target, ".one button");
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
// Click on two
|
||||
await click(target, ".two button");
|
||||
assert.containsN(target, ".dropdown-menu", 2);
|
||||
// Click on one again
|
||||
await click(target, ".one button");
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
assert.containsNone(target.querySelector(".one"), ".dropdown-menu");
|
||||
// Hover on one
|
||||
const one = target.querySelector(".one");
|
||||
one.querySelector("button").dispatchEvent(new MouseEvent("mouseenter"));
|
||||
assert.containsOnce(target, ".one .dropdown-menu");
|
||||
// Hover on two
|
||||
const two = target.querySelector(".two");
|
||||
two.querySelector("button").dispatchEvent(new MouseEvent("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
assert.containsNone(target.querySelector(".one"), ".dropdown-menu");
|
||||
assert.containsOnce(target, ".one .dropdown-menu");
|
||||
// Hover on three
|
||||
const three = target.querySelector(".three");
|
||||
three.querySelector("button").dispatchEvent(new MouseEvent("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
assert.containsOnce(target, ".one .dropdown-menu");
|
||||
// Click outside
|
||||
await click(target, "div.outside");
|
||||
assert.containsNone(target, ".dropdown-menu");
|
||||
// Click on three
|
||||
await click(target, ".three button");
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
assert.containsOnce(target, ".three .dropdown-menu");
|
||||
// Hover on two
|
||||
two.querySelector("button").dispatchEvent(new MouseEvent("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
assert.containsOnce(target, ".three .dropdown-menu");
|
||||
// Hover on four
|
||||
const four = target.querySelector(".four");
|
||||
four.querySelector("button").dispatchEvent(new MouseEvent("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
assert.containsOnce(target, ".four .dropdown-menu");
|
||||
});
|
||||
|
||||
QUnit.test("siblings dropdowns: toggler focused on mouseenter", async (assert) => {
|
||||
|
|
@ -737,9 +827,6 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
// Define the ArrowDown key with standard API (for hotkey_service)
|
||||
key: "ArrowDown",
|
||||
code: "ArrowDown",
|
||||
// Define the ArrowDown key with deprecated API (for bootstrap)
|
||||
keyCode: 40,
|
||||
which: 40,
|
||||
});
|
||||
select.dispatchEvent(ev);
|
||||
await nextTick();
|
||||
|
|
@ -749,9 +836,6 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
// Define the ESC key with standard API (for hotkey_service)
|
||||
key: "Escape",
|
||||
code: "Escape",
|
||||
// Define the ESC key with deprecated API (for bootstrap)
|
||||
keyCode: 27,
|
||||
which: 27,
|
||||
});
|
||||
select.dispatchEvent(ev);
|
||||
await nextTick();
|
||||
|
|
@ -980,7 +1064,7 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
assert.expect(9);
|
||||
patchWithCleanup(Dropdown.prototype, {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
super.setup(...arguments);
|
||||
const isSubmenu = Boolean(this.parentDropdown);
|
||||
if (isSubmenu) {
|
||||
onMounted(() => {
|
||||
|
|
@ -1064,6 +1148,46 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
);
|
||||
});
|
||||
|
||||
QUnit.test("caret should be repositioned to default direction when closed", async (assert) => {
|
||||
class Parent extends Component {
|
||||
static components = { Dropdown };
|
||||
static template = xml`
|
||||
<div style="height: 384px;"/> <!-- filler: takes half the runbot's browser_size -->
|
||||
<Dropdown showCaret="true">
|
||||
<t t-set-slot="toggler">🍋</t>
|
||||
<div style="height: 400px; width: 50px;"/> <!-- menu filler -->
|
||||
</Dropdown>
|
||||
`;
|
||||
}
|
||||
// The fixture should be shown for this test, as the positioning container is the html node
|
||||
target.style.position = "fixed";
|
||||
target.style.top = "0";
|
||||
target.style.left = "0";
|
||||
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
const dropdown = target.querySelector(".o-dropdown");
|
||||
assert.doesNotHaveClass(dropdown, "show");
|
||||
assert.hasClass(dropdown, "dropdown");
|
||||
|
||||
// open
|
||||
await click(target, ".dropdown-toggle");
|
||||
await nextTick(); // awaits for the caret to get patched
|
||||
assert.hasClass(dropdown, "show");
|
||||
assert.hasClass(dropdown, "dropend");
|
||||
|
||||
// close
|
||||
await click(target, ".dropdown-toggle");
|
||||
assert.doesNotHaveClass(dropdown, "show");
|
||||
assert.hasClass(dropdown, "dropdown");
|
||||
|
||||
// open
|
||||
await click(target, ".dropdown-toggle");
|
||||
await nextTick(); // awaits for the caret to get patched
|
||||
assert.hasClass(dropdown, "show");
|
||||
assert.hasClass(dropdown, "dropend");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"multi-level dropdown: mouseentering a dropdown item should close any subdropdown",
|
||||
async (assert) => {
|
||||
|
|
@ -1109,7 +1233,7 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
let hotkeyRegistrationsCount = 0;
|
||||
patchWithCleanup(env.services.hotkey, {
|
||||
add() {
|
||||
const remove = this._super(...arguments);
|
||||
const remove = super.add(...arguments);
|
||||
hotkeyRegistrationsCount += 1;
|
||||
return () => {
|
||||
remove();
|
||||
|
|
@ -1174,8 +1298,8 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
QUnit.test("Dropdown with a tooltip", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
class MyComponent extends owl.Component {}
|
||||
MyComponent.template = owl.xml`
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`
|
||||
<Dropdown tooltip="'My tooltip'">
|
||||
<DropdownItem/>
|
||||
</Dropdown>`;
|
||||
|
|
@ -1190,16 +1314,17 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
QUnit.test(
|
||||
"Dropdown with a date picker inside do not close when a click occurs in date picker",
|
||||
async (assert) => {
|
||||
class MyComponent extends owl.Component {}
|
||||
MyComponent.template = owl.xml`
|
||||
registry.category("services").add("datetime_picker", datetimePickerService);
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`
|
||||
<Dropdown>
|
||||
<t t-set-slot="toggler">
|
||||
Dropdown toggler
|
||||
</t>
|
||||
<DateTimePicker onDateTimeChanged="() => {}" date="false"/>
|
||||
<DateTimeInput />
|
||||
</Dropdown>
|
||||
`;
|
||||
MyComponent.components = { Dropdown, DateTimePicker };
|
||||
MyComponent.components = { DateTimeInput, Dropdown };
|
||||
|
||||
await makeParent(MyComponent);
|
||||
|
||||
|
|
@ -1208,20 +1333,205 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
await click(target, ".dropdown-toggle");
|
||||
|
||||
assert.containsOnce(target, ".o-dropdown--menu");
|
||||
assert.containsNone(document.body, ".bootstrap-datetimepicker-widget");
|
||||
assert.strictEqual(target.querySelector(".o_datepicker_input").value, "");
|
||||
assert.containsNone(target, ".o_datetime_picker");
|
||||
assert.strictEqual(target.querySelector(".o_datetime_input").value, "");
|
||||
|
||||
await click(target, ".o_datepicker_input");
|
||||
await click(target, ".o_datetime_input");
|
||||
|
||||
assert.containsOnce(target, ".o-dropdown--menu");
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
assert.strictEqual(target.querySelector(".o_datepicker_input").value, "");
|
||||
assert.containsOnce(target, ".o_datetime_picker");
|
||||
assert.strictEqual(target.querySelector(".o_datetime_input").value, "");
|
||||
|
||||
await click(document.querySelectorAll(".datepicker table td")[15]); // select some day
|
||||
await click(getPickerCell("15")); // select some day
|
||||
|
||||
assert.containsOnce(target, ".o-dropdown--menu");
|
||||
assert.containsOnce(document.body, ".bootstrap-datetimepicker-widget");
|
||||
assert.notOk(target.querySelector(".o_datepicker_input").value === "");
|
||||
assert.containsOnce(target, ".o_datetime_picker");
|
||||
assert.notOk(target.querySelector(".o_datetime_input").value === "");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("onOpened callback props called after the menu has been mounted", async (assert) => {
|
||||
const beforeOpenProm = makeDeferred();
|
||||
class Parent extends Component {
|
||||
beforeOpenCallback() {
|
||||
assert.step("beforeOpened");
|
||||
return beforeOpenProm;
|
||||
}
|
||||
onOpenedCallback() {
|
||||
assert.step("onOpened");
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<Dropdown onOpened.bind="onOpenedCallback" beforeOpen.bind="beforeOpenCallback" />
|
||||
`;
|
||||
Parent.components = { Dropdown, DropdownItem };
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
await click(target, "button.dropdown-toggle");
|
||||
assert.verifySteps(["beforeOpened"]);
|
||||
beforeOpenProm.resolve();
|
||||
await nextTick();
|
||||
assert.verifySteps(["onOpened"]);
|
||||
});
|
||||
|
||||
QUnit.test("dropdown button can be disabled", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`<Dropdown disabled="true"/>`;
|
||||
Parent.components = { Dropdown };
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
assert.strictEqual(
|
||||
target.querySelector(".dropdown").outerHTML,
|
||||
'<div class="o-dropdown dropdown o-dropdown--no-caret"><button type="button" class="dropdown-toggle" disabled="" tabindex="0" aria-expanded="false"></button></div>'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Dropdown with CheckboxItem: toggle value", async (assert) => {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ checked: false });
|
||||
}
|
||||
onSelected() {
|
||||
this.state.checked = !this.state.checked;
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<Dropdown>
|
||||
<t t-set-slot="toggler">Click to open</t>
|
||||
<CheckboxItem
|
||||
class="{ selected: state.checked }"
|
||||
checked="state.checked"
|
||||
parentClosingMode="'none'"
|
||||
onSelected.bind="onSelected">
|
||||
My checkbox item
|
||||
</CheckboxItem>
|
||||
</Dropdown>`;
|
||||
Parent.components = { Dropdown, CheckboxItem };
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
await click(target, ".dropdown-toggle");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".dropdown-item").outerHTML,
|
||||
`<span class="dropdown-item" role="menuitemcheckbox" tabindex="0" aria-checked="false"> My checkbox item </span>`
|
||||
);
|
||||
await click(target, ".dropdown-item");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".dropdown-item").outerHTML,
|
||||
`<span class="dropdown-item selected" role="menuitemcheckbox" tabindex="0" aria-checked="true"> My checkbox item </span>`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("don't close dropdown outside the active element", async (assert) => {
|
||||
// This test checks that if a dropdown element opens a dialog with a dropdown inside,
|
||||
// opening this dropdown will not close the first dropdown.
|
||||
class CustomDialog extends Component {}
|
||||
CustomDialog.template = xml`
|
||||
<Dialog title="'Welcome'">
|
||||
<Dropdown>
|
||||
<DropdownItem>Item</DropdownItem>
|
||||
</Dropdown>
|
||||
<div class="outside_dialog">Outside Dialog</div>
|
||||
</Dialog>`;
|
||||
CustomDialog.components = { Dialog, Dropdown, DropdownItem };
|
||||
|
||||
const mainComponentRegistry = registry.category("main_components");
|
||||
clearRegistryWithCleanup(mainComponentRegistry);
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
serviceRegistry.add("l10n", makeFakeLocalizationService());
|
||||
|
||||
class PseudoWebClient extends Component {
|
||||
setup() {
|
||||
this.Components = mainComponentRegistry.getEntries();
|
||||
}
|
||||
clicked() {
|
||||
env.services.dialog.add(CustomDialog);
|
||||
}
|
||||
}
|
||||
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>
|
||||
<Dropdown>
|
||||
<button class="click-me" t-on-click="clicked">Click me</button>
|
||||
</Dropdown>
|
||||
<div class="outside_parent">Outside Parent</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
PseudoWebClient.components = { Dropdown };
|
||||
|
||||
env = await makeTestEnv();
|
||||
await mount(PseudoWebClient, target, { env });
|
||||
await click(target, "button.dropdown-toggle");
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
await click(target, "button.click-me");
|
||||
assert.containsOnce(target, ".modal-dialog");
|
||||
await click(target, ".modal-dialog button.dropdown-toggle");
|
||||
assert.containsN(target, ".dropdown-menu", 2);
|
||||
await click(target, ".outside_dialog");
|
||||
assert.containsOnce(target, ".modal-dialog");
|
||||
assert.containsN(target, ".dropdown-menu", 1);
|
||||
await click(target, ".modal-dialog .btn-primary");
|
||||
assert.containsNone(target, ".modal-dialog");
|
||||
assert.containsN(target, ".dropdown-menu", 1);
|
||||
await click(target, ".outside_parent");
|
||||
assert.containsNone(target, ".dropdown-menu");
|
||||
});
|
||||
|
||||
QUnit.test("multi-level dropdown is well positioned", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.template = xml`
|
||||
<Dropdown class="'parent'" menuClass="'border-0'">
|
||||
<DropdownItem>A</DropdownItem>
|
||||
<Dropdown class="'first'" menuClass="'border-0'">
|
||||
<t t-set-slot="toggler">
|
||||
B
|
||||
</t>
|
||||
<DropdownItem>B A</DropdownItem>
|
||||
<DropdownItem>B C</DropdownItem>
|
||||
</Dropdown>
|
||||
<Dropdown class="'second'" menuClass="'border-0'">
|
||||
<t t-set-slot="toggler">
|
||||
C
|
||||
</t>
|
||||
<DropdownItem>C A</DropdownItem>
|
||||
<DropdownItem>C B</DropdownItem>
|
||||
</Dropdown>
|
||||
</Dropdown>
|
||||
`;
|
||||
Parent.components = { Dropdown, DropdownItem };
|
||||
env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
await click(target, "button.dropdown-toggle:last-child");
|
||||
const parentMenu = target.querySelector(".parent .dropdown-menu");
|
||||
const parentMenuBox = parentMenu.getBoundingClientRect();
|
||||
|
||||
// Hover first sub-dropdown
|
||||
await mouseEnter(target, ".o-dropdown.first > button");
|
||||
await nextTick();
|
||||
let currentDropdown = target.querySelector(".first .dropdown-menu");
|
||||
let currentDropdownBox = currentDropdown.getBoundingClientRect();
|
||||
//Check X position of the first subdropdown, should not overlap parent dropdown
|
||||
assert.ok(currentDropdownBox.x >= parentMenuBox.x + parentMenuBox.width);
|
||||
//Check Y position of the first subdropdown, should not overlap parent dropdown
|
||||
let parentTogglerBox = parentMenu.querySelector(".show").getBoundingClientRect();
|
||||
let currentTogglerBox = currentDropdown.querySelector(".focus").getBoundingClientRect();
|
||||
assert.ok(Math.abs(parentTogglerBox.y - currentTogglerBox.y) < 5);
|
||||
|
||||
// Hover second sub-dropdown
|
||||
await mouseEnter(target, ".o-dropdown.second > button");
|
||||
await nextTick();
|
||||
currentDropdown = target.querySelector(".second .dropdown-menu");
|
||||
currentDropdownBox = currentDropdown.getBoundingClientRect();
|
||||
//Check X position of the second subdropdown, should not overlap parent dropdown
|
||||
assert.ok(currentDropdownBox.x >= parentMenuBox.x + parentMenuBox.width);
|
||||
//Check Y position of the first subdropdown, should not overlap parent dropdown
|
||||
parentTogglerBox = parentMenu.querySelector(".show").getBoundingClientRect();
|
||||
currentTogglerBox = currentDropdown.querySelector(".focus").getBoundingClientRect();
|
||||
assert.ok(Math.abs(parentTogglerBox.y - currentTogglerBox.y) < 5);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,27 +17,14 @@ import {
|
|||
} from "../../helpers/utils";
|
||||
|
||||
import { Component, markup, xml } from "@odoo/owl";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
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 });
|
||||
const parent = await mount(MainComponentsContainer, target, { env });
|
||||
return parent;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
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 { makeDialogTestEnv } from "../../helpers/mock_env";
|
||||
import { makeFakeDialogService, makeFakeLocalizationService } from "../../helpers/mock_services";
|
||||
import { click, getFixture, mount, nextTick, patchWithCleanup } from "../../helpers/utils";
|
||||
|
||||
|
|
@ -20,15 +20,6 @@ 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();
|
||||
|
|
@ -58,26 +49,26 @@ QUnit.test("ErrorDialog with traceback", async (assert) => {
|
|||
const mainButtons = target.querySelectorAll("main button");
|
||||
assert.deepEqual(
|
||||
[...mainButtons].map((el) => el.textContent),
|
||||
["Copy the full error to clipboard", "See details"]
|
||||
["See details"]
|
||||
);
|
||||
const footerButtons = target.querySelectorAll("footer button");
|
||||
assert.deepEqual(
|
||||
[...footerButtons].map((el) => el.textContent),
|
||||
["Close", "Copy error to clipboard"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("main .clearfix p")].map((el) => el.textContent),
|
||||
[...target.querySelector("main p").childNodes].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]);
|
||||
click(mainButtons[0]);
|
||||
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",
|
||||
]
|
||||
["Something bad happened"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("main .clearfix code")].map((el) => el.textContent),
|
||||
|
|
@ -112,26 +103,26 @@ QUnit.test("Client ErrorDialog with traceback", async (assert) => {
|
|||
const mainButtons = target.querySelectorAll("main button");
|
||||
assert.deepEqual(
|
||||
[...mainButtons].map((el) => el.textContent),
|
||||
["Copy the full error to clipboard", "See details"]
|
||||
["See details"]
|
||||
);
|
||||
const footerButtons = target.querySelectorAll("footer button");
|
||||
assert.deepEqual(
|
||||
[...footerButtons].map((el) => el.textContent),
|
||||
["Close", "Copy error to clipboard"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("main .clearfix p")].map((el) => el.textContent),
|
||||
[...target.querySelector("main p").childNodes].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]);
|
||||
click(mainButtons[0]);
|
||||
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",
|
||||
]
|
||||
["Something bad happened"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll("main .clearfix code")].map((el) => el.textContent),
|
||||
|
|
@ -191,10 +182,10 @@ QUnit.test("WarningDialog", async (assert) => {
|
|||
},
|
||||
});
|
||||
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("header .modal-title").textContent, "Invalid Operation");
|
||||
assert.containsOnce(target, ".o_error_dialog");
|
||||
assert.strictEqual(target.querySelector("main").textContent, "Some strange unreadable message");
|
||||
assert.strictEqual(target.querySelector(".o_dialog footer button").textContent, "Ok");
|
||||
assert.strictEqual(target.querySelector(".o_dialog footer button").textContent, "Close");
|
||||
});
|
||||
|
||||
QUnit.test("RedirectWarningDialog", async (assert) => {
|
||||
|
|
@ -233,7 +224,7 @@ QUnit.test("RedirectWarningDialog", async (assert) => {
|
|||
const footerButtons = target.querySelectorAll("footer button");
|
||||
assert.deepEqual(
|
||||
[...footerButtons].map((el) => el.textContent),
|
||||
["Buy book on cryptography", "Cancel"]
|
||||
["Buy book on cryptography", "Close"]
|
||||
);
|
||||
await click(footerButtons[0]); // click on "Buy book on cryptography"
|
||||
assert.verifySteps(["buy_action_id", "dialog-closed"]);
|
||||
|
|
@ -253,7 +244,7 @@ QUnit.test("Error504Dialog", async (assert) => {
|
|||
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");
|
||||
assert.strictEqual(target.querySelector(".o_dialog footer button").textContent, "Close");
|
||||
});
|
||||
|
||||
QUnit.test("SessionExpiredDialog", async (assert) => {
|
||||
|
|
@ -278,7 +269,7 @@ QUnit.test("SessionExpiredDialog", async (assert) => {
|
|||
" 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");
|
||||
assert.strictEqual(footerButton.textContent, "Close");
|
||||
click(footerButton);
|
||||
assert.verifySteps(["location reload"]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,10 +2,15 @@
|
|||
|
||||
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 {
|
||||
ClientErrorDialog,
|
||||
RPCErrorDialog,
|
||||
standardErrorDialogProps,
|
||||
} 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 { overlayService } from "@web/core/overlay/overlay_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
|
|
@ -17,17 +22,21 @@ import {
|
|||
makeFakeRPCService,
|
||||
} from "../../helpers/mock_services";
|
||||
import { getFixture, makeDeferred, mount, nextTick, patchWithCleanup } from "../../helpers/utils";
|
||||
import { omit } from "@web/core/utils/objects";
|
||||
|
||||
import { Component, xml, onError, OwlError, onWillStart } from "@odoo/owl";
|
||||
import { defaultHandler } from "@web/core/errors/error_handlers";
|
||||
const errorDialogRegistry = registry.category("error_dialogs");
|
||||
const errorHandlerRegistry = registry.category("error_handlers");
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let errorCb;
|
||||
let unhandledRejectionCb;
|
||||
let preventDefault;
|
||||
|
||||
QUnit.module("Error Service", {
|
||||
async beforeEach() {
|
||||
serviceRegistry.add("overlay", overlayService);
|
||||
serviceRegistry.add("error", errorService);
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
serviceRegistry.add("notification", notificationService);
|
||||
|
|
@ -35,16 +44,17 @@ QUnit.module("Error Service", {
|
|||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
serviceRegistry.add("ui", uiService);
|
||||
const windowAddEventListener = browser.addEventListener;
|
||||
preventDefault = Event.prototype.preventDefault;
|
||||
browser.addEventListener = (type, cb) => {
|
||||
if (type === "unhandledrejection") {
|
||||
unhandledRejectionCb = (ev) => {
|
||||
ev.preventDefault();
|
||||
preventDefault.call(ev);
|
||||
cb(ev);
|
||||
};
|
||||
}
|
||||
if (type === "error") {
|
||||
errorCb = (ev) => {
|
||||
ev.preventDefault();
|
||||
preventDefault.call(ev);
|
||||
cb(ev);
|
||||
};
|
||||
}
|
||||
|
|
@ -83,7 +93,7 @@ QUnit.test("handle RPC_ERROR of type='server' and no associated dialog class", a
|
|||
error.subType = "strange_error";
|
||||
function addDialog(dialogClass, props) {
|
||||
assert.strictEqual(dialogClass, RPCErrorDialog);
|
||||
assert.deepEqual(_.omit(props, "traceback"), {
|
||||
assert.deepEqual(omit(props, "traceback"), {
|
||||
name: "RPC_ERROR",
|
||||
type: "server",
|
||||
code: 701,
|
||||
|
|
@ -94,8 +104,8 @@ QUnit.test("handle RPC_ERROR of type='server' and no associated dialog class", a
|
|||
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);
|
||||
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();
|
||||
|
|
@ -114,6 +124,7 @@ QUnit.test(
|
|||
class CustomDialog extends Component {}
|
||||
CustomDialog.template = xml`<RPCErrorDialog title="'Strange Error'"/>`;
|
||||
CustomDialog.components = { RPCErrorDialog };
|
||||
CustomDialog.props = { ...standardErrorDialogProps };
|
||||
const error = new RPCError();
|
||||
error.code = 701;
|
||||
error.message = "Some strange error occured";
|
||||
|
|
@ -124,7 +135,7 @@ QUnit.test(
|
|||
error.data = errorData;
|
||||
function addDialog(dialogClass, props) {
|
||||
assert.strictEqual(dialogClass, CustomDialog);
|
||||
assert.deepEqual(_.omit(props, "traceback"), {
|
||||
assert.deepEqual(omit(props, "traceback"), {
|
||||
name: "RPC_ERROR",
|
||||
type: "server",
|
||||
code: 701,
|
||||
|
|
@ -133,8 +144,8 @@ QUnit.test(
|
|||
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);
|
||||
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();
|
||||
|
|
@ -168,7 +179,7 @@ QUnit.test(
|
|||
error.data = errorData;
|
||||
function addDialog(dialogClass, props) {
|
||||
assert.strictEqual(dialogClass, NormalDialog);
|
||||
assert.deepEqual(_.omit(props, "traceback"), {
|
||||
assert.deepEqual(omit(props, "traceback"), {
|
||||
name: "RPC_ERROR",
|
||||
type: "server",
|
||||
code: 701,
|
||||
|
|
@ -177,8 +188,8 @@ QUnit.test(
|
|||
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);
|
||||
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();
|
||||
|
|
@ -222,7 +233,7 @@ QUnit.test("handle CONNECTION_LOST_ERROR", async (assert) => {
|
|||
}
|
||||
};
|
||||
await makeTestEnv({ mockRPC });
|
||||
const error = new ConnectionLostError();
|
||||
const error = new ConnectionLostError("/fake_url");
|
||||
const errorEvent = new PromiseRejectionEvent("error", {
|
||||
reason: error,
|
||||
promise: null,
|
||||
|
|
@ -246,6 +257,7 @@ QUnit.test("will let handlers from the registry handle errors first", async (ass
|
|||
assert.strictEqual(originalError, error);
|
||||
assert.strictEqual(env.someValue, 14);
|
||||
assert.step("in handler");
|
||||
return true;
|
||||
});
|
||||
const testEnv = await makeTestEnv();
|
||||
testEnv.someValue = 14;
|
||||
|
|
@ -266,6 +278,7 @@ QUnit.test("originalError is the root cause of the error chain", async (assert)
|
|||
assert.ok(err.cause instanceof OwlError); // Wrapped by owl
|
||||
assert.strictEqual(err.cause.cause, originalError); // original error
|
||||
assert.step("in handler");
|
||||
return true;
|
||||
});
|
||||
const testEnv = await makeTestEnv();
|
||||
testEnv.someValue = 14;
|
||||
|
|
@ -313,6 +326,7 @@ QUnit.test("originalError is the root cause of the error chain", async (assert)
|
|||
});
|
||||
|
||||
QUnit.test("handle uncaught promise errors", async (assert) => {
|
||||
assert.expectErrors();
|
||||
class TestError extends Error {}
|
||||
const error = new TestError();
|
||||
error.message = "This is an error test";
|
||||
|
|
@ -320,12 +334,12 @@ QUnit.test("handle uncaught promise errors", async (assert) => {
|
|||
|
||||
function addDialog(dialogClass, props) {
|
||||
assert.strictEqual(dialogClass, ClientErrorDialog);
|
||||
assert.deepEqual(_.omit(props, "traceback"), {
|
||||
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);
|
||||
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();
|
||||
|
|
@ -336,9 +350,11 @@ QUnit.test("handle uncaught promise errors", async (assert) => {
|
|||
cancelable: true,
|
||||
});
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
assert.verifyErrors(["This is an error test"]);
|
||||
});
|
||||
|
||||
QUnit.test("handle uncaught client errors", async (assert) => {
|
||||
assert.expectErrors();
|
||||
class TestError extends Error {}
|
||||
const error = new TestError();
|
||||
error.message = "This is an error test";
|
||||
|
|
@ -360,6 +376,7 @@ QUnit.test("handle uncaught client errors", async (assert) => {
|
|||
cancelable: true,
|
||||
});
|
||||
await errorCb(errorEvent);
|
||||
assert.verifyErrors(["This is an error test"]);
|
||||
});
|
||||
|
||||
QUnit.test("don't show dialog for errors in third-party scripts", async (assert) => {
|
||||
|
|
@ -382,6 +399,7 @@ QUnit.test("don't show dialog for errors in third-party scripts", async (assert)
|
|||
});
|
||||
|
||||
QUnit.test("show dialog for errors in third-party scripts in debug mode", async (assert) => {
|
||||
assert.expectErrors();
|
||||
class TestError extends Error {}
|
||||
const error = new TestError();
|
||||
error.message = "Script error.";
|
||||
|
|
@ -394,55 +412,19 @@ QUnit.test("show dialog for errors in third-party scripts in debug mode", async
|
|||
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
|
||||
// Error events from errors in third-party scripts have 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"]);
|
||||
assert.verifyErrors(["Script error."]);
|
||||
});
|
||||
|
||||
QUnit.test("lazy loaded handlers", async (assert) => {
|
||||
assert.expectErrors();
|
||||
await makeTestEnv();
|
||||
const errorEvent = new PromiseRejectionEvent("error", {
|
||||
reason: new Error(),
|
||||
reason: new Error("error"),
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
|
|
@ -452,10 +434,12 @@ QUnit.test("lazy loaded handlers", async (assert) => {
|
|||
|
||||
errorHandlerRegistry.add("__test_handler__", () => {
|
||||
assert.step("in handler");
|
||||
return true;
|
||||
});
|
||||
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
assert.verifySteps(["in handler"]);
|
||||
assert.verifyErrors(["error"]); // for the first throw, before registering the handler
|
||||
});
|
||||
|
||||
// The following test(s) do not want the preventDefault to be done automatically.
|
||||
|
|
@ -467,6 +451,10 @@ QUnit.module("Error Service", {
|
|||
serviceRegistry.add("rpc", makeFakeRPCService());
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
serviceRegistry.add("ui", uiService);
|
||||
// remove the override of the defaultHandler done in qunit.js
|
||||
registry
|
||||
.category("error_handlers")
|
||||
.add("defaultHandler", defaultHandler, { sequence: 100, force: true });
|
||||
const windowAddEventListener = browser.addEventListener;
|
||||
browser.addEventListener = (type, cb) => {
|
||||
if (type === "unhandledrejection") {
|
||||
|
|
@ -539,3 +527,51 @@ QUnit.test("logs the traceback of the full error chain for uncaughterror", async
|
|||
await errorCb(errorEvent);
|
||||
assert.ok(errorEvent.defaultPrevented);
|
||||
});
|
||||
|
||||
QUnit.test("error in handlers while handling an error", async (assert) => {
|
||||
// Scenario: an error occurs at the early stage of the "boot" sequence, error handlers
|
||||
// that are supposed to spawn dialogs are not ready then and will crash.
|
||||
// We assert that *exactly one* error message is logged, that contains the original error's traceback
|
||||
// and an indication that a handler has crashed just for not loosing information.
|
||||
// The crash of the error handler should merely be seen as a consequence of the early stage at which the error occurs.
|
||||
errorHandlerRegistry.add(
|
||||
"__test_handler__",
|
||||
(env, err, originalError) => {
|
||||
throw new Error("Boom in handler");
|
||||
},
|
||||
{ sequence: 0 }
|
||||
);
|
||||
// We want to assert that the error_service code does the preventDefault.
|
||||
preventDefault = () => {};
|
||||
patchWithCleanup(console, {
|
||||
error(errorMessage) {
|
||||
assert.ok(
|
||||
errorMessage.startsWith(
|
||||
`@web/core/error_service: handler "__test_handler__" failed with "Error: Boom in handler" while trying to handle:\nError: Genuine Business Boom`
|
||||
)
|
||||
);
|
||||
assert.step("error logged");
|
||||
},
|
||||
});
|
||||
|
||||
await makeTestEnv();
|
||||
let errorEvent = new Event("error", {
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
errorEvent.error = new Error("Genuine Business Boom");
|
||||
errorEvent.error.annotatedTraceback = "annotated";
|
||||
errorEvent.filename = "dummy_file.js"; // needed to not be treated as a CORS error
|
||||
await errorCb(errorEvent);
|
||||
assert.ok(errorEvent.defaultPrevented);
|
||||
assert.verifySteps(["error logged"]);
|
||||
|
||||
errorEvent = new PromiseRejectionEvent("unhandledrejection", {
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
reason: new Error("Genuine Business Boom"),
|
||||
});
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
assert.ok(errorEvent.defaultPrevented);
|
||||
assert.verifySteps(["error logged"]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { ExpressionEditorDialog } from "@web/core/expression_editor_dialog/expression_editor_dialog";
|
||||
import { makeDialogTestEnv } from "../helpers/mock_env";
|
||||
import { mount, getFixture, nextTick, click } from "../helpers/utils";
|
||||
import {
|
||||
getTreeEditorContent,
|
||||
makeServerData,
|
||||
setupConditionTreeEditorServices,
|
||||
} from "./condition_tree_editor_helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
/**
|
||||
* @typedef {Record<keyof DomainSelectorDialog.props, any>} Props
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Partial<Props> & { mockRPC: Function }} [params]
|
||||
*/
|
||||
async function makeExpressionEditorDialog(params = {}) {
|
||||
const props = { ...params };
|
||||
const mockRPC = props.mockRPC;
|
||||
delete props.mockRPC;
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { ExpressionEditorDialog };
|
||||
static template = xml`<ExpressionEditorDialog t-props="expressionEditorProps"/>`;
|
||||
setup() {
|
||||
this.expressionEditorProps = {
|
||||
resModel: "partner",
|
||||
expression: "1",
|
||||
close: () => {},
|
||||
onConfirm: () => {},
|
||||
...props,
|
||||
};
|
||||
this.expressionEditorProps.fields =
|
||||
this.expressionEditorProps.fields ||
|
||||
serverData.models[this.expressionEditorProps.resModel]?.fields ||
|
||||
{};
|
||||
Object.entries(this.expressionEditorProps.fields).forEach(([fieldName, field]) => {
|
||||
field.name = fieldName;
|
||||
});
|
||||
}
|
||||
async set(expression) {
|
||||
this.expressionEditorProps.expression = expression;
|
||||
this.render();
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
|
||||
const env = await makeDialogTestEnv({ serverData, mockRPC });
|
||||
return mount(Parent, target, { env, props });
|
||||
}
|
||||
|
||||
/** @type {Element} */
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
QUnit.module("Components", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
serverData = makeServerData();
|
||||
setupConditionTreeEditorServices();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("ExpressionEditorDialog");
|
||||
|
||||
QUnit.test("expr well sent, onConfirm and onClose", async (assert) => {
|
||||
const expression = `foo == 'batestr' and bar == True`;
|
||||
await makeExpressionEditorDialog({
|
||||
expression,
|
||||
close: () => {
|
||||
assert.step("close");
|
||||
},
|
||||
onConfirm: (result) => {
|
||||
assert.step(result);
|
||||
},
|
||||
});
|
||||
assert.containsOnce(target, ".o_technical_modal");
|
||||
const confirmButton = target.querySelector(".o_dialog footer button");
|
||||
await click(confirmButton);
|
||||
assert.verifySteps([expression, "close"]);
|
||||
});
|
||||
|
||||
QUnit.test("expr well sent but wrong, so notification when onConfirm", async (assert) => {
|
||||
const expression = `foo == 'bar' and bar = True`;
|
||||
registry.category("services").add(
|
||||
"notification",
|
||||
{
|
||||
start() {
|
||||
return {
|
||||
add(message, options) {
|
||||
assert.strictEqual(message, "Expression is invalid. Please correct it");
|
||||
assert.deepEqual(options, { type: "danger" });
|
||||
assert.step("notification");
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{ force: true }
|
||||
);
|
||||
await makeExpressionEditorDialog({
|
||||
expression,
|
||||
});
|
||||
assert.containsOnce(target, ".o_technical_modal");
|
||||
const confirmButton = target.querySelector(".modal-footer button");
|
||||
const resetButton = target.querySelector(".modal-body button");
|
||||
await click(confirmButton);
|
||||
await click(resetButton);
|
||||
assert.deepEqual(getTreeEditorContent(target), [{ level: 0, value: "all" }]);
|
||||
assert.verifySteps(["notification"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,490 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
getFixture,
|
||||
getNodesTextContent,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "../helpers/utils";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { ExpressionEditor } from "@web/core/expression_editor/expression_editor";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import {
|
||||
SELECTORS as treeEditorSELECTORS,
|
||||
clickOnButtonAddBranch,
|
||||
clickOnButtonAddNewRule,
|
||||
clickOnButtonDeleteNode,
|
||||
editValue,
|
||||
get,
|
||||
getTreeEditorContent,
|
||||
makeServerData,
|
||||
selectOperator,
|
||||
setupConditionTreeEditorServices,
|
||||
getOperatorOptions,
|
||||
getValueOptions,
|
||||
isNotSupportedPath,
|
||||
clearNotSupported,
|
||||
} from "./condition_tree_editor_helpers";
|
||||
import { openModelFieldSelectorPopover } from "./model_field_selector_tests";
|
||||
|
||||
export {
|
||||
clickOnButtonAddBranch,
|
||||
clickOnButtonAddNewRule,
|
||||
clickOnButtonDeleteNode,
|
||||
editValue,
|
||||
get,
|
||||
getTreeEditorContent,
|
||||
makeServerData,
|
||||
selectOperator,
|
||||
setupConditionTreeEditorServices,
|
||||
isNotSupportedPath,
|
||||
clearNotSupported,
|
||||
} from "./condition_tree_editor_helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
export const SELECTORS = {
|
||||
...treeEditorSELECTORS,
|
||||
debugArea: ".o_expression_editor_debug_container textarea",
|
||||
};
|
||||
|
||||
async function editExpression(target, value, index = 0) {
|
||||
const input = get(target, SELECTORS.complexConditionInput, index);
|
||||
await editInput(input, null, value);
|
||||
}
|
||||
|
||||
async function selectConnector(target, value, index = 0) {
|
||||
const toggler = get(target, `${SELECTORS.connector} .dropdown-toggle`, index);
|
||||
await click(toggler);
|
||||
const dropdownMenu = get(target, `${SELECTORS.connector} .dropdown-menu `, index);
|
||||
const items = [...dropdownMenu.querySelectorAll(".dropdown-item")];
|
||||
const item = items.find((i) => i.innerText === value);
|
||||
await click(item);
|
||||
}
|
||||
|
||||
async function makeExpressionEditor(params = {}) {
|
||||
const props = { ...params };
|
||||
const mockRPC = props.mockRPC;
|
||||
delete props.mockRPC;
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.expressionEditorProps = {
|
||||
resModel: "partner",
|
||||
expression: "1",
|
||||
...props,
|
||||
update: (expression) => {
|
||||
if (props.update) {
|
||||
props.update(expression);
|
||||
}
|
||||
this.expressionEditorProps.expression = expression;
|
||||
this.render();
|
||||
},
|
||||
};
|
||||
this.expressionEditorProps.fields =
|
||||
this.expressionEditorProps.fields ||
|
||||
serverData.models[this.expressionEditorProps.resModel]?.fields ||
|
||||
{};
|
||||
Object.entries(this.expressionEditorProps.fields).forEach(([fieldName, field]) => {
|
||||
field.name = fieldName;
|
||||
});
|
||||
}
|
||||
async set(expression) {
|
||||
this.expressionEditorProps.expression = expression;
|
||||
this.render();
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
Parent.components = { ExpressionEditor };
|
||||
Parent.template = xml`<ExpressionEditor t-props="expressionEditorProps"/>`;
|
||||
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
await mount(MainComponentsContainer, target, { env });
|
||||
return mount(Parent, target, { env, props });
|
||||
}
|
||||
|
||||
QUnit.module("Components", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
serverData = makeServerData();
|
||||
setupConditionTreeEditorServices();
|
||||
target = getFixture();
|
||||
patchWithCleanup(odoo, { debug: true });
|
||||
});
|
||||
|
||||
QUnit.module("ExpressionEditor");
|
||||
|
||||
QUnit.test("rendering of truthy values", async (assert) => {
|
||||
const toTests = [`True`, `true`, `1`, `-1`, `"a"`];
|
||||
const parent = await makeExpressionEditor();
|
||||
|
||||
for (const expr of toTests) {
|
||||
await parent.set(expr);
|
||||
const tree = getTreeEditorContent(target);
|
||||
assert.deepEqual(tree, [{ level: 0, value: "all" }]);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("rendering of falsy values", async (assert) => {
|
||||
const toTests = [`False`, `false`, `0`, `""`];
|
||||
const parent = await makeExpressionEditor();
|
||||
for (const expr of toTests) {
|
||||
await parent.set(expr);
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ value: "all", level: 0 },
|
||||
{ value: ["0", "=", "1"], level: 1 },
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("rendering of 'expr'", async (assert) => {
|
||||
patchWithCleanup(odoo, { debug: false });
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
]);
|
||||
assert.strictEqual(target.querySelector(SELECTORS.complexConditionInput).readOnly, true);
|
||||
});
|
||||
|
||||
QUnit.test("rendering of 'expr' in dev mode", async (assert) => {
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
]);
|
||||
assert.strictEqual(target.querySelector(SELECTORS.complexConditionInput).readOnly, false);
|
||||
});
|
||||
|
||||
QUnit.test("edit a complex condition in dev mode", async (assert) => {
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
assert.containsNone(target, SELECTORS.condition);
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
]);
|
||||
await editExpression(target, "uid");
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "uid", level: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("delete a complex condition", async (assert) => {
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
]);
|
||||
await clickOnButtonDeleteNode(target);
|
||||
assert.deepEqual(getTreeEditorContent(target), [{ value: "all", level: 0 }]);
|
||||
});
|
||||
|
||||
QUnit.test("copy a complex condition", async (assert) => {
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
assert.containsNone(target, SELECTORS.condition);
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
]);
|
||||
await clickOnButtonAddNewRule(target);
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
{ value: "expr", level: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("change path, operator and value", async (assert) => {
|
||||
patchWithCleanup(odoo, { debug: false });
|
||||
await makeExpressionEditor({ expression: `bar != "blabla"` });
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: ["Bar", "is not", "blabla"] },
|
||||
]);
|
||||
const tree = getTreeEditorContent(target, { node: true });
|
||||
await openModelFieldSelectorPopover(target);
|
||||
await click(target.querySelectorAll(".o_model_field_selector_popover_item_name")[4]);
|
||||
await selectOperator(tree[1].node, "not in");
|
||||
await editValue(target, ["Doku", "Lukaku", "KDB"]);
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: ["Foo", "is not in", "Doku,Lukaku,KDB"] },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("create a new branch from a complex condition control panel", async (assert) => {
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
]);
|
||||
await clickOnButtonAddBranch(target);
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
{ level: 1, value: "any" },
|
||||
{ level: 2, value: ["ID", "=", "1"] },
|
||||
{ level: 2, value: ["ID", "=", "1"] },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("rendering of a valid fieldName in fields", async (assert) => {
|
||||
const fields = { foo: { string: "Foo", type: "char", searchable: true } };
|
||||
const parent = await makeExpressionEditor({ fields });
|
||||
|
||||
const toTests = [
|
||||
{ expr: `foo`, condition: ["Foo", "is set"] },
|
||||
{ expr: `foo == "a"`, condition: ["Foo", "=", "a"] },
|
||||
{ expr: `foo != "a"`, condition: ["Foo", "!=", "a"] },
|
||||
// { expr: `foo is "a"`, complexCondition: `foo is "a"` },
|
||||
// { expr: `foo is not "a"`, complexCondition: `foo is not "a"` },
|
||||
{ expr: `not foo`, condition: ["Foo", "is not set"] },
|
||||
{ expr: `foo + "a"`, complexCondition: `foo + "a"` },
|
||||
];
|
||||
|
||||
for (const { expr, condition, complexCondition } of toTests) {
|
||||
await parent.set(expr);
|
||||
const tree = getTreeEditorContent(target);
|
||||
if (condition) {
|
||||
assert.deepEqual(tree, [
|
||||
{ value: "all", level: 0 },
|
||||
{ value: condition, level: 1 },
|
||||
]);
|
||||
} else if (complexCondition) {
|
||||
assert.deepEqual(tree, [
|
||||
{ value: "all", level: 0 },
|
||||
{ value: complexCondition, level: 1 },
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("rendering of simple conditions", async (assert) => {
|
||||
const fields = {
|
||||
foo: { string: "Foo", type: "char", searchable: true },
|
||||
bar: { string: "Bar", type: "char", searchable: true },
|
||||
};
|
||||
const parent = await makeExpressionEditor({ fields });
|
||||
|
||||
const toTests = [
|
||||
{ expr: `bar == "a"`, condition: ["Bar", "=", "a"] },
|
||||
{ expr: `foo == expr`, condition: ["Foo", "=", "expr"] },
|
||||
{ expr: `"a" == foo`, condition: ["Foo", "=", "a"] },
|
||||
{ expr: `expr == foo`, condition: ["Foo", "=", "expr"] },
|
||||
{ expr: `foo == bar`, complexCondition: `foo == bar` },
|
||||
{ expr: `"a" == "b"`, complexCondition: `"a" == "b"` },
|
||||
{ expr: `expr1 == expr2`, complexCondition: `expr1 == expr2` },
|
||||
|
||||
{ expr: `foo < "a"`, condition: ["Foo", "<", "a"] },
|
||||
{ expr: `foo < expr`, condition: ["Foo", "<", "expr"] },
|
||||
{ expr: `"a" < foo`, condition: ["Foo", ">", "a"] },
|
||||
{ expr: `expr < foo`, condition: ["Foo", ">", "expr"] },
|
||||
{ expr: `foo < bar`, complexCondition: `foo < bar` },
|
||||
{ expr: `"a" < "b"`, complexCondition: `"a" < "b"` },
|
||||
{ expr: `expr1 < expr2`, complexCondition: `expr1 < expr2` },
|
||||
|
||||
{ expr: `foo in ["a"]`, condition: ["Foo", "is in", "a"] },
|
||||
{ expr: `foo in [expr]`, condition: ["Foo", "is in", "expr"] },
|
||||
{ expr: `"a" in foo`, complexCondition: `"a" in foo` },
|
||||
{ expr: `expr in foo`, complexCondition: `expr in foo` },
|
||||
{ expr: `foo in bar`, complexCondition: `foo in bar` },
|
||||
{ expr: `"a" in "b"`, complexCondition: `"a" in "b"` },
|
||||
{ expr: `expr1 in expr2`, complexCondition: `expr1 in expr2` },
|
||||
];
|
||||
|
||||
for (const { expr, condition, complexCondition } of toTests) {
|
||||
await parent.set(expr);
|
||||
const tree = getTreeEditorContent(target);
|
||||
if (condition) {
|
||||
assert.deepEqual(tree, [
|
||||
{ value: "all", level: 0 },
|
||||
{ value: condition, level: 1 },
|
||||
]);
|
||||
} else if (complexCondition) {
|
||||
assert.deepEqual(tree, [
|
||||
{ value: "all", level: 0 },
|
||||
{ value: complexCondition, level: 1 },
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("rendering of connectors", async (assert) => {
|
||||
await makeExpressionEditor({ expression: `expr and foo == "abc" or not bar` });
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(`${SELECTORS.connector} .dropdown-toggle`)),
|
||||
["any", "all"]
|
||||
);
|
||||
const tree = getTreeEditorContent(target);
|
||||
assert.deepEqual(tree, [
|
||||
{ level: 0, value: "any" },
|
||||
{ level: 1, value: "all" },
|
||||
{ level: 2, value: "expr" },
|
||||
{ level: 2, value: ["Foo", "=", "abc"] },
|
||||
{ level: 1, value: ["Bar", "is", "not set"] },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("rendering of connectors (2)", async (assert) => {
|
||||
await makeExpressionEditor({
|
||||
expression: `not (expr or foo == "abc")`,
|
||||
update(expression) {
|
||||
assert.step(expression);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(
|
||||
target.querySelector(`${SELECTORS.connector} .dropdown-toggle`).textContent,
|
||||
"none"
|
||||
);
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ level: 0, value: "none" },
|
||||
{ level: 1, value: "expr" },
|
||||
{ level: 1, value: ["Foo", "=", "abc"] },
|
||||
]);
|
||||
assert.verifySteps([]);
|
||||
assert.strictEqual(
|
||||
target.querySelector(SELECTORS.debugArea).value,
|
||||
`not (expr or foo == "abc")`
|
||||
);
|
||||
|
||||
await selectConnector(target, "all");
|
||||
assert.strictEqual(
|
||||
target.querySelector(`${SELECTORS.connector} .dropdown-toggle`).textContent,
|
||||
"all"
|
||||
);
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: "expr" },
|
||||
{ level: 1, value: ["Foo", "=", "abc"] },
|
||||
]);
|
||||
assert.verifySteps([`expr and foo == "abc"`]);
|
||||
assert.strictEqual(
|
||||
target.querySelector(SELECTORS.debugArea).value,
|
||||
`expr and foo == "abc"`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("rendering of if else", async (assert) => {
|
||||
await makeExpressionEditor({ expression: `True if False else False` });
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ level: 0, value: "any" },
|
||||
{ level: 1, value: "all" },
|
||||
{ level: 2, value: ["0", "=", "1"] },
|
||||
{ level: 2, value: ["1", "=", "1"] },
|
||||
{ level: 1, value: "all" },
|
||||
{ level: 2, value: ["1", "=", "1"] },
|
||||
{ level: 2, value: ["0", "=", "1"] },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("check condition by default when creating a new rule", async (assert) => {
|
||||
patchWithCleanup(odoo, { debug: false });
|
||||
serverData.models.partner.fields.country_id = { string: "Country ID", type: "char" };
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
await click(target, "a[role='button']");
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: "expr" },
|
||||
{ level: 1, value: ["Country ID", "=", ""] },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("allow selection of boolean field", async (assert) => {
|
||||
await makeExpressionEditor({ expression: "id" });
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: ["ID", "is set"] },
|
||||
]);
|
||||
await openModelFieldSelectorPopover(target);
|
||||
await click(target.querySelector(".o_model_field_selector_popover_item_name"));
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: ["Bar", "is", "set"] },
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("render false and true leaves", async (assert) => {
|
||||
await makeExpressionEditor({ expression: `False and True` });
|
||||
assert.deepEqual(getOperatorOptions(target), ["="]);
|
||||
assert.deepEqual(getValueOptions(target), ["1"]);
|
||||
assert.deepEqual(getOperatorOptions(target, -1), ["="]);
|
||||
assert.deepEqual(getValueOptions(target, -1), ["1"]);
|
||||
});
|
||||
|
||||
QUnit.test("no field of type properties in model field selector", async (assert) => {
|
||||
patchWithCleanup(odoo, { debug: false });
|
||||
serverData.models.partner.fields.properties = {
|
||||
string: "Properties",
|
||||
type: "properties",
|
||||
definition_record: "product_id",
|
||||
definition_record_field: "definitions",
|
||||
searchable: true,
|
||||
};
|
||||
serverData.models.product.fields.definitions = {
|
||||
string: "Definitions",
|
||||
type: "properties_definition",
|
||||
};
|
||||
|
||||
await makeExpressionEditor({
|
||||
expression: `properties`,
|
||||
fields: Object.fromEntries(
|
||||
Object.entries(serverData.models.partner.fields).filter(([name]) =>
|
||||
["bar", "foo", "properties"].includes(name)
|
||||
)
|
||||
),
|
||||
update(expression) {
|
||||
assert.step(expression);
|
||||
},
|
||||
});
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{
|
||||
level: 0,
|
||||
value: "all",
|
||||
},
|
||||
{
|
||||
level: 1,
|
||||
value: ["Properties", "is set"],
|
||||
},
|
||||
]);
|
||||
assert.ok(isNotSupportedPath(target));
|
||||
|
||||
await clearNotSupported(target);
|
||||
assert.verifySteps([`foo == ""`]);
|
||||
|
||||
await openModelFieldSelectorPopover(target);
|
||||
assert.deepEqual(
|
||||
getNodesTextContent([
|
||||
...target.querySelectorAll(".o_model_field_selector_popover_item_name"),
|
||||
]),
|
||||
["Bar", "Foo"]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("between operator", async (assert) => {
|
||||
await makeExpressionEditor({
|
||||
expression: `id == 1`,
|
||||
update(expression) {
|
||||
assert.step(expression);
|
||||
},
|
||||
});
|
||||
assert.deepEqual(getOperatorOptions(target), [
|
||||
"=",
|
||||
"!=",
|
||||
">",
|
||||
">=",
|
||||
"<",
|
||||
"<=",
|
||||
"is between",
|
||||
"is set",
|
||||
"is not set",
|
||||
]);
|
||||
assert.verifySteps([]);
|
||||
await selectOperator(target, "between");
|
||||
assert.verifySteps([`id >= 1 and id <= 1`]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { fieldService } from "@web/core/field_service";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { getFixture, makeDeferred, mount, nextTick } from "@web/../tests/helpers/utils";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
function getModelInfo(resModel) {
|
||||
return {
|
||||
resModel,
|
||||
fieldDefs: serverData.models[resModel].fields,
|
||||
};
|
||||
}
|
||||
|
||||
function getDefinitions() {
|
||||
const records = serverData.models.species.records;
|
||||
const fieldDefs = {};
|
||||
for (const record of records) {
|
||||
for (const definition of record.definitions) {
|
||||
fieldDefs[definition.name] = {
|
||||
is_property: true,
|
||||
searchable: true,
|
||||
record_name: record.display_name,
|
||||
record_id: record.id,
|
||||
...definition,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { resModel: "*", fieldDefs };
|
||||
}
|
||||
|
||||
let serverData;
|
||||
QUnit.module("Field Service", {
|
||||
async beforeEach() {
|
||||
serverData = {
|
||||
models: {
|
||||
tortoise: {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
display_name: { string: "Display Name", type: "char" },
|
||||
name: { string: "Name", type: "char", default: "name" },
|
||||
write_date: { string: "Last Modified on", type: "datetime" },
|
||||
age: { type: "integer", string: "Age" },
|
||||
location_id: { type: "many2one", string: "Location", relation: "location" },
|
||||
species: { type: "many2one", string: "Species", relation: "species" },
|
||||
property_field: {
|
||||
string: "Properties",
|
||||
type: "properties",
|
||||
definition_record: "species",
|
||||
definition_record_field: "definitions",
|
||||
},
|
||||
},
|
||||
},
|
||||
location: {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
display_name: { string: "Display Name", type: "char" },
|
||||
name: { string: "Name", type: "char", default: "name" },
|
||||
write_date: { string: "Last Modified on", type: "datetime" },
|
||||
tortoise_ids: { type: "one2many", string: "Turtles", relation: "tortoise" },
|
||||
},
|
||||
},
|
||||
species: {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
display_name: { string: "Display Name", type: "char" },
|
||||
name: { string: "Name", type: "char", default: "name" },
|
||||
write_date: { string: "Last Modified on", type: "datetime" },
|
||||
definitions: { string: "Definitions", type: "properties_definition" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "Galápagos tortoise",
|
||||
definitions: [
|
||||
{
|
||||
name: "galapagos_lifespans",
|
||||
string: "Lifespans",
|
||||
type: "integer",
|
||||
},
|
||||
{
|
||||
name: "location_ids",
|
||||
string: "Locations",
|
||||
type: "many2many",
|
||||
relation: "location",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "Aldabra giant tortoise",
|
||||
definitions: [
|
||||
{ name: "aldabra_lifespans", string: "Lifespans", type: "integer" },
|
||||
{ name: "color", string: "Color", type: "char" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("field", fieldService);
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("loadPath", async (assert) => {
|
||||
const toTest = [
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "*",
|
||||
expectedResult: {
|
||||
names: ["*"],
|
||||
modelsInfo: [getModelInfo("tortoise")],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "*.a",
|
||||
expectedResult: {
|
||||
isInvalid: "path",
|
||||
names: ["*", "a"],
|
||||
modelsInfo: [getModelInfo("tortoise")],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "location_id.*",
|
||||
expectedResult: {
|
||||
names: ["location_id", "*"],
|
||||
modelsInfo: [getModelInfo("tortoise"), getModelInfo("location")],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "age",
|
||||
expectedResult: {
|
||||
names: ["age"],
|
||||
modelsInfo: [getModelInfo("tortoise")],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "location_id",
|
||||
expectedResult: {
|
||||
names: ["location_id"],
|
||||
modelsInfo: [getModelInfo("tortoise")],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "location_id.tortoise_ids",
|
||||
expectedResult: {
|
||||
names: ["location_id", "tortoise_ids"],
|
||||
modelsInfo: [getModelInfo("tortoise"), getModelInfo("location")],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "location_id.tortoise_ids.age",
|
||||
expectedResult: {
|
||||
names: ["location_id", "tortoise_ids", "age"],
|
||||
modelsInfo: [
|
||||
getModelInfo("tortoise"),
|
||||
getModelInfo("location"),
|
||||
getModelInfo("tortoise"),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "location_id.tortoise_ids.age",
|
||||
expectedResult: {
|
||||
names: ["location_id", "tortoise_ids", "age"],
|
||||
modelsInfo: [
|
||||
getModelInfo("tortoise"),
|
||||
getModelInfo("location"),
|
||||
getModelInfo("tortoise"),
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "property_field",
|
||||
expectedResult: {
|
||||
names: ["property_field"],
|
||||
modelsInfo: [getModelInfo("tortoise")],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "property_field.galapagos_lifespans",
|
||||
expectedResult: {
|
||||
names: ["property_field", "galapagos_lifespans"],
|
||||
modelsInfo: [getModelInfo("tortoise"), getDefinitions()],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "property_field.location_ids.tortoise_ids",
|
||||
expectedResult: {
|
||||
isInvalid: "path",
|
||||
names: ["property_field", "location_ids", "tortoise_ids"],
|
||||
modelsInfo: [getModelInfo("tortoise"), getDefinitions()],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const env = await makeTestEnv({ serverData });
|
||||
|
||||
for (const { resModel, path, expectedResult } of toTest) {
|
||||
const result = await env.services.field.loadPath(resModel, path);
|
||||
assert.deepEqual(result, expectedResult);
|
||||
}
|
||||
|
||||
const errorToTest = [
|
||||
{ resModel: "notAModel" },
|
||||
{ resModel: "tortoise", path: {} },
|
||||
{ resModel: "tortoise", path: "" },
|
||||
];
|
||||
|
||||
for (const { resModel, path } of errorToTest) {
|
||||
try {
|
||||
await env.services.field.loadPath(resModel, path);
|
||||
} catch {
|
||||
assert.step("error");
|
||||
}
|
||||
}
|
||||
assert.verifySteps(errorToTest.map(() => "error"));
|
||||
});
|
||||
|
||||
QUnit.test("store loadFields calls in cache in success", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
const mockRPC = (route) => {
|
||||
if (route.includes("fields_get")) {
|
||||
assert.step("fields_get");
|
||||
}
|
||||
};
|
||||
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
|
||||
await env.services.field.loadFields("tortoise");
|
||||
await env.services.field.loadFields("tortoise");
|
||||
|
||||
assert.verifySteps(["fields_get"]);
|
||||
});
|
||||
|
||||
QUnit.test("does not store loadFields calls in cache when failed", async (assert) => {
|
||||
assert.expect(5);
|
||||
|
||||
const mockRPC = (route) => {
|
||||
if (route.includes("fields_get")) {
|
||||
assert.step("fields_get");
|
||||
return Promise.reject("my little error");
|
||||
}
|
||||
};
|
||||
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
|
||||
try {
|
||||
await env.services.field.loadFields("take.five");
|
||||
} catch (error) {
|
||||
assert.strictEqual(error, "my little error");
|
||||
}
|
||||
try {
|
||||
await env.services.field.loadFields("take.five");
|
||||
} catch (error) {
|
||||
assert.strictEqual(error, "my little error");
|
||||
}
|
||||
|
||||
assert.verifySteps(["fields_get", "fields_get"]);
|
||||
});
|
||||
|
||||
QUnit.test("async method loadFields is protected", async (assert) => {
|
||||
assert.expect(7);
|
||||
|
||||
let callFieldService;
|
||||
class Child extends Component {
|
||||
static template = xml`
|
||||
<div class="o_child_component" />
|
||||
`;
|
||||
setup() {
|
||||
this.fieldService = useService("field");
|
||||
callFieldService = async () => {
|
||||
assert.step("loadFields called");
|
||||
await this.fieldService.loadFields("tortoise");
|
||||
assert.step("loadFields result get");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { Child };
|
||||
static template = xml`
|
||||
<t t-if="state.displayChild">
|
||||
<Child />
|
||||
</t>
|
||||
`;
|
||||
setup() {
|
||||
this.state = useState({ displayChild: true });
|
||||
}
|
||||
}
|
||||
|
||||
const target = getFixture();
|
||||
const def = makeDeferred();
|
||||
const env = await makeTestEnv({
|
||||
serverData,
|
||||
async mockRPC() {
|
||||
await def;
|
||||
},
|
||||
});
|
||||
const parent = await mount(Parent, target, { env });
|
||||
|
||||
assert.containsOnce(target, ".o_child_component");
|
||||
|
||||
callFieldService();
|
||||
assert.verifySteps(["loadFields called"]);
|
||||
|
||||
parent.state.displayChild = false;
|
||||
await nextTick();
|
||||
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
try {
|
||||
await callFieldService();
|
||||
} catch (e) {
|
||||
assert.step(e.message);
|
||||
}
|
||||
|
||||
assert.verifySteps(["loadFields called", "Component is destroyed"]);
|
||||
});
|
||||
|
|
@ -7,6 +7,8 @@ import {
|
|||
mount,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
makeDeferred,
|
||||
nextTick,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { FileInput } from "@web/core/file_input/file_input";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
|
@ -130,6 +132,26 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
assert.isNotVisible(target.querySelector(".o_file_input"));
|
||||
});
|
||||
|
||||
QUnit.test("uploading the same file twice triggers the onChange twice", async (assert) => {
|
||||
await createFileInput({
|
||||
props: {
|
||||
onUpload(files) {
|
||||
assert.step(files[0].name);
|
||||
},
|
||||
},
|
||||
mockPost: (route, params) => {
|
||||
return JSON.stringify([{ name: params.ufile[0].name }]);
|
||||
},
|
||||
});
|
||||
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await editInput(target, ".o_file_input input", file);
|
||||
assert.verifySteps(["fake_file.txt"], "file has been initially uploaded");
|
||||
|
||||
await editInput(target, ".o_file_input input", file);
|
||||
assert.verifySteps(["fake_file.txt"], "file has been uploaded a second time");
|
||||
});
|
||||
|
||||
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 });
|
||||
|
|
@ -160,4 +182,29 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
"Only the notification will be triggered and the file won't be uploaded."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Upload button is disabled if attachment upload is not finished", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
const uploadedPromise = makeDeferred();
|
||||
await createFileInput({
|
||||
mockPost: async (route, params) => {
|
||||
if (route === "/web/binary/upload_attachment") {
|
||||
await uploadedPromise;
|
||||
}
|
||||
return "[]";
|
||||
},
|
||||
props: {},
|
||||
});
|
||||
//enable button
|
||||
const input = target.querySelector(".o_file_input input");
|
||||
await triggerEvent(input, null, "change", {}, { skipVisibilityCheck: true });
|
||||
|
||||
//disable button
|
||||
assert.ok(input.disabled, "the upload button should be disabled on upload");
|
||||
|
||||
uploadedPromise.resolve();
|
||||
await nextTick();
|
||||
assert.notOk(input.disabled, "the upload button should be enabled for upload");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
Object.assign(xhr, {
|
||||
upload: new window.EventTarget(),
|
||||
open() {},
|
||||
send(data) { customSend && customSend(data); },
|
||||
send(data) {
|
||||
customSend && customSend(data);
|
||||
},
|
||||
});
|
||||
return xhr;
|
||||
},
|
||||
|
|
@ -132,7 +134,7 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
fileUploadService.uploads[1].xhr.dispatchEvent(new Event("abort"));
|
||||
},
|
||||
});
|
||||
await click(target, ".o-file-upload-progress-bar-abort", true);
|
||||
await click(target, ".o-file-upload-progress-bar-abort", { skipVisibilityCheck: true });
|
||||
assert.containsNone(target, ".file_upload");
|
||||
});
|
||||
|
||||
|
|
@ -149,10 +151,16 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
progressEvent.total = 500000000;
|
||||
fileUploadService.uploads[1].xhr.upload.dispatchEvent(progressEvent);
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".file_upload_progress_text_left").textContent, "Uploading... (50%)");
|
||||
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)");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".file_upload_progress_text_right").textContent,
|
||||
"(350/500MB)"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { FileModel } from "@web/core/file_viewer/file_model";
|
||||
|
||||
QUnit.module("FileModel", () => {
|
||||
QUnit.module("URL Routing", () => {
|
||||
QUnit.test("returns correct URL for files with ID", (assert) => {
|
||||
const imageFile = new FileModel();
|
||||
Object.assign(imageFile, {
|
||||
id: 123,
|
||||
mimetype: "image/png",
|
||||
name: "test.png",
|
||||
});
|
||||
|
||||
const regularFile = new FileModel();
|
||||
Object.assign(regularFile, {
|
||||
id: 456,
|
||||
mimetype: "application/pdf",
|
||||
name: "test.pdf",
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
imageFile.urlRoute,
|
||||
"/web/image/123",
|
||||
"Should return correct image URL route with ID"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
regularFile.urlRoute,
|
||||
"/web/content/456",
|
||||
"Should return correct content URL route with ID"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("returns direct URL for files without ID", (assert) => {
|
||||
const fileWithoutId = new FileModel();
|
||||
const directUrl = "https://example.com/file.pdf";
|
||||
Object.assign(fileWithoutId, {
|
||||
id: undefined,
|
||||
url: directUrl,
|
||||
mimetype: "application/pdf",
|
||||
name: "file.pdf",
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
fileWithoutId.urlRoute,
|
||||
directUrl,
|
||||
"Should return direct URL when ID is not present"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("prioritizes ID over direct URL when both are present", (assert) => {
|
||||
const imageFile = new FileModel();
|
||||
Object.assign(imageFile, {
|
||||
id: 789,
|
||||
url: "https://example.com/direct-image.jpg",
|
||||
mimetype: "image/jpeg",
|
||||
name: "image.jpg",
|
||||
});
|
||||
|
||||
const regularFile = new FileModel();
|
||||
Object.assign(regularFile, {
|
||||
id: 101,
|
||||
url: "https://example.com/direct-file.pdf",
|
||||
mimetype: "application/pdf",
|
||||
name: "document.pdf",
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
imageFile.urlRoute,
|
||||
"/web/image/789",
|
||||
"Should use ID-based route for image even when direct URL is present"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
regularFile.urlRoute,
|
||||
"/web/content/101",
|
||||
"Should use ID-based route for regular file even when direct URL is present"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -4,7 +4,7 @@ import { browser } from "@web/core/browser/browser";
|
|||
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService, useActiveElement } from "@web/core/ui/ui_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { getActiveHotkey, hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
import {
|
||||
destroy,
|
||||
|
|
@ -158,7 +158,7 @@ QUnit.test("[accesskey] attrs replaced by [data-hotkey], part 2", async (assert)
|
|||
useActiveElement("bouh");
|
||||
}
|
||||
}
|
||||
UIOwnershipTakerComponent.template = xml`<p class="owner" t-ref="bouh">bouh</p>`;
|
||||
UIOwnershipTakerComponent.template = xml`<p class="owner" t-ref="bouh"><button/></p>`;
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ foo: true });
|
||||
|
|
@ -766,6 +766,7 @@ QUnit.test("registrations and elements belong to the correct UI owner", async (a
|
|||
await nextTick();
|
||||
|
||||
destroy(comp2);
|
||||
await Promise.resolve();
|
||||
triggerHotkey("a");
|
||||
triggerHotkey("b", true);
|
||||
await nextTick();
|
||||
|
|
@ -1059,7 +1060,7 @@ QUnit.test("operating area and UI active element", async (assert) => {
|
|||
useActiveElement("bouh");
|
||||
}
|
||||
}
|
||||
UIOwnershipTakerComponent.template = xml`<p class="owner" t-ref="bouh">bouh</p>`;
|
||||
UIOwnershipTakerComponent.template = xml`<p class="owner" t-ref="bouh"><button/></p>`;
|
||||
class C extends Component {
|
||||
setup() {
|
||||
this.state = useState({ foo: false });
|
||||
|
|
@ -1119,7 +1120,7 @@ QUnit.test("operating area and UI active element", async (assert) => {
|
|||
});
|
||||
|
||||
QUnit.test("validating option", async (assert) => {
|
||||
let isValid = false;
|
||||
let isAvailable = false;
|
||||
class A extends Component {
|
||||
setup() {
|
||||
useHotkey(
|
||||
|
|
@ -1128,10 +1129,7 @@ QUnit.test("validating option", async (assert) => {
|
|||
assert.step("RGNTDJÛ!");
|
||||
},
|
||||
{
|
||||
validate: (eventTarget) => {
|
||||
assert.strictEqual(eventTarget, document.activeElement);
|
||||
return isValid;
|
||||
},
|
||||
isAvailable: () => isAvailable,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -1143,14 +1141,14 @@ QUnit.test("validating option", async (assert) => {
|
|||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
isValid = true;
|
||||
isAvailable = true;
|
||||
triggerHotkey("Space");
|
||||
await nextTick();
|
||||
assert.verifySteps(["RGNTDJÛ!"]);
|
||||
});
|
||||
|
||||
QUnit.test("operation area with validating option", async (assert) => {
|
||||
let isValid;
|
||||
let isAvailable;
|
||||
class A extends Component {
|
||||
setup() {
|
||||
const areaRef = useRef("area");
|
||||
|
|
@ -1159,7 +1157,7 @@ QUnit.test("operation area with validating option", async (assert) => {
|
|||
() => {
|
||||
assert.step("RGNTDJÛ!");
|
||||
},
|
||||
{ area: () => areaRef.el, validate: () => isValid }
|
||||
{ area: () => areaRef.el, isAvailable: () => isAvailable }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1172,12 +1170,12 @@ QUnit.test("operation area with validating option", async (assert) => {
|
|||
// Trigger hotkeys from the 'one'
|
||||
target.querySelector(".one").focus();
|
||||
|
||||
isValid = false;
|
||||
isAvailable = false;
|
||||
triggerHotkey("Space");
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
isValid = true;
|
||||
isAvailable = true;
|
||||
triggerHotkey("Space");
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
|
@ -1185,12 +1183,12 @@ QUnit.test("operation area with validating option", async (assert) => {
|
|||
// Trigger hotkeys from the 'two'
|
||||
target.querySelector(".two").focus();
|
||||
|
||||
isValid = false;
|
||||
isAvailable = false;
|
||||
triggerHotkey("Space");
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
|
||||
isValid = true;
|
||||
isAvailable = true;
|
||||
triggerHotkey("Space");
|
||||
await nextTick();
|
||||
assert.verifySteps(["RGNTDJÛ!"]);
|
||||
|
|
@ -1212,3 +1210,18 @@ QUnit.test("mixing hotkeys with and without operation area", async (assert) => {
|
|||
await nextTick();
|
||||
assert.verifySteps(["withArea"]);
|
||||
});
|
||||
|
||||
QUnit.test("native browser space key ' ' is correctly translated to 'space' ", async (assert) => {
|
||||
class A extends Component {
|
||||
setup() {
|
||||
useHotkey("space", () => assert.step("space"));
|
||||
}
|
||||
}
|
||||
A.template = xml``;
|
||||
|
||||
assert.strictEqual(getActiveHotkey({ key: " " }), "space");
|
||||
|
||||
await mount(A, target, { env });
|
||||
await triggerHotkey(" "); // event key triggered by the browser
|
||||
assert.verifySteps(["space"]);
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,9 +6,8 @@ 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 { translatedTerms, translationLoaded, _t } 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";
|
||||
|
|
@ -18,19 +17,7 @@ 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 });
|
||||
function patchFetch() {
|
||||
patchWithCleanup(browser, {
|
||||
fetch: async () => ({
|
||||
ok: true,
|
||||
|
|
@ -48,72 +35,45 @@ async function patchLang(lang) {
|
|||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 });
|
||||
patchFetch();
|
||||
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");
|
||||
QUnit.test("lang is given by the user context", async (assert) => {
|
||||
patchWithCleanup(session.user_context, { lang: "fr_FR" });
|
||||
patchWithCleanup(session, {
|
||||
cache_hashes: { translations: 1 },
|
||||
})
|
||||
patchFetch();
|
||||
patchWithCleanup(browser, {
|
||||
fetch(url) {
|
||||
assert.strictEqual(url, "/web/webclient/translations/1?lang=fr_FR");
|
||||
return super.fetch(...arguments);
|
||||
},
|
||||
});
|
||||
serviceRegistry.add("localization", localizationService);
|
||||
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(() => {
|
||||
|
|
@ -122,24 +82,65 @@ QUnit.test("lang is given by an attribute on the DOM root node", async (assert)
|
|||
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,
|
||||
},
|
||||
};
|
||||
patchFetch();
|
||||
patchWithCleanup(browser, {
|
||||
fetch(url) {
|
||||
assert.strictEqual(url, "/web/webclient/translations/1?lang=fr_FR");
|
||||
return super.fetch(...arguments);
|
||||
},
|
||||
});
|
||||
serviceRegistry.add("localization", localizationService);
|
||||
await makeTestEnv();
|
||||
});
|
||||
|
||||
QUnit.test("url is given by the session", async (assert) => {
|
||||
patchWithCleanup(session, {
|
||||
translationURL: "/get_translations",
|
||||
cache_hashes: { translations: 1 },
|
||||
})
|
||||
patchFetch();
|
||||
patchWithCleanup(browser, {
|
||||
fetch(url) {
|
||||
assert.strictEqual(url, "/get_translations/1?lang=en");
|
||||
return super.fetch(...arguments);
|
||||
},
|
||||
});
|
||||
serviceRegistry.add("localization", localizationService);
|
||||
await makeTestEnv();
|
||||
});
|
||||
|
||||
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();
|
||||
patchWithCleanup(translatedTerms, { ...terms });
|
||||
const target = getFixture();
|
||||
await mount(TestComponent, target, { env });
|
||||
assert.strictEqual(target.innerText, "Bonjour");
|
||||
});
|
||||
|
||||
QUnit.test("can lazy translate", async (assert) => {
|
||||
// Can't use patchWithCleanup cause it doesn't support Symbol
|
||||
translatedTerms[translationLoaded] = false;
|
||||
assert.expect(3);
|
||||
TestComponent.template = xml`<div><t t-esc="constructor.someLazyText" /></div>`;
|
||||
TestComponent.someLazyText = _t("Hello");
|
||||
assert.throws(() => TestComponent.someLazyText.toString());
|
||||
assert.throws(() => TestComponent.someLazyText.valueOf());
|
||||
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
const env = await makeTestEnv();
|
||||
patchWithCleanup(translatedTerms, { ...terms });
|
||||
translatedTerms[translationLoaded] = true;
|
||||
const target = getFixture();
|
||||
await mount(TestComponent, target, { env });
|
||||
assert.strictEqual(target.innerText, "Bonjour");
|
||||
});
|
||||
|
||||
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.module("Numbering system");
|
||||
|
|
@ -215,3 +216,23 @@ QUnit.test("tamil has the correct numbering system", async (assert) => {
|
|||
"௧௦/௧௨/௨௦௨௧ ௧௨:௦௦:௦௦"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"_t fills the format specifiers in translated terms with its extra arguments",
|
||||
async (assert) => {
|
||||
patchWithCleanup(translatedTerms, { "Due in %s days": "Échéance dans %s jours" });
|
||||
const translatedStr = _t("Due in %s days", 513);
|
||||
assert.strictEqual(translatedStr, "Échéance dans 513 jours");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"_t fills the format specifiers in lazy translated terms with its extra arguments",
|
||||
async (assert) => {
|
||||
translatedTerms[translationLoaded] = false;
|
||||
const translatedStr = _t("Due in %s days", 513);
|
||||
patchWithCleanup(translatedTerms, { "Due in %s days": "Échéance dans %s jours" });
|
||||
translatedTerms[translationLoaded] = true;
|
||||
assert.equal(translatedStr, "Échéance dans 513 jours");
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ QUnit.module(
|
|||
{
|
||||
beforeEach() {
|
||||
target = getFixture();
|
||||
engine = new MacroEngine(target);
|
||||
mock = mockTimeout();
|
||||
},
|
||||
afterEach() {
|
||||
|
|
@ -38,6 +37,10 @@ QUnit.module(
|
|||
|
||||
QUnit.test("simple use", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
engine = new MacroEngine({
|
||||
target: target.querySelector(".counter"),
|
||||
defaultCheckDelay: 500,
|
||||
});
|
||||
|
||||
const span = target.querySelector("span.value");
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
|
|
@ -51,7 +54,6 @@ QUnit.module(
|
|||
},
|
||||
],
|
||||
});
|
||||
// default interval is 500
|
||||
await mock.advanceTime(300);
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
await mock.advanceTime(300);
|
||||
|
|
@ -60,6 +62,10 @@ QUnit.module(
|
|||
|
||||
QUnit.test("multiple steps", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
engine = new MacroEngine({
|
||||
target: target.querySelector(".counter"),
|
||||
defaultCheckDelay: 500,
|
||||
});
|
||||
|
||||
const span = target.querySelector("span.value");
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
|
|
@ -92,13 +98,19 @@ QUnit.module(
|
|||
|
||||
QUnit.test("can use a function as action", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
engine = new MacroEngine({
|
||||
target: target.querySelector(".counter"),
|
||||
defaultCheckDelay: 500,
|
||||
});
|
||||
let flag = false;
|
||||
engine.activate({
|
||||
name: "test",
|
||||
steps: [
|
||||
{
|
||||
trigger: "button.inc",
|
||||
action: () => (flag = true),
|
||||
action: () => {
|
||||
flag = true;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -109,7 +121,11 @@ QUnit.module(
|
|||
|
||||
QUnit.test("can input values", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
const input = target.querySelector("input");
|
||||
engine = new MacroEngine({
|
||||
target: target.querySelector(".counter"),
|
||||
defaultCheckDelay: 500,
|
||||
});
|
||||
const input = engine.target.querySelector("input");
|
||||
|
||||
engine.activate({
|
||||
name: "test",
|
||||
|
|
@ -128,7 +144,11 @@ QUnit.module(
|
|||
|
||||
QUnit.test("a step can have no trigger", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
const input = target.querySelector("input");
|
||||
engine = new MacroEngine({
|
||||
target: target.querySelector(".counter"),
|
||||
defaultCheckDelay: 500,
|
||||
});
|
||||
const input = engine.target.querySelector("input");
|
||||
|
||||
engine.activate({
|
||||
name: "test",
|
||||
|
|
@ -151,8 +171,11 @@ QUnit.module(
|
|||
|
||||
QUnit.test("onStep function is called at each step", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
|
||||
const span = target.querySelector("span.value");
|
||||
engine = new MacroEngine({
|
||||
target: target.querySelector(".counter"),
|
||||
defaultCheckDelay: 500,
|
||||
});
|
||||
const span = engine.target.querySelector("span.value");
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
|
||||
engine.activate({
|
||||
|
|
@ -177,8 +200,11 @@ QUnit.module(
|
|||
|
||||
QUnit.test("trigger can be a function returning an htmlelement", async function (assert) {
|
||||
await mount(TestComponent, target);
|
||||
|
||||
const span = target.querySelector("span.value");
|
||||
engine = new MacroEngine({
|
||||
target: target.querySelector(".counter"),
|
||||
defaultCheckDelay: 500,
|
||||
});
|
||||
const span = engine.target.querySelector("span.value");
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
|
||||
engine.activate({
|
||||
|
|
@ -199,9 +225,12 @@ QUnit.module(
|
|||
|
||||
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");
|
||||
engine = new MacroEngine({
|
||||
target: target.querySelector(".counter"),
|
||||
defaultCheckDelay: 500,
|
||||
});
|
||||
const span = engine.target.querySelector("span.value");
|
||||
const button = engine.target.querySelector("button.inc");
|
||||
assert.strictEqual(span.textContent, "0");
|
||||
|
||||
engine.activate({
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
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";
|
||||
import { registerCleanup } from "../helpers/cleanup";
|
||||
const mainComponentsRegistry = registry.category("main_components");
|
||||
|
||||
let target;
|
||||
|
|
@ -38,6 +38,7 @@ QUnit.module("Components", (hooks) => {
|
|||
});
|
||||
|
||||
QUnit.test("unmounts erroring main component", async function (assert) {
|
||||
assert.expectErrors();
|
||||
const env = await makeTestEnv();
|
||||
|
||||
let compA;
|
||||
|
|
@ -67,24 +68,18 @@ QUnit.module("Components", (hooks) => {
|
|||
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: () => {},
|
||||
registerCleanup(() => {
|
||||
window.removeEventListener("unhandledrejection", handler);
|
||||
});
|
||||
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.verifyErrors(["BOOM"]);
|
||||
|
||||
assert.equal(
|
||||
target.querySelector(".o-main-components-container").innerHTML,
|
||||
|
|
@ -93,6 +88,8 @@ QUnit.module("Components", (hooks) => {
|
|||
});
|
||||
|
||||
QUnit.test("unmounts erroring main component: variation", async function (assert) {
|
||||
assert.expectErrors();
|
||||
|
||||
const env = await makeTestEnv();
|
||||
|
||||
class MainComponentA extends Component {}
|
||||
|
|
@ -122,24 +119,18 @@ QUnit.module("Components", (hooks) => {
|
|||
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: () => {},
|
||||
registerCleanup(() => {
|
||||
window.removeEventListener("unhandledrejection", handler);
|
||||
});
|
||||
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.verifyErrors(["BOOM"]);
|
||||
assert.equal(
|
||||
target.querySelector(".o-main-components-container").innerHTML,
|
||||
"<span>MainComponentA</span>"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -163,7 +163,11 @@ QUnit.test("model_selector: with more than 8 models", async function (assert) {
|
|||
"model_10",
|
||||
]);
|
||||
await openAutocomplete();
|
||||
assert.containsN(fixture, "li.o-autocomplete--dropdown-item", 8);
|
||||
assert.containsN(fixture, "li.o-autocomplete--dropdown-item", 9);
|
||||
assert.strictEqual(
|
||||
fixture.querySelectorAll("li.o-autocomplete--dropdown-item")[8].innerText,
|
||||
"Start typing..."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
|
|
@ -227,6 +231,31 @@ QUnit.test("model_selector: select a model", async function (assert) {
|
|||
assert.verifySteps(["model selected"]);
|
||||
});
|
||||
|
||||
QUnit.test("model_selector: click on start typing", 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();
|
||||
await click(fixture.querySelectorAll("li.o-autocomplete--dropdown-item")[8]);
|
||||
assert.equal(fixture.querySelector(".o-autocomplete--input").value, "");
|
||||
assert.equal(fixture.querySelector(".o-autocomplete.dropdown ul"), null);
|
||||
|
||||
//label must be empty
|
||||
assert.equal(fixture.querySelector(".o_global_filter_label"), null);
|
||||
|
||||
//Default value and matching fields should not be available
|
||||
assert.equal(fixture.querySelector(".o_side_panel_section"), null);
|
||||
});
|
||||
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -5,7 +5,16 @@ 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";
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
getFixture,
|
||||
mount,
|
||||
mockTimeout,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
} from "../helpers/utils";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
|
|
@ -13,6 +22,16 @@ let env;
|
|||
let target;
|
||||
let props;
|
||||
|
||||
const getNameAndSignatureButtonNames = (target) => {
|
||||
return [...target.querySelectorAll(".card-header .col-auto")].reduce((names, el) => {
|
||||
const text = el.textContent.trim();
|
||||
if (text) {
|
||||
names.push(text);
|
||||
}
|
||||
return names;
|
||||
}, []);
|
||||
};
|
||||
|
||||
QUnit.module("Components", ({ beforeEach }) => {
|
||||
beforeEach(async () => {
|
||||
const mockRPC = async (route, args) => {
|
||||
|
|
@ -37,11 +56,11 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
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.deepEqual(getNameAndSignatureButtonNames(target), ["Auto", "Draw", "Load"]);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_web_sign_auto_select_style",
|
||||
"should show font selection dropdown"
|
||||
);
|
||||
assert.containsOnce(target, ".card-header .active");
|
||||
assert.strictEqual(target.querySelector(".card-header .active").textContent.trim(), "Auto");
|
||||
|
|
@ -49,26 +68,14 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
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.deepEqual(getNameAndSignatureButtonNames(target), ["Auto", "Draw", "Load"]);
|
||||
assert.containsOnce(target, ".o_web_sign_draw_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.deepEqual(getNameAndSignatureButtonNames(target), ["Auto", "Draw", "Load"]);
|
||||
assert.containsOnce(target, ".o_web_sign_load_file");
|
||||
assert.containsOnce(target, ".card-header .active");
|
||||
assert.strictEqual(target.querySelector(".card-header .active").textContent.trim(), "Load");
|
||||
});
|
||||
|
|
@ -81,12 +88,8 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
|
||||
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.deepEqual(getNameAndSignatureButtonNames(target), ["Auto", "Draw", "Load"]);
|
||||
assert.containsOnce(target, ".o_web_sign_auto_select_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");
|
||||
|
|
@ -106,12 +109,8 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
};
|
||||
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.deepEqual(getNameAndSignatureButtonNames(target), ["Auto", "Draw", "Load"]);
|
||||
assert.containsOnce(target, ".o_web_sign_auto_select_style");
|
||||
assert.containsOnce(target, ".card-header .active");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".card-header .active").textContent.trim(),
|
||||
|
|
@ -128,12 +127,8 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
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.deepEqual(getNameAndSignatureButtonNames(target), ["Draw", "Load"]);
|
||||
assert.containsOnce(target, ".o_web_sign_draw_clear");
|
||||
assert.containsOnce(target, ".card-header .active");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".card-header .active").textContent.trim(),
|
||||
|
|
@ -141,4 +136,44 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"test name_and_signature widget update signmode with onSignatureChange prop",
|
||||
async function (assert) {
|
||||
const defaultName = "Noi dea";
|
||||
let currentSignMode = "";
|
||||
props = {
|
||||
...props,
|
||||
onSignatureChange: function (signMode) {
|
||||
if (currentSignMode !== signMode) {
|
||||
currentSignMode = signMode;
|
||||
assert.step(signMode);
|
||||
}
|
||||
},
|
||||
};
|
||||
props.signature.name = defaultName;
|
||||
await mount(NameAndSignature, target, { env, props });
|
||||
await click(target, ".o_web_sign_draw_button");
|
||||
assert.verifySteps(["auto", "draw"], "should be draw");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("resize events are handled", async function (assert) {
|
||||
patchWithCleanup(NameAndSignature.prototype, {
|
||||
resizeSignature() {
|
||||
assert.step("resized");
|
||||
return super.resizeSignature();
|
||||
},
|
||||
});
|
||||
const { advanceTime } = mockTimeout();
|
||||
await mount(NameAndSignature, target, { env, props });
|
||||
await editInput(target, ".o_web_sign_name_group input", "plop");
|
||||
await nextTick();
|
||||
await click(target, ".o_web_sign_draw_button");
|
||||
await nextTick();
|
||||
assert.verifySteps(["resized"]);
|
||||
await triggerEvent(window, null, "resize");
|
||||
await advanceTime(300);
|
||||
assert.verifySteps(["resized"]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ERROR_INACCESSIBLE_OR_MISSING, nameService } from "@web/core/name_service";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let serverData;
|
||||
QUnit.module("Name Service", {
|
||||
async beforeEach() {
|
||||
serverData = {
|
||||
models: {
|
||||
dev: {
|
||||
fields: {},
|
||||
records: [
|
||||
{ id: 1, display_name: "Julien" },
|
||||
{ id: 2, display_name: "Pierre" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("name", nameService);
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("single loadDisplayNames", async (assert) => {
|
||||
const env = await makeTestEnv({ serverData });
|
||||
const displayNames = await env.services.name.loadDisplayNames("dev", [1, 2]);
|
||||
assert.deepEqual(displayNames, { 1: "Julien", 2: "Pierre" });
|
||||
});
|
||||
|
||||
QUnit.test("loadDisplayNames is done in silent mode", async (assert) => {
|
||||
assert.expect(2);
|
||||
const env = await makeTestEnv({ serverData });
|
||||
env.bus.addEventListener("RPC:REQUEST", (ev) => {
|
||||
const silent = ev.detail.settings.silent;
|
||||
assert.step("RPC:REQUEST" + (silent ? " (silent)" : ""));
|
||||
});
|
||||
await env.services.name.loadDisplayNames("dev", [1]);
|
||||
assert.verifySteps(["RPC:REQUEST (silent)"]);
|
||||
});
|
||||
|
||||
QUnit.test("single loadDisplayNames following addDisplayNames", async (assert) => {
|
||||
const mockRPC = (_, { method }) => {
|
||||
assert.step(method);
|
||||
};
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
env.services.name.addDisplayNames("dev", { 1: "JUM", 2: "PIPU" });
|
||||
const displayNames = await env.services.name.loadDisplayNames("dev", [1, 2]);
|
||||
assert.deepEqual(displayNames, { 1: "JUM", 2: "PIPU" });
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("single loadDisplayNames following addDisplayNames (2)", async (assert) => {
|
||||
const mockRPC = (_, { kwargs, method }) => {
|
||||
assert.step(method);
|
||||
const ids = kwargs.domain[0][2];
|
||||
assert.step(`id(s): ${ids.join(", ")}`);
|
||||
};
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
env.services.name.addDisplayNames("dev", { 1: "JUM" });
|
||||
const displayNames = await env.services.name.loadDisplayNames("dev", [1, 2]);
|
||||
assert.deepEqual(displayNames, { 1: "JUM", 2: "Pierre" });
|
||||
assert.verifySteps(["web_search_read", "id(s): 2"]);
|
||||
});
|
||||
|
||||
QUnit.test("loadDisplayNames in batch", async (assert) => {
|
||||
const mockRPC = (_, { kwargs, method }) => {
|
||||
assert.step(method);
|
||||
const ids = kwargs.domain[0][2];
|
||||
assert.step(`id(s): ${ids.join(", ")}`);
|
||||
};
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
const prom1 = env.services.name.loadDisplayNames("dev", [1]);
|
||||
assert.verifySteps([]);
|
||||
const prom2 = env.services.name.loadDisplayNames("dev", [2]);
|
||||
assert.verifySteps([]);
|
||||
const [displayNames1, displayNames2] = await Promise.all([prom1, prom2]);
|
||||
assert.deepEqual(displayNames1, { 1: "Julien" });
|
||||
assert.deepEqual(displayNames2, { 2: "Pierre" });
|
||||
assert.verifySteps(["web_search_read", "id(s): 1, 2"]);
|
||||
});
|
||||
|
||||
QUnit.test("loadDisplayNames on different models", async (assert) => {
|
||||
serverData.models.PO = {
|
||||
fields: {},
|
||||
records: [{ id: 1, display_name: "Damien" }],
|
||||
};
|
||||
const mockRPC = (_, { kwargs, method, model }) => {
|
||||
assert.step(method);
|
||||
assert.step(model);
|
||||
const ids = kwargs.domain[0][2];
|
||||
assert.step(`id(s): ${ids.join(", ")}`);
|
||||
};
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
const prom1 = env.services.name.loadDisplayNames("dev", [1]);
|
||||
assert.verifySteps([]);
|
||||
const prom2 = env.services.name.loadDisplayNames("PO", [1]);
|
||||
assert.verifySteps([]);
|
||||
const [displayNames1, displayNames2] = await Promise.all([prom1, prom2]);
|
||||
assert.deepEqual(displayNames1, { 1: "Julien" });
|
||||
assert.deepEqual(displayNames2, { 1: "Damien" });
|
||||
assert.verifySteps(["web_search_read", "dev", "id(s): 1", "web_search_read", "PO", "id(s): 1"]);
|
||||
});
|
||||
|
||||
QUnit.test("invalid id", async (assert) => {
|
||||
assert.expect(1);
|
||||
const env = await makeTestEnv({ serverData });
|
||||
try {
|
||||
await env.services.name.loadDisplayNames("dev", ["a"]);
|
||||
} catch (e) {
|
||||
assert.strictEqual(e.message, "Invalid ID: a");
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("inaccessible or missing id", async (assert) => {
|
||||
const mockRPC = (_, { method }) => {
|
||||
assert.step(method);
|
||||
};
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
const displayNames = await env.services.name.loadDisplayNames("dev", [3]);
|
||||
assert.deepEqual(displayNames, { 3: ERROR_INACCESSIBLE_OR_MISSING });
|
||||
assert.verifySteps(["web_search_read"]);
|
||||
});
|
||||
|
||||
QUnit.test("batch + inaccessible/missing", async (assert) => {
|
||||
const mockRPC = (_, { method, kwargs }) => {
|
||||
assert.step(method);
|
||||
const ids = kwargs.domain[0][2];
|
||||
assert.step(`id(s): ${ids.join(", ")}`);
|
||||
};
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
const prom1 = env.services.name.loadDisplayNames("dev", [1, 3]);
|
||||
assert.verifySteps([]);
|
||||
const prom2 = env.services.name.loadDisplayNames("dev", [2, 4]);
|
||||
assert.verifySteps([]);
|
||||
const [displayNames1, displayNames2] = await Promise.all([prom1, prom2]);
|
||||
assert.deepEqual(displayNames1, { 1: "Julien", 3: ERROR_INACCESSIBLE_OR_MISSING });
|
||||
assert.deepEqual(displayNames2, { 2: "Pierre", 4: ERROR_INACCESSIBLE_OR_MISSING });
|
||||
assert.verifySteps(["web_search_read", "id(s): 1, 3, 2, 4"]);
|
||||
});
|
||||
|
|
@ -18,13 +18,7 @@ QUnit.module("download", (hooks) => {
|
|||
}
|
||||
const MockXHR = makeMockXHR("", send);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
|
||||
|
||||
let error;
|
||||
try {
|
||||
|
|
@ -47,13 +41,7 @@ QUnit.module("download", (hooks) => {
|
|||
}
|
||||
const MockXHR = makeMockXHR("", send);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
|
||||
|
||||
let error;
|
||||
try {
|
||||
|
|
@ -87,13 +75,7 @@ QUnit.module("download", (hooks) => {
|
|||
}
|
||||
const MockXHR = makeMockXHR("", send);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
|
||||
|
||||
let error;
|
||||
try {
|
||||
|
|
@ -122,13 +104,7 @@ QUnit.module("download", (hooks) => {
|
|||
}
|
||||
const MockXHR = makeMockXHR("", send);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
|
||||
|
||||
let error;
|
||||
try {
|
||||
|
|
@ -161,13 +137,7 @@ QUnit.module("download", (hooks) => {
|
|||
}
|
||||
const MockXHR = makeMockXHR("", send);
|
||||
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
|
||||
|
||||
assert.containsNone(document.body, "a[download]");
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { patchWithCleanup } from "../../helpers/utils";
|
||||
import { get, post } from "@web/core/network/http_service";
|
||||
|
||||
function onFetch(fetch) {
|
||||
patchWithCleanup(browser, {
|
||||
fetch: (route, params) => {
|
||||
const result = fetch(route, params);
|
||||
return result ?? new Response("{}");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
QUnit.module("HTTP");
|
||||
|
||||
QUnit.test("method is correctly set", async (assert) => {
|
||||
onFetch((_, { method }) => {
|
||||
assert.step(method);
|
||||
});
|
||||
await get("/call_get");
|
||||
assert.verifySteps(["GET"]);
|
||||
await post("/call_post");
|
||||
assert.verifySteps(["POST"]);
|
||||
});
|
||||
|
||||
QUnit.test("check status 502", async (assert) => {
|
||||
onFetch(() => {
|
||||
return new Response({}, { status: 502 });
|
||||
});
|
||||
try {
|
||||
await get("/custom_route");
|
||||
assert.notOk(true);
|
||||
} catch (e) {
|
||||
assert.strictEqual(e.message, "Failed to fetch");
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("FormData is built by post", async (assert) => {
|
||||
onFetch((_, { body }) => {
|
||||
assert.ok(body instanceof FormData);
|
||||
assert.strictEqual(body.get("s"), "1");
|
||||
assert.strictEqual(body.get("a"), "1");
|
||||
assert.deepEqual(body.getAll("a"), ["1", "2", "3"]);
|
||||
});
|
||||
await post("/call_post", { s: 1, a: [1, 2, 3] });
|
||||
});
|
||||
|
||||
QUnit.test("FormData is given to post", async (assert) => {
|
||||
onFetch((_, { body }) => {
|
||||
assert.strictEqual(body, formData);
|
||||
});
|
||||
const formData = new FormData();
|
||||
await post("/call_post", formData);
|
||||
});
|
||||
|
|
@ -5,7 +5,6 @@ import { ConnectionAbortedError, ConnectionLostError, rpcService } from "@web/co
|
|||
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 {
|
||||
|
|
@ -20,7 +19,6 @@ import { registerCleanup } from "../../helpers/cleanup";
|
|||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
let isXHRMocked = false;
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let isDeployed = false;
|
||||
|
|
@ -31,18 +29,7 @@ async function testRPC(route, params) {
|
|||
request = data;
|
||||
url = this.url;
|
||||
});
|
||||
if (isXHRMocked) {
|
||||
unpatch(browser, "mock.xhr");
|
||||
}
|
||||
patch(
|
||||
browser,
|
||||
"mock.xhr",
|
||||
{
|
||||
XMLHttpRequest: MockXHR,
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
isXHRMocked = true;
|
||||
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
|
||||
|
||||
if (isDeployed) {
|
||||
clearRegistryWithCleanup(registry.category("main_components"));
|
||||
|
|
@ -66,12 +53,6 @@ QUnit.module("RPC", {
|
|||
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) => {
|
||||
|
|
@ -82,12 +63,11 @@ QUnit.test("can perform a simple rpc", async (assert) => {
|
|||
assert.ok(typeof request.id === "number");
|
||||
});
|
||||
|
||||
patch(browser, "mock.xhr", { XMLHttpRequest: MockXHR }, { pure: true });
|
||||
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
|
||||
|
||||
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) => {
|
||||
|
|
@ -101,17 +81,16 @@ QUnit.test("trigger an error when response has 'error' key", async (assert) => {
|
|||
},
|
||||
};
|
||||
const MockXHR = makeMockXHR({ error });
|
||||
patch(browser, "mock.xhr", { XMLHttpRequest: MockXHR }, { pure: true });
|
||||
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
|
||||
|
||||
const env = await makeTestEnv({
|
||||
serviceRegistry,
|
||||
});
|
||||
try {
|
||||
await env.services.rpc("/test/");
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
assert.ok(true);
|
||||
}
|
||||
unpatch(browser, "mock.xhr");
|
||||
});
|
||||
|
||||
QUnit.test("rpc with simple routes", async (assert) => {
|
||||
|
|
@ -133,7 +112,7 @@ QUnit.test("rpc coming from destroyed components are left pending", async (asser
|
|||
MyComponent.template = xml`<div/>`;
|
||||
const def = makeDeferred();
|
||||
const MockXHR = makeMockXHR({ result: "1" }, () => {}, def);
|
||||
patch(browser, "mock.xhr", { XMLHttpRequest: MockXHR }, { pure: true });
|
||||
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
|
||||
|
||||
const env = await makeTestEnv({
|
||||
serviceRegistry,
|
||||
|
|
@ -157,7 +136,6 @@ QUnit.test("rpc coming from destroyed components are left pending", async (asser
|
|||
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) => {
|
||||
|
|
@ -183,29 +161,31 @@ QUnit.test("rpc initiated from destroyed components throw exception", async (ass
|
|||
|
||||
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 });
|
||||
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
|
||||
|
||||
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:REQUEST", (ev) => {
|
||||
rpcIdsRequest.push(ev.detail.data.id);
|
||||
const silent = ev.detail.settings.silent;
|
||||
assert.step("RPC:REQUEST" + (silent ? "(silent)" : ""));
|
||||
});
|
||||
env.bus.addEventListener("RPC:RESPONSE", (rpcId) => {
|
||||
rpcIdsResponse.push(rpcId);
|
||||
assert.step("RPC:RESPONSE");
|
||||
env.bus.addEventListener("RPC:RESPONSE", (ev) => {
|
||||
rpcIdsResponse.push(ev.detail.data.id);
|
||||
const silent = ev.detail.settings.silent ? "(silent)" : "";
|
||||
const success = "result" in ev.detail ? "(ok)" : "";
|
||||
const fail = "error" in ev.detail ? "(ko)" : "";
|
||||
assert.step("RPC:RESPONSE" + silent + success + fail);
|
||||
});
|
||||
await env.services.rpc("/test/");
|
||||
assert.strictEqual(rpcIdsRequest.toString(), rpcIdsResponse.toString());
|
||||
assert.verifySteps(["RPC:REQUEST", "RPC:RESPONSE"]);
|
||||
assert.verifySteps(["RPC:REQUEST", "RPC:RESPONSE(ok)"]);
|
||||
|
||||
await env.services.rpc("/test/", {}, { silent: true });
|
||||
assert.verifySteps([]);
|
||||
|
||||
unpatch(browser, "mock.xhr");
|
||||
assert.verifySteps(["RPC:REQUEST(silent)", "RPC:RESPONSE(silent)(ok)"]);
|
||||
});
|
||||
|
||||
QUnit.test("check trigger RPC:REQUEST and RPC:RESPONSE for a rpc with an error", async (assert) => {
|
||||
|
|
@ -218,34 +198,36 @@ QUnit.test("check trigger RPC:REQUEST and RPC:RESPONSE for a rpc with an error",
|
|||
},
|
||||
};
|
||||
const MockXHR = makeMockXHR({ error });
|
||||
patch(browser, "mock.xhr", { XMLHttpRequest: MockXHR }, { pure: true });
|
||||
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
|
||||
const env = await makeTestEnv({
|
||||
serviceRegistry,
|
||||
});
|
||||
const rpcIdsRequest = [];
|
||||
const rpcIdsResponse = [];
|
||||
env.bus.addEventListener("RPC:REQUEST", (rpcId) => {
|
||||
rpcIdsRequest.push(rpcId);
|
||||
env.bus.addEventListener("RPC:REQUEST", (ev) => {
|
||||
rpcIdsRequest.push(ev);
|
||||
assert.step("RPC:REQUEST");
|
||||
});
|
||||
env.bus.addEventListener("RPC:RESPONSE", (rpcId) => {
|
||||
rpcIdsResponse.push(rpcId);
|
||||
assert.step("RPC:RESPONSE");
|
||||
env.bus.addEventListener("RPC:RESPONSE", (ev) => {
|
||||
rpcIdsResponse.push(ev);
|
||||
const silent = ev.detail.settings.silent ? "(silent)" : "";
|
||||
const success = "result" in ev.detail ? "(ok)" : "";
|
||||
const fail = "error" in ev.detail ? "(ko)" : "";
|
||||
assert.step("RPC:RESPONSE" + silent + success + fail);
|
||||
});
|
||||
try {
|
||||
await env.services.rpc("/test/");
|
||||
} catch (_e) {
|
||||
assert.ok(true);
|
||||
} catch {
|
||||
assert.step("ok");
|
||||
}
|
||||
assert.strictEqual(rpcIdsRequest.toString(), rpcIdsResponse.toString());
|
||||
assert.verifySteps(["RPC:REQUEST", "RPC:RESPONSE"]);
|
||||
unpatch(browser, "mock.xhr");
|
||||
assert.verifySteps(["RPC:REQUEST", "RPC:RESPONSE(ko)", "ok"]);
|
||||
});
|
||||
|
||||
QUnit.test("check connection aborted", async (assert) => {
|
||||
const def = makeDeferred();
|
||||
const MockXHR = makeMockXHR({}, () => {}, def);
|
||||
patchWithCleanup(browser, { XMLHttpRequest: MockXHR }, { pure: true });
|
||||
patchWithCleanup(browser, { XMLHttpRequest: MockXHR });
|
||||
const env = await makeTestEnv({ serviceRegistry });
|
||||
env.bus.addEventListener("RPC:REQUEST", () => {
|
||||
assert.step("RPC:REQUEST");
|
||||
|
|
|
|||
|
|
@ -334,4 +334,93 @@ QUnit.module("Components", (hooks) => {
|
|||
assert.containsOnce(target, ".page2");
|
||||
assert.strictEqual(target.querySelector(".nav-link.active").textContent, "page2");
|
||||
});
|
||||
|
||||
QUnit.test("disabled pages are greyed out and can't be toggled", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { Notebook };
|
||||
Parent.template = xml`
|
||||
<Notebook defaultPage="'1'">
|
||||
<t t-set-slot="1" title="'page1'" isVisible="true">
|
||||
<div class="page1" />
|
||||
</t>
|
||||
<t t-set-slot="2" title="'page2'" isVisible="true" isDisabled="true">
|
||||
<div class="page2" />
|
||||
</t>
|
||||
<t t-set-slot="3" title="'page3'" isVisible="true">
|
||||
<div class="page3" />
|
||||
</t>
|
||||
</Notebook>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".page1", "the default page is displayed");
|
||||
assert.hasClass(
|
||||
target.querySelector(".nav-item:nth-child(2)"),
|
||||
"disabled",
|
||||
"tab of the disabled page is greyed out"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".nav-item:nth-child(2) .nav-link"));
|
||||
assert.containsOnce(target, ".page1", "the same page is still displayed");
|
||||
|
||||
await click(target.querySelector(".nav-item:nth-child(3) .nav-link"));
|
||||
assert.containsOnce(target, ".page3", "the third page is now displayed");
|
||||
});
|
||||
|
||||
QUnit.test("icons can be given for each page tab", async (assert) => {
|
||||
class Parent extends Component {
|
||||
get icons() {
|
||||
return {
|
||||
1: "fa-trash",
|
||||
3: "fa-pencil",
|
||||
};
|
||||
}
|
||||
}
|
||||
Parent.components = { Notebook };
|
||||
Parent.template = xml`
|
||||
<Notebook defaultPage="'1'" icons="icons">
|
||||
<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="true">
|
||||
<div class="page3" />
|
||||
</t>
|
||||
</Notebook>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
await mount(Parent, target, { env });
|
||||
assert.hasClass(
|
||||
target.querySelector(".nav-item:nth-child(1) i"),
|
||||
"fa-trash",
|
||||
"tab of the first page has the given icon"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".nav-item:nth-child(1)").textContent,
|
||||
"page1",
|
||||
"tab of the second page has the right text"
|
||||
);
|
||||
assert.containsNone(
|
||||
target.querySelector(".nav-item:nth-child(2)"),
|
||||
"i",
|
||||
"tab of the second page doesn't have an icon"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".nav-item:nth-child(2)").textContent,
|
||||
"page2",
|
||||
"tab of the second page has the right text"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".nav-item:nth-child(3) i"),
|
||||
"fa-pencil",
|
||||
"tab of the third page has the given icon"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".nav-item:nth-child(3)").textContent,
|
||||
"page3",
|
||||
"tab of the second page has the right text"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,15 @@ 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 {
|
||||
click,
|
||||
getFixture,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
mockTimeout,
|
||||
triggerEvent,
|
||||
} from "../../helpers/utils";
|
||||
|
||||
import { markup } from "@odoo/owl";
|
||||
|
||||
|
|
@ -315,6 +323,33 @@ QUnit.test("can close a non-sticky notification", async (assert) => {
|
|||
assert.containsNone(target, ".o_notification");
|
||||
});
|
||||
|
||||
QUnit.test("can refresh the duration of a non-sticky notification", async (assert) => {
|
||||
const { advanceTime } = mockTimeout();
|
||||
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 first non-sticky notification");
|
||||
notifService.add("I'm a second non-sticky notification");
|
||||
await nextTick();
|
||||
assert.containsN(target, ".o_notification", 2);
|
||||
|
||||
await advanceTime(3000);
|
||||
await triggerEvent(target, ".o_notification:first-child", "mouseenter");
|
||||
await advanceTime(5000);
|
||||
// Both notifications should be visible as long as mouse is over one of them
|
||||
assert.containsN(target, ".o_notification", 2);
|
||||
await triggerEvent(target, ".o_notification:first-child", "mouseleave");
|
||||
await advanceTime(3000);
|
||||
// Both notifications should be refreshed in duration (4000 ms)
|
||||
assert.containsN(target, ".o_notification", 2);
|
||||
await advanceTime(1000);
|
||||
assert.containsNone(target, ".o_notification");
|
||||
});
|
||||
|
||||
QUnit.test("close a non-sticky notification while another one remains", async (assert) => {
|
||||
let timeoutCB;
|
||||
patchWithCleanup(browser, {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ QUnit.test("basic method call of model", async (assert) => {
|
|||
});
|
||||
});
|
||||
|
||||
QUnit.test("create method", async (assert) => {
|
||||
QUnit.test("create method: one record", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
|
|
@ -99,9 +99,11 @@ QUnit.test("create method", async (assert) => {
|
|||
assert.strictEqual(query.route, "/web/dataset/call_kw/partner/create");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [
|
||||
{
|
||||
color: "red",
|
||||
},
|
||||
[
|
||||
{
|
||||
color: "red",
|
||||
},
|
||||
],
|
||||
],
|
||||
kwargs: {
|
||||
context: {
|
||||
|
|
@ -115,25 +117,32 @@ QUnit.test("create method", async (assert) => {
|
|||
});
|
||||
});
|
||||
|
||||
QUnit.test("nameGet method", async (assert) => {
|
||||
QUnit.test("create method: several records", 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");
|
||||
await env.services.orm.create("partner", [{ color: "red" }, { color: "green" }]);
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/partner/create");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [[2, 5]],
|
||||
args: [
|
||||
[
|
||||
{
|
||||
color: "red",
|
||||
},
|
||||
{
|
||||
color: "green",
|
||||
},
|
||||
],
|
||||
],
|
||||
kwargs: {
|
||||
context: {
|
||||
complete: true,
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "name_get",
|
||||
model: "sale.order",
|
||||
method: "create",
|
||||
model: "partner",
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -266,6 +275,25 @@ QUnit.test("readGroup method", async (assert) => {
|
|||
});
|
||||
});
|
||||
|
||||
QUnit.test("test readGroup method removes duplicate values from groupby", 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:month", "date_order:month"],
|
||||
{ offset: 1 }
|
||||
);
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/sale.order/read_group");
|
||||
assert.deepEqual(
|
||||
query.params.kwargs.groupby,
|
||||
["date_order:month"],
|
||||
"Duplicate values should be removed from groupby"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("searchRead method", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
|
|
@ -308,11 +336,38 @@ QUnit.test("searchCount method", async (assert) => {
|
|||
});
|
||||
});
|
||||
|
||||
QUnit.test("webRead method", async (assert) => {
|
||||
const [query, rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
const context = { abc: 3 };
|
||||
await env.services.orm.webRead("sale.order", [2, 5], {
|
||||
specification: { name: {}, amount: {} },
|
||||
context,
|
||||
});
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/sale.order/web_read");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [[2, 5]],
|
||||
kwargs: {
|
||||
specification: { name: {}, amount: {} },
|
||||
context: {
|
||||
abc: 3,
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "web_read",
|
||||
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"]);
|
||||
const specification = { amount_total: {} };
|
||||
await env.services.orm.webSearchRead("sale.order", [["user_id", "=", 2]], { specification });
|
||||
assert.strictEqual(query.route, "/web/dataset/call_kw/sale.order/web_search_read");
|
||||
assert.deepEqual(query.params, {
|
||||
args: [],
|
||||
|
|
@ -323,14 +378,14 @@ QUnit.test("webSearchRead method", async (assert) => {
|
|||
uid: 7,
|
||||
},
|
||||
domain: [["user_id", "=", 2]],
|
||||
fields: ["amount_total"],
|
||||
specification: { amount_total: {} },
|
||||
},
|
||||
method: "web_search_read",
|
||||
model: "sale.order",
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("useModel is specialized for component", async (assert) => {
|
||||
QUnit.test("orm is specialized for component", async (assert) => {
|
||||
const [, /* query */ rpc] = makeFakeRPC();
|
||||
serviceRegistry.add("rpc", rpc);
|
||||
const env = await makeTestEnv();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { overlayService } from "@web/core/overlay/overlay_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { getFixture, mount, nextTick } from "../helpers/utils";
|
||||
|
||||
let target = null;
|
||||
let overlay = null;
|
||||
|
||||
QUnit.module("overlay service", {
|
||||
async beforeEach() {
|
||||
registry.category("services").add("overlay", overlayService);
|
||||
|
||||
target = getFixture();
|
||||
const env = await makeTestEnv();
|
||||
await mount(MainComponentsContainer, target, { env });
|
||||
|
||||
overlay = env.services.overlay;
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("simple case", async (assert) => {
|
||||
assert.containsOnce(target, ".o-overlay-container");
|
||||
|
||||
class MyComp extends Component {
|
||||
static template = xml`
|
||||
<div class="overlayed"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
const remove = overlay.add(MyComp, {});
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o-overlay-container > .overlayed");
|
||||
|
||||
remove();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o-overlay-container > .overlayed");
|
||||
});
|
||||
|
||||
QUnit.test("onRemove callback", async (assert) => {
|
||||
class MyComp extends Component {
|
||||
static template = xml``;
|
||||
}
|
||||
|
||||
const onRemove = () => assert.step("onRemove");
|
||||
const remove = overlay.add(MyComp, {}, { onRemove });
|
||||
|
||||
assert.verifySteps([]);
|
||||
remove();
|
||||
assert.verifySteps(["onRemove"]);
|
||||
});
|
||||
|
||||
QUnit.test("multiple overlays", async (assert) => {
|
||||
class MyComp extends Component {
|
||||
static template = xml`
|
||||
<div class="overlayed" t-att-class="props.className"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
const remove1 = overlay.add(MyComp, { className: "o1" });
|
||||
const remove2 = overlay.add(MyComp, { className: "o2" });
|
||||
const remove3 = overlay.add(MyComp, { className: "o3" });
|
||||
await nextTick();
|
||||
assert.containsN(target, ".overlayed", 3);
|
||||
assert.hasClass(target.querySelector(".overlayed:nth-child(1)"), "o1");
|
||||
assert.hasClass(target.querySelector(".overlayed:nth-child(2)"), "o2");
|
||||
assert.hasClass(target.querySelector(".overlayed:nth-child(3)"), "o3");
|
||||
|
||||
remove1();
|
||||
await nextTick();
|
||||
assert.containsN(target, ".overlayed", 2);
|
||||
assert.hasClass(target.querySelector(".overlayed:nth-child(1)"), "o2");
|
||||
assert.hasClass(target.querySelector(".overlayed:nth-child(2)"), "o3");
|
||||
|
||||
remove2();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".overlayed");
|
||||
assert.hasClass(target.querySelector(".overlayed:nth-child(1)"), "o3");
|
||||
|
||||
remove3();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".overlayed");
|
||||
});
|
||||
|
||||
QUnit.test("sequence", async (assert) => {
|
||||
class MyComp extends Component {
|
||||
static template = xml`
|
||||
<div class="overlayed" t-att-class="props.className"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
const remove1 = overlay.add(MyComp, { className: "o1" }, { sequence: 50 });
|
||||
const remove2 = overlay.add(MyComp, { className: "o2" }, { sequence: 60 });
|
||||
const remove3 = overlay.add(MyComp, { className: "o3" }, { sequence: 40 });
|
||||
await nextTick();
|
||||
assert.containsN(target, ".overlayed", 3);
|
||||
assert.hasClass(target.querySelector(".overlayed:nth-child(1)"), "o3");
|
||||
assert.hasClass(target.querySelector(".overlayed:nth-child(2)"), "o1");
|
||||
assert.hasClass(target.querySelector(".overlayed:nth-child(3)"), "o2");
|
||||
|
||||
remove1();
|
||||
await nextTick();
|
||||
assert.containsN(target, ".overlayed", 2);
|
||||
assert.hasClass(target.querySelector(".overlayed:nth-child(1)"), "o3");
|
||||
assert.hasClass(target.querySelector(".overlayed:nth-child(2)"), "o2");
|
||||
|
||||
remove2();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".overlayed");
|
||||
assert.hasClass(target.querySelector(".overlayed:nth-child(1)"), "o3");
|
||||
|
||||
remove3();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".overlayed");
|
||||
});
|
||||
|
|
@ -183,8 +183,6 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
});
|
||||
|
||||
QUnit.test("pager disabling", async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
const reloadPromise = makeDeferred();
|
||||
|
||||
const pager = await makePager({
|
||||
|
|
@ -203,9 +201,9 @@ QUnit.module("Components", ({ beforeEach }) => {
|
|||
});
|
||||
const pagerButtons = target.querySelectorAll("button");
|
||||
|
||||
// Click twice
|
||||
await click(target.querySelector(`.o_pager button.o_pager_next`));
|
||||
// Click and check button is disabled
|
||||
await click(target.querySelector(`.o_pager button.o_pager_next`));
|
||||
assert.ok(target.querySelector(`.o_pager button.o_pager_next`).disabled);
|
||||
// Try to edit the pager value
|
||||
await click(target, ".o_pager_value");
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ 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 { click, destroy, getFixture, mount, nextTick } from "../../helpers/utils";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { makeFakeLocalizationService } from "../../helpers/mock_services";
|
||||
|
||||
let env;
|
||||
let target;
|
||||
|
|
@ -34,7 +36,11 @@ PseudoWebClient.template = xml`
|
|||
QUnit.module("Popover hook", {
|
||||
async beforeEach() {
|
||||
clearRegistryWithCleanup(mainComponents);
|
||||
registry.category("services").add("popover", popoverService);
|
||||
registry
|
||||
.category("services")
|
||||
.add("popover", popoverService)
|
||||
.add("localization", makeFakeLocalizationService())
|
||||
.add("hotkey", hotkeyService);
|
||||
target = getFixture();
|
||||
env = await makeTestEnv();
|
||||
await mount(PseudoWebClient, target, { env });
|
||||
|
|
@ -48,17 +54,17 @@ QUnit.test("close popover when component is unmounted", async (assert) => {
|
|||
|
||||
class CompWithPopover extends Component {
|
||||
setup() {
|
||||
this.popover = usePopover();
|
||||
this.popover = usePopover(Comp);
|
||||
}
|
||||
}
|
||||
CompWithPopover.template = xml`<div />`;
|
||||
|
||||
const comp1 = await mount(CompWithPopover, target, { env });
|
||||
comp1.popover.add(popoverTarget, Comp, { id: "comp1" });
|
||||
comp1.popover.open(popoverTarget, { id: "comp1" });
|
||||
await nextTick();
|
||||
|
||||
const comp2 = await mount(CompWithPopover, target, { env });
|
||||
comp2.popover.add(popoverTarget, Comp, { id: "comp2" });
|
||||
comp2.popover.open(popoverTarget, { id: "comp2" });
|
||||
await nextTick();
|
||||
|
||||
assert.containsN(target, ".o_popover", 2);
|
||||
|
|
@ -79,3 +85,54 @@ QUnit.test("close popover when component is unmounted", async (assert) => {
|
|||
assert.containsNone(target, ".o_popover #comp1");
|
||||
assert.containsNone(target, ".o_popover #comp2");
|
||||
});
|
||||
|
||||
QUnit.test("popover opened from another", async (assert) => {
|
||||
class Comp extends Component {
|
||||
static id = 0;
|
||||
static template = xml`
|
||||
<div class="p-4">
|
||||
<button class="pop-open" t-on-click="(ev) => this.popover.open(ev.target, {})">open popover</button>
|
||||
</div>
|
||||
`;
|
||||
setup() {
|
||||
this.popover = usePopover(Comp, {
|
||||
popoverClass: `popover-${++Comp.id}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mount(Comp, target, { env });
|
||||
|
||||
await click(target, ".pop-open");
|
||||
assert.containsOnce(target, ".popover-1", "open first popover");
|
||||
|
||||
await click(target, ".popover-1 .pop-open");
|
||||
assert.containsN(target, ".o_popover", 2, "open second popover from the first one");
|
||||
assert.containsOnce(target, ".popover-1");
|
||||
assert.containsOnce(target, ".popover-2");
|
||||
|
||||
await click(target, ".popover-2 .pop-open");
|
||||
assert.containsN(target, ".o_popover", 3, "open third popover from the second one");
|
||||
assert.containsOnce(target, ".popover-1");
|
||||
assert.containsOnce(target, ".popover-2");
|
||||
assert.containsOnce(target, ".popover-3");
|
||||
|
||||
await click(target, ".popover-3");
|
||||
assert.containsN(target, ".o_popover", 3, "clicking inside third popover closes nothing");
|
||||
assert.containsOnce(target, ".popover-1");
|
||||
assert.containsOnce(target, ".popover-2");
|
||||
assert.containsOnce(target, ".popover-3");
|
||||
|
||||
await click(target, ".popover-2");
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_popover",
|
||||
2,
|
||||
"clicking inside second popover closes third popover"
|
||||
);
|
||||
assert.containsOnce(target, ".popover-1");
|
||||
assert.containsOnce(target, ".popover-2");
|
||||
|
||||
await click(target, "#close");
|
||||
assert.containsNone(target, ".o_popover", "clicking out of any popover closes them all");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
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";
|
||||
import { makeFakeLocalizationService } from "../../helpers/mock_services";
|
||||
import { click, getFixture, mount, nextTick, triggerEvent } from "../../helpers/utils";
|
||||
|
||||
let env;
|
||||
let fixture;
|
||||
|
|
@ -35,7 +36,11 @@ PseudoWebClient.template = xml`
|
|||
QUnit.module("Popover service", {
|
||||
async beforeEach() {
|
||||
clearRegistryWithCleanup(mainComponents);
|
||||
registry.category("services").add("popover", popoverService);
|
||||
registry
|
||||
.category("services")
|
||||
.add("popover", popoverService)
|
||||
.add("localization", makeFakeLocalizationService())
|
||||
.add("hotkey", hotkeyService);
|
||||
|
||||
fixture = getFixture();
|
||||
env = await makeTestEnv();
|
||||
|
|
@ -46,8 +51,6 @@ QUnit.module("Popover service", {
|
|||
});
|
||||
|
||||
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>`;
|
||||
|
||||
|
|
@ -67,8 +70,6 @@ QUnit.test("simple use", async (assert) => {
|
|||
});
|
||||
|
||||
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>`;
|
||||
|
||||
|
|
@ -84,9 +85,23 @@ QUnit.test("close on click away", async (assert) => {
|
|||
assert.containsNone(fixture, ".o_popover #comp");
|
||||
});
|
||||
|
||||
QUnit.test("do not close on click away", async (assert) => {
|
||||
assert.containsOnce(fixture, ".o_popover_container");
|
||||
QUnit.test("close on 'Escape' keydown", async (assert) => {
|
||||
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 triggerEvent(fixture, null, "keydown", { key: "Escape" });
|
||||
|
||||
assert.containsNone(fixture, ".o_popover");
|
||||
assert.containsNone(fixture, ".o_popover #comp");
|
||||
});
|
||||
|
||||
QUnit.test("do not close on click away", async (assert) => {
|
||||
class Comp extends Component {}
|
||||
Comp.template = xml`<div id="comp">in popover</div>`;
|
||||
|
||||
|
|
@ -109,10 +124,6 @@ QUnit.test("do not close on click away", async (assert) => {
|
|||
});
|
||||
|
||||
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>`;
|
||||
|
||||
|
|
@ -129,8 +140,6 @@ QUnit.test("close callback", async (assert) => {
|
|||
});
|
||||
|
||||
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>`;
|
||||
|
||||
|
|
@ -147,8 +156,6 @@ QUnit.test("sub component triggers close", async (assert) => {
|
|||
});
|
||||
|
||||
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>`;
|
||||
|
||||
|
|
@ -166,14 +173,10 @@ QUnit.test("close popover if target is removed", async (assert) => {
|
|||
});
|
||||
|
||||
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>`;
|
||||
|
||||
|
|
@ -188,8 +191,6 @@ QUnit.test("close and do not crash if target parent does not exist", async (asse
|
|||
});
|
||||
|
||||
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>`;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@ 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";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
|
||||
let env;
|
||||
let fixture;
|
||||
let popoverTarget;
|
||||
|
||||
|
|
@ -19,12 +23,16 @@ QUnit.module("Popover", {
|
|||
registerCleanup(() => {
|
||||
popoverTarget.remove();
|
||||
});
|
||||
|
||||
registry.category("services").add("ui", uiService);
|
||||
env = await makeTestEnv();
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("popover can have custom class", async (assert) => {
|
||||
await mount(Popover, fixture, {
|
||||
props: { target: popoverTarget, popoverClass: "custom-popover" },
|
||||
env,
|
||||
props: { target: popoverTarget, class: "custom-popover" },
|
||||
});
|
||||
|
||||
assert.containsOnce(fixture, ".o_popover.custom-popover");
|
||||
|
|
@ -32,68 +40,130 @@ QUnit.test("popover can have custom class", async (assert) => {
|
|||
|
||||
QUnit.test("popover can have more than one custom class", async (assert) => {
|
||||
await mount(Popover, fixture, {
|
||||
props: { target: popoverTarget, popoverClass: "custom-popover popover-custom" },
|
||||
env,
|
||||
props: { target: popoverTarget, class: "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 }) {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
assert.equal(direction, "bottom");
|
||||
assert.equal(variant, "middle");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
env,
|
||||
props: { target: popoverTarget },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("popover is rendered nearby target (bottom)", async (assert) => {
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction }) {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
assert.equal(direction, "bottom");
|
||||
assert.equal(variant, "middle");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
env,
|
||||
props: { target: popoverTarget, position: "bottom" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("popover is rendered nearby target (top)", async (assert) => {
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction }) {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
assert.equal(direction, "top");
|
||||
assert.equal(variant, "middle");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
env,
|
||||
props: { target: popoverTarget, position: "top" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("popover is rendered nearby target (left)", async (assert) => {
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction }) {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
assert.equal(direction, "left");
|
||||
assert.equal(variant, "middle");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
env,
|
||||
props: { target: popoverTarget, position: "left" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("popover is rendered nearby target (right)", async (assert) => {
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction }) {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
assert.equal(direction, "right");
|
||||
assert.equal(variant, "middle");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
env,
|
||||
props: { target: popoverTarget, position: "right" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("popover is rendered nearby target (bottom-start)", async (assert) => {
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
assert.equal(direction, "bottom");
|
||||
assert.equal(variant, "start");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
env,
|
||||
props: { target: popoverTarget, position: "bottom-start" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("popover is rendered nearby target (bottom-middle)", async (assert) => {
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
assert.equal(direction, "bottom");
|
||||
assert.equal(variant, "middle");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
env,
|
||||
props: { target: popoverTarget, position: "bottom-middle" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("popover is rendered nearby target (bottom-end)", async (assert) => {
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
assert.equal(direction, "bottom");
|
||||
assert.equal(variant, "end");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
env,
|
||||
props: { target: popoverTarget, position: "bottom-end" },
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("popover is rendered nearby target (bottom-fit)", async (assert) => {
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
assert.equal(direction, "bottom");
|
||||
assert.equal(variant, "fit");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
env,
|
||||
props: { target: popoverTarget, position: "bottom-fit" },
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
|
|
@ -126,7 +196,7 @@ QUnit.test("reposition popover should properly change classNames", async (assert
|
|||
const TestPopover = class extends Popover {
|
||||
setup() {
|
||||
// Don't call super.setup() in order to replace the use of usePosition hook...
|
||||
usePosition(this.props.target, {
|
||||
usePosition("ref", () => this.props.target, {
|
||||
container,
|
||||
onPositioned: this.onPositioned.bind(this),
|
||||
position: this.props.position,
|
||||
|
|
@ -134,14 +204,14 @@ QUnit.test("reposition popover should properly change classNames", async (assert
|
|||
}
|
||||
};
|
||||
|
||||
await mount(TestPopover, container, { props: { target: popoverTarget } });
|
||||
await mount(TestPopover, container, { env, 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"
|
||||
"o_popover popover mw-100 bs-popover-bottom o-popover-bottom o-popover--bm"
|
||||
);
|
||||
assert.strictEqual(arrow.className, "popover-arrow start-0 end-0 mx-auto");
|
||||
|
||||
|
|
@ -154,7 +224,7 @@ QUnit.test("reposition popover should properly change classNames", async (assert
|
|||
// 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"
|
||||
"o_popover popover mw-100 bs-popover-end o-popover-right o-popover--re"
|
||||
);
|
||||
assert.strictEqual(arrow.className, "popover-arrow top-auto");
|
||||
});
|
||||
|
|
@ -168,14 +238,17 @@ QUnit.test("within iframe", async (assert) => {
|
|||
fixture.appendChild(iframe);
|
||||
await def;
|
||||
|
||||
let popoverEl;
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned(el, { direction }) {
|
||||
popoverEl = el;
|
||||
assert.step(direction);
|
||||
}
|
||||
};
|
||||
|
||||
popoverTarget = iframe.contentDocument.getElementById("target");
|
||||
await mount(TestPopover, fixture, {
|
||||
env,
|
||||
props: { target: popoverTarget },
|
||||
});
|
||||
assert.verifySteps(["bottom"]);
|
||||
|
|
@ -186,4 +259,98 @@ QUnit.test("within iframe", async (assert) => {
|
|||
iframe.contentDocument.documentElement.querySelectorAll(".o_popover").length,
|
||||
0
|
||||
);
|
||||
|
||||
// The popover should be rendered in the correct position
|
||||
const { top: targetTop, left: targetLeft } = popoverTarget.getBoundingClientRect();
|
||||
const { top: iframeTop, left: iframeLeft } = iframe.getBoundingClientRect();
|
||||
let popoverBox = popoverEl.getBoundingClientRect();
|
||||
let expectedTop = iframeTop + targetTop + popoverTarget.offsetHeight;
|
||||
let expectedLeft =
|
||||
iframeLeft + targetLeft + popoverTarget.offsetWidth / 2 - popoverBox.width / 2;
|
||||
assert.strictEqual(popoverBox.top, expectedTop);
|
||||
assert.strictEqual(popoverBox.left, expectedLeft);
|
||||
|
||||
// Scrolling inside the iframe should reposition the popover accordingly
|
||||
const scrollOffset = 100;
|
||||
const scrollable = popoverTarget.ownerDocument.documentElement;
|
||||
scrollable.scrollTop = scrollOffset;
|
||||
await nextTick();
|
||||
assert.verifySteps(["bottom"]);
|
||||
popoverBox = popoverEl.getBoundingClientRect();
|
||||
expectedTop = iframeTop + targetTop + popoverTarget.offsetHeight - scrollOffset;
|
||||
expectedLeft = iframeLeft + targetLeft + popoverTarget.offsetWidth / 2 - popoverBox.width / 2;
|
||||
assert.strictEqual(popoverBox.top, expectedTop);
|
||||
assert.strictEqual(popoverBox.left, expectedLeft);
|
||||
});
|
||||
|
||||
QUnit.test("within iframe -- wrong element class", async (assert) => {
|
||||
/**
|
||||
* This use case exists in real life, when adding some blocks with the OdooEditor
|
||||
* in an iframe. The HTML spec discourages it though.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Document/importNode
|
||||
*/
|
||||
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 wrongElement = document.createElement("div");
|
||||
wrongElement.classList.add("wrong-element");
|
||||
iframe.contentDocument.body.appendChild(wrongElement);
|
||||
class TestPopover extends Popover {
|
||||
static props = {
|
||||
...Popover.props,
|
||||
target: {
|
||||
validate: (...args) => {
|
||||
const val = Popover.props.target.validate(...args);
|
||||
assert.step(`validate target props: "${val}"`);
|
||||
return val;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
await mount(TestPopover, fixture, {
|
||||
env,
|
||||
props: { target: wrongElement },
|
||||
test: true,
|
||||
});
|
||||
assert.containsOnce(fixture, ".o_popover");
|
||||
assert.verifySteps(['validate target props: "true"']);
|
||||
});
|
||||
|
||||
QUnit.test("popover fixed position", async (assert) => {
|
||||
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.height = "50px";
|
||||
container.appendChild(popoverTarget);
|
||||
fixture.appendChild(container);
|
||||
|
||||
const TestPopover = class extends Popover {
|
||||
onPositioned() {
|
||||
assert.step("onPositioned");
|
||||
}
|
||||
};
|
||||
await mount(TestPopover, fixture, {
|
||||
env,
|
||||
props: { target: container, position: "bottom-fit", fixedPosition: true },
|
||||
});
|
||||
|
||||
assert.verifySteps(["onPositioned"]);
|
||||
|
||||
// force the DOM update
|
||||
container.style.height = "125px";
|
||||
container.style.alignItems = "flex-end";
|
||||
triggerEvent(document, null, "scroll");
|
||||
await nextTick();
|
||||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { registerCleanup } from "../helpers/cleanup";
|
|||
import {
|
||||
destroy,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
mockAnimationFrame,
|
||||
mount,
|
||||
nextTick,
|
||||
|
|
@ -13,25 +14,44 @@ import {
|
|||
triggerEvent,
|
||||
} from "../helpers/utils";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
|
||||
import { Component, useRef, xml } from "@odoo/owl";
|
||||
|
||||
const FLEXBOX_STYLE = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
};
|
||||
const CONTAINER_STYLE = {
|
||||
...FLEXBOX_STYLE,
|
||||
backgroundColor: "salmon",
|
||||
height: "450px",
|
||||
width: "450px",
|
||||
margin: "25px",
|
||||
};
|
||||
const TARGET_STYLE = {
|
||||
backgroundColor: "tomato",
|
||||
height: "50px",
|
||||
width: "50px",
|
||||
};
|
||||
let container;
|
||||
|
||||
/**
|
||||
* @param {import("@web/core/position_hook").Options} popperOptions
|
||||
* @returns {Component}
|
||||
*/
|
||||
function getTestComponent(popperOptions = {}) {
|
||||
const reference = document.createElement("div");
|
||||
reference.id = "reference";
|
||||
reference.style.backgroundColor = "yellow";
|
||||
reference.style.height = "50px";
|
||||
reference.style.width = "50px";
|
||||
container.appendChild(reference);
|
||||
function getTestComponent(popperOptions = {}, target = document.createElement("div")) {
|
||||
popperOptions.container = popperOptions.container || container;
|
||||
|
||||
target.id = "target";
|
||||
Object.assign(target.style, TARGET_STYLE);
|
||||
if (!target.isConnected) {
|
||||
// If the target is not in any DOM, we append it to the container by default
|
||||
popperOptions.container.appendChild(target);
|
||||
}
|
||||
|
||||
class TestComp extends Component {
|
||||
setup() {
|
||||
usePosition(reference, { container, ...popperOptions });
|
||||
usePosition("popper", () => target, popperOptions);
|
||||
}
|
||||
}
|
||||
TestComp.template = xml`<div id="popper" t-ref="popper" />`;
|
||||
|
|
@ -43,12 +63,7 @@ QUnit.module("usePosition Hook", {
|
|||
// Force container style, to make these tests independent of screen size
|
||||
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";
|
||||
Object.assign(container.style, CONTAINER_STYLE);
|
||||
getFixture().prepend(container);
|
||||
registerCleanup(() => {
|
||||
getFixture().removeChild(container);
|
||||
|
|
@ -57,7 +72,7 @@ QUnit.module("usePosition Hook", {
|
|||
const sheet = document.createElement("style");
|
||||
sheet.textContent = `
|
||||
#popper {
|
||||
background-color: cyan;
|
||||
background-color: plum;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
|
|
@ -105,35 +120,35 @@ QUnit.test("can add margin", async (assert) => {
|
|||
const TestComp = getTestComponent(popperOptions);
|
||||
const popper = await mount(TestComp, container);
|
||||
const popBox = document.getElementById("popper").getBoundingClientRect();
|
||||
const refBox = document.getElementById("reference").getBoundingClientRect();
|
||||
const targetBox = document.getElementById("target").getBoundingClientRect();
|
||||
destroy(popper);
|
||||
container.removeChild(document.getElementById("reference"));
|
||||
return [popBox, refBox];
|
||||
container.removeChild(document.getElementById("target"));
|
||||
return [popBox, targetBox];
|
||||
}
|
||||
|
||||
// With/without additional margin (default direction is bottom)
|
||||
let [popBox, refBox] = await _mountTestComponentAndDestroy();
|
||||
assert.strictEqual(popBox.top, refBox.bottom + SHEET_MARGINS.top);
|
||||
[popBox, refBox] = await _mountTestComponentAndDestroy({ margin: 10 });
|
||||
assert.strictEqual(popBox.top, refBox.bottom + SHEET_MARGINS.top + 10);
|
||||
let [popBox, targetBox] = await _mountTestComponentAndDestroy();
|
||||
assert.strictEqual(popBox.top, targetBox.bottom + SHEET_MARGINS.top);
|
||||
[popBox, targetBox] = await _mountTestComponentAndDestroy({ margin: 10 });
|
||||
assert.strictEqual(popBox.top, targetBox.bottom + SHEET_MARGINS.top + 10);
|
||||
|
||||
// With/without additional margin, direction is top
|
||||
[popBox, refBox] = await _mountTestComponentAndDestroy({ position: "top" });
|
||||
assert.strictEqual(popBox.top, refBox.top - popBox.height - SHEET_MARGINS.bottom);
|
||||
[popBox, refBox] = await _mountTestComponentAndDestroy({ position: "top", margin: 10 });
|
||||
assert.strictEqual(popBox.top, refBox.top - popBox.height - SHEET_MARGINS.bottom - 10);
|
||||
[popBox, targetBox] = await _mountTestComponentAndDestroy({ position: "top" });
|
||||
assert.strictEqual(popBox.top, targetBox.top - popBox.height - SHEET_MARGINS.bottom);
|
||||
[popBox, targetBox] = await _mountTestComponentAndDestroy({ position: "top", margin: 10 });
|
||||
assert.strictEqual(popBox.top, targetBox.top - popBox.height - SHEET_MARGINS.bottom - 10);
|
||||
|
||||
// With/without additional margin, direction is left
|
||||
[popBox, refBox] = await _mountTestComponentAndDestroy({ position: "left" });
|
||||
assert.strictEqual(popBox.left, refBox.left - popBox.width - SHEET_MARGINS.right);
|
||||
[popBox, refBox] = await _mountTestComponentAndDestroy({ position: "left", margin: 10 });
|
||||
assert.strictEqual(popBox.left, refBox.left - popBox.width - SHEET_MARGINS.right - 10);
|
||||
[popBox, targetBox] = await _mountTestComponentAndDestroy({ position: "left" });
|
||||
assert.strictEqual(popBox.left, targetBox.left - popBox.width - SHEET_MARGINS.right);
|
||||
[popBox, targetBox] = await _mountTestComponentAndDestroy({ position: "left", margin: 10 });
|
||||
assert.strictEqual(popBox.left, targetBox.left - popBox.width - SHEET_MARGINS.right - 10);
|
||||
|
||||
// With/without additional margin, direction is right
|
||||
[popBox, refBox] = await _mountTestComponentAndDestroy({ position: "right" });
|
||||
assert.strictEqual(popBox.left, refBox.right + SHEET_MARGINS.left);
|
||||
[popBox, refBox] = await _mountTestComponentAndDestroy({ position: "right", margin: 10 });
|
||||
assert.strictEqual(popBox.left, refBox.right + SHEET_MARGINS.left + 10);
|
||||
[popBox, targetBox] = await _mountTestComponentAndDestroy({ position: "right" });
|
||||
assert.strictEqual(popBox.left, targetBox.right + SHEET_MARGINS.left);
|
||||
[popBox, targetBox] = await _mountTestComponentAndDestroy({ position: "right", margin: 10 });
|
||||
assert.strictEqual(popBox.left, targetBox.right + SHEET_MARGINS.left + 10);
|
||||
});
|
||||
|
||||
QUnit.test("is restricted to its container, even with margins", async (assert) => {
|
||||
|
|
@ -154,14 +169,14 @@ QUnit.test("is restricted to its container, even with margins", async (assert) =
|
|||
});
|
||||
const popper = await mount(TestComp, container);
|
||||
destroy(popper);
|
||||
container.removeChild(document.getElementById("reference"));
|
||||
container.removeChild(document.getElementById("target"));
|
||||
}
|
||||
|
||||
const minSize = 150; // => popper is 100px, ref is 50px
|
||||
const minSize = 150; // => popper is 100px, target is 50px
|
||||
const margin = 10; // will serve as additional margin
|
||||
|
||||
// === DIRECTION: BOTTOM ===
|
||||
// Container style changes: push ref to top
|
||||
// Container style changes: push target to top
|
||||
Object.assign(container.style, { alignItems: "flex-start" });
|
||||
|
||||
// --> Without additional margin
|
||||
|
|
@ -185,7 +200,7 @@ QUnit.test("is restricted to its container, even with margins", async (assert) =
|
|||
assert.verifySteps(["right-start"]);
|
||||
|
||||
// === DIRECTION: TOP ===
|
||||
// Container style changes: push ref to bottom
|
||||
// Container style changes: push target to bottom
|
||||
Object.assign(container.style, { alignItems: "flex-end" });
|
||||
|
||||
// --> Without additional margin
|
||||
|
|
@ -213,7 +228,7 @@ QUnit.test("is restricted to its container, even with margins", async (assert) =
|
|||
// === DIRECTION: LEFT ===
|
||||
// Container style changes: reset previous changes
|
||||
Object.assign(container.style, { alignItems: "center", height: "450px" });
|
||||
// Container style changes: push ref to right
|
||||
// Container style changes: push target to right
|
||||
Object.assign(container.style, { justifyContent: "flex-end" });
|
||||
|
||||
// --> Without additional margin
|
||||
|
|
@ -239,7 +254,7 @@ QUnit.test("is restricted to its container, even with margins", async (assert) =
|
|||
assert.verifySteps(["bottom-end"]);
|
||||
|
||||
// === DIRECTION: RIGHT ===
|
||||
// Container style changes: push ref to left
|
||||
// Container style changes: push target to left
|
||||
Object.assign(container.style, { justifyContent: "flex-start" });
|
||||
|
||||
// --> Without additional margin
|
||||
|
|
@ -279,25 +294,8 @@ QUnit.test("popper is an inner element", async (assert) => {
|
|||
await mount(TestComp, container);
|
||||
});
|
||||
|
||||
QUnit.test("can change the popper reference name", async (assert) => {
|
||||
assert.expect(2);
|
||||
const TestComp = getTestComponent({
|
||||
popper: "myRef",
|
||||
onPositioned: (el) => {
|
||||
assert.notOk(document.getElementById("not-popper") === el);
|
||||
assert.ok(document.getElementById("popper") === el);
|
||||
},
|
||||
});
|
||||
TestComp.template = xml`
|
||||
<div id="not-popper">
|
||||
<div id="popper" t-ref="myRef"/>
|
||||
</div>
|
||||
`;
|
||||
await mount(TestComp, container);
|
||||
});
|
||||
|
||||
QUnit.test("has no effect when component is destroyed", async (assert) => {
|
||||
const execRegisteredCallbacks = mockAnimationFrame();
|
||||
mockAnimationFrame();
|
||||
const TestComp = getTestComponent({
|
||||
onPositioned: () => {
|
||||
assert.step("onPositioned called");
|
||||
|
|
@ -306,16 +304,12 @@ QUnit.test("has no effect when component is destroyed", async (assert) => {
|
|||
const comp = await mount(TestComp, container);
|
||||
assert.verifySteps(["onPositioned called"], "onPositioned called when component mounted");
|
||||
|
||||
triggerEvent(document, null, "scroll");
|
||||
await nextTick();
|
||||
assert.verifySteps([]);
|
||||
execRegisteredCallbacks();
|
||||
await triggerEvent(document, null, "scroll");
|
||||
assert.verifySteps(["onPositioned called"], "onPositioned called when document scrolled");
|
||||
|
||||
triggerEvent(document, null, "scroll");
|
||||
await nextTick();
|
||||
destroy(comp);
|
||||
execRegisteredCallbacks();
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
[],
|
||||
"onPositioned not called even if scroll happened right before the component destroys"
|
||||
|
|
@ -334,6 +328,20 @@ QUnit.test("reposition popper when a load event occurs", async (assert) => {
|
|||
assert.verifySteps(["onPositioned called"], "onPositioned called when load event is triggered");
|
||||
});
|
||||
|
||||
QUnit.test("reposition popper when a scroll event occurs", async (assert) => {
|
||||
const TestComp = getTestComponent({
|
||||
onPositioned: () => {
|
||||
assert.step("onPositioned called");
|
||||
},
|
||||
});
|
||||
await mount(TestComp, container);
|
||||
assert.verifySteps(["onPositioned called"]);
|
||||
await document.querySelector("#popper").dispatchEvent(new Event("scroll"));
|
||||
assert.verifySteps([], "onPositioned not called when scroll event is triggered inside popper");
|
||||
await document.querySelector("#popper").parentElement.dispatchEvent(new Event("scroll"));
|
||||
assert.verifySteps(["onPositioned called"]);
|
||||
});
|
||||
|
||||
QUnit.test("is positioned relative to its containing block", async (assert) => {
|
||||
const fixtureBox = getFixture().getBoundingClientRect();
|
||||
// offset the container
|
||||
|
|
@ -349,7 +357,7 @@ QUnit.test("is positioned relative to its containing block", async (assert) => {
|
|||
|
||||
const popBox1 = document.getElementById("popper").getBoundingClientRect();
|
||||
destroy(popper);
|
||||
document.getElementById("reference").remove();
|
||||
document.getElementById("target").remove();
|
||||
|
||||
// make container the containing block instead of the viewport
|
||||
container.style.contain = "layout";
|
||||
|
|
@ -371,36 +379,283 @@ QUnit.test("is positioned relative to its containing block", async (assert) => {
|
|||
assert.equal(popBox1.left, popBox2.left);
|
||||
});
|
||||
|
||||
QUnit.test("iframe: popper is outside, target inside", async (assert) => {
|
||||
// Prepare target inside iframe
|
||||
const IFRAME_STYLE = {
|
||||
margin: "25px",
|
||||
height: "200px",
|
||||
width: "400px",
|
||||
};
|
||||
const iframe = document.createElement("iframe");
|
||||
Object.assign(iframe.style, IFRAME_STYLE);
|
||||
iframe.srcdoc = `<div id="target" />`;
|
||||
const def = makeDeferred();
|
||||
iframe.onload = def.resolve;
|
||||
container.appendChild(iframe);
|
||||
await def;
|
||||
const iframeBody = iframe.contentDocument.body;
|
||||
Object.assign(iframeBody.style, {
|
||||
...FLEXBOX_STYLE,
|
||||
backgroundColor: "papayawhip",
|
||||
height: "300px",
|
||||
width: "400px",
|
||||
overflowX: "hidden",
|
||||
});
|
||||
|
||||
// Prepare popper outside iframe
|
||||
const popperTarget = iframe.contentDocument.getElementById("target");
|
||||
let onPositionedArgs;
|
||||
const Popper = getTestComponent(
|
||||
{
|
||||
onPositioned: (el, solution) => {
|
||||
onPositionedArgs = { el, solution };
|
||||
assert.step(`${solution.direction}-${solution.variant}`);
|
||||
},
|
||||
},
|
||||
popperTarget
|
||||
);
|
||||
await mount(Popper, container);
|
||||
assert.verifySteps(["bottom-middle"]);
|
||||
|
||||
// Check everything is rendered where it should be
|
||||
assert.containsOnce(container, "#popper");
|
||||
assert.containsNone(container, "#target");
|
||||
|
||||
assert.strictEqual(iframeBody.querySelectorAll("#target").length, 1);
|
||||
assert.strictEqual(iframeBody.querySelectorAll("#popper").length, 0);
|
||||
|
||||
// Check the expected position
|
||||
const { top: iframeTop, left: iframeLeft } = iframe.getBoundingClientRect();
|
||||
let targetBox = popperTarget.getBoundingClientRect();
|
||||
let popperBox = onPositionedArgs.el.getBoundingClientRect();
|
||||
let expectedTop = iframeTop + targetBox.top + popperTarget.offsetHeight;
|
||||
let expectedLeft =
|
||||
iframeLeft + targetBox.left + popperTarget.offsetWidth / 2 - popperBox.width / 2;
|
||||
|
||||
assert.strictEqual(popperBox.top, expectedTop);
|
||||
assert.strictEqual(popperBox.top, onPositionedArgs.solution.top);
|
||||
|
||||
assert.strictEqual(popperBox.left, expectedLeft);
|
||||
assert.strictEqual(popperBox.left, onPositionedArgs.solution.left);
|
||||
|
||||
// Scrolling inside the iframe should reposition the popover accordingly
|
||||
const previousPositionSolution = onPositionedArgs.solution;
|
||||
const scrollOffset = 100;
|
||||
const scrollable = iframe.contentDocument.documentElement;
|
||||
scrollable.scrollTop = scrollOffset;
|
||||
await nextTick();
|
||||
assert.verifySteps(["bottom-middle"]);
|
||||
assert.strictEqual(previousPositionSolution.top, onPositionedArgs.solution.top + scrollOffset);
|
||||
|
||||
// Check the expected position
|
||||
targetBox = popperTarget.getBoundingClientRect();
|
||||
popperBox = onPositionedArgs.el.getBoundingClientRect();
|
||||
expectedTop = iframeTop + targetBox.top + popperTarget.offsetHeight;
|
||||
expectedLeft = iframeLeft + targetBox.left + popperTarget.offsetWidth / 2 - popperBox.width / 2;
|
||||
|
||||
assert.strictEqual(popperBox.top, expectedTop);
|
||||
assert.strictEqual(popperBox.top, onPositionedArgs.solution.top);
|
||||
|
||||
assert.strictEqual(popperBox.left, expectedLeft);
|
||||
assert.strictEqual(popperBox.left, onPositionedArgs.solution.left);
|
||||
});
|
||||
|
||||
QUnit.test("iframe: both popper and target inside", async (assert) => {
|
||||
// Prepare target inside iframe
|
||||
const IFRAME_STYLE = {
|
||||
height: "300px",
|
||||
width: "400px",
|
||||
};
|
||||
const iframe = document.createElement("iframe");
|
||||
Object.assign(iframe.style, IFRAME_STYLE);
|
||||
iframe.srcdoc = `<div id="inner-container" />`;
|
||||
let def = makeDeferred();
|
||||
iframe.onload = def.resolve;
|
||||
container.appendChild(iframe);
|
||||
await def; // wait for the iframe to be loaded
|
||||
const iframeBody = iframe.contentDocument.body;
|
||||
Object.assign(iframeBody.style, {
|
||||
...FLEXBOX_STYLE,
|
||||
backgroundColor: "papayawhip",
|
||||
margin: "25px",
|
||||
overflowX: "hidden",
|
||||
});
|
||||
|
||||
def = makeDeferred();
|
||||
const iframeSheet = iframe.contentDocument.createElement("style");
|
||||
iframeSheet.onload = def.resolve;
|
||||
iframeSheet.textContent = `
|
||||
#popper {
|
||||
background-color: plum;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
`;
|
||||
iframe.contentDocument.head.appendChild(iframeSheet);
|
||||
await def; // wait for the iframe's stylesheet to be loaded
|
||||
|
||||
const innerContainer = iframe.contentDocument.getElementById("inner-container");
|
||||
Object.assign(innerContainer.style, {
|
||||
...CONTAINER_STYLE,
|
||||
backgroundColor: "khaki",
|
||||
});
|
||||
|
||||
// Prepare popper inside iframe
|
||||
let onPositionedArgs;
|
||||
const Popper = getTestComponent({
|
||||
container: innerContainer,
|
||||
onPositioned: (el, solution) => {
|
||||
onPositionedArgs = { el, solution };
|
||||
assert.step(`${solution.direction}-${solution.variant}`);
|
||||
},
|
||||
});
|
||||
await mount(Popper, innerContainer);
|
||||
assert.verifySteps(["bottom-middle"]);
|
||||
|
||||
// Check everything is rendered where it should be
|
||||
assert.strictEqual(innerContainer.ownerDocument, iframe.contentDocument);
|
||||
assert.strictEqual(innerContainer.querySelectorAll("#target").length, 1);
|
||||
assert.strictEqual(innerContainer.querySelectorAll("#popper").length, 1);
|
||||
assert.strictEqual(iframeBody.querySelectorAll("#target").length, 1);
|
||||
assert.strictEqual(iframeBody.querySelectorAll("#popper").length, 1);
|
||||
|
||||
// Check the expected position
|
||||
const popperTarget = innerContainer.querySelector("#target");
|
||||
// const { top: iframeTop, left: iframeLeft } = iframe.getBoundingClientRect();
|
||||
let targetBox = popperTarget.getBoundingClientRect();
|
||||
let popperBox = onPositionedArgs.el.getBoundingClientRect();
|
||||
let expectedTop = targetBox.top + popperTarget.offsetHeight;
|
||||
let expectedLeft = targetBox.left + popperTarget.offsetWidth / 2 - popperBox.width / 2;
|
||||
|
||||
assert.strictEqual(popperBox.top, expectedTop);
|
||||
assert.strictEqual(popperBox.top, onPositionedArgs.solution.top);
|
||||
|
||||
assert.strictEqual(popperBox.left, expectedLeft);
|
||||
assert.strictEqual(popperBox.left, onPositionedArgs.solution.left);
|
||||
|
||||
// Scrolling inside the iframe should reposition the popover accordingly
|
||||
const previousPositionSolution = onPositionedArgs.solution;
|
||||
const scrollOffset = 100;
|
||||
const scrollable = iframe.contentDocument.documentElement;
|
||||
scrollable.scrollTop = scrollOffset;
|
||||
await nextTick();
|
||||
assert.verifySteps(["bottom-middle"]);
|
||||
assert.strictEqual(previousPositionSolution.top, onPositionedArgs.solution.top + scrollOffset);
|
||||
|
||||
// Check the expected position
|
||||
targetBox = popperTarget.getBoundingClientRect();
|
||||
popperBox = onPositionedArgs.el.getBoundingClientRect();
|
||||
expectedTop = targetBox.top + popperTarget.offsetHeight;
|
||||
expectedLeft = targetBox.left + popperTarget.offsetWidth / 2 - popperBox.width / 2;
|
||||
|
||||
assert.strictEqual(popperBox.top, expectedTop);
|
||||
assert.strictEqual(popperBox.top, onPositionedArgs.solution.top);
|
||||
|
||||
assert.strictEqual(popperBox.left, expectedLeft);
|
||||
assert.strictEqual(popperBox.left, onPositionedArgs.solution.left);
|
||||
});
|
||||
|
||||
QUnit.test("iframe: default container is the popper owner's document", async (assert) => {
|
||||
assert.expect(1);
|
||||
// Prepare an outer iframe, that will hold the popper element
|
||||
let def = makeDeferred();
|
||||
const outerIframe = document.createElement("iframe");
|
||||
Object.assign(outerIframe.style, { height: "450px", width: "450px" });
|
||||
outerIframe.onload = def.resolve;
|
||||
getFixture().prepend(outerIframe);
|
||||
registerCleanup(() => outerIframe.remove());
|
||||
await def;
|
||||
Object.assign(outerIframe.contentDocument.body.style, {
|
||||
...CONTAINER_STYLE,
|
||||
margin: "0",
|
||||
justifyContent: "flex-start",
|
||||
});
|
||||
|
||||
def = makeDeferred();
|
||||
const iframeSheet = outerIframe.contentDocument.createElement("style");
|
||||
iframeSheet.onload = def.resolve;
|
||||
iframeSheet.textContent = `
|
||||
#popper {
|
||||
background-color: plum;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
`;
|
||||
outerIframe.contentDocument.head.appendChild(iframeSheet);
|
||||
await def; // wait for the iframe's stylesheet to be loaded
|
||||
|
||||
// Prepare the inner iframe, that will hold the target element
|
||||
def = makeDeferred();
|
||||
const innerIframe = document.createElement("iframe");
|
||||
innerIframe.srcdoc = `<div id="target" />`;
|
||||
Object.assign(innerIframe.style, {
|
||||
height: "300px",
|
||||
width: "120px",
|
||||
marginLeft: "10px",
|
||||
});
|
||||
innerIframe.onload = def.resolve;
|
||||
outerIframe.contentDocument.body.appendChild(innerIframe);
|
||||
await def;
|
||||
Object.assign(innerIframe.contentDocument.body.style, {
|
||||
...FLEXBOX_STYLE,
|
||||
height: "300px",
|
||||
width: "120px",
|
||||
margin: "0",
|
||||
});
|
||||
|
||||
// Prepare the target element
|
||||
const target = innerIframe.contentDocument.getElementById("target");
|
||||
Object.assign(target.style, TARGET_STYLE);
|
||||
|
||||
// Mount the popper component and check its position
|
||||
class Popper extends Component {
|
||||
static template = xml`<div id="popper" t-ref="popper" />`;
|
||||
setup() {
|
||||
usePosition("popper", () => target, {
|
||||
position: "top-start",
|
||||
onPositioned: (_, { direction, variant }) => {
|
||||
assert.strictEqual(`${direction}-${variant}`, "top-start");
|
||||
// the style setup in this test leaves enough space in the inner iframe
|
||||
// for the popper to be positioned at top-middle, but this is exactly
|
||||
// what we want to avoid: the popper's base container should not be the
|
||||
// inner iframe, but the outer iframe, so the popper should be positioned
|
||||
// at top-start.
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
await mount(Popper, outerIframe.contentDocument.body);
|
||||
});
|
||||
|
||||
QUnit.test("popper as child of another", async (assert) => {
|
||||
class Child extends Component {
|
||||
static template = /* xml */ xml`
|
||||
<div id="child">
|
||||
<div class="target" t-ref="ref" />
|
||||
<div class="popper" t-ref="popper" />
|
||||
</div>
|
||||
`;
|
||||
setup() {
|
||||
const ref = useRef("ref");
|
||||
usePosition(() => ref.el, { popper: "popper", container, position: "left" });
|
||||
usePosition("popper", () => ref.el, { position: "left" });
|
||||
}
|
||||
}
|
||||
Child.template = /* xml */ xml`
|
||||
<div id="child">
|
||||
<div class="ref" t-ref="ref" />
|
||||
<div class="popper" t-ref="popper" />
|
||||
</div>
|
||||
`;
|
||||
const reference = container.querySelector("#reference");
|
||||
const target = document.createElement("div");
|
||||
target.id = "target";
|
||||
Object.assign(target.style, TARGET_STYLE);
|
||||
container.appendChild(target);
|
||||
class Parent extends Component {
|
||||
static components = { Child };
|
||||
static template = /* xml */ xml`<div id="popper" t-ref="popper"><Child/></div>`;
|
||||
setup() {
|
||||
usePosition(reference, { container });
|
||||
usePosition("popper", () => target);
|
||||
}
|
||||
}
|
||||
Parent.components = { Child };
|
||||
Parent.template = /* xml */ xml`
|
||||
<div id="popper">
|
||||
<Child/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const sheet = document.createElement("style");
|
||||
sheet.textContent = `
|
||||
#child .ref {
|
||||
background-color: salmon;
|
||||
#child .target {
|
||||
background-color: peachpuff;
|
||||
height: 100px;
|
||||
width: 10px;
|
||||
}
|
||||
|
|
@ -416,7 +671,6 @@ QUnit.test("popper as child of another", async (assert) => {
|
|||
await mount(Parent, container);
|
||||
const parentPopBox1 = container.querySelector("#popper").getBoundingClientRect();
|
||||
const childPopBox1 = container.querySelector("#child .popper").getBoundingClientRect();
|
||||
|
||||
const spacer = document.createElement("div");
|
||||
spacer.id = "foo";
|
||||
spacer.style.height = "1px";
|
||||
|
|
@ -433,6 +687,35 @@ QUnit.test("popper as child of another", async (assert) => {
|
|||
assert.strictEqual(childPopBox2.left, childPopBox1.left + spacer.offsetWidth * 0.5);
|
||||
});
|
||||
|
||||
QUnit.test("batch update call", async (assert) => {
|
||||
const target = document.createElement("div");
|
||||
target.id = "target";
|
||||
Object.assign(target.style, TARGET_STYLE);
|
||||
container.appendChild(target);
|
||||
|
||||
let position = null;
|
||||
|
||||
class TestComponent extends Component {
|
||||
static template = xml`<div class="popper" t-ref="popper">Popper</div>`;
|
||||
setup() {
|
||||
position = usePosition("popper", () => target, {
|
||||
onPositioned: () => {
|
||||
assert.step("positioned");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mount(TestComponent, container);
|
||||
assert.verifySteps(["positioned"]);
|
||||
|
||||
position.unlock();
|
||||
position.unlock();
|
||||
position.unlock();
|
||||
await nextTick();
|
||||
assert.verifySteps(["positioned"]);
|
||||
});
|
||||
|
||||
function getPositionTest(position, positionToCheck) {
|
||||
return async (assert) => {
|
||||
assert.expect(2);
|
||||
|
|
@ -498,8 +781,8 @@ const CONTAINER_STYLE_MAP = {
|
|||
left: { justifyContent: "flex-start" },
|
||||
right: { justifyContent: "flex-end" },
|
||||
slimfit: { height: "100px", width: "100px" }, // height and width of popper
|
||||
h125: { height: "125px" }, // height of popper + 1/2 reference
|
||||
w125: { width: "125px" }, // width of popper + 1/2 reference
|
||||
h125: { height: "125px" }, // height of popper + 1/2 target
|
||||
w125: { width: "125px" }, // width of popper + 1/2 target
|
||||
};
|
||||
|
||||
function getRepositionTest(from, to, containerStyleChanges) {
|
||||
|
|
@ -1114,3 +1397,32 @@ QUnit.test(
|
|||
"reposition from right-end to top-end",
|
||||
getRepositionTest("right-end", "top-end", "w125 right")
|
||||
);
|
||||
|
||||
function getFittingTest(position, styleAttribute) {
|
||||
return async (assert) => {
|
||||
const TestComp = getTestComponent({ position });
|
||||
await mount(TestComp, container);
|
||||
assert.strictEqual(container.querySelector("#popper").style[styleAttribute], "50px");
|
||||
};
|
||||
}
|
||||
|
||||
QUnit.test(
|
||||
"reposition from bottom-fit to top-fit",
|
||||
getRepositionTest("bottom-fit", "top-fit", "bottom")
|
||||
);
|
||||
QUnit.test(
|
||||
"reposition from top-fit to bottom-fit",
|
||||
getRepositionTest("top-fit", "bottom-fit", "top")
|
||||
);
|
||||
QUnit.test(
|
||||
"reposition from right-fit to left-fit",
|
||||
getRepositionTest("right-fit", "left-fit", "right")
|
||||
);
|
||||
QUnit.test(
|
||||
"reposition from left-fit to right-fit",
|
||||
getRepositionTest("left-fit", "right-fit", "left")
|
||||
);
|
||||
QUnit.test("bottom-fit has the same width as the target", getFittingTest("bottom-fit", "width"));
|
||||
QUnit.test("top-fit has the same width as the target", getFittingTest("top-fit", "width"));
|
||||
QUnit.test("left-fit has the same height as the target", getFittingTest("left-fit", "height"));
|
||||
QUnit.test("right-fit has the same height as the target", getFittingTest("right-fit", "height"));
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ QUnit.module("py", {}, () => {
|
|||
QUnit.test("throws when period negative", (assert) => {
|
||||
const matcher = (errorMessage) => {
|
||||
return function match(err) {
|
||||
return err.message === errorMessage;
|
||||
return err.message.includes(errorMessage);
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
import { evaluateExpr, evaluateBooleanExpr } from "@web/core/py_js/py";
|
||||
|
||||
QUnit.module("py", {}, () => {
|
||||
QUnit.module("interpreter", () => {
|
||||
|
|
@ -29,6 +29,8 @@ QUnit.module("py", {}, () => {
|
|||
assert.strictEqual(evaluateExpr('""'), "");
|
||||
assert.strictEqual(evaluateExpr('"foo"'), "foo");
|
||||
assert.strictEqual(evaluateExpr("'foo'"), "foo");
|
||||
assert.strictEqual(evaluateExpr("'FOO'.lower()"), "foo");
|
||||
assert.strictEqual(evaluateExpr("'foo'.upper()"), "FOO");
|
||||
});
|
||||
|
||||
QUnit.test("boolean", (assert) => {
|
||||
|
|
@ -96,6 +98,7 @@ QUnit.module("py", {}, () => {
|
|||
assert.strictEqual(evaluateExpr("not False"), true);
|
||||
assert.strictEqual(evaluateExpr("not foo", { foo: false }), true);
|
||||
assert.strictEqual(evaluateExpr("not None"), true);
|
||||
assert.strictEqual(evaluateExpr("not []"), true);
|
||||
assert.strictEqual(evaluateExpr("True == False or True == True"), true);
|
||||
assert.strictEqual(evaluateExpr("False == True and False"), false);
|
||||
});
|
||||
|
|
@ -123,6 +126,7 @@ QUnit.module("py", {}, () => {
|
|||
assert.strictEqual(evaluateExpr('None or "bar"'), "bar");
|
||||
assert.strictEqual(evaluateExpr("False or None"), null);
|
||||
assert.strictEqual(evaluateExpr("0 or 1"), 1);
|
||||
assert.strictEqual(evaluateExpr("[] or False"), false);
|
||||
});
|
||||
|
||||
QUnit.module("values from context");
|
||||
|
|
@ -286,8 +290,19 @@ QUnit.module("py", {}, () => {
|
|||
QUnit.module("conversions");
|
||||
|
||||
QUnit.test("to bool", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("bool()"), false);
|
||||
assert.strictEqual(evaluateExpr("bool(0)"), false);
|
||||
assert.strictEqual(evaluateExpr("bool(1)"), true);
|
||||
assert.strictEqual(evaluateExpr("bool(False)"), false);
|
||||
assert.strictEqual(evaluateExpr("bool(True)"), true);
|
||||
assert.strictEqual(evaluateExpr("bool({})"), false);
|
||||
assert.strictEqual(evaluateExpr("bool({ 'a': 1 })"), true);
|
||||
assert.strictEqual(evaluateExpr("bool([])"), false);
|
||||
assert.strictEqual(evaluateExpr("bool([1])"), true);
|
||||
assert.strictEqual(evaluateExpr("bool('')"), false);
|
||||
assert.strictEqual(evaluateExpr("bool('foo')"), true);
|
||||
assert.strictEqual(evaluateExpr("bool(set())"), false);
|
||||
assert.strictEqual(evaluateExpr("bool(set([1]))"), true);
|
||||
assert.strictEqual(
|
||||
evaluateExpr("bool(date_deadline)", { date_deadline: "2008" }),
|
||||
true
|
||||
|
|
@ -321,6 +336,22 @@ QUnit.module("py", {}, () => {
|
|||
assert.strictEqual(evaluateExpr("{'a': 1}.get('b', 54)"), 54);
|
||||
});
|
||||
|
||||
QUnit.test("can get values from values 'context'", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("context.get('a')", { context: { a: 123 } }), 123);
|
||||
const values = { context: { a: { b: { c: 321 } } } };
|
||||
assert.strictEqual(evaluateExpr("context.get('a').b.c", values), 321);
|
||||
assert.strictEqual(evaluateExpr("context.get('a', {'e': 5}).b.c", values), 321);
|
||||
assert.strictEqual(evaluateExpr("context.get('d', 3)", values), 3);
|
||||
assert.strictEqual(evaluateExpr("context.get('d', {'e': 5})['e']", values), 5);
|
||||
});
|
||||
|
||||
QUnit.test("can check if a key is in the 'context'", (assert) => {
|
||||
assert.strictEqual(evaluateExpr("'a' in context", { context: { a: 123 } }), true);
|
||||
assert.strictEqual(evaluateExpr("'a' in context", { context: { b: 123 } }), false);
|
||||
assert.strictEqual(evaluateExpr("'a' not in context", { context: { a: 123 } }), false);
|
||||
assert.strictEqual(evaluateExpr("'a' not in context", { context: { b: 123 } }), true);
|
||||
});
|
||||
|
||||
QUnit.module("objects");
|
||||
|
||||
QUnit.test("can read values from object", (assert) => {
|
||||
|
|
@ -350,5 +381,184 @@ QUnit.module("py", {}, () => {
|
|||
QUnit.test("tuple in list", (assert) => {
|
||||
assert.deepEqual(evaluateExpr("[(1 + 2,'foo', True)]"), [[3, "foo", true]]);
|
||||
});
|
||||
|
||||
QUnit.module("evaluate to boolean");
|
||||
|
||||
QUnit.test("simple expression", (assert) => {
|
||||
assert.strictEqual(evaluateBooleanExpr("12"), true);
|
||||
assert.strictEqual(evaluateBooleanExpr("0"), false);
|
||||
assert.strictEqual(evaluateBooleanExpr("0 + 3 - 1"), true);
|
||||
assert.strictEqual(evaluateBooleanExpr("0 + 3 - 1 - 2"), false);
|
||||
assert.strictEqual(evaluateBooleanExpr('"foo"'), true);
|
||||
assert.strictEqual(evaluateBooleanExpr("[1]"), true);
|
||||
assert.strictEqual(evaluateBooleanExpr("[]"), false);
|
||||
});
|
||||
|
||||
QUnit.test("use contextual values", (assert) => {
|
||||
assert.strictEqual(evaluateBooleanExpr("a", { a: 12 }), true);
|
||||
assert.strictEqual(evaluateBooleanExpr("a", { a: 0 }), false);
|
||||
assert.strictEqual(evaluateBooleanExpr("0 + 3 - a", { a: 1 }), true);
|
||||
assert.strictEqual(evaluateBooleanExpr("0 + 3 - a - 2", { a: 1 }), false);
|
||||
assert.strictEqual(evaluateBooleanExpr("0 + 3 - a - b", { a: 1, b: 2 }), false);
|
||||
assert.strictEqual(evaluateBooleanExpr("a", { a: "foo" }), true);
|
||||
assert.strictEqual(evaluateBooleanExpr("a", { a: [1] }), true);
|
||||
assert.strictEqual(evaluateBooleanExpr("a", { a: [] }), false);
|
||||
});
|
||||
|
||||
QUnit.test("throw if has missing value", (assert) => {
|
||||
assert.throws(() => evaluateBooleanExpr("a", { b: 0 }));
|
||||
assert.strictEqual(evaluateBooleanExpr("1 or a"), true); // do not throw (lazy value)
|
||||
assert.throws(() => evaluateBooleanExpr("0 or a"));
|
||||
assert.throws(() => evaluateBooleanExpr("a or b", { b: true }));
|
||||
assert.throws(() => evaluateBooleanExpr("a and b", { b: true }));
|
||||
assert.throws(() => evaluateBooleanExpr("a()"));
|
||||
assert.throws(() => evaluateBooleanExpr("a[0]"));
|
||||
assert.throws(() => evaluateBooleanExpr("a.b"));
|
||||
assert.throws(() => evaluateBooleanExpr("0 + 3 - a", { b: 1 }));
|
||||
assert.throws(() => evaluateBooleanExpr("0 + 3 - a - 2", { b: 1 }));
|
||||
assert.throws(() => evaluateBooleanExpr("0 + 3 - a - b", { b: 2 }));
|
||||
});
|
||||
|
||||
QUnit.module("sets");
|
||||
|
||||
QUnit.test("static set", (assert) => {
|
||||
assert.deepEqual(evaluateExpr("set()"), new Set());
|
||||
assert.deepEqual(evaluateExpr("set([])"), new Set([]));
|
||||
assert.deepEqual(evaluateExpr("set([0])"), new Set([0]));
|
||||
assert.deepEqual(evaluateExpr("set([1])"), new Set([1]));
|
||||
assert.deepEqual(evaluateExpr("set([0, 0])"), new Set([0]));
|
||||
assert.deepEqual(evaluateExpr("set([0, 1])"), new Set([0, 1]));
|
||||
assert.deepEqual(evaluateExpr("set([1, 1])"), new Set([1]));
|
||||
|
||||
assert.deepEqual(evaluateExpr("set('')"), new Set());
|
||||
assert.deepEqual(evaluateExpr("set('a')"), new Set(["a"]));
|
||||
assert.deepEqual(evaluateExpr("set('ab')"), new Set(["a", "b"]));
|
||||
|
||||
assert.deepEqual(evaluateExpr("set({})"), new Set());
|
||||
assert.deepEqual(evaluateExpr("set({ 'a': 1 })"), new Set(["a"]));
|
||||
assert.deepEqual(evaluateExpr("set({ '': 1, 'a': 1 })"), new Set(["", "a"]));
|
||||
|
||||
assert.throws(() => evaluateExpr("set(0)"));
|
||||
assert.throws(() => evaluateExpr("set(1)"));
|
||||
assert.throws(() => evaluateExpr("set(None)"));
|
||||
assert.throws(() => evaluateExpr("set(false)"));
|
||||
assert.throws(() => evaluateExpr("set(true)"));
|
||||
assert.throws(() => evaluateExpr("set(1, 2)"));
|
||||
|
||||
assert.throws(() => evaluateExpr("set(expr)", { expr: undefined }));
|
||||
assert.throws(() => evaluateExpr("set(expr)", { expr: null }));
|
||||
|
||||
assert.throws(() => evaluateExpr("set([], [])")); // valid but not supported by py_js
|
||||
|
||||
assert.throws(() => evaluateExpr("set({ 'a' })")); // valid but not supported by py_js
|
||||
});
|
||||
|
||||
QUnit.test("set intersection", (assert) => {
|
||||
assert.deepEqual(evaluateExpr("set([1,2,3]).intersection()"), new Set([1, 2, 3]));
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set([1,2,3]).intersection(set([2,3]))"),
|
||||
new Set([2, 3])
|
||||
);
|
||||
assert.deepEqual(evaluateExpr("set([1,2,3]).intersection([2,3])"), new Set([2, 3]));
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set([1,2,3]).intersection(r)", { r: [2, 3] }),
|
||||
new Set([2, 3])
|
||||
);
|
||||
assert.deepEqual(
|
||||
evaluateExpr("r.intersection([2,3])", { r: new Set([1, 2, 3, 2]) }),
|
||||
new Set([2, 3])
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set(foo_ids).intersection([2,3])", { foo_ids: [1, 2] }),
|
||||
new Set([2])
|
||||
);
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set(foo_ids).intersection([2,3])", { foo_ids: [1] }),
|
||||
new Set()
|
||||
);
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set([foo_id]).intersection([2,3])", { foo_id: 1 }),
|
||||
new Set()
|
||||
);
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set([foo_id]).intersection([2,3])", { foo_id: 2 }),
|
||||
new Set([2])
|
||||
);
|
||||
|
||||
assert.throws(() => evaluateExpr("set([]).intersection([], [])")); // valid but not supported by py_js
|
||||
assert.throws(() => evaluateExpr("set([]).intersection([], [], [])")); // valid but not supported by py_js
|
||||
});
|
||||
|
||||
QUnit.test("set difference", (assert) => {
|
||||
assert.deepEqual(evaluateExpr("set([1,2,3]).difference()"), new Set([1, 2, 3]));
|
||||
assert.deepEqual(evaluateExpr("set([1,2,3]).difference(set([2,3]))"), new Set([1]));
|
||||
assert.deepEqual(evaluateExpr("set([1,2,3]).difference([2,3])"), new Set([1]));
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set([1,2,3]).difference(r)", { r: [2, 3] }),
|
||||
new Set([1])
|
||||
);
|
||||
assert.deepEqual(
|
||||
evaluateExpr("r.difference([2,3])", { r: new Set([1, 2, 3, 2, 4]) }),
|
||||
new Set([1, 4])
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set(foo_ids).difference([2,3])", { foo_ids: [1, 2] }),
|
||||
new Set([1])
|
||||
);
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set(foo_ids).difference([2,3])", { foo_ids: [1] }),
|
||||
new Set([1])
|
||||
);
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set([foo_id]).difference([2,3])", { foo_id: 1 }),
|
||||
new Set([1])
|
||||
);
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set([foo_id]).difference([2,3])", { foo_id: 2 }),
|
||||
new Set()
|
||||
);
|
||||
|
||||
assert.throws(() => evaluateExpr("set([]).difference([], [])")); // valid but not supported by py_js
|
||||
assert.throws(() => evaluateExpr("set([]).difference([], [], [])")); // valid but not supported by py_js
|
||||
});
|
||||
|
||||
QUnit.test("set union", (assert) => {
|
||||
assert.deepEqual(evaluateExpr("set([1,2,3]).union()"), new Set([1, 2, 3]));
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set([1,2,3]).union(set([2,3,4]))"),
|
||||
new Set([1, 2, 3, 4])
|
||||
);
|
||||
assert.deepEqual(evaluateExpr("set([1,2,3]).union([2,4])"), new Set([1, 2, 3, 4]));
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set([1,2,3]).union(r)", { r: [2, 4] }),
|
||||
new Set([1, 2, 3, 4])
|
||||
);
|
||||
assert.deepEqual(
|
||||
evaluateExpr("r.union([2,3])", { r: new Set([1, 2, 2, 4]) }),
|
||||
new Set([1, 2, 3, 4])
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set(foo_ids).union([2,3])", { foo_ids: [1, 2] }),
|
||||
new Set([1, 2, 3])
|
||||
);
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set(foo_ids).union([2,3])", { foo_ids: [1] }),
|
||||
new Set([1, 2, 3])
|
||||
);
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set([foo_id]).union([2,3])", { foo_id: 1 }),
|
||||
new Set([1, 2, 3])
|
||||
);
|
||||
assert.deepEqual(
|
||||
evaluateExpr("set([foo_id]).union([2,3])", { foo_id: 2 }),
|
||||
new Set([2, 3])
|
||||
);
|
||||
|
||||
assert.throws(() => evaluateExpr("set([]).union([], [])")); // valid but not supported by py_js
|
||||
assert.throws(() => evaluateExpr("set([]).union([], [], [])")); // valid but not supported by py_js
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { EventBus, reactive } from "@odoo/owl";
|
||||
import { Reactive, effect, withComputedProperties } from "@web/core/utils/reactive";
|
||||
|
||||
QUnit.module("Reactive utils", () => {
|
||||
QUnit.module("Reactive class", () => {
|
||||
QUnit.test(
|
||||
"callback registered without Reactive class constructor will not notify",
|
||||
(assert) => {
|
||||
// This test exists to showcase why we need the Reactive class
|
||||
const bus = new EventBus();
|
||||
class MyReactiveClass {
|
||||
constructor() {
|
||||
this.counter = 0;
|
||||
bus.addEventListener("change", () => this.counter++);
|
||||
}
|
||||
}
|
||||
const obj = reactive(new MyReactiveClass(), () => {
|
||||
assert.step(`counter: ${obj.counter}`);
|
||||
});
|
||||
obj.counter; // initial subscription to counter
|
||||
obj.counter++;
|
||||
assert.verifySteps(["counter: 1"]);
|
||||
bus.trigger("change");
|
||||
assert.equal(obj.counter, 2);
|
||||
assert.verifySteps([
|
||||
// The mutation in the event handler was missed by the reactivity, this is because
|
||||
// the `this` in the event handler is captured during construction and is not reactive
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("callback registered in Reactive class constructor will notify", (assert) => {
|
||||
const bus = new EventBus();
|
||||
class MyReactiveClass extends Reactive {
|
||||
constructor() {
|
||||
super();
|
||||
this.counter = 0;
|
||||
bus.addEventListener("change", () => this.counter++);
|
||||
}
|
||||
}
|
||||
const obj = reactive(new MyReactiveClass(), () => {
|
||||
assert.step(`counter: ${obj.counter}`);
|
||||
});
|
||||
obj.counter; // initial subscription to counter
|
||||
obj.counter++;
|
||||
assert.verifySteps(["counter: 1"]);
|
||||
bus.trigger("change");
|
||||
assert.equal(obj.counter, 2);
|
||||
assert.verifySteps(["counter: 2"]);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("effect", () => {
|
||||
QUnit.test("effect runs once immediately", (assert) => {
|
||||
const state = reactive({ counter: 0 });
|
||||
assert.verifySteps([]);
|
||||
effect(
|
||||
(state) => {
|
||||
assert.step(`counter: ${state.counter}`);
|
||||
},
|
||||
[state]
|
||||
);
|
||||
assert.verifySteps(["counter: 0"]);
|
||||
});
|
||||
|
||||
QUnit.test("effect runs when reactive deps change", (assert) => {
|
||||
const state = reactive({ counter: 0 });
|
||||
assert.verifySteps([]);
|
||||
effect(
|
||||
(state) => {
|
||||
assert.step(`counter: ${state.counter}`);
|
||||
},
|
||||
[state]
|
||||
);
|
||||
assert.verifySteps(["counter: 0"], "effect runs immediately");
|
||||
state.counter++;
|
||||
assert.verifySteps(["counter: 1"], "first mutation runs the effect");
|
||||
state.counter++;
|
||||
assert.verifySteps(["counter: 2"], "subsequent mutations run the effect");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"Original reactive callback is not subscribed to keys observed by effect",
|
||||
(assert) => {
|
||||
let reactiveCallCount = 0;
|
||||
const state = reactive(
|
||||
{
|
||||
counter: 0,
|
||||
},
|
||||
() => reactiveCallCount++
|
||||
);
|
||||
assert.verifySteps([]);
|
||||
assert.equal(reactiveCallCount, 0);
|
||||
effect(
|
||||
(state) => {
|
||||
assert.step(`counter: ${state.counter}`);
|
||||
},
|
||||
[state]
|
||||
);
|
||||
assert.verifySteps(["counter: 0"]);
|
||||
assert.equal(reactiveCallCount, 0, "did not call the original reactive's callback");
|
||||
state.counter = 1;
|
||||
assert.verifySteps(["counter: 1"]);
|
||||
assert.equal(reactiveCallCount, 0, "did not call the original reactive's callback");
|
||||
state.counter; // subscribe the original reactive
|
||||
state.counter = 2;
|
||||
assert.verifySteps(["counter: 2"]);
|
||||
assert.equal(
|
||||
reactiveCallCount,
|
||||
1,
|
||||
"the original callback was called because it is subscribed independently"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("mutating keys not observed by the effect doesn't cause it to run", (assert) => {
|
||||
const state = reactive({ counter: 0, unobserved: 0 });
|
||||
effect(
|
||||
(state) => {
|
||||
assert.step(`counter: ${state.counter}`);
|
||||
},
|
||||
[state]
|
||||
);
|
||||
assert.verifySteps(["counter: 0"]);
|
||||
state.counter = 1;
|
||||
assert.verifySteps(["counter: 1"]);
|
||||
state.unobserved = 1;
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("withComputedProperties", () => {
|
||||
QUnit.test("computed properties are set immediately", (assert) => {
|
||||
const source = reactive({ counter: 1 });
|
||||
const derived = withComputedProperties(reactive({}), [source], {
|
||||
doubleCounter(source) {
|
||||
return source.counter * 2;
|
||||
},
|
||||
});
|
||||
assert.equal(derived.doubleCounter, 2);
|
||||
});
|
||||
|
||||
QUnit.test("computed properties are recomputed when dependencies change", (assert) => {
|
||||
const source = reactive({ counter: 1 });
|
||||
const derived = withComputedProperties(reactive({}), [source], {
|
||||
doubleCounter(source) {
|
||||
return source.counter * 2;
|
||||
},
|
||||
});
|
||||
assert.equal(derived.doubleCounter, 2);
|
||||
source.counter++;
|
||||
assert.equal(derived.doubleCounter, 4);
|
||||
});
|
||||
|
||||
QUnit.test("can observe computed properties", (assert) => {
|
||||
const source = reactive({ counter: 1 });
|
||||
const derived = withComputedProperties(reactive({}), [source], {
|
||||
doubleCounter(source) {
|
||||
return source.counter * 2;
|
||||
},
|
||||
});
|
||||
const observed = reactive(derived, () => {
|
||||
assert.step(`doubleCounter: ${observed.doubleCounter}`);
|
||||
});
|
||||
observed.doubleCounter; // subscribe to doubleCounter
|
||||
assert.verifySteps([]);
|
||||
source.counter++;
|
||||
assert.verifySteps(["doubleCounter: 4"]);
|
||||
});
|
||||
|
||||
QUnit.test("computed properties can use nested objects", (assert) => {
|
||||
const source = reactive({ subObj: { counter: 1 } });
|
||||
const derived = withComputedProperties(reactive({}), [source], {
|
||||
doubleCounter(source) {
|
||||
return source.subObj.counter * 2;
|
||||
},
|
||||
});
|
||||
const observed = reactive(derived, () => {
|
||||
assert.step(`doubleCounter: ${observed.doubleCounter}`);
|
||||
});
|
||||
observed.doubleCounter; // subscribe to doubleCounter
|
||||
assert.equal(observed.doubleCounter, 2);
|
||||
assert.verifySteps([]);
|
||||
source.subObj.counter++;
|
||||
assert.equal(derived.doubleCounter, 4);
|
||||
assert.verifySteps(
|
||||
["doubleCounter: 4"],
|
||||
"reactive gets notified even for computed properties dervied from nested objects"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { MultiRecordSelector } from "@web/core/record_selectors/multi_record_selector";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
import { getFixture, mount, click, triggerEvent, editInput } from "../../helpers/utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { nameService } from "@web/core/name_service";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
|
||||
QUnit.module("Web Components", (hooks) => {
|
||||
QUnit.module("MultiRecordSelector");
|
||||
|
||||
let target;
|
||||
const serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Display name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "Alice" },
|
||||
{ id: 2, display_name: "Bob" },
|
||||
{ id: 3, display_name: "Charlie" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async function makeMultiRecordSelector(props, { mockRPC } = {}) {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ resIds: props.resIds });
|
||||
}
|
||||
|
||||
get recordProps() {
|
||||
return {
|
||||
...props,
|
||||
resIds: this.state.resIds,
|
||||
update: (resIds) => this._update(resIds),
|
||||
};
|
||||
}
|
||||
|
||||
_update(resIds) {
|
||||
this.state.resIds = resIds;
|
||||
}
|
||||
}
|
||||
Parent.components = { MultiRecordSelector };
|
||||
Parent.template = xml`
|
||||
<MultiRecordSelector t-props="recordProps" />`;
|
||||
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
await mount(Parent, target, { env });
|
||||
}
|
||||
|
||||
hooks.beforeEach(async () => {
|
||||
target = getFixture();
|
||||
registry.category("services").add("hotkey", hotkeyService);
|
||||
registry.category("services").add("dialog", dialogService);
|
||||
registry.category("services").add("name", nameService);
|
||||
});
|
||||
|
||||
QUnit.test("Can be renderer with no values", async (assert) => {
|
||||
await makeMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [],
|
||||
});
|
||||
const input = target.querySelector(".o_multi_record_selector input");
|
||||
assert.strictEqual(input.value, "", "The input should be empty");
|
||||
assert.hasClass(input, "o_input");
|
||||
});
|
||||
|
||||
QUnit.test("Can be renderer with a value", async (assert) => {
|
||||
await makeMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1],
|
||||
});
|
||||
const input = target.querySelector(".o_multi_record_selector input");
|
||||
assert.strictEqual(input.value, "");
|
||||
assert.strictEqual(target.querySelectorAll(".o_tag").length, 1);
|
||||
assert.strictEqual(target.querySelector(".o_tag").textContent, "Alice");
|
||||
});
|
||||
|
||||
QUnit.test("Can be renderer with multiple values", async (assert) => {
|
||||
await makeMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1, 2],
|
||||
});
|
||||
const input = target.querySelector(".o_multi_record_selector input");
|
||||
assert.strictEqual(input.value, "");
|
||||
assert.strictEqual(target.querySelectorAll(".o_tag").length, 2);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_tag")].map((el) => el.textContent),
|
||||
["Alice", "Bob"]
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Can be updated from autocomplete", async (assert) => {
|
||||
await makeMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [],
|
||||
});
|
||||
const input = target.querySelector(".o_multi_record_selector input");
|
||||
assert.containsNone(target, ".o_tag");
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
await click(input);
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
|
||||
const secondItem = target.querySelectorAll("li.o-autocomplete--dropdown-item")[1];
|
||||
await click(secondItem);
|
||||
assert.containsOnce(target, ".o_tag");
|
||||
assert.strictEqual(target.querySelector(".o_tag").textContent, "Bob");
|
||||
});
|
||||
|
||||
QUnit.test("Display name is correctly fetched", async (assert) => {
|
||||
await makeMultiRecordSelector(
|
||||
{
|
||||
resModel: "partner",
|
||||
resIds: [1],
|
||||
},
|
||||
{
|
||||
mockRPC: (route, args) => {
|
||||
if (args.method === "web_search_read") {
|
||||
assert.step("web_search_read");
|
||||
assert.strictEqual(args.model, "partner");
|
||||
assert.deepEqual(args.kwargs.domain, [["id", "in", [1]]]);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
assert.strictEqual(target.querySelectorAll(".o_tag").length, 1);
|
||||
assert.strictEqual(target.querySelector(".o_tag").textContent, "Alice");
|
||||
assert.verifySteps(["web_search_read"]);
|
||||
});
|
||||
|
||||
QUnit.test("Can give domain and context props for the name search", async (assert) => {
|
||||
await makeMultiRecordSelector(
|
||||
{
|
||||
resModel: "partner",
|
||||
resIds: [1],
|
||||
domain: [["display_name", "=", "Bob"]],
|
||||
context: { blip: "blop " },
|
||||
},
|
||||
{
|
||||
mockRPC: (route, args) => {
|
||||
if (args.method === "name_search") {
|
||||
assert.step("name_search");
|
||||
assert.strictEqual(args.model, "partner");
|
||||
assert.deepEqual(args.kwargs.args, [
|
||||
"&",
|
||||
["display_name", "=", "Bob"],
|
||||
"!",
|
||||
["id", "in", [1]],
|
||||
]);
|
||||
assert.strictEqual(args.kwargs.context.blip, "blop ");
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
const input = target.querySelector(".o_multi_record_selector input");
|
||||
assert.verifySteps([]);
|
||||
await click(input);
|
||||
assert.verifySteps(["name_search"]);
|
||||
});
|
||||
|
||||
QUnit.test("Support placeholder", async (assert) => {
|
||||
await makeMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [],
|
||||
placeholder: "Select a partner",
|
||||
});
|
||||
const input = target.querySelector(".o_multi_record_selector input");
|
||||
assert.strictEqual(input.placeholder, "Select a partner");
|
||||
await click(input);
|
||||
const firstItem = target.querySelectorAll("li.o-autocomplete--dropdown-item")[0];
|
||||
await click(firstItem);
|
||||
assert.strictEqual(input.placeholder, "");
|
||||
});
|
||||
|
||||
QUnit.test("Placeholder is not set if values are selected", async (assert) => {
|
||||
await makeMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1],
|
||||
placeholder: "Select a partner",
|
||||
});
|
||||
const input = target.querySelector(".o_multi_record_selector input");
|
||||
assert.strictEqual(input.placeholder, "");
|
||||
});
|
||||
|
||||
QUnit.test("Can delete a tag with Backspace", async (assert) => {
|
||||
await makeMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1, 2],
|
||||
});
|
||||
await triggerEvent(target, ".o-autocomplete input", "keydown", { key: "Backspace" });
|
||||
assert.strictEqual(target.querySelectorAll(".o_tag").length, 1);
|
||||
assert.strictEqual(target.querySelector(".o_tag").textContent, "Alice");
|
||||
});
|
||||
|
||||
QUnit.test("Can focus tags with arrow right and left", async (assert) => {
|
||||
await makeMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1, 2],
|
||||
});
|
||||
target.querySelector(".o-autocomplete input").focus();
|
||||
await triggerEvent(document.activeElement, "", "keydown", { key: "arrowleft" });
|
||||
assert.strictEqual(document.activeElement.textContent, "Bob");
|
||||
await triggerEvent(document.activeElement, "", "keydown", { key: "arrowleft" });
|
||||
assert.strictEqual(document.activeElement.textContent, "Alice");
|
||||
await triggerEvent(document.activeElement, "", "keydown", { key: "arrowleft" });
|
||||
assert.hasClass(document.activeElement, "o-autocomplete--input");
|
||||
await triggerEvent(document.activeElement, "", "keydown", { key: "arrowright" });
|
||||
assert.strictEqual(document.activeElement.textContent, "Alice");
|
||||
await triggerEvent(document.activeElement, "", "keydown", { key: "arrowright" });
|
||||
assert.strictEqual(document.activeElement.textContent, "Bob");
|
||||
await triggerEvent(document.activeElement, "", "keydown", { key: "arrowright" });
|
||||
assert.hasClass(document.activeElement, "o-autocomplete--input");
|
||||
});
|
||||
|
||||
QUnit.test("Delete the focused element", async (assert) => {
|
||||
await makeMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1, 2],
|
||||
});
|
||||
target.querySelector(".o-autocomplete input").focus();
|
||||
await triggerEvent(document.activeElement, "", "keydown", { key: "arrowright" });
|
||||
assert.strictEqual(document.activeElement.textContent, "Alice");
|
||||
await triggerEvent(document.activeElement, "", "keydown", { key: "Backspace" });
|
||||
assert.strictEqual(target.querySelectorAll(".o_tag").length, 1);
|
||||
assert.strictEqual(target.querySelector(".o_tag").textContent, "Bob");
|
||||
});
|
||||
|
||||
QUnit.test("Backspace do nothing when the input is currently edited", async (assert) => {
|
||||
await makeMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1, 2],
|
||||
});
|
||||
target.querySelector(".o-autocomplete input").focus();
|
||||
await editInput(target, ".o-autocomplete input", "a");
|
||||
assert.strictEqual(document.activeElement.value, "a");
|
||||
await triggerEvent(document.activeElement, "", "keydown", { key: "Backspace" });
|
||||
assert.strictEqual(target.querySelectorAll(".o_tag").length, 2);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { RecordSelector } from "@web/core/record_selectors/record_selector";
|
||||
import { makeTestEnv } from "../../helpers/mock_env";
|
||||
import { getFixture, mount, click } from "../../helpers/utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { nameService } from "@web/core/name_service";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
|
||||
QUnit.module("Web Components", (hooks) => {
|
||||
QUnit.module("RecordSelector");
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
async function makeRecordSelector(props, { mockRPC } = {}) {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.state = useState({ resId: props.resId });
|
||||
}
|
||||
|
||||
get recordProps() {
|
||||
return {
|
||||
...props,
|
||||
resId: this.state.resId,
|
||||
update: (resId) => this._update(resId),
|
||||
};
|
||||
}
|
||||
|
||||
_update(resId) {
|
||||
this.state.resId = resId;
|
||||
}
|
||||
}
|
||||
Parent.components = { RecordSelector, MainComponentsContainer };
|
||||
Parent.template = xml`
|
||||
<MainComponentsContainer />
|
||||
<RecordSelector t-props="recordProps" />`;
|
||||
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
await mount(Parent, target, { env });
|
||||
}
|
||||
|
||||
hooks.beforeEach(async () => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Display name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "Alice" },
|
||||
{ id: 2, display_name: "Bob" },
|
||||
{ id: 3, display_name: "Charlie" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
registry.category("services").add("hotkey", hotkeyService);
|
||||
registry.category("services").add("dialog", dialogService);
|
||||
registry.category("services").add("name", nameService);
|
||||
});
|
||||
|
||||
QUnit.test("Can be renderer with no values", async (assert) => {
|
||||
await makeRecordSelector({
|
||||
resModel: "partner",
|
||||
resId: false,
|
||||
});
|
||||
const input = target.querySelector(".o_record_selector input");
|
||||
assert.strictEqual(input.value, "", "The input should be empty");
|
||||
assert.hasClass(input, "o_input");
|
||||
});
|
||||
|
||||
QUnit.test("Can be renderer with a value", async (assert) => {
|
||||
await makeRecordSelector({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
const input = target.querySelector(".o_record_selector input");
|
||||
assert.strictEqual(input.value, "Alice");
|
||||
});
|
||||
|
||||
QUnit.test("Can be updated from autocomplete", async (assert) => {
|
||||
await makeRecordSelector({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
const input = target.querySelector(".o_record_selector input");
|
||||
assert.strictEqual(input.value, "Alice");
|
||||
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
|
||||
await click(input);
|
||||
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
|
||||
const secondItem = target.querySelectorAll("li.o-autocomplete--dropdown-item")[1];
|
||||
await click(secondItem);
|
||||
assert.strictEqual(input.value, "Bob");
|
||||
});
|
||||
|
||||
QUnit.test("Display name is correctly fetched", async (assert) => {
|
||||
await makeRecordSelector(
|
||||
{
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
},
|
||||
{
|
||||
mockRPC: (route, args) => {
|
||||
if (args.method === "web_search_read") {
|
||||
assert.step("web_search_read");
|
||||
assert.strictEqual(args.model, "partner");
|
||||
assert.deepEqual(args.kwargs.domain, [["id", "in", [1]]]);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
const input = target.querySelector(".o_record_selector input");
|
||||
assert.strictEqual(input.value, "Alice");
|
||||
assert.verifySteps(["web_search_read"]);
|
||||
});
|
||||
|
||||
QUnit.test("Can give domain and context props for the name search", async (assert) => {
|
||||
await makeRecordSelector(
|
||||
{
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
domain: [["display_name", "=", "Bob"]],
|
||||
context: { blip: "blop " },
|
||||
},
|
||||
{
|
||||
mockRPC: (route, args) => {
|
||||
if (args.method === "name_search") {
|
||||
assert.step("name_search");
|
||||
assert.strictEqual(args.model, "partner");
|
||||
assert.deepEqual(args.kwargs.args, [
|
||||
"&",
|
||||
["display_name", "=", "Bob"],
|
||||
"!",
|
||||
["id", "in", []],
|
||||
]);
|
||||
assert.strictEqual(args.kwargs.context.blip, "blop ");
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
const input = target.querySelector(".o_record_selector input");
|
||||
assert.strictEqual(input.value, "Alice");
|
||||
assert.verifySteps([]);
|
||||
await click(input);
|
||||
assert.verifySteps(["name_search"]);
|
||||
});
|
||||
|
||||
QUnit.test("Support placeholder", async (assert) => {
|
||||
await makeRecordSelector({
|
||||
resModel: "partner",
|
||||
resId: false,
|
||||
placeholder: "Select a partner",
|
||||
});
|
||||
const input = target.querySelector(".o_record_selector input");
|
||||
assert.strictEqual(input.placeholder, "Select a partner");
|
||||
});
|
||||
|
||||
QUnit.test("domain is passed to search more", async (assert) => {
|
||||
serverData.models.partner.records = [...new Array(10)].map((el, i) => {
|
||||
return {
|
||||
id: i + 1,
|
||||
display_name: `a_${i + 1}`,
|
||||
};
|
||||
});
|
||||
serverData.views = {
|
||||
"partner,false,list": `<tree><field name="display_name" /></tree>`,
|
||||
"partner,false,search": "<search />",
|
||||
};
|
||||
|
||||
const fakeService = {
|
||||
start() {},
|
||||
};
|
||||
registry.category("services").add("view", viewService);
|
||||
registry.category("services").add("action", {
|
||||
start() {
|
||||
return { doAction: () => {} };
|
||||
},
|
||||
});
|
||||
registry.category("services").add("field", fakeService);
|
||||
registry.category("services").add("company", {
|
||||
start() {
|
||||
return { currentCompany: {} };
|
||||
},
|
||||
});
|
||||
registry.category("services").add("notification", fakeService);
|
||||
|
||||
await makeRecordSelector(
|
||||
{
|
||||
resModel: "partner",
|
||||
resId: false,
|
||||
domain: [["display_name", "!=", "some name"]],
|
||||
placeholder: "Select a partner",
|
||||
},
|
||||
{
|
||||
mockRPC: (route, args) => {
|
||||
if (args.method === "has_group") {
|
||||
return true;
|
||||
}
|
||||
if (args.method === "web_search_read") {
|
||||
assert.step("web_search_read");
|
||||
assert.deepEqual(args.kwargs.domain, [
|
||||
"&",
|
||||
["display_name", "!=", "some name"],
|
||||
"!",
|
||||
["id", "in", []],
|
||||
]);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
await click(target, ".o-autocomplete--input.o_input");
|
||||
await click(target, ".o_m2o_dropdown_option a");
|
||||
assert.containsOnce(target, ".modal .o_list_view");
|
||||
assert.verifySteps(["web_search_read"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, reactive, xml } from "@odoo/owl";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { ResizablePanel } from "@web/core/resizable_panel/resizable_panel";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import {
|
||||
getFixture,
|
||||
patchWithCleanup,
|
||||
mount,
|
||||
nextTick,
|
||||
triggerEvents,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
|
||||
QUnit.module("Resizable Panel", ({ beforeEach }) => {
|
||||
let env;
|
||||
let target;
|
||||
|
||||
beforeEach(async () => {
|
||||
env = await makeTestEnv();
|
||||
target = getFixture();
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => Promise.resolve().then(fn),
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("Width cannot exceed viewport width", async (assert) => {
|
||||
class Parent extends Component {
|
||||
static components = { ResizablePanel };
|
||||
static template = xml`
|
||||
<ResizablePanel>
|
||||
<p>A</p>
|
||||
<p>Cool</p>
|
||||
<p>Paragraph</p>
|
||||
</ResizablePanel>
|
||||
`;
|
||||
}
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsOnce(target, ".o_resizable_panel");
|
||||
assert.containsOnce(target, ".o_resizable_panel_handle");
|
||||
|
||||
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
|
||||
const sidepanel = target.querySelector(".o_resizable_panel");
|
||||
sidepanel.style.width = `${vw + 100}px`;
|
||||
|
||||
const sidepanelWidth = sidepanel.getBoundingClientRect().width;
|
||||
assert.ok(
|
||||
sidepanelWidth <= vw && sidepanelWidth > vw * 0.95,
|
||||
"The sidepanel should be smaller or equal to the view width"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("handles right-to-left", async (assert) => {
|
||||
class Parent extends Component {
|
||||
static components = { ResizablePanel };
|
||||
static template = xml`
|
||||
<div class="d-flex parent-el" style="direction: rtl;">
|
||||
<div style="width: 50px;" />
|
||||
<ResizablePanel minWidth="20" initialWidth="30">
|
||||
<div style="width: 10px;" class="text-break">
|
||||
A cool paragraph
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
await mount(Parent, target, { env });
|
||||
const parentEl = target.querySelector(".parent-el");
|
||||
const resizablePanelEl = target.querySelector(".o_resizable_panel");
|
||||
let resizablePabelRect = resizablePanelEl.getBoundingClientRect();
|
||||
assert.strictEqual(resizablePabelRect.width, 30);
|
||||
|
||||
const handle = resizablePanelEl.querySelector(".o_resizable_panel_handle");
|
||||
await triggerEvents(handle, null, ["mousedown", ["mousemove", { clientX: 10 }], "mouseup"]);
|
||||
resizablePabelRect = resizablePanelEl.getBoundingClientRect();
|
||||
assert.ok(resizablePabelRect.width > parentEl.offsetWidth - 10 - 50);
|
||||
});
|
||||
|
||||
QUnit.test("handles resize handle at start in fixed position", async (assert) => {
|
||||
class Parent extends Component {
|
||||
static components = { ResizablePanel };
|
||||
static template = xml`
|
||||
<div class="d-flex parent-el">
|
||||
<ResizablePanel minWidth="20" initialWidth="30" handleSide="'start'" class="'position-fixed'">
|
||||
<div style="width: 10px;" class="text-break">
|
||||
A cool paragraph
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
await mount(Parent, target, { env });
|
||||
const resizablePanelEl = target.querySelector(".o_resizable_panel");
|
||||
resizablePanelEl.style.setProperty("right", "100px");
|
||||
let resizablePabelRect = resizablePanelEl.getBoundingClientRect();
|
||||
assert.strictEqual(resizablePabelRect.width, 30);
|
||||
|
||||
const handle = resizablePanelEl.querySelector(".o_resizable_panel_handle");
|
||||
await triggerEvents(handle, null, [
|
||||
"mousedown",
|
||||
["mousemove", { clientX: window.innerWidth - 200 }],
|
||||
"mouseup",
|
||||
]);
|
||||
resizablePabelRect = resizablePanelEl.getBoundingClientRect();
|
||||
assert.strictEqual(resizablePabelRect.width, 100 + handle.offsetWidth / 2);
|
||||
});
|
||||
|
||||
QUnit.test("resizing the window adapts the panel", async (assert) => {
|
||||
class Parent extends Component {
|
||||
static components = { ResizablePanel };
|
||||
static template = xml`
|
||||
<div style="width: 400px;" class="parent-el position-relative">
|
||||
<ResizablePanel>
|
||||
<p>A</p>
|
||||
<p>Cool</p>
|
||||
<p>Paragraph</p>
|
||||
</ResizablePanel>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
await mount(Parent, target, { env });
|
||||
const resizablePanelEl = target.querySelector(".o_resizable_panel");
|
||||
const handle = resizablePanelEl.querySelector(".o_resizable_panel_handle");
|
||||
await triggerEvents(handle, null, [
|
||||
"mousedown",
|
||||
["mousemove", { clientX: 99999 }],
|
||||
"mouseup",
|
||||
]);
|
||||
assert.strictEqual(resizablePanelEl.offsetWidth, 398);
|
||||
target.querySelector(".parent-el").style.setProperty("width", "200px");
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
await nextTick();
|
||||
assert.strictEqual(resizablePanelEl.offsetWidth, 198);
|
||||
});
|
||||
|
||||
QUnit.test("minWidth props can be updated", async (assert) => {
|
||||
class Parent extends Component {
|
||||
static components = { ResizablePanel };
|
||||
static template = xml`
|
||||
<div class="d-flex">
|
||||
<ResizablePanel minWidth="props.state.minWidth">
|
||||
<div style="width: 10px;" class="text-break">
|
||||
A cool paragraph
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const state = reactive({ minWidth: 20 });
|
||||
await mount(Parent, target, { env, props: { state } });
|
||||
const resizablePanelEl = target.querySelector(".o_resizable_panel");
|
||||
const handle = resizablePanelEl.querySelector(".o_resizable_panel_handle");
|
||||
await triggerEvents(handle, null, ["mousedown", ["mousemove", { clientX: 15 }], "mouseup"]);
|
||||
|
||||
assert.strictEqual(resizablePanelEl.getBoundingClientRect().width, 20);
|
||||
state.minWidth = 40;
|
||||
await nextTick();
|
||||
await triggerEvents(handle, null, ["mousedown", ["mousemove", { clientX: 15 }], "mouseup"]);
|
||||
assert.strictEqual(resizablePanelEl.getBoundingClientRect().width, 40);
|
||||
});
|
||||
});
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
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";
|
||||
|
||||
|
|
@ -101,37 +100,6 @@ QUnit.test("routeToUrl encodes URI compatible strings", (assert) => {
|
|||
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) => {
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ QUnit.test("clicking anchor when no scrollable", async (assert) => {
|
|||
<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">
|
||||
<div class="inactive-container" style="max-height: 0; overflow: hidden">
|
||||
<h2>There should be no scrollable if this element has 0 height</h2>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
|
|
@ -318,9 +318,9 @@ QUnit.test("clicking anchor when multi levels scrollables", async (assert) => {
|
|||
});
|
||||
|
||||
QUnit.test("Simple scroll to HTML elements", async (assert) => {
|
||||
assert.expect(6);
|
||||
assert.expect(13);
|
||||
const scrollableParent = document.createElement("div");
|
||||
scrollableParent.style.overflow = "scroll";
|
||||
scrollableParent.style["overflow-y"] = "scroll";
|
||||
scrollableParent.style.height = "150px";
|
||||
scrollableParent.style.width = "400px";
|
||||
target.append(scrollableParent);
|
||||
|
|
@ -359,6 +359,38 @@ QUnit.test("Simple scroll to HTML elements", async (assert) => {
|
|||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue.
|
||||
</p>
|
||||
<div id="fake-scrollable">
|
||||
<div id="o-div-3">A div is an HTML element</div>
|
||||
</div>
|
||||
<div id="sub-scrollable">
|
||||
<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="o-div-4">A div is an HTML element</div>
|
||||
</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>
|
||||
`;
|
||||
await mount(MyComponent, scrollableParent, { env });
|
||||
|
|
@ -377,8 +409,8 @@ QUnit.test("Simple scroll to HTML elements", async (assert) => {
|
|||
const element = el.getBoundingClientRect();
|
||||
const scrollable = scrollableParent.getBoundingClientRect();
|
||||
return {
|
||||
top: parseInt(element.top - scrollable.top) < 10,
|
||||
bottom: parseInt(scrollable.bottom - element.bottom) < 10,
|
||||
top: parseInt(element.top - scrollable.top) === 0,
|
||||
bottom: parseInt(scrollable.bottom - element.bottom) === 0,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -386,6 +418,13 @@ QUnit.test("Simple scroll to HTML elements", async (assert) => {
|
|||
// 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");
|
||||
const div_3 = scrollableParent.querySelector("#o-div-3");
|
||||
const div_4 = scrollableParent.querySelector("#o-div-4");
|
||||
const fakeScrollable = scrollableParent.querySelector("#fake-scrollable");
|
||||
const subScrollable = scrollableParent.querySelector("#sub-scrollable");
|
||||
subScrollable.style["overflow-y"] = "scroll";
|
||||
subScrollable.style.height = getComputedStyle(subScrollable)["line-height"];
|
||||
subScrollable.style.width = "300px";
|
||||
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);
|
||||
|
|
@ -393,4 +432,20 @@ QUnit.test("Simple scroll to HTML elements", async (assert) => {
|
|||
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");
|
||||
assert.ok(!isVisible(div_3) && !isVisible(div_4));
|
||||
// Specify a scrollable which can not be scrolled, the effective scrollable
|
||||
// should be its closest actually scrollable parent.
|
||||
scrollTo(div_3, { scrollable: fakeScrollable });
|
||||
assert.ok(isVisible(div_3) && !isVisible(div_4));
|
||||
assert.ok(border(div_3).bottom, "the element must be at the bottom border");
|
||||
// Reset the position
|
||||
scrollTo(div_1);
|
||||
assert.ok(isVisible(div_1) && !isVisible(div_3) && !isVisible(div_4));
|
||||
// Scrolling should be recursive in case of a hierarchy of
|
||||
// scrollables, if `isAnchor` is set to `true`, and it must be scrolled
|
||||
// to the top even if it was positioned below the scroll view.
|
||||
scrollTo(div_4, { isAnchor: true });
|
||||
assert.ok(isVisible(div_4));
|
||||
assert.ok(border(div_4).top, "the element must be at the top border");
|
||||
assert.ok(border(subScrollable).top, "the element must be a the thop border");
|
||||
});
|
||||
|
|
|
|||
1243
odoo-bringout-oca-ocb-web/web/static/tests/core/select_menu_tests.js
Normal file
1243
odoo-bringout-oca-ocb-web/web/static/tests/core/select_menu_tests.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,210 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { TagsList } from "@web/core/tags_list/tags_list";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { getFixture, patchWithCleanup, mount, click } from "../helpers/utils";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
QUnit.module("Web Components", (hooks) => {
|
||||
QUnit.module("TagsList");
|
||||
|
||||
let env;
|
||||
let target;
|
||||
|
||||
hooks.beforeEach(async () => {
|
||||
env = await makeTestEnv();
|
||||
target = getFixture();
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => Promise.resolve().then(fn),
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("Can be rendered with different tags", async (assert) => {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.tags = [
|
||||
{
|
||||
id: "tag1",
|
||||
text: "Earth",
|
||||
},
|
||||
{
|
||||
colorIndex: 1,
|
||||
id: "tag2",
|
||||
text: "Wind",
|
||||
onDelete: () => {
|
||||
assert.step(`tag2 delete button has been clicked`);
|
||||
},
|
||||
},
|
||||
{
|
||||
colorIndex: 2,
|
||||
id: "tag3",
|
||||
text: "Fire",
|
||||
onClick: () => {
|
||||
assert.step(`tag3 has been clicked`);
|
||||
},
|
||||
onDelete: () => {
|
||||
assert.step(`tag3 delete button has been clicked`);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
Parent.components = { TagsList };
|
||||
Parent.template = xml`
|
||||
<TagsList tags="tags" />`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsN(target, ".o_tag", 3);
|
||||
|
||||
await click(target.querySelector(".o_tag:nth-of-type(2) .o_delete"));
|
||||
assert.verifySteps(["tag2 delete button has been clicked"]);
|
||||
|
||||
await click(target.querySelector(".o_tag:nth-of-type(3)"));
|
||||
assert.verifySteps(["tag3 has been clicked"]);
|
||||
});
|
||||
|
||||
QUnit.test("Tags can be displayed with an image", async (assert) => {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.tags = [
|
||||
{
|
||||
img: "fake/url",
|
||||
id: "tag1",
|
||||
text: "Earth",
|
||||
},
|
||||
{
|
||||
img: "fake/url/2",
|
||||
id: "tag2",
|
||||
text: "Wind",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
Parent.components = { TagsList };
|
||||
Parent.template = xml`
|
||||
<TagsList tags="tags" />`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsN(target, ".o_tag", 2);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_tag:nth-of-type(1) img").dataset.src,
|
||||
"fake/url"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_tag:nth-of-type(2) img").dataset.src,
|
||||
"fake/url/2"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Tags can be displayed with an icon", async (assert) => {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.tags = [
|
||||
{
|
||||
icon: "fa-trash",
|
||||
id: "tag1",
|
||||
text: "Bad",
|
||||
},
|
||||
{
|
||||
icon: "fa-check",
|
||||
id: "tag2",
|
||||
text: "Good",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
Parent.components = { TagsList };
|
||||
Parent.template = xml`
|
||||
<TagsList tags="tags" />`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsN(target, ".o_tag", 2);
|
||||
assert.hasClass(target.querySelector(".o_tag:nth-of-type(1) i"), "fa fa-trash");
|
||||
assert.hasClass(target.querySelector(".o_tag:nth-of-type(2) i"), "fa fa-check");
|
||||
});
|
||||
|
||||
QUnit.test("Limiting the visible tags displays a counter", async (assert) => {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.tags = [
|
||||
{
|
||||
id: "tag1",
|
||||
text: "Water",
|
||||
onDelete: () => {},
|
||||
},
|
||||
{
|
||||
id: "tag2",
|
||||
text: "Grass",
|
||||
},
|
||||
{
|
||||
id: "tag3",
|
||||
text: "Fire",
|
||||
},
|
||||
{
|
||||
id: "tag4",
|
||||
text: "Earth",
|
||||
},
|
||||
{
|
||||
id: "tag5",
|
||||
text: "Wind",
|
||||
},
|
||||
{
|
||||
id: "tag6",
|
||||
text: "Dust",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
Parent.components = { TagsList };
|
||||
Parent.template = xml`
|
||||
<TagsList tags="tags" itemsVisible="3" />`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsN(target, ".o_tag", 2);
|
||||
|
||||
const counter = target.querySelector(".rounded-circle");
|
||||
assert.strictEqual(counter.textContent, "+4", "the counter displays 4 more items");
|
||||
assert.deepEqual(
|
||||
JSON.parse(counter.dataset.tooltipInfo),
|
||||
{
|
||||
tags: [
|
||||
{ text: "Fire", id: "tag3" },
|
||||
{ text: "Earth", id: "tag4" },
|
||||
{ text: "Wind", id: "tag5" },
|
||||
{ text: "Dust", id: "tag6" },
|
||||
],
|
||||
},
|
||||
"the counter has a tooltip displaying other items"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Tags with img have a backdrop only if they can be deleted", async (assert) => {
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.tags = [
|
||||
{
|
||||
id: "tag1",
|
||||
text: "Earth",
|
||||
img: "fake/url",
|
||||
},
|
||||
{
|
||||
colorIndex: 1,
|
||||
id: "tag2",
|
||||
text: "Wind",
|
||||
img: "fake/url",
|
||||
onDelete: () => {},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
Parent.components = { TagsList };
|
||||
Parent.template = xml`<TagsList tags="tags" />`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsN(target, ".o_tag", 2);
|
||||
assert.containsNone(target.querySelectorAll(".o_tag")[0], ".o_avatar_backdrop");
|
||||
assert.containsOnce(target.querySelectorAll(".o_tag")[1], ".o_avatar_backdrop");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
/** @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";
|
||||
import { templates } from "@web/core/assets";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { tooltipService } from "@web/core/tooltip/tooltip_service";
|
||||
import { registerCleanup } from "../../helpers/cleanup";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
|
||||
import { makeFakeLocalizationService } from "../../helpers/mock_services";
|
||||
import { getFixture, nextTick, patchWithCleanup, triggerEvent } from "../../helpers/utils";
|
||||
|
||||
const mainComponents = registry.category("main_components");
|
||||
|
||||
|
|
@ -49,6 +49,7 @@ export async function makeParent(Child, options = {}) {
|
|||
registry.category("services").add("popover", popoverService);
|
||||
registry.category("services").add("tooltip", tooltipService);
|
||||
registry.category("services").add("localization", makeFakeLocalizationService());
|
||||
registry.category("services").add("hotkey", hotkeyService, { force: true });
|
||||
let env = await makeTestEnv();
|
||||
if (options.extraEnv) {
|
||||
env = Object.create(env, Object.getOwnPropertyDescriptors(options.extraEnv));
|
||||
|
|
@ -56,7 +57,7 @@ export async function makeParent(Child, options = {}) {
|
|||
|
||||
patchWithCleanup(env.services.popover, {
|
||||
add(...args) {
|
||||
const result = this._super(...args);
|
||||
const result = super.add(...args);
|
||||
if (options.onPopoverAdded) {
|
||||
options.onPopoverAdded(...args);
|
||||
}
|
||||
|
|
@ -65,20 +66,13 @@ export async function makeParent(Child, options = {}) {
|
|||
});
|
||||
|
||||
class Parent extends Component {
|
||||
setup() {
|
||||
this.Components = mainComponents.getEntries();
|
||||
}
|
||||
}
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<Child/>
|
||||
static template = xml`
|
||||
<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 };
|
||||
<Child/>
|
||||
<MainComponentsContainer />
|
||||
</div>`;
|
||||
static components = { Child, MainComponentsContainer };
|
||||
}
|
||||
|
||||
const app = new App(Parent, {
|
||||
env,
|
||||
|
|
@ -109,22 +103,19 @@ QUnit.module("Tooltip service", (hooks) => {
|
|||
};
|
||||
await makeParent(MyComponent, { mockSetTimeout });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
target.querySelector(".mybtn").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
|
||||
simulateTimeout();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_popover_container .o_popover").innerText,
|
||||
"hello"
|
||||
);
|
||||
assert.containsOnce(target, ".o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "hello");
|
||||
|
||||
target.querySelector(".mybtn").dispatchEvent(new Event("mouseleave"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("basic rendering 2", async (assert) => {
|
||||
|
|
@ -136,28 +127,25 @@ QUnit.module("Tooltip service", (hooks) => {
|
|||
};
|
||||
await makeParent(MyComponent, { mockSetTimeout });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".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");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
|
||||
simulateTimeout();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_popover_container .o_popover").innerText,
|
||||
"hello"
|
||||
);
|
||||
assert.containsOnce(target, ".o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "hello");
|
||||
|
||||
innerSpan.dispatchEvent(new Event("mouseleave"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.containsOnce(target, ".o_popover");
|
||||
|
||||
outerSpan.dispatchEvent(new Event("mouseleave"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("remove element with opened tooltip", async (assert) => {
|
||||
|
|
@ -179,17 +167,17 @@ QUnit.module("Tooltip service", (hooks) => {
|
|||
await makeParent(MyComponent, { mockSetInterval });
|
||||
|
||||
assert.containsOnce(target, "button");
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
target.querySelector("button").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.containsOnce(target, ".o_popover");
|
||||
|
||||
compState.visible = false;
|
||||
await nextTick();
|
||||
assert.containsNone(target, "button");
|
||||
simulateInterval();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("rendering with several tooltips", async (assert) => {
|
||||
|
|
@ -201,15 +189,15 @@ QUnit.module("Tooltip service", (hooks) => {
|
|||
</div>`;
|
||||
await makeParent(MyComponent);
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
target.querySelector("button.button_1").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.containsOnce(target, ".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.containsOnce(target, ".o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "tooltip 2");
|
||||
});
|
||||
|
||||
|
|
@ -237,35 +225,35 @@ QUnit.module("Tooltip service", (hooks) => {
|
|||
// default
|
||||
target.querySelector("button.default").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.containsOnce(target, ".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.containsOnce(target, ".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.containsOnce(target, ".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.containsOnce(target, ".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.containsOnce(target, ".o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "left");
|
||||
assert.verifySteps(["popover added with position: left"]);
|
||||
});
|
||||
|
|
@ -281,10 +269,10 @@ QUnit.module("Tooltip service", (hooks) => {
|
|||
};
|
||||
await makeParent(MyComponent, { templates, extraEnv: { tooltip_text: "tooltip" } });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o-tooltip");
|
||||
assert.containsNone(target, ".o-tooltip");
|
||||
target.querySelector("button").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o-tooltip");
|
||||
assert.containsOnce(target, ".o-tooltip");
|
||||
assert.strictEqual(target.querySelector(".o-tooltip").innerHTML, "<i>tooltip</i>");
|
||||
});
|
||||
|
||||
|
|
@ -312,10 +300,10 @@ QUnit.module("Tooltip service", (hooks) => {
|
|||
};
|
||||
await makeParent(MyComponent, { templates });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o-tooltip");
|
||||
assert.containsNone(target, ".o-tooltip");
|
||||
target.querySelector("button").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o-tooltip");
|
||||
assert.containsOnce(target, ".o-tooltip");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o-tooltip").innerHTML,
|
||||
"<ul><li>X: 3</li><li>Y: abc</li></ul>"
|
||||
|
|
@ -335,12 +323,12 @@ QUnit.module("Tooltip service", (hooks) => {
|
|||
};
|
||||
await makeParent(MyComponent, { mockSetTimeout });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
target.querySelector("button").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
simulateTimeout();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("tooltip with no delay (default delay)", async (assert) => {
|
||||
|
|
@ -381,24 +369,21 @@ QUnit.module("Tooltip service", (hooks) => {
|
|||
const mockOnTouchStart = () => {};
|
||||
await makeParent(MyComponent, { mockSetTimeout, mockSetInterval, mockOnTouchStart });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
await triggerEvent(target, "button", "touchstart");
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
|
||||
simulateTimeout();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_popover_container .o_popover").innerText,
|
||||
"hello"
|
||||
);
|
||||
assert.containsOnce(target, ".o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "hello");
|
||||
|
||||
await triggerEvent(target, "button", "touchend");
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.containsOnce(target, ".o_popover");
|
||||
simulateInterval();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("touch rendering - tap-to-show", async (assert) => {
|
||||
|
|
@ -415,27 +400,24 @@ QUnit.module("Tooltip service", (hooks) => {
|
|||
const mockOnTouchStart = () => {};
|
||||
await makeParent(MyComponent, { mockSetTimeout, mockSetInterval, mockOnTouchStart });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
await triggerEvent(target, "button[data-tooltip]", "touchstart");
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
|
||||
simulateTimeout();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_popover_container .o_popover").innerText,
|
||||
"hello"
|
||||
);
|
||||
assert.containsOnce(target, ".o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "hello");
|
||||
|
||||
await triggerEvent(target, "button[data-tooltip]", "touchend");
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.containsOnce(target, ".o_popover");
|
||||
simulateInterval();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.containsOnce(target, ".o_popover");
|
||||
|
||||
await triggerEvent(target, "button[data-tooltip]", "touchstart");
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("tooltip does not crash with disappearing target", async (assert) => {
|
||||
|
|
@ -447,10 +429,10 @@ QUnit.module("Tooltip service", (hooks) => {
|
|||
};
|
||||
await makeParent(MyComponent, { mockSetTimeout });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
target.querySelector(".mybtn").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
|
||||
// the element disappeared from the DOM during the setTimeout
|
||||
target.querySelector(".mybtn").remove();
|
||||
|
|
@ -459,6 +441,49 @@ QUnit.module("Tooltip service", (hooks) => {
|
|||
await nextTick();
|
||||
|
||||
// tooltip did not crash and is not shown
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
assert.containsNone(target, ".o_popover");
|
||||
});
|
||||
|
||||
QUnit.test("tooltip using the mouse with a touch enabled device", async (assert) => {
|
||||
// patch matchMedia to alter hasTouch value
|
||||
patchWithCleanup(browser, {
|
||||
matchMedia: (media) => {
|
||||
if (media === "(pointer:coarse)") {
|
||||
return { matches: true };
|
||||
}
|
||||
return this._super();
|
||||
},
|
||||
});
|
||||
|
||||
class MyComponent extends Component {}
|
||||
MyComponent.template = xml`<button class="mybtn" data-tooltip="hello">Action</button>`;
|
||||
let simulateTimeout;
|
||||
let simulateInterval;
|
||||
const mockSetTimeout = (fn) => {
|
||||
simulateTimeout = fn;
|
||||
};
|
||||
const mockSetInterval = (fn) => {
|
||||
simulateInterval = fn;
|
||||
};
|
||||
await makeParent(MyComponent, { mockSetInterval, mockSetTimeout });
|
||||
|
||||
assert.containsNone(target, ".o_popover");
|
||||
target.querySelector(".mybtn").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover");
|
||||
|
||||
simulateTimeout();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "hello");
|
||||
|
||||
simulateInterval();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover");
|
||||
assert.strictEqual(target.querySelector(".o_popover").innerText, "hello");
|
||||
|
||||
target.querySelector(".mybtn").dispatchEvent(new Event("mouseleave"));
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ QUnit.test("use block and unblock several times to block ui with ui service", as
|
|||
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) => {
|
||||
QUnit.test("a component can be the UI active element: simple usage", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useActiveElement("delegatedRef");
|
||||
|
|
@ -76,7 +76,9 @@ QUnit.test("a component can be the UI active element: with t-ref delegation", a
|
|||
MyComponent.template = xml`
|
||||
<div>
|
||||
<h1>My Component</h1>
|
||||
<div t-if="hasRef" id="owner" t-ref="delegatedRef"/>
|
||||
<div t-if="hasRef" id="owner" t-ref="delegatedRef">
|
||||
<input type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
@ -85,12 +87,15 @@ QUnit.test("a component can be the UI active element: with t-ref delegation", a
|
|||
assert.deepEqual(ui.activeElement, document);
|
||||
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
const input = target.querySelector("#owner input");
|
||||
assert.deepEqual(ui.activeElement, document.getElementById("owner"));
|
||||
assert.strictEqual(document.activeElement, input);
|
||||
comp.hasRef = false;
|
||||
comp.render();
|
||||
await nextTick();
|
||||
|
||||
assert.deepEqual(ui.activeElement, document);
|
||||
assert.strictEqual(document.activeElement, document.body);
|
||||
});
|
||||
|
||||
QUnit.test("UI active element: trap focus", async (assert) => {
|
||||
|
|
@ -119,30 +124,19 @@ QUnit.test("UI active element: trap focus", async (assert) => {
|
|||
);
|
||||
|
||||
// Pressing 'Tab'
|
||||
let event = triggerEvent(
|
||||
document.activeElement,
|
||||
null,
|
||||
"keydown",
|
||||
{ key: "Tab" },
|
||||
{ fast: true }
|
||||
);
|
||||
let event = await triggerEvent(document.activeElement, null, "keydown", { key: "Tab" });
|
||||
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 }
|
||||
);
|
||||
event = await triggerEvent(document.activeElement, null, "keydown", {
|
||||
key: "Tab",
|
||||
shiftKey: true,
|
||||
});
|
||||
assert.strictEqual(event.defaultPrevented, true);
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector("input[placeholder=withFocus]")
|
||||
|
|
@ -177,47 +171,33 @@ QUnit.test("UI active element: trap focus - default focus with autofocus", async
|
|||
);
|
||||
|
||||
// Pressing 'Tab'
|
||||
let event = triggerEvent(
|
||||
document.activeElement,
|
||||
null,
|
||||
"keydown",
|
||||
{ key: "Tab" },
|
||||
{ fast: true }
|
||||
);
|
||||
let event = await triggerEvent(document.activeElement, null, "keydown", { key: "Tab" });
|
||||
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 }
|
||||
);
|
||||
event = await triggerEvent(document.activeElement, null, "keydown", {
|
||||
key: "Tab",
|
||||
shiftKey: 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 }
|
||||
);
|
||||
event = await triggerEvent(document.activeElement, null, "keydown", {
|
||||
key: "Tab",
|
||||
shiftKey: true,
|
||||
});
|
||||
assert.strictEqual(event.defaultPrevented, false);
|
||||
});
|
||||
|
||||
QUnit.test("UI active element: trap focus - no focus element", async (assert) => {
|
||||
QUnit.test("do not become UI active element if no element to focus", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useActiveElement("delegatedRef");
|
||||
|
|
@ -237,35 +217,7 @@ QUnit.test("UI active element: trap focus - no focus element", async (assert) =>
|
|||
|
||||
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]"));
|
||||
assert.strictEqual(env.services.ui.activeElement, document);
|
||||
});
|
||||
|
||||
QUnit.test("UI active element: trap focus - first or last tabable changes", async (assert) => {
|
||||
|
|
@ -364,7 +316,7 @@ QUnit.test(
|
|||
);
|
||||
|
||||
// Pressing 'Shift + Tab'
|
||||
event = triggerEvent(
|
||||
event = await triggerEvent(
|
||||
document.activeElement,
|
||||
null,
|
||||
"keydown",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { userService } from "@web/core/user_service";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { session } from "@web/session";
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
|
|
@ -25,3 +27,22 @@ QUnit.test("successive calls to hasGroup", async (assert) => {
|
|||
|
||||
assert.verifySteps(["res.users/has_group/x", "res.users/has_group/y"]);
|
||||
});
|
||||
|
||||
QUnit.test("set user settings do not override old valid keys", async (assert) => {
|
||||
patchWithCleanup(session, {
|
||||
...session,
|
||||
user_settings: { a: 1, b: 2 },
|
||||
});
|
||||
serviceRegistry.add("user", userService);
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
assert.step(JSON.stringify(args.kwargs.new_settings));
|
||||
return { a: 3, c: 4 };
|
||||
};
|
||||
const env = await makeTestEnv({ mockRPC });
|
||||
assert.deepEqual(env.services.user.settings, { a: 1, b: 2 });
|
||||
|
||||
await env.services.user.setUserSettings("a", 3);
|
||||
assert.verifySteps(['{"a":3}']);
|
||||
assert.deepEqual(env.services.user.settings, { a: 3, b: 2, c: 4 });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@
|
|||
|
||||
import {
|
||||
cartesian,
|
||||
ensureArray,
|
||||
groupBy,
|
||||
intersection,
|
||||
shallowEqual,
|
||||
sortBy,
|
||||
unique,
|
||||
zip,
|
||||
zipWith,
|
||||
} from "@web/core/utils/arrays";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
|
|
@ -236,6 +239,18 @@ QUnit.module("utils", () => {
|
|||
assert.deepEqual(cartesian([1], [2], [3], [4]), [[1, 2, 3, 4]]);
|
||||
});
|
||||
|
||||
QUnit.test("ensure array", async (assert) => {
|
||||
const arrayRef = [];
|
||||
assert.notEqual(ensureArray(arrayRef), arrayRef, "Should be a different array");
|
||||
assert.deepEqual(ensureArray([]), []);
|
||||
assert.deepEqual(ensureArray(), [undefined]);
|
||||
assert.deepEqual(ensureArray(null), [null]);
|
||||
assert.deepEqual(ensureArray({ a: 1 }), [{ a: 1 }]);
|
||||
assert.deepEqual(ensureArray("foo"), ["foo"]);
|
||||
assert.deepEqual(ensureArray([1, 2, "3"]), [1, 2, "3"]);
|
||||
assert.deepEqual(ensureArray(new Set([1, 2, 3])), [1, 2, 3]);
|
||||
});
|
||||
|
||||
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(" "));
|
||||
|
|
@ -266,4 +281,20 @@ QUnit.module("utils", () => {
|
|||
assert.ok(shallowEqual([fn], [fn]));
|
||||
assert.notOk(shallowEqual([() => {}], [() => {}]));
|
||||
});
|
||||
|
||||
QUnit.test("zip", function (assert) {
|
||||
assert.deepEqual(zip([1, 2], []), []);
|
||||
assert.deepEqual(zip([1, 2], ["a"]), [[1, "a"]]);
|
||||
assert.deepEqual(zip([1, 2], ["a", "b"]), [
|
||||
[1, "a"],
|
||||
[2, "b"],
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("zipWith", function (assert) {
|
||||
assert.deepEqual(
|
||||
zipWith([{ a: 1 }, { b: 2 }], ["a", "b"], (o, k) => o[k]),
|
||||
[1, 2]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { humanSize } from "@web/core/utils/binary";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("binary");
|
||||
|
||||
QUnit.test("humanSize", (assert) => {
|
||||
assert.strictEqual(humanSize(0), "0.00 Bytes");
|
||||
assert.strictEqual(humanSize(3), "3.00 Bytes");
|
||||
assert.strictEqual(humanSize(2048), "2.00 Kb");
|
||||
assert.strictEqual(humanSize(2645000), "2.52 Mb");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,458 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
drag,
|
||||
dragAndDrop,
|
||||
getFixture,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { useDraggable } from "@web/core/utils/draggable";
|
||||
|
||||
import { Component, reactive, useRef, useState, xml } from "@odoo/owl";
|
||||
|
||||
let target;
|
||||
QUnit.module("Draggable", ({ beforeEach }) => {
|
||||
beforeEach(() => (target = getFixture()));
|
||||
|
||||
QUnit.module("Draggable hook");
|
||||
|
||||
QUnit.test("Parameters error handling", async (assert) => {
|
||||
assert.expect(5);
|
||||
|
||||
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(() => {
|
||||
useDraggable({});
|
||||
}, true);
|
||||
await mountListAndAssert(() => {
|
||||
useDraggable({
|
||||
elements: ".item",
|
||||
});
|
||||
}, true);
|
||||
|
||||
// Correct params
|
||||
await mountListAndAssert(() => {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
});
|
||||
}, false);
|
||||
await mountListAndAssert(() => {
|
||||
useDraggable({
|
||||
ref: {},
|
||||
elements: ".item",
|
||||
enable: false,
|
||||
});
|
||||
}, false);
|
||||
await mountListAndAssert(() => {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
});
|
||||
}, false);
|
||||
});
|
||||
|
||||
QUnit.test("Simple dragging in single group", async (assert) => {
|
||||
assert.expect(16);
|
||||
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
onDragStart({ element }) {
|
||||
assert.step("start");
|
||||
assert.strictEqual(element.innerText, "1");
|
||||
},
|
||||
onDrag({ element }) {
|
||||
assert.step("drag");
|
||||
assert.strictEqual(element.innerText, "1");
|
||||
},
|
||||
onDragEnd({ element }) {
|
||||
assert.step("end");
|
||||
assert.strictEqual(element.innerText, "1");
|
||||
assert.containsN(target, ".item", 3);
|
||||
},
|
||||
onDrop({ element }) {
|
||||
assert.step("drop");
|
||||
assert.strictEqual(element.innerText, "1");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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.containsNone(target, ".o_dragged");
|
||||
assert.verifySteps([]);
|
||||
|
||||
// First item after 2nd item
|
||||
const { drop, moveTo } = await drag(".item:first-child");
|
||||
await moveTo(".item:nth-child(2)");
|
||||
|
||||
assert.hasClass(target.querySelector(".item"), "o_dragged");
|
||||
|
||||
await drop();
|
||||
|
||||
assert.containsN(target, ".item", 3);
|
||||
assert.containsNone(target, ".o_dragged");
|
||||
assert.verifySteps(["start", "drag", "drop", "end"]);
|
||||
});
|
||||
|
||||
QUnit.test("Dynamically disable draggable feature", async (assert) => {
|
||||
assert.expect(4);
|
||||
|
||||
const state = reactive({ enableDrag: true });
|
||||
class List extends Component {
|
||||
setup() {
|
||||
this.state = useState(state);
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
enable: () => this.state.enableDrag,
|
||||
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.enableDrag = 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("Ignore specified elements", async (assert) => {
|
||||
assert.expect(6);
|
||||
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useDraggable({
|
||||
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([]);
|
||||
});
|
||||
|
||||
QUnit.test("Ignore specific elements in a nested draggable", async (assert) => {
|
||||
assert.expect(7);
|
||||
|
||||
class List extends Component {
|
||||
static components = { List };
|
||||
static template = xml`
|
||||
<div t-ref="root" class="root">
|
||||
<ul class="list">
|
||||
<li t-foreach="[0, 1]" t-as="i" t-key="i"
|
||||
t-attf-class="item parent #{ i % 2 ? 'ignored' : 'not-ignored' }">
|
||||
<span t-esc="'parent' + i" />
|
||||
<ul class="list">
|
||||
<li t-foreach="[0, 1]" t-as="j" t-key="j"
|
||||
t-attf-class="item child #{ j % 2 ? 'ignored' : 'not-ignored' }">
|
||||
<span t-esc="'child' + j" />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>`;
|
||||
setup() {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
preventDrag: (el) => el.classList.contains("ignored"),
|
||||
onDragStart() {
|
||||
assert.step("drag");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mount(List, target);
|
||||
|
||||
assert.verifySteps([]);
|
||||
|
||||
// Drag ignored under non-ignored -> block
|
||||
await dragAndDrop(
|
||||
".not-ignored.parent .ignored.child",
|
||||
".not-ignored.parent .not-ignored.child"
|
||||
);
|
||||
assert.verifySteps([]);
|
||||
|
||||
// Drag not-ignored-under not-ignored -> succeed
|
||||
await dragAndDrop(
|
||||
".not-ignored.parent .not-ignored.child",
|
||||
".not-ignored.parent .ignored.child"
|
||||
);
|
||||
assert.verifySteps(["drag"]);
|
||||
|
||||
// Drag ignored under ignored -> block
|
||||
await dragAndDrop(".ignored.parent .ignored.child", ".ignored.parent .not-ignored.child");
|
||||
assert.verifySteps([]);
|
||||
|
||||
// Drag not-ignored under ignored -> succeed
|
||||
await dragAndDrop(".ignored.parent .not-ignored.child", ".ignored.parent .ignored.child");
|
||||
assert.verifySteps(["drag"]);
|
||||
});
|
||||
|
||||
QUnit.test("Dragging element with touch event", async (assert) => {
|
||||
assert.expect(10);
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
matchMedia: (media) => {
|
||||
if (media === "(pointer:coarse)") {
|
||||
return { matches: true };
|
||||
} else {
|
||||
this._super();
|
||||
}
|
||||
},
|
||||
setTimeout: (fn, delay) => {
|
||||
assert.strictEqual(delay, 300, "touch drag has a default 300ms initiation delay");
|
||||
fn();
|
||||
},
|
||||
});
|
||||
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
onDragStart({ element }) {
|
||||
assert.step("start");
|
||||
assert.hasClass(
|
||||
element,
|
||||
"o_touch_bounce",
|
||||
"element has the animation class applied"
|
||||
);
|
||||
},
|
||||
onDrag() {
|
||||
assert.step("drag");
|
||||
},
|
||||
onDragEnd() {
|
||||
assert.step("end");
|
||||
},
|
||||
async onDrop({ element }) {
|
||||
assert.step("drop");
|
||||
await nextTick();
|
||||
assert.doesNotHaveClass(
|
||||
element,
|
||||
"o_touch_bounce",
|
||||
"element no longer has the animation class applied"
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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([]);
|
||||
|
||||
const { drop, moveTo } = await drag(".item:first-child", "touch");
|
||||
await moveTo(".item:nth-child(2)");
|
||||
assert.hasClass(target.querySelector(".item"), "o_dragged");
|
||||
|
||||
await drop();
|
||||
assert.verifySteps(["start", "drag", "drop", "end"]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"Dragging element with touch event: initiation delay can be overrided",
|
||||
async (assert) => {
|
||||
patchWithCleanup(browser, {
|
||||
matchMedia: (media) => {
|
||||
if (media === "(pointer:coarse)") {
|
||||
return { matches: true };
|
||||
} else {
|
||||
this._super();
|
||||
}
|
||||
},
|
||||
setTimeout: (fn, delay) => {
|
||||
assert.strictEqual(delay, 1000, "touch drag has the custom initiation delay");
|
||||
fn();
|
||||
},
|
||||
});
|
||||
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
delay: 1000,
|
||||
elements: ".item",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
const { drop, moveTo } = await drag(".item:first-child", "touch");
|
||||
await moveTo(".item:nth-child(2)");
|
||||
await drop();
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("Elements are confined within their container", async (assert) => {
|
||||
/**
|
||||
* @param {string} selector
|
||||
*/
|
||||
const getRect = (selector) => document.querySelector(selector).getBoundingClientRect();
|
||||
|
||||
class List extends Component {
|
||||
static template = xml`
|
||||
<div t-ref="root" class="root">
|
||||
<ul class="list list-unstyled m-0 d-flex flex-column">
|
||||
<li t-foreach="[1, 2, 3]" t-as="i" t-key="i" t-esc="i" class="item w-50" />
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Reset fixture style to test coordinates properly
|
||||
Object.assign(target.style, {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
"z-index": -1,
|
||||
});
|
||||
await mount(List, target);
|
||||
|
||||
const containerRect = getRect(".root");
|
||||
|
||||
const { moveTo, drop } = await drag(".item:first-child");
|
||||
|
||||
let itemRect = getRect(".item:first-child");
|
||||
|
||||
assert.strictEqual(itemRect.x, containerRect.x);
|
||||
assert.strictEqual(itemRect.y, containerRect.y);
|
||||
assert.strictEqual(itemRect.width, containerRect.width / 2);
|
||||
|
||||
await moveTo(".item:last-child", { y: 9999 });
|
||||
|
||||
itemRect = getRect(".item:first-child");
|
||||
|
||||
assert.strictEqual(itemRect.x, containerRect.x);
|
||||
assert.strictEqual(itemRect.y, containerRect.y + containerRect.height - itemRect.height);
|
||||
|
||||
await moveTo(".item:last-child", { x: 9999, y: 9999 });
|
||||
|
||||
itemRect = getRect(".item:first-child");
|
||||
|
||||
assert.strictEqual(itemRect.x, containerRect.x + containerRect.width - itemRect.width);
|
||||
assert.strictEqual(itemRect.y, containerRect.y + containerRect.height - itemRect.height);
|
||||
|
||||
await moveTo(".item:last-child", { x: -9999, y: -9999 });
|
||||
|
||||
itemRect = getRect(".item:first-child");
|
||||
|
||||
assert.strictEqual(itemRect.x, containerRect.x);
|
||||
assert.strictEqual(itemRect.y, containerRect.y);
|
||||
|
||||
await drop();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { memoize } from "@web/core/utils/functions";
|
||||
import { memoize, uniqueId } from "@web/core/utils/functions";
|
||||
import { patchWithCleanup } from "../../helpers/utils";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("Functions");
|
||||
|
|
@ -68,4 +69,15 @@ QUnit.module("utils", () => {
|
|||
const memoized2 = memoize(function () {});
|
||||
assert.strictEqual(memoized2.name, "memoized");
|
||||
});
|
||||
|
||||
QUnit.test("uniqueId", (assert) => {
|
||||
patchWithCleanup(uniqueId, { nextId: 0 });
|
||||
assert.strictEqual(uniqueId("test_"), "test_1");
|
||||
assert.strictEqual(uniqueId("bla"), "bla2");
|
||||
assert.strictEqual(uniqueId("test_"), "test_3");
|
||||
assert.strictEqual(uniqueId("bla"), "bla4");
|
||||
assert.strictEqual(uniqueId("test_"), "test_5");
|
||||
assert.strictEqual(uniqueId("test_"), "test_6");
|
||||
assert.strictEqual(uniqueId("bla"), "bla7");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,20 +1,30 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { useAutofocus, useBus, useChildRef, useForwardRefToParent, useListener, useService } from "@web/core/utils/hooks";
|
||||
import {
|
||||
useAutofocus,
|
||||
useBus,
|
||||
useChildRef,
|
||||
useForwardRefToParent,
|
||||
useService,
|
||||
useSpellCheck,
|
||||
} 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,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { LegacyComponent } from "@web/legacy/legacy_component";
|
||||
|
||||
import { Component, onMounted, useState, xml } from "@odoo/owl";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { CommandPalette } from "@web/core/commands/command_palette";
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
|
|
@ -108,15 +118,79 @@ QUnit.module("utils", () => {
|
|||
assert.strictEqual(document.activeElement, comp.inputRef.el);
|
||||
});
|
||||
|
||||
QUnit.test("useAutofocus returns also a ref when isSmall is true", async function (assert) {
|
||||
assert.expect(2);
|
||||
QUnit.test(
|
||||
"useAutofocus returns also a ref when screen has touch",
|
||||
async function (assert) {
|
||||
assert.expect(1);
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus();
|
||||
onMounted(() => {
|
||||
assert.ok(this.inputRef.el);
|
||||
});
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<span>
|
||||
<input type="text" t-ref="autofocus" />
|
||||
</span>
|
||||
`;
|
||||
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
// patch matchMedia to alter hasTouch value
|
||||
patchWithCleanup(browser, {
|
||||
matchMedia: (media) => {
|
||||
if (media === "(pointer:coarse)") {
|
||||
return { matches: true };
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
});
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
await mount(MyComponent, target, { env });
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"useAutofocus works when screen has touch and you provide mobile param",
|
||||
async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus({ mobile: true });
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<span>
|
||||
<input type="text" t-ref="autofocus" />
|
||||
</span>
|
||||
`;
|
||||
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
// patch matchMedia to alter hasTouch value
|
||||
patchWithCleanup(browser, {
|
||||
matchMedia: (media) => {
|
||||
if (media === "(pointer:coarse)") {
|
||||
return { matches: true };
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
});
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
assert.strictEqual(document.activeElement, comp.inputRef.el);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("useAutofocus does not focus when screen has touch", async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus();
|
||||
assert.ok(this.env.isSmall);
|
||||
onMounted(() => {
|
||||
assert.ok(this.inputRef.el);
|
||||
});
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
|
|
@ -125,24 +199,22 @@ QUnit.module("utils", () => {
|
|||
</span>
|
||||
`;
|
||||
|
||||
const fakeUIService = {
|
||||
start(env) {
|
||||
const ui = {};
|
||||
Object.defineProperty(env, "isSmall", {
|
||||
get() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
return ui;
|
||||
// patch matchMedia to alter hasTouch value
|
||||
patchWithCleanup(browser, {
|
||||
matchMedia: (media) => {
|
||||
if (media === "(pointer:coarse)") {
|
||||
return { matches: true };
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("ui", fakeUIService);
|
||||
});
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
await mount(MyComponent, target, { env });
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
assert.notEqual(document.activeElement, comp.inputRef.el);
|
||||
});
|
||||
|
||||
QUnit.test("supports different ref names", async (assert) => {
|
||||
|
|
@ -203,6 +275,48 @@ QUnit.module("utils", () => {
|
|||
assert.strictEqual(comp.inputRef.el.selectionEnd, 10);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"useAutofocus: autofocus outside of active element doesn't work (CommandPalette)",
|
||||
async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
this.inputRef = useAutofocus();
|
||||
}
|
||||
get OverlayContainer() {
|
||||
return registry.category("main_components").get("OverlayContainer");
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<div>
|
||||
<input type="text" t-ref="autofocus" />
|
||||
<div class="o_dialog_container"/>
|
||||
<t t-component="OverlayContainer.Component" t-props="OverlayContainer.props" />
|
||||
</div>
|
||||
`;
|
||||
|
||||
registry.category("services").add("ui", uiService);
|
||||
registry.category("services").add("dialog", dialogService);
|
||||
registry.category("services").add("hotkey", hotkeyService);
|
||||
|
||||
const config = { providers: [] };
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
const comp = await mount(MyComponent, target, { env });
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(document.activeElement, comp.inputRef.el);
|
||||
|
||||
env.services.dialog.add(CommandPalette, { config });
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_command_palette");
|
||||
assert.notStrictEqual(document.activeElement, comp.inputRef.el);
|
||||
|
||||
comp.render();
|
||||
await nextTick();
|
||||
assert.notStrictEqual(document.activeElement, comp.inputRef.el);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.module("useBus");
|
||||
|
||||
QUnit.test("useBus hook: simple usecase", async function (assert) {
|
||||
|
|
@ -229,85 +343,6 @@ QUnit.module("utils", () => {
|
|||
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) {
|
||||
|
|
@ -424,6 +459,281 @@ QUnit.module("utils", () => {
|
|||
assert.strictEqual(nbCalls, 8);
|
||||
});
|
||||
|
||||
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("useSpellCheck");
|
||||
|
||||
QUnit.test("useSpellCheck: ref is on the textarea", async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useSpellCheck();
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<div><textarea t-ref="spellcheck" class="textArea"/></div>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
await mount(MyComponent, target, { env });
|
||||
const textArea = target.querySelector(".textArea");
|
||||
assert.strictEqual(textArea.spellcheck, true, "by default, spellcheck is enabled");
|
||||
textArea.focus();
|
||||
textArea.blur();
|
||||
assert.strictEqual(
|
||||
textArea.spellcheck,
|
||||
false,
|
||||
"spellcheck is disabled once the element has lost its focus"
|
||||
);
|
||||
textArea.focus();
|
||||
assert.strictEqual(
|
||||
textArea.spellcheck,
|
||||
true,
|
||||
"spellcheck is re-enabled once the element is focused"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("useSpellCheck: use a different refName", async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useSpellCheck({ refName: "myreference" });
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`<div><textarea t-ref="myreference" class="textArea"/></div>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
await mount(MyComponent, target, { env });
|
||||
const textArea = target.querySelector(".textArea");
|
||||
assert.strictEqual(textArea.spellcheck, true, "by default, spellcheck is enabled");
|
||||
textArea.focus();
|
||||
textArea.blur();
|
||||
assert.strictEqual(
|
||||
textArea.spellcheck,
|
||||
false,
|
||||
"spellcheck is disabled once the element has lost its focus"
|
||||
);
|
||||
textArea.focus();
|
||||
assert.strictEqual(
|
||||
textArea.spellcheck,
|
||||
true,
|
||||
"spellcheck is re-enabled once the element is focused"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"useSpellCheck: ref is on the root element and two editable elements",
|
||||
async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useSpellCheck();
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<div t-ref="spellcheck">
|
||||
<textarea class="textArea"/>
|
||||
<div contenteditable="true" class="editableDiv"/>
|
||||
</div>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
await mount(MyComponent, target, { env });
|
||||
const textArea = target.querySelector(".textArea");
|
||||
const editableDiv = target.querySelector(".editableDiv");
|
||||
assert.strictEqual(
|
||||
textArea.spellcheck,
|
||||
true,
|
||||
"by default, spellcheck is enabled on the textarea"
|
||||
);
|
||||
assert.strictEqual(
|
||||
editableDiv.spellcheck,
|
||||
true,
|
||||
"by default, spellcheck is enabled on the editable div"
|
||||
);
|
||||
textArea.focus();
|
||||
textArea.blur();
|
||||
editableDiv.focus();
|
||||
assert.strictEqual(
|
||||
textArea.spellcheck,
|
||||
false,
|
||||
"spellcheck is disabled once the element has lost its focus"
|
||||
);
|
||||
editableDiv.blur();
|
||||
assert.strictEqual(
|
||||
editableDiv.spellcheck,
|
||||
false,
|
||||
"spellcheck is disabled once the element has lost its focus"
|
||||
);
|
||||
textArea.focus();
|
||||
assert.strictEqual(
|
||||
textArea.spellcheck,
|
||||
true,
|
||||
"spellcheck is re-enabled once the element is focused"
|
||||
);
|
||||
assert.strictEqual(
|
||||
editableDiv.spellcheck,
|
||||
false,
|
||||
"spellcheck is still disabled as it is not focused"
|
||||
);
|
||||
editableDiv.focus();
|
||||
assert.strictEqual(
|
||||
editableDiv.spellcheck,
|
||||
true,
|
||||
"spellcheck is re-enabled once the element is focused"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"useSpellCheck: ref is on the root element and one element has disabled the spellcheck",
|
||||
async function (assert) {
|
||||
class MyComponent extends Component {
|
||||
setup() {
|
||||
useSpellCheck();
|
||||
}
|
||||
}
|
||||
MyComponent.template = xml`
|
||||
<div t-ref="spellcheck">
|
||||
<textarea class="textArea"/>
|
||||
<div contenteditable="true" spellcheck="false" class="editableDiv"/>
|
||||
</div>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
await mount(MyComponent, target, { env });
|
||||
const textArea = target.querySelector(".textArea");
|
||||
const editableDiv = target.querySelector(".editableDiv");
|
||||
assert.strictEqual(
|
||||
textArea.spellcheck,
|
||||
true,
|
||||
"by default, spellcheck is enabled on the textarea"
|
||||
);
|
||||
assert.strictEqual(
|
||||
editableDiv.spellcheck,
|
||||
false,
|
||||
"by default, spellcheck is disabled on the editable div"
|
||||
);
|
||||
textArea.focus();
|
||||
textArea.blur();
|
||||
editableDiv.focus();
|
||||
assert.strictEqual(
|
||||
textArea.spellcheck,
|
||||
false,
|
||||
"spellcheck is disabled once the element has lost its focus"
|
||||
);
|
||||
assert.strictEqual(
|
||||
editableDiv.spellcheck,
|
||||
false,
|
||||
"spellcheck has not been enabled since it was disabled on purpose"
|
||||
);
|
||||
editableDiv.blur();
|
||||
assert.strictEqual(
|
||||
editableDiv.spellcheck,
|
||||
false,
|
||||
"spellcheck stays disabled once the element has lost its focus"
|
||||
);
|
||||
textArea.focus();
|
||||
assert.strictEqual(
|
||||
textArea.spellcheck,
|
||||
true,
|
||||
"spellcheck is re-enabled once the element is focused"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("ref is on an element with contenteditable attribute", async (assert) => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<div t-ref="spellcheck" contenteditable="true" class="editableDiv" />`;
|
||||
setup() {
|
||||
useSpellCheck();
|
||||
}
|
||||
}
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
await mount(MyComponent, target, { env });
|
||||
const editableDiv = target.querySelector(".editableDiv");
|
||||
|
||||
assert.strictEqual(editableDiv.spellcheck, true);
|
||||
editableDiv.focus();
|
||||
assert.strictEqual(editableDiv.spellcheck, true);
|
||||
editableDiv.blur();
|
||||
assert.strictEqual(editableDiv.spellcheck, false);
|
||||
});
|
||||
|
||||
QUnit.module("useChildRef / useForwardRefToParent");
|
||||
|
||||
QUnit.test("simple usecase", async function (assert) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,15 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { range } from "@web/core/utils/numbers";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("Numbers", () => {
|
||||
QUnit.test("test range function from core/utils/numbers.js", (assert) => {
|
||||
assert.deepEqual(range(0, 10), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
assert.deepEqual(range(0, -10, -1), [0, -1, -2, -3, -4, -5, -6, -7, -8, -9]);
|
||||
assert.deepEqual(range(0, 35, 5), [0, 5, 10, 15, 20, 25, 30]);
|
||||
assert.deepEqual(range(-10, 6, 2), [-10, -8, -6, -4, -2, 0, 2, 4]);
|
||||
assert.deepEqual(range(4, -4, -1), [4, 3, 2, 1, 0, -1, -2, -3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { defaultLocalization } from "@web/../tests/helpers/mock_services";
|
||||
import { patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
|
||||
import { roundPrecision, roundDecimals, floatIsZero, formatFloat } from "@web/core/utils/numbers";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("numbers");
|
||||
|
||||
QUnit.test("roundPrecision", function (assert) {
|
||||
assert.expect(26);
|
||||
|
||||
assert.strictEqual(String(roundPrecision(1.0, 1)), "1");
|
||||
assert.strictEqual(String(roundPrecision(1.0, 0.1)), "1");
|
||||
assert.strictEqual(String(roundPrecision(1.0, 0.01)), "1");
|
||||
assert.strictEqual(String(roundPrecision(1.0, 0.001)), "1");
|
||||
assert.strictEqual(String(roundPrecision(1.0, 0.0001)), "1");
|
||||
assert.strictEqual(String(roundPrecision(1.0, 0.00001)), "1");
|
||||
assert.strictEqual(String(roundPrecision(1.0, 0.000001)), "1");
|
||||
assert.strictEqual(String(roundPrecision(1.0, 0.0000001)), "1");
|
||||
assert.strictEqual(String(roundPrecision(1.0, 0.00000001)), "1");
|
||||
assert.strictEqual(String(roundPrecision(0.5, 1)), "1");
|
||||
assert.strictEqual(String(roundPrecision(-0.5, 1)), "-1");
|
||||
assert.strictEqual(String(roundPrecision(2.6745, 0.001)), "2.6750000000000003");
|
||||
assert.strictEqual(String(roundPrecision(-2.6745, 0.001)), "-2.6750000000000003");
|
||||
assert.strictEqual(String(roundPrecision(2.6744, 0.001)), "2.674");
|
||||
assert.strictEqual(String(roundPrecision(-2.6744, 0.001)), "-2.674");
|
||||
assert.strictEqual(String(roundPrecision(0.0004, 0.001)), "0");
|
||||
assert.strictEqual(String(roundPrecision(-0.0004, 0.001)), "0");
|
||||
assert.strictEqual(String(roundPrecision(357.4555, 0.001)), "357.456");
|
||||
assert.strictEqual(String(roundPrecision(-357.4555, 0.001)), "-357.456");
|
||||
assert.strictEqual(String(roundPrecision(457.4554, 0.001)), "457.455");
|
||||
assert.strictEqual(String(roundPrecision(-457.4554, 0.001)), "-457.455");
|
||||
assert.strictEqual(String(roundPrecision(-457.4554, 0.05)), "-457.45000000000005");
|
||||
assert.strictEqual(String(roundPrecision(457.444, 0.5)), "457.5");
|
||||
assert.strictEqual(String(roundPrecision(457.3, 5)), "455");
|
||||
assert.strictEqual(String(roundPrecision(457.5, 5)), "460");
|
||||
assert.strictEqual(String(roundPrecision(457.1, 3)), "456");
|
||||
});
|
||||
|
||||
QUnit.test("roundDecimals", function (assert) {
|
||||
assert.expect(21);
|
||||
|
||||
assert.strictEqual(String(roundDecimals(1.0, 0)), "1");
|
||||
assert.strictEqual(String(roundDecimals(1.0, 1)), "1");
|
||||
assert.strictEqual(String(roundDecimals(1.0, 2)), "1");
|
||||
assert.strictEqual(String(roundDecimals(1.0, 3)), "1");
|
||||
assert.strictEqual(String(roundDecimals(1.0, 4)), "1");
|
||||
assert.strictEqual(String(roundDecimals(1.0, 5)), "1");
|
||||
assert.strictEqual(String(roundDecimals(1.0, 6)), "1");
|
||||
assert.strictEqual(String(roundDecimals(1.0, 7)), "1");
|
||||
assert.strictEqual(String(roundDecimals(1.0, 8)), "1");
|
||||
assert.strictEqual(String(roundDecimals(0.5, 0)), "1");
|
||||
assert.strictEqual(String(roundDecimals(-0.5, 0)), "-1");
|
||||
assert.strictEqual(String(roundDecimals(2.6745, 3)), "2.6750000000000003");
|
||||
assert.strictEqual(String(roundDecimals(-2.6745, 3)), "-2.6750000000000003");
|
||||
assert.strictEqual(String(roundDecimals(2.6744, 3)), "2.674");
|
||||
assert.strictEqual(String(roundDecimals(-2.6744, 3)), "-2.674");
|
||||
assert.strictEqual(String(roundDecimals(0.0004, 3)), "0");
|
||||
assert.strictEqual(String(roundDecimals(-0.0004, 3)), "0");
|
||||
assert.strictEqual(String(roundDecimals(357.4555, 3)), "357.456");
|
||||
assert.strictEqual(String(roundDecimals(-357.4555, 3)), "-357.456");
|
||||
assert.strictEqual(String(roundDecimals(457.4554, 3)), "457.455");
|
||||
assert.strictEqual(String(roundDecimals(-457.4554, 3)), "-457.455");
|
||||
});
|
||||
|
||||
QUnit.test("floatIsZero", function (assert) {
|
||||
assert.strictEqual(floatIsZero(1, 0), false);
|
||||
assert.strictEqual(floatIsZero(0.9999, 0), false);
|
||||
assert.strictEqual(floatIsZero(0.50001, 0), false);
|
||||
assert.strictEqual(floatIsZero(0.5, 0), false);
|
||||
assert.strictEqual(floatIsZero(0.49999, 0), true);
|
||||
assert.strictEqual(floatIsZero(0, 0), true);
|
||||
assert.strictEqual(floatIsZero(0.49999, 0), true);
|
||||
assert.strictEqual(floatIsZero(-0.50001, 0), false);
|
||||
assert.strictEqual(floatIsZero(-0.5, 0), false);
|
||||
assert.strictEqual(floatIsZero(-0.9999, 0), false);
|
||||
assert.strictEqual(floatIsZero(-1, 0), false);
|
||||
|
||||
assert.strictEqual(floatIsZero(0.1, 1), false);
|
||||
assert.strictEqual(floatIsZero(0.099999, 1), false);
|
||||
assert.strictEqual(floatIsZero(0.050001, 1), false);
|
||||
assert.strictEqual(floatIsZero(0.05, 1), false);
|
||||
assert.strictEqual(floatIsZero(0.049999, 1), true);
|
||||
assert.strictEqual(floatIsZero(0, 1), true);
|
||||
assert.strictEqual(floatIsZero(-0.049999, 1), true);
|
||||
assert.strictEqual(floatIsZero(-0.05, 1), false);
|
||||
assert.strictEqual(floatIsZero(-0.050001, 1), false);
|
||||
assert.strictEqual(floatIsZero(-0.099999, 1), false);
|
||||
assert.strictEqual(floatIsZero(-0.1, 1), false);
|
||||
|
||||
assert.strictEqual(floatIsZero(0.01, 2), false);
|
||||
assert.strictEqual(floatIsZero(0.0099999, 2), false);
|
||||
assert.strictEqual(floatIsZero(0.005, 2), false);
|
||||
assert.strictEqual(floatIsZero(0.0050001, 2), false);
|
||||
assert.strictEqual(floatIsZero(0.0049999, 2), true);
|
||||
assert.strictEqual(floatIsZero(0, 2), true);
|
||||
assert.strictEqual(floatIsZero(-0.0049999, 2), true);
|
||||
assert.strictEqual(floatIsZero(-0.0050001, 2), false);
|
||||
assert.strictEqual(floatIsZero(-0.005, 2), false);
|
||||
assert.strictEqual(floatIsZero(-0.0099999, 2), false);
|
||||
assert.strictEqual(floatIsZero(-0.01, 2), false);
|
||||
|
||||
// 4 and 5 decimal places are mentioned as special cases in `roundDecimals` method.
|
||||
assert.strictEqual(floatIsZero(0.0001, 4), false);
|
||||
assert.strictEqual(floatIsZero(0.000099999, 4), false);
|
||||
assert.strictEqual(floatIsZero(0.00005, 4), false);
|
||||
assert.strictEqual(floatIsZero(0.000050001, 4), false);
|
||||
assert.strictEqual(floatIsZero(0.000049999, 4), true);
|
||||
assert.strictEqual(floatIsZero(0, 4), true);
|
||||
assert.strictEqual(floatIsZero(-0.000049999, 4), true);
|
||||
assert.strictEqual(floatIsZero(-0.000050001, 4), false);
|
||||
assert.strictEqual(floatIsZero(-0.00005, 4), false);
|
||||
assert.strictEqual(floatIsZero(-0.000099999, 4), false);
|
||||
assert.strictEqual(floatIsZero(-0.0001, 4), false);
|
||||
|
||||
assert.strictEqual(floatIsZero(0.00001, 5), false);
|
||||
assert.strictEqual(floatIsZero(0.0000099999, 5), false);
|
||||
assert.strictEqual(floatIsZero(0.000005, 5), false);
|
||||
assert.strictEqual(floatIsZero(0.0000050001, 5), false);
|
||||
assert.strictEqual(floatIsZero(0.0000049999, 5), true);
|
||||
assert.strictEqual(floatIsZero(0, 5), true);
|
||||
assert.strictEqual(floatIsZero(-0.0000049999, 5), true);
|
||||
assert.strictEqual(floatIsZero(-0.0000050001, 5), false);
|
||||
assert.strictEqual(floatIsZero(-0.000005, 5), false);
|
||||
assert.strictEqual(floatIsZero(-0.0000099999, 5), false);
|
||||
assert.strictEqual(floatIsZero(-0.00001, 5), false);
|
||||
|
||||
assert.strictEqual(floatIsZero(0.0000001, 7), false);
|
||||
assert.strictEqual(floatIsZero(0.000000099999, 7), false);
|
||||
assert.strictEqual(floatIsZero(0.00000005, 7), false);
|
||||
assert.strictEqual(floatIsZero(0.000000050001, 7), false);
|
||||
assert.strictEqual(floatIsZero(0.000000049999, 7), true);
|
||||
assert.strictEqual(floatIsZero(0, 7), true);
|
||||
assert.strictEqual(floatIsZero(-0.000000049999, 7), true);
|
||||
assert.strictEqual(floatIsZero(-0.000000050001, 7), false);
|
||||
assert.strictEqual(floatIsZero(-0.00000005, 7), false);
|
||||
assert.strictEqual(floatIsZero(-0.000000099999, 7), false);
|
||||
assert.strictEqual(floatIsZero(-0.0000001, 7), false);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("utils", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
patchWithCleanup(localization, { ...defaultLocalization, grouping: [3, 0] });
|
||||
});
|
||||
|
||||
QUnit.module("numbers");
|
||||
|
||||
QUnit.test("formatFloat", function (assert) {
|
||||
assert.strictEqual(formatFloat(1000000), "1,000,000.00");
|
||||
|
||||
const options = { grouping: [3, 2, -1], decimalPoint: "?", thousandsSep: "€" };
|
||||
assert.strictEqual(formatFloat(106500, options), "1€06€500?00");
|
||||
|
||||
assert.strictEqual(formatFloat(1500, { thousandsSep: "" }), "1500.00");
|
||||
assert.strictEqual(formatFloat(-1.01), "-1.01");
|
||||
assert.strictEqual(formatFloat(-0.01), "-0.01");
|
||||
|
||||
assert.strictEqual(formatFloat(38.0001, { trailingZeros: false }), "38");
|
||||
assert.strictEqual(formatFloat(38.1, { trailingZeros: false }), "38.1");
|
||||
assert.strictEqual(formatFloat(38.0001, { digits: [16, 0], trailingZeros: false }), "38");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 3, 3, 3] });
|
||||
assert.strictEqual(formatFloat(1000000), "1,000,000.00");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 2, -1] });
|
||||
assert.strictEqual(formatFloat(106500), "1,06,500.00");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [1, 2, -1] });
|
||||
assert.strictEqual(formatFloat(106500), "106,50,0.00");
|
||||
|
||||
patchWithCleanup(localization, {
|
||||
grouping: [2, 0],
|
||||
decimalPoint: "!",
|
||||
thousandsSep: "@",
|
||||
});
|
||||
assert.strictEqual(formatFloat(6000), "60@00!00");
|
||||
});
|
||||
|
||||
QUnit.test("formatFloat (humanReadable=true)", async (assert) => {
|
||||
assert.strictEqual(
|
||||
formatFloat(1020, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1.02k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1020000, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"1,020k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(10200000, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"10.20M"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1020, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1.02k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1002, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1.00k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(101, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"101.00"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(64.2, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"64.20"
|
||||
);
|
||||
assert.strictEqual(formatFloat(1e18, { humanReadable: true }), "1E");
|
||||
assert.strictEqual(
|
||||
formatFloat(1e21, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1e+21"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1.0045e22, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1e+22"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1.0045e22, { humanReadable: true, decimals: 3, minDigits: 1 }),
|
||||
"1.005e+22"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1.012e43, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"1.01e+43"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(1.012e43, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"1.01e+43"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1020, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1.02k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1020000, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"-1,020k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-10200000, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"-10.20M"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1020, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1.02k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1002, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1.00k"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-101, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-101.00"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-64.2, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-64.20"
|
||||
);
|
||||
assert.strictEqual(formatFloat(-1e18, { humanReadable: true }), "-1E");
|
||||
assert.strictEqual(
|
||||
formatFloat(-1e21, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1e+21"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1.0045e22, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1e+22"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1.0045e22, { humanReadable: true, decimals: 3, minDigits: 1 }),
|
||||
"-1.004e+22"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1.012e43, { humanReadable: true, decimals: 2, minDigits: 1 }),
|
||||
"-1.01e+43"
|
||||
);
|
||||
assert.strictEqual(
|
||||
formatFloat(-1.012e43, { humanReadable: true, decimals: 2, minDigits: 2 }),
|
||||
"-1.01e+43"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -57,4 +57,16 @@ QUnit.module("utils", () => {
|
|||
assert.ok(shallowEqual({ a: fn }, { a: fn }));
|
||||
assert.notOk(shallowEqual({ a: () => {} }, { a: () => {} }));
|
||||
});
|
||||
|
||||
QUnit.test("shallowEqual: custom comparison function", function (assert) {
|
||||
const dateA = new Date();
|
||||
const dateB = new Date(dateA);
|
||||
|
||||
assert.notOk(shallowEqual({ a: 1, date: dateA }, { a: 1, date: dateB }));
|
||||
assert.ok(
|
||||
shallowEqual({ a: 1, date: dateA }, { a: 1, date: dateB }, (a, b) =>
|
||||
a instanceof Date ? Number(a) === Number(b) : a === b
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { patch, unpatch } from "@web/core/utils/patch";
|
||||
import legacyUtils from "web.utils";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
function makeBaseClass(assert, assertInSetup) {
|
||||
class BaseClass {
|
||||
|
|
@ -51,13 +50,13 @@ QUnit.module("utils", () => {
|
|||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -72,24 +71,24 @@ QUnit.module("utils", () => {
|
|||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch1", {
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch1.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch1.fn");
|
||||
},
|
||||
});
|
||||
|
||||
patch(BaseClass.prototype, "patch2", {
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch2.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch2.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -105,31 +104,18 @@ QUnit.module("utils", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
QUnit.test("two patches with same name on same base class", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const A = class {};
|
||||
|
||||
patch(A.prototype, "patch");
|
||||
|
||||
// keys should be unique
|
||||
assert.throws(() => {
|
||||
patch(A.prototype, "patch");
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("unpatch", async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -137,7 +123,7 @@ QUnit.module("utils", () => {
|
|||
new BaseClass().fn();
|
||||
|
||||
assert.verifySteps(["base.setup", "patch.setup", "base.fn", "patch.fn"]);
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
new BaseClass().fn();
|
||||
|
||||
|
|
@ -149,24 +135,24 @@ QUnit.module("utils", () => {
|
|||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch1", {
|
||||
const unpatch1 = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch1.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch1.fn");
|
||||
},
|
||||
});
|
||||
|
||||
patch(BaseClass.prototype, "patch2", {
|
||||
const unpatch2 = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch2.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch2.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -182,13 +168,13 @@ QUnit.module("utils", () => {
|
|||
"patch2.fn",
|
||||
]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch1");
|
||||
unpatch1();
|
||||
|
||||
new BaseClass().fn();
|
||||
|
||||
assert.verifySteps(["base.setup", "patch2.setup", "base.fn", "patch2.fn"]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch2");
|
||||
unpatch2();
|
||||
|
||||
new BaseClass().fn();
|
||||
|
||||
|
|
@ -202,24 +188,24 @@ QUnit.module("utils", () => {
|
|||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch1", {
|
||||
const unpatch1 = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch1.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch1.fn");
|
||||
},
|
||||
});
|
||||
|
||||
patch(BaseClass.prototype, "patch2", {
|
||||
const unpatch2 = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch2.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch2.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -235,13 +221,13 @@ QUnit.module("utils", () => {
|
|||
"patch2.fn",
|
||||
]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch1");
|
||||
unpatch1();
|
||||
|
||||
new BaseClass().fn();
|
||||
|
||||
assert.verifySteps(["base.setup", "patch2.setup", "base.fn", "patch2.fn"]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch2");
|
||||
unpatch2();
|
||||
|
||||
new BaseClass().fn();
|
||||
|
||||
|
|
@ -249,18 +235,6 @@ QUnit.module("utils", () => {
|
|||
}
|
||||
);
|
||||
|
||||
QUnit.test("unpatch twice the same patch name", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const A = class {};
|
||||
patch(A.prototype, "patch");
|
||||
|
||||
unpatch(A.prototype, "patch");
|
||||
assert.throws(() => {
|
||||
unpatch(A.prototype, "patch");
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("patch for specialization", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
|
@ -275,9 +249,9 @@ QUnit.module("utils", () => {
|
|||
}
|
||||
};
|
||||
|
||||
patch(A.prototype, "patch", {
|
||||
patch(A.prototype, {
|
||||
setup() {
|
||||
this._super("patch", ...arguments);
|
||||
super.setup("patch", ...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -290,9 +264,9 @@ QUnit.module("utils", () => {
|
|||
assert.expect(3);
|
||||
|
||||
const BaseClass = makeBaseClass(assert, false);
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
super.setup(...arguments);
|
||||
|
||||
this.str += "patch";
|
||||
this.arr.push("patch");
|
||||
|
|
@ -313,7 +287,7 @@ QUnit.module("utils", () => {
|
|||
|
||||
assert.notOk(new BaseClass().f);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
f() {
|
||||
assert.step("patch.f");
|
||||
},
|
||||
|
|
@ -322,7 +296,7 @@ QUnit.module("utils", () => {
|
|||
new BaseClass().f();
|
||||
assert.verifySteps(["patch.f"]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
assert.notOk(new BaseClass().f);
|
||||
});
|
||||
|
|
@ -335,9 +309,9 @@ QUnit.module("utils", () => {
|
|||
BaseClass.staticFn();
|
||||
assert.verifySteps(["base.staticFn"]);
|
||||
|
||||
patch(BaseClass, "patch", {
|
||||
const unpatch = patch(BaseClass, {
|
||||
staticFn() {
|
||||
this._super();
|
||||
super.staticFn();
|
||||
assert.step("patch.staticFn");
|
||||
},
|
||||
});
|
||||
|
|
@ -345,7 +319,7 @@ QUnit.module("utils", () => {
|
|||
BaseClass.staticFn();
|
||||
assert.verifySteps(["base.staticFn", "patch.staticFn"]);
|
||||
|
||||
unpatch(BaseClass, "patch");
|
||||
unpatch();
|
||||
|
||||
BaseClass.staticFn();
|
||||
assert.verifySteps(["base.staticFn"]);
|
||||
|
|
@ -356,7 +330,7 @@ QUnit.module("utils", () => {
|
|||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass, "patch", {
|
||||
const unpatch = patch(BaseClass, {
|
||||
staticStr: BaseClass.staticStr + "patch",
|
||||
staticArr: [...BaseClass.staticArr, "patch"],
|
||||
staticObj: { ...BaseClass.staticObj, patch: "patch" },
|
||||
|
|
@ -366,7 +340,7 @@ QUnit.module("utils", () => {
|
|||
assert.deepEqual(BaseClass.staticArr, ["base", "patch"]);
|
||||
assert.deepEqual(BaseClass.staticObj, { base: "base", patch: "patch" });
|
||||
|
||||
unpatch(BaseClass, "patch");
|
||||
unpatch();
|
||||
|
||||
assert.strictEqual(BaseClass.staticStr, "base");
|
||||
assert.deepEqual(BaseClass.staticArr, ["base"]);
|
||||
|
|
@ -379,14 +353,14 @@ QUnit.module("utils", () => {
|
|||
const BaseClass = makeBaseClass(assert);
|
||||
const instance = new BaseClass();
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
// will not be called
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -394,7 +368,7 @@ QUnit.module("utils", () => {
|
|||
instance.fn();
|
||||
assert.verifySteps(["base.setup", "base.fn", "patch.fn"]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
instance.fn();
|
||||
assert.verifySteps(["base.fn"]);
|
||||
|
|
@ -405,16 +379,16 @@ QUnit.module("utils", () => {
|
|||
|
||||
const BaseClass = makeBaseClass(assert, false);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
get dynamic() {
|
||||
return this._super() + "patch";
|
||||
return super.dynamic + "patch";
|
||||
},
|
||||
});
|
||||
|
||||
const instance = new BaseClass();
|
||||
assert.strictEqual(instance.dynamic, "basepatch");
|
||||
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
assert.strictEqual(instance.dynamic, "base");
|
||||
});
|
||||
|
||||
|
|
@ -423,9 +397,9 @@ QUnit.module("utils", () => {
|
|||
|
||||
const BaseClass = makeBaseClass(assert, false);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
set dynamic(value) {
|
||||
this._super("patch:" + value);
|
||||
super.dynamic = "patch:" + value;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -435,7 +409,7 @@ QUnit.module("utils", () => {
|
|||
instance.dynamic = "patch";
|
||||
assert.strictEqual(instance.dynamic, "patch:patch");
|
||||
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
instance.dynamic = "base";
|
||||
assert.strictEqual(instance.dynamic, "base");
|
||||
|
|
@ -450,7 +424,7 @@ QUnit.module("utils", () => {
|
|||
BaseClass.prototype,
|
||||
"dynamic"
|
||||
);
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
dynamic: "patched",
|
||||
});
|
||||
|
||||
|
|
@ -460,11 +434,11 @@ QUnit.module("utils", () => {
|
|||
value: "patched",
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
enumerable: false, // class properties are not enumerable
|
||||
});
|
||||
assert.equal(instance.dynamic, "patched");
|
||||
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
instance.dynamic = "base";
|
||||
assert.deepEqual(
|
||||
|
|
@ -479,11 +453,10 @@ QUnit.module("utils", () => {
|
|||
|
||||
const BaseClass = makeBaseClass(assert, false);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
patch(BaseClass.prototype, {
|
||||
async asyncFn() {
|
||||
const _super = this._super;
|
||||
await Promise.resolve();
|
||||
await _super(...arguments);
|
||||
await super.asyncFn(...arguments);
|
||||
assert.step("patch.asyncFn");
|
||||
},
|
||||
});
|
||||
|
|
@ -500,20 +473,18 @@ QUnit.module("utils", () => {
|
|||
|
||||
const BaseClass = makeBaseClass(assert, false);
|
||||
|
||||
patch(BaseClass.prototype, "patch1", {
|
||||
patch(BaseClass.prototype, {
|
||||
async asyncFn() {
|
||||
const _super = this._super;
|
||||
await Promise.resolve();
|
||||
await _super(...arguments);
|
||||
await super.asyncFn(...arguments);
|
||||
// also check this binding
|
||||
assert.step(`patch1.${this.str}`);
|
||||
},
|
||||
});
|
||||
patch(BaseClass.prototype, "patch2", {
|
||||
patch(BaseClass.prototype, {
|
||||
async asyncFn() {
|
||||
const _super = this._super;
|
||||
await Promise.resolve();
|
||||
await _super(...arguments);
|
||||
await super.asyncFn(...arguments);
|
||||
// also check this binding
|
||||
assert.step(`patch2.${this.str}`);
|
||||
},
|
||||
|
|
@ -526,6 +497,25 @@ QUnit.module("utils", () => {
|
|||
assert.verifySteps(["base.asyncFn", "patch1.asyncFn", "patch2.asyncFn"]);
|
||||
});
|
||||
|
||||
QUnit.test("call another super method", async function (assert) {
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
assert.step("patch.setup");
|
||||
super.fn();
|
||||
},
|
||||
fn() {
|
||||
assert.step("patch.fn"); // should not called
|
||||
},
|
||||
});
|
||||
|
||||
new BaseClass();
|
||||
assert.verifySteps([
|
||||
"patch.setup",
|
||||
"base.fn",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.module("inheritance");
|
||||
|
||||
QUnit.test("inherit a patched class (extends before patch)", async function (assert) {
|
||||
|
|
@ -547,13 +537,13 @@ QUnit.module("utils", () => {
|
|||
new Extension().fn();
|
||||
assert.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -574,13 +564,13 @@ QUnit.module("utils", () => {
|
|||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -627,13 +617,13 @@ QUnit.module("utils", () => {
|
|||
new Extension().fn();
|
||||
assert.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
|
||||
patch(Extension.prototype, "patch", {
|
||||
patch(Extension.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -654,13 +644,13 @@ QUnit.module("utils", () => {
|
|||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -676,13 +666,13 @@ QUnit.module("utils", () => {
|
|||
}
|
||||
}
|
||||
|
||||
patch(Extension.prototype, "patch", {
|
||||
patch(Extension.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.extension.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.extension.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -700,6 +690,50 @@ QUnit.module("utils", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
QUnit.test("patch an inherited patched class 2", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
class Extension extends BaseClass {
|
||||
// nothing in the prototype
|
||||
}
|
||||
|
||||
// First patch Extension
|
||||
patch(Extension.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
assert.step("patch.extension.setup");
|
||||
},
|
||||
fn() {
|
||||
super.fn();
|
||||
assert.step("patch.extension.fn");
|
||||
},
|
||||
});
|
||||
|
||||
// Then patch BaseClass
|
||||
patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
|
||||
new Extension().fn();
|
||||
assert.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"patch.extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("unpatch base class", async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
|
|
@ -716,13 +750,13 @@ QUnit.module("utils", () => {
|
|||
}
|
||||
}
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -737,7 +771,7 @@ QUnit.module("utils", () => {
|
|||
"extension.fn",
|
||||
]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
new Extension().fn();
|
||||
assert.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
|
|
@ -759,13 +793,13 @@ QUnit.module("utils", () => {
|
|||
}
|
||||
}
|
||||
|
||||
patch(Extension.prototype, "patch", {
|
||||
const unpatch = patch(Extension.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -780,7 +814,7 @@ QUnit.module("utils", () => {
|
|||
"patch.fn",
|
||||
]);
|
||||
|
||||
unpatch(Extension.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
new Extension().fn();
|
||||
assert.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
|
|
@ -793,13 +827,13 @@ QUnit.module("utils", () => {
|
|||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch.BaseClass", {
|
||||
const unpatchBase = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -815,13 +849,13 @@ QUnit.module("utils", () => {
|
|||
}
|
||||
}
|
||||
|
||||
patch(Extension.prototype, "patch.Extension", {
|
||||
const unpatchExtension = patch(Extension.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.extension.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.extension.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -838,7 +872,7 @@ QUnit.module("utils", () => {
|
|||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch.BaseClass");
|
||||
unpatchBase();
|
||||
|
||||
new Extension().fn();
|
||||
assert.verifySteps([
|
||||
|
|
@ -850,7 +884,7 @@ QUnit.module("utils", () => {
|
|||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatch(Extension.prototype, "patch.Extension");
|
||||
unpatchExtension();
|
||||
|
||||
new Extension().fn();
|
||||
assert.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
|
|
@ -864,13 +898,13 @@ QUnit.module("utils", () => {
|
|||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass.prototype, "patch.BaseClass", {
|
||||
const unpatchBase = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -886,13 +920,13 @@ QUnit.module("utils", () => {
|
|||
}
|
||||
}
|
||||
|
||||
patch(Extension.prototype, "patch.Extension", {
|
||||
const unpatchExtension = patch(Extension.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
assert.step("patch.extension.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.extension.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -909,7 +943,7 @@ QUnit.module("utils", () => {
|
|||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatch(Extension.prototype, "patch.Extension");
|
||||
unpatchExtension();
|
||||
|
||||
new Extension().fn();
|
||||
assert.verifySteps([
|
||||
|
|
@ -921,7 +955,7 @@ QUnit.module("utils", () => {
|
|||
"extension.fn",
|
||||
]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch.BaseClass");
|
||||
unpatchBase();
|
||||
|
||||
new Extension().fn();
|
||||
assert.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
|
|
@ -940,16 +974,16 @@ QUnit.module("utils", () => {
|
|||
}
|
||||
}
|
||||
|
||||
patch(BaseClass, "patch.BaseClass", {
|
||||
const unpatchBase = patch(BaseClass, {
|
||||
staticFn() {
|
||||
this._super();
|
||||
super.staticFn();
|
||||
assert.step("patch.staticFn");
|
||||
},
|
||||
});
|
||||
|
||||
patch(Extension, "patch.Extension", {
|
||||
const unpatchExtension = patch(Extension, {
|
||||
staticFn() {
|
||||
this._super();
|
||||
super.staticFn();
|
||||
assert.step("patch.extension.staticFn");
|
||||
},
|
||||
});
|
||||
|
|
@ -962,12 +996,12 @@ QUnit.module("utils", () => {
|
|||
"patch.extension.staticFn",
|
||||
]);
|
||||
|
||||
unpatch(BaseClass, "patch.BaseClass");
|
||||
unpatchBase();
|
||||
|
||||
Extension.staticFn();
|
||||
assert.verifySteps(["base.staticFn", "extension.staticFn", "patch.extension.staticFn"]);
|
||||
|
||||
unpatch(Extension, "patch.Extension");
|
||||
unpatchExtension();
|
||||
|
||||
Extension.staticFn();
|
||||
assert.verifySteps(["base.staticFn", "extension.staticFn"]);
|
||||
|
|
@ -978,7 +1012,7 @@ QUnit.module("utils", () => {
|
|||
|
||||
const BaseClass = makeBaseClass(assert);
|
||||
|
||||
patch(BaseClass, "patch.BaseClass", {
|
||||
const unpatch = patch(BaseClass, {
|
||||
staticStr: BaseClass.staticStr + "patch",
|
||||
staticArr: [...BaseClass.staticArr, "patch"],
|
||||
staticObj: { ...BaseClass.staticObj, patch: "patch" },
|
||||
|
|
@ -997,7 +1031,7 @@ QUnit.module("utils", () => {
|
|||
extension: "extension",
|
||||
});
|
||||
|
||||
unpatch(BaseClass, "patch.BaseClass");
|
||||
unpatch();
|
||||
|
||||
// /!\ WARNING /!\
|
||||
// If inherit comes after the patch then extension will still have
|
||||
|
|
@ -1024,7 +1058,7 @@ QUnit.module("utils", () => {
|
|||
// /!\ WARNING /!\
|
||||
// If patch comes after the inherit then extension won't have
|
||||
// the patched data.
|
||||
patch(BaseClass, "patch.BaseClass", {
|
||||
const unpatch = patch(BaseClass, {
|
||||
staticStr: BaseClass.staticStr + "patch",
|
||||
staticArr: [...BaseClass.staticArr, "patch"],
|
||||
staticObj: { ...BaseClass.staticObj, patch: "patch" },
|
||||
|
|
@ -1034,7 +1068,7 @@ QUnit.module("utils", () => {
|
|||
assert.deepEqual(Extension.staticArr, ["base", "extension"]);
|
||||
assert.deepEqual(Extension.staticObj, { base: "base", extension: "extension" });
|
||||
|
||||
unpatch(BaseClass, "patch.BaseClass");
|
||||
unpatch();
|
||||
|
||||
assert.strictEqual(Extension.staticStr, "baseextension");
|
||||
assert.deepEqual(Extension.staticArr, ["base", "extension"]);
|
||||
|
|
@ -1058,14 +1092,14 @@ QUnit.module("utils", () => {
|
|||
|
||||
const instance = new Extension();
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
super.setup();
|
||||
// will not be called
|
||||
assert.step("patch.setup");
|
||||
},
|
||||
fn() {
|
||||
this._super();
|
||||
super.fn();
|
||||
assert.step("patch.fn");
|
||||
},
|
||||
});
|
||||
|
|
@ -1079,7 +1113,7 @@ QUnit.module("utils", () => {
|
|||
"extension.fn",
|
||||
]);
|
||||
|
||||
unpatch(BaseClass.prototype, "patch");
|
||||
unpatch();
|
||||
|
||||
instance.fn();
|
||||
assert.verifySteps(["base.fn", "extension.fn"]);
|
||||
|
|
@ -1097,7 +1131,7 @@ QUnit.module("utils", () => {
|
|||
assert.strictEqual(descriptor.configurable, true);
|
||||
assert.strictEqual(descriptor.enumerable, false);
|
||||
|
||||
patch(BaseClass.prototype, "patch", {
|
||||
patch(BaseClass.prototype, {
|
||||
// getter declared in object are enumerable
|
||||
get getter() {
|
||||
return true;
|
||||
|
|
@ -1121,10 +1155,10 @@ QUnit.module("utils", () => {
|
|||
},
|
||||
};
|
||||
|
||||
patch(obj, "patch", {
|
||||
const unpatch = patch(obj, {
|
||||
var: obj.var + "patch",
|
||||
fn() {
|
||||
this._super(...arguments);
|
||||
super.fn(...arguments);
|
||||
assert.step("patch");
|
||||
},
|
||||
});
|
||||
|
|
@ -1134,7 +1168,7 @@ QUnit.module("utils", () => {
|
|||
obj.fn();
|
||||
assert.verifySteps(["obj", "patch"]);
|
||||
|
||||
unpatch(obj, "patch");
|
||||
unpatch();
|
||||
|
||||
assert.strictEqual(obj.var, "obj");
|
||||
|
||||
|
|
@ -1153,7 +1187,7 @@ QUnit.module("utils", () => {
|
|||
};
|
||||
|
||||
const originalFn = obj.fn;
|
||||
patch(obj, "patch", {
|
||||
patch(obj, {
|
||||
fn() {
|
||||
assert.step("patched");
|
||||
originalFn();
|
||||
|
|
@ -1165,41 +1199,5 @@ QUnit.module("utils", () => {
|
|||
|
||||
assert.verifySteps(["patched", "original"]);
|
||||
});
|
||||
|
||||
QUnit.test("patch an object with a legacy patch", async function (assert) {
|
||||
const a = {
|
||||
doSomething() {
|
||||
assert.step("a");
|
||||
},
|
||||
};
|
||||
legacyUtils.patch(a, "a.patch.legacy", {
|
||||
doSomething() {
|
||||
this._super();
|
||||
assert.step("a.patch.legacy");
|
||||
},
|
||||
});
|
||||
patch(a, "a.patch", {
|
||||
doSomething() {
|
||||
this._super();
|
||||
assert.step("a.patch");
|
||||
},
|
||||
});
|
||||
a.doSomething();
|
||||
assert.verifySteps(["a", "a.patch.legacy", "a.patch"]);
|
||||
});
|
||||
|
||||
QUnit.module("patch 'pure' option");
|
||||
|
||||
QUnit.test("function objects are preserved with 'pure' patch", async function (assert) {
|
||||
const obj1 = { a: () => {} };
|
||||
const obj2 = { a: () => {} };
|
||||
function someValue() {}
|
||||
|
||||
patch(obj1, "patch1", { a: someValue });
|
||||
assert.notStrictEqual(obj1.a, someValue);
|
||||
|
||||
patch(obj2, "patch2", { a: someValue }, { pure: true });
|
||||
assert.strictEqual(obj2.a, someValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { renderToElement, renderToString } from "@web/core/utils/render";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("render");
|
||||
|
||||
QUnit.test("renderToElement always returns an element", (assert) => {
|
||||
renderToString.app.addTemplate(
|
||||
"test.render.template.1",
|
||||
`<t t-if="False">
|
||||
<div>NotOk</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div>Ok</div>
|
||||
</t>`
|
||||
);
|
||||
const compiledTemplate = renderToElement("test.render.template.1");
|
||||
assert.strictEqual(
|
||||
compiledTemplate.parentElement,
|
||||
null,
|
||||
"compiledTemplate.parentElement must be empty"
|
||||
);
|
||||
assert.strictEqual(
|
||||
compiledTemplate.nodeType,
|
||||
Node.ELEMENT_NODE,
|
||||
"compiledTemplate must be an element"
|
||||
);
|
||||
assert.strictEqual(compiledTemplate.outerHTML, "<div>Ok</div>");
|
||||
});
|
||||
});
|
||||
|
|
@ -4,25 +4,22 @@ import {
|
|||
drag,
|
||||
dragAndDrop,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
mockAnimationFrame,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
triggerHotkey
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { useSortable } from "@web/core/utils/sortable";
|
||||
import { useSortable } from "@web/core/utils/sortable_owl";
|
||||
|
||||
import { Component, reactive, useRef, useState, xml } from "@odoo/owl";
|
||||
|
||||
let target;
|
||||
QUnit.module("UI", ({ beforeEach }) => {
|
||||
QUnit.module("Draggable", ({ beforeEach }) => {
|
||||
beforeEach(() => (target = getFixture()));
|
||||
|
||||
QUnit.module("Sortable hook");
|
||||
|
||||
QUnit.test("Parameters error handling", async (assert) => {
|
||||
assert.expect(8);
|
||||
assert.expect(6);
|
||||
|
||||
const mountListAndAssert = async (setupList, shouldThrow) => {
|
||||
class List extends Component {
|
||||
|
|
@ -51,11 +48,6 @@ QUnit.module("UI", ({ beforeEach }) => {
|
|||
await mountListAndAssert(() => {
|
||||
useSortable({});
|
||||
}, true);
|
||||
await mountListAndAssert(() => {
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
});
|
||||
}, true);
|
||||
await mountListAndAssert(() => {
|
||||
useSortable({
|
||||
elements: ".item",
|
||||
|
|
@ -67,20 +59,13 @@ QUnit.module("UI", ({ beforeEach }) => {
|
|||
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: useRef("root"),
|
||||
});
|
||||
}, false);
|
||||
await mountListAndAssert(() => {
|
||||
useSortable({
|
||||
ref: {},
|
||||
|
|
@ -98,7 +83,7 @@ QUnit.module("UI", ({ beforeEach }) => {
|
|||
});
|
||||
|
||||
QUnit.test("Simple sorting in single group", async (assert) => {
|
||||
assert.expect(19);
|
||||
assert.expect(22);
|
||||
|
||||
class List extends Component {
|
||||
setup() {
|
||||
|
|
@ -115,7 +100,7 @@ QUnit.module("UI", ({ beforeEach }) => {
|
|||
assert.strictEqual(element.innerText, "2");
|
||||
},
|
||||
onDragEnd({ element, group }) {
|
||||
assert.step("stop");
|
||||
assert.step("end");
|
||||
assert.notOk(group);
|
||||
assert.strictEqual(element.innerText, "1");
|
||||
assert.containsN(target, ".item", 4);
|
||||
|
|
@ -142,13 +127,20 @@ QUnit.module("UI", ({ beforeEach }) => {
|
|||
await mount(List, target);
|
||||
|
||||
assert.containsN(target, ".item", 3);
|
||||
assert.containsNone(target, ".o_dragged");
|
||||
assert.verifySteps([]);
|
||||
|
||||
// First item after 2nd item
|
||||
await dragAndDrop(".item:first-child", ".item:nth-child(2)");
|
||||
const { drop, moveTo } = await drag(".item:first-child");
|
||||
await moveTo(".item:nth-child(2)");
|
||||
|
||||
assert.hasClass(target.querySelector(".item"), "o_dragged");
|
||||
|
||||
await drop();
|
||||
|
||||
assert.containsN(target, ".item", 3);
|
||||
assert.verifySteps(["start", "elemententer", "stop", "drop"]);
|
||||
assert.containsNone(target, ".o_dragged");
|
||||
assert.verifySteps(["start", "elemententer", "drop", "end"]);
|
||||
});
|
||||
|
||||
QUnit.test("Simple sorting in multiple groups", async (assert) => {
|
||||
|
|
@ -171,7 +163,7 @@ QUnit.module("UI", ({ beforeEach }) => {
|
|||
assert.hasClass(group, "list1");
|
||||
},
|
||||
onDragEnd({ element, group }) {
|
||||
assert.step("stop");
|
||||
assert.step("end");
|
||||
assert.hasClass(group, "list2");
|
||||
assert.strictEqual(element.innerText, "2 1");
|
||||
},
|
||||
|
|
@ -205,35 +197,11 @@ QUnit.module("UI", ({ beforeEach }) => {
|
|||
|
||||
assert.containsN(target, ".list", 3);
|
||||
assert.containsN(target, ".item", 9);
|
||||
assert.verifySteps(["start", "groupenter", "stop", "drop"]);
|
||||
assert.verifySteps(["start", "groupenter", "drop", "end"]);
|
||||
});
|
||||
|
||||
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 },
|
||||
});
|
||||
|
||||
const { advanceFrame } = mockAnimationFrame();
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useSortable({
|
||||
|
|
@ -272,15 +240,15 @@ QUnit.module("UI", ({ beforeEach }) => {
|
|||
const assertScrolling = (top, left) => {
|
||||
assert.strictEqual(scrollParentY.scrollTop, top);
|
||||
assert.strictEqual(scrollParentX.scrollLeft, left);
|
||||
}
|
||||
const cancelDrag = async () => {
|
||||
triggerHotkey("Escape");
|
||||
};
|
||||
const cancelDrag = async (cancel) => {
|
||||
await cancel();
|
||||
await nextTick();
|
||||
scrollParentY.scrollTop = 0;
|
||||
scrollParentX.scrollLeft = 0;
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_dragged");
|
||||
}
|
||||
};
|
||||
assert.containsNone(target, ".o_dragged");
|
||||
|
||||
// Negative horizontal scrolling.
|
||||
|
|
@ -288,37 +256,110 @@ QUnit.module("UI", ({ beforeEach }) => {
|
|||
scrollParentX.scrollLeft = 16;
|
||||
await nextTick();
|
||||
assertScrolling(50, 16);
|
||||
await drag(".item12", ".item11", "left");
|
||||
await nextAnimationFrame(16);
|
||||
let dragHelpers = await drag(".item12");
|
||||
await dragHelpers.moveTo(".item11", "left");
|
||||
await advanceFrame();
|
||||
assertScrolling(50, 0);
|
||||
await cancelDrag();
|
||||
await cancelDrag(dragHelpers.cancel);
|
||||
|
||||
// Positive horizontal scrolling.
|
||||
target.querySelector(".spacer_horizontal").scrollIntoView();
|
||||
await nextTick();
|
||||
assertScrolling(50, 0);
|
||||
await drag(".item11", ".item12", "right");
|
||||
await nextAnimationFrame(16);
|
||||
dragHelpers = await drag(".item11");
|
||||
await dragHelpers.moveTo(".item12", "right");
|
||||
await advanceFrame();
|
||||
assertScrolling(50, 16);
|
||||
await cancelDrag();
|
||||
await cancelDrag(dragHelpers.cancel);
|
||||
|
||||
// Negative vertical scrolling.
|
||||
target.querySelector(".root").scrollIntoView();
|
||||
await nextTick();
|
||||
assertScrolling(100, 0);
|
||||
await drag(".item11", ".item11", "top");
|
||||
await nextAnimationFrame(16);
|
||||
dragHelpers = await drag(".item11");
|
||||
await dragHelpers.moveTo(".item11", "top");
|
||||
await advanceFrame();
|
||||
assertScrolling(84, 0);
|
||||
await cancelDrag();
|
||||
await cancelDrag(dragHelpers.cancel);
|
||||
|
||||
// Positive vertical scrolling.
|
||||
target.querySelector(".spacer_before").scrollIntoView();
|
||||
await nextTick();
|
||||
assertScrolling(0, 0);
|
||||
await drag(".item21", ".item21", "bottom");
|
||||
await nextAnimationFrame(16);
|
||||
dragHelpers = await drag(".item21");
|
||||
await dragHelpers.moveTo(".item21", "bottom");
|
||||
await advanceFrame();
|
||||
assertScrolling(16, 0);
|
||||
await cancelDrag();
|
||||
await cancelDrag(dragHelpers.cancel);
|
||||
});
|
||||
|
||||
QUnit.test("draggable area contains overflowing visible elements", async (assert) => {
|
||||
const { advanceFrame } = mockAnimationFrame();
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useSortable({
|
||||
ref: useRef("renderer"),
|
||||
elements: ".item",
|
||||
groups: ".list",
|
||||
connectGroups: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
List.template = xml`
|
||||
<div class="controller" style="max-width: 900px; min-width: 900px;">
|
||||
<div class="content" style="max-width: 600px;">
|
||||
<div t-ref="renderer" class="renderer d-flex" style="overflow: visible;">
|
||||
<div t-foreach="[1, 2, 3]" t-as="c" t-key="c" t-attf-class="list m-0 list{{ c }}">
|
||||
<div style="min-width: 300px; min-height: 50px;"
|
||||
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>
|
||||
`;
|
||||
await mount(List, target);
|
||||
|
||||
const controller = target.querySelector(".controller");
|
||||
const content = target.querySelector(".content");
|
||||
const renderer = target.querySelector(".renderer");
|
||||
|
||||
assert.strictEqual(content.scrollLeft, 0);
|
||||
assert.strictEqual(controller.getBoundingClientRect().width, 900);
|
||||
assert.strictEqual(content.getBoundingClientRect().width, 600);
|
||||
assert.strictEqual(renderer.getBoundingClientRect().width, 600);
|
||||
assert.strictEqual(renderer.scrollWidth, 900);
|
||||
assert.containsNone(target, ".item.o_dragged");
|
||||
|
||||
const dragHelpers = await drag(".item11");
|
||||
|
||||
// Drag first record of first group to the right
|
||||
await dragHelpers.moveTo(".list3 .item");
|
||||
|
||||
// Next frame (normal time delta)
|
||||
await advanceFrame();
|
||||
|
||||
// Verify that there is no scrolling
|
||||
assert.strictEqual(content.scrollLeft, 0);
|
||||
assert.containsOnce(target, ".item.o_dragged");
|
||||
|
||||
const dragged = target.querySelector(".item.o_dragged");
|
||||
const sibling = target.querySelector(".list3 .item");
|
||||
// Verify that the dragged element is allowed to go inside the
|
||||
// overflowing part of the draggable container.
|
||||
assert.strictEqual(
|
||||
dragged.getBoundingClientRect().right,
|
||||
900 + target.getBoundingClientRect().x
|
||||
);
|
||||
assert.strictEqual(
|
||||
sibling.getBoundingClientRect().right,
|
||||
900 + target.getBoundingClientRect().x
|
||||
);
|
||||
|
||||
// Cancel drag: press "Escape"
|
||||
await dragHelpers.cancel();
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(target, ".item.o_dragged");
|
||||
});
|
||||
|
||||
QUnit.test("Dynamically disable sortable feature", async (assert) => {
|
||||
|
|
@ -366,38 +407,6 @@ QUnit.module("UI", ({ beforeEach }) => {
|
|||
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) => {
|
||||
|
|
@ -409,7 +418,7 @@ QUnit.module("UI", ({ beforeEach }) => {
|
|||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
onDragStart() {
|
||||
assert.step("Initation of the drag sequence");
|
||||
assert.step("Initiation of the drag sequence");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -425,13 +434,20 @@ QUnit.module("UI", ({ beforeEach }) => {
|
|||
await mount(List, target);
|
||||
|
||||
// Move the element from only 5 pixels
|
||||
await dragAndDrop(".item:first-child", ".item:first-child", { x: 5, y: 5 });
|
||||
const listItem = target.querySelector(".item:first-child");
|
||||
await dragAndDrop(listItem, listItem, {
|
||||
x: listItem.getBoundingClientRect().width / 2,
|
||||
y: listItem.getBoundingClientRect().height / 2 + 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 });
|
||||
await dragAndDrop(".item:first-child", ".item:first-child", {
|
||||
x: listItem.getBoundingClientRect().width / 2 + 10,
|
||||
y: listItem.getBoundingClientRect().height / 2 + 10,
|
||||
});
|
||||
assert.verifySteps(
|
||||
["Initation of the drag sequence"],
|
||||
["Initiation of the drag sequence"],
|
||||
"A drag sequence should have been initiated"
|
||||
);
|
||||
}
|
||||
|
|
@ -482,4 +498,115 @@ QUnit.module("UI", ({ beforeEach }) => {
|
|||
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test("the classes parameters (placeholderElement, helpElement)", async (assert) => {
|
||||
assert.expect(7);
|
||||
|
||||
let dragElement;
|
||||
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
placeholderClasses: ["placeholder-t1", "placeholder-t2"],
|
||||
followingElementClasses: ["add-1", "add-2"],
|
||||
onDragStart({ element }) {
|
||||
dragElement = element;
|
||||
assert.hasClass(dragElement, "add-1");
|
||||
assert.hasClass(dragElement, "add-2");
|
||||
// the placeholder is added in onDragStart after the current element
|
||||
const children = [...dragElement.parentElement.children];
|
||||
const placeholder = children[children.indexOf(dragElement) + 1];
|
||||
assert.hasClass(placeholder, "placeholder-t1");
|
||||
assert.hasClass(placeholder, "placeholder-t2");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// First item after 2nd item
|
||||
const { drop, moveTo } = await drag(".item:first-child");
|
||||
await moveTo(".item:nth-child(2)");
|
||||
await drop();
|
||||
assert.doesNotHaveClass(dragElement, "add-1");
|
||||
assert.doesNotHaveClass(dragElement, "add-2");
|
||||
assert.containsNone(target, ".item.placeholder-t1.placeholder-t2");
|
||||
});
|
||||
|
||||
QUnit.test("applyChangeOnDrop option", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
placeholderClasses: ["placeholder"],
|
||||
applyChangeOnDrop: true,
|
||||
onDragStart({ element }) {
|
||||
const items = [...target.querySelectorAll(".item:not(.placeholder)")];
|
||||
assert.strictEqual(items.map((el) => el.innerText).toString(), "1,2,3");
|
||||
},
|
||||
onDragEnd({ element, group }) {
|
||||
const items = [...target.querySelectorAll(".item:not(.placeholder)")];
|
||||
assert.strictEqual(items.map((el) => el.innerText).toString(), "2,1,3");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// First item after 2nd item
|
||||
const { drop, moveTo } = await drag(".item:first-child");
|
||||
await moveTo(".item:nth-child(2)");
|
||||
await drop();
|
||||
});
|
||||
|
||||
QUnit.test("clone option", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
class List extends Component {
|
||||
setup() {
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
placeholderClasses: ["placeholder"],
|
||||
clone: false,
|
||||
onDragStart({ element }) {
|
||||
assert.containsOnce(target, ".placeholder:not(.item)");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// First item after 2nd item
|
||||
const { drop, moveTo } = await drag(".item:first-child");
|
||||
await moveTo(".item:nth-child(2)");
|
||||
await drop();
|
||||
assert.containsNone(target, ".placeholder:not(.item)");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { escapeRegExp, intersperse, sprintf } from "@web/core/utils/strings";
|
||||
import { _lt, translatedTerms } from "@web/core/l10n/translation";
|
||||
import {
|
||||
escape,
|
||||
escapeRegExp,
|
||||
intersperse,
|
||||
isEmail,
|
||||
sprintf,
|
||||
unaccent,
|
||||
} from "@web/core/utils/strings";
|
||||
import { _t, translatedTerms } from "@web/core/l10n/translation";
|
||||
import { patchWithCleanup } from "../../helpers/utils";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
|
|
@ -52,6 +59,13 @@ QUnit.module("utils", () => {
|
|||
assert.deepEqual(intersperse("12345678", [3, 0], "."), "12.345.678");
|
||||
});
|
||||
|
||||
QUnit.test("unaccent", function (assert) {
|
||||
assert.strictEqual(unaccent("éèàôù"), "eeaou");
|
||||
assert.strictEqual(unaccent("ⱮɀꝾƶⱵȥ"), "mzgzhz"); // single characters
|
||||
assert.strictEqual(unaccent("DZDŽꝎꜩꝡƕ"), "dzdzootzvyhv"); // doubled characters
|
||||
assert.strictEqual(unaccent("ⱮɀꝾƶⱵȥ", true), "MzGzHz"); // case sensitive characters
|
||||
});
|
||||
|
||||
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!");
|
||||
|
|
@ -88,13 +102,39 @@ QUnit.module("utils", () => {
|
|||
two: "två",
|
||||
});
|
||||
|
||||
assert.deepEqual(sprintf("Hello %s", _lt("one")), "Hello en");
|
||||
assert.deepEqual(sprintf("Hello %s %s", _lt("one"), _lt("two")), "Hello en två");
|
||||
assert.deepEqual(sprintf("Hello %s", _t("one")), "Hello en");
|
||||
assert.deepEqual(sprintf("Hello %s %s", _t("one"), _t("two")), "Hello en två");
|
||||
|
||||
const vals = {
|
||||
one: _lt("one"),
|
||||
two: _lt("two"),
|
||||
one: _t("one"),
|
||||
two: _t("two"),
|
||||
};
|
||||
assert.deepEqual(sprintf("Hello %(two)s %(one)s", vals), "Hello två en");
|
||||
});
|
||||
|
||||
QUnit.test("escape", (assert) => {
|
||||
assert.strictEqual(escape("<a>this is a link</a>"), "<a>this is a link</a>");
|
||||
assert.strictEqual(
|
||||
escape(`<a href="https://www.odoo.com">odoo<a>`),
|
||||
`<a href="https://www.odoo.com">odoo<a>`
|
||||
);
|
||||
assert.strictEqual(
|
||||
escape(`<a href='https://www.odoo.com'>odoo<a>`),
|
||||
`<a href='https://www.odoo.com'>odoo<a>`
|
||||
);
|
||||
assert.strictEqual(
|
||||
escape("<a href='https://www.odoo.com'>Odoo`s website<a>"),
|
||||
`<a href='https://www.odoo.com'>Odoo`s website<a>`
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("isEmail", (assert) => {
|
||||
assert.notOk(isEmail(""));
|
||||
assert.notOk(isEmail("test"));
|
||||
assert.notOk(isEmail("test@odoo"));
|
||||
assert.notOk(isEmail("test@odoo@odoo.com"));
|
||||
assert.notOk(isEmail("te st@odoo.com"));
|
||||
|
||||
assert.ok(isEmail("test@odoo.com"));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,163 +1,549 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { debounce, throttleForAnimation } from "@web/core/utils/timing";
|
||||
import {
|
||||
batched,
|
||||
debounce,
|
||||
throttleForAnimation,
|
||||
useDebounced,
|
||||
useThrottleForAnimation,
|
||||
} from "@web/core/utils/timing";
|
||||
import {
|
||||
makeDeferred,
|
||||
patchWithCleanup,
|
||||
mockTimeout,
|
||||
mockAnimationFrame,
|
||||
mount,
|
||||
getFixture,
|
||||
click,
|
||||
destroy,
|
||||
} from "../../helpers/utils";
|
||||
|
||||
function nextMicroTick() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function nextAnimationFrame() {
|
||||
return new Promise((resolve) => window.requestAnimationFrame(resolve));
|
||||
}
|
||||
|
||||
function nextSetTimeout() {
|
||||
return new Promise(setTimeout);
|
||||
}
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("timing");
|
||||
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
|
||||
|
||||
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}`);
|
||||
assert.verifySteps(["resolved 42"]);
|
||||
});
|
||||
|
||||
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("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, 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 leading and trailing", async function (assert) {
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
const myFunc = (lastValue) => {
|
||||
assert.step("myFunc");
|
||||
return lastValue;
|
||||
};
|
||||
const myDebouncedFunc = debounce(myFunc, 3000, { leading: true, trailing: true });
|
||||
myDebouncedFunc(42).then((x) => {
|
||||
assert.step("resolved " + x);
|
||||
});
|
||||
myDebouncedFunc(43).then((x) => {
|
||||
assert.step("resolved " + x);
|
||||
});
|
||||
myDebouncedFunc(44).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"]);
|
||||
|
||||
await execRegisteredTimeouts();
|
||||
await Promise.resolve(); // wait for the inner promise
|
||||
assert.verifySteps(["myFunc", "resolved 44"]);
|
||||
});
|
||||
|
||||
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) => {
|
||||
const { advanceFrame, execRegisteredAnimationFrames } = mockAnimationFrame();
|
||||
const throttledFn = throttleForAnimation((val) => {
|
||||
assert.step(`${val}`);
|
||||
});
|
||||
|
||||
// A single call is executed immediately
|
||||
throttledFn(1);
|
||||
assert.verifySteps(["1"], "has been called on the leading edge");
|
||||
execRegisteredAnimationFrames();
|
||||
assert.verifySteps([], "has not been called");
|
||||
|
||||
// Successive calls
|
||||
throttledFn(1);
|
||||
assert.verifySteps(["1"], "has been called on the leading edge");
|
||||
throttledFn(2);
|
||||
throttledFn(3);
|
||||
assert.verifySteps([], "has not been called");
|
||||
execRegisteredAnimationFrames();
|
||||
assert.verifySteps(["3"], "only the last queued call was executed");
|
||||
|
||||
// Can be cancelled
|
||||
throttledFn(1);
|
||||
assert.verifySteps(["1"], "has been called on the leading edge");
|
||||
throttledFn(2);
|
||||
throttledFn(3);
|
||||
throttledFn.cancel();
|
||||
execRegisteredAnimationFrames();
|
||||
assert.verifySteps([], "queued throttled function calls were cancelled correctly");
|
||||
|
||||
// Successive calls: more precise timing case
|
||||
throttledFn(1);
|
||||
assert.verifySteps(["1"], "has been called on the leading edge");
|
||||
await advanceFrame();
|
||||
throttledFn(2);
|
||||
assert.verifySteps(["2"], "has been called on the leading edge");
|
||||
throttledFn(3);
|
||||
throttledFn(4);
|
||||
await advanceFrame();
|
||||
assert.verifySteps(["4"], "last call is executed on the trailing edge");
|
||||
execRegisteredAnimationFrames();
|
||||
assert.verifySteps([], "has not been called");
|
||||
});
|
||||
|
||||
QUnit.module("hooks", () => {
|
||||
QUnit.test("useDebounced: cancels on comp destroy", async function (assert) {
|
||||
const { advanceTime } = mockTimeout();
|
||||
class C extends Component {
|
||||
static template = xml`<button class="c" t-on-click="debounced">C</button>`;
|
||||
setup() {
|
||||
this.debounced = useDebounced(() => assert.step("debounced"), 1000);
|
||||
}
|
||||
}
|
||||
const fixture = getFixture();
|
||||
const comp = await mount(C, fixture);
|
||||
assert.verifySteps([]);
|
||||
assert.containsOnce(fixture, "button.c");
|
||||
|
||||
await click(fixture, "button.c");
|
||||
await advanceTime(999);
|
||||
assert.verifySteps([]);
|
||||
await advanceTime(1);
|
||||
assert.verifySteps(["debounced"]);
|
||||
|
||||
await click(fixture, "button.c");
|
||||
await advanceTime(999);
|
||||
assert.verifySteps([]);
|
||||
destroy(comp);
|
||||
await advanceTime(1);
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"useDebounced with execBeforeUnmount option (callback not resolved before comp destroy)",
|
||||
async function (assert) {
|
||||
const { advanceTime } = mockTimeout();
|
||||
class C extends Component {
|
||||
static template = xml`<button class="c" t-on-click="() => this.debounced('hello')">C</button>`;
|
||||
setup() {
|
||||
this.debounced = useDebounced(
|
||||
(p) => assert.step(`debounced: ${p}`),
|
||||
1000,
|
||||
{
|
||||
execBeforeUnmount: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
const fixture = getFixture();
|
||||
const comp = await mount(C, fixture);
|
||||
assert.verifySteps([]);
|
||||
assert.containsOnce(fixture, "button.c");
|
||||
|
||||
await click(fixture, "button.c");
|
||||
await advanceTime(999);
|
||||
assert.verifySteps([]);
|
||||
await advanceTime(1);
|
||||
assert.verifySteps(["debounced: hello"]);
|
||||
|
||||
await click(fixture, "button.c");
|
||||
await advanceTime(999);
|
||||
assert.verifySteps([]);
|
||||
destroy(comp);
|
||||
assert.verifySteps(["debounced: hello"]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"useDebounced with execBeforeUnmount option (callback resolved before comp destroy)",
|
||||
async function (assert) {
|
||||
const { advanceTime } = mockTimeout();
|
||||
class C extends Component {
|
||||
static template = xml`<button class="c" t-on-click="debounced">C</button>`;
|
||||
setup() {
|
||||
this.debounced = useDebounced(() => assert.step("debounced"), 1000, {
|
||||
execBeforeUnmount: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
const fixture = getFixture();
|
||||
const comp = await mount(C, fixture);
|
||||
assert.verifySteps([]);
|
||||
assert.containsOnce(fixture, "button.c");
|
||||
|
||||
await click(fixture, "button.c");
|
||||
await advanceTime(999);
|
||||
assert.verifySteps([]);
|
||||
await advanceTime(1);
|
||||
assert.verifySteps(["debounced"]);
|
||||
|
||||
destroy(comp);
|
||||
await advanceTime(1);
|
||||
assert.verifySteps([]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("useThrottleForAnimation: cancels on comp destroy", async function (assert) {
|
||||
const { advanceFrame, execRegisteredAnimationFrames } = mockAnimationFrame();
|
||||
class C extends Component {
|
||||
static template = xml`<button class="c" t-on-click="throttled">C</button>`;
|
||||
setup() {
|
||||
this.throttled = useThrottleForAnimation(
|
||||
() => assert.step("throttled"),
|
||||
1000
|
||||
);
|
||||
}
|
||||
}
|
||||
const fixture = getFixture();
|
||||
const comp = await mount(C, fixture);
|
||||
assert.verifySteps([]);
|
||||
assert.containsOnce(fixture, "button.c");
|
||||
|
||||
// Without destroy
|
||||
await click(fixture, "button.c");
|
||||
assert.verifySteps(["throttled"]);
|
||||
await click(fixture, "button.c");
|
||||
assert.verifySteps([]);
|
||||
await advanceFrame();
|
||||
assert.verifySteps(["throttled"]);
|
||||
|
||||
// Clean restart
|
||||
execRegisteredAnimationFrames();
|
||||
assert.verifySteps([]);
|
||||
|
||||
// With destroy
|
||||
await click(fixture, "button.c");
|
||||
assert.verifySteps(["throttled"]);
|
||||
await click(fixture, "button.c");
|
||||
assert.verifySteps([]);
|
||||
destroy(comp);
|
||||
await advanceFrame();
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("batched", () => {
|
||||
QUnit.test("callback is called only once after operations", async (assert) => {
|
||||
let n = 0;
|
||||
const fn = batched(() => n++);
|
||||
assert.strictEqual(n, 0);
|
||||
fn();
|
||||
fn();
|
||||
assert.strictEqual(n, 0);
|
||||
|
||||
await nextMicroTick();
|
||||
assert.strictEqual(n, 1);
|
||||
await nextMicroTick();
|
||||
assert.strictEqual(n, 1);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"calling batched function from within the callback is not treated as part of the original batch",
|
||||
async (assert) => {
|
||||
let n = 0;
|
||||
const fn = batched(() => {
|
||||
n++;
|
||||
if (n === 1) {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(n, 0);
|
||||
fn();
|
||||
assert.strictEqual(n, 0);
|
||||
await nextMicroTick(); // First batch
|
||||
assert.strictEqual(n, 1);
|
||||
await nextMicroTick(); // Second batch initiated from within the callback
|
||||
assert.strictEqual(n, 2);
|
||||
await nextMicroTick();
|
||||
assert.strictEqual(n, 2);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("callback is called twice", async (assert) => {
|
||||
let n = 0;
|
||||
const fn = batched(() => n++);
|
||||
assert.strictEqual(n, 0);
|
||||
fn();
|
||||
assert.strictEqual(n, 0);
|
||||
await nextMicroTick();
|
||||
assert.strictEqual(n, 1);
|
||||
fn();
|
||||
assert.strictEqual(n, 1);
|
||||
await nextMicroTick();
|
||||
assert.strictEqual(n, 2);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"callback is called only once after operations (synchronize at nextAnimationFrame)",
|
||||
async (assert) => {
|
||||
let n = 0;
|
||||
|
||||
const fn = batched(
|
||||
() => n++,
|
||||
() => new Promise((resolve) => window.requestAnimationFrame(resolve))
|
||||
);
|
||||
assert.strictEqual(n, 0);
|
||||
fn();
|
||||
fn();
|
||||
assert.strictEqual(n, 0);
|
||||
|
||||
await nextAnimationFrame();
|
||||
assert.strictEqual(n, 1);
|
||||
await nextAnimationFrame();
|
||||
assert.strictEqual(n, 1);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"calling batched function from within the callback is not treated as part of the original batch (synchronize at nextAnimationFrame)",
|
||||
async (assert) => {
|
||||
let n = 0;
|
||||
const fn = batched(
|
||||
() => {
|
||||
n++;
|
||||
if (n === 1) {
|
||||
fn();
|
||||
}
|
||||
},
|
||||
() => new Promise((resolve) => window.requestAnimationFrame(resolve))
|
||||
);
|
||||
|
||||
assert.strictEqual(n, 0);
|
||||
fn();
|
||||
assert.strictEqual(n, 0);
|
||||
await nextAnimationFrame(); // First batch
|
||||
assert.strictEqual(n, 1);
|
||||
await nextAnimationFrame(); // Second batch initiated from within the callback
|
||||
assert.strictEqual(n, 2);
|
||||
await nextAnimationFrame();
|
||||
assert.strictEqual(n, 2);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"callback is called twice (synchronize at nextAnimationFrame)",
|
||||
async (assert) => {
|
||||
let n = 0;
|
||||
const fn = batched(
|
||||
() => n++,
|
||||
() => new Promise((resolve) => window.requestAnimationFrame(resolve))
|
||||
);
|
||||
assert.strictEqual(n, 0);
|
||||
fn();
|
||||
assert.strictEqual(n, 0);
|
||||
await nextAnimationFrame();
|
||||
assert.strictEqual(n, 1);
|
||||
fn();
|
||||
assert.strictEqual(n, 1);
|
||||
await nextAnimationFrame();
|
||||
assert.strictEqual(n, 2);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"callback is called only once after operations (synchronize at setTimeout)",
|
||||
async (assert) => {
|
||||
let n = 0;
|
||||
|
||||
const fn = batched(
|
||||
() => n++,
|
||||
() => new Promise(setTimeout)
|
||||
);
|
||||
assert.strictEqual(n, 0);
|
||||
fn();
|
||||
fn();
|
||||
assert.strictEqual(n, 0);
|
||||
|
||||
await nextSetTimeout();
|
||||
assert.strictEqual(n, 1);
|
||||
await nextSetTimeout();
|
||||
assert.strictEqual(n, 1);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"calling batched function from within the callback is not treated as part of the original batch (synchronize at setTimeout)",
|
||||
async (assert) => {
|
||||
let n = 0;
|
||||
const fn = batched(
|
||||
() => {
|
||||
n++;
|
||||
if (n === 1) {
|
||||
fn();
|
||||
}
|
||||
},
|
||||
() => new Promise(setTimeout)
|
||||
);
|
||||
|
||||
assert.strictEqual(n, 0);
|
||||
fn();
|
||||
assert.strictEqual(n, 0);
|
||||
await nextSetTimeout(); // First batch
|
||||
assert.strictEqual(n, 1);
|
||||
await nextSetTimeout(); // Second batch initiated from within the callback
|
||||
assert.strictEqual(n, 2);
|
||||
await nextSetTimeout();
|
||||
assert.strictEqual(n, 2);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("callback is called twice (synchronize at setTimeout)", async (assert) => {
|
||||
let n = 0;
|
||||
const fn = batched(
|
||||
() => n++,
|
||||
() => new Promise(setTimeout)
|
||||
);
|
||||
assert.strictEqual(n, 0);
|
||||
fn();
|
||||
assert.strictEqual(n, 0);
|
||||
await nextSetTimeout();
|
||||
assert.strictEqual(n, 1);
|
||||
fn();
|
||||
assert.strictEqual(n, 1);
|
||||
await nextSetTimeout();
|
||||
assert.strictEqual(n, 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("throttleForAnimationScrollEvent", async (assert) => {
|
||||
assert.expect(5);
|
||||
const execAnimationFrameCallbacks = mockAnimationFrame();
|
||||
assert.expect(9);
|
||||
const { advanceFrame, execRegisteredAnimationFrames } = mockAnimationFrame();
|
||||
let resolveThrottled;
|
||||
const throttled = new Promise(resolve => resolveThrottled = resolve);
|
||||
let 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.
|
||||
|
|
@ -174,7 +560,7 @@ QUnit.module("utils", () => {
|
|||
const childEl = document.createElement("div");
|
||||
childEl.style = "height: 200px; width: 200px;";
|
||||
let resolveScrolled;
|
||||
const scrolled = new Promise(resolve => resolveScrolled = resolve);
|
||||
let scrolled = new Promise(resolve => resolveScrolled = resolve);
|
||||
el.appendChild(childEl);
|
||||
el.addEventListener("scroll", (ev) => {
|
||||
assert.step("before scroll");
|
||||
|
|
@ -185,19 +571,34 @@ QUnit.module("utils", () => {
|
|||
document.body.appendChild(el);
|
||||
el.scrollBy(1, 1);
|
||||
el.scrollBy(2, 2);
|
||||
el.remove();
|
||||
await scrolled;
|
||||
await throttled;
|
||||
|
||||
assert.verifySteps([
|
||||
"before scroll",
|
||||
"throttled function called with DIV in event, but DIV in parameter",
|
||||
"after scroll",
|
||||
], "scroll happened and direct first call to throttled function happened too");
|
||||
|
||||
throttled = new Promise(resolve => resolveThrottled = resolve);
|
||||
scrolled = new Promise(resolve => resolveScrolled = resolve);
|
||||
el.scrollBy(3, 3);
|
||||
await scrolled;
|
||||
assert.verifySteps([
|
||||
"before scroll",
|
||||
// Further call is delayed.
|
||||
"after scroll",
|
||||
], "scroll happened but throttled function hasn't been called yet");
|
||||
setTimeout(execAnimationFrameCallbacks);
|
||||
setTimeout(() => {
|
||||
advanceFrame();
|
||||
execRegisteredAnimationFrames();
|
||||
});
|
||||
await throttled;
|
||||
assert.verifySteps(
|
||||
["throttled function called with null in event, but DIV in parameter"],
|
||||
"currentTarget was not available in throttled function's event"
|
||||
);
|
||||
el.remove();
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { getDataURLFromFile, getOrigin, url } from "@web/core/utils/urls";
|
||||
import {
|
||||
getDataURLFromFile,
|
||||
getOrigin,
|
||||
redirect,
|
||||
RedirectionError,
|
||||
url,
|
||||
} from "@web/core/utils/urls";
|
||||
import { patchWithCleanup } from "../../helpers/utils";
|
||||
|
||||
QUnit.module("URLS", (hooks) => {
|
||||
|
|
@ -63,6 +69,33 @@ QUnit.module("URLS", (hooks) => {
|
|||
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");
|
||||
assert.strictEqual(
|
||||
dataUrl,
|
||||
"data:text/plain;base64,",
|
||||
"dataURL for empty file is not proper"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("redirect", (assert) => {
|
||||
function testRedirect(url) {
|
||||
browser.location = {
|
||||
protocol: "http:",
|
||||
host: "testhost",
|
||||
origin: "http://www.test.com",
|
||||
pathname: "/some/tests",
|
||||
};
|
||||
redirect(url);
|
||||
return browser.location;
|
||||
}
|
||||
|
||||
assert.strictEqual(testRedirect("abc"), "http://www.test.com/some/abc");
|
||||
assert.strictEqual(testRedirect("./abc"), "http://www.test.com/some/abc");
|
||||
assert.strictEqual(testRedirect("../abc/def"), "http://www.test.com/abc/def");
|
||||
assert.strictEqual(testRedirect("/abc/def"), "http://www.test.com/abc/def");
|
||||
assert.strictEqual(testRedirect("/abc/def?x=y"), "http://www.test.com/abc/def?x=y");
|
||||
assert.strictEqual(testRedirect("/abc?x=y#a=1&b=2"), "http://www.test.com/abc?x=y#a=1&b=2");
|
||||
|
||||
assert.throws(() => testRedirect("https://www.odoo.com"), RedirectionError);
|
||||
assert.throws(() => testRedirect("javascript:alert('boom');"), RedirectionError);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/** @odoo-module **/
|
||||
import { XMLParser } from "@web/core/utils/xml";
|
||||
import { parseXML } from "@web/core/utils/xml";
|
||||
|
||||
QUnit.module("utils", () => {
|
||||
QUnit.module("xml");
|
||||
|
|
@ -7,10 +7,9 @@ QUnit.module("utils", () => {
|
|||
QUnit.test("parse error throws an exception", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
const parser = new XMLParser();
|
||||
let XMLToParse = "<invalid'>";
|
||||
try {
|
||||
parser.parseXML(XMLToParse);
|
||||
parseXML(XMLToParse);
|
||||
assert.step("no error");
|
||||
} catch (e) {
|
||||
if (e.message.includes("error occured while parsing")) {
|
||||
|
|
@ -20,7 +19,7 @@ QUnit.module("utils", () => {
|
|||
|
||||
XMLToParse = "<div><div>Valid</div><div><Invalid</div></div>";
|
||||
try {
|
||||
parser.parseXML(XMLToParse);
|
||||
parseXML(XMLToParse);
|
||||
assert.step("no error");
|
||||
} catch (e) {
|
||||
if (e.message.includes("error occured while parsing")) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,155 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, useRef, xml } from "@odoo/owl";
|
||||
import { useVirtual } from "@web/core/virtual_hook";
|
||||
import { getFixture, mount, patchWithCleanup, triggerEvent } from "../helpers/utils";
|
||||
|
||||
/**
|
||||
* @typedef ItemType
|
||||
* @property {number} id
|
||||
*
|
||||
* @typedef {import("@web/core/virtual_hook").VirtualHookParams<ItemType>} TestComponentProps
|
||||
*/
|
||||
|
||||
function objectToStyle(obj) {
|
||||
return Object.entries(obj)
|
||||
.map(([k, v]) => `${k}: ${v};`)
|
||||
.join("");
|
||||
}
|
||||
|
||||
/** @type {ItemType[]} */
|
||||
const ITEMS = Array.from({ length: 200 }, (_, i) => ({ id: i + 1 }));
|
||||
const ITEM_HEIGHT = 50;
|
||||
const ITEM_STYLE = objectToStyle({
|
||||
height: `${ITEM_HEIGHT}px`,
|
||||
width: "100%",
|
||||
border: "1px solid black",
|
||||
position: "absolute",
|
||||
"background-color": "white",
|
||||
});
|
||||
const CONTAINER_SIZE = 5 * ITEM_HEIGHT;
|
||||
const CONTAINER_STYLE = objectToStyle({
|
||||
height: `${CONTAINER_SIZE}px`,
|
||||
width: "100%",
|
||||
overflow: "auto",
|
||||
position: "relative",
|
||||
"background-color": "lightblue",
|
||||
});
|
||||
const MAX_SCROLL_TOP = ITEMS.length * ITEM_HEIGHT - CONTAINER_SIZE;
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} [target]
|
||||
* @param {TestComponentProps} [props]
|
||||
*/
|
||||
async function mountTestComponent(target, props) {
|
||||
/** @extends {Component<ItemType, any>} **/
|
||||
class Item extends Component {
|
||||
static props = ["id"];
|
||||
static template = xml`
|
||||
<div class="item" t-att-data-id="props.id" t-att-style="style" t-esc="props.id"/>
|
||||
`;
|
||||
get style() {
|
||||
return `top: ${(this.props.id - 1) * ITEM_HEIGHT}px; ${ITEM_STYLE}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** @extends {Component<TestComponentProps, any>} **/
|
||||
class TestComponent extends Component {
|
||||
static props = ["getItems?", "scrollableRef?", "initialScroll?", "getItemHeight?"];
|
||||
static components = { Item };
|
||||
static template = xml`
|
||||
<div class="scrollable" t-ref="scrollable" style="${CONTAINER_STYLE}">
|
||||
<div class="inner" t-att-style="innerStyle">
|
||||
<t t-foreach="items" t-as="item" t-key="item.id">
|
||||
<Item id="item.id"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
setup() {
|
||||
const scrollableRef = useRef("scrollable");
|
||||
this.items = useVirtual({
|
||||
getItems: () => ITEMS,
|
||||
getItemHeight: () => ITEM_HEIGHT,
|
||||
scrollableRef,
|
||||
...this.props,
|
||||
});
|
||||
}
|
||||
get innerStyle() {
|
||||
return `height: ${ITEMS.length * ITEM_HEIGHT}px;`;
|
||||
}
|
||||
}
|
||||
await mount(TestComponent, target, { props });
|
||||
}
|
||||
|
||||
function scroll(target, scrollTop) {
|
||||
target.querySelector(".scrollable").scrollTop = scrollTop;
|
||||
return triggerEvent(target, ".scrollable", "scroll");
|
||||
}
|
||||
|
||||
let target;
|
||||
QUnit.module("useVirtual hook", {
|
||||
async beforeEach() {
|
||||
target = getFixture();
|
||||
// In this test suite, we trick the hook by setting the window size to the size
|
||||
// of the scrollable, so that it is a measurable size and this suite can run
|
||||
// in a window of any size.
|
||||
patchWithCleanup(window, { innerHeight: CONTAINER_SIZE });
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("basic usage", async (assert) => {
|
||||
await mountTestComponent(target);
|
||||
assert.containsN(target, ".item", 11);
|
||||
assert.strictEqual(target.querySelector(".item").dataset.id, "1");
|
||||
assert.strictEqual(target.querySelector(".item:last-child").dataset.id, "11");
|
||||
|
||||
// scroll to the middle
|
||||
await scroll(target, MAX_SCROLL_TOP / 2);
|
||||
assert.containsN(target, ".item", 16);
|
||||
assert.strictEqual(target.querySelector(".item").dataset.id, "93");
|
||||
assert.strictEqual(target.querySelector(".item:last-child").dataset.id, "108");
|
||||
|
||||
// scroll to the end
|
||||
await scroll(target, MAX_SCROLL_TOP);
|
||||
assert.containsN(target, ".item", 11);
|
||||
assert.strictEqual(target.querySelector(".item").dataset.id, "190");
|
||||
assert.strictEqual(target.querySelector(".item:last-child").dataset.id, "200");
|
||||
});
|
||||
|
||||
QUnit.test("updates on resize", async (assert) => {
|
||||
await mountTestComponent(target);
|
||||
assert.containsN(target, ".item", 11);
|
||||
assert.strictEqual(target.querySelector(".item").dataset.id, "1");
|
||||
assert.strictEqual(target.querySelector(".item:last-child").dataset.id, "11");
|
||||
|
||||
// resize the window
|
||||
patchWithCleanup(window, { innerHeight: CONTAINER_SIZE / 2 });
|
||||
await triggerEvent(window, null, "resize");
|
||||
assert.containsN(target, ".item", 6);
|
||||
assert.strictEqual(target.querySelector(".item").dataset.id, "1");
|
||||
assert.strictEqual(target.querySelector(".item:last-child").dataset.id, "6");
|
||||
|
||||
// resize the window
|
||||
patchWithCleanup(window, { innerHeight: CONTAINER_SIZE * 2 });
|
||||
await triggerEvent(window, null, "resize");
|
||||
assert.containsN(target, ".item", 21);
|
||||
assert.strictEqual(target.querySelector(".item").dataset.id, "1");
|
||||
assert.strictEqual(target.querySelector(".item:last-child").dataset.id, "21");
|
||||
});
|
||||
|
||||
QUnit.test("initialScroll: middle", async (assert) => {
|
||||
const initialScroll = { top: MAX_SCROLL_TOP / 2 };
|
||||
await mountTestComponent(target, { initialScroll });
|
||||
assert.containsN(target, ".item", 16);
|
||||
assert.strictEqual(target.querySelector(".item").dataset.id, "93");
|
||||
assert.strictEqual(target.querySelector(".item:last-child").dataset.id, "108");
|
||||
});
|
||||
|
||||
QUnit.test("initialScroll: bottom", async (assert) => {
|
||||
const initialScroll = { top: MAX_SCROLL_TOP };
|
||||
await mountTestComponent(target, { initialScroll });
|
||||
assert.containsN(target, ".item", 11);
|
||||
assert.strictEqual(target.querySelector(".item").dataset.id, "190");
|
||||
assert.strictEqual(target.querySelector(".item:last-child").dataset.id, "200");
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue