mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 19:52:00 +02:00
vanilla 18.0
This commit is contained in:
parent
5454004ff9
commit
d7f6d2725e
979 changed files with 428093 additions and 0 deletions
|
|
@ -0,0 +1,678 @@
|
|||
/** @odoo-module alias=@web/../tests/mobile/core/action_swiper_tests default=false */
|
||||
|
||||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { hover, queryFirst } from "@odoo/hoot-dom";
|
||||
import { advanceTime, animationFrame, mockTouch } from "@odoo/hoot-mock";
|
||||
import { Component, onPatched, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineParams,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
swipeLeft,
|
||||
swipeRight,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { ActionSwiper } from "@web/core/action_swiper/action_swiper";
|
||||
import { Deferred } from "@web/core/utils/concurrency";
|
||||
|
||||
beforeEach(() => mockTouch(true));
|
||||
|
||||
// Tests marked as will fail on browsers that don't support
|
||||
// TouchEvent by default. It might be an option to activate on some browser.
|
||||
|
||||
test("render only its target if no props is given", async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { ActionSwiper };
|
||||
static template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper>
|
||||
<div class="target-component"/>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
expect("div.o_actionswiper").toHaveCount(0);
|
||||
expect("div.target-component").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("only render the necessary divs", async () => {
|
||||
await mountWithCleanup(ActionSwiper, {
|
||||
props: {
|
||||
onRightSwipe: {
|
||||
action: () => {},
|
||||
icon: "fa-circle",
|
||||
bgColor: "bg-warning",
|
||||
},
|
||||
slots: {},
|
||||
},
|
||||
});
|
||||
expect("div.o_actionswiper_right_swipe_area").toHaveCount(1);
|
||||
expect("div.o_actionswiper_left_swipe_area").toHaveCount(0);
|
||||
await mountWithCleanup(ActionSwiper, {
|
||||
props: {
|
||||
onLeftSwipe: {
|
||||
action: () => {},
|
||||
icon: "fa-circle",
|
||||
bgColor: "bg-warning",
|
||||
},
|
||||
slots: {},
|
||||
},
|
||||
});
|
||||
expect("div.o_actionswiper_right_swipe_area").toHaveCount(1);
|
||||
expect("div.o_actionswiper_left_swipe_area").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("render with the height of its content", async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { ActionSwiper };
|
||||
static template = xml`
|
||||
<div class="o-container d-flex" style="width: 200px; height: 200px; overflow: auto">
|
||||
<ActionSwiper onRightSwipe = "{
|
||||
action: () => this.onRightSwipe(),
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning'
|
||||
}">
|
||||
<div class="target-component" style="height: 800px">This element is very high and
|
||||
the o-container element must have a scrollbar</div>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
onRightSwipe() {
|
||||
expect.step("onRightSwipe");
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
expect(queryFirst(".o_actionswiper").scrollHeight).toBe(
|
||||
queryFirst(".target-component").scrollHeight,
|
||||
{ message: "the swiper has the height of its content" }
|
||||
);
|
||||
expect(queryFirst(".o_actionswiper").scrollHeight).toBeGreaterThan(
|
||||
queryFirst(".o_actionswiper").clientHeight,
|
||||
{ message: "the height of the swiper must make the parent div scrollable" }
|
||||
);
|
||||
});
|
||||
|
||||
test("can perform actions by swiping to the right", async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { ActionSwiper };
|
||||
static template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper onRightSwipe = "{
|
||||
action: () => this.onRightSwipe(),
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning'
|
||||
}">
|
||||
<div class="target-component" style="width: 200px; height: 80px">Test</div>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
onRightSwipe() {
|
||||
expect.step("onRightSwipe");
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
const swiper = queryFirst(".o_actionswiper");
|
||||
const targetContainer = queryFirst(".o_actionswiper_target_container");
|
||||
const dragHelper = await contains(swiper).drag({
|
||||
position: {
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
},
|
||||
});
|
||||
await dragHelper.moveTo(swiper, {
|
||||
position: {
|
||||
clientX: (3 * swiper.clientWidth) / 4,
|
||||
clientY: 0,
|
||||
},
|
||||
});
|
||||
expect(targetContainer.style.transform).toInclude("translateX", {
|
||||
message: "target has translateX",
|
||||
});
|
||||
|
||||
// Touch ends before the half of the distance has been reached
|
||||
await dragHelper.moveTo(swiper, {
|
||||
position: {
|
||||
clientX: swiper.clientWidth / 2 - 1,
|
||||
clientY: 0,
|
||||
},
|
||||
});
|
||||
await dragHelper.drop();
|
||||
await animationFrame();
|
||||
expect(targetContainer.style.transform).not.toInclude("translateX", {
|
||||
message: "target does not have a translate value",
|
||||
});
|
||||
|
||||
// Touch ends once the half of the distance has been crossed
|
||||
await swipeRight(".o_actionswiper");
|
||||
// The action is performed AND the component is reset
|
||||
expect(targetContainer.style.transform).not.toInclude("translateX", {
|
||||
message: "target does not have a translate value",
|
||||
});
|
||||
|
||||
expect.verifySteps(["onRightSwipe"]);
|
||||
});
|
||||
|
||||
test("can perform actions by swiping in both directions", async () => {
|
||||
expect.assertions(5);
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { ActionSwiper };
|
||||
static template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper
|
||||
onRightSwipe = "{
|
||||
action: () => this.onRightSwipe(),
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning'
|
||||
}"
|
||||
onLeftSwipe = "{
|
||||
action: () => this.onLeftSwipe(),
|
||||
icon: 'fa-check',
|
||||
bgColor: 'bg-success'
|
||||
}">
|
||||
<div class="target-component" style="width: 250px; height: 80px">Swipe in both directions</div>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
onRightSwipe() {
|
||||
expect.step("onRightSwipe");
|
||||
}
|
||||
onLeftSwipe() {
|
||||
expect.step("onLeftSwipe");
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
const swiper = queryFirst(".o_actionswiper");
|
||||
const targetContainer = queryFirst(".o_actionswiper_target_container");
|
||||
const dragHelper = await contains(swiper).drag({
|
||||
position: {
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
},
|
||||
});
|
||||
await dragHelper.moveTo(swiper, {
|
||||
position: {
|
||||
clientX: (3 * swiper.clientWidth) / 4,
|
||||
clientY: 0,
|
||||
},
|
||||
});
|
||||
expect(targetContainer.style.transform).toInclude("translateX", {
|
||||
message: "target has translateX",
|
||||
});
|
||||
// Touch ends before the half of the distance has been reached to the left
|
||||
await dragHelper.moveTo(swiper, {
|
||||
position: {
|
||||
clientX: -swiper.clientWidth / 2 + 1,
|
||||
clientY: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await dragHelper.drop();
|
||||
|
||||
expect(targetContainer.style.transform).not.toInclude("translateX", {
|
||||
message: "target does not have a translate value",
|
||||
});
|
||||
|
||||
// Touch ends once the half of the distance has been crossed to the left
|
||||
await swipeLeft(".o_actionswiper");
|
||||
expect.verifySteps(["onLeftSwipe"]);
|
||||
// Touch ends once the half of the distance has been crossed to the right
|
||||
await swipeRight(".o_actionswiper");
|
||||
|
||||
expect(targetContainer.style.transform).not.toInclude("translateX", {
|
||||
message: "target doesn't have translateX after all actions are performed",
|
||||
});
|
||||
|
||||
expect.verifySteps(["onRightSwipe"]);
|
||||
});
|
||||
|
||||
test("invert the direction of swipes when language is rtl", async () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
direction: "rtl",
|
||||
},
|
||||
});
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { ActionSwiper };
|
||||
static template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper
|
||||
onRightSwipe = "{
|
||||
action: () => this.onRightSwipe(),
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning'
|
||||
}"
|
||||
onLeftSwipe = "{
|
||||
action: () => this.onLeftSwipe(),
|
||||
icon: 'fa-check',
|
||||
bgColor: 'bg-success'
|
||||
}">
|
||||
<div class="target-component" style="width: 250px; height: 80px">Swipe in both directions</div>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
onRightSwipe() {
|
||||
expect.step("onRightSwipe");
|
||||
}
|
||||
onLeftSwipe() {
|
||||
expect.step("onLeftSwipe");
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
// Touch ends once the half of the distance has been crossed to the left
|
||||
await swipeLeft(".o_actionswiper");
|
||||
await advanceTime(500);
|
||||
// In rtl languages, actions are permuted
|
||||
expect.verifySteps(["onRightSwipe"]);
|
||||
await swipeRight(".o_actionswiper");
|
||||
await advanceTime(500);
|
||||
// In rtl languages, actions are permuted
|
||||
expect.verifySteps(["onLeftSwipe"]);
|
||||
});
|
||||
|
||||
test("swiping when the swiper contains scrollable areas", async () => {
|
||||
expect.assertions(7);
|
||||
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { ActionSwiper };
|
||||
static template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper
|
||||
onRightSwipe = "{
|
||||
action: () => this.onRightSwipe(),
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning'
|
||||
}"
|
||||
onLeftSwipe = "{
|
||||
action: () => this.onLeftSwipe(),
|
||||
icon: 'fa-check',
|
||||
bgColor: 'bg-success'
|
||||
}">
|
||||
<div class="target-component" style="width: 200px; height: 300px">
|
||||
<h1>Test about swiping and scrolling</h1>
|
||||
<div class="large-content overflow-auto">
|
||||
<h2>This div contains a larger element that will make it scrollable</h2>
|
||||
<p class="large-text" style="width: 400px">This element is so large it needs to be scrollable</p>
|
||||
</div>
|
||||
</div>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
|
||||
onRightSwipe() {
|
||||
expect.step("onRightSwipe");
|
||||
}
|
||||
|
||||
onLeftSwipe() {
|
||||
expect.step("onLeftSwipe");
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
const swiper = queryFirst(".o_actionswiper");
|
||||
const targetContainer = queryFirst(".o_actionswiper_target_container");
|
||||
const scrollable = queryFirst(".large-content");
|
||||
const largeText = queryFirst(".large-text", { root: scrollable });
|
||||
const clientYMiddleScrollBar = Math.floor(
|
||||
scrollable.getBoundingClientRect().top + scrollable.getBoundingClientRect().height / 2
|
||||
);
|
||||
|
||||
// The scrollable element is set as scrollable
|
||||
scrollable.scrollLeft = 100;
|
||||
let dragHelper = await contains(swiper).drag({
|
||||
position: {
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
},
|
||||
});
|
||||
await dragHelper.moveTo(swiper, {
|
||||
position: {
|
||||
clientX: (3 * swiper.clientWidth) / 4,
|
||||
clientY: 0,
|
||||
},
|
||||
});
|
||||
expect(targetContainer.style.transform).toInclude("translateX", {
|
||||
message: "the swiper can swipe if the scrollable area is not under touch pressure",
|
||||
});
|
||||
await dragHelper.moveTo(swiper, {
|
||||
position: {
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
},
|
||||
});
|
||||
await dragHelper.drop();
|
||||
|
||||
dragHelper = await contains(largeText).drag({
|
||||
position: {
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY: clientYMiddleScrollBar,
|
||||
},
|
||||
});
|
||||
await dragHelper.moveTo(largeText, {
|
||||
position: {
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY: clientYMiddleScrollBar,
|
||||
},
|
||||
});
|
||||
expect(targetContainer.style.transform).not.toInclude("translateX", {
|
||||
message:
|
||||
"the swiper has not swiped to the right because the scrollable element was scrollable to the left",
|
||||
});
|
||||
await dragHelper.drop();
|
||||
// The scrollable element is set at its left limit
|
||||
scrollable.scrollLeft = 0;
|
||||
await hover(largeText, {
|
||||
position: {
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY: clientYMiddleScrollBar,
|
||||
},
|
||||
});
|
||||
dragHelper = await contains(largeText).drag({
|
||||
position: {
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY: clientYMiddleScrollBar,
|
||||
},
|
||||
});
|
||||
await dragHelper.moveTo(largeText, {
|
||||
position: {
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY: clientYMiddleScrollBar,
|
||||
},
|
||||
});
|
||||
expect(targetContainer.style.transform).toInclude("translateX", {
|
||||
message:
|
||||
"the swiper has swiped to the right because the scrollable element couldn't scroll anymore to the left",
|
||||
});
|
||||
await dragHelper.drop();
|
||||
await advanceTime(500);
|
||||
expect.verifySteps(["onRightSwipe"]);
|
||||
|
||||
dragHelper = await contains(largeText).drag({
|
||||
position: {
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY: clientYMiddleScrollBar,
|
||||
},
|
||||
});
|
||||
await dragHelper.moveTo(largeText, {
|
||||
position: {
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY: clientYMiddleScrollBar,
|
||||
},
|
||||
});
|
||||
expect(targetContainer.style.transform).not.toInclude("translateX", {
|
||||
message:
|
||||
"the swiper has not swiped to the left because the scrollable element was scrollable to the right",
|
||||
});
|
||||
await dragHelper.drop();
|
||||
|
||||
// The scrollable element is set at its right limit
|
||||
scrollable.scrollLeft = scrollable.scrollWidth - scrollable.getBoundingClientRect().right;
|
||||
await hover(largeText, {
|
||||
position: {
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY: clientYMiddleScrollBar,
|
||||
},
|
||||
});
|
||||
dragHelper = await contains(largeText).drag({
|
||||
position: {
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY: clientYMiddleScrollBar,
|
||||
},
|
||||
});
|
||||
await dragHelper.moveTo(largeText, {
|
||||
position: {
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY: clientYMiddleScrollBar,
|
||||
},
|
||||
});
|
||||
expect(targetContainer.style.transform).toInclude("translateX", {
|
||||
message:
|
||||
"the swiper has swiped to the left because the scrollable element couldn't scroll anymore to the right",
|
||||
});
|
||||
await dragHelper.drop();
|
||||
await advanceTime(500);
|
||||
expect.verifySteps(["onLeftSwipe"]);
|
||||
});
|
||||
|
||||
test("preventing swipe on scrollable areas when language is rtl", async () => {
|
||||
expect.assertions(6);
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
direction: "rtl",
|
||||
},
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { ActionSwiper };
|
||||
static template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper
|
||||
onRightSwipe="{
|
||||
action: () => this.onRightSwipe(),
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning'
|
||||
}"
|
||||
onLeftSwipe="{
|
||||
action: () => this.onLeftSwipe(),
|
||||
icon: 'fa-check',
|
||||
bgColor: 'bg-success'
|
||||
}">
|
||||
<div class="target-component" style="width: 200px; height: 300px">
|
||||
<h1>Test about swiping and scrolling for rtl</h1>
|
||||
<div class="large-content overflow-auto">
|
||||
<h2>elballorcs ti ekam lliw taht tnemele regral a sniatnoc vid sihT</h2>
|
||||
<p class="large-text" style="width: 400px">elballorcs eb ot sdeen ti egral os si tnemele sihT</p>
|
||||
</div>
|
||||
</div>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
|
||||
onRightSwipe() {
|
||||
expect.step("onRightSwipe");
|
||||
}
|
||||
|
||||
onLeftSwipe() {
|
||||
expect.step("onLeftSwipe");
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
const targetContainer = queryFirst(".o_actionswiper_target_container");
|
||||
const scrollable = queryFirst(".large-content");
|
||||
const largeText = queryFirst(".large-text", { root: scrollable });
|
||||
const scrollableMiddleClientY = Math.floor(
|
||||
scrollable.getBoundingClientRect().top + scrollable.getBoundingClientRect().height / 2
|
||||
);
|
||||
// RIGHT => Left trigger
|
||||
// The scrollable element is set as scrollable
|
||||
scrollable.scrollLeft = 100;
|
||||
let dragHelper = await contains(largeText).drag({
|
||||
position: {
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY: scrollableMiddleClientY,
|
||||
},
|
||||
});
|
||||
await dragHelper.moveTo(largeText, {
|
||||
position: {
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY: scrollableMiddleClientY,
|
||||
},
|
||||
});
|
||||
|
||||
expect(targetContainer.style.transform).not.toInclude("translateX", {
|
||||
message:
|
||||
"the swiper has not swiped to the right because the scrollable element was scrollable to the left",
|
||||
});
|
||||
await dragHelper.drop();
|
||||
|
||||
// The scrollable element is set at its left limit
|
||||
scrollable.scrollLeft = 0;
|
||||
await hover(largeText, {
|
||||
position: {
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY: scrollableMiddleClientY,
|
||||
},
|
||||
});
|
||||
dragHelper = await contains(largeText).drag({
|
||||
position: {
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY: scrollableMiddleClientY,
|
||||
},
|
||||
});
|
||||
await dragHelper.moveTo(largeText, {
|
||||
position: {
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY: scrollableMiddleClientY,
|
||||
},
|
||||
});
|
||||
expect(targetContainer.style.transform).toInclude("translateX", {
|
||||
message:
|
||||
"the swiper has swiped to the right because the scrollable element couldn't scroll anymore to the left",
|
||||
});
|
||||
await dragHelper.drop();
|
||||
await advanceTime(500);
|
||||
// In rtl languages, actions are permuted
|
||||
expect.verifySteps(["onLeftSwipe"]);
|
||||
// LEFT => RIGHT trigger
|
||||
await hover(largeText, {
|
||||
position: {
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY: scrollableMiddleClientY,
|
||||
},
|
||||
});
|
||||
dragHelper = await contains(largeText).drag({
|
||||
position: {
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY: scrollableMiddleClientY,
|
||||
},
|
||||
});
|
||||
await dragHelper.moveTo(largeText, {
|
||||
position: {
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY: scrollableMiddleClientY,
|
||||
},
|
||||
});
|
||||
expect(targetContainer.style.transform).not.toInclude("translateX", {
|
||||
message:
|
||||
"the swiper has not swiped to the left because the scrollable element was scrollable to the right",
|
||||
});
|
||||
await dragHelper.drop();
|
||||
// The scrollable element is set at its right limit
|
||||
scrollable.scrollLeft = scrollable.scrollWidth - scrollable.getBoundingClientRect().right;
|
||||
await hover(largeText, {
|
||||
position: {
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY: scrollableMiddleClientY,
|
||||
},
|
||||
});
|
||||
dragHelper = await contains(largeText).drag({
|
||||
position: {
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY: scrollableMiddleClientY,
|
||||
},
|
||||
});
|
||||
await dragHelper.moveTo(largeText, {
|
||||
position: {
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY: scrollableMiddleClientY,
|
||||
},
|
||||
});
|
||||
expect(targetContainer.style.transform).toInclude("translateX", {
|
||||
message:
|
||||
"the swiper has swiped to the left because the scrollable element couldn't scroll anymore to the right",
|
||||
});
|
||||
await dragHelper.drop();
|
||||
await advanceTime(500);
|
||||
|
||||
// In rtl languages, actions are permuted
|
||||
expect.verifySteps(["onRightSwipe"]);
|
||||
});
|
||||
|
||||
test("swipeInvalid prop prevents swiping", async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { ActionSwiper };
|
||||
static template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper onRightSwipe = "{
|
||||
action: () => this.onRightSwipe(),
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning',
|
||||
}" swipeInvalid = "swipeInvalid">
|
||||
<div class="target-component" style="width: 200px; height: 80px">Test</div>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
onRightSwipe() {
|
||||
expect.step("onRightSwipe");
|
||||
}
|
||||
swipeInvalid() {
|
||||
expect.step("swipeInvalid");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
const targetContainer = queryFirst(".o_actionswiper_target_container");
|
||||
// Touch ends once the half of the distance has been crossed
|
||||
await swipeRight(".o_actionswiper");
|
||||
|
||||
expect(targetContainer.style.transform).not.toInclude("translateX", {
|
||||
message: "target doesn't have translateX after action is performed",
|
||||
});
|
||||
expect.verifySteps(["swipeInvalid"]);
|
||||
});
|
||||
|
||||
test("action should be done before a new render", async () => {
|
||||
let executingAction = false;
|
||||
const prom = new Deferred();
|
||||
|
||||
patchWithCleanup(ActionSwiper.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
onPatched(() => {
|
||||
if (executingAction) {
|
||||
expect.step("ActionSwiper patched");
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
static props = [];
|
||||
static components = { ActionSwiper };
|
||||
static template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper animationType="'forwards'" onRightSwipe = "{
|
||||
action: () => this.onRightSwipe(),
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning',
|
||||
}">
|
||||
<span>test</span>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
|
||||
async onRightSwipe() {
|
||||
await animationFrame();
|
||||
expect.step("action done");
|
||||
prom.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
await swipeRight(".o_actionswiper");
|
||||
executingAction = true;
|
||||
await prom;
|
||||
await animationFrame();
|
||||
expect.verifySteps(["action done", "ActionSwiper patched"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,817 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
isInViewPort,
|
||||
isScrollable,
|
||||
pointerDown,
|
||||
pointerUp,
|
||||
press,
|
||||
queryAllAttributes,
|
||||
queryAllTexts,
|
||||
queryFirst,
|
||||
queryOne,
|
||||
queryRect,
|
||||
} from "@odoo/hoot-dom";
|
||||
import { Deferred, animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
|
||||
import { contains, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
|
||||
|
||||
/**
|
||||
* Helper needed until `isInViewPort` also checks intermediate parent elements.
|
||||
* This is to make sure an element is actually visible, not just "within
|
||||
* viewport boundaries" but below or above a parent's scroll point.
|
||||
*
|
||||
* @param {import("@odoo/hoot-dom").Target} target
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInViewWithinScrollableY(target) {
|
||||
const element = queryFirst(target);
|
||||
let container = element.parentElement;
|
||||
while (
|
||||
container &&
|
||||
(container.scrollHeight <= container.clientHeight ||
|
||||
!["auto", "scroll"].includes(getComputedStyle(container).overflowY))
|
||||
) {
|
||||
container = container.parentElement;
|
||||
}
|
||||
if (!container) {
|
||||
return isInViewPort(element);
|
||||
}
|
||||
const { x, y } = queryRect(element);
|
||||
const { height: containerHeight, width: containerWidth } = queryRect(container);
|
||||
return y > 0 && y < containerHeight && x > 0 && x < containerWidth;
|
||||
}
|
||||
|
||||
test("can be rendered", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<AutoComplete
|
||||
value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
onSelect="() => {}"
|
||||
/>
|
||||
`;
|
||||
static props = {};
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o-autocomplete").toHaveCount(1);
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
|
||||
await contains(".o-autocomplete input").click();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
|
||||
expect(queryAllTexts(".o-autocomplete--dropdown-item")).toEqual(["World", "Hello"]);
|
||||
|
||||
const dropdownItemIds = queryAllAttributes(".dropdown-item", "id");
|
||||
expect(dropdownItemIds).toEqual(["autocomplete_0_0", "autocomplete_0_1"]);
|
||||
expect(queryAllAttributes(".dropdown-item", "role")).toEqual(["option", "option"]);
|
||||
expect(queryAllAttributes(".dropdown-item", "aria-selected")).toEqual(["true", "false"]);
|
||||
expect(".o-autocomplete--input").toHaveAttribute("aria-activedescendant", dropdownItemIds[0]);
|
||||
});
|
||||
|
||||
test("select option", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<AutoComplete
|
||||
value="state.value"
|
||||
sources="sources"
|
||||
onSelect="(option) => this.onSelect(option)"
|
||||
/>
|
||||
`;
|
||||
static props = {};
|
||||
setup() {
|
||||
this.state = useState({
|
||||
value: "Hello",
|
||||
});
|
||||
}
|
||||
get sources() {
|
||||
return [
|
||||
{
|
||||
options: [{ label: "World" }, { label: "Hello" }],
|
||||
},
|
||||
];
|
||||
}
|
||||
onSelect(option) {
|
||||
this.state.value = option.label;
|
||||
expect.step(option.label);
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o-autocomplete input").toHaveValue("Hello");
|
||||
|
||||
await contains(".o-autocomplete input").click();
|
||||
await contains(queryFirst(".o-autocomplete--dropdown-item")).click();
|
||||
expect(".o-autocomplete input").toHaveValue("World");
|
||||
expect.verifySteps(["World"]);
|
||||
|
||||
await contains(".o-autocomplete input").click();
|
||||
await contains(".o-autocomplete--dropdown-item:last").click();
|
||||
expect(".o-autocomplete input").toHaveValue("Hello");
|
||||
expect.verifySteps(["Hello"]);
|
||||
});
|
||||
|
||||
test("autocomplete with resetOnSelect='true'", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<div>
|
||||
<div class= "test_value" t-esc="state.value"/>
|
||||
<AutoComplete
|
||||
value="''"
|
||||
sources="sources"
|
||||
onSelect="(option) => this.onSelect(option)"
|
||||
resetOnSelect="true"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
static props = {};
|
||||
setup() {
|
||||
this.state = useState({
|
||||
value: "Hello",
|
||||
});
|
||||
}
|
||||
get sources() {
|
||||
return [
|
||||
{
|
||||
options: [{ label: "World" }, { label: "Hello" }],
|
||||
},
|
||||
];
|
||||
}
|
||||
onSelect(option) {
|
||||
this.state.value = option.label;
|
||||
expect.step(option.label);
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".test_value").toHaveText("Hello");
|
||||
expect(".o-autocomplete input").toHaveValue("");
|
||||
|
||||
await contains(".o-autocomplete input").edit("Blip", { confirm: false });
|
||||
await runAllTimers();
|
||||
await contains(".o-autocomplete--dropdown-item:last").click();
|
||||
expect(".test_value").toHaveText("Hello");
|
||||
expect(".o-autocomplete input").toHaveValue("");
|
||||
expect.verifySteps(["Hello"]);
|
||||
});
|
||||
|
||||
test("open dropdown on input", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<AutoComplete
|
||||
value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
onSelect="() => {}"
|
||||
/>
|
||||
`;
|
||||
static props = {};
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
await contains(".o-autocomplete input").fill("a", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("cancel result on escape keydown", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<AutoComplete
|
||||
value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
onSelect="() => {}"
|
||||
autoSelect="true"
|
||||
/>
|
||||
`;
|
||||
static props = {};
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
expect(".o-autocomplete input").toHaveValue("Hello");
|
||||
|
||||
await contains(".o-autocomplete input").click();
|
||||
await contains(".o-autocomplete input").edit("H", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
|
||||
|
||||
await contains(".o-autocomplete input").press("Escape");
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
expect(".o-autocomplete input").toHaveValue("Hello");
|
||||
});
|
||||
|
||||
test("select input text on first focus", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<AutoComplete value="'Bar'" sources="[{ options: [{ label: 'Bar' }] }]" onSelect="() => {}"/>
|
||||
`;
|
||||
static props = {};
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
await contains(".o-autocomplete input").click();
|
||||
await runAllTimers();
|
||||
expect(getSelection().toString()).toBe("Bar");
|
||||
});
|
||||
|
||||
test("scroll outside should cancel result", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<div class="autocomplete_container overflow-auto" style="max-height: 100px;">
|
||||
<div style="height: 1000px;">
|
||||
<AutoComplete
|
||||
value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
onSelect="() => {}"
|
||||
autoSelect="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
static props = {};
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
expect(".o-autocomplete input").toHaveValue("Hello");
|
||||
|
||||
await contains(".o-autocomplete input").click();
|
||||
await contains(".o-autocomplete input").edit("H", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
|
||||
|
||||
await contains(".autocomplete_container").scroll({ top: 10 });
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
expect(".o-autocomplete input").toHaveValue("Hello");
|
||||
});
|
||||
|
||||
test("scroll inside should keep dropdown open", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<div class="autocomplete_container overflow-auto" style="max-height: 100px;">
|
||||
<div style="height: 1000px;">
|
||||
<AutoComplete
|
||||
value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
onSelect="() => {}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
static props = {};
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
|
||||
await contains(".o-autocomplete input").click();
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
|
||||
|
||||
await contains(".o-autocomplete .dropdown-menu").scroll({ top: 10 });
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("losing focus should cancel result", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<AutoComplete
|
||||
value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
onSelect="() => {}"
|
||||
autoSelect="true"
|
||||
/>
|
||||
`;
|
||||
static props = {};
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
expect(".o-autocomplete input").toHaveValue("Hello");
|
||||
|
||||
await contains(".o-autocomplete input").click();
|
||||
await contains(".o-autocomplete input").edit("H", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
|
||||
|
||||
await contains(document.body).click();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
expect(".o-autocomplete input").toHaveValue("Hello");
|
||||
});
|
||||
|
||||
test("click out after clearing input", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<AutoComplete
|
||||
value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
onSelect="() => {}"
|
||||
/>
|
||||
`;
|
||||
static props = {};
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
expect(".o-autocomplete input").toHaveValue("Hello");
|
||||
|
||||
await contains(".o-autocomplete input").click();
|
||||
await contains(".o-autocomplete input").clear({ confirm: false });
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
|
||||
|
||||
await contains(document.body).click();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
expect(".o-autocomplete input").toHaveValue("");
|
||||
});
|
||||
|
||||
test("open twice should not display previous results", async () => {
|
||||
let def = new Deferred();
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<AutoComplete value="''" sources="sources" onSelect="() => {}"/>
|
||||
`;
|
||||
static props = {};
|
||||
get sources() {
|
||||
return [
|
||||
{
|
||||
async options(search) {
|
||||
await def;
|
||||
if (search === "A") {
|
||||
return [{ label: "AB" }, { label: "AC" }];
|
||||
}
|
||||
return [{ label: "AB" }, { label: "AC" }, { label: "BC" }];
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
|
||||
await contains(".o-autocomplete input").click();
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
|
||||
expect(".o-autocomplete--dropdown-item").toHaveCount(1);
|
||||
expect(".o-autocomplete--dropdown-item .fa-spin").toHaveCount(1); // loading
|
||||
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
expect(".o-autocomplete--dropdown-item").toHaveCount(3);
|
||||
expect(".fa-spin").toHaveCount(0);
|
||||
|
||||
def = new Deferred();
|
||||
await contains(".o-autocomplete input").fill("A", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete--dropdown-item").toHaveCount(1);
|
||||
expect(".o-autocomplete--dropdown-item .fa-spin").toHaveCount(1); // loading
|
||||
def.resolve();
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete--dropdown-item").toHaveCount(2);
|
||||
expect(".fa-spin").toHaveCount(0);
|
||||
|
||||
await contains(queryFirst(".o-autocomplete--dropdown-item")).click();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
|
||||
// re-open the dropdown -> should not display the previous results
|
||||
def = new Deferred();
|
||||
await contains(".o-autocomplete input").click();
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
|
||||
expect(".o-autocomplete--dropdown-item").toHaveCount(1);
|
||||
expect(".o-autocomplete--dropdown-item .fa-spin").toHaveCount(1); // loading
|
||||
});
|
||||
|
||||
test("press enter on autocomplete with empty source", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`<AutoComplete value="''" sources="sources" onSelect="onSelect"/>`;
|
||||
static props = {};
|
||||
get sources() {
|
||||
return [{ options: [] }];
|
||||
}
|
||||
onSelect() {}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o-autocomplete input").toHaveCount(1);
|
||||
expect(".o-autocomplete input").toHaveValue("");
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
|
||||
// click inside the input and press "enter", because why not
|
||||
await contains(".o-autocomplete input").click();
|
||||
await runAllTimers();
|
||||
await contains(".o-autocomplete input").press("Enter");
|
||||
|
||||
expect(".o-autocomplete input").toHaveCount(1);
|
||||
expect(".o-autocomplete input").toHaveValue("");
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("press enter on autocomplete with empty source (2)", async () => {
|
||||
// in this test, the source isn't empty at some point, but becomes empty as the user
|
||||
// updates the input's value.
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`<AutoComplete value="''" sources="sources" onSelect="onSelect"/>`;
|
||||
static props = {};
|
||||
get sources() {
|
||||
const options = (val) => {
|
||||
if (val.length > 2) {
|
||||
return [{ label: "test A" }, { label: "test B" }, { label: "test C" }];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
return [{ options }];
|
||||
}
|
||||
onSelect() {}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o-autocomplete input").toHaveCount(1);
|
||||
expect(".o-autocomplete input").toHaveValue("");
|
||||
|
||||
await contains(".o-autocomplete input").edit("test", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
|
||||
expect(".o-autocomplete .dropdown-menu .o-autocomplete--dropdown-item").toHaveCount(3);
|
||||
|
||||
await contains(".o-autocomplete input").edit("t", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
|
||||
await contains(".o-autocomplete input").press("Enter");
|
||||
expect(".o-autocomplete input").toHaveCount(1);
|
||||
expect(".o-autocomplete input").toHaveValue("t");
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("autofocus=true option work as expected", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<AutoComplete value="'Hello'"
|
||||
sources="[{ options: [{ label: 'World' }, { label: 'Hello' }] }]"
|
||||
autofocus="true"
|
||||
onSelect="() => {}"
|
||||
/>
|
||||
`;
|
||||
static props = {};
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
expect(".o-autocomplete input").toBeFocused();
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("autocomplete in edition keep edited value before select option", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<button class="myButton" t-on-mouseover="onHover">My button</button>
|
||||
<AutoComplete value="this.state.value"
|
||||
sources="[{ options: [{ label: 'My Selection' }] }]"
|
||||
onSelect.bind="onSelect"
|
||||
/>
|
||||
`;
|
||||
static props = {};
|
||||
setup() {
|
||||
this.state = useState({ value: "Hello" });
|
||||
}
|
||||
|
||||
onHover() {
|
||||
this.state.value = "My Click";
|
||||
}
|
||||
|
||||
onSelect() {
|
||||
this.state.value = "My Selection";
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
await contains(".o-autocomplete input").edit("Yolo", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete input").toHaveValue("Yolo");
|
||||
|
||||
// We want to simulate an external value edition (like a delayed onChange)
|
||||
await contains(".myButton").hover();
|
||||
expect(".o-autocomplete input").toHaveValue("Yolo");
|
||||
|
||||
// Leave inEdition mode when selecting an option
|
||||
await contains(".o-autocomplete input").click();
|
||||
await runAllTimers();
|
||||
await contains(queryFirst(".o-autocomplete--dropdown-item")).click();
|
||||
expect(".o-autocomplete input").toHaveValue("My Selection");
|
||||
|
||||
// Will also trigger the hover event
|
||||
await contains(".myButton").click();
|
||||
expect(".o-autocomplete input").toHaveValue("My Click");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("autocomplete in edition keep edited value before blur", async () => {
|
||||
let count = 0;
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<button class="myButton" t-on-mouseover="onHover">My button</button>
|
||||
<AutoComplete value="this.state.value"
|
||||
sources="[]"
|
||||
onSelect="() => {}"
|
||||
/>
|
||||
`;
|
||||
static props = {};
|
||||
setup() {
|
||||
this.state = useState({ value: "Hello" });
|
||||
}
|
||||
|
||||
onHover() {
|
||||
this.state.value = `My Click ${count++}`;
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
await contains(".o-autocomplete input").edit("", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete input").toHaveValue("");
|
||||
|
||||
// We want to simulate an external value edition (like a delayed onChange)
|
||||
await contains(".myButton").hover();
|
||||
expect(".o-autocomplete input").toHaveValue("");
|
||||
|
||||
// Leave inEdition mode when blur the input
|
||||
await contains(document.body).click();
|
||||
expect(".o-autocomplete input").toHaveValue("");
|
||||
|
||||
// Will also trigger the hover event
|
||||
await contains(".myButton").click();
|
||||
expect(".o-autocomplete input").toHaveValue("My Click 1");
|
||||
});
|
||||
|
||||
test("correct sequence of blur, focus and select", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { AutoComplete };
|
||||
static template = xml`
|
||||
<AutoComplete
|
||||
value="state.value"
|
||||
sources="sources"
|
||||
onSelect.bind="onSelect"
|
||||
onBlur.bind="onBlur"
|
||||
onChange.bind="onChange"
|
||||
autoSelect="true"
|
||||
/>
|
||||
`;
|
||||
static props = {};
|
||||
setup() {
|
||||
this.state = useState({
|
||||
value: "",
|
||||
});
|
||||
}
|
||||
get sources() {
|
||||
return [
|
||||
{
|
||||
options: [{ label: "World" }, { label: "Hello" }],
|
||||
},
|
||||
];
|
||||
}
|
||||
onChange() {
|
||||
expect.step("change");
|
||||
}
|
||||
onSelect(option, params) {
|
||||
queryOne(".o-autocomplete input").value = option.label;
|
||||
expect.step("select " + option.label);
|
||||
expect(params.triggeredOnBlur).not.toBe(true);
|
||||
}
|
||||
onBlur() {
|
||||
expect.step("blur");
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o-autocomplete input").toHaveCount(1);
|
||||
await contains(".o-autocomplete input").click();
|
||||
|
||||
// Navigate suggestions using arrow keys
|
||||
let dropdownItemIds = queryAllAttributes(".dropdown-item", "id");
|
||||
expect(dropdownItemIds).toEqual(["autocomplete_0_0", "autocomplete_0_1"]);
|
||||
expect(queryAllAttributes(".dropdown-item", "role")).toEqual(["option", "option"]);
|
||||
expect(queryAllAttributes(".dropdown-item", "aria-selected")).toEqual(["true", "false"]);
|
||||
expect(".o-autocomplete--input").toHaveAttribute("aria-activedescendant", dropdownItemIds[0]);
|
||||
|
||||
await contains(".o-autocomplete--input").press("ArrowDown");
|
||||
|
||||
dropdownItemIds = queryAllAttributes(".dropdown-item", "id");
|
||||
expect(dropdownItemIds).toEqual(["autocomplete_0_0", "autocomplete_0_1"]);
|
||||
expect(queryAllAttributes(".dropdown-item", "role")).toEqual(["option", "option"]);
|
||||
expect(queryAllAttributes(".dropdown-item", "aria-selected")).toEqual(["false", "true"]);
|
||||
expect(".o-autocomplete--input").toHaveAttribute("aria-activedescendant", dropdownItemIds[1]);
|
||||
|
||||
// Start typing hello and click on the result
|
||||
await contains(".o-autocomplete input").edit("h", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(1);
|
||||
await contains(".o-autocomplete--dropdown-item:last").click();
|
||||
expect.verifySteps(["change", "select Hello"]);
|
||||
expect(".o-autocomplete input").toBeFocused();
|
||||
|
||||
// Clear input and focus out
|
||||
await contains(".o-autocomplete input").edit("", { confirm: false });
|
||||
await runAllTimers();
|
||||
await contains(document.body).click();
|
||||
expect.verifySteps(["blur", "change"]);
|
||||
expect(".o-autocomplete .dropdown-menu").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("autocomplete always closes on click away", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<AutoComplete
|
||||
value="state.value"
|
||||
sources="sources"
|
||||
onSelect.bind="onSelect"
|
||||
autoSelect="true"
|
||||
/>
|
||||
`;
|
||||
static props = ["*"];
|
||||
static components = { AutoComplete };
|
||||
setup() {
|
||||
this.state = useState({
|
||||
value: "",
|
||||
});
|
||||
}
|
||||
get sources() {
|
||||
return [
|
||||
{
|
||||
options: [{ label: "World" }, { label: "Hello" }],
|
||||
},
|
||||
];
|
||||
}
|
||||
onSelect(option) {
|
||||
queryOne(".o-autocomplete--input").value = option.label;
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o-autocomplete input").toHaveCount(1);
|
||||
await contains(".o-autocomplete input").click();
|
||||
expect(".o-autocomplete--dropdown-item").toHaveCount(2);
|
||||
await pointerDown(".o-autocomplete--dropdown-item:last");
|
||||
await pointerUp(document.body);
|
||||
expect(".o-autocomplete--dropdown-item").toHaveCount(2);
|
||||
await contains(document.body).click();
|
||||
expect(".o-autocomplete--dropdown-item").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("autocomplete trim spaces for search", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<AutoComplete value="state.value" sources="sources" onSelect="() => {}"/>
|
||||
`;
|
||||
static props = ["*"];
|
||||
static components = { AutoComplete };
|
||||
setup() {
|
||||
this.state = useState({ value: " World" });
|
||||
}
|
||||
get sources() {
|
||||
return [
|
||||
{
|
||||
options(search) {
|
||||
return [{ label: "World" }, { label: "Hello" }].filter(({ label }) =>
|
||||
label.startsWith(search)
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
await contains(`.o-autocomplete input`).click();
|
||||
expect(queryAllTexts(`.o-autocomplete--dropdown-item`)).toEqual(["World", "Hello"]);
|
||||
});
|
||||
|
||||
test("tab and shift+tab close the dropdown", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<AutoComplete value="state.value" sources="sources" onSelect="() => {}"/>
|
||||
`;
|
||||
static props = ["*"];
|
||||
static components = { AutoComplete };
|
||||
setup() {
|
||||
this.state = useState({ value: "" });
|
||||
}
|
||||
get sources() {
|
||||
return [
|
||||
{
|
||||
options: [{ label: "World" }, { label: "Hello" }],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
const input = ".o-autocomplete input";
|
||||
const dropdown = ".o-autocomplete--dropdown-menu";
|
||||
expect(input).toHaveCount(1);
|
||||
// Tab
|
||||
await contains(input).click();
|
||||
expect(dropdown).toBeVisible();
|
||||
await press("Tab");
|
||||
await animationFrame();
|
||||
expect(dropdown).not.toHaveCount();
|
||||
// Shift + Tab
|
||||
await contains(input).click();
|
||||
expect(dropdown).toBeVisible();
|
||||
await press("Tab", { shiftKey: true });
|
||||
await animationFrame();
|
||||
expect(dropdown).not.toHaveCount();
|
||||
});
|
||||
|
||||
test("autocomplete scrolls when moving with arrows", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<style>
|
||||
.o-autocomplete--dropdown-menu {
|
||||
max-height: 100px;
|
||||
}
|
||||
</style>
|
||||
<AutoComplete
|
||||
value="state.value"
|
||||
sources="sources"
|
||||
onSelect="() => {}"
|
||||
autoSelect="true"
|
||||
/>
|
||||
`;
|
||||
static props = ["*"];
|
||||
static components = { AutoComplete };
|
||||
setup() {
|
||||
this.state = useState({
|
||||
value: "",
|
||||
});
|
||||
}
|
||||
get sources() {
|
||||
return [
|
||||
{
|
||||
options: [
|
||||
{ label: "Never" },
|
||||
{ label: "Gonna" },
|
||||
{ label: "Give" },
|
||||
{ label: "You" },
|
||||
{ label: "Up" },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
const dropdownSelector = ".o-autocomplete--dropdown-menu";
|
||||
const activeItemSelector = ".o-autocomplete--dropdown-item .ui-state-active";
|
||||
const msgInView = "active item should be in view within dropdown";
|
||||
const msgNotInView = "item should not be in view within dropdown";
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o-autocomplete input").toHaveCount(1);
|
||||
// Open with arrow key.
|
||||
await contains(".o-autocomplete input").focus();
|
||||
await press("ArrowDown");
|
||||
await animationFrame();
|
||||
expect(".o-autocomplete--dropdown-item").toHaveCount(5);
|
||||
expect(isScrollable(dropdownSelector)).toBe(true, { message: "dropdown should be scrollable" });
|
||||
// First element focused and visible (dropdown is not scrolled yet).
|
||||
expect(".o-autocomplete--dropdown-item:first-child a").toHaveClass("ui-state-active");
|
||||
expect(isInViewWithinScrollableY(activeItemSelector)).toBe(true, { message: msgInView });
|
||||
// Navigate with the arrow keys. Go to the last item.
|
||||
expect(isInViewWithinScrollableY(".o-autocomplete--dropdown-item:contains('Up')")).toBe(false, {
|
||||
message: "'Up' " + msgNotInView,
|
||||
});
|
||||
await press("ArrowUp");
|
||||
await press("ArrowUp");
|
||||
await animationFrame();
|
||||
expect(activeItemSelector).toHaveText("Up");
|
||||
expect(isInViewWithinScrollableY(activeItemSelector)).toBe(true, { message: msgInView });
|
||||
// Navigate to an item that is not currently visible.
|
||||
expect(isInViewWithinScrollableY(".o-autocomplete--dropdown-item:contains('Never')")).toBe(
|
||||
false,
|
||||
{ message: "'Never' " + msgNotInView }
|
||||
);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await press("ArrowUp");
|
||||
}
|
||||
await animationFrame();
|
||||
expect(activeItemSelector).toHaveText("Never");
|
||||
expect(isInViewWithinScrollableY(activeItemSelector)).toBe(true, { message: msgInView });
|
||||
expect(isInViewWithinScrollableY(".o-autocomplete--dropdown-item:last")).toBe(false, {
|
||||
message: "last " + msgNotInView,
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { getService, makeMockEnv } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
let titleService;
|
||||
|
||||
beforeEach(async () => {
|
||||
await makeMockEnv();
|
||||
titleService = getService("title");
|
||||
});
|
||||
|
||||
test("simple title", () => {
|
||||
titleService.setParts({ one: "MyOdoo" });
|
||||
expect(titleService.current).toBe("MyOdoo");
|
||||
});
|
||||
|
||||
test("add title part", () => {
|
||||
titleService.setParts({ one: "MyOdoo", two: null });
|
||||
expect(titleService.current).toBe("MyOdoo");
|
||||
titleService.setParts({ three: "Import" });
|
||||
expect(titleService.current).toBe("MyOdoo - Import");
|
||||
});
|
||||
|
||||
test("modify title part", () => {
|
||||
titleService.setParts({ one: "MyOdoo" });
|
||||
expect(titleService.current).toBe("MyOdoo");
|
||||
titleService.setParts({ one: "Zopenerp" });
|
||||
expect(titleService.current).toBe("Zopenerp");
|
||||
});
|
||||
|
||||
test("delete title part", () => {
|
||||
titleService.setParts({ one: "MyOdoo" });
|
||||
expect(titleService.current).toBe("MyOdoo");
|
||||
titleService.setParts({ one: null });
|
||||
expect(titleService.current).toBe("Odoo");
|
||||
});
|
||||
|
||||
test("all at once", () => {
|
||||
titleService.setParts({ one: "MyOdoo", two: "Import" });
|
||||
expect(titleService.current).toBe("MyOdoo - Import");
|
||||
titleService.setParts({ one: "Zopenerp", two: null, three: "Sauron" });
|
||||
expect(titleService.current).toBe("Zopenerp - Sauron");
|
||||
});
|
||||
|
||||
test("get title parts", () => {
|
||||
expect(titleService.current).toBe("");
|
||||
titleService.setParts({ one: "MyOdoo", two: "Import" });
|
||||
expect(titleService.current).toBe("MyOdoo - Import");
|
||||
const parts = titleService.getParts();
|
||||
expect(parts).toEqual({ one: "MyOdoo", two: "Import" });
|
||||
parts.action = "Export";
|
||||
expect(titleService.current).toBe("MyOdoo - Import"); // parts is a copy!
|
||||
});
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { Deferred } from "@odoo/hoot-mock";
|
||||
|
||||
import { Cache } from "@web/core/utils/cache";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("do not call getValue if already cached", () => {
|
||||
const cache = new Cache((key) => {
|
||||
expect.step(key);
|
||||
return key.toUpperCase();
|
||||
});
|
||||
|
||||
expect(cache.read("a")).toBe("A");
|
||||
expect(cache.read("b")).toBe("B");
|
||||
expect(cache.read("a")).toBe("A");
|
||||
|
||||
expect.verifySteps(["a", "b"]);
|
||||
});
|
||||
|
||||
test("multiple cache key", async () => {
|
||||
const cache = new Cache((...keys) => expect.step(keys.join("-")));
|
||||
|
||||
cache.read("a", 1);
|
||||
cache.read("a", 2);
|
||||
cache.read("a", 1);
|
||||
|
||||
expect.verifySteps(["a-1", "a-2"]);
|
||||
});
|
||||
|
||||
test("compute key", async () => {
|
||||
const cache = new Cache(
|
||||
(key) => expect.step(key),
|
||||
(key) => key.toLowerCase()
|
||||
);
|
||||
|
||||
cache.read("a");
|
||||
cache.read("A");
|
||||
|
||||
expect.verifySteps(["a"]);
|
||||
});
|
||||
|
||||
test("cache promise", async () => {
|
||||
const cache = new Cache((key) => {
|
||||
expect.step(`read ${key}`);
|
||||
return new Deferred();
|
||||
});
|
||||
|
||||
cache.read("a").then((k) => expect.step(`then ${k}`));
|
||||
cache.read("b").then((k) => expect.step(`then ${k}`));
|
||||
cache.read("a").then((k) => expect.step(`then ${k}`));
|
||||
cache.read("a").resolve("a");
|
||||
cache.read("b").resolve("b");
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
expect.verifySteps(["read a", "read b", "then a", "then a", "then b"]);
|
||||
});
|
||||
|
||||
test("clear cache", async () => {
|
||||
const cache = new Cache((key) => expect.step(key));
|
||||
|
||||
cache.read("a");
|
||||
cache.read("b");
|
||||
expect.verifySteps(["a", "b"]);
|
||||
|
||||
cache.read("a");
|
||||
cache.read("b");
|
||||
expect.verifySteps([]);
|
||||
|
||||
cache.clear("a");
|
||||
cache.read("a");
|
||||
cache.read("b");
|
||||
expect.verifySteps(["a"]);
|
||||
|
||||
cache.clear();
|
||||
cache.read("a");
|
||||
cache.read("b");
|
||||
expect.verifySteps([]);
|
||||
|
||||
cache.invalidate();
|
||||
cache.read("a");
|
||||
cache.read("b");
|
||||
expect.verifySteps(["a", "b"]);
|
||||
});
|
||||
135
odoo-bringout-oca-ocb-web/web/static/tests/core/checkbox.test.js
Normal file
135
odoo-bringout-oca-ocb-web/web/static/tests/core/checkbox.test.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { check, uncheck } from "@odoo/hoot-dom";
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { contains, defineParams, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { CheckBox } from "@web/core/checkbox/checkbox";
|
||||
|
||||
test("can be rendered", async () => {
|
||||
await mountWithCleanup(CheckBox);
|
||||
|
||||
expect(`.o-checkbox input[type=checkbox]`).toHaveCount(1);
|
||||
expect(`.o-checkbox input[type=checkbox]`).toBeEnabled();
|
||||
});
|
||||
|
||||
test("has a slot for translatable text", async () => {
|
||||
defineParams({ translations: { ragabadabadaba: "rugubudubudubu" } });
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { CheckBox };
|
||||
static props = {};
|
||||
static template = xml`<CheckBox>ragabadabadaba</CheckBox>`;
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
expect(`.form-check`).toHaveCount(1);
|
||||
expect(`.form-check`).toHaveText("rugubudubudubu", { exact: true });
|
||||
});
|
||||
|
||||
test("call onChange prop when some change occurs", async () => {
|
||||
let value = false;
|
||||
class Parent extends Component {
|
||||
static components = { CheckBox };
|
||||
static props = {};
|
||||
static template = xml`<CheckBox onChange="onChange" />`;
|
||||
onChange(checked) {
|
||||
value = checked;
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
expect(`.o-checkbox input`).toHaveCount(1);
|
||||
|
||||
await check("input");
|
||||
|
||||
expect(value).toBe(true);
|
||||
|
||||
await uncheck("input");
|
||||
|
||||
expect(value).toBe(false);
|
||||
});
|
||||
|
||||
test("checkbox with props disabled", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { CheckBox };
|
||||
static props = {};
|
||||
static template = xml`<CheckBox disabled="true" />`;
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
expect(`.o-checkbox input`).toHaveCount(1);
|
||||
expect(`.o-checkbox input`).not.toBeEnabled();
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("can toggle value by pressing ENTER", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { CheckBox };
|
||||
static props = {};
|
||||
static template = xml`<CheckBox onChange.bind="onChange" value="state.value" />`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ value: false });
|
||||
}
|
||||
|
||||
onChange(checked) {
|
||||
this.state.value = checked;
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
expect(`.o-checkbox input`).toHaveCount(1);
|
||||
expect(`.o-checkbox input`).not.toBeChecked();
|
||||
|
||||
await contains(".o-checkbox input").press("Enter");
|
||||
|
||||
expect(`.o-checkbox input`).toBeChecked();
|
||||
|
||||
await contains(".o-checkbox input").press("Enter");
|
||||
|
||||
expect(`.o-checkbox input`).not.toBeChecked();
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("toggling through multiple ways", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { CheckBox };
|
||||
static props = {};
|
||||
static template = xml`<CheckBox onChange.bind="onChange" value="state.value" />`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({ value: false });
|
||||
}
|
||||
|
||||
onChange(checked) {
|
||||
this.state.value = checked;
|
||||
expect.step(String(checked));
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
expect(`.o-checkbox input`).toHaveCount(1);
|
||||
expect(`.o-checkbox input`).not.toBeChecked();
|
||||
|
||||
await contains(".o-checkbox").click();
|
||||
|
||||
expect(`.o-checkbox input`).toBeChecked();
|
||||
|
||||
await contains(".o-checkbox > .form-check-label", { visible: false }).uncheck();
|
||||
|
||||
expect(`.o-checkbox input`).not.toBeChecked();
|
||||
|
||||
await contains(".o-checkbox input").press("Enter");
|
||||
|
||||
expect(`.o-checkbox input`).toBeChecked();
|
||||
|
||||
await contains(".o-checkbox input").press(" ");
|
||||
|
||||
expect(`.o-checkbox input`).not.toBeChecked();
|
||||
expect.verifySteps(["true", "false", "true", "false"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAll, queryAllTexts, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, markup, useState, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
editAce,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
preloadBundle,
|
||||
preventResizeObserverError,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { CodeEditor } from "@web/core/code_editor/code_editor";
|
||||
import { debounce } from "@web/core/utils/timing";
|
||||
|
||||
preloadBundle("web.ace_lib");
|
||||
preventResizeObserverError();
|
||||
|
||||
function getDomValue() {
|
||||
return queryAll(".ace_line")
|
||||
.map((root) => queryAllTexts(`:scope > span`, { root }).join(""))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
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: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
A custom implementation to dispatch keyboard events for ace specifically
|
||||
It is very naive and simple, and could extended
|
||||
|
||||
FIXME: Specificities of Ace 1.32.3
|
||||
-- Ace heavily relies on KeyboardEvent.keyCode, so hoot's helpers
|
||||
cannot be used for this simple test.
|
||||
-- Ace still relies on the keypress event
|
||||
-- The textarea has no size in ace, it is a "hidden" input and a part of Ace's internals
|
||||
hoot's helpers won't focus it naturally
|
||||
-- The same Ace considers that if "Win" is not part of the useragent's string, we are in a MAC environment
|
||||
So, instead of patching the useragent, we send to ace what it wants. (ie: Command + metaKey: true)
|
||||
*/
|
||||
function dispatchKeyboardEvents(el, tupleArray) {
|
||||
for (const [evType, eventInit] of tupleArray) {
|
||||
el.dispatchEvent(new KeyboardEvent(evType, { ...eventInit, bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
test("Can be rendered", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`<CodeEditor maxLines="10" mode="'xml'" />`;
|
||||
static props = ["*"];
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".ace_editor").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("CodeEditor shouldn't accepts markup values", async () => {
|
||||
expect.errors(1);
|
||||
|
||||
patchWithCleanup(console, {
|
||||
warn: (msg) => expect.step(msg),
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`<CodeEditor value="props.value" />`;
|
||||
static props = ["*"];
|
||||
}
|
||||
class GrandParent extends Component {
|
||||
static components = { Parent };
|
||||
static template = xml`<Parent value="state.value"/>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({ value: `<div>Some Text</div>` });
|
||||
}
|
||||
}
|
||||
|
||||
const codeEditor = await mountWithCleanup(GrandParent);
|
||||
const textMarkup = markup("<div>Some Text</div>");
|
||||
|
||||
codeEditor.state.value = textMarkup;
|
||||
await animationFrame();
|
||||
|
||||
expect.verifyErrors(["Invalid props for component 'CodeEditor': 'value' is not valid"]);
|
||||
expect.verifySteps(["[Owl] Unhandled error. Destroying the root component"]);
|
||||
});
|
||||
|
||||
test("onChange props called when code is edited", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`<CodeEditor maxLines="10" onChange.bind="onChange" />`;
|
||||
static props = ["*"];
|
||||
onChange(value) {
|
||||
expect.step(value);
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
await editAce("Some Text");
|
||||
expect.verifySteps(["Some Text"]);
|
||||
});
|
||||
|
||||
test("onChange props not called when value props is updated", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`
|
||||
<CodeEditor
|
||||
value="state.value"
|
||||
maxLines="10"
|
||||
onChange.bind="onChange"
|
||||
/>
|
||||
`;
|
||||
static props = ["*"];
|
||||
state = useState({ value: "initial value" });
|
||||
onChange(value) {
|
||||
expect.step(value || "__emptystring__");
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mountWithCleanup(Parent);
|
||||
expect(".ace_line").toHaveText("initial value");
|
||||
|
||||
parent.state.value = "new value";
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(".ace_line").toHaveText("new value");
|
||||
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("Default value correctly set and updates", async () => {
|
||||
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"
|
||||
/>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({ value: textA });
|
||||
this.onChange = debounce(this.onChange.bind(this));
|
||||
}
|
||||
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;
|
||||
}
|
||||
expect.step(value);
|
||||
}
|
||||
changeValue(newValue) {
|
||||
this.state.value = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
const codeEditor = await mountWithCleanup(Parent);
|
||||
expect(getDomValue()).toBe(textA);
|
||||
|
||||
// Disable XML autocompletion for xml end tag.
|
||||
// Necessary because the contains().edit() helpers triggers as if it was
|
||||
// a real user interaction.
|
||||
const ace_editor = window.ace.edit(queryOne(".ace_editor"));
|
||||
ace_editor.setBehavioursEnabled(false);
|
||||
|
||||
const aceEditor = window.ace.edit(queryOne(".ace_editor"));
|
||||
aceEditor.selectAll();
|
||||
await editAce(textB);
|
||||
expect(getDomValue()).toBe(textB);
|
||||
|
||||
codeEditor.changeValue(textC);
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(getDomValue()).toBe(textC);
|
||||
expect.verifySteps([textB]);
|
||||
});
|
||||
|
||||
test("Mode props update imports the mode", async () => {
|
||||
const fakeAceEditor = getFakeAceEditor();
|
||||
fakeAceEditor.session.setMode = (mode) => {
|
||||
expect.step(mode);
|
||||
};
|
||||
|
||||
patchWithCleanup(window.ace, {
|
||||
edit: () => fakeAceEditor,
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`<CodeEditor maxLines="10" mode="state.mode" />`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({ mode: "xml" });
|
||||
}
|
||||
setMode(newMode) {
|
||||
this.state.mode = newMode;
|
||||
}
|
||||
}
|
||||
|
||||
const codeEditor = await mountWithCleanup(Parent);
|
||||
expect.verifySteps(["ace/mode/xml"]);
|
||||
|
||||
await codeEditor.setMode("javascript");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["ace/mode/javascript"]);
|
||||
});
|
||||
|
||||
test("Theme props updates imports the theme", async () => {
|
||||
const fakeAceEditor = getFakeAceEditor();
|
||||
fakeAceEditor.setTheme = (theme) => {
|
||||
expect.step(theme ? theme : "default");
|
||||
};
|
||||
|
||||
patchWithCleanup(window.ace, {
|
||||
edit: () => fakeAceEditor,
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`<CodeEditor maxLines="10" theme="state.theme" />`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({ theme: "" });
|
||||
}
|
||||
setTheme(newTheme) {
|
||||
this.state.theme = newTheme;
|
||||
}
|
||||
}
|
||||
|
||||
const codeEditor = await mountWithCleanup(Parent);
|
||||
expect.verifySteps(["default"]);
|
||||
|
||||
await codeEditor.setTheme("monokai");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["ace/theme/monokai"]);
|
||||
});
|
||||
|
||||
test("initial value cannot be undone", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`<CodeEditor mode="'xml'" value="'some value'" class="'h-100'" />`;
|
||||
static props = ["*"];
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
await animationFrame();
|
||||
expect(".ace_editor").toHaveCount(1);
|
||||
expect(".ace_editor .ace_content").toHaveText("some value");
|
||||
|
||||
const editor = window.ace.edit(queryOne(".ace_editor"));
|
||||
const undo = editor.session.$undoManager.undo.bind(editor.session.$undoManager);
|
||||
editor.session.$undoManager.undo = (...args) => {
|
||||
expect.step("ace undo");
|
||||
return undo(...args);
|
||||
};
|
||||
|
||||
const aceContent = queryOne(".ace_editor textarea");
|
||||
dispatchKeyboardEvents(aceContent, [
|
||||
["keydown", { key: "Control", keyCode: 17 }],
|
||||
["keypress", { key: "Control", keyCode: 17 }],
|
||||
["keydown", { key: "z", keyCode: 90, ctrlKey: true }],
|
||||
["keypress", { key: "z", keyCode: 90, ctrlKey: true }],
|
||||
["keyup", { key: "z", keyCode: 90, ctrlKey: true }],
|
||||
["keyup", { key: "Control", keyCode: 17 }],
|
||||
]);
|
||||
await animationFrame();
|
||||
expect(".ace_editor .ace_content").toHaveText("some value");
|
||||
expect.verifySteps(["ace undo"]);
|
||||
});
|
||||
|
||||
test("code editor can take an initial cursor position", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { CodeEditor };
|
||||
static template = xml`<CodeEditor maxLines="2" value="value" initialCursorPosition="initialPosition" onChange="onChange"/>`;
|
||||
static props = ["*"];
|
||||
|
||||
setup() {
|
||||
this.value = `
|
||||
1
|
||||
2
|
||||
3
|
||||
4aa
|
||||
5
|
||||
`.replace(/^\s*/gm, ""); // simple dedent
|
||||
|
||||
this.initialPosition = { row: 3, column: 2 };
|
||||
}
|
||||
|
||||
onChange(value, startPosition) {
|
||||
expect.step({ value, startPosition });
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
await animationFrame();
|
||||
|
||||
const editor = window.ace.edit(queryOne(".ace_editor"));
|
||||
expect(document.activeElement).toBe(editor.textInput.getElement());
|
||||
expect(editor.getCursorPosition()).toEqual({ row: 3, column: 2 });
|
||||
|
||||
expect(queryAllTexts(".ace_gutter-cell")).toEqual(["3", "4", "5"]);
|
||||
expect.verifySteps([]);
|
||||
await contains(".ace_editor textarea", { displayed: true, visible: false }).edit("new\nvalue", {
|
||||
instantly: true,
|
||||
});
|
||||
expect.verifySteps([
|
||||
{
|
||||
startPosition: {
|
||||
column: 0,
|
||||
row: 0,
|
||||
},
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
startPosition: {
|
||||
column: 0,
|
||||
row: 0,
|
||||
},
|
||||
value: "new\nvalue",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { contains, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { ColorList } from "@web/core/colorlist/colorlist";
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<t t-component="Component" t-props="componentProps"/>
|
||||
<div class="outsideDiv">Outside div</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
|
||||
get Component() {
|
||||
return this.props.Component || ColorList;
|
||||
}
|
||||
|
||||
get componentProps() {
|
||||
const props = { ...this.props };
|
||||
delete props.Component;
|
||||
if (!props.onColorSelected) {
|
||||
props.onColorSelected = () => {};
|
||||
}
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
test("basic rendering with forceExpanded props", async () => {
|
||||
await mountWithCleanup(Parent, {
|
||||
props: {
|
||||
colors: [0, 9],
|
||||
forceExpanded: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_colorlist").toHaveCount(1);
|
||||
expect(".o_colorlist button").toHaveCount(2);
|
||||
expect(".o_colorlist button:eq(1)").toHaveAttribute("title", "Raspberry");
|
||||
expect(".o_colorlist button:eq(1)").toHaveClass("o_colorlist_item_color_9");
|
||||
});
|
||||
|
||||
test("color click does not open the list if canToggle props is not given", async () => {
|
||||
const selectedColorId = 0;
|
||||
await mountWithCleanup(Parent, {
|
||||
props: {
|
||||
colors: [4, 5, 6],
|
||||
selectedColor: selectedColorId,
|
||||
onColorSelected: (colorId) => expect.step("color #" + colorId + " is selected"),
|
||||
},
|
||||
});
|
||||
expect(".o_colorlist").toHaveCount(1);
|
||||
expect("button.o_colorlist_toggler").toHaveCount(1);
|
||||
|
||||
await contains(".o_colorlist").click();
|
||||
expect("button.o_colorlist_toggler").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("open the list of colors if canToggle props is given", async function () {
|
||||
const selectedColorId = 0;
|
||||
await mountWithCleanup(Parent, {
|
||||
props: {
|
||||
canToggle: true,
|
||||
colors: [4, 5, 6],
|
||||
selectedColor: selectedColorId,
|
||||
onColorSelected: (colorId) => expect.step("color #" + colorId + " is selected"),
|
||||
},
|
||||
});
|
||||
expect(".o_colorlist").toHaveCount(1);
|
||||
expect(".o_colorlist button").toHaveClass("o_colorlist_item_color_" + selectedColorId);
|
||||
|
||||
await contains(".o_colorlist button").click();
|
||||
expect("button.o_colorlist_toggler").toHaveCount(0);
|
||||
expect(".o_colorlist button").toHaveCount(3);
|
||||
|
||||
await contains(".outsideDiv").click();
|
||||
expect(".o_colorlist button").toHaveCount(1);
|
||||
expect("button.o_colorlist_toggler").toHaveCount(1);
|
||||
|
||||
await contains(".o_colorlist_toggler").click();
|
||||
await contains(".o_colorlist button:eq(2)").click();
|
||||
expect.verifySteps(["color #6 is selected"]);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,141 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { press, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineActions,
|
||||
defineMenus,
|
||||
getService,
|
||||
mountWithCleanup,
|
||||
useTestClientAction,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
defineMenus([
|
||||
{ id: 0 }, // prevents auto-loading the first action
|
||||
{ id: 1, name: "Contact", actionID: 1001 },
|
||||
{
|
||||
id: 2,
|
||||
name: "Sales",
|
||||
actionID: 1002,
|
||||
appID: 2,
|
||||
children: [
|
||||
{
|
||||
id: 3,
|
||||
name: "Info",
|
||||
appID: 2,
|
||||
actionID: 1003,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Report",
|
||||
appID: 2,
|
||||
actionID: 1004,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const testAction = useTestClientAction();
|
||||
defineActions([
|
||||
{ ...testAction, id: 1001, params: { description: "Id 1" } },
|
||||
{ ...testAction, id: 1003, params: { description: "Info" } },
|
||||
{ ...testAction, id: 1004, params: { description: "Report" } },
|
||||
]);
|
||||
|
||||
test.tags("desktop");
|
||||
test("displays only apps if the search value is '/'", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(".o_menu_brand").toHaveCount(0);
|
||||
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
await contains(".o_command_palette_search input").edit("/", { confirm: false });
|
||||
await animationFrame();
|
||||
expect(".o_command_palette").toHaveCount(1);
|
||||
expect(".o_command_category").toHaveCount(1);
|
||||
expect(".o_command").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_command_name")).toEqual(["Contact", "Sales"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("displays apps and menu items if the search value is not only '/'", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
await contains(".o_command_palette_search input").edit("/sal", { confirm: false });
|
||||
await animationFrame();
|
||||
expect(".o_command_palette").toHaveCount(1);
|
||||
expect(".o_command").toHaveCount(3);
|
||||
expect(queryAllTexts(".o_command_name")).toEqual(["Sales", "Sales / Info", "Sales / Report"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("opens an app", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(".o_menu_brand").toHaveCount(0);
|
||||
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
await contains(".o_command_palette_search input").edit("/", { confirm: false });
|
||||
await animationFrame();
|
||||
expect(".o_command_palette").toHaveCount(1);
|
||||
|
||||
await press("enter");
|
||||
await animationFrame();
|
||||
// empty screen for now, wait for actual action to show up
|
||||
await animationFrame();
|
||||
expect(".o_menu_brand").toHaveText("Contact");
|
||||
expect(".test_client_action").toHaveText("ClientAction_Id 1");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("opens a menu items", async () => {
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(".o_menu_brand").toHaveCount(0);
|
||||
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
await contains(".o_command_palette_search input").edit("/sal", { confirm: false });
|
||||
await animationFrame();
|
||||
expect(".o_command_palette").toHaveCount(1);
|
||||
expect(".o_command_category").toHaveCount(2);
|
||||
|
||||
await contains("#o_command_2").click();
|
||||
await animationFrame();
|
||||
// empty screen for now, wait for actual action to show up
|
||||
await animationFrame();
|
||||
expect(".o_menu_brand").toHaveText("Sales");
|
||||
expect(".test_client_action").toHaveText("ClientAction_Report");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("open a menu item when a dialog is displayed", async () => {
|
||||
class CustomDialog extends Component {
|
||||
static template = xml`<Dialog contentClass="'test'">content</Dialog>`;
|
||||
static components = { Dialog };
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
expect(".o_menu_brand").toHaveCount(0);
|
||||
expect(".modal .test").toHaveCount(0);
|
||||
|
||||
getService("dialog").add(CustomDialog);
|
||||
await animationFrame();
|
||||
expect(".modal .test").toHaveCount(1);
|
||||
|
||||
await press(["control", "k"]);
|
||||
await animationFrame();
|
||||
await contains(".o_command_palette_search input").edit("/sal", { confirm: false });
|
||||
await animationFrame();
|
||||
expect(".o_command_palette").toHaveCount(1);
|
||||
expect(".modal .test").toHaveCount(1);
|
||||
|
||||
await contains("#o_command_2").click();
|
||||
await animationFrame();
|
||||
expect(".o_menu_brand").toHaveText("Sales");
|
||||
});
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { click, edit } from "@odoo/hoot-dom";
|
||||
import { animationFrame, tick } from "@odoo/hoot-mock";
|
||||
import { Component, reactive, useState, xml } from "@odoo/owl";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { useDateTimePicker } from "@web/core/datetime/datetime_hook";
|
||||
import { DateTimeInput } from "@web/core/datetime/datetime_input";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
/**
|
||||
* @param {() => any} setup
|
||||
*/
|
||||
const mountInput = async (setup) => {
|
||||
await mountWithCleanup(Root, { props: { setup } });
|
||||
};
|
||||
|
||||
class Root extends Component {
|
||||
static components = { DateTimeInput };
|
||||
static template = xml`<input type="text" class="datetime_hook_input" t-ref="start-date"/>`;
|
||||
static props = ["*"];
|
||||
|
||||
setup() {
|
||||
this.props.setup();
|
||||
}
|
||||
}
|
||||
|
||||
test("reactivity: update inert object", async () => {
|
||||
const pickerProps = {
|
||||
value: false,
|
||||
type: "date",
|
||||
};
|
||||
|
||||
await mountInput(() => {
|
||||
useDateTimePicker({ pickerProps });
|
||||
});
|
||||
|
||||
expect(".datetime_hook_input").toHaveValue("");
|
||||
|
||||
pickerProps.value = DateTime.fromSQL("2023-06-06");
|
||||
await tick();
|
||||
|
||||
expect(".datetime_hook_input").toHaveText("");
|
||||
});
|
||||
|
||||
test("reactivity: useState & update getter object", async () => {
|
||||
const pickerProps = reactive({
|
||||
value: false,
|
||||
type: "date",
|
||||
});
|
||||
|
||||
await mountInput(() => {
|
||||
const state = useState(pickerProps);
|
||||
state.value; // artificially subscribe to value
|
||||
|
||||
useDateTimePicker({
|
||||
get pickerProps() {
|
||||
return pickerProps;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(".datetime_hook_input").toHaveValue("");
|
||||
|
||||
pickerProps.value = DateTime.fromSQL("2023-06-06");
|
||||
await animationFrame();
|
||||
|
||||
expect(".datetime_hook_input").toHaveValue("06/06/2023");
|
||||
});
|
||||
|
||||
test("reactivity: update reactive object returned by the hook", async () => {
|
||||
let pickerProps;
|
||||
const defaultPickerProps = {
|
||||
value: false,
|
||||
type: "date",
|
||||
};
|
||||
|
||||
await mountInput(() => {
|
||||
pickerProps = useDateTimePicker({ pickerProps: defaultPickerProps }).state;
|
||||
});
|
||||
|
||||
expect(".datetime_hook_input").toHaveValue("");
|
||||
expect(pickerProps.value).toBe(false);
|
||||
|
||||
pickerProps.value = DateTime.fromSQL("2023-06-06");
|
||||
await tick();
|
||||
|
||||
expect(".datetime_hook_input").toHaveValue("06/06/2023");
|
||||
});
|
||||
|
||||
test("returned value is updated when input has changed", async () => {
|
||||
let pickerProps;
|
||||
const defaultPickerProps = {
|
||||
value: false,
|
||||
type: "date",
|
||||
};
|
||||
|
||||
await mountInput(() => {
|
||||
pickerProps = useDateTimePicker({ pickerProps: defaultPickerProps }).state;
|
||||
});
|
||||
|
||||
expect(".datetime_hook_input").toHaveValue("");
|
||||
expect(pickerProps.value).toBe(false);
|
||||
await click(".datetime_hook_input");
|
||||
await edit("06/06/2023");
|
||||
await click(document.body);
|
||||
|
||||
expect(pickerProps.value.toSQL().split(" ")[0]).toBe("2023-06-06");
|
||||
});
|
||||
|
||||
test("value is not updated if it did not change", async () => {
|
||||
const getShortDate = (date) => date.toSQL().split(" ")[0];
|
||||
|
||||
let pickerProps;
|
||||
const defaultPickerProps = {
|
||||
value: DateTime.fromSQL("2023-06-06"),
|
||||
type: "date",
|
||||
};
|
||||
|
||||
await mountInput(() => {
|
||||
pickerProps = useDateTimePicker({
|
||||
pickerProps: defaultPickerProps,
|
||||
onApply: (value) => {
|
||||
expect.step(getShortDate(value));
|
||||
},
|
||||
}).state;
|
||||
});
|
||||
|
||||
expect(".datetime_hook_input").toHaveValue("06/06/2023");
|
||||
expect(getShortDate(pickerProps.value)).toBe("2023-06-06");
|
||||
|
||||
await click(".datetime_hook_input");
|
||||
await edit("06/06/2023");
|
||||
await click(document.body);
|
||||
|
||||
expect(getShortDate(pickerProps.value)).toBe("2023-06-06");
|
||||
expect.verifySteps([]);
|
||||
|
||||
await click(".datetime_hook_input");
|
||||
await edit("07/07/2023");
|
||||
await click(document.body);
|
||||
|
||||
expect(getShortDate(pickerProps.value)).toBe("2023-07-07");
|
||||
expect.verifySteps(["2023-07-07"]);
|
||||
});
|
||||
|
||||
test("close popover when owner component is unmounted", async() => {
|
||||
class Child extends Component {
|
||||
static components = { DateTimeInput };
|
||||
static props = [];
|
||||
static template = xml`
|
||||
<div>
|
||||
<input type="text" class="datetime_hook_input" t-ref="start-date"/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
useDateTimePicker({
|
||||
pickerProps: {
|
||||
value: [false, false],
|
||||
type: "date",
|
||||
range: true,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { resolve: hidePopover, promise } = Promise.withResolvers();
|
||||
|
||||
class DateTimeToggler extends Component {
|
||||
static components = { Child };
|
||||
static props = [];
|
||||
static template = xml`<Child t-if="!state.hidden"/>`;
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
hidden: false,
|
||||
});
|
||||
promise.then(() => {
|
||||
this.state.hidden = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(DateTimeToggler);
|
||||
|
||||
await click("input.datetime_hook_input");
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
|
||||
// we can't simply add a button because `useClickAway` will be triggered, thus closing the popover properly
|
||||
hidePopover();
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,577 @@
|
|||
import { test, expect, describe } from "@odoo/hoot";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { assertDateTimePicker, getPickerCell } from "../../datetime/datetime_test_helpers";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { DateTimeInput } from "@web/core/datetime/datetime_input";
|
||||
import {
|
||||
contains,
|
||||
defineParams,
|
||||
makeMockEnv,
|
||||
mountWithCleanup,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { click, edit, queryAll, queryFirst, select } from "@odoo/hoot-dom";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
class DateTimeInputComp extends Component {
|
||||
static components = { DateTimeInput };
|
||||
static template = xml`<DateTimeInput t-props="props" />`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
async function changeLang(lang) {
|
||||
serverState.lang = lang;
|
||||
await makeMockEnv();
|
||||
}
|
||||
|
||||
describe("DateTimeInput (date)", () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
date_format: "%d/%m/%Y",
|
||||
time_format: "%H:%M:%S",
|
||||
},
|
||||
});
|
||||
|
||||
test("basic rendering", async () => {
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_datetime_input").toHaveCount(1);
|
||||
assertDateTimePicker(false);
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("09/01/1997");
|
||||
|
||||
await click(".o_datetime_input");
|
||||
await animationFrame();
|
||||
|
||||
assertDateTimePicker({
|
||||
title: "January 1997",
|
||||
date: [
|
||||
{
|
||||
cells: [
|
||||
[0, 0, 0, 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, 0],
|
||||
],
|
||||
daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
weekNumbers: [1, 2, 3, 4, 5],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("pick a date", async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
await makeMockEnv();
|
||||
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
onChange: (date) => {
|
||||
expect.step("datetime-changed");
|
||||
expect(date.toFormat("dd/MM/yyyy")).toBe("08/02/1997", {
|
||||
message: "Event should transmit the correct date",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await contains(".o_datetime_input").click();
|
||||
await contains(".o_datetime_picker .o_next").click();
|
||||
|
||||
expect.verifySteps([]);
|
||||
await contains(getPickerCell("8")).click();
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("08/02/1997");
|
||||
expect.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
test("pick a date with FR locale", async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
await changeLang("fr-FR");
|
||||
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
format: "dd MMM, yyyy",
|
||||
onChange: (date) => {
|
||||
expect.step("datetime-changed");
|
||||
expect(date.toFormat("dd/MM/yyyy")).toBe("19/09/1997", {
|
||||
message: "Event should transmit the correct date",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("09 janv., 1997");
|
||||
|
||||
await contains(".o_datetime_input").click();
|
||||
await contains(".o_zoom_out").click();
|
||||
await contains(getPickerCell("sept.")).click();
|
||||
await contains(getPickerCell("19")).click();
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("19 sept., 1997");
|
||||
expect.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
test("pick a date with locale (locale with different symbols)", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
await changeLang("gu");
|
||||
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
format: "dd MMM, yyyy",
|
||||
onChange: (date) => {
|
||||
expect.step("datetime-changed");
|
||||
expect(date.toFormat("dd/MM/yyyy")).toBe("19/09/1997", {
|
||||
message: "Event should transmit the correct date",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("09 જાન્યુ, 1997");
|
||||
|
||||
await contains(".o_datetime_input").click();
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("09 જાન્યુ, 1997");
|
||||
|
||||
await contains(".o_zoom_out").click();
|
||||
await contains(getPickerCell("સપ્ટે")).click();
|
||||
await contains(getPickerCell("19")).click();
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("19 સપ્ટે, 1997");
|
||||
expect.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
test("enter a date value", async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
onChange: (date) => {
|
||||
expect.step("datetime-changed");
|
||||
expect(date.toFormat("dd/MM/yyyy")).toBe("08/02/1997", {
|
||||
message: "Event should transmit the correct date",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect.verifySteps([]);
|
||||
|
||||
await contains(".o_datetime_input").click();
|
||||
await edit("08/02/1997");
|
||||
await animationFrame();
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps(["datetime-changed"]);
|
||||
|
||||
await click(".o_datetime_input");
|
||||
await animationFrame();
|
||||
|
||||
expect(getPickerCell("8")).toHaveClass("o_selected");
|
||||
});
|
||||
|
||||
test("Date format is correctly set", async () => {
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
format: "yyyy/MM/dd",
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("1997/01/09");
|
||||
|
||||
// Forces an update to assert that the registered format is the correct one
|
||||
await contains(".o_datetime_input").click();
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("1997/01/09");
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("popover should have enough space to be displayed", async () => {
|
||||
class Root extends Component {
|
||||
static components = { DateTimeInput };
|
||||
static template = xml`<div class="d-flex"><DateTimeInput t-props="props" /></div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
await mountWithCleanup(Root, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
},
|
||||
});
|
||||
const parent = queryFirst(".o_datetime_input").parentElement;
|
||||
const initialParentHeight = parent.clientHeight;
|
||||
|
||||
await contains(".o_datetime_input", { root: parent }).click();
|
||||
|
||||
const pickerRectHeight = queryFirst(".o_datetime_picker").clientHeight;
|
||||
|
||||
expect(initialParentHeight).toBeLessThan(pickerRectHeight, {
|
||||
message: "initial height shouldn't be big enough to display the picker",
|
||||
});
|
||||
expect(parent.clientHeight).toBeGreaterThan(pickerRectHeight, {
|
||||
message: "initial height should be big enough to display the picker",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("DateTimeInput (datetime)", () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
date_format: "%d/%m/%Y",
|
||||
time_format: "%H:%M:%S",
|
||||
},
|
||||
});
|
||||
|
||||
test("basic rendering", async () => {
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_datetime_input").toHaveCount(1);
|
||||
assertDateTimePicker(false);
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("09/01/1997 12:30:01");
|
||||
|
||||
await contains(".o_datetime_input").click();
|
||||
|
||||
assertDateTimePicker({
|
||||
title: "January 1997",
|
||||
date: [
|
||||
{
|
||||
cells: [
|
||||
[0, 0, 0, 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, 0],
|
||||
],
|
||||
daysOfWeek: ["#", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
weekNumbers: [1, 2, 3, 4, 5],
|
||||
},
|
||||
],
|
||||
time: [[12, 30]],
|
||||
});
|
||||
});
|
||||
|
||||
test("pick a date and time", async () => {
|
||||
await makeMockEnv();
|
||||
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
onChange: (date) => expect.step(date.toSQL().split(".")[0]),
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("09/01/1997 12:30:01");
|
||||
|
||||
await contains(".o_datetime_input").click();
|
||||
|
||||
// Select February 8th
|
||||
await contains(".o_datetime_picker .o_next").click();
|
||||
await contains(getPickerCell("8")).click();
|
||||
|
||||
// Select 15:45
|
||||
const [hourSelect, minuteSelect] = queryAll(".o_time_picker_select");
|
||||
await select("15", { target: hourSelect });
|
||||
await animationFrame();
|
||||
await select("45", { target: minuteSelect });
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("08/02/1997 15:45:01");
|
||||
expect.verifySteps(["1997-02-08 12:30:01", "1997-02-08 15:30:01", "1997-02-08 15:45:01"]);
|
||||
});
|
||||
|
||||
test("pick a date and time with locale", async () => {
|
||||
await changeLang("fr_FR");
|
||||
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
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) => expect.step(date.toSQL().split(".")[0]),
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("09 janv., 1997 12:30:01");
|
||||
|
||||
await contains(".o_datetime_input").click();
|
||||
|
||||
await contains(".o_zoom_out").click();
|
||||
await contains(getPickerCell("sept.")).click();
|
||||
await contains(getPickerCell("1")).click();
|
||||
|
||||
// Select 15:45
|
||||
const [hourSelect, minuteSelect] = queryAll(".o_time_picker_select");
|
||||
await select("15", { target: hourSelect });
|
||||
await animationFrame();
|
||||
await select("45", { target: minuteSelect });
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("01 sept., 1997 15:45:01");
|
||||
expect.verifySteps(["1997-09-01 12:30:01", "1997-09-01 15:30:01", "1997-09-01 15:45:01"]);
|
||||
});
|
||||
|
||||
test("pick a time with 12 hour format without meridiem", async () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
date_format: "%d/%m/%Y",
|
||||
time_format: "%I:%M:%S",
|
||||
},
|
||||
});
|
||||
|
||||
await makeMockEnv();
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("09/01/1997 08:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
onChange: (date) => expect.step(date.toSQL().split(".")[0]),
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("09/01/1997 08:30:01");
|
||||
|
||||
await contains(".o_datetime_input").click();
|
||||
|
||||
const [, minuteSelect] = queryAll(".o_time_picker_select");
|
||||
await select("15", { target: minuteSelect });
|
||||
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps(["1997-01-09 08:15:01"]);
|
||||
});
|
||||
|
||||
test("enter a datetime value", async () => {
|
||||
expect.assertions(7);
|
||||
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
onChange: (date) => {
|
||||
expect.step("datetime-changed");
|
||||
expect(date.toFormat("dd/MM/yyyy HH:mm:ss")).toBe("08/02/1997 15:45:05", {
|
||||
message: "Event should transmit the correct date",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect.verifySteps([]);
|
||||
|
||||
await contains(".o_datetime_input").click();
|
||||
await edit("08/02/1997 15:45:05");
|
||||
await animationFrame();
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps(["datetime-changed"]);
|
||||
|
||||
await contains(".o_datetime_input").click();
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("08/02/1997 15:45:05");
|
||||
expect(getPickerCell("8")).toHaveClass("o_selected");
|
||||
|
||||
const [hourSelect, minuteSelect] = queryAll(".o_time_picker_select");
|
||||
expect(hourSelect).toHaveValue("15");
|
||||
expect(minuteSelect).toHaveValue("45");
|
||||
});
|
||||
|
||||
test("Date time format is correctly set", async () => {
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("09/01/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
format: "HH:mm:ss yyyy/MM/dd",
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("12:30:01 1997/01/09");
|
||||
|
||||
// Forces an update to assert that the registered format is the correct one
|
||||
await contains(".o_datetime_input").click();
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("12:30:01 1997/01/09");
|
||||
});
|
||||
|
||||
test("Datepicker works with norwegian locale", async () => {
|
||||
expect.assertions(7);
|
||||
|
||||
await changeLang("nb");
|
||||
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("09/04/1997 12:30:01", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
format: "dd MMM, yyyy",
|
||||
onChange(date) {
|
||||
expect.step("datetime-changed");
|
||||
expect(date.toFormat("dd/MM/yyyy")).toBe("01/04/1997", {
|
||||
message: "Event should transmit the correct date",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("09 apr., 1997");
|
||||
|
||||
// Forces an update to assert that the registered format is the correct one
|
||||
await contains(".o_datetime_input").click();
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("09 apr., 1997");
|
||||
|
||||
await contains(getPickerCell("1")).click();
|
||||
expect(".o_datetime_input").toHaveValue("01 apr., 1997");
|
||||
expect.verifySteps(["datetime-changed"]);
|
||||
|
||||
await click(".o_apply");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["datetime-changed"]);
|
||||
});
|
||||
|
||||
test("Datepicker works with dots and commas in format", async () => {
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("10/03/2023 13:14:27", "dd/MM/yyyy HH:mm:ss"),
|
||||
type: "datetime",
|
||||
format: "dd.MM,yyyy",
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("10.03,2023");
|
||||
|
||||
// Forces an update to assert that the registered format is the correct one
|
||||
await contains(".o_datetime_input").click();
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("10.03,2023");
|
||||
});
|
||||
|
||||
test("start with no value", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
await makeMockEnv();
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
type: "datetime",
|
||||
onChange(date) {
|
||||
expect.step("datetime-changed");
|
||||
expect(date.toFormat("dd/MM/yyyy HH:mm:ss")).toBe("08/02/1997 15:45:05", {
|
||||
message: "Event should transmit the correct date",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("");
|
||||
expect.verifySteps([]);
|
||||
|
||||
await contains(".o_datetime_input").click();
|
||||
await edit("08/02/1997 15:45:05");
|
||||
await animationFrame();
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps(["datetime-changed"]);
|
||||
expect(".o_datetime_input").toHaveValue("08/02/1997 15:45:05");
|
||||
});
|
||||
|
||||
test("Clicking close button closes datetime picker", async () => {
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
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 contains(".o_datetime_input").click();
|
||||
await contains(".o_datetime_picker .o_datetime_buttons .btn-secondary").click();
|
||||
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("check datepicker in localization with textual month format", async () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
date_format: "%b/%d/%Y",
|
||||
time_format: "%H:%M:%S",
|
||||
},
|
||||
});
|
||||
|
||||
let onChangeDate;
|
||||
|
||||
await mountWithCleanup(DateTimeInputComp, {
|
||||
props: {
|
||||
value: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy"),
|
||||
type: "date",
|
||||
onChange: (date) => (onChangeDate = date),
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("Jan/09/1997");
|
||||
|
||||
await contains(".o_datetime_input").click();
|
||||
await contains(getPickerCell("5")).click();
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("Jan/05/1997");
|
||||
expect(onChangeDate.toFormat("dd/MM/yyyy")).toBe("05/01/1997");
|
||||
});
|
||||
|
||||
test("arab locale, latin numbering system as input", async () => {
|
||||
defineParams({
|
||||
lang_parameters: {
|
||||
date_format: "%d %b, %Y",
|
||||
time_format: "%H:%M:%S",
|
||||
},
|
||||
});
|
||||
|
||||
await changeLang("ar-001");
|
||||
|
||||
await mountWithCleanup(DateTimeInputComp);
|
||||
|
||||
await contains(".o_datetime_input").click();
|
||||
await edit("٠٤ يونيو, ٢٠٢٣ ١١:٣٣:٠٠");
|
||||
await animationFrame();
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("٠٤ يونيو, ٢٠٢٣ ١١:٣٣:٠٠");
|
||||
|
||||
await contains(".o_datetime_input").click();
|
||||
await edit("15 07, 2020 12:30:43");
|
||||
await animationFrame();
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_datetime_input").toHaveValue("١٥ يوليو, ٢٠٢٠ ١٢:٣٠:٤٣");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,460 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { Deferred, animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import { click, press } from "@odoo/hoot-dom";
|
||||
import { Pager } from "@web/core/pager/pager";
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { contains, mountWithCleanup, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { config as transitionConfig } from "@web/core/transition";
|
||||
|
||||
class PagerController extends Component {
|
||||
static template = xml`<Pager t-props="state" />`;
|
||||
static components = { Pager };
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({ ...this.props });
|
||||
}
|
||||
async updateProps(nextProps) {
|
||||
Object.assign(this.state, nextProps);
|
||||
await animationFrame();
|
||||
}
|
||||
}
|
||||
|
||||
test("basic interactions", async () => {
|
||||
const pager = await mountWithCleanup(PagerController, {
|
||||
props: {
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
async onUpdate(data) {
|
||||
expect.step(`offset: ${data.offset}, limit: ${data.limit}`);
|
||||
await pager.updateProps(data);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await contains(".o_pager button.o_pager_next:enabled").click();
|
||||
await contains(".o_pager button.o_pager_previous:enabled").click();
|
||||
|
||||
expect.verifySteps(["offset: 4, limit: 4", "offset: 0, limit: 4"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("basic interactions on desktop", async () => {
|
||||
const pager = await mountWithCleanup(PagerController, {
|
||||
props: {
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
async onUpdate(data) {
|
||||
await pager.updateProps(data);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_pager_counter .o_pager_value").toHaveText("1-4");
|
||||
|
||||
await click(".o_pager button.o_pager_next");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_pager_counter .o_pager_value").toHaveText("5-8");
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("basic interactions on mobile", async () => {
|
||||
patchWithCleanup(transitionConfig, { disabled: true });
|
||||
const pager = await mountWithCleanup(PagerController, {
|
||||
props: {
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
async onUpdate(data) {
|
||||
await pager.updateProps(data);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_pager_indicator").toHaveCount(0);
|
||||
|
||||
await click(".o_pager button.o_pager_next");
|
||||
await animationFrame();
|
||||
await animationFrame(); // transition
|
||||
|
||||
expect(".o_pager_indicator").toHaveCount(1);
|
||||
expect(".o_pager_indicator .o_pager_value").toHaveText("5-8");
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_pager_indicator").toHaveCount(0);
|
||||
|
||||
await click(".o_pager button.o_pager_previous");
|
||||
await animationFrame();
|
||||
await animationFrame(); // transition
|
||||
|
||||
expect(".o_pager_indicator").toHaveCount(1);
|
||||
expect(".o_pager_indicator .o_pager_value").toHaveText("1-4");
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_pager_indicator").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("edit the pager", async () => {
|
||||
const pager = await mountWithCleanup(PagerController, {
|
||||
props: {
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
async onUpdate(data) {
|
||||
await pager.updateProps(data);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await click(".o_pager_value");
|
||||
await animationFrame();
|
||||
|
||||
expect("input").toHaveCount(1);
|
||||
expect(".o_pager_counter .o_pager_value").toHaveValue("1-4");
|
||||
|
||||
await contains("input.o_pager_value").edit("1-6");
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
|
||||
expect("input").toHaveCount(0);
|
||||
expect(".o_pager_counter .o_pager_value").toHaveText("1-6");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("keydown on pager with same value", async () => {
|
||||
await mountWithCleanup(PagerController, {
|
||||
props: {
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
onUpdate(data) {
|
||||
expect.step("pager-changed");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await click(".o_pager_value");
|
||||
await animationFrame();
|
||||
|
||||
expect("input").toHaveCount(1);
|
||||
expect(".o_pager_counter .o_pager_value").toHaveValue("1-4");
|
||||
expect.verifySteps([]);
|
||||
|
||||
await press("Enter");
|
||||
await animationFrame();
|
||||
expect("input").toHaveCount(0);
|
||||
expect(".o_pager_counter .o_pager_value").toHaveText("1-4");
|
||||
expect.verifySteps(["pager-changed"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("pager value formatting", async () => {
|
||||
expect.assertions(8);
|
||||
const pager = await mountWithCleanup(PagerController, {
|
||||
props: {
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
async onUpdate(data) {
|
||||
await pager.updateProps(data);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_pager_counter .o_pager_value").toHaveText("1-4");
|
||||
|
||||
async function inputAndAssert(inputValue, expected) {
|
||||
await click(".o_pager_counter .o_pager_value");
|
||||
await animationFrame();
|
||||
await contains("input.o_pager_value").edit(inputValue);
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(".o_pager_counter .o_pager_value").toHaveText(expected);
|
||||
}
|
||||
|
||||
await inputAndAssert("4-4", "4");
|
||||
await inputAndAssert("1-11", "1-10");
|
||||
await inputAndAssert("20-15", "10");
|
||||
await inputAndAssert("6-5", "10");
|
||||
await inputAndAssert("definitelyValidNumber", "10");
|
||||
await inputAndAssert(" 1 , 2 ", "1-2");
|
||||
await inputAndAssert("3 8", "3-8");
|
||||
});
|
||||
|
||||
test("pager disabling", async () => {
|
||||
const reloadPromise = new Deferred();
|
||||
const pager = await mountWithCleanup(PagerController, {
|
||||
props: {
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
// The goal here is to test the reactivity of the pager; in a
|
||||
// typical views, we disable the pager after switching page
|
||||
// to avoid switching twice with the same action (double click).
|
||||
async onUpdate(data) {
|
||||
// 1. Simulate a (long) server action
|
||||
await reloadPromise;
|
||||
// 2. Update the view with loaded data
|
||||
await pager.updateProps(data);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Click and check button is disabled
|
||||
await click(".o_pager button.o_pager_next");
|
||||
await animationFrame();
|
||||
expect(".o_pager button.o_pager_next").toHaveAttribute("disabled");
|
||||
|
||||
await click(".o_pager button.o_pager_previous");
|
||||
await animationFrame();
|
||||
expect(".o_pager button.o_pager_previous").toHaveAttribute("disabled");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("pager disabling on desktop", async () => {
|
||||
const reloadPromise = new Deferred();
|
||||
const pager = await mountWithCleanup(PagerController, {
|
||||
props: {
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
// The goal here is to test the reactivity of the pager; in a
|
||||
// typical views, we disable the pager after switching page
|
||||
// to avoid switching twice with the same action (double click).
|
||||
async onUpdate(data) {
|
||||
// 1. Simulate a (long) server action
|
||||
await reloadPromise;
|
||||
// 2. Update the view with loaded data
|
||||
await pager.updateProps(data);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await click(".o_pager button.o_pager_next");
|
||||
await animationFrame();
|
||||
// Try to edit the pager value
|
||||
await click(".o_pager_value");
|
||||
await animationFrame();
|
||||
|
||||
expect("button").toHaveCount(2);
|
||||
expect("button:nth-child(1)").toHaveAttribute("disabled");
|
||||
expect("button:nth-child(2)").toHaveAttribute("disabled");
|
||||
expect("span.o_pager_value").toHaveCount(1);
|
||||
|
||||
reloadPromise.resolve();
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
|
||||
expect("button").toHaveCount(2);
|
||||
expect("button:nth-child(1)").not.toHaveAttribute("disabled");
|
||||
expect("button:nth-child(2)").not.toHaveAttribute("disabled");
|
||||
expect(".o_pager_counter .o_pager_value").toHaveText("5-8");
|
||||
|
||||
await click(".o_pager_value");
|
||||
await animationFrame();
|
||||
|
||||
expect("input.o_pager_value").toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("desktop input interaction", async () => {
|
||||
const pager = await mountWithCleanup(PagerController, {
|
||||
props: {
|
||||
offset: 0,
|
||||
limit: 4,
|
||||
total: 10,
|
||||
async onUpdate(data) {
|
||||
await pager.updateProps(data);
|
||||
},
|
||||
},
|
||||
});
|
||||
await click(".o_pager_value");
|
||||
await animationFrame();
|
||||
|
||||
expect("input").toHaveCount(1);
|
||||
expect("input").toBeFocused();
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect("input").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("updateTotal props: click on total", async () => {
|
||||
const pager = await mountWithCleanup(PagerController, {
|
||||
props: {
|
||||
offset: 0,
|
||||
limit: 5,
|
||||
total: 10,
|
||||
onUpdate() {},
|
||||
async updateTotal() {
|
||||
await pager.updateProps({ total: 25, updateTotal: undefined });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_pager_value").toHaveText("1-5");
|
||||
expect(".o_pager_limit").toHaveText("10+");
|
||||
expect(".o_pager_limit").toHaveClass("o_pager_limit_fetch");
|
||||
|
||||
await click(".o_pager_limit_fetch");
|
||||
await animationFrame();
|
||||
expect(".o_pager_value").toHaveText("1-5");
|
||||
expect(".o_pager_limit").toHaveText("25");
|
||||
expect(".o_pager_limit").not.toHaveClass("o_pager_limit_fetch");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("updateTotal props: click next", async () => {
|
||||
let tempTotal = 10;
|
||||
const realTotal = 18;
|
||||
const pager = await mountWithCleanup(PagerController, {
|
||||
props: {
|
||||
offset: 0,
|
||||
limit: 5,
|
||||
total: tempTotal,
|
||||
async onUpdate(data) {
|
||||
tempTotal = Math.min(realTotal, Math.max(tempTotal, data.offset + data.limit));
|
||||
const nextProps = { ...data, total: tempTotal };
|
||||
if (tempTotal === realTotal) {
|
||||
nextProps.updateTotal = undefined;
|
||||
}
|
||||
await pager.updateProps(nextProps);
|
||||
},
|
||||
updateTotal() {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_pager_value").toHaveText("1-5");
|
||||
expect(".o_pager_limit").toHaveText("10+");
|
||||
expect(".o_pager_limit").toHaveClass("o_pager_limit_fetch");
|
||||
|
||||
await contains(".o_pager_next:enabled").click();
|
||||
|
||||
expect(".o_pager_value").toHaveText("6-10");
|
||||
expect(".o_pager_limit").toHaveText("10+");
|
||||
expect(".o_pager_limit").toHaveClass("o_pager_limit_fetch");
|
||||
|
||||
await contains(".o_pager_next:enabled").click();
|
||||
|
||||
expect(".o_pager_value").toHaveText("11-15");
|
||||
expect(".o_pager_limit").toHaveText("15+");
|
||||
expect(".o_pager_limit").toHaveClass("o_pager_limit_fetch");
|
||||
|
||||
await contains(".o_pager_next:enabled").click();
|
||||
|
||||
expect(".o_pager_value").toHaveText("16-18");
|
||||
expect(".o_pager_limit").toHaveText("18");
|
||||
expect(".o_pager_limit").not.toHaveClass("o_pager_limit_fetch");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("updateTotal props: edit input", async () => {
|
||||
let tempTotal = 10;
|
||||
const realTotal = 18;
|
||||
const pager = await mountWithCleanup(PagerController, {
|
||||
props: {
|
||||
offset: 0,
|
||||
limit: 5,
|
||||
total: tempTotal,
|
||||
async onUpdate(data) {
|
||||
tempTotal = Math.min(realTotal, Math.max(tempTotal, data.offset + data.limit));
|
||||
const nextProps = { ...data, total: tempTotal };
|
||||
if (tempTotal === realTotal) {
|
||||
nextProps.updateTotal = undefined;
|
||||
}
|
||||
await pager.updateProps(nextProps);
|
||||
},
|
||||
updateTotal() {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_pager_value").toHaveText("1-5");
|
||||
expect(".o_pager_limit").toHaveText("10+");
|
||||
expect(".o_pager_limit").toHaveClass("o_pager_limit_fetch");
|
||||
|
||||
await click(".o_pager_value");
|
||||
await animationFrame();
|
||||
await contains("input.o_pager_value").edit("3-8");
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_pager_value").toHaveText("3-8");
|
||||
expect(".o_pager_limit").toHaveText("10+");
|
||||
expect(".o_pager_limit").toHaveClass("o_pager_limit_fetch");
|
||||
|
||||
await click(".o_pager_value");
|
||||
await animationFrame();
|
||||
await contains("input.o_pager_value").edit("3-20");
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
await animationFrame();
|
||||
expect(".o_pager_value").toHaveText("3-18");
|
||||
expect(".o_pager_limit").toHaveText("18");
|
||||
expect(".o_pager_limit").not.toHaveClass("o_pager_limit_fetch");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("updateTotal props: can use next even if single page", async () => {
|
||||
const pager = await mountWithCleanup(PagerController, {
|
||||
props: {
|
||||
offset: 0,
|
||||
limit: 5,
|
||||
total: 5,
|
||||
async onUpdate(data) {
|
||||
await pager.updateProps({ ...data, total: 10 });
|
||||
},
|
||||
updateTotal() {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_pager_value").toHaveText("1-5");
|
||||
expect(".o_pager_limit").toHaveText("5+");
|
||||
expect(".o_pager_limit").toHaveClass("o_pager_limit_fetch");
|
||||
|
||||
await click(".o_pager_next");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_pager_value").toHaveText("6-10");
|
||||
expect(".o_pager_limit").toHaveText("10+");
|
||||
expect(".o_pager_limit").toHaveClass("o_pager_limit_fetch");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("updateTotal props: click previous", async () => {
|
||||
const pager = await mountWithCleanup(PagerController, {
|
||||
props: {
|
||||
offset: 0,
|
||||
limit: 5,
|
||||
total: 10,
|
||||
async onUpdate(data) {
|
||||
await pager.updateProps(data);
|
||||
},
|
||||
async updateTotal() {
|
||||
const total = 23;
|
||||
await pager.updateProps({ total, updateTotal: undefined });
|
||||
return total;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_pager_value").toHaveText("1-5");
|
||||
expect(".o_pager_limit").toHaveText("10+");
|
||||
expect(".o_pager_limit").toHaveClass("o_pager_limit_fetch");
|
||||
|
||||
await click(".o_pager_previous");
|
||||
await animationFrame();
|
||||
await animationFrame(); // double call to updateProps
|
||||
|
||||
expect(".o_pager_value").toHaveText("21-23");
|
||||
expect(".o_pager_limit").toHaveText("23");
|
||||
expect(".o_pager_limit").not.toHaveClass("o_pager_limit_fetch");
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { PagerIndicator } from "@web/core/pager/pager_indicator";
|
||||
import { mountWithCleanup, patchWithCleanup } from "../../web_test_helpers";
|
||||
import { config as transitionConfig } from "@web/core/transition";
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
import { PAGER_UPDATED_EVENT, pagerBus } from "@web/core/pager/pager";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
|
||||
test("displays the pager indicator", async () => {
|
||||
patchWithCleanup(transitionConfig, { disabled: true });
|
||||
await mountWithCleanup(PagerIndicator, { noMainContainer: true });
|
||||
expect(".o_pager_indicator").toHaveCount(0, {
|
||||
message: "the pager indicator should not be displayed",
|
||||
});
|
||||
pagerBus.trigger(PAGER_UPDATED_EVENT, { value: "1-4", total: 10 });
|
||||
await animationFrame();
|
||||
expect(".o_pager_indicator").toHaveCount(1, {
|
||||
message: "the pager indicator should be displayed",
|
||||
});
|
||||
expect(".o_pager_indicator").toHaveText("1-4 / 10");
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
expect(".o_pager_indicator").toHaveCount(0, {
|
||||
message: "the pager indicator should not be displayed",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
import { expect, test, describe, destroy } from "@odoo/hoot";
|
||||
import { tick, Deferred } from "@odoo/hoot-mock";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
import { mountWithCleanup, contains, makeDialogMockEnv } from "@web/../tests/web_test_helpers";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("check content confirmation dialog", async () => {
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ConfirmationDialog, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {},
|
||||
confirm: () => {},
|
||||
cancel: () => {},
|
||||
},
|
||||
});
|
||||
expect(".modal-header").toHaveText("Confirmation");
|
||||
expect(".modal-body").toHaveText("Some content");
|
||||
});
|
||||
|
||||
test("Without dismiss callback: pressing escape to close the dialog", async () => {
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ConfirmationDialog, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {
|
||||
expect.step("Close action");
|
||||
},
|
||||
confirm: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
cancel: () => {
|
||||
expect.step("Cancel action");
|
||||
},
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await press("escape");
|
||||
await tick();
|
||||
expect.verifySteps(["Cancel action", "Close action"]);
|
||||
});
|
||||
|
||||
test("With dismiss callback: pressing escape to close the dialog", async () => {
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ConfirmationDialog, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {
|
||||
expect.step("Close action");
|
||||
},
|
||||
confirm: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
cancel: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
dismiss: () => {
|
||||
expect.step("Dismiss action");
|
||||
},
|
||||
},
|
||||
});
|
||||
await press("escape");
|
||||
await tick();
|
||||
expect.verifySteps(["Dismiss action", "Close action"]);
|
||||
});
|
||||
|
||||
test("Without dismiss callback: clicking on 'X' to close the dialog", async () => {
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ConfirmationDialog, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {
|
||||
expect.step("Close action");
|
||||
},
|
||||
confirm: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
cancel: () => {
|
||||
expect.step("Cancel action");
|
||||
},
|
||||
},
|
||||
});
|
||||
await contains(".modal-header .btn-close").click();
|
||||
expect.verifySteps(["Cancel action", "Close action"]);
|
||||
});
|
||||
|
||||
test("With dismiss callback: clicking on 'X' to close the dialog", async () => {
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ConfirmationDialog, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {
|
||||
expect.step("Close action");
|
||||
},
|
||||
confirm: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
cancel: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
dismiss: () => {
|
||||
expect.step("Dismiss action");
|
||||
},
|
||||
},
|
||||
});
|
||||
await contains(".modal-header .btn-close").click();
|
||||
expect.verifySteps(["Dismiss action", "Close action"]);
|
||||
});
|
||||
|
||||
test("clicking on 'Ok'", async () => {
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ConfirmationDialog, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {
|
||||
expect.step("Close action");
|
||||
},
|
||||
confirm: () => {
|
||||
expect.step("Confirm action");
|
||||
},
|
||||
cancel: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains(".modal-footer .btn-primary").click();
|
||||
expect.verifySteps(["Confirm action", "Close action"]);
|
||||
});
|
||||
|
||||
test("clicking on 'Cancel'", async () => {
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ConfirmationDialog, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {
|
||||
expect.step("Close action");
|
||||
},
|
||||
confirm: () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
cancel: () => {
|
||||
expect.step("Cancel action");
|
||||
},
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
await contains(".modal-footer .btn-secondary").click();
|
||||
expect.verifySteps(["Cancel action", "Close action"]);
|
||||
});
|
||||
|
||||
test("can't click twice on 'Ok'", async () => {
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ConfirmationDialog, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {},
|
||||
confirm: () => {
|
||||
expect.step("Confirm action");
|
||||
},
|
||||
cancel: () => {},
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
expect(".modal-footer .btn-primary").not.toHaveAttribute("disabled");
|
||||
expect(".modal-footer .btn-secondary").not.toHaveAttribute("disabled");
|
||||
await contains(".modal-footer .btn-primary").click();
|
||||
expect(".modal-footer .btn-primary").toHaveAttribute("disabled");
|
||||
expect(".modal-footer .btn-secondary").toHaveAttribute("disabled");
|
||||
expect.verifySteps(["Confirm action"]);
|
||||
});
|
||||
|
||||
test("can't click twice on 'Cancel'", async () => {
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ConfirmationDialog, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {},
|
||||
confirm: () => {},
|
||||
cancel: () => {
|
||||
expect.step("Cancel action");
|
||||
},
|
||||
},
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
expect(".modal-footer .btn-primary").not.toHaveAttribute("disabled");
|
||||
expect(".modal-footer .btn-secondary").not.toHaveAttribute("disabled");
|
||||
await contains(".modal-footer .btn-secondary").click();
|
||||
expect(".modal-footer .btn-primary").toHaveAttribute("disabled");
|
||||
expect(".modal-footer .btn-secondary").toHaveAttribute("disabled");
|
||||
expect.verifySteps(["Cancel action"]);
|
||||
});
|
||||
|
||||
test("can't cancel (with escape) after confirm", async () => {
|
||||
const def = new Deferred();
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ConfirmationDialog, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {
|
||||
expect.step("Close action");
|
||||
},
|
||||
confirm: () => {
|
||||
expect.step("Confirm action");
|
||||
return def;
|
||||
},
|
||||
cancel: () => {
|
||||
throw new Error("should not cancel");
|
||||
},
|
||||
},
|
||||
});
|
||||
await contains(".modal-footer .btn-primary").click();
|
||||
expect.verifySteps(["Confirm action"]);
|
||||
await press("escape");
|
||||
await tick();
|
||||
expect.verifySteps([]);
|
||||
def.resolve();
|
||||
await tick();
|
||||
expect.verifySteps(["Close action"]);
|
||||
});
|
||||
|
||||
test("wait for confirm callback before closing", async () => {
|
||||
const def = new Deferred();
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ConfirmationDialog, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
close: () => {
|
||||
expect.step("Close action");
|
||||
},
|
||||
confirm: () => {
|
||||
expect.step("Confirm action");
|
||||
return def;
|
||||
},
|
||||
},
|
||||
});
|
||||
await contains(".modal-footer .btn-primary").click();
|
||||
expect.verifySteps(["Confirm action"]);
|
||||
def.resolve();
|
||||
await tick();
|
||||
expect.verifySteps(["Close action"]);
|
||||
});
|
||||
|
||||
test("Focus is correctly restored after confirmation", async () => {
|
||||
const env = await makeDialogMockEnv();
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<div class="my-comp"><input type="text" class="my-input"/></div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent, { env });
|
||||
await contains(".my-input").focus();
|
||||
expect(".my-input").toBeFocused();
|
||||
|
||||
const dialog = await mountWithCleanup(ConfirmationDialog, {
|
||||
env,
|
||||
props: {
|
||||
body: "Some content",
|
||||
title: "Confirmation",
|
||||
confirm: () => {},
|
||||
close: () => {},
|
||||
},
|
||||
});
|
||||
expect(".modal-footer .btn-primary").toBeFocused();
|
||||
await contains(".modal-footer .btn-primary").click();
|
||||
expect(document.body).toBeFocused();
|
||||
destroy(dialog);
|
||||
await Promise.resolve();
|
||||
expect(".my-input").toBeFocused();
|
||||
});
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import { evalPartialContext, makeContext } from "@web/core/context";
|
||||
import { expect, test, describe } from "@odoo/hoot";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
describe("makeContext", () => {
|
||||
test("return empty context", () => {
|
||||
expect(makeContext([])).toEqual({});
|
||||
});
|
||||
|
||||
test("duplicate a context", () => {
|
||||
const ctx1 = { a: 1 };
|
||||
const ctx2 = makeContext([ctx1]);
|
||||
expect(ctx1).not.toBe(ctx2);
|
||||
expect(ctx1).toEqual(ctx2);
|
||||
});
|
||||
|
||||
test("can accept undefined or empty string", () => {
|
||||
expect(makeContext([undefined])).toEqual({});
|
||||
expect(makeContext([{ a: 1 }, undefined, { b: 2 }])).toEqual({ a: 1, b: 2 });
|
||||
expect(makeContext([""])).toEqual({});
|
||||
expect(makeContext([{ a: 1 }, "", { b: 2 }])).toEqual({ a: 1, b: 2 });
|
||||
});
|
||||
|
||||
test("evaluate strings", () => {
|
||||
expect(makeContext(["{'a': 33}"])).toEqual({ a: 33 });
|
||||
});
|
||||
|
||||
test("evaluated context is used as evaluation context along the way", () => {
|
||||
expect(makeContext([{ a: 1 }, "{'a': a + 1}"])).toEqual({ a: 2 });
|
||||
expect(makeContext([{ a: 1 }, "{'b': a + 1}"])).toEqual({ a: 1, b: 2 });
|
||||
expect(makeContext([{ a: 1 }, "{'b': a + 1}", "{'c': b + 1}"])).toEqual({
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
});
|
||||
expect(makeContext([{ a: 1 }, "{'b': a + 1}", "{'a': b + 1}"])).toEqual({ a: 3, b: 2 });
|
||||
});
|
||||
|
||||
test("initial evaluation context", () => {
|
||||
expect(makeContext(["{'a': a + 1}"], { a: 1 })).toEqual({ a: 2 });
|
||||
expect(makeContext(["{'b': a + 1}"], { a: 1 })).toEqual({ b: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("evalPartialContext", () => {
|
||||
test("static contexts", () => {
|
||||
expect(evalPartialContext("{}", {})).toEqual({});
|
||||
expect(evalPartialContext("{'a': 1}", {})).toEqual({ a: 1 });
|
||||
expect(evalPartialContext("{'a': 'b'}", {})).toEqual({ a: "b" });
|
||||
expect(evalPartialContext("{'a': true}", {})).toEqual({ a: true });
|
||||
expect(evalPartialContext("{'a': None}", {})).toEqual({ a: null });
|
||||
});
|
||||
|
||||
test("complete dynamic contexts", () => {
|
||||
expect(evalPartialContext("{'a': a, 'b': 1}", { a: 2 })).toEqual({ a: 2, b: 1 });
|
||||
});
|
||||
|
||||
test("partial dynamic contexts", () => {
|
||||
expect(evalPartialContext("{'a': a}", {})).toEqual({});
|
||||
expect(evalPartialContext("{'a': a, 'b': 1}", {})).toEqual({ b: 1 });
|
||||
expect(evalPartialContext("{'a': a, 'b': b}", { a: 2 })).toEqual({ a: 2 });
|
||||
});
|
||||
|
||||
test("value of type obj (15)", () => {
|
||||
expect(evalPartialContext("{'a': a.b.c}", {})).toEqual({});
|
||||
expect(evalPartialContext("{'a': a.b.c}", { a: {} })).toEqual({});
|
||||
expect(evalPartialContext("{'a': a.b.c}", { a: { b: { c: 2 } } })).toEqual({ a: 2 });
|
||||
});
|
||||
|
||||
test("value of type op (14)", () => {
|
||||
expect(evalPartialContext("{'a': a + 1}", {})).toEqual({});
|
||||
expect(evalPartialContext("{'a': a + b}", {})).toEqual({});
|
||||
expect(evalPartialContext("{'a': a + b}", { a: 2 })).toEqual({});
|
||||
expect(evalPartialContext("{'a': a + 1}", { a: 2 })).toEqual({ a: 3 });
|
||||
expect(evalPartialContext("{'a': a + b}", { a: 2, b: 3 })).toEqual({ a: 5 });
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { makeMockEnv, serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { formatCurrency } from "@web/core/currency";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
beforeEach(async () => {
|
||||
await makeMockEnv(); // To start the localization service
|
||||
});
|
||||
|
||||
test("formatCurrency", async () => {
|
||||
serverState.currencies = [
|
||||
{ id: 1, position: "after", symbol: "€" },
|
||||
{ id: 2, position: "before", symbol: "$" },
|
||||
];
|
||||
|
||||
expect(formatCurrency(200)).toBe("200.00");
|
||||
expect(formatCurrency(1234567.654, 1)).toBe("1,234,567.65\u00a0€");
|
||||
expect(formatCurrency(1234567.654, 2)).toBe("$\u00a01,234,567.65");
|
||||
expect(formatCurrency(1234567.654, 44)).toBe("1,234,567.65");
|
||||
expect(formatCurrency(1234567.654, 1, { noSymbol: true })).toBe("1,234,567.65");
|
||||
expect(formatCurrency(8.0, 1, { humanReadable: true })).toBe("8.00\u00a0€");
|
||||
expect(formatCurrency(1234567.654, 1, { humanReadable: true })).toBe("1.23M\u00a0€");
|
||||
expect(formatCurrency(1990000.001, 1, { humanReadable: true })).toBe("1.99M\u00a0€");
|
||||
expect(formatCurrency(1234567.654, 44, { digits: [69, 1] })).toBe("1,234,567.7");
|
||||
expect(formatCurrency(1234567.654, 2, { digits: [69, 1] })).toBe("$\u00a01,234,567.7", {
|
||||
message: "options digits should take over currency digits when both are defined",
|
||||
});
|
||||
});
|
||||
|
||||
test("formatCurrency without currency", async () => {
|
||||
serverState.currencies = [];
|
||||
|
||||
expect(formatCurrency(1234567.654, 10, { humanReadable: true })).toBe("1.23M");
|
||||
expect(formatCurrency(1234567.654, 10)).toBe("1,234,567.65");
|
||||
});
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
import { expect } from "@odoo/hoot";
|
||||
import {
|
||||
click,
|
||||
queryAll,
|
||||
queryAllTexts,
|
||||
queryAllValues,
|
||||
queryFirst,
|
||||
queryText,
|
||||
} from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
|
||||
const PICKER_COLS = 7;
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
// Check for picker in DOM
|
||||
if (expectedParams) {
|
||||
expect(".o_datetime_picker").toHaveCount(1);
|
||||
} else {
|
||||
expect(".o_datetime_picker").toHaveCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const { title, date, time } = expectedParams;
|
||||
|
||||
// Title
|
||||
if (title) {
|
||||
expect(".o_datetime_picker_header").toHaveCount(1);
|
||||
expect(".o_datetime_picker_header").toHaveText(title);
|
||||
} else {
|
||||
expect(".o_datetime_picker_header").toHaveCount(0);
|
||||
}
|
||||
|
||||
// Time picker
|
||||
if (time) {
|
||||
expect(".o_time_picker").toHaveCount(time.length);
|
||||
for (let i = 0; i < time.length; i++) {
|
||||
const expectedTime = time[i];
|
||||
const values = queryAll(`.o_time_picker:nth-child(${i + 1}) .o_time_picker_select`).map(
|
||||
(sel) => sel.value
|
||||
);
|
||||
const actual = [...values.slice(0, 2).map(Number), ...values.slice(2)];
|
||||
expect(actual).toEqual(expectedTime, {
|
||||
message: `time values should be [${expectedTime}]`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
expect(".o_time_picker").toHaveCount(0);
|
||||
}
|
||||
|
||||
// Date picker
|
||||
expect(".o_date_picker").toHaveCount(date.length);
|
||||
|
||||
let selectedCells = 0;
|
||||
let invalidCells = 0;
|
||||
let outOfRangeCells = 0;
|
||||
let todayCells = 0;
|
||||
for (let i = 0; i < date.length; i++) {
|
||||
const { cells, daysOfWeek, weekNumbers } = date[i];
|
||||
const cellEls = queryAll(`.o_date_picker:nth-child(${i + 1}) .o_date_item_cell`);
|
||||
const pickerRows = cells.length;
|
||||
expect(cellEls.length).toBe(pickerRows * PICKER_COLS, {
|
||||
message: `picker should have ${
|
||||
pickerRows * PICKER_COLS
|
||||
} cells (${pickerRows} rows and ${PICKER_COLS} columns)`,
|
||||
});
|
||||
|
||||
if (daysOfWeek) {
|
||||
const actualDow = queryAllTexts(
|
||||
`.o_date_picker:nth-child(${i + 1}) .o_day_of_week_cell`
|
||||
);
|
||||
expect(actualDow).toEqual(daysOfWeek, {
|
||||
message: `picker should display the days of week: ${daysOfWeek
|
||||
.map((dow) => `"${dow}"`)
|
||||
.join(", ")}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (weekNumbers) {
|
||||
expect(
|
||||
queryAllTexts(`.o_date_picker:nth-child(${i + 1}) .o_week_number_cell`).map(Number)
|
||||
).toEqual(weekNumbers, {
|
||||
message: `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];
|
||||
let value = cell;
|
||||
if (Array.isArray(cell)) {
|
||||
// Selected
|
||||
value = value[0];
|
||||
selectedCells++;
|
||||
expect(cellEl).toHaveClass("o_selected");
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
// Today
|
||||
value = Number(value);
|
||||
todayCells++;
|
||||
expect(cellEl).toHaveClass("o_today");
|
||||
}
|
||||
if (value === 0) {
|
||||
// Out of range
|
||||
value = "";
|
||||
outOfRangeCells++;
|
||||
expect(cellEl).toHaveClass("o_out_of_range");
|
||||
} else if (value < 0) {
|
||||
// Invalid
|
||||
value = Math.abs(value);
|
||||
invalidCells++;
|
||||
expect(cellEl).toHaveAttribute("disabled");
|
||||
}
|
||||
return String(value);
|
||||
})
|
||||
);
|
||||
|
||||
expect(cellEls.map((cell) => queryText(cell))).toEqual(expectedCells, {
|
||||
message: `cell content should match the expected values: [${expectedCells.join(", ")}]`,
|
||||
});
|
||||
}
|
||||
|
||||
expect(".o_selected").toHaveCount(selectedCells);
|
||||
expect(".o_datetime_button[disabled]").toHaveCount(invalidCells);
|
||||
expect(".o_out_of_range").toHaveCount(outOfRangeCells);
|
||||
expect(".o_today").toHaveCount(todayCells);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RegExp | string} expr
|
||||
* @param {boolean} [inBounds=false]
|
||||
*/
|
||||
export function getPickerCell(expr, inBounds = false) {
|
||||
const cells = queryAll(
|
||||
`.o_datetime_picker .o_date_item_cell${
|
||||
inBounds ? ":not(.o_out_of_range)" : ""
|
||||
}:contains("/^${expr}$/")`
|
||||
);
|
||||
return cells.length === 1 ? cells[0] : cells;
|
||||
}
|
||||
|
||||
export function getPickerApplyButton() {
|
||||
return queryFirst(".o_datetime_picker .o_datetime_buttons .o_apply");
|
||||
}
|
||||
|
||||
export async function zoomOut() {
|
||||
click(".o_zoom_out");
|
||||
await animationFrame();
|
||||
}
|
||||
|
||||
export function getTimePickers({ parse = false } = {}) {
|
||||
return queryAll(".o_time_picker").map((timePickerEl) => {
|
||||
if (parse) {
|
||||
return queryAllValues(".o_time_picker_select", { root: timePickerEl });
|
||||
}
|
||||
return queryAll(".o_time_picker_select", { root: timePickerEl });
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,804 @@
|
|||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
click,
|
||||
queryAll,
|
||||
queryAllProperties,
|
||||
queryAllTexts,
|
||||
queryOne,
|
||||
queryText,
|
||||
} from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import {
|
||||
clearRegistry,
|
||||
contains,
|
||||
defineModels,
|
||||
defineWebModels,
|
||||
fields,
|
||||
getService,
|
||||
makeDialogMockEnv,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
webModels,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { useDebugCategory, useOwnDebugContext } from "@web/core/debug/debug_context";
|
||||
import { DebugMenu } from "@web/core/debug/debug_menu";
|
||||
import { becomeSuperuser, regenerateAssets } from "@web/core/debug/debug_menu_items";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { user } from "@web/core/user";
|
||||
import { ActionDialog } from "@web/webclient/actions/action_dialog";
|
||||
import { openViewItem } from "@web/webclient/debug/debug_items";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
|
||||
class DebugMenuParent extends Component {
|
||||
static template = xml`<DebugMenu/>`;
|
||||
static components = { DebugMenu };
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
useOwnDebugContext({ categories: ["default", "custom"] });
|
||||
}
|
||||
}
|
||||
|
||||
const debugRegistry = registry.category("debug");
|
||||
|
||||
onRpc(async (args) => {
|
||||
if (args.method === "has_access") {
|
||||
return true;
|
||||
}
|
||||
if (args.route === "/web/dataset/call_kw/ir.attachment/regenerate_assets_bundles") {
|
||||
expect.step("ir.attachment/regenerate_assets_bundles");
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Remove this service to clear the debug menu from anything else than what the test insert into
|
||||
registry.category("services").remove("profiling");
|
||||
clearRegistry(debugRegistry.category("default"));
|
||||
clearRegistry(debugRegistry.category("custom"));
|
||||
});
|
||||
|
||||
describe.tags("desktop");
|
||||
describe("DebugMenu", () => {
|
||||
test("can be rendered", async () => {
|
||||
debugRegistry
|
||||
.category("default")
|
||||
.add("item_1", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 1",
|
||||
callback: () => {
|
||||
expect.step("callback item_1");
|
||||
},
|
||||
sequence: 10,
|
||||
section: "a",
|
||||
};
|
||||
})
|
||||
.add("item_2", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 2",
|
||||
callback: () => {
|
||||
expect.step("callback item_2");
|
||||
},
|
||||
sequence: 5,
|
||||
section: "a",
|
||||
};
|
||||
})
|
||||
.add("item_3", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 3",
|
||||
callback: () => {
|
||||
expect.step("callback item_3");
|
||||
},
|
||||
section: "b",
|
||||
};
|
||||
})
|
||||
.add("item_4", () => {
|
||||
return null;
|
||||
});
|
||||
await mountWithCleanup(DebugMenuParent);
|
||||
await contains("button.dropdown-toggle").click();
|
||||
expect(".dropdown-menu .dropdown-item").toHaveCount(3);
|
||||
expect(".dropdown-menu .dropdown-menu_group").toHaveCount(2);
|
||||
const children = [...queryOne(".dropdown-menu").children] || [];
|
||||
expect(children.map((el) => el.tagName)).toEqual(["DIV", "SPAN", "SPAN", "DIV", "SPAN"]);
|
||||
expect(queryAllTexts(children)).toEqual(["a", "Item 2", "Item 1", "b", "Item 3"]);
|
||||
|
||||
for (const item of queryAll(".dropdown-menu .dropdown-item")) {
|
||||
await click(item);
|
||||
}
|
||||
|
||||
expect.verifySteps(["callback item_2", "callback item_1", "callback item_3"]);
|
||||
});
|
||||
|
||||
test("items are sorted by sequence regardless of category", async () => {
|
||||
debugRegistry
|
||||
.category("default")
|
||||
.add("item_1", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 4",
|
||||
sequence: 4,
|
||||
};
|
||||
})
|
||||
.add("item_2", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 1",
|
||||
sequence: 1,
|
||||
};
|
||||
});
|
||||
debugRegistry
|
||||
.category("custom")
|
||||
.add("item_1", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 3",
|
||||
sequence: 3,
|
||||
};
|
||||
})
|
||||
.add("item_2", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 2",
|
||||
sequence: 2,
|
||||
};
|
||||
});
|
||||
await mountWithCleanup(DebugMenuParent);
|
||||
await contains("button.dropdown-toggle").click();
|
||||
expect(queryAllTexts(".dropdown-menu .dropdown-item")).toEqual([
|
||||
"Item 1",
|
||||
"Item 2",
|
||||
"Item 3",
|
||||
"Item 4",
|
||||
]);
|
||||
});
|
||||
|
||||
test("Don't display the DebugMenu if debug mode is disabled", async () => {
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ActionDialog, {
|
||||
env,
|
||||
props: { close: () => {} },
|
||||
});
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect(".o_dialog .o_debug_manager .fa-bug").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Display the DebugMenu correctly in a ActionDialog if debug mode is enabled", async () => {
|
||||
debugRegistry.category("default").add("global", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Global 1",
|
||||
callback: () => {
|
||||
expect.step("callback global_1");
|
||||
},
|
||||
sequence: 0,
|
||||
};
|
||||
});
|
||||
debugRegistry
|
||||
.category("custom")
|
||||
.add("item1", () => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 1",
|
||||
callback: () => {
|
||||
expect.step("callback item_1");
|
||||
},
|
||||
sequence: 10,
|
||||
};
|
||||
})
|
||||
.add("item2", ({ customKey }) => {
|
||||
return {
|
||||
type: "item",
|
||||
description: "Item 2",
|
||||
callback: () => {
|
||||
expect.step("callback item_2");
|
||||
expect(customKey).toBe("abc");
|
||||
},
|
||||
sequence: 20,
|
||||
};
|
||||
});
|
||||
class WithCustom extends ActionDialog {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
useDebugCategory("custom", { customKey: "abc" });
|
||||
}
|
||||
}
|
||||
serverState.debug = "1";
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(WithCustom, {
|
||||
env,
|
||||
props: { close: () => {} },
|
||||
});
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect(".o_dialog .o_debug_manager .fa-bug").toHaveCount(1);
|
||||
await contains(".o_dialog .o_debug_manager button").click();
|
||||
expect(".dropdown-menu .dropdown-item").toHaveCount(2);
|
||||
// Check that global debugManager elements are not displayed (global_1)
|
||||
const items = queryAll(".dropdown-menu .dropdown-item");
|
||||
expect(queryAllTexts(items)).toEqual(["Item 1", "Item 2"]);
|
||||
for (const item of items) {
|
||||
await click(item);
|
||||
}
|
||||
expect.verifySteps(["callback item_1", "callback item_2"]);
|
||||
});
|
||||
|
||||
test("can regenerate assets bundles", async () => {
|
||||
patchWithCleanup(browser.location, {
|
||||
reload: () => expect.step("reloadPage"),
|
||||
});
|
||||
debugRegistry.category("default").add("regenerateAssets", regenerateAssets);
|
||||
await mountWithCleanup(DebugMenuParent);
|
||||
await contains("button.dropdown-toggle").click();
|
||||
expect(".dropdown-menu .dropdown-item").toHaveCount(1);
|
||||
const item = queryOne(".dropdown-menu .dropdown-item");
|
||||
expect(item).toHaveText("Regenerate Assets");
|
||||
await click(item);
|
||||
await animationFrame();
|
||||
expect.verifySteps(["ir.attachment/regenerate_assets_bundles", "reloadPage"]);
|
||||
});
|
||||
|
||||
test("cannot acess the Become superuser menu if not admin", async () => {
|
||||
debugRegistry.category("default").add("becomeSuperuser", becomeSuperuser);
|
||||
user.isAdmin = false;
|
||||
await mountWithCleanup(DebugMenuParent);
|
||||
await contains("button.dropdown-toggle").click();
|
||||
expect(".dropdown-menu .dropdown-item").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("can open a view", async () => {
|
||||
serverState.debug = "1";
|
||||
|
||||
webModels.IrUiView._views.list = `<list><field name="name"/><field name="type"/></list>`;
|
||||
webModels.ResPartner._views["form,1"] = `<form><div class="some_view"/></form>`;
|
||||
|
||||
webModels.IrUiView._records.push({
|
||||
id: 1,
|
||||
name: "formView",
|
||||
model: "res.partner",
|
||||
type: "form",
|
||||
active: true,
|
||||
});
|
||||
|
||||
defineWebModels();
|
||||
registry.category("debug").category("default").add("openViewItem", openViewItem);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await contains(".o_debug_manager button").click();
|
||||
await contains(".dropdown-menu .dropdown-item").click();
|
||||
expect(".modal .o_list_view").toHaveCount(1);
|
||||
await contains(".modal .o_list_view .o_data_row td").click();
|
||||
expect(".modal").toHaveCount(0);
|
||||
expect(".some_view").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("get view: basic rendering", async () => {
|
||||
serverState.debug = "1";
|
||||
|
||||
webModels.ResPartner._views.list = `<list><field name="name"/></list>`;
|
||||
|
||||
defineWebModels();
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Partners",
|
||||
res_model: "res.partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "list"]],
|
||||
});
|
||||
|
||||
await contains(".o_debug_manager button").click();
|
||||
await contains(".dropdown-menu .dropdown-item:contains('Computed Arch')").click();
|
||||
expect(".modal").toHaveCount(1);
|
||||
expect(".modal-body").toHaveText(`<list><field name="name"/></list>`);
|
||||
});
|
||||
|
||||
test("can edit a pivot view", async () => {
|
||||
serverState.debug = "1";
|
||||
|
||||
webModels.ResPartner._views["pivot,18"] = "<pivot></pivot>";
|
||||
webModels.IrUiView._records.push({ id: 18, name: "Edit View" });
|
||||
webModels.IrUiView._views.form = `<form><field name="id"/></form>`;
|
||||
|
||||
defineWebModels();
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Partners",
|
||||
res_model: "res.partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "pivot"]],
|
||||
});
|
||||
await contains(".o_debug_manager button").click();
|
||||
await contains(".dropdown-menu .dropdown-item:contains('View: Pivot')").click();
|
||||
|
||||
expect(".breadcrumb-item").toHaveCount(1);
|
||||
expect(".o_breadcrumb .active").toHaveCount(1);
|
||||
expect(".o_breadcrumb .active").toHaveText("Edit View");
|
||||
expect(".o_field_widget[name=id]").toHaveText("18");
|
||||
await click(".breadcrumb .o_back_button");
|
||||
await animationFrame();
|
||||
expect(".o_breadcrumb .active").toHaveCount(1);
|
||||
expect(".o_breadcrumb .active").toHaveText("Partners");
|
||||
});
|
||||
|
||||
test("can edit a search view", async () => {
|
||||
serverState.debug = "1";
|
||||
|
||||
webModels.ResPartner._views.list = `<list><field name="id"/></list>`;
|
||||
webModels.ResPartner._views["search,293"] = "<search></search>";
|
||||
webModels.IrUiView._records.push({ id: 293, name: "Edit View" });
|
||||
webModels.IrUiView._views.form = `<form><field name="id"/></form>`;
|
||||
|
||||
defineWebModels();
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Partners",
|
||||
res_model: "res.partner",
|
||||
type: "ir.actions.act_window",
|
||||
search_view_id: [293, "some_search_view"],
|
||||
views: [[false, "list"]],
|
||||
});
|
||||
await contains(".o_debug_manager button").click();
|
||||
await contains(".dropdown-menu .dropdown-item:contains('SearchView')").click();
|
||||
expect(".breadcrumb-item").toHaveCount(1);
|
||||
expect(".o_breadcrumb .active").toHaveCount(1);
|
||||
expect(".o_breadcrumb .active").toHaveText("Edit View");
|
||||
expect(".o_field_widget[name=id]").toHaveText("293");
|
||||
});
|
||||
|
||||
test("edit search view on action without search_view_id", async () => {
|
||||
serverState.debug = "1";
|
||||
|
||||
webModels.ResPartner._views.list = `<list><field name="id"/></list>`;
|
||||
webModels.ResPartner._views["search,293"] = "<search></search>";
|
||||
webModels.IrUiView._records.push({ id: 293, name: "Edit View" });
|
||||
webModels.IrUiView._views.form = `<form><field name="id"/></form>`;
|
||||
|
||||
defineWebModels();
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Partners",
|
||||
res_model: "res.partner",
|
||||
type: "ir.actions.act_window",
|
||||
search_view_id: false,
|
||||
views: [[false, "list"]],
|
||||
});
|
||||
await contains(".o_debug_manager button").click();
|
||||
await contains(".dropdown-menu .dropdown-item:contains('SearchView')").click();
|
||||
expect(".breadcrumb-item").toHaveCount(1);
|
||||
expect(".o_breadcrumb .active").toHaveCount(1);
|
||||
expect(".o_breadcrumb .active").toHaveText("Edit View");
|
||||
expect(".o_field_widget[name=id]").toHaveText("293");
|
||||
});
|
||||
|
||||
test("cannot edit the control panel of a form view contained in a dialog without control panel.", async () => {
|
||||
serverState.debug = "1";
|
||||
|
||||
webModels.ResPartner._views.form = `<form><field name="id"/></form>`;
|
||||
|
||||
defineWebModels();
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Create a Partner",
|
||||
res_model: "res.partner",
|
||||
type: "ir.actions.act_window",
|
||||
target: "new",
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
|
||||
await contains(".o_dialog .o_debug_manager button").click();
|
||||
expect(".dropdown-menu .dropdown-item:contains('SearchView')").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("set defaults: basic rendering", async () => {
|
||||
serverState.debug = "1";
|
||||
|
||||
webModels.ResPartner._views["form,24"] = `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
</form>`;
|
||||
webModels.ResPartner._records.push({ id: 1000, name: "p1" });
|
||||
webModels.IrUiView._records.push({ id: 24 });
|
||||
|
||||
defineWebModels();
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Partners",
|
||||
res_model: "res.partner",
|
||||
res_id: 1000,
|
||||
type: "ir.actions.act_window",
|
||||
views: [[24, "form"]],
|
||||
});
|
||||
await contains(".o_debug_manager button").click();
|
||||
await contains(".dropdown-menu .dropdown-item:contains('Set Default Values')").click();
|
||||
expect(".modal").toHaveCount(1);
|
||||
expect(".modal select#formview_default_fields").toHaveCount(1);
|
||||
expect(".modal #formview_default_fields option").toHaveCount(2);
|
||||
expect(".modal #formview_default_fields option").toHaveCount(2);
|
||||
expect(".modal #formview_default_fields option:nth-child(1)").toHaveText("");
|
||||
expect(".modal #formview_default_fields option:nth-child(2)").toHaveText("Name = p1");
|
||||
});
|
||||
|
||||
test("set defaults: click close", async () => {
|
||||
serverState.debug = "1";
|
||||
|
||||
onRpc("ir.default", "set", async () => {
|
||||
throw new Error("should not create a default");
|
||||
});
|
||||
|
||||
webModels.ResPartner._views["form,25"] = `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
</form>`;
|
||||
webModels.ResPartner._records.push({ id: 1001, name: "p1" });
|
||||
webModels.IrUiView._records.push({ id: 25 });
|
||||
|
||||
defineWebModels();
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Partners",
|
||||
res_model: "res.partner",
|
||||
res_id: 1001,
|
||||
type: "ir.actions.act_window",
|
||||
views: [[25, "form"]],
|
||||
});
|
||||
await contains(".o_debug_manager button").click();
|
||||
await contains(".dropdown-menu .dropdown-item:contains('Set Default Values')").click();
|
||||
expect(".modal").toHaveCount(1);
|
||||
await contains(".modal .modal-footer button").click();
|
||||
expect(".modal").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("set defaults: select and save", async () => {
|
||||
expect.assertions(3);
|
||||
serverState.debug = "1";
|
||||
|
||||
onRpc("ir.default", "set", async (args) => {
|
||||
expect(args.args).toEqual(["res.partner", "name", "p1", true, true, false]);
|
||||
return true;
|
||||
});
|
||||
|
||||
webModels.ResPartner._views["form,26"] = `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
</form>`;
|
||||
webModels.ResPartner._records.push({ id: 1002, name: "p1" });
|
||||
webModels.IrUiView._records.push({ id: 26 });
|
||||
|
||||
defineWebModels();
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Partners",
|
||||
res_model: "res.partner",
|
||||
res_id: 1002,
|
||||
type: "ir.actions.act_window",
|
||||
views: [[26, "form"]],
|
||||
});
|
||||
await contains(".o_debug_manager button").click();
|
||||
await contains(".dropdown-menu .dropdown-item:contains('Set Default Values')").click();
|
||||
expect(".modal").toHaveCount(1);
|
||||
|
||||
await contains(".modal #formview_default_fields").select("name");
|
||||
await contains(".modal .modal-footer button:nth-child(2)").click();
|
||||
expect(".modal").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("fetch raw data: basic rendering", async () => {
|
||||
serverState.debug = "1";
|
||||
|
||||
class Custom extends models.Model {
|
||||
_name = "custom";
|
||||
|
||||
name = fields.Char();
|
||||
raw = fields.Binary();
|
||||
properties = fields.Properties({
|
||||
string: "Properties",
|
||||
definition_record: "product_id",
|
||||
definition_record_field: "definitions",
|
||||
});
|
||||
definitions = fields.PropertiesDefinition({
|
||||
string: "Definitions",
|
||||
});
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "custom1",
|
||||
raw: "<raw>",
|
||||
properties: [
|
||||
{
|
||||
name: "bd6404492c244cff",
|
||||
string: "test",
|
||||
type: "char",
|
||||
},
|
||||
],
|
||||
definitions: [{ name: "xphone_prop_1", string: "P1", type: "boolean" }],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineWebModels();
|
||||
defineModels([Custom]);
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Custom",
|
||||
res_model: "custom",
|
||||
res_id: 1,
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
await contains(".o_debug_manager button").click();
|
||||
await contains(".dropdown-menu .dropdown-item:contains(/^Data/)").click();
|
||||
expect(".modal").toHaveCount(1);
|
||||
const data = queryText(".modal-body pre");
|
||||
const modalObj = JSON.parse(data);
|
||||
expect(modalObj).toInclude("create_date");
|
||||
expect(modalObj).toInclude("write_date");
|
||||
expect(modalObj).not.toInclude("raw");
|
||||
const expectedObj = {
|
||||
display_name: "custom1",
|
||||
id: 1,
|
||||
name: "custom1",
|
||||
properties: false,
|
||||
definitions: [
|
||||
{
|
||||
name: "xphone_prop_1",
|
||||
string: "P1",
|
||||
type: "boolean",
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(modalObj).toMatchObject(expectedObj);
|
||||
});
|
||||
|
||||
test("view metadata: basic rendering", async () => {
|
||||
serverState.debug = "1";
|
||||
|
||||
onRpc("get_metadata", async () => {
|
||||
return [
|
||||
{
|
||||
create_date: "2023-01-26 14:12:10",
|
||||
create_uid: [4, "Some user"],
|
||||
id: 1003,
|
||||
noupdate: false,
|
||||
write_date: "2023-01-26 14:13:31",
|
||||
write_uid: [6, "Another User"],
|
||||
xmlid: "abc.partner_16",
|
||||
xmlids: [{ xmlid: "abc.partner_16", noupdate: false }],
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
webModels.ResPartner._records.push({ id: 1003, name: "p1" });
|
||||
|
||||
defineWebModels();
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Partner",
|
||||
res_model: "res.partner",
|
||||
res_id: 1003,
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
await contains(".o_debug_manager button").click();
|
||||
await contains(".dropdown-menu .dropdown-item:contains('Metadata')").click();
|
||||
expect(".modal").toHaveCount(1);
|
||||
const contentModal = queryAll(".modal-body table tr th, .modal-body table tr td");
|
||||
expect(queryAllTexts(contentModal)).toEqual([
|
||||
"ID:",
|
||||
"1003",
|
||||
"XML ID:",
|
||||
"abc.partner_16",
|
||||
"No Update:",
|
||||
"false (change)",
|
||||
"Creation User:",
|
||||
"Some user",
|
||||
"Creation Date:",
|
||||
"01/26/2023 15:12:10",
|
||||
"Latest Modification by:",
|
||||
"Another User",
|
||||
"Latest Modification Date:",
|
||||
"01/26/2023 15:13:31",
|
||||
]);
|
||||
});
|
||||
|
||||
test("set defaults: setting default value for datetime field", async () => {
|
||||
serverState.debug = "1";
|
||||
|
||||
const argSteps = [];
|
||||
|
||||
onRpc("ir.default", "set", async (args) => {
|
||||
argSteps.push(args.args);
|
||||
return true;
|
||||
});
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "partner";
|
||||
|
||||
datetime = fields.Datetime();
|
||||
reference = fields.Reference({ selection: [["pony", "Pony"]] });
|
||||
m2o = fields.Many2one({ relation: "pony" });
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "p1",
|
||||
datetime: "2024-01-24 16:46:16",
|
||||
reference: "pony,1",
|
||||
m2o: 1,
|
||||
},
|
||||
];
|
||||
|
||||
_views = {
|
||||
"form,18": /* xml */ `
|
||||
<form>
|
||||
<field name="datetime"/>
|
||||
<field name="reference"/>
|
||||
<field name="m2o"/>
|
||||
</form>
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
class Pony extends models.Model {
|
||||
_name = "pony";
|
||||
|
||||
_records = [{ id: 1 }];
|
||||
}
|
||||
|
||||
class IrUiView extends models.Model {
|
||||
_name = "ir.ui.view";
|
||||
|
||||
name = fields.Char();
|
||||
model = fields.Char();
|
||||
|
||||
_records = [{ id: 18 }];
|
||||
}
|
||||
|
||||
defineModels([Partner, Pony, IrUiView]);
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
for (const field_name of ["datetime", "reference", "m2o"]) {
|
||||
await getService("action").doAction({
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
res_id: 1,
|
||||
type: "ir.actions.act_window",
|
||||
views: [[18, "form"]],
|
||||
});
|
||||
await contains(".o_debug_manager button").click();
|
||||
await contains(".dropdown-menu .dropdown-item:contains('Set Default Values')").click();
|
||||
expect(".modal").toHaveCount(1);
|
||||
|
||||
await contains(".modal #formview_default_fields").select(field_name);
|
||||
await contains(".modal .modal-footer button:nth-child(2)").click();
|
||||
expect(".modal").toHaveCount(0);
|
||||
}
|
||||
|
||||
expect(argSteps).toEqual([
|
||||
["partner", "datetime", "2024-01-24 16:46:16", true, true, false],
|
||||
[
|
||||
"partner",
|
||||
"reference",
|
||||
{ resId: 1, resModel: "pony", displayName: "pony,1" },
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
],
|
||||
["partner", "m2o", 1, true, true, false],
|
||||
]);
|
||||
});
|
||||
|
||||
test("display model view in developer tools", async () => {
|
||||
serverState.debug = "1";
|
||||
webModels.ResPartner._views.form = `<form><field name="name"/></form>`;
|
||||
webModels.ResPartner._records.push({ id: 88, name: "p1" });
|
||||
webModels.IrModel._views.form = `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name="model"/>
|
||||
</form>`;
|
||||
|
||||
defineWebModels();
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction({
|
||||
name: "Partners",
|
||||
res_model: "res.partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
|
||||
await contains(".o_debug_manager button").click();
|
||||
await contains(".dropdown-menu .dropdown-item:contains('Model:')").click();
|
||||
|
||||
expect(".breadcrumb-item").toHaveCount(1);
|
||||
expect(".o_breadcrumb .active").toHaveCount(1);
|
||||
expect(".o_breadcrumb .active").toHaveText("Partner");
|
||||
});
|
||||
|
||||
test("set defaults: settings default value with a very long value", async () => {
|
||||
serverState.debug = "1";
|
||||
|
||||
const fooValue = "12".repeat(250);
|
||||
const argSteps = [];
|
||||
|
||||
onRpc("ir.default", "set", async (args) => {
|
||||
argSteps.push(args.args);
|
||||
return true;
|
||||
});
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "partner";
|
||||
|
||||
foo = fields.Char();
|
||||
description = fields.Html();
|
||||
bar = fields.Many2one({ relation: "ir.ui.view" });
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "p1",
|
||||
foo: fooValue,
|
||||
description: fooValue,
|
||||
bar: 18,
|
||||
},
|
||||
];
|
||||
|
||||
_views = {
|
||||
form: `
|
||||
<form>
|
||||
<field name="foo"/>
|
||||
<field name="description"/>
|
||||
<field name="bar" invisible="1"/>
|
||||
</form>`,
|
||||
};
|
||||
}
|
||||
|
||||
class IrUiView extends models.Model {
|
||||
_name = "ir.ui.view";
|
||||
|
||||
name = fields.Char();
|
||||
model = fields.Char();
|
||||
|
||||
_records = [{ id: 18 }];
|
||||
}
|
||||
|
||||
defineModels([Partner, IrUiView]);
|
||||
|
||||
await mountWithCleanup(WebClient);
|
||||
|
||||
await getService("action").doAction({
|
||||
name: "Partners",
|
||||
res_model: "partner",
|
||||
res_id: 1,
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
await contains(".o_debug_manager button").click();
|
||||
await contains(".dropdown-menu .dropdown-item:contains('Set Default Values')").click();
|
||||
expect(".modal").toHaveCount(1);
|
||||
|
||||
expect(queryAllTexts`.modal #formview_default_fields option`).toEqual([
|
||||
"",
|
||||
"Foo = 121212121212121212121212121212121212121212121212121212121...",
|
||||
"Description = 121212121212121212121212121212121212121212121212121212121...",
|
||||
]);
|
||||
|
||||
expect(queryAllProperties(".modal #formview_default_fields option", "value")).toEqual([
|
||||
"",
|
||||
"foo",
|
||||
"description",
|
||||
]);
|
||||
|
||||
await contains(".modal #formview_default_fields").select("foo");
|
||||
await contains(".modal .modal-footer button:nth-child(2)").click();
|
||||
expect(".modal").toHaveCount(0);
|
||||
expect(argSteps).toEqual([["partner", "foo", fooValue, true, true, false]]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { test, expect, beforeEach } from "@odoo/hoot";
|
||||
import { runAllTimers } from "@odoo/hoot-mock";
|
||||
import { fields, models, defineModels, mountView } from "@web/../tests/web_test_helpers";
|
||||
|
||||
beforeEach(() => {
|
||||
const qweb = JSON.stringify([
|
||||
{
|
||||
exec_context: [],
|
||||
results: {
|
||||
archs: {
|
||||
1: `<t name="Test1" t-name="test">
|
||||
<t t-call-assets="web.assets_tests"/>
|
||||
</t>`,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
delay: 0.1,
|
||||
directive: 't-call-assets="web.assets_tests"',
|
||||
query: 9,
|
||||
view_id: 1,
|
||||
xpath: "/t/t",
|
||||
},
|
||||
],
|
||||
},
|
||||
stack: [],
|
||||
start: 42,
|
||||
},
|
||||
]);
|
||||
|
||||
class Custom extends models.Model {
|
||||
_name = "custom";
|
||||
|
||||
qweb = fields.Text();
|
||||
|
||||
_records = [{ qweb }];
|
||||
}
|
||||
|
||||
class View extends models.Model {
|
||||
_name = "ir.ui.view";
|
||||
|
||||
name = fields.Char();
|
||||
model = fields.Char();
|
||||
type = fields.Char();
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "formView",
|
||||
model: "custom",
|
||||
type: "form",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Custom, View]);
|
||||
});
|
||||
|
||||
test("profiling qweb view field renders delay and query", async function (assert) {
|
||||
await mountView({
|
||||
resModel: "custom",
|
||||
type: "form",
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="qweb" widget="profiling_qweb_view"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await runAllTimers();
|
||||
|
||||
expect("[name='qweb'] .ace_gutter .ace_gutter-cell").toHaveCount(3);
|
||||
expect("[name='qweb'] .ace_gutter .ace_gutter-cell .o_info").toHaveCount(1);
|
||||
expect("[name='qweb'] .ace_gutter .ace_gutter-cell .o_info .o_delay").toHaveText("0.1");
|
||||
expect("[name='qweb'] .ace_gutter .ace_gutter-cell .o_info .o_query").toHaveText("9");
|
||||
expect("[name='qweb'] .o_select_view_profiling .o_delay").toHaveText("0.1 ms");
|
||||
expect("[name='qweb'] .o_select_view_profiling .o_query").toHaveText("9 query");
|
||||
});
|
||||
427
odoo-bringout-oca-ocb-web/web/static/tests/core/dialog.test.js
Normal file
427
odoo-bringout-oca-ocb-web/web/static/tests/core/dialog.test.js
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
import { destroy, expect, test } from "@odoo/hoot";
|
||||
import { keyDown, keyUp, press, queryAllTexts, queryOne, resize } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, onMounted, useState, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
getService,
|
||||
makeDialogMockEnv,
|
||||
mountWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
test("simple rendering", async () => {
|
||||
expect.assertions(8);
|
||||
class Parent extends Component {
|
||||
static components = { Dialog };
|
||||
static template = xml`
|
||||
<Dialog title="'Wow(l) Effect'">
|
||||
Hello!
|
||||
</Dialog>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
await makeDialogMockEnv();
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect(".o_dialog header .modal-title").toHaveCount(1, {
|
||||
message: "the header is rendered by default",
|
||||
});
|
||||
expect("header .modal-title").toHaveText("Wow(l) Effect");
|
||||
expect(".o_dialog main").toHaveCount(1, { message: "a dialog has always a main node" });
|
||||
expect("main").toHaveText("Hello!");
|
||||
expect(".o_dialog footer").toHaveCount(1, { message: "the footer is rendered by default" });
|
||||
expect(".o_dialog footer button").toHaveCount(1, {
|
||||
message: "the footer is rendered with a single button 'Ok' by default",
|
||||
});
|
||||
expect("footer button").toHaveText("Ok");
|
||||
});
|
||||
|
||||
test("hotkeys work on dialogs", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { Dialog };
|
||||
static template = xml`
|
||||
<Dialog title="'Wow(l) Effect'">
|
||||
Hello!
|
||||
</Dialog>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
await makeDialogMockEnv({
|
||||
dialogData: {
|
||||
close: () => expect.step("close"),
|
||||
dismiss: () => expect.step("dismiss"),
|
||||
},
|
||||
});
|
||||
await mountWithCleanup(Parent);
|
||||
expect("header .modal-title").toHaveText("Wow(l) Effect");
|
||||
expect("footer button").toHaveText("Ok");
|
||||
// Same effect as clicking on the x button
|
||||
await press("escape");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["dismiss", "close"]);
|
||||
// Same effect as clicking on the Ok button
|
||||
await keyDown("control+enter");
|
||||
await keyUp("ctrl+enter");
|
||||
expect.verifySteps(["close"]);
|
||||
});
|
||||
|
||||
test("simple rendering with two dialogs", async () => {
|
||||
expect.assertions(3);
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Dialog title="'First Title'">
|
||||
Hello!
|
||||
</Dialog>
|
||||
<Dialog title="'Second Title'">
|
||||
Hello again!
|
||||
</Dialog>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
static components = { Dialog };
|
||||
}
|
||||
await makeDialogMockEnv();
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_dialog").toHaveCount(2);
|
||||
expect(queryAllTexts("header .modal-title")).toEqual(["First Title", "Second Title"]);
|
||||
expect(queryAllTexts(".o_dialog .modal-body")).toEqual(["Hello!", "Hello again!"]);
|
||||
});
|
||||
|
||||
test("click on the button x triggers the service close", async () => {
|
||||
expect.assertions(2);
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Dialog>
|
||||
Hello!
|
||||
</Dialog>
|
||||
`;
|
||||
static props = ["*"];
|
||||
static components = { Dialog };
|
||||
}
|
||||
await makeDialogMockEnv({
|
||||
dialogData: {
|
||||
close: () => expect.step("close"),
|
||||
dismiss: () => expect.step("dismiss"),
|
||||
},
|
||||
});
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
await contains(".o_dialog header button[aria-label='Close']").click();
|
||||
expect.verifySteps(["dismiss", "close"]);
|
||||
});
|
||||
|
||||
test("click on the button x triggers the close and dismiss defined by a Child component", async () => {
|
||||
expect.assertions(2);
|
||||
class Child extends Component {
|
||||
static template = xml`<div>Hello</div>`;
|
||||
static props = ["*"];
|
||||
|
||||
setup() {
|
||||
this.env.dialogData.close = () => expect.step("close");
|
||||
this.env.dialogData.dismiss = () => expect.step("dismiss");
|
||||
this.env.dialogData.scrollToOrigin = () => {};
|
||||
}
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Dialog>
|
||||
<Child/>
|
||||
</Dialog>
|
||||
`;
|
||||
static props = ["*"];
|
||||
static components = { Child, Dialog };
|
||||
}
|
||||
await makeDialogMockEnv();
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
|
||||
await contains(".o_dialog header button[aria-label='Close']").click();
|
||||
expect.verifySteps(["dismiss", "close"]);
|
||||
});
|
||||
|
||||
test("click on the default footer button triggers the service close", async () => {
|
||||
expect.assertions(2);
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Dialog>
|
||||
Hello!
|
||||
</Dialog>
|
||||
`;
|
||||
static props = ["*"];
|
||||
static components = { Dialog };
|
||||
}
|
||||
await makeDialogMockEnv({
|
||||
dialogData: {
|
||||
close: () => expect.step("close"),
|
||||
dismiss: () => expect.step("dismiss"),
|
||||
},
|
||||
});
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
|
||||
await contains(".o_dialog footer button").click();
|
||||
expect.verifySteps(["close"]);
|
||||
});
|
||||
|
||||
test("render custom footer buttons is possible", async () => {
|
||||
expect.assertions(2);
|
||||
class SimpleButtonsDialog extends Component {
|
||||
static components = { Dialog };
|
||||
static template = xml`
|
||||
<Dialog>
|
||||
content
|
||||
<t t-set-slot="footer">
|
||||
<div>
|
||||
<button class="btn btn-primary">The First Button</button>
|
||||
<button class="btn btn-primary">The Second Button</button>
|
||||
</div>
|
||||
</t>
|
||||
</Dialog>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<SimpleButtonsDialog/>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
static components = { SimpleButtonsDialog };
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = useState({
|
||||
displayDialog: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
await makeDialogMockEnv();
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect(".o_dialog footer button").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("embed an arbitrary component in a dialog is possible", async () => {
|
||||
expect.assertions(4);
|
||||
class SubComponent extends Component {
|
||||
static template = xml`
|
||||
<div class="o_subcomponent" t-esc="props.text" t-on-click="_onClick"/>
|
||||
`;
|
||||
static props = ["*"];
|
||||
_onClick() {
|
||||
expect.step("subcomponent-clicked");
|
||||
this.props.onClicked();
|
||||
}
|
||||
}
|
||||
class Parent extends Component {
|
||||
static components = { Dialog, SubComponent };
|
||||
static template = xml`
|
||||
<Dialog>
|
||||
<SubComponent text="'Wow(l) Effect'" onClicked="_onSubcomponentClicked"/>
|
||||
</Dialog>
|
||||
`;
|
||||
static props = ["*"];
|
||||
_onSubcomponentClicked() {
|
||||
expect.step("message received by parent");
|
||||
}
|
||||
}
|
||||
await makeDialogMockEnv();
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect(".o_dialog main .o_subcomponent").toHaveCount(1);
|
||||
expect(".o_subcomponent").toHaveText("Wow(l) Effect");
|
||||
await contains(".o_subcomponent").click();
|
||||
expect.verifySteps(["subcomponent-clicked", "message received by parent"]);
|
||||
});
|
||||
|
||||
test("dialog without header/footer", async () => {
|
||||
expect.assertions(4);
|
||||
class Parent extends Component {
|
||||
static components = { Dialog };
|
||||
static template = xml`
|
||||
<Dialog header="false" footer="false">content</Dialog>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
await makeDialogMockEnv();
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect(".o_dialog header").toHaveCount(0);
|
||||
expect("main").toHaveCount(1, { message: "a dialog has always a main node" });
|
||||
expect(".o_dialog footer").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("dialog size can be chosen", async () => {
|
||||
expect.assertions(5);
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<Dialog contentClass="'xl'" size="'xl'">content</Dialog>
|
||||
<Dialog contentClass="'lg'">content</Dialog>
|
||||
<Dialog contentClass="'md'" size="'md'">content</Dialog>
|
||||
<Dialog contentClass="'sm'" size="'sm'">content</Dialog>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
static components = { Dialog };
|
||||
}
|
||||
await makeDialogMockEnv();
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_dialog").toHaveCount(4);
|
||||
expect(".o_dialog .modal-dialog.modal-xl .xl").toHaveCount(1);
|
||||
expect(".o_dialog .modal-dialog.modal-lg .lg").toHaveCount(1);
|
||||
expect(".o_dialog .modal-dialog.modal-md .md").toHaveCount(1);
|
||||
expect(".o_dialog .modal-dialog.modal-sm .sm").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("dialog can be rendered on fullscreen", async () => {
|
||||
expect.assertions(2);
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Dialog fullscreen="true">content</Dialog>
|
||||
`;
|
||||
static props = ["*"];
|
||||
static components = { Dialog };
|
||||
}
|
||||
await makeDialogMockEnv();
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect(".o_dialog .modal").toHaveClass("o_modal_full");
|
||||
});
|
||||
|
||||
test("can be the UI active element", async () => {
|
||||
expect.assertions(4);
|
||||
class Parent extends Component {
|
||||
static template = xml`<Dialog>content</Dialog>`;
|
||||
static props = ["*"];
|
||||
static components = { Dialog };
|
||||
setup() {
|
||||
this.ui = useService("ui");
|
||||
expect(this.ui.activeElement).toBe(document, {
|
||||
message:
|
||||
"UI active element should be the default (document) as Parent is not mounted yet",
|
||||
});
|
||||
onMounted(() => {
|
||||
expect(".modal").toHaveCount(1);
|
||||
expect(this.ui.activeElement).toBe(
|
||||
queryOne(".modal", { message: "UI active element should be the dialog modal" })
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
await makeDialogMockEnv();
|
||||
const parent = await mountWithCleanup(Parent);
|
||||
destroy(parent);
|
||||
await Promise.resolve();
|
||||
expect(getService("ui").activeElement).toBe(document, {
|
||||
message: "UI owner should be reset to the default (document)",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("dialog can't be moved on small screen", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<Dialog>content</Dialog>`;
|
||||
static components = { Dialog };
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await makeDialogMockEnv();
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
expect(".modal-content").toHaveStyle({
|
||||
top: "0px",
|
||||
left: "0px",
|
||||
});
|
||||
|
||||
const header = queryOne(".modal-header");
|
||||
const headerRect = header.getBoundingClientRect();
|
||||
|
||||
// Even if the `dragAndDrop` is called, confirms that there are no effects
|
||||
await contains(header).dragAndDrop(".modal-content", {
|
||||
position: {
|
||||
// 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,
|
||||
},
|
||||
});
|
||||
|
||||
expect(".modal-content").toHaveStyle({
|
||||
top: "0px",
|
||||
left: "0px",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("dialog can be moved", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<Dialog>content</Dialog>`;
|
||||
static props = ["*"];
|
||||
static components = { Dialog };
|
||||
}
|
||||
await makeDialogMockEnv();
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".modal-content").toHaveStyle({
|
||||
left: "0px",
|
||||
top: "0px",
|
||||
});
|
||||
|
||||
const modalRect = queryOne(".modal").getBoundingClientRect();
|
||||
const header = queryOne(".modal-header");
|
||||
const headerRect = header.getBoundingClientRect();
|
||||
await contains(header).dragAndDrop(".modal-content", {
|
||||
position: {
|
||||
// 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,
|
||||
},
|
||||
});
|
||||
expect(".modal-content").toHaveStyle({
|
||||
left: `${modalRect.y + 20}px`,
|
||||
top: `${modalRect.x + 50}px`,
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("dialog's position is reset on resize", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<Dialog>content</Dialog>`;
|
||||
static props = ["*"];
|
||||
static components = { Dialog };
|
||||
}
|
||||
await makeDialogMockEnv();
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".modal-content").toHaveStyle({
|
||||
left: "0px",
|
||||
top: "0px",
|
||||
});
|
||||
|
||||
const modalRect = queryOne(".modal").getBoundingClientRect();
|
||||
const header = queryOne(".modal-header");
|
||||
const headerRect = header.getBoundingClientRect();
|
||||
await contains(header).dragAndDrop(".modal-content", {
|
||||
position: {
|
||||
// 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,
|
||||
},
|
||||
});
|
||||
expect(".modal-content").toHaveStyle({
|
||||
left: `${modalRect.y + 20}px`,
|
||||
top: `${modalRect.x + 50}px`,
|
||||
});
|
||||
|
||||
await resize();
|
||||
await animationFrame();
|
||||
expect(".modal-content").toHaveStyle({
|
||||
left: "0px",
|
||||
top: "0px",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
import { test, expect, beforeEach, describe } from "@odoo/hoot";
|
||||
import { click, press, queryAll, queryAllTexts, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { getService, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { useAutofocus } from "@web/core/utils/hooks";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
beforeEach(async () => {
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
});
|
||||
|
||||
test("Simple rendering with a single dialog", async () => {
|
||||
class CustomDialog extends Component {
|
||||
static components = { Dialog };
|
||||
static template = xml`<Dialog title="'Welcome'">content</Dialog>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
getService("dialog").add(CustomDialog);
|
||||
await animationFrame();
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect("header .modal-title").toHaveText("Welcome");
|
||||
await click(".o_dialog footer button");
|
||||
await animationFrame();
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Simple rendering and close a single dialog", async () => {
|
||||
class CustomDialog extends Component {
|
||||
static components = { Dialog };
|
||||
static template = xml`<Dialog title="'Welcome'">content</Dialog>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
const removeDialog = getService("dialog").add(CustomDialog);
|
||||
await animationFrame();
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect("header .modal-title").toHaveText("Welcome");
|
||||
|
||||
removeDialog();
|
||||
await animationFrame();
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
|
||||
// Call a second time, the close on the dialog.
|
||||
// As the dialog is already close, this call is just ignored. No error should be raised.
|
||||
removeDialog();
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("rendering with two dialogs", async () => {
|
||||
class CustomDialog extends Component {
|
||||
static components = { Dialog };
|
||||
static template = xml`<Dialog title="props.title">content</Dialog>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
getService("dialog").add(CustomDialog, { title: "Hello" });
|
||||
await animationFrame();
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect("header .modal-title").toHaveText("Hello");
|
||||
|
||||
getService("dialog").add(CustomDialog, { title: "Sauron" });
|
||||
await animationFrame();
|
||||
expect(".o_dialog").toHaveCount(2);
|
||||
expect(queryAllTexts("header .modal-title")).toEqual(["Hello", "Sauron"]);
|
||||
await click(".o_dialog footer button");
|
||||
await animationFrame();
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect("header .modal-title").toHaveText("Sauron");
|
||||
});
|
||||
|
||||
test("multiple dialogs can become the UI active element", async () => {
|
||||
class CustomDialog extends Component {
|
||||
static components = { Dialog };
|
||||
static template = xml`<Dialog title="props.title">content</Dialog>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
getService("dialog").add(CustomDialog, { title: "Hello" });
|
||||
await animationFrame();
|
||||
expect(queryOne(".o_dialog:not(.o_inactive_modal) .modal")).toBe(
|
||||
getService("ui").activeElement
|
||||
);
|
||||
|
||||
getService("dialog").add(CustomDialog, { title: "Sauron" });
|
||||
await animationFrame();
|
||||
expect(queryOne(".o_dialog:not(.o_inactive_modal) .modal")).toBe(
|
||||
getService("ui").activeElement
|
||||
);
|
||||
|
||||
getService("dialog").add(CustomDialog, { title: "Rafiki" });
|
||||
await animationFrame();
|
||||
expect(queryOne(".o_dialog:not(.o_inactive_modal) .modal")).toBe(
|
||||
getService("ui").activeElement
|
||||
);
|
||||
});
|
||||
|
||||
test("a popover with an autofocus child can become the UI active element", async () => {
|
||||
class TestPopover extends Component {
|
||||
static template = xml`<input type="text" t-ref="autofocus" />`;
|
||||
static props = ["*"];
|
||||
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>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.popover = usePopover(TestPopover);
|
||||
}
|
||||
showPopover(event) {
|
||||
this.popover.open(event.target, {});
|
||||
}
|
||||
}
|
||||
|
||||
expect(document).toBe(getService("ui").activeElement);
|
||||
expect(document.body).toBeFocused();
|
||||
|
||||
getService("dialog").add(CustomDialog, { title: "Hello" });
|
||||
await animationFrame();
|
||||
expect(queryOne(".o_dialog:not(.o_inactive_modal) .modal")).toBe(
|
||||
getService("ui").activeElement
|
||||
);
|
||||
expect(".btn.test").toBeFocused();
|
||||
|
||||
await click(".btn.test");
|
||||
await animationFrame();
|
||||
expect(queryOne(".o_popover")).toBe(getService("ui").activeElement);
|
||||
expect(".o_popover input").toBeFocused();
|
||||
});
|
||||
|
||||
test("Interactions between multiple dialogs", async () => {
|
||||
function activity(modals) {
|
||||
const active = [];
|
||||
const names = [];
|
||||
for (let i = 0; i < modals.length; i++) {
|
||||
active[i] = !modals[i].classList.contains("o_inactive_modal");
|
||||
names[i] = modals[i].querySelector(".modal-title").textContent;
|
||||
}
|
||||
return { active, names };
|
||||
}
|
||||
|
||||
class CustomDialog extends Component {
|
||||
static components = { Dialog };
|
||||
static template = xml`<Dialog title="props.title">content</Dialog>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
getService("dialog").add(CustomDialog, { title: "Hello" });
|
||||
await animationFrame();
|
||||
getService("dialog").add(CustomDialog, { title: "Sauron" });
|
||||
await animationFrame();
|
||||
getService("dialog").add(CustomDialog, { title: "Rafiki" });
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_dialog").toHaveCount(3);
|
||||
let res = activity(queryAll(".o_dialog"));
|
||||
expect(res.active).toEqual([false, false, true]);
|
||||
expect(res.names).toEqual(["Hello", "Sauron", "Rafiki"]);
|
||||
|
||||
await press("Escape", { bubbles: true });
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_dialog").toHaveCount(2);
|
||||
res = activity(queryAll(".o_dialog"));
|
||||
expect(res.active).toEqual([false, true]);
|
||||
expect(res.names).toEqual(["Hello", "Sauron"]);
|
||||
|
||||
await click(".o_dialog:not(.o_inactive_modal) footer button");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
res = activity(queryAll(".o_dialog"));
|
||||
expect(res.active).toEqual([true]);
|
||||
expect(res.names).toEqual(["Hello"]);
|
||||
|
||||
await click("footer button");
|
||||
await animationFrame();
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("dialog component crashes", async () => {
|
||||
expect.errors(1);
|
||||
|
||||
class FailingDialog extends Component {
|
||||
static components = { Dialog };
|
||||
static template = xml`<Dialog title="'Error'">content</Dialog>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
throw new Error("Some Error");
|
||||
}
|
||||
}
|
||||
|
||||
getService("dialog").add(FailingDialog);
|
||||
await animationFrame();
|
||||
|
||||
expect(".modal .o_error_dialog").toHaveCount(1);
|
||||
expect.verifyErrors(["Error: Some Error"]);
|
||||
});
|
||||
589
odoo-bringout-oca-ocb-web/web/static/tests/core/domain.test.js
Normal file
589
odoo-bringout-oca-ocb-web/web/static/tests/core/domain.test.js
Normal file
|
|
@ -0,0 +1,589 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
|
||||
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { PyDate } from "@web/core/py_js/py_date";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Basic properties
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
describe("Basic Properties", () => {
|
||||
test("empty", () => {
|
||||
expect(new Domain([]).contains({})).toBe(true);
|
||||
expect(new Domain([]).toString()).toBe("[]");
|
||||
expect(new Domain([]).toList()).toEqual([]);
|
||||
});
|
||||
|
||||
test("undefined domain", () => {
|
||||
expect(new Domain(undefined).contains({})).toBe(true);
|
||||
expect(new Domain(undefined).toString()).toBe("[]");
|
||||
expect(new Domain(undefined).toList()).toEqual([]);
|
||||
});
|
||||
|
||||
test("simple condition", () => {
|
||||
expect(new Domain([["a", "=", 3]]).contains({ a: 3 })).toBe(true);
|
||||
expect(new Domain([["a", "=", 3]]).contains({ a: 5 })).toBe(false);
|
||||
expect(new Domain([["a", "=", 3]]).toString()).toBe(`[("a", "=", 3)]`);
|
||||
expect(new Domain([["a", "=", 3]]).toList()).toEqual([["a", "=", 3]]);
|
||||
});
|
||||
|
||||
test("can be created from domain", () => {
|
||||
const domain = new Domain([["a", "=", 3]]);
|
||||
expect(new Domain(domain).toString()).toBe(`[("a", "=", 3)]`);
|
||||
});
|
||||
|
||||
test("basic", () => {
|
||||
const record = {
|
||||
a: 3,
|
||||
group_method: "line",
|
||||
select1: "day",
|
||||
rrule_type: "monthly",
|
||||
};
|
||||
expect(new Domain([["a", "=", 3]]).contains(record)).toBe(true);
|
||||
expect(new Domain([["a", "=", 5]]).contains(record)).toBe(false);
|
||||
expect(new Domain([["group_method", "!=", "count"]]).contains(record)).toBe(true);
|
||||
expect(
|
||||
new Domain([
|
||||
["select1", "=", "day"],
|
||||
["rrule_type", "=", "monthly"],
|
||||
]).contains(record)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("support of '=?' operator", () => {
|
||||
const record = { a: 3 };
|
||||
expect(new Domain([["a", "=?", null]]).contains(record)).toBe(true);
|
||||
expect(new Domain([["a", "=?", false]]).contains(record)).toBe(true);
|
||||
expect(new Domain(["!", ["a", "=?", false]]).contains(record)).toBe(false);
|
||||
expect(new Domain([["a", "=?", 1]]).contains(record)).toBe(false);
|
||||
expect(new Domain([["a", "=?", 3]]).contains(record)).toBe(true);
|
||||
expect(new Domain(["!", ["a", "=?", 3]]).contains(record)).toBe(false);
|
||||
});
|
||||
|
||||
test("or", () => {
|
||||
const currentDomain = [
|
||||
"|",
|
||||
["section_id", "=", 42],
|
||||
"|",
|
||||
["user_id", "=", 3],
|
||||
["member_ids", "in", [3]],
|
||||
];
|
||||
const record = {
|
||||
section_id: null,
|
||||
user_id: null,
|
||||
member_ids: null,
|
||||
};
|
||||
expect(new Domain(currentDomain).contains({ ...record, section_id: 42 })).toBe(true);
|
||||
expect(new Domain(currentDomain).contains({ ...record, user_id: 3 })).toBe(true);
|
||||
expect(new Domain(currentDomain).contains({ ...record, member_ids: 3 })).toBe(true);
|
||||
});
|
||||
|
||||
test("and", () => {
|
||||
const domain = new Domain(["&", "&", ["a", "=", 1], ["b", "=", 2], ["c", "=", 3]]);
|
||||
|
||||
expect(domain.contains({ a: 1, b: 2, c: 3 })).toBe(true);
|
||||
expect(domain.contains({ a: -1, b: 2, c: 3 })).toBe(false);
|
||||
expect(domain.contains({ a: 1, b: -1, c: 3 })).toBe(false);
|
||||
expect(domain.contains({ a: 1, b: 2, c: -1 })).toBe(false);
|
||||
});
|
||||
|
||||
test("not", () => {
|
||||
const record = {
|
||||
a: 5,
|
||||
group_method: "line",
|
||||
};
|
||||
expect(new Domain(["!", ["a", "=", 3]]).contains(record)).toBe(true);
|
||||
expect(new Domain(["!", ["group_method", "=", "count"]]).contains(record)).toBe(true);
|
||||
});
|
||||
|
||||
test("like, =like, ilike, =ilike, not like and not ilike", () => {
|
||||
expect.assertions(28);
|
||||
|
||||
expect(new Domain([["a", "like", "value"]]).contains({ a: "value" })).toBe(true);
|
||||
expect(new Domain([["a", "like", "value"]]).contains({ a: "some value" })).toBe(true);
|
||||
expect(new Domain([["a", "like", "value"]]).contains({ a: "Some Value" })).not.toBe(true);
|
||||
expect(new Domain([["a", "like", "value"]]).contains({ a: false })).toBe(false);
|
||||
|
||||
expect(new Domain([["a", "=like", "%value"]]).contains({ a: "value" })).toBe(true);
|
||||
expect(new Domain([["a", "=like", "%value"]]).contains({ a: "some value" })).toBe(true);
|
||||
expect(new Domain([["a", "=like", "%value"]]).contains({ a: "Some Value" })).not.toBe(true);
|
||||
expect(new Domain([["a", "=like", "%value"]]).contains({ a: false })).toBe(false);
|
||||
|
||||
expect(new Domain([["a", "ilike", "value"]]).contains({ a: "value" })).toBe(true);
|
||||
expect(new Domain([["a", "ilike", "value"]]).contains({ a: "some value" })).toBe(true);
|
||||
expect(new Domain([["a", "ilike", "value"]]).contains({ a: "Some Value" })).toBe(true);
|
||||
expect(new Domain([["a", "ilike", "value"]]).contains({ a: false })).toBe(false);
|
||||
|
||||
expect(new Domain([["a", "=ilike", "%value"]]).contains({ a: "value" })).toBe(true);
|
||||
expect(new Domain([["a", "=ilike", "%value"]]).contains({ a: "some value" })).toBe(true);
|
||||
expect(new Domain([["a", "=ilike", "%value"]]).contains({ a: "Some Value" })).toBe(true);
|
||||
expect(new Domain([["a", "=ilike", "%value"]]).contains({ a: false })).toBe(false);
|
||||
|
||||
expect(new Domain([["a", "not like", "value"]]).contains({ a: "value" })).not.toBe(true);
|
||||
expect(new Domain([["a", "not like", "value"]]).contains({ a: "some value" })).not.toBe(
|
||||
true
|
||||
);
|
||||
expect(new Domain([["a", "not like", "value"]]).contains({ a: "Some Value" })).toBe(true);
|
||||
expect(new Domain([["a", "not like", "value"]]).contains({ a: "something" })).toBe(true);
|
||||
expect(new Domain([["a", "not like", "value"]]).contains({ a: "Something" })).toBe(true);
|
||||
expect(new Domain([["a", "not like", "value"]]).contains({ a: false })).toBe(false);
|
||||
|
||||
expect(new Domain([["a", "not ilike", "value"]]).contains({ a: "value" })).not.toBe(true);
|
||||
expect(new Domain([["a", "not ilike", "value"]]).contains({ a: "some value" })).toBe(false);
|
||||
expect(new Domain([["a", "not ilike", "value"]]).contains({ a: "Some Value" })).toBe(false);
|
||||
expect(new Domain([["a", "not ilike", "value"]]).contains({ a: "something" })).toBe(true);
|
||||
expect(new Domain([["a", "not ilike", "value"]]).contains({ a: "Something" })).toBe(true);
|
||||
expect(new Domain([["a", "not ilike", "value"]]).contains({ a: false })).toBe(false);
|
||||
});
|
||||
|
||||
test("complex domain", () => {
|
||||
const domain = new Domain(["&", "!", ["a", "=", 1], "|", ["a", "=", 2], ["a", "=", 3]]);
|
||||
|
||||
expect(domain.contains({ a: 1 })).toBe(false);
|
||||
expect(domain.contains({ a: 2 })).toBe(true);
|
||||
expect(domain.contains({ a: 3 })).toBe(true);
|
||||
expect(domain.contains({ a: 4 })).toBe(false);
|
||||
});
|
||||
|
||||
test("toList", () => {
|
||||
expect(new Domain([]).toList()).toEqual([]);
|
||||
expect(new Domain([["a", "=", 3]]).toList()).toEqual([["a", "=", 3]]);
|
||||
expect(
|
||||
new Domain([
|
||||
["a", "=", 3],
|
||||
["b", "!=", "4"],
|
||||
]).toList()
|
||||
).toEqual(["&", ["a", "=", 3], ["b", "!=", "4"]]);
|
||||
expect(new Domain(["!", ["a", "=", 3]]).toList()).toEqual(["!", ["a", "=", 3]]);
|
||||
});
|
||||
|
||||
test("toString", () => {
|
||||
expect(new Domain([]).toString()).toBe(`[]`);
|
||||
expect(new Domain([["a", "=", 3]]).toString()).toBe(`[("a", "=", 3)]`);
|
||||
expect(
|
||||
new Domain([
|
||||
["a", "=", 3],
|
||||
["b", "!=", "4"],
|
||||
]).toString()
|
||||
).toBe(`["&", ("a", "=", 3), ("b", "!=", "4")]`);
|
||||
expect(new Domain(["!", ["a", "=", 3]]).toString()).toBe(`["!", ("a", "=", 3)]`);
|
||||
expect(new Domain([["name", "=", null]]).toString()).toBe('[("name", "=", None)]');
|
||||
expect(new Domain([["name", "=", false]]).toString()).toBe('[("name", "=", False)]');
|
||||
expect(new Domain([["name", "=", true]]).toString()).toBe('[("name", "=", True)]');
|
||||
expect(new Domain([["name", "=", "null"]]).toString()).toBe('[("name", "=", "null")]');
|
||||
expect(new Domain([["name", "=", "false"]]).toString()).toBe('[("name", "=", "false")]');
|
||||
expect(new Domain([["name", "=", "true"]]).toString()).toBe('[("name", "=", "true")]');
|
||||
expect(new Domain().toString()).toBe("[]");
|
||||
expect(new Domain([["name", "in", [true, false]]]).toString()).toBe(
|
||||
'[("name", "in", [True, False])]'
|
||||
);
|
||||
expect(new Domain([["name", "in", [null]]]).toString()).toBe('[("name", "in", [None])]');
|
||||
expect(new Domain([["name", "in", ["foo", "bar"]]]).toString()).toBe(
|
||||
'[("name", "in", ["foo", "bar"])]'
|
||||
);
|
||||
expect(new Domain([["name", "in", [1, 2]]]).toString()).toBe('[("name", "in", [1, 2])]');
|
||||
expect(new Domain(["&", ["name", "=", "foo"], ["type", "=", "bar"]]).toString()).toBe(
|
||||
'["&", ("name", "=", "foo"), ("type", "=", "bar")]'
|
||||
);
|
||||
expect(new Domain(["|", ["name", "=", "foo"], ["type", "=", "bar"]]).toString()).toBe(
|
||||
'["|", ("name", "=", "foo"), ("type", "=", "bar")]'
|
||||
);
|
||||
expect(new Domain().toString()).toBe("[]");
|
||||
|
||||
// string domains are only reformatted
|
||||
expect(new Domain('[("name","ilike","foo")]').toString()).toBe(
|
||||
'[("name", "ilike", "foo")]'
|
||||
);
|
||||
});
|
||||
|
||||
test("toJson", () => {
|
||||
expect(new Domain([]).toJson()).toEqual([]);
|
||||
expect(new Domain("[]").toJson()).toEqual([]);
|
||||
expect(new Domain([["a", "=", 3]]).toJson()).toEqual([["a", "=", 3]]);
|
||||
expect(new Domain('[("a", "=", 3)]').toJson()).toEqual([["a", "=", 3]]);
|
||||
expect(new Domain('[("user_id", "=", uid)]').toJson()).toBe('[("user_id", "=", uid)]');
|
||||
expect(new Domain('[("date", "=", context_today())]').toJson()).toBe(
|
||||
'[("date", "=", context_today())]'
|
||||
);
|
||||
});
|
||||
|
||||
test("implicit &", () => {
|
||||
const domain = new Domain([
|
||||
["a", "=", 3],
|
||||
["b", "=", 4],
|
||||
]);
|
||||
expect(domain.contains({})).toBe(false);
|
||||
expect(domain.contains({ a: 3, b: 4 })).toBe(true);
|
||||
expect(domain.contains({ a: 3, b: 5 })).toBe(false);
|
||||
});
|
||||
|
||||
test("comparison operators", () => {
|
||||
expect(new Domain([["a", "=", 3]]).contains({ a: 3 })).toBe(true);
|
||||
expect(new Domain([["a", "=", 3]]).contains({ a: 4 })).toBe(false);
|
||||
expect(new Domain([["a", "=", 3]]).toString()).toBe(`[("a", "=", 3)]`);
|
||||
expect(new Domain([["a", "==", 3]]).contains({ a: 3 })).toBe(true);
|
||||
expect(new Domain([["a", "==", 3]]).contains({ a: 4 })).toBe(false);
|
||||
expect(new Domain([["a", "==", 3]]).toString()).toBe(`[("a", "==", 3)]`);
|
||||
expect(new Domain([["a", "!=", 3]]).contains({ a: 3 })).toBe(false);
|
||||
expect(new Domain([["a", "!=", 3]]).contains({ a: 4 })).toBe(true);
|
||||
expect(new Domain([["a", "!=", 3]]).toString()).toBe(`[("a", "!=", 3)]`);
|
||||
expect(new Domain([["a", "<>", 3]]).contains({ a: 3 })).toBe(false);
|
||||
expect(new Domain([["a", "<>", 3]]).contains({ a: 4 })).toBe(true);
|
||||
expect(new Domain([["a", "<>", 3]]).toString()).toBe(`[("a", "<>", 3)]`);
|
||||
expect(new Domain([["a", "<", 3]]).contains({ a: 5 })).toBe(false);
|
||||
expect(new Domain([["a", "<", 3]]).contains({ a: 3 })).toBe(false);
|
||||
expect(new Domain([["a", "<", 3]]).contains({ a: 2 })).toBe(true);
|
||||
expect(new Domain([["a", "<", 3]]).toString()).toBe(`[("a", "<", 3)]`);
|
||||
expect(new Domain([["a", "<=", 3]]).contains({ a: 5 })).toBe(false);
|
||||
expect(new Domain([["a", "<=", 3]]).contains({ a: 3 })).toBe(true);
|
||||
expect(new Domain([["a", "<=", 3]]).contains({ a: 2 })).toBe(true);
|
||||
expect(new Domain([["a", "<=", 3]]).toString()).toBe(`[("a", "<=", 3)]`);
|
||||
expect(new Domain([["a", ">", 3]]).contains({ a: 5 })).toBe(true);
|
||||
expect(new Domain([["a", ">", 3]]).contains({ a: 3 })).toBe(false);
|
||||
expect(new Domain([["a", ">", 3]]).contains({ a: 2 })).toBe(false);
|
||||
expect(new Domain([["a", ">", 3]]).toString()).toBe(`[("a", ">", 3)]`);
|
||||
expect(new Domain([["a", ">=", 3]]).contains({ a: 5 })).toBe(true);
|
||||
expect(new Domain([["a", ">=", 3]]).contains({ a: 3 })).toBe(true);
|
||||
expect(new Domain([["a", ">=", 3]]).contains({ a: 2 })).toBe(false);
|
||||
expect(new Domain([["a", ">=", 3]]).toString()).toBe(`[("a", ">=", 3)]`);
|
||||
});
|
||||
|
||||
test("other operators", () => {
|
||||
expect(new Domain([["a", "in", 3]]).contains({ a: 3 })).toBe(true);
|
||||
expect(new Domain([["a", "in", [1, 2, 3]]]).contains({ a: 3 })).toBe(true);
|
||||
expect(new Domain([["a", "in", [1, 2, 3]]]).contains({ a: [3] })).toBe(true);
|
||||
expect(new Domain([["a", "in", 3]]).contains({ a: 5 })).toBe(false);
|
||||
expect(new Domain([["a", "in", [1, 2, 3]]]).contains({ a: 5 })).toBe(false);
|
||||
expect(new Domain([["a", "in", [1, 2, 3]]]).contains({ a: [5] })).toBe(false);
|
||||
expect(new Domain([["a", "not in", 3]]).contains({ a: 3 })).toBe(false);
|
||||
expect(new Domain([["a", "not in", [1, 2, 3]]]).contains({ a: 3 })).toBe(false);
|
||||
expect(new Domain([["a", "not in", [1, 2, 3]]]).contains({ a: [3] })).toBe(false);
|
||||
expect(new Domain([["a", "not in", 3]]).contains({ a: 5 })).toBe(true);
|
||||
expect(new Domain([["a", "not in", [1, 2, 3]]]).contains({ a: 5 })).toBe(true);
|
||||
expect(new Domain([["a", "not in", [1, 2, 3]]]).contains({ a: [5] })).toBe(true);
|
||||
expect(new Domain([["a", "like", "abc"]]).contains({ a: "abc" })).toBe(true);
|
||||
expect(new Domain([["a", "like", "abc"]]).contains({ a: "def" })).toBe(false);
|
||||
expect(new Domain([["a", "=like", "abc"]]).contains({ a: "abc" })).toBe(true);
|
||||
expect(new Domain([["a", "=like", "abc"]]).contains({ a: "def" })).toBe(false);
|
||||
expect(new Domain([["a", "ilike", "abc"]]).contains({ a: "abc" })).toBe(true);
|
||||
expect(new Domain([["a", "ilike", "abc"]]).contains({ a: "def" })).toBe(false);
|
||||
expect(new Domain([["a", "=ilike", "abc"]]).contains({ a: "abc" })).toBe(true);
|
||||
expect(new Domain([["a", "=ilike", "abc"]]).contains({ a: "def" })).toBe(false);
|
||||
});
|
||||
|
||||
test("creating a domain with a string expression", () => {
|
||||
expect(new Domain(`[('a', '>=', 3)]`).toString()).toBe(`[("a", ">=", 3)]`);
|
||||
expect(new Domain(`[('a', '>=', 3)]`).contains({ a: 5 })).toBe(true);
|
||||
});
|
||||
|
||||
test("can evaluate a python expression", () => {
|
||||
expect(new Domain(`[('date', '!=', False)]`).toList()).toEqual([["date", "!=", false]]);
|
||||
expect(new Domain(`[('date', '!=', False)]`).toList()).toEqual([["date", "!=", false]]);
|
||||
expect(new Domain(`[('date', '!=', 1 + 2)]`).toString()).toBe(`[("date", "!=", 1 + 2)]`);
|
||||
expect(new Domain(`[('date', '!=', 1 + 2)]`).toList()).toEqual([["date", "!=", 3]]);
|
||||
expect(new Domain(`[('a', '==', 1 + 2)]`).contains({ a: 3 })).toBe(true);
|
||||
expect(new Domain(`[('a', '==', 1 + 2)]`).contains({ a: 2 })).toBe(false);
|
||||
});
|
||||
|
||||
test("some expression with date stuff", () => {
|
||||
patchWithCleanup(PyDate, {
|
||||
today() {
|
||||
return new PyDate(2013, 4, 24);
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
new Domain(
|
||||
"[('date','>=', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]"
|
||||
).toList()
|
||||
).toEqual([["date", ">=", "2013-03-25"]]);
|
||||
|
||||
const domainList = new Domain(
|
||||
"[('date', '>=', context_today() - relativedelta(days=30))]"
|
||||
).toList(); // domain creation using `parseExpr` function since the parameter is a string.
|
||||
|
||||
expect(domainList[0][2]).toEqual(PyDate.create({ day: 25, month: 3, year: 2013 }), {
|
||||
message: "The right item in the rule in the domain should be a PyDate object",
|
||||
});
|
||||
expect(JSON.stringify(domainList)).toBe('[["date",">=","2013-03-25"]]');
|
||||
|
||||
const domainList2 = new Domain(domainList).toList(); // domain creation using `toAST` function since the parameter is a list.
|
||||
expect(domainList2[0][2]).toEqual(PyDate.create({ day: 25, month: 3, year: 2013 }), {
|
||||
message: "The right item in the rule in the domain should be a PyDate object",
|
||||
});
|
||||
expect(JSON.stringify(domainList2)).toBe('[["date",">=","2013-03-25"]]');
|
||||
});
|
||||
|
||||
test("Check that there is no dependency between two domains", () => {
|
||||
// The purpose of this test is to verify that a domain created on the basis
|
||||
// of another one does not share any dependency.
|
||||
const domain1 = new Domain(`[('date', '!=', False)]`);
|
||||
const domain2 = new Domain(domain1);
|
||||
expect(domain1.toString()).toBe(domain2.toString());
|
||||
|
||||
domain2.ast.value.unshift({ type: 1, value: "!" });
|
||||
expect(domain1.toString()).not.toBe(domain2.toString());
|
||||
});
|
||||
|
||||
test("TRUE and FALSE Domain", () => {
|
||||
expect(Domain.TRUE.contains({})).toBe(true);
|
||||
expect(Domain.FALSE.contains({})).toBe(false);
|
||||
|
||||
expect(Domain.and([Domain.TRUE, new Domain([["a", "=", 3]])]).contains({ a: 3 })).toBe(
|
||||
true
|
||||
);
|
||||
expect(Domain.and([Domain.FALSE, new Domain([["a", "=", 3]])]).contains({ a: 3 })).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("invalid domains should not succeed", () => {
|
||||
expect(() => new Domain(["|", ["hr_presence_state", "=", "absent"]])).toThrow(
|
||||
/invalid domain .* \(missing 1 segment/
|
||||
);
|
||||
expect(
|
||||
() =>
|
||||
new Domain([
|
||||
"|",
|
||||
"|",
|
||||
["hr_presence_state", "=", "absent"],
|
||||
["attendance_state", "=", "checked_in"],
|
||||
])
|
||||
).toThrow(/invalid domain .* \(missing 1 segment/);
|
||||
expect(() => new Domain(["|", "|", ["hr_presence_state", "=", "absent"]])).toThrow(
|
||||
/invalid domain .* \(missing 2 segment\(s\)/
|
||||
);
|
||||
expect(() => new Domain(["&", ["composition_mode", "!=", "mass_post"]])).toThrow(
|
||||
/invalid domain .* \(missing 1 segment/
|
||||
);
|
||||
expect(() => new Domain(["!"])).toThrow(/invalid domain .* \(missing 1 segment/);
|
||||
expect(() => new Domain(`[(1, 2)]`)).toThrow(/Invalid domain AST/);
|
||||
expect(() => new Domain(`[(1, 2, 3, 4)]`)).toThrow(/Invalid domain AST/);
|
||||
expect(() => new Domain(`["a"]`)).toThrow(/Invalid domain AST/);
|
||||
expect(() => new Domain(`[1]`)).toThrow(/Invalid domain AST/);
|
||||
expect(() => new Domain(`[x]`)).toThrow(/Invalid domain AST/);
|
||||
expect(() => new Domain(`[True]`)).toThrow(/Invalid domain AST/); // will possibly change with CHM work
|
||||
expect(() => new Domain(`[(x.=, "=", 1)]`)).toThrow(/Invalid domain representation/);
|
||||
expect(() => new Domain(`[(+, "=", 1)]`)).toThrow(/Invalid domain representation/);
|
||||
expect(() => new Domain([{}])).toThrow(/Invalid domain representation/);
|
||||
expect(() => new Domain([1])).toThrow(/Invalid domain representation/);
|
||||
});
|
||||
|
||||
test("follow relations", () => {
|
||||
expect(
|
||||
new Domain([["partner.city", "ilike", "Bru"]]).contains({
|
||||
name: "Lucas",
|
||||
partner: {
|
||||
city: "Bruxelles",
|
||||
},
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
new Domain([["partner.city.name", "ilike", "Bru"]]).contains({
|
||||
name: "Lucas",
|
||||
partner: {
|
||||
city: {
|
||||
name: "Bruxelles",
|
||||
},
|
||||
},
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("Arrays comparison", () => {
|
||||
const domain = new Domain(["&", ["a", "==", []], ["b", "!=", []]]);
|
||||
|
||||
expect(domain.contains({ a: [] })).toBe(true);
|
||||
expect(domain.contains({ a: [], b: [4] })).toBe(true);
|
||||
expect(domain.contains({ a: [1] })).toBe(false);
|
||||
expect(domain.contains({ b: [] })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Normalization
|
||||
// ---------------------------------------------------------------------------
|
||||
describe("Normalization", () => {
|
||||
test("return simple (normalized) domains", () => {
|
||||
const domains = ["[]", `[("a", "=", 1)]`, `["!", ("a", "=", 1)]`];
|
||||
for (const domain of domains) {
|
||||
expect(new Domain(domain).toString()).toBe(domain);
|
||||
}
|
||||
});
|
||||
|
||||
test("properly add the & in a non normalized domain", () => {
|
||||
expect(new Domain(`[("a", "=", 1), ("b", "=", 2)]`).toString()).toBe(
|
||||
`["&", ("a", "=", 1), ("b", "=", 2)]`
|
||||
);
|
||||
});
|
||||
|
||||
test("normalize domain with ! operator", () => {
|
||||
expect(new Domain(`["!", ("a", "=", 1), ("b", "=", 2)]`).toString()).toBe(
|
||||
`["&", "!", ("a", "=", 1), ("b", "=", 2)]`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Combining domains
|
||||
// ---------------------------------------------------------------------------
|
||||
describe("Combining domains", () => {
|
||||
test("combining zero domain", () => {
|
||||
expect(Domain.combine([], "AND").toString()).toBe("[]");
|
||||
expect(Domain.combine([], "OR").toString()).toBe("[]");
|
||||
expect(Domain.combine([], "AND").contains({ a: 1, b: 2 })).toBe(true);
|
||||
});
|
||||
|
||||
test("combining one domain", () => {
|
||||
expect(Domain.combine([`[("a", "=", 1)]`], "AND").toString()).toBe(`[("a", "=", 1)]`);
|
||||
expect(Domain.combine([`[("user_id", "=", uid)]`], "AND").toString()).toBe(
|
||||
`[("user_id", "=", uid)]`
|
||||
);
|
||||
expect(Domain.combine([[["a", "=", 1]]], "AND").toString()).toBe(`[("a", "=", 1)]`);
|
||||
expect(Domain.combine(["[('a', '=', '1'), ('b', '!=', 2)]"], "AND").toString()).toBe(
|
||||
`["&", ("a", "=", "1"), ("b", "!=", 2)]`
|
||||
);
|
||||
});
|
||||
|
||||
test("combining two domains", () => {
|
||||
expect(Domain.combine([`[("a", "=", 1)]`, "[]"], "AND").toString()).toBe(`[("a", "=", 1)]`);
|
||||
expect(Domain.combine([`[("a", "=", 1)]`, []], "AND").toString()).toBe(`[("a", "=", 1)]`);
|
||||
expect(Domain.combine([new Domain(`[("a", "=", 1)]`), "[]"], "AND").toString()).toBe(
|
||||
`[("a", "=", 1)]`
|
||||
);
|
||||
expect(Domain.combine([new Domain(`[("a", "=", 1)]`), "[]"], "OR").toString()).toBe(
|
||||
`[("a", "=", 1)]`
|
||||
);
|
||||
expect(Domain.combine([[["a", "=", 1]], "[('uid', '<=', uid)]"], "AND").toString()).toBe(
|
||||
`["&", ("a", "=", 1), ("uid", "<=", uid)]`
|
||||
);
|
||||
expect(Domain.combine([[["a", "=", 1]], "[('b', '<=', 3)]"], "OR").toString()).toBe(
|
||||
`["|", ("a", "=", 1), ("b", "<=", 3)]`
|
||||
);
|
||||
expect(
|
||||
Domain.combine(
|
||||
["[('a', '=', '1'), ('c', 'in', [4, 5])]", "[('b', '<=', 3)]"],
|
||||
"OR"
|
||||
).toString()
|
||||
).toBe(`["|", "&", ("a", "=", "1"), ("c", "in", [4, 5]), ("b", "<=", 3)]`);
|
||||
expect(
|
||||
Domain.combine(
|
||||
[new Domain("[('a', '=', '1'), ('c', 'in', [4, 5])]"), "[('b', '<=', 3)]"],
|
||||
"OR"
|
||||
).toString()
|
||||
).toBe(`["|", "&", ("a", "=", "1"), ("c", "in", [4, 5]), ("b", "<=", 3)]`);
|
||||
});
|
||||
|
||||
test("combining three domains", () => {
|
||||
expect(
|
||||
Domain.combine(
|
||||
[
|
||||
new Domain("[('a', '=', '1'), ('c', 'in', [4, 5])]"),
|
||||
[["b", "<=", 3]],
|
||||
`['!', ('uid', '=', uid)]`,
|
||||
],
|
||||
"OR"
|
||||
).toString()
|
||||
).toBe(
|
||||
`["|", "&", ("a", "=", "1"), ("c", "in", [4, 5]), "|", ("b", "<=", 3), "!", ("uid", "=", uid)]`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OPERATOR AND / OR / NOT
|
||||
// ---------------------------------------------------------------------------
|
||||
describe("Operator and - or - not", () => {
|
||||
test("combining two domains with and/or", () => {
|
||||
expect(Domain.and([`[("a", "=", 1)]`, "[]"]).toString()).toBe(`[("a", "=", 1)]`);
|
||||
expect(Domain.and([`[("a", "=", 1)]`, []]).toString()).toBe(`[("a", "=", 1)]`);
|
||||
expect(Domain.and([new Domain(`[("a", "=", 1)]`), "[]"]).toString()).toBe(
|
||||
`[("a", "=", 1)]`
|
||||
);
|
||||
expect(Domain.or([new Domain(`[("a", "=", 1)]`), "[]"]).toString()).toBe(`[("a", "=", 1)]`);
|
||||
expect(Domain.and([[["a", "=", 1]], "[('uid', '<=', uid)]"]).toString()).toBe(
|
||||
`["&", ("a", "=", 1), ("uid", "<=", uid)]`
|
||||
);
|
||||
expect(Domain.or([[["a", "=", 1]], "[('b', '<=', 3)]"]).toString()).toBe(
|
||||
`["|", ("a", "=", 1), ("b", "<=", 3)]`
|
||||
);
|
||||
expect(
|
||||
Domain.or(["[('a', '=', '1'), ('c', 'in', [4, 5])]", "[('b', '<=', 3)]"]).toString()
|
||||
).toBe(`["|", "&", ("a", "=", "1"), ("c", "in", [4, 5]), ("b", "<=", 3)]`);
|
||||
expect(
|
||||
Domain.or([
|
||||
new Domain("[('a', '=', '1'), ('c', 'in', [4, 5])]"),
|
||||
"[('b', '<=', 3)]",
|
||||
]).toString()
|
||||
).toBe(`["|", "&", ("a", "=", "1"), ("c", "in", [4, 5]), ("b", "<=", 3)]`);
|
||||
});
|
||||
|
||||
test("apply `NOT` on a Domain", () => {
|
||||
expect(Domain.not("[('a', '=', 1)]").toString()).toBe(`["!", ("a", "=", 1)]`);
|
||||
expect(Domain.not('[("uid", "<=", uid)]').toString()).toBe(`["!", ("uid", "<=", uid)]`);
|
||||
expect(Domain.not(new Domain("[('a', '=', 1)]")).toString()).toBe(`["!", ("a", "=", 1)]`);
|
||||
expect(Domain.not(new Domain([["a", "=", 1]])).toString()).toBe(`["!", ("a", "=", 1)]`);
|
||||
});
|
||||
|
||||
test("tuple are supported", () => {
|
||||
expect(
|
||||
new Domain(`(("field", "like", "string"), ("field", "like", "strOng"))`).toList()
|
||||
).toEqual(["&", ["field", "like", "string"], ["field", "like", "strOng"]]);
|
||||
expect(new Domain(`("!",("field", "like", "string"))`).toList()).toEqual([
|
||||
"!",
|
||||
["field", "like", "string"],
|
||||
]);
|
||||
expect(() => new Domain(`(("field", "like", "string"))`)).toThrow(/Invalid domain AST/);
|
||||
expect(() => new Domain(`("&", "&", "|")`)).toThrow(/Invalid domain AST/);
|
||||
expect(() => new Domain(`("&", "&", 3)`)).toThrow(/Invalid domain AST/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Remove domain leaf", () => {
|
||||
test("Remove leaf in domain.", () => {
|
||||
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],
|
||||
]);
|
||||
expect(newDomain.toList({})).toEqual(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],
|
||||
]);
|
||||
expect(newDomain.toList({})).toEqual(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]]);
|
||||
expect(newDomain.toList({})).toEqual(expectedDomain.toList({}));
|
||||
});
|
||||
});
|
||||
1200
odoo-bringout-oca-ocb-web/web/static/tests/core/domain_field.test.js
Normal file
1200
odoo-bringout-oca-ocb-web/web/static/tests/core/domain_field.test.js
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,119 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import {
|
||||
Country,
|
||||
Partner,
|
||||
Player,
|
||||
Product,
|
||||
Stage,
|
||||
Team,
|
||||
} from "@web/../tests/core/tree_editor/condition_tree_editor_test_helpers";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
makeDialogMockEnv,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { DomainSelectorDialog } from "@web/core/domain_selector_dialog/domain_selector_dialog";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
async function makeDomainSelectorDialog(params = {}) {
|
||||
const props = { ...params };
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { DomainSelectorDialog };
|
||||
static template = xml`<DomainSelectorDialog t-props="domainSelectorProps"/>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.domainSelectorProps = {
|
||||
readonly: false,
|
||||
domain: "[]",
|
||||
close: () => {},
|
||||
onConfirm: () => {},
|
||||
...props,
|
||||
resModel: "partner",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const env = await makeDialogMockEnv();
|
||||
return mountWithCleanup(Parent, { env, props });
|
||||
}
|
||||
|
||||
defineModels([Partner, Product, Team, Player, Country, Stage]);
|
||||
|
||||
test("a domain with a user context dynamic part is valid", async () => {
|
||||
await makeDomainSelectorDialog({
|
||||
domain: "[('foo', '=', uid)]",
|
||||
onConfirm(domain) {
|
||||
expect(domain).toBe("[('foo', '=', uid)]");
|
||||
expect.step("confirmed");
|
||||
},
|
||||
});
|
||||
onRpc("/web/domain/validate", () => {
|
||||
expect.step("validation");
|
||||
return true;
|
||||
});
|
||||
await contains(".o_dialog footer button").click();
|
||||
expect.verifySteps(["validation", "confirmed"]);
|
||||
});
|
||||
|
||||
test("can extend eval context", async () => {
|
||||
await makeDomainSelectorDialog({
|
||||
domain: "['&', ('foo', '=', uid), ('bar', '=', var)]",
|
||||
context: { uid: 99, var: "true" },
|
||||
onConfirm(domain) {
|
||||
expect(domain).toBe("['&', ('foo', '=', uid), ('bar', '=', var)]");
|
||||
expect.step("confirmed");
|
||||
},
|
||||
});
|
||||
onRpc("/web/domain/validate", () => {
|
||||
expect.step("validation");
|
||||
return true;
|
||||
});
|
||||
await contains(".o_dialog footer button").click();
|
||||
expect.verifySteps(["validation", "confirmed"]);
|
||||
});
|
||||
|
||||
test("a domain with an unknown expression is not valid", async () => {
|
||||
await makeDomainSelectorDialog({
|
||||
domain: "[('foo', '=', unknown)]",
|
||||
onConfirm() {
|
||||
expect.step("confirmed");
|
||||
},
|
||||
});
|
||||
onRpc("/web/domain/validate", () => {
|
||||
expect.step("validation");
|
||||
return true;
|
||||
});
|
||||
await contains(".o_dialog footer button").click();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("model_field_selector should close on dialog drag", async () => {
|
||||
await makeDomainSelectorDialog({
|
||||
domain: "[('foo', '=', unknown)]",
|
||||
});
|
||||
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
await contains(".o_model_field_selector_value").click();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(1);
|
||||
|
||||
const header = queryOne(".modal-header");
|
||||
const headerRect = header.getBoundingClientRect();
|
||||
await contains(header).dragAndDrop(document.body, {
|
||||
position: {
|
||||
// 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,
|
||||
},
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { SELECTORS as treeEditorSELECTORS } from "@web/../tests/core/tree_editor/condition_tree_editor_test_helpers";
|
||||
|
||||
export const SELECTORS = {
|
||||
...treeEditorSELECTORS,
|
||||
debugArea: ".o_domain_selector_debug_container textarea",
|
||||
resetButton: ".o_domain_selector_row > button",
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,84 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { click, press, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { AccordionItem } from "@web/core/dropdown/accordion_item";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
|
||||
test("accordion can be rendered", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<AccordionItem description="'Test'" class="'text-primary'" selected="false"><h5>In accordion</h5></AccordionItem>`;
|
||||
static components = { AccordionItem };
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect("div.o_accordion").toHaveCount(1);
|
||||
expect(".o_accordion button.o_accordion_toggle").toHaveCount(1);
|
||||
expect(".o_accordion_values").toHaveCount(0);
|
||||
|
||||
await click("button.o_accordion_toggle");
|
||||
await animationFrame();
|
||||
expect(".o_accordion_values").toHaveCount(1);
|
||||
expect(queryOne(".o_accordion_values").innerHTML).toBe(`<h5>In accordion</h5>`);
|
||||
});
|
||||
|
||||
test("dropdown with accordion keyboard navigation", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Dropdown>
|
||||
<button>dropdown</button>
|
||||
<t t-set-slot="content">
|
||||
<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>
|
||||
</t>
|
||||
</Dropdown>
|
||||
`;
|
||||
static components = { Dropdown, DropdownItem, AccordionItem };
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
await click(".o-dropdown.dropdown-toggle");
|
||||
await animationFrame();
|
||||
|
||||
expect(".dropdown-menu > .focus").toHaveCount(0);
|
||||
|
||||
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 (let i = 0; i < scenarioSteps.length; i++) {
|
||||
const step = scenarioSteps[i];
|
||||
await press(step.key);
|
||||
await animationFrame();
|
||||
await runAllTimers();
|
||||
expect(`.dropdown-menu .focus:contains(${step.expected})`).toBeFocused();
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, hover, queryOne } from "@odoo/hoot-dom";
|
||||
import { Deferred, animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
import { getDropdownMenu, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownGroup } from "@web/core/dropdown/dropdown_group";
|
||||
|
||||
const DROPDOWN_MENU = ".o-dropdown--menu.dropdown-menu";
|
||||
|
||||
test.tags("desktop");
|
||||
test("DropdownGroup: when one Dropdown is open, others with same group name can be toggled on mouse-enter", async () => {
|
||||
expect.assertions(16);
|
||||
const beforeOpenProm = new Deferred();
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { Dropdown, DropdownGroup };
|
||||
static props = [];
|
||||
static template = xml`
|
||||
<div>
|
||||
<div class="outside">OUTSIDE</div>
|
||||
<DropdownGroup>
|
||||
<Dropdown menuClass="'menu-one'">
|
||||
<button class="one">One</button>
|
||||
<t t-set-slot="content">
|
||||
Content One
|
||||
</t>
|
||||
</Dropdown>
|
||||
<Dropdown beforeOpen="() => this.beforeOpen()" menuClass="'menu-two'">
|
||||
<button class="two">Two</button>
|
||||
<t t-set-slot="content">
|
||||
Content Two
|
||||
</t>
|
||||
</Dropdown>
|
||||
<Dropdown menuClass="'menu-three'">
|
||||
<button class="three">Three</button>
|
||||
<t t-set-slot="content">
|
||||
Content Three
|
||||
</t>
|
||||
</Dropdown>
|
||||
</DropdownGroup>
|
||||
<DropdownGroup>
|
||||
<Dropdown menuClass="'menu-four'">
|
||||
<button class="four">Four</button>
|
||||
<t t-set-slot="content">
|
||||
Content Four
|
||||
</t>
|
||||
</Dropdown>
|
||||
</DropdownGroup>
|
||||
</div>
|
||||
`;
|
||||
|
||||
beforeOpen() {
|
||||
expect.step("beforeOpen");
|
||||
return beforeOpenProm;
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
// Click on ONE
|
||||
await click(queryOne(".one"));
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps([]);
|
||||
expect(DROPDOWN_MENU).toHaveCount(1);
|
||||
expect(".one").toHaveClass("show");
|
||||
|
||||
// Hover on TWO
|
||||
await hover(".two");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["beforeOpen"]);
|
||||
expect(DROPDOWN_MENU).toHaveCount(1);
|
||||
expect(".menu-two").toHaveCount(0);
|
||||
|
||||
beforeOpenProm.resolve();
|
||||
await animationFrame();
|
||||
expect(DROPDOWN_MENU).toHaveCount(1);
|
||||
expect(".menu-two").toHaveCount(1);
|
||||
|
||||
// Hover on THREE
|
||||
await hover(".three");
|
||||
await animationFrame();
|
||||
expect(DROPDOWN_MENU).toHaveCount(1);
|
||||
expect(".menu-three").toHaveCount(1);
|
||||
|
||||
// Hover on FOUR (Should not open)
|
||||
expect(".menu-four").toHaveCount(0);
|
||||
await hover(".four");
|
||||
await animationFrame();
|
||||
expect(DROPDOWN_MENU).toHaveCount(1);
|
||||
expect(".menu-three").toHaveCount(1);
|
||||
expect(".menu-four").toHaveCount(0);
|
||||
|
||||
// Click on OUTSIDE
|
||||
await click("div.outside");
|
||||
await animationFrame();
|
||||
expect(DROPDOWN_MENU).toHaveCount(0);
|
||||
|
||||
// Hover on ONE, TWO, THREE
|
||||
await hover(".one");
|
||||
await hover(".two");
|
||||
await hover(".three");
|
||||
await animationFrame();
|
||||
expect(DROPDOWN_MENU).toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("DropdownGroup: when non-sibling Dropdown is open, other must not be toggled on mouse-enter", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<DropdownGroup>
|
||||
<Dropdown>
|
||||
<button class="one">One</button>
|
||||
<t t-set-slot="content">One Content</t>
|
||||
</Dropdown>
|
||||
</DropdownGroup>
|
||||
<DropdownGroup>
|
||||
<Dropdown>
|
||||
<button class="two">Two</button>
|
||||
<t t-set-slot="content">Two Content</t>
|
||||
</Dropdown>
|
||||
</DropdownGroup>
|
||||
</div>
|
||||
`;
|
||||
static components = { Dropdown, DropdownGroup };
|
||||
static props = [];
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
// Click on One
|
||||
await click(".one");
|
||||
await animationFrame();
|
||||
expect(getDropdownMenu(".one")).toHaveCount(1);
|
||||
|
||||
// Hover on Two
|
||||
await hover(".two");
|
||||
await animationFrame();
|
||||
expect(getDropdownMenu(".one")).toHaveCount(1);
|
||||
|
||||
expect(".one").toHaveClass("show");
|
||||
expect(".two").not.toHaveClass("show");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("DropdownGroup: when one is open, then non-sibling toggled, siblings must not be toggled on mouse-enter", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { Dropdown, DropdownGroup };
|
||||
static props = [];
|
||||
static template = xml`
|
||||
<div>
|
||||
<DropdownGroup>
|
||||
<Dropdown>
|
||||
<button class="one">One</button>
|
||||
<t t-set-slot="content">
|
||||
One Content
|
||||
</t>
|
||||
</Dropdown>
|
||||
</DropdownGroup>
|
||||
<DropdownGroup>
|
||||
<Dropdown>
|
||||
<button class="two">Two</button>
|
||||
<t t-set-slot="content">
|
||||
Two Content
|
||||
</t>
|
||||
</Dropdown>
|
||||
</DropdownGroup>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
// Click on BAR1
|
||||
await click(".two");
|
||||
await animationFrame();
|
||||
expect(DROPDOWN_MENU).toHaveCount(1);
|
||||
|
||||
// Click on FOO
|
||||
await click(".one");
|
||||
await animationFrame();
|
||||
expect(DROPDOWN_MENU).toHaveCount(1);
|
||||
|
||||
// Hover on BAR1
|
||||
await hover(".two");
|
||||
await animationFrame();
|
||||
expect(DROPDOWN_MENU).toHaveCount(1);
|
||||
expect(".two-menu").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("DropdownGroup: toggler focused on mouseenter", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { Dropdown, DropdownGroup };
|
||||
static props = [];
|
||||
static template = xml`
|
||||
<DropdownGroup>
|
||||
<Dropdown>
|
||||
<button class="one">One</button>
|
||||
<t t-set-slot="content">
|
||||
One Content
|
||||
</t>
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<button class="two">Two</button>
|
||||
<t t-set-slot="content">
|
||||
Two Content
|
||||
</t>
|
||||
</Dropdown>
|
||||
</DropdownGroup>
|
||||
`;
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
// Click on one
|
||||
await click("button.one");
|
||||
await animationFrame();
|
||||
expect("button.one").toBeFocused();
|
||||
expect(DROPDOWN_MENU).toHaveText("One Content");
|
||||
|
||||
// Hover on two
|
||||
await hover("button.two");
|
||||
await animationFrame();
|
||||
expect("button.two").toBeFocused();
|
||||
expect(DROPDOWN_MENU).toHaveText("Two Content");
|
||||
});
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
import { mountWithCleanup, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
|
||||
const DROPDOWN_TOGGLE = ".o-dropdown.dropdown-toggle";
|
||||
const DROPDOWN_MENU = ".o-dropdown--menu.dropdown-menu";
|
||||
const DROPDOWN_ITEM = ".o-dropdown-item.dropdown-item:not(.o-dropdown)";
|
||||
|
||||
test("can be rendered as <span/>", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { DropdownItem };
|
||||
static props = [];
|
||||
static template = xml`<DropdownItem>coucou</DropdownItem>`;
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
expect(".dropdown-item").toHaveClass(["o-dropdown-item", "o-navigable", "dropdown-item"]);
|
||||
expect(".dropdown-item").toHaveAttribute("role", "menuitem");
|
||||
});
|
||||
|
||||
test("(with href prop) can be rendered as <a/>", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { DropdownItem };
|
||||
static props = [];
|
||||
static template = xml`<DropdownItem attrs="{ href: '#' }">coucou</DropdownItem>`;
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
expect(DROPDOWN_ITEM).toHaveAttribute("href", "#");
|
||||
});
|
||||
|
||||
test("prevents click default with href", async () => {
|
||||
expect.assertions(4);
|
||||
// A DropdownItem should preventDefault a click as it may take the shape
|
||||
// of an <a/> tag with an [href] attribute and e.g. could change the url when clicked.
|
||||
patchWithCleanup(DropdownItem.prototype, {
|
||||
onClick(ev) {
|
||||
expect(!ev.defaultPrevented).toBe(true);
|
||||
super.onClick(...arguments);
|
||||
const href = ev.target.getAttribute("href");
|
||||
// defaultPrevented only if props.href is defined
|
||||
expect(href !== null ? ev.defaultPrevented : !ev.defaultPrevented).toBe(true);
|
||||
},
|
||||
});
|
||||
class Parent extends Component {
|
||||
static components = { Dropdown, DropdownItem };
|
||||
static props = [];
|
||||
static template = xml`
|
||||
<Dropdown>
|
||||
<button>Coucou</button>
|
||||
<t t-set-slot="content">
|
||||
<DropdownItem class="'link'" attrs="{href: '#'}"/>
|
||||
<DropdownItem class="'nolink'" />
|
||||
</t>
|
||||
</Dropdown>`;
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
// The item containing the link class contains an href prop,
|
||||
// which will turn it into <a href=> So it must be defaultPrevented
|
||||
// The other one not contain any href props, it must not be defaultPrevented,
|
||||
// so as not to prevent the background change flow for example
|
||||
await click(DROPDOWN_TOGGLE);
|
||||
await animationFrame();
|
||||
await click(".link");
|
||||
await click("button.dropdown-toggle");
|
||||
await click(".nolink");
|
||||
});
|
||||
|
||||
test("can be styled", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { Dropdown, DropdownItem };
|
||||
static props = [];
|
||||
static template = xml`
|
||||
<Dropdown menuClass="'test-menu'">
|
||||
<button class="test-toggler">Coucou</button>
|
||||
<t t-set-slot="content">
|
||||
<DropdownItem class="'test-dropdown-item'">Item</DropdownItem>
|
||||
</t>
|
||||
</Dropdown>
|
||||
`;
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(DROPDOWN_TOGGLE).toHaveClass("test-toggler");
|
||||
|
||||
await click(DROPDOWN_TOGGLE);
|
||||
await animationFrame();
|
||||
expect(DROPDOWN_MENU).toHaveClass("test-menu");
|
||||
expect(DROPDOWN_ITEM).toHaveClass("test-dropdown-item");
|
||||
});
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { click, manuallyDispatchProgrammaticEvent, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, markup, xml } from "@odoo/owl";
|
||||
import { getService, mountWithCleanup, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { user } from "@web/core/user";
|
||||
|
||||
let effectParams;
|
||||
|
||||
beforeEach(async () => {
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
effectParams = {
|
||||
message: markup("<div>Congrats!</div>"),
|
||||
};
|
||||
});
|
||||
|
||||
test("effect service displays a rainbowman by default", async () => {
|
||||
getService("effect").add();
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_reward").toHaveCount(1);
|
||||
expect(".o_reward").toHaveText("Well Done!");
|
||||
});
|
||||
|
||||
test("rainbowman effect with show_effect: false", async () => {
|
||||
patchWithCleanup(user, { showEffect: false });
|
||||
|
||||
getService("effect").add();
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_reward").toHaveCount(0);
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("rendering a rainbowman destroy after animation", async () => {
|
||||
getService("effect").add(effectParams);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_reward").toHaveCount(1);
|
||||
expect(".o_reward_rainbow").toHaveCount(1);
|
||||
expect(".o_reward_msg_content").toHaveInnerHTML("<div>Congrats!</div>");
|
||||
|
||||
await manuallyDispatchProgrammaticEvent(queryOne(".o_reward"), "animationend", {
|
||||
animationName: "reward-fading-reverse",
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_reward").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("rendering a rainbowman destroy on click", async () => {
|
||||
getService("effect").add(effectParams);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_reward").toHaveCount(1);
|
||||
expect(".o_reward_rainbow").toHaveCount(1);
|
||||
|
||||
await click(".o_reward");
|
||||
await animationFrame();
|
||||
expect(".o_reward").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("rendering a rainbowman with an escaped message", async () => {
|
||||
getService("effect").add(effectParams);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_reward").toHaveCount(1);
|
||||
expect(".o_reward_rainbow").toHaveCount(1);
|
||||
expect(".o_reward_msg_content").toHaveText("Congrats!");
|
||||
});
|
||||
|
||||
test("rendering a rainbowman with a custom component", async () => {
|
||||
expect.assertions(2);
|
||||
const props = { foo: "bar" };
|
||||
|
||||
class Custom extends Component {
|
||||
static template = xml`<div class="custom">foo is <t t-esc="props.foo"/></div>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
expect(this.props).toEqual(props);
|
||||
}
|
||||
}
|
||||
|
||||
getService("effect").add({ Component: Custom, props });
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_reward_msg_content").toHaveInnerHTML(`<div class="custom">foo is bar</div>`);
|
||||
});
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
import { browser } from "@web/core/browser/browser";
|
||||
import { describe, test, expect } from "@odoo/hoot";
|
||||
import { animationFrame, tick } from "@odoo/hoot-mock";
|
||||
import {
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
mockService,
|
||||
makeDialogMockEnv,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { click, freezeTime, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import {
|
||||
ClientErrorDialog,
|
||||
Error504Dialog,
|
||||
ErrorDialog,
|
||||
RedirectWarningDialog,
|
||||
SessionExpiredDialog,
|
||||
WarningDialog,
|
||||
} from "@web/core/errors/error_dialogs";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("ErrorDialog with traceback", async () => {
|
||||
freezeTime();
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ErrorDialog, {
|
||||
env,
|
||||
props: {
|
||||
message: "Something bad happened",
|
||||
data: { debug: "Some strange unreadable stack" },
|
||||
name: "ERROR_NAME",
|
||||
traceback: "This is a traceback string",
|
||||
close() {},
|
||||
},
|
||||
});
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect("header .modal-title").toHaveText("Oops!");
|
||||
expect("main button").toHaveText("See technical details");
|
||||
expect(queryAllTexts("footer button")).toEqual(["Close"]);
|
||||
expect("main p").toHaveText(
|
||||
"Something went wrong... If you really are stuck, share the report with your friendly support service"
|
||||
);
|
||||
expect("div.o_error_detail").toHaveCount(0);
|
||||
await click("main button");
|
||||
await animationFrame();
|
||||
expect(queryAllTexts("main .clearfix p")).toEqual([
|
||||
"Odoo Error",
|
||||
"Something bad happened",
|
||||
"Occured on 2019-03-11 09:30:00 GMT",
|
||||
]);
|
||||
expect("main .clearfix code").toHaveText("ERROR_NAME");
|
||||
expect("div.o_error_detail").toHaveCount(1);
|
||||
expect("div.o_error_detail pre").toHaveText("This is a traceback string");
|
||||
});
|
||||
|
||||
test("Client ErrorDialog with traceback", async () => {
|
||||
freezeTime();
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ClientErrorDialog, {
|
||||
env,
|
||||
props: {
|
||||
message: "Something bad happened",
|
||||
data: { debug: "Some strange unreadable stack" },
|
||||
name: "ERROR_NAME",
|
||||
traceback: "This is a traceback string",
|
||||
close() {},
|
||||
},
|
||||
});
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect("header .modal-title").toHaveText("Oops!");
|
||||
expect("main button").toHaveText("See technical details");
|
||||
expect(queryAllTexts("footer button")).toEqual(["Close"]);
|
||||
expect("main p").toHaveText(
|
||||
"Something went wrong... If you really are stuck, share the report with your friendly support service"
|
||||
);
|
||||
expect("div.o_error_detail").toHaveCount(0);
|
||||
await click("main button");
|
||||
await animationFrame();
|
||||
expect(queryAllTexts("main .clearfix p")).toEqual([
|
||||
"Odoo Client Error",
|
||||
"Something bad happened",
|
||||
"Occured on 2019-03-11 09:30:00 GMT",
|
||||
]);
|
||||
expect("main .clearfix code").toHaveText("ERROR_NAME");
|
||||
expect("div.o_error_detail").toHaveCount(1);
|
||||
expect("div.o_error_detail pre").toHaveText("This is a traceback string");
|
||||
});
|
||||
|
||||
test("button clipboard copy error traceback", async () => {
|
||||
freezeTime();
|
||||
expect.assertions(1);
|
||||
const error = new Error();
|
||||
error.name = "ERROR_NAME";
|
||||
error.message = "This is the message";
|
||||
error.traceback = "This is a traceback";
|
||||
patchWithCleanup(navigator.clipboard, {
|
||||
writeText(value) {
|
||||
expect(value).toBe(
|
||||
`${error.name}\n\n${error.message}\n\nOccured on 2019-03-11 09:30:00 GMT\n\n${error.traceback}`
|
||||
);
|
||||
},
|
||||
});
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ErrorDialog, {
|
||||
env,
|
||||
props: {
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
traceback: error.traceback,
|
||||
close() {},
|
||||
},
|
||||
});
|
||||
await click("main button");
|
||||
await animationFrame();
|
||||
await click(".fa-clone");
|
||||
await tick();
|
||||
});
|
||||
|
||||
test("Display a tooltip on clicking copy button", async () => {
|
||||
expect.assertions(1);
|
||||
mockService("popover", () => ({
|
||||
add(el, comp, params) {
|
||||
expect(params).toEqual({ tooltip: "Copied" });
|
||||
return () => {};
|
||||
},
|
||||
}));
|
||||
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(ErrorDialog, {
|
||||
env,
|
||||
props: {
|
||||
message: "This is the message",
|
||||
name: "ERROR_NAME",
|
||||
traceback: "This is a traceback",
|
||||
close() {},
|
||||
},
|
||||
});
|
||||
await click("main button");
|
||||
await animationFrame();
|
||||
await click(".fa-clone");
|
||||
});
|
||||
|
||||
test("WarningDialog", async () => {
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(WarningDialog, {
|
||||
env,
|
||||
props: {
|
||||
exceptionName: "odoo.exceptions.UserError",
|
||||
message: "...",
|
||||
data: { arguments: ["Some strange unreadable message"] },
|
||||
close() {},
|
||||
},
|
||||
});
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect("header .modal-title").toHaveText("Invalid Operation");
|
||||
expect(".o_error_dialog").toHaveCount(1);
|
||||
expect("main").toHaveText("Some strange unreadable message");
|
||||
expect(".o_dialog footer button").toHaveText("Close");
|
||||
});
|
||||
|
||||
test("RedirectWarningDialog", async () => {
|
||||
mockService("action", {
|
||||
doAction(actionId) {
|
||||
expect.step(actionId);
|
||||
},
|
||||
});
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(RedirectWarningDialog, {
|
||||
env,
|
||||
props: {
|
||||
data: {
|
||||
arguments: [
|
||||
"Some strange unreadable message",
|
||||
"buy_action_id",
|
||||
"Buy book on cryptography",
|
||||
],
|
||||
},
|
||||
close() {
|
||||
expect.step("dialog-closed");
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect("header .modal-title").toHaveText("Odoo Warning");
|
||||
expect("main").toHaveText("Some strange unreadable message");
|
||||
expect(queryAllTexts("footer button")).toEqual(["Buy book on cryptography", "Close"]);
|
||||
|
||||
await click("footer button:nth-child(1)"); // click on "Buy book on cryptography"
|
||||
await animationFrame();
|
||||
expect.verifySteps(["buy_action_id", "dialog-closed"]);
|
||||
|
||||
await click("footer button:nth-child(2)"); // click on "Cancel"
|
||||
await animationFrame();
|
||||
expect.verifySteps(["dialog-closed"]);
|
||||
});
|
||||
|
||||
test("Error504Dialog", async () => {
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(Error504Dialog, { env, props: { close() {} } });
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect("header .modal-title").toHaveText("Request timeout");
|
||||
expect("main p").toHaveText(
|
||||
"The operation was interrupted. This usually means that the current operation is taking too much time."
|
||||
);
|
||||
expect(".o_dialog footer button").toHaveText("Close");
|
||||
});
|
||||
|
||||
test("SessionExpiredDialog", async () => {
|
||||
patchWithCleanup(browser.location, {
|
||||
reload() {
|
||||
expect.step("location reload");
|
||||
},
|
||||
});
|
||||
expect(".o_dialog").toHaveCount(0);
|
||||
const env = await makeDialogMockEnv();
|
||||
await mountWithCleanup(SessionExpiredDialog, { env, props: { close() {} } });
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect(".o_dialog").toHaveCount(1);
|
||||
expect("header .modal-title").toHaveText("Odoo Session Expired");
|
||||
expect("main p").toHaveText(
|
||||
"Your Odoo session expired. The current page is about to be refreshed."
|
||||
);
|
||||
expect(".o_dialog footer button").toHaveText("Close");
|
||||
await click(".o_dialog footer button");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["location reload"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,529 @@
|
|||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { manuallyDispatchProgrammaticEvent } from "@odoo/hoot-dom";
|
||||
import { Deferred, advanceTime, animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, OwlError, onError, onWillStart, xml } from "@odoo/owl";
|
||||
import {
|
||||
makeMockEnv,
|
||||
mockService,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import {
|
||||
ClientErrorDialog,
|
||||
RPCErrorDialog,
|
||||
standardErrorDialogProps,
|
||||
} from "@web/core/errors/error_dialogs";
|
||||
import { UncaughtPromiseError } from "@web/core/errors/error_service";
|
||||
import { ConnectionLostError, RPCError } from "@web/core/network/rpc";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { omit } from "@web/core/utils/objects";
|
||||
|
||||
const errorDialogRegistry = registry.category("error_dialogs");
|
||||
const errorHandlerRegistry = registry.category("error_handlers");
|
||||
|
||||
test("can handle rejected promise errors with a string as reason", async () => {
|
||||
expect.assertions(2);
|
||||
expect.errors(1);
|
||||
await makeMockEnv();
|
||||
errorHandlerRegistry.add(
|
||||
"__test_handler__",
|
||||
(env, err, originalError) => {
|
||||
expect(originalError).toBe("-- something went wrong --");
|
||||
},
|
||||
{ sequence: 0 }
|
||||
);
|
||||
Promise.reject("-- something went wrong --");
|
||||
await animationFrame();
|
||||
expect.verifyErrors(["-- something went wrong --"]);
|
||||
});
|
||||
|
||||
test("handle RPC_ERROR of type='server' and no associated dialog class", async () => {
|
||||
expect.assertions(5);
|
||||
expect.errors(1);
|
||||
const error = new RPCError();
|
||||
error.code = 701;
|
||||
error.message = "Some strange error occured";
|
||||
error.data = { debug: "somewhere" };
|
||||
error.subType = "strange_error";
|
||||
error.id = 12;
|
||||
error.model = "some model";
|
||||
|
||||
mockService("dialog", {
|
||||
add(dialogClass, props) {
|
||||
expect(dialogClass).toBe(RPCErrorDialog);
|
||||
expect(omit(props, "traceback", "serverHost")).toEqual({
|
||||
name: "RPC_ERROR",
|
||||
type: "server",
|
||||
code: 701,
|
||||
data: {
|
||||
debug: "somewhere",
|
||||
},
|
||||
subType: "strange_error",
|
||||
message: "Some strange error occured",
|
||||
exceptionName: null,
|
||||
id: 12,
|
||||
model: "some model",
|
||||
});
|
||||
expect(props.traceback).toMatch(/RPC_ERROR/);
|
||||
expect(props.traceback).toMatch(/Some strange error occured/);
|
||||
},
|
||||
});
|
||||
await makeMockEnv();
|
||||
Promise.reject(error);
|
||||
await animationFrame();
|
||||
expect.verifyErrors(["RPC_ERROR: Some strange error occured"]);
|
||||
});
|
||||
|
||||
test("handle custom RPC_ERROR of type='server' and associated custom dialog class", async () => {
|
||||
expect.assertions(5);
|
||||
expect.errors(1);
|
||||
class CustomDialog extends Component {
|
||||
static template = xml`<RPCErrorDialog title="'Strange Error'"/>`;
|
||||
static components = { RPCErrorDialog };
|
||||
static props = { ...standardErrorDialogProps };
|
||||
}
|
||||
const error = new RPCError();
|
||||
error.code = 701;
|
||||
error.message = "Some strange error occured";
|
||||
error.id = 12;
|
||||
error.model = "some model";
|
||||
const errorData = {
|
||||
context: { exception_class: "strange_error" },
|
||||
name: "strange_error",
|
||||
};
|
||||
error.data = errorData;
|
||||
|
||||
mockService("dialog", {
|
||||
add(dialogClass, props) {
|
||||
expect(dialogClass).toBe(CustomDialog);
|
||||
expect(omit(props, "traceback", "serverHost")).toEqual({
|
||||
name: "RPC_ERROR",
|
||||
type: "server",
|
||||
code: 701,
|
||||
data: errorData,
|
||||
subType: null,
|
||||
message: "Some strange error occured",
|
||||
exceptionName: null,
|
||||
id: 12,
|
||||
model: "some model",
|
||||
});
|
||||
expect(props.traceback).toMatch(/RPC_ERROR/);
|
||||
expect(props.traceback).toMatch(/Some strange error occured/);
|
||||
},
|
||||
});
|
||||
await makeMockEnv();
|
||||
errorDialogRegistry.add("strange_error", CustomDialog);
|
||||
Promise.reject(error);
|
||||
await animationFrame();
|
||||
expect.verifyErrors(["RPC_ERROR: Some strange error occured"]);
|
||||
});
|
||||
|
||||
test("handle normal RPC_ERROR of type='server' and associated custom dialog class", async () => {
|
||||
expect.assertions(5);
|
||||
expect.errors(1);
|
||||
class CustomDialog extends Component {
|
||||
static template = xml`<RPCErrorDialog title="'Strange Error'"/>`;
|
||||
static components = { RPCErrorDialog };
|
||||
static props = ["*"];
|
||||
}
|
||||
class NormalDialog extends Component {
|
||||
static template = xml`<RPCErrorDialog title="'Normal Error'"/>`;
|
||||
static components = { RPCErrorDialog };
|
||||
static props = ["*"];
|
||||
}
|
||||
const error = new RPCError();
|
||||
error.code = 701;
|
||||
error.message = "A normal error occured";
|
||||
const errorData = {
|
||||
context: { exception_class: "strange_error" },
|
||||
};
|
||||
error.exceptionName = "normal_error";
|
||||
error.data = errorData;
|
||||
error.id = 12;
|
||||
error.model = "some model";
|
||||
mockService("dialog", {
|
||||
add(dialogClass, props) {
|
||||
expect(dialogClass).toBe(NormalDialog);
|
||||
expect(omit(props, "traceback", "serverHost")).toEqual({
|
||||
name: "RPC_ERROR",
|
||||
type: "server",
|
||||
code: 701,
|
||||
data: errorData,
|
||||
subType: null,
|
||||
message: "A normal error occured",
|
||||
exceptionName: "normal_error",
|
||||
id: 12,
|
||||
model: "some model",
|
||||
});
|
||||
expect(props.traceback).toMatch(/RPC_ERROR/);
|
||||
expect(props.traceback).toMatch(/A normal error occured/);
|
||||
},
|
||||
});
|
||||
await makeMockEnv();
|
||||
errorDialogRegistry.add("strange_error", CustomDialog);
|
||||
errorDialogRegistry.add("normal_error", NormalDialog);
|
||||
Promise.reject(error);
|
||||
await animationFrame();
|
||||
expect.verifyErrors(["RPC_ERROR: A normal error occured"]);
|
||||
});
|
||||
|
||||
test("handle CONNECTION_LOST_ERROR", async () => {
|
||||
expect.errors(1);
|
||||
mockService("notification", {
|
||||
add(message) {
|
||||
expect.step(`create (${message})`);
|
||||
return () => {
|
||||
expect.step(`close`);
|
||||
};
|
||||
},
|
||||
});
|
||||
const values = [false, true]; // simulate the 'back online status' after 2 'version_info' calls
|
||||
onRpc("/web/webclient/version_info", async () => {
|
||||
expect.step("version_info");
|
||||
const online = values.shift();
|
||||
if (online) {
|
||||
return true;
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
});
|
||||
|
||||
await makeMockEnv();
|
||||
const error = new ConnectionLostError("/fake_url");
|
||||
Promise.reject(error);
|
||||
await animationFrame();
|
||||
patchWithCleanup(Math, {
|
||||
random: () => 0,
|
||||
});
|
||||
// wait for timeouts
|
||||
await advanceTime(2000);
|
||||
await advanceTime(3500);
|
||||
expect.verifySteps([
|
||||
"create (Connection lost. Trying to reconnect...)",
|
||||
"version_info",
|
||||
"version_info",
|
||||
"close",
|
||||
"create (Connection restored. You are back online.)",
|
||||
]);
|
||||
expect.verifyErrors([
|
||||
`Error: Connection to "/fake_url" couldn't be established or was interrupted`,
|
||||
]);
|
||||
});
|
||||
|
||||
test("will let handlers from the registry handle errors first", async () => {
|
||||
expect.assertions(4);
|
||||
expect.errors(1);
|
||||
const testEnv = await makeMockEnv();
|
||||
testEnv.someValue = 14;
|
||||
errorHandlerRegistry.add("__test_handler__", (env, err, originalError) => {
|
||||
expect(originalError).toBe(error);
|
||||
expect(env.someValue).toBe(14);
|
||||
expect.step("in handler");
|
||||
return true;
|
||||
});
|
||||
const error = new Error();
|
||||
error.name = "boom";
|
||||
|
||||
Promise.reject(error);
|
||||
await animationFrame();
|
||||
expect.verifyErrors(["boom"]);
|
||||
expect.verifySteps(["in handler"]);
|
||||
});
|
||||
|
||||
test("originalError is the root cause of the error chain", async () => {
|
||||
expect.assertions(10);
|
||||
expect.errors(2);
|
||||
await makeMockEnv();
|
||||
const error = new Error();
|
||||
error.name = "boom";
|
||||
errorHandlerRegistry.add("__test_handler__", (env, err, originalError) => {
|
||||
expect(err).toBeInstanceOf(UncaughtPromiseError); // Wrapped by error service
|
||||
expect(err.cause).toBeInstanceOf(OwlError); // Wrapped by owl
|
||||
expect(err.cause.cause).toBe(originalError); // original error
|
||||
expect.step("in handler");
|
||||
return true;
|
||||
});
|
||||
|
||||
class ErrHandler extends Component {
|
||||
static template = xml`<t t-component="props.comp"/>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
onError(async (err) => {
|
||||
Promise.reject(err);
|
||||
await animationFrame();
|
||||
prom.resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
class ThrowInSetup extends Component {
|
||||
static template = xml``;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let prom = new Deferred();
|
||||
mountWithCleanup(ErrHandler, { props: { comp: ThrowInSetup } });
|
||||
await prom;
|
||||
expect.verifyErrors([
|
||||
`Error: An error occured in the owl lifecycle (see this Error's "cause" property)`,
|
||||
]);
|
||||
expect.verifySteps(["in handler"]);
|
||||
|
||||
class ThrowInWillStart extends Component {
|
||||
static template = xml``;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
onWillStart(() => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
prom = new Deferred();
|
||||
mountWithCleanup(ErrHandler, { props: { comp: ThrowInWillStart } });
|
||||
await prom;
|
||||
expect.verifyErrors([`Error: The following error occurred in onWillStart: ""`]);
|
||||
expect.verifySteps(["in handler"]);
|
||||
});
|
||||
|
||||
test("handle uncaught promise errors", async () => {
|
||||
expect.assertions(5);
|
||||
expect.errors(1);
|
||||
class TestError extends Error {}
|
||||
const error = new TestError();
|
||||
error.message = "This is an error test";
|
||||
error.name = "TestError";
|
||||
|
||||
mockService("dialog", {
|
||||
add(dialogClass, props) {
|
||||
expect(dialogClass).toBe(ClientErrorDialog);
|
||||
expect(omit(props, "traceback", "serverHost")).toEqual({
|
||||
name: "UncaughtPromiseError > TestError",
|
||||
message: "Uncaught Promise > This is an error test",
|
||||
});
|
||||
expect(props.traceback).toMatch(/TestError/);
|
||||
expect(props.traceback).toMatch(/This is an error test/);
|
||||
},
|
||||
});
|
||||
await makeMockEnv();
|
||||
|
||||
Promise.reject(error);
|
||||
await animationFrame();
|
||||
expect.verifyErrors(["TestError: This is an error test"]);
|
||||
});
|
||||
|
||||
test("handle uncaught client errors", async () => {
|
||||
expect.assertions(4);
|
||||
expect.errors(1);
|
||||
class TestError extends Error {}
|
||||
const error = new TestError();
|
||||
error.message = "This is an error test";
|
||||
error.name = "TestError";
|
||||
|
||||
mockService("dialog", {
|
||||
add(dialogClass, props) {
|
||||
expect(dialogClass).toBe(ClientErrorDialog);
|
||||
expect(props.name).toBe("UncaughtClientError > TestError");
|
||||
expect(props.message).toBe("Uncaught Javascript Error > This is an error test");
|
||||
},
|
||||
});
|
||||
await makeMockEnv();
|
||||
|
||||
setTimeout(() => {
|
||||
throw error;
|
||||
});
|
||||
await animationFrame();
|
||||
expect.verifyErrors(["TestError: This is an error test"]);
|
||||
});
|
||||
|
||||
test("don't show dialog for errors in third-party scripts", async () => {
|
||||
expect.errors(1);
|
||||
class TestError extends Error {}
|
||||
const error = new TestError();
|
||||
error.name = "Script error.";
|
||||
|
||||
mockService("dialog", {
|
||||
add(_dialogClass, props) {
|
||||
throw new Error("should not pass here");
|
||||
},
|
||||
});
|
||||
await makeMockEnv();
|
||||
|
||||
// Error events from errors in third-party scripts have no colno, no lineno and no filename
|
||||
// because of CORS.
|
||||
await manuallyDispatchProgrammaticEvent(window, "error", { error });
|
||||
await animationFrame();
|
||||
expect.verifyErrors(["Script error."]);
|
||||
});
|
||||
|
||||
test("show dialog for errors in third-party scripts in debug mode", async () => {
|
||||
expect.errors(1);
|
||||
class TestError extends Error {}
|
||||
const error = new TestError();
|
||||
error.name = "Script error.";
|
||||
serverState.debug = "1";
|
||||
|
||||
mockService("dialog", {
|
||||
add(_dialogClass, props) {
|
||||
expect.step("Dialog: " + props.message);
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
await makeMockEnv();
|
||||
|
||||
// Error events from errors in third-party scripts have no colno, no lineno and no filename
|
||||
// because of CORS.
|
||||
await manuallyDispatchProgrammaticEvent(window, "error", { error });
|
||||
await animationFrame();
|
||||
expect.verifyErrors(["Script error."]);
|
||||
expect.verifySteps(["Dialog: Third-Party Script Error"]);
|
||||
});
|
||||
|
||||
test("lazy loaded handlers", async () => {
|
||||
expect.assertions(3);
|
||||
expect.errors(2);
|
||||
await makeMockEnv();
|
||||
|
||||
Promise.reject(new Error("error"));
|
||||
await animationFrame();
|
||||
|
||||
expect.verifyErrors(["Error: error"]);
|
||||
|
||||
errorHandlerRegistry.add("__test_handler__", () => {
|
||||
expect.step("in handler");
|
||||
return true;
|
||||
});
|
||||
|
||||
Promise.reject(new Error("error"));
|
||||
await animationFrame();
|
||||
|
||||
expect.verifyErrors(["Error: error"]);
|
||||
expect.verifySteps(["in handler"]);
|
||||
});
|
||||
|
||||
let unhandledRejectionCb;
|
||||
let errorCb;
|
||||
|
||||
describe("Error Service Logs", () => {
|
||||
beforeEach(() => {
|
||||
patchWithCleanup(browser, {
|
||||
addEventListener: (type, cb) => {
|
||||
if (type === "unhandledrejection") {
|
||||
unhandledRejectionCb = cb;
|
||||
} else if (type === "error") {
|
||||
errorCb = cb;
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("logs the traceback of the full error chain for unhandledrejection", async () => {
|
||||
expect.assertions(2);
|
||||
const regexParts = [
|
||||
/^.*This is a wrapper error/,
|
||||
/Caused by:.*This is a second wrapper error/,
|
||||
/Caused by:.*This is the original error/,
|
||||
];
|
||||
const errorRegex = new RegExp(regexParts.map((re) => re.source).join(/[\s\S]*/.source));
|
||||
patchWithCleanup(console, {
|
||||
error(errorMessage) {
|
||||
expect(errorMessage).toMatch(errorRegex);
|
||||
},
|
||||
});
|
||||
|
||||
const error = new Error("This is a wrapper error");
|
||||
error.cause = new Error("This is a second wrapper error");
|
||||
error.cause.cause = new Error("This is the original error");
|
||||
|
||||
await makeMockEnv();
|
||||
const errorEvent = new PromiseRejectionEvent("unhandledrejection", {
|
||||
reason: error,
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
expect(errorEvent.defaultPrevented).toBe(true);
|
||||
});
|
||||
|
||||
test("logs the traceback of the full error chain for uncaughterror", async () => {
|
||||
expect.assertions(2);
|
||||
const regexParts = [
|
||||
/^.*This is a wrapper error/,
|
||||
/Caused by:.*This is a second wrapper error/,
|
||||
/Caused by:.*This is the original error/,
|
||||
];
|
||||
const errorRegex = new RegExp(regexParts.map((re) => re.source).join(/[\s\S]*/.source));
|
||||
patchWithCleanup(console, {
|
||||
error(errorMessage) {
|
||||
expect(errorMessage).toMatch(errorRegex);
|
||||
},
|
||||
});
|
||||
|
||||
const error = new Error("This is a wrapper error");
|
||||
error.cause = new Error("This is a second wrapper error");
|
||||
error.cause.cause = new Error("This is the original error");
|
||||
|
||||
await makeMockEnv();
|
||||
const errorEvent = new Event("error", {
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
});
|
||||
errorEvent.error = error;
|
||||
errorEvent.filename = "dummy_file.js"; // needed to not be treated as a CORS error
|
||||
await errorCb(errorEvent);
|
||||
expect(errorEvent.defaultPrevented).toBe(true);
|
||||
});
|
||||
|
||||
test("error in handlers while handling an error", async () => {
|
||||
// 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.
|
||||
patchWithCleanup(console, {
|
||||
error(errorMessage) {
|
||||
expect(errorMessage).toMatch(
|
||||
new RegExp(
|
||||
`^@web/core/error_service: handler "__test_handler__" failed with "Error: Boom in handler" while trying to handle:\nError: Genuine Business Boom.*`
|
||||
)
|
||||
);
|
||||
expect.step("error logged");
|
||||
},
|
||||
});
|
||||
|
||||
await makeMockEnv();
|
||||
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);
|
||||
expect(errorEvent.defaultPrevented).toBe(true);
|
||||
expect.verifySteps(["error logged"]);
|
||||
|
||||
errorEvent = new PromiseRejectionEvent("unhandledrejection", {
|
||||
promise: null,
|
||||
cancelable: true,
|
||||
reason: new Error("Genuine Business Boom"),
|
||||
});
|
||||
await unhandledRejectionCb(errorEvent);
|
||||
expect(errorEvent.defaultPrevented).toBe(true);
|
||||
expect.verifySteps(["error logged"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,449 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { click, edit, queryAllTexts, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import {
|
||||
Country,
|
||||
Partner,
|
||||
Player,
|
||||
Product,
|
||||
Stage,
|
||||
Team,
|
||||
addNewRule,
|
||||
clearNotSupported,
|
||||
clickOnButtonAddBranch,
|
||||
clickOnButtonAddNewRule,
|
||||
clickOnButtonDeleteNode,
|
||||
editValue,
|
||||
getOperatorOptions,
|
||||
getTreeEditorContent,
|
||||
getValueOptions,
|
||||
isNotSupportedPath,
|
||||
openModelFieldSelectorPopover,
|
||||
selectOperator,
|
||||
SELECTORS as treeEditorSELECTORS,
|
||||
} from "@web/../tests/core/tree_editor/condition_tree_editor_test_helpers";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
mountWithCleanup,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { ExpressionEditor } from "@web/core/expression_editor/expression_editor";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { pick } from "@web/core/utils/objects";
|
||||
|
||||
const SELECTORS = {
|
||||
...treeEditorSELECTORS,
|
||||
debugArea: ".o_expression_editor_debug_container textarea",
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
async function editExpression(value) {
|
||||
await click(SELECTORS.complexConditionInput);
|
||||
|
||||
await edit(value);
|
||||
await animationFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
async function selectConnector(value) {
|
||||
await contains(`${SELECTORS.connector} .dropdown-toggle`).click();
|
||||
await contains(`.dropdown-menu .dropdown-item:contains(${value})`).click();
|
||||
}
|
||||
|
||||
async function makeExpressionEditor(params = {}) {
|
||||
const fieldFilters = params.fieldFilters;
|
||||
delete params.fieldFilters;
|
||||
const props = { ...params };
|
||||
class Parent extends Component {
|
||||
static components = { ExpressionEditor };
|
||||
static template = xml`<ExpressionEditor t-props="expressionEditorProps"/>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.expressionEditorProps = {
|
||||
expression: "1",
|
||||
...props,
|
||||
resModel: "partner",
|
||||
update: (expression) => {
|
||||
if (props.update) {
|
||||
props.update(expression);
|
||||
}
|
||||
this.expressionEditorProps.expression = expression;
|
||||
this.render();
|
||||
},
|
||||
};
|
||||
this.expressionEditorProps.fields = fieldFilters
|
||||
? pick(Partner._fields, ...fieldFilters)
|
||||
: Partner._fields;
|
||||
}
|
||||
async set(expression) {
|
||||
this.expressionEditorProps.expression = expression;
|
||||
this.render();
|
||||
await animationFrame();
|
||||
}
|
||||
}
|
||||
|
||||
return mountWithCleanup(Parent, { props });
|
||||
}
|
||||
|
||||
defineModels([Partner, Product, Team, Player, Country, Stage]);
|
||||
|
||||
beforeEach(() => {
|
||||
serverState.debug = "1";
|
||||
});
|
||||
|
||||
test("rendering of truthy values", async () => {
|
||||
const toTests = [`True`, `true`, `1`, `-1`, `"a"`];
|
||||
const parent = await makeExpressionEditor();
|
||||
for (const expr of toTests) {
|
||||
await parent.set(expr);
|
||||
expect(getTreeEditorContent()).toEqual([{ level: 0, value: "all" }]);
|
||||
}
|
||||
});
|
||||
|
||||
test("rendering of falsy values", async () => {
|
||||
const toTests = [`False`, `false`, `0`, `""`];
|
||||
const parent = await makeExpressionEditor();
|
||||
for (const expr of toTests) {
|
||||
await parent.set(expr);
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ value: "all", level: 0 },
|
||||
{ value: ["0", "=", "1"], level: 1 },
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test("rendering of 'expr'", async () => {
|
||||
serverState.debug = "";
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
]);
|
||||
expect(queryOne(SELECTORS.complexConditionInput).readOnly).toBe(true);
|
||||
});
|
||||
|
||||
test("rendering of 'expr' in dev mode", async () => {
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
]);
|
||||
expect(queryOne(SELECTORS.complexConditionInput).readOnly).toBe(false);
|
||||
});
|
||||
|
||||
test("edit a complex condition in dev mode", async () => {
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
expect(SELECTORS.condition).toHaveCount(0);
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
]);
|
||||
await editExpression("uid");
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "uid", level: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("delete a complex condition", async () => {
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
]);
|
||||
await clickOnButtonDeleteNode();
|
||||
expect(getTreeEditorContent()).toEqual([{ value: "all", level: 0 }]);
|
||||
});
|
||||
|
||||
test("copy a complex condition", async () => {
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
expect(SELECTORS.condition).toHaveCount(0);
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
]);
|
||||
await clickOnButtonAddNewRule();
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
{ value: "expr", level: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("change path, operator and value", async () => {
|
||||
serverState.debug = "";
|
||||
await makeExpressionEditor({ expression: `bar != "blabla"` });
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: ["Bar", "is not", "blabla"] },
|
||||
]);
|
||||
const tree = getTreeEditorContent({ node: true });
|
||||
await openModelFieldSelectorPopover();
|
||||
await contains(".o_model_field_selector_popover_item_name:eq(5)").click();
|
||||
await selectOperator("not in", 0, tree[1].node);
|
||||
await editValue(["Doku", "Lukaku", "KDB"]);
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: ["Foo", "is not in", "Doku,Lukaku,KDB"] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("create a new branch from a complex condition control panel", async () => {
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ value: "all", level: 0 },
|
||||
{ value: "expr", level: 1 },
|
||||
]);
|
||||
await clickOnButtonAddBranch();
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: "expr" },
|
||||
{ level: 1, value: "any" },
|
||||
{ level: 2, value: ["Id", "=", "1"] },
|
||||
{ level: 2, value: ["Id", "=", "1"] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("rendering of a valid fieldName in fields", async () => {
|
||||
const parent = await makeExpressionEditor({ fieldFilters: ["foo"] });
|
||||
|
||||
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();
|
||||
if (condition) {
|
||||
expect(tree).toEqual([
|
||||
{ value: "all", level: 0 },
|
||||
{ value: condition, level: 1 },
|
||||
]);
|
||||
} else if (complexCondition) {
|
||||
expect(tree).toEqual([
|
||||
{ value: "all", level: 0 },
|
||||
{ value: complexCondition, level: 1 },
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("rendering of simple conditions", async () => {
|
||||
Partner._fields.bar = fields.Char();
|
||||
Partner._records = [];
|
||||
const parent = await makeExpressionEditor({ fieldFilters: ["foo", "bar"] });
|
||||
|
||||
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();
|
||||
if (condition) {
|
||||
expect(tree).toEqual([
|
||||
{ value: "all", level: 0 },
|
||||
{ value: condition, level: 1 },
|
||||
]);
|
||||
} else if (complexCondition) {
|
||||
expect(tree).toEqual([
|
||||
{ value: "all", level: 0 },
|
||||
{ value: complexCondition, level: 1 },
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("rendering of connectors", async () => {
|
||||
await makeExpressionEditor({ expression: `expr and foo == "abc" or not bar` });
|
||||
expect(queryAllTexts(`${SELECTORS.connector} .dropdown-toggle`)).toEqual(["any", "all"]);
|
||||
const tree = getTreeEditorContent();
|
||||
expect(tree).toEqual([
|
||||
{ level: 0, value: "any" },
|
||||
{ level: 1, value: "all" },
|
||||
{ level: 2, value: "expr" },
|
||||
{ level: 2, value: ["Foo", "=", "abc"] },
|
||||
{ level: 1, value: ["Bar", "is", "not set"] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("rendering of connectors (2)", async () => {
|
||||
await makeExpressionEditor({
|
||||
expression: `not (expr or foo == "abc")`,
|
||||
update(expression) {
|
||||
expect.step(expression);
|
||||
},
|
||||
});
|
||||
expect(`${SELECTORS.connector} .dropdown-toggle:only`).toHaveText("none");
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ level: 0, value: "none" },
|
||||
{ level: 1, value: "expr" },
|
||||
{ level: 1, value: ["Foo", "=", "abc"] },
|
||||
]);
|
||||
expect.verifySteps([]);
|
||||
expect(queryOne(SELECTORS.debugArea)).toHaveValue(`not (expr or foo == "abc")`);
|
||||
|
||||
await selectConnector("all");
|
||||
expect(`${SELECTORS.connector} .dropdown-toggle:only`).toHaveText("all");
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: "expr" },
|
||||
{ level: 1, value: ["Foo", "=", "abc"] },
|
||||
]);
|
||||
expect.verifySteps([`expr and foo == "abc"`]);
|
||||
expect(queryOne(SELECTORS.debugArea)).toHaveValue(`expr and foo == "abc"`);
|
||||
});
|
||||
|
||||
test("rendering of if else", async () => {
|
||||
await makeExpressionEditor({ expression: `True if False else False` });
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ 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"] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("check condition by default when creating a new rule", async () => {
|
||||
serverState.debug = "";
|
||||
Partner._fields.country_id = fields.Char({ string: "Country ID" });
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
await contains("a[role='button']").click();
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: "expr" },
|
||||
{ level: 1, value: ["Country ID", "=", ""] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("allow selection of boolean field", async () => {
|
||||
await makeExpressionEditor({ expression: "id" });
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: ["Id", "is set"] },
|
||||
]);
|
||||
await openModelFieldSelectorPopover();
|
||||
await contains(".o_model_field_selector_popover_item_name").click();
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: ["Bar", "is", "set"] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("render false and true leaves", async () => {
|
||||
await makeExpressionEditor({ expression: `False and True` });
|
||||
expect(getOperatorOptions()).toEqual(["="]);
|
||||
expect(getValueOptions()).toEqual(["1"]);
|
||||
expect(getOperatorOptions(-1)).toEqual(["="]);
|
||||
expect(getValueOptions(-1)).toEqual(["1"]);
|
||||
});
|
||||
|
||||
test("no field of type properties in model field selector", async () => {
|
||||
serverState.debug = "";
|
||||
Partner._fields.properties = fields.Properties({
|
||||
string: "Properties",
|
||||
definition_record: "product_id",
|
||||
definition_record_field: "definitions",
|
||||
});
|
||||
Product._fields.definitions = fields.PropertiesDefinition();
|
||||
await makeExpressionEditor({
|
||||
expression: `properties`,
|
||||
fieldFilters: ["foo", "bar", "properties"],
|
||||
update(expression) {
|
||||
expect.step(expression);
|
||||
},
|
||||
});
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: ["Properties", "is set"] },
|
||||
]);
|
||||
expect(isNotSupportedPath()).toBe(true);
|
||||
await clearNotSupported();
|
||||
expect.verifySteps([`foo == ""`]);
|
||||
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(queryAllTexts(".o_model_field_selector_popover_item_name")).toEqual(["Bar", "Foo"]);
|
||||
});
|
||||
|
||||
test("no special fields in fields", async () => {
|
||||
serverState.debug = "";
|
||||
await makeExpressionEditor({
|
||||
expression: `bar`,
|
||||
fieldFilters: ["foo", "bar", "properties"],
|
||||
update(expression) {
|
||||
expect.step(expression);
|
||||
},
|
||||
});
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: ["Bar", "is not", "not set"] },
|
||||
]);
|
||||
await addNewRule();
|
||||
expect(getTreeEditorContent()).toEqual([
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: ["Bar", "is not", "not set"] },
|
||||
{ level: 1, value: ["Foo", "=", ""] },
|
||||
]);
|
||||
expect.verifySteps([`bar and foo == ""`]);
|
||||
});
|
||||
|
||||
test("between operator", async () => {
|
||||
await makeExpressionEditor({
|
||||
expression: `id == 1`,
|
||||
update(expression) {
|
||||
expect.step(expression);
|
||||
},
|
||||
});
|
||||
expect(getOperatorOptions()).toEqual([
|
||||
"=",
|
||||
"!=",
|
||||
">",
|
||||
">=",
|
||||
"<",
|
||||
"<=",
|
||||
"is between",
|
||||
"is set",
|
||||
"is not set",
|
||||
]);
|
||||
expect.verifySteps([]);
|
||||
await selectOperator("between");
|
||||
expect.verifySteps([`id >= 1 and id <= 1`]);
|
||||
});
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import { expect, test, describe } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
mountWithCleanup,
|
||||
makeDialogMockEnv,
|
||||
mockService,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import {
|
||||
Country,
|
||||
Partner,
|
||||
Player,
|
||||
Product,
|
||||
Stage,
|
||||
Team,
|
||||
getTreeEditorContent,
|
||||
} from "@web/../tests/core/tree_editor/condition_tree_editor_test_helpers";
|
||||
|
||||
import { ExpressionEditorDialog } from "@web/core/expression_editor_dialog/expression_editor_dialog";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
async function makeExpressionEditorDialog(params = {}) {
|
||||
const props = { ...params };
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { ExpressionEditorDialog };
|
||||
static template = xml`<ExpressionEditorDialog t-props="expressionEditorProps"/>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.expressionEditorProps = {
|
||||
expression: "1",
|
||||
close: () => {},
|
||||
onConfirm: () => {},
|
||||
...props,
|
||||
resModel: "partner",
|
||||
};
|
||||
this.expressionEditorProps.fields = Partner._fields;
|
||||
}
|
||||
async set(expression) {
|
||||
this.expressionEditorProps.expression = expression;
|
||||
this.render();
|
||||
await animationFrame();
|
||||
}
|
||||
}
|
||||
const env = await makeDialogMockEnv();
|
||||
return mountWithCleanup(Parent, { env, props });
|
||||
}
|
||||
|
||||
defineModels([Partner, Product, Team, Player, Country, Stage]);
|
||||
|
||||
test("expr well sent, onConfirm and onClose", async () => {
|
||||
const expression = `foo == 'batestr' and bar == True`;
|
||||
await makeExpressionEditorDialog({
|
||||
expression,
|
||||
close: () => {
|
||||
expect.step("close");
|
||||
},
|
||||
onConfirm: (result) => {
|
||||
expect.step(result);
|
||||
},
|
||||
});
|
||||
expect(".o_technical_modal").toHaveCount(1);
|
||||
await contains(".o_dialog footer button").click();
|
||||
expect.verifySteps([expression, "close"]);
|
||||
});
|
||||
|
||||
test("expr well sent but wrong, so notification when onConfirm", async () => {
|
||||
const expression = `foo == 'bar' and bar = True`;
|
||||
mockService("notification", {
|
||||
add(message, options) {
|
||||
expect(message).toBe("Expression is invalid. Please correct it");
|
||||
expect(options).toEqual({ type: "danger" });
|
||||
expect.step("notification");
|
||||
},
|
||||
});
|
||||
await makeExpressionEditorDialog({
|
||||
expression,
|
||||
});
|
||||
expect(".o_technical_modal").toHaveCount(1);
|
||||
await contains(".modal-footer button").click();
|
||||
await contains(".modal-body button").click();
|
||||
expect(getTreeEditorContent()).toEqual([{ level: 0, value: "all" }]);
|
||||
expect.verifySteps(["notification"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
getService,
|
||||
makeMockEnv,
|
||||
MockServer,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Deferred, animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
function getModelInfo(resModel) {
|
||||
return {
|
||||
resModel: resModel._name,
|
||||
fieldDefs: JSON.parse(JSON.stringify(resModel._fields)),
|
||||
};
|
||||
}
|
||||
|
||||
function getDefinitions() {
|
||||
const fieldDefs = {};
|
||||
for (const record of MockServer.env["species"]) {
|
||||
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 };
|
||||
}
|
||||
|
||||
class Tortoise extends models.Model {
|
||||
name = fields.Char();
|
||||
age = fields.Integer();
|
||||
location_id = fields.Many2one({ string: "Location", relation: "location" });
|
||||
species = fields.Many2one({ relation: "species" });
|
||||
property_field = fields.Properties({
|
||||
string: "Properties",
|
||||
definition_record: "species",
|
||||
definition_record_field: "definitions",
|
||||
});
|
||||
}
|
||||
|
||||
class Location extends models.Model {
|
||||
name = fields.Char();
|
||||
tortoise_ids = fields.One2many({ string: "Turtles", relation: "tortoise" });
|
||||
}
|
||||
|
||||
class Species extends models.Model {
|
||||
name = fields.Char();
|
||||
definitions = fields.PropertiesDefinition();
|
||||
|
||||
_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" },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Tortoise, Location, Species]);
|
||||
|
||||
test("loadPath", async () => {
|
||||
await makeMockEnv();
|
||||
|
||||
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()],
|
||||
},
|
||||
},
|
||||
];
|
||||
for (const { resModel, path, expectedResult } of toTest) {
|
||||
const result = await getService("field").loadPath(resModel, path);
|
||||
expect(result).toEqual(expectedResult);
|
||||
}
|
||||
|
||||
const errorToTest = [
|
||||
{ resModel: "notAModel" },
|
||||
{ resModel: "tortoise", path: {} },
|
||||
{ resModel: "tortoise", path: "" },
|
||||
];
|
||||
|
||||
for (const { resModel, path } of errorToTest) {
|
||||
try {
|
||||
await getService("field").loadPath(resModel, path);
|
||||
} catch {
|
||||
expect.step("error");
|
||||
}
|
||||
}
|
||||
expect.verifySteps(errorToTest.map(() => "error"));
|
||||
});
|
||||
|
||||
test("store loadFields calls in cache in success", async () => {
|
||||
onRpc("fields_get", () => {
|
||||
expect.step("fields_get");
|
||||
});
|
||||
|
||||
await makeMockEnv();
|
||||
|
||||
await getService("field").loadFields("tortoise");
|
||||
await getService("field").loadFields("tortoise");
|
||||
|
||||
expect.verifySteps(["fields_get"]);
|
||||
});
|
||||
|
||||
test("does not store loadFields calls in cache when failed", async () => {
|
||||
onRpc("fields_get", () => {
|
||||
expect.step("fields_get");
|
||||
throw "my little error";
|
||||
});
|
||||
|
||||
await makeMockEnv();
|
||||
await expect(getService("field").loadFields("take.five")).rejects.toThrow(/my little error/);
|
||||
await expect(getService("field").loadFields("take.five")).rejects.toThrow(/my little error/);
|
||||
|
||||
expect.verifySteps(["fields_get", "fields_get"]);
|
||||
});
|
||||
|
||||
test("async method loadFields is protected", async () => {
|
||||
let callFieldService;
|
||||
class Child extends Component {
|
||||
static template = xml`
|
||||
<div class="o_child_component" />
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.fieldService = useService("field");
|
||||
callFieldService = async () => {
|
||||
expect.step("loadFields called");
|
||||
await this.fieldService.loadFields("tortoise");
|
||||
expect.step("loadFields result get");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { Child };
|
||||
static template = xml`
|
||||
<t t-if="state.displayChild">
|
||||
<Child />
|
||||
</t>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({ displayChild: true });
|
||||
}
|
||||
}
|
||||
|
||||
const def = new Deferred();
|
||||
onRpc(async () => {
|
||||
await def;
|
||||
});
|
||||
const parent = await mountWithCleanup(Parent);
|
||||
|
||||
expect(".o_child_component").toHaveCount(1);
|
||||
|
||||
callFieldService();
|
||||
expect.verifySteps(["loadFields called"]);
|
||||
|
||||
parent.state.displayChild = false;
|
||||
await animationFrame();
|
||||
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps([]);
|
||||
|
||||
try {
|
||||
await callFieldService();
|
||||
} catch (e) {
|
||||
expect.step(e.message);
|
||||
}
|
||||
|
||||
expect.verifySteps(["loadFields called", "Component is destroyed"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
contains,
|
||||
mockService,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { setInputFiles } from "@odoo/hoot-dom";
|
||||
import { Deferred, animationFrame } from "@odoo/hoot-mock";
|
||||
import { FileInput } from "@web/core/file_input/file_input";
|
||||
import { session } from "@web/session";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
async function createFileInput({ mockPost, mockAdd, props }) {
|
||||
mockService("notification", {
|
||||
add: mockAdd || (() => {}),
|
||||
});
|
||||
mockService("http", {
|
||||
post: mockPost || (() => {}),
|
||||
});
|
||||
await mountWithCleanup(FileInput, { props });
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Tests
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
beforeEach(() => {
|
||||
patchWithCleanup(odoo, { csrf_token: "dummy" });
|
||||
});
|
||||
|
||||
test("Upload a file: default props", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
await createFileInput({
|
||||
mockPost: (route, params) => {
|
||||
expect(params).toEqual({
|
||||
csrf_token: "dummy",
|
||||
ufile: [],
|
||||
});
|
||||
expect.step(route);
|
||||
return "[]";
|
||||
},
|
||||
props: {},
|
||||
});
|
||||
|
||||
expect(".o_file_input").toHaveText("Choose File", {
|
||||
message: "File input total text should match its given inner element's text",
|
||||
});
|
||||
expect(".o_file_input input").toHaveAttribute("accept", "*", {
|
||||
message: "Input should accept all files by default",
|
||||
});
|
||||
|
||||
await contains(".o_file_input input", { visible: false }).click();
|
||||
await setInputFiles([]);
|
||||
|
||||
expect(".o_file_input input").not.toHaveAttribute("multiple", null, {
|
||||
message: "'multiple' attribute should not be set",
|
||||
});
|
||||
expect.verifySteps(["/web/binary/upload_attachment"]);
|
||||
});
|
||||
|
||||
test("Upload a file: custom attachment", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
await createFileInput({
|
||||
props: {
|
||||
acceptedFileExtensions: ".png",
|
||||
multiUpload: true,
|
||||
resId: 5,
|
||||
resModel: "res.model",
|
||||
route: "/web/binary/upload",
|
||||
onUpload(files) {
|
||||
expect(files).toHaveLength(0, {
|
||||
message: "'files' property should be an empty array",
|
||||
});
|
||||
},
|
||||
},
|
||||
mockPost: (route, params) => {
|
||||
expect(params).toEqual({
|
||||
id: 5,
|
||||
model: "res.model",
|
||||
csrf_token: "dummy",
|
||||
ufile: [],
|
||||
});
|
||||
expect.step(route);
|
||||
return "[]";
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_file_input input").toHaveAttribute("accept", ".png", {
|
||||
message: "Input should now only accept pngs",
|
||||
});
|
||||
|
||||
await contains(".o_file_input input", { visible: false }).click();
|
||||
await setInputFiles([]);
|
||||
|
||||
expect(".o_file_input input").toHaveAttribute("multiple", null, {
|
||||
message: "'multiple' attribute should be set",
|
||||
});
|
||||
|
||||
expect.verifySteps(["/web/binary/upload"]);
|
||||
});
|
||||
|
||||
test("Hidden file input", async () => {
|
||||
await createFileInput({
|
||||
props: { hidden: true },
|
||||
});
|
||||
|
||||
expect(".o_file_input").not.toBeVisible();
|
||||
});
|
||||
|
||||
test("uploading the same file twice triggers the onChange twice", async () => {
|
||||
await createFileInput({
|
||||
props: {
|
||||
onUpload(files) {
|
||||
expect.step(files[0].name);
|
||||
},
|
||||
},
|
||||
mockPost: (_, params) => {
|
||||
return JSON.stringify([{ name: params.ufile[0].name }]);
|
||||
},
|
||||
});
|
||||
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await contains(".o_file_input input", { visible: false }).click();
|
||||
await setInputFiles([file]);
|
||||
await animationFrame();
|
||||
expect.verifySteps(["fake_file.txt"]);
|
||||
|
||||
await contains(".o_file_input input", { visible: false }).click();
|
||||
await setInputFiles([file]);
|
||||
await animationFrame();
|
||||
expect.verifySteps(["fake_file.txt"]);
|
||||
});
|
||||
|
||||
test("uploading a file that is too heavy will send a notification", async () => {
|
||||
patchWithCleanup(session, { max_file_upload_size: 2 });
|
||||
await createFileInput({
|
||||
props: {
|
||||
onUpload(files) {
|
||||
// This code should be unreachable in this case
|
||||
expect.step(files[0].name);
|
||||
},
|
||||
},
|
||||
mockPost: (_, params) => {
|
||||
return JSON.stringify([{ name: params.ufile[0].name }]);
|
||||
},
|
||||
mockAdd: (message) => {
|
||||
expect.step("notification");
|
||||
// Message is a bit weird because values (2 and 4 bytes) are simplified to 2 decimals in regards to megabytes
|
||||
expect(message).toBe(
|
||||
"The selected file (4B) is larger than the maximum allowed file size (2B)."
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
|
||||
await contains(".o_file_input input", { visible: false }).click();
|
||||
await setInputFiles([file]);
|
||||
await animationFrame();
|
||||
expect.verifySteps(["notification"]);
|
||||
});
|
||||
|
||||
test("Upload button is disabled if attachment upload is not finished", async () => {
|
||||
const uploadedPromise = new Deferred();
|
||||
await createFileInput({
|
||||
mockPost: async (route) => {
|
||||
if (route === "/web/binary/upload_attachment") {
|
||||
await uploadedPromise;
|
||||
}
|
||||
return "[]";
|
||||
},
|
||||
props: {},
|
||||
});
|
||||
//enable button
|
||||
await contains(".o_file_input input", { visible: false }).click();
|
||||
await setInputFiles([]);
|
||||
await animationFrame();
|
||||
|
||||
//disable button
|
||||
expect(".o_file_input input").not.toBeEnabled({
|
||||
message: "the upload button should be disabled on upload",
|
||||
});
|
||||
|
||||
uploadedPromise.resolve();
|
||||
await animationFrame();
|
||||
expect(".o_file_input input").toBeEnabled({
|
||||
message: "the upload button should be enabled for upload",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
|
||||
import { FileModel } from "@web/core/file_viewer/file_model";
|
||||
|
||||
test("url query params of FileModel returns proper params", () => {
|
||||
const attachmentData = {
|
||||
access_token: "4b52e31e-a155-4598-8d15-538f64f0fb7b",
|
||||
checksum: "f6a9d2bcbb34ce90a73785d8c8d1b82e5cdf0b5b",
|
||||
extension: "jpg",
|
||||
name: "test.jpg",
|
||||
mimetype: "image/jpeg",
|
||||
};
|
||||
const expectedQueryParams = {
|
||||
access_token: "4b52e31e-a155-4598-8d15-538f64f0fb7b",
|
||||
filename: "test.jpg",
|
||||
unique: "f6a9d2bcbb34ce90a73785d8c8d1b82e5cdf0b5b",
|
||||
};
|
||||
const fileModel = Object.assign(new FileModel(), attachmentData);
|
||||
expect(fileModel.urlQueryParams).toEqual(expectedQueryParams);
|
||||
});
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
contains,
|
||||
getService,
|
||||
mockService,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Deferred, animationFrame } from "@odoo/hoot-mock";
|
||||
import { FileUploadProgressContainer } from "@web/core/file_upload/file_upload_progress_container";
|
||||
import { FileUploadProgressRecord } from "@web/core/file_upload/file_upload_progress_record";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
class FileUploadProgressTestRecord extends FileUploadProgressRecord {
|
||||
static template = xml`
|
||||
<t t-set="progressTexts" t-value="getProgressTexts()"/>
|
||||
<div class="file_upload">
|
||||
<div class="file_upload_progress_text_left" t-esc="progressTexts.left"/>
|
||||
<div class="file_upload_progress_text_right" t-esc="progressTexts.right"/>
|
||||
<FileUploadProgressBar fileUpload="props.fileUpload"/>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
class Parent extends Component {
|
||||
static components = {
|
||||
FileUploadProgressContainer,
|
||||
};
|
||||
static template = xml`
|
||||
<div class="parent">
|
||||
<FileUploadProgressContainer fileUploads="fileUploadService.uploads" shouldDisplay="props.shouldDisplay" Component="FileUploadProgressTestRecord"/>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.fileUploadService = useService("file_upload");
|
||||
this.FileUploadProgressTestRecord = FileUploadProgressTestRecord;
|
||||
}
|
||||
}
|
||||
|
||||
onRpc("/test/", () => new Deferred());
|
||||
|
||||
test("can be rendered", async () => {
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".parent").toHaveCount(1);
|
||||
expect(".file_upload").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("upload renders new component(s)", async () => {
|
||||
await mountWithCleanup(Parent);
|
||||
const fileUploadService = await getService("file_upload");
|
||||
fileUploadService.upload("/test/", []);
|
||||
await animationFrame();
|
||||
expect(".file_upload").toHaveCount(1);
|
||||
fileUploadService.upload("/test/", []);
|
||||
await animationFrame();
|
||||
expect(".file_upload").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("upload end removes component", async () => {
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
const fileUploadService = await getService("file_upload");
|
||||
fileUploadService.upload("/test/", []);
|
||||
await animationFrame();
|
||||
fileUploadService.uploads[1].xhr.dispatchEvent(new Event("load"));
|
||||
await animationFrame();
|
||||
expect(".file_upload").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("upload error removes component", async () => {
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
const fileUploadService = await getService("file_upload");
|
||||
fileUploadService.upload("/test/", []);
|
||||
await animationFrame();
|
||||
fileUploadService.uploads[1].xhr.dispatchEvent(new Event("error"));
|
||||
await animationFrame();
|
||||
expect(".file_upload").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("upload abort removes component", async () => {
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
const fileUploadService = await getService("file_upload");
|
||||
fileUploadService.upload("/test/", []);
|
||||
await animationFrame();
|
||||
fileUploadService.uploads[1].xhr.dispatchEvent(new Event("abort"));
|
||||
await animationFrame();
|
||||
expect(".file_upload").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("upload can be aborted by clicking on cross", async () => {
|
||||
mockService("dialog", {
|
||||
add() {
|
||||
fileUploadService.uploads[1].xhr.dispatchEvent(new Event("abort"));
|
||||
},
|
||||
});
|
||||
await mountWithCleanup(Parent);
|
||||
const fileUploadService = await getService("file_upload");
|
||||
fileUploadService.upload("/test/", []);
|
||||
await animationFrame();
|
||||
await contains(".o-file-upload-progress-bar-abort", { visible: false }).click();
|
||||
await animationFrame();
|
||||
expect(".file_upload").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("upload updates on progress", async () => {
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
const fileUploadService = await getService("file_upload");
|
||||
fileUploadService.upload("/test/", []);
|
||||
await animationFrame();
|
||||
|
||||
const progressEvent = new Event("progress", { bubbles: true });
|
||||
progressEvent.loaded = 250000000;
|
||||
progressEvent.total = 500000000;
|
||||
fileUploadService.uploads[1].xhr.upload.dispatchEvent(progressEvent);
|
||||
await animationFrame();
|
||||
expect(".file_upload_progress_text_left").toHaveText("Uploading... (50%)");
|
||||
progressEvent.loaded = 350000000;
|
||||
fileUploadService.uploads[1].xhr.upload.dispatchEvent(progressEvent);
|
||||
await animationFrame();
|
||||
expect(".file_upload_progress_text_right").toHaveText("(350/500MB)");
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,115 @@
|
|||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import { animationFrame, mockFetch } from "@odoo/hoot-mock";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
makeMockEnv,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { InstallScopedApp } from "@web/core/install_scoped_app/install_scoped_app";
|
||||
|
||||
const mountManifestLink = (href) => {
|
||||
const fixture = getFixture();
|
||||
const manifestLink = document.createElement("link");
|
||||
manifestLink.rel = "manifest";
|
||||
manifestLink.href = href;
|
||||
fixture.append(manifestLink);
|
||||
};
|
||||
|
||||
test("Installation page displays the app info correctly", async () => {
|
||||
const beforeInstallPromptEvent = new CustomEvent("beforeinstallprompt");
|
||||
beforeInstallPromptEvent.preventDefault = () => {};
|
||||
beforeInstallPromptEvent.prompt = async () => ({ outcome: "accepted" });
|
||||
browser.BeforeInstallPromptEvent = beforeInstallPromptEvent;
|
||||
await makeMockEnv();
|
||||
patchWithCleanup(browser.location, {
|
||||
replace: (url) => {
|
||||
expect(url.searchParams.get("app_name")).toBe("%3COtto%26", {
|
||||
message: "ask to redirect with updated searchParams",
|
||||
});
|
||||
expect.step("URL replace");
|
||||
},
|
||||
});
|
||||
mountManifestLink("/web/manifest.scoped_app_manifest");
|
||||
mockFetch((route) => {
|
||||
expect.step(route);
|
||||
return {
|
||||
icons: [
|
||||
{
|
||||
src: "/fake_image_src",
|
||||
sizes: "any",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
name: "My App",
|
||||
scope: "/scoped_app/myApp",
|
||||
start_url: "/scoped_app/myApp",
|
||||
};
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { InstallScopedApp };
|
||||
static template = xml`<InstallScopedApp/>`;
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect.verifySteps(["/web/manifest.scoped_app_manifest"]);
|
||||
await animationFrame();
|
||||
expect(".o_install_scoped_app").toHaveCount(1);
|
||||
expect(".o_install_scoped_app h1").toHaveText("My App");
|
||||
expect(".o_install_scoped_app img").toHaveAttribute("data-src", "/fake_image_src");
|
||||
expect(".fa-pencil").toHaveCount(0);
|
||||
expect("button.btn-primary").toHaveCount(0);
|
||||
expect("div.bg-info").toHaveCount(1);
|
||||
expect("div.bg-info").toHaveText("You can install the app from the browser menu");
|
||||
browser.dispatchEvent(beforeInstallPromptEvent);
|
||||
await animationFrame();
|
||||
expect(".fa-pencil").toHaveCount(1);
|
||||
expect("div.bg-info").toHaveCount(0);
|
||||
expect("button.btn-primary").toHaveCount(1);
|
||||
expect("button.btn-primary").toHaveText("Install");
|
||||
await contains(".fa-pencil").click();
|
||||
await contains("input").edit("<Otto&");
|
||||
expect.verifySteps(["URL replace"]);
|
||||
});
|
||||
|
||||
test("Installation page displays the error message when browser is not supported", async () => {
|
||||
delete browser.BeforeInstallPromptEvent;
|
||||
await makeMockEnv();
|
||||
mountManifestLink("/web/manifest.scoped_app_manifest");
|
||||
mockFetch((route) => {
|
||||
expect.step(route);
|
||||
return {
|
||||
icons: [
|
||||
{
|
||||
src: "/fake_image_src",
|
||||
sizes: "any",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
name: "My App",
|
||||
scope: "/scoped_app/myApp",
|
||||
start_url: "/scoped_app/myApp",
|
||||
};
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { InstallScopedApp };
|
||||
static template = xml`<InstallScopedApp/>`;
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect.verifySteps(["/web/manifest.scoped_app_manifest"]);
|
||||
await animationFrame();
|
||||
expect(".o_install_scoped_app").toHaveCount(1);
|
||||
expect(".o_install_scoped_app h1").toHaveText("My App");
|
||||
expect(".o_install_scoped_app img").toHaveAttribute("data-src", "/fake_image_src");
|
||||
expect("button.btn-primary").toHaveCount(0);
|
||||
expect("div.bg-info").toHaveCount(1);
|
||||
expect("div.bg-info").toHaveText("The app cannot be installed with this browser");
|
||||
});
|
||||
|
|
@ -0,0 +1,670 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { mockDate, mockTimeZone } from "@odoo/hoot-mock";
|
||||
import {
|
||||
defineParams,
|
||||
makeMockEnv,
|
||||
patchTranslations,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import {
|
||||
deserializeDate,
|
||||
deserializeDateTime,
|
||||
formatDate,
|
||||
formatDateTime,
|
||||
parseDate,
|
||||
parseDateTime,
|
||||
serializeDate,
|
||||
serializeDateTime,
|
||||
strftimeToLuxonFormat,
|
||||
} from "@web/core/l10n/dates";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
|
||||
const { DateTime, Settings } = luxon;
|
||||
|
||||
const formats = {
|
||||
date: "%d.%m/%Y",
|
||||
time: "%H:%M:%S",
|
||||
};
|
||||
const dateFormat = strftimeToLuxonFormat(formats.date);
|
||||
const timeFormat = strftimeToLuxonFormat(formats.time);
|
||||
|
||||
beforeEach(() => {
|
||||
patchTranslations();
|
||||
});
|
||||
|
||||
test("formatDate/formatDateTime specs", async () => {
|
||||
patchWithCleanup(localization, {
|
||||
dateFormat: "MM/dd/yyyy",
|
||||
dateTimeFormat: "MM/dd/yyyy HH:mm:ss",
|
||||
});
|
||||
mockDate("2009-05-04 11:34:56", +1);
|
||||
|
||||
const utc = DateTime.utc(); // 2009-05-04T11:34:56.000Z
|
||||
const local = DateTime.local(); // 2009-05-04T12:34:56.000+01:00
|
||||
const minus13FromLocalTZ = local.setZone("UTC-12"); // 2009-05-03T23:34:56.000-12:00
|
||||
|
||||
// For dates, regardless of the input timezone, outputs only the date
|
||||
expect(formatDate(utc)).toBe("05/04/2009");
|
||||
expect(formatDate(local)).toBe("05/04/2009");
|
||||
expect(formatDate(minus13FromLocalTZ)).toBe("05/03/2009");
|
||||
|
||||
// For datetimes, input timezone is taken into account, outputs in local timezone
|
||||
expect(formatDateTime(utc)).toBe("05/04/2009 12:34:56");
|
||||
expect(formatDateTime(local)).toBe("05/04/2009 12:34:56");
|
||||
expect(formatDateTime(minus13FromLocalTZ)).toBe("05/04/2009 12:34:56");
|
||||
});
|
||||
|
||||
test("formatDate/formatDateTime specs, at midnight", async () => {
|
||||
patchWithCleanup(localization, {
|
||||
dateFormat: "MM/dd/yyyy",
|
||||
dateTimeFormat: "MM/dd/yyyy HH:mm:ss",
|
||||
});
|
||||
mockDate("2009-05-03 23:00:00", +1);
|
||||
|
||||
const utc = DateTime.utc(); // 2009-05-03T23:00:00.000Z
|
||||
const local = DateTime.local(); // 2009-05-04T00:00:00.000+01:00
|
||||
const minus13FromLocalTZ = local.setZone("UTC-12"); // 2009-05-03T11:00:00.000-12:00
|
||||
|
||||
// For dates, regardless of the input timezone, outputs only the date
|
||||
expect(formatDate(utc)).toBe("05/03/2009");
|
||||
expect(formatDate(local)).toBe("05/04/2009");
|
||||
expect(formatDate(minus13FromLocalTZ)).toBe("05/03/2009");
|
||||
|
||||
// For datetimes, input timezone is taken into account, outputs in local timezone
|
||||
expect(formatDateTime(utc)).toBe("05/04/2009 00:00:00");
|
||||
expect(formatDateTime(local)).toBe("05/04/2009 00:00:00");
|
||||
expect(formatDateTime(minus13FromLocalTZ)).toBe("05/04/2009 00:00:00");
|
||||
});
|
||||
|
||||
test("formatDate/formatDateTime with condensed option", async () => {
|
||||
mockDate("2009-05-03 08:00:00");
|
||||
mockTimeZone(0);
|
||||
const now = DateTime.now();
|
||||
|
||||
patchWithCleanup(localization, {
|
||||
dateFormat: "MM/dd/yyyy",
|
||||
dateTimeFormat: "MM/dd/yyyy HH:mm:ss",
|
||||
});
|
||||
expect(formatDate(now, { condensed: true })).toBe("5/3/2009");
|
||||
expect(formatDateTime(now, { condensed: true })).toBe("5/3/2009 8:00:00");
|
||||
|
||||
patchWithCleanup(localization, { dateFormat: "yyyy-MM-dd" });
|
||||
expect(formatDate(now, { condensed: true })).toBe("2009-5-3");
|
||||
|
||||
patchWithCleanup(localization, { dateFormat: "dd MMM yy" });
|
||||
expect(formatDate(now, { condensed: true })).toBe("3 May 09");
|
||||
});
|
||||
|
||||
test("formatDateTime in different timezone", async () => {
|
||||
patchWithCleanup(localization, {
|
||||
dateFormat: "MM/dd/yyyy",
|
||||
dateTimeFormat: "MM/dd/yyyy HH:mm:ss",
|
||||
});
|
||||
mockDate("2009-05-04 00:00:00", 0);
|
||||
expect(formatDateTime(DateTime.utc())).toBe("05/04/2009 00:00:00");
|
||||
expect(formatDateTime(DateTime.utc(), { tz: "Asia/Kolkata" })).toBe("05/04/2009 05:30:00");
|
||||
});
|
||||
|
||||
test("parseDate(Time) outputs DateTime objects in local TZ", async () => {
|
||||
await makeMockEnv();
|
||||
mockTimeZone(+1);
|
||||
expect(parseDate("01/13/2019").toISO()).toBe("2019-01-13T00:00:00.000+01:00");
|
||||
expect(parseDateTime("01/13/2019 10:05:45").toISO()).toBe("2019-01-13T10:05:45.000+01:00");
|
||||
|
||||
mockTimeZone(+5.5);
|
||||
expect(parseDate("01/13/2019").toISO()).toBe("2019-01-13T00:00:00.000+05:30");
|
||||
expect(parseDateTime("01/13/2019 10:05:45").toISO()).toBe("2019-01-13T10:05:45.000+05:30");
|
||||
|
||||
mockTimeZone(-11);
|
||||
expect(parseDate("01/13/2019").toISO()).toBe("2019-01-13T00:00:00.000-11:00");
|
||||
expect(parseDateTime("01/13/2019 10:05:45").toISO()).toBe("2019-01-13T10:05:45.000-11:00");
|
||||
});
|
||||
|
||||
test("parseDateTime in different timezone", async () => {
|
||||
await makeMockEnv();
|
||||
mockTimeZone(+1);
|
||||
expect(parseDateTime("01/13/2019 10:05:45").toISO()).toBe("2019-01-13T10:05:45.000+01:00");
|
||||
expect(parseDateTime("01/13/2019 10:05:45", { tz: "Asia/Kolkata" }).toISO()).toBe(
|
||||
"2019-01-13T10:05:45.000+05:30"
|
||||
);
|
||||
});
|
||||
|
||||
test("parseDate with different numbering system", async () => {
|
||||
patchWithCleanup(localization, {
|
||||
dateFormat: "dd MMM, yyyy",
|
||||
dateTimeFormat: "dd MMM, yyyy hh:mm:ss",
|
||||
timeFormat: "hh:mm:ss",
|
||||
});
|
||||
|
||||
patchWithCleanup(Settings, { defaultNumberingSystem: "arab", defaultLocale: "ar" });
|
||||
|
||||
expect(parseDate("٠١ فبراير, ٢٠٢٣").toISO()).toBe("2023-02-01T00:00:00.000+01:00");
|
||||
});
|
||||
|
||||
test("parseDateTime", async () => {
|
||||
expect(() => parseDateTime("13/01/2019 12:00:00")).toThrow(/is not a correct/, {
|
||||
message: "Wrongly formated dates should be invalid",
|
||||
});
|
||||
expect(() => parseDateTime("01/01/0999 12:00:00")).toThrow(/is not a correct/, {
|
||||
message: "Dates before 1000 should be invalid",
|
||||
});
|
||||
expect(() => parseDateTime("01/01/10000 12:00:00")).toThrow(/is not a correct/, {
|
||||
message: "Dates after 9999 should be invalid",
|
||||
});
|
||||
expect(() => parseDateTime("invalid value")).toThrow(/is not a correct/);
|
||||
|
||||
const expected = "2019-01-13T10:05:45.000+01:00";
|
||||
expect(parseDateTime("01/13/2019 10:05:45").toISO()).toBe(expected, {
|
||||
message: "Date with leading 0",
|
||||
});
|
||||
expect(parseDateTime("1/13/2019 10:5:45").toISO()).toBe(expected, {
|
||||
message: "Date without leading 0",
|
||||
});
|
||||
});
|
||||
|
||||
test("parseDateTime (norwegian locale)", async () => {
|
||||
defineParams({
|
||||
lang: "no", // Norwegian
|
||||
lang_parameters: {
|
||||
date_format: "%d. %b %Y",
|
||||
time_format: "%H:%M:%S",
|
||||
},
|
||||
});
|
||||
await makeMockEnv();
|
||||
|
||||
expect(parseDateTime("16. des 2019 10:05:45").toISO()).toBe("2019-12-16T10:05:45.000+01:00", {
|
||||
message: "Day/month inverted + month i18n",
|
||||
});
|
||||
});
|
||||
|
||||
test("parseDate", async () => {
|
||||
await makeMockEnv();
|
||||
expect(parseDate("07/21/2022").toISO()).toBe("2022-07-21T00:00:00.000+01:00");
|
||||
expect(parseDate("07/22/2022").toISO()).toBe("2022-07-22T00:00:00.000+01:00");
|
||||
});
|
||||
|
||||
test("parseDate without separator", async () => {
|
||||
const dateFormat = strftimeToLuxonFormat("%d.%m/%Y");
|
||||
const timeFormat = strftimeToLuxonFormat("%H:%M:%S");
|
||||
patchWithCleanup(localization, {
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
dateTimeFormat: `${dateFormat} ${timeFormat}`,
|
||||
});
|
||||
|
||||
const testDateFormat = "dd.MM/yyyy";
|
||||
|
||||
expect(() => parseDate("1137")).toThrow(/is not a correct/, {
|
||||
message: "Wrongly formated dates should be invalid",
|
||||
});
|
||||
expect(() => parseDate("1197")).toThrow(/is not a correct/, {
|
||||
message: "Wrongly formated dates should be invalid",
|
||||
});
|
||||
expect(() => parseDate("0131")).toThrow(/is not a correct/, {
|
||||
message: "Wrongly formated dates should be invalid",
|
||||
});
|
||||
expect(() => parseDate("970131")).toThrow(/is not a correct/, {
|
||||
message: "Wrongly formated dates should be invalid",
|
||||
});
|
||||
expect(parseDate("2001").toFormat(testDateFormat)).toBe("20.01/" + DateTime.utc().year);
|
||||
expect(parseDate("3101").toFormat(testDateFormat)).toBe("31.01/" + DateTime.utc().year);
|
||||
expect(parseDate("31.01").toFormat(testDateFormat)).toBe("31.01/" + DateTime.utc().year);
|
||||
expect(parseDate("310197").toFormat(testDateFormat)).toBe("31.01/1997");
|
||||
expect(parseDate("310117").toFormat(testDateFormat)).toBe("31.01/2017");
|
||||
expect(parseDate("31011985").toFormat(testDateFormat)).toBe("31.01/1985");
|
||||
});
|
||||
|
||||
test("parseDateTime without separator", async () => {
|
||||
const dateFormat = strftimeToLuxonFormat("%d.%m/%Y");
|
||||
const timeFormat = strftimeToLuxonFormat("%H:%M:%S");
|
||||
patchWithCleanup(localization, {
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
dateTimeFormat: `${dateFormat} ${timeFormat}`,
|
||||
});
|
||||
|
||||
const dateTimeFormat = "dd.MM/yyyy HH:mm/ss";
|
||||
expect(parseDateTime("3101198508").toFormat(dateTimeFormat)).toBe("31.01/1985 08:00/00");
|
||||
expect(parseDateTime("310119850833").toFormat(dateTimeFormat)).toBe("31.01/1985 08:33/00");
|
||||
expect(parseDateTime("31/01/1985 08").toFormat(dateTimeFormat)).toBe("31.01/1985 08:00/00");
|
||||
});
|
||||
|
||||
test("parseDateTime with escaped characters (eg. Basque locale)", async () => {
|
||||
const dateFormat = strftimeToLuxonFormat("%a, %Y.eko %bren %da");
|
||||
const timeFormat = strftimeToLuxonFormat("%H:%M:%S");
|
||||
patchWithCleanup(localization, {
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
dateTimeFormat: `${dateFormat} ${timeFormat}`,
|
||||
});
|
||||
|
||||
const dateTimeFormat = `${dateFormat} ${timeFormat}`;
|
||||
expect(dateTimeFormat).toBe("ccc, yyyy.'e''k''o' MMM'r''e''n' dd'a' HH:mm:ss");
|
||||
expect(parseDateTime("1985-01-31 08:30:00").toFormat(dateTimeFormat)).toBe(
|
||||
"Thu, 1985.eko Janren 31a 08:30:00"
|
||||
);
|
||||
});
|
||||
|
||||
test("parse smart date input", async () => {
|
||||
mockDate("2020-01-01 00:00:00", 0);
|
||||
|
||||
const format = "yyyy-MM-dd HH:mm";
|
||||
// with parseDate
|
||||
expect(parseDate("+0").toFormat(format)).toBe("2020-01-01 00:00");
|
||||
expect(parseDate("-0").toFormat(format)).toBe("2020-01-01 00:00");
|
||||
expect(parseDate("+1d").toFormat(format)).toBe("2020-01-02 00:00");
|
||||
expect(parseDate("+2w").toFormat(format)).toBe("2020-01-15 00:00");
|
||||
expect(parseDate("+3m").toFormat(format)).toBe("2020-04-01 00:00");
|
||||
expect(parseDate("+4y").toFormat(format)).toBe("2024-01-01 00:00");
|
||||
expect(parseDate("+5").toFormat(format)).toBe("2020-01-06 00:00");
|
||||
expect(parseDate("-5").toFormat(format)).toBe("2019-12-27 00:00");
|
||||
expect(parseDate("-4y").toFormat(format)).toBe("2016-01-01 00:00");
|
||||
expect(parseDate("-3m").toFormat(format)).toBe("2019-10-01 00:00");
|
||||
expect(parseDate("-2w").toFormat(format)).toBe("2019-12-18 00:00");
|
||||
expect(parseDate("-1d").toFormat(format)).toBe("2019-12-31 00:00");
|
||||
// with parseDateTime
|
||||
expect(parseDateTime("+0").toFormat(format)).toBe("2020-01-01 00:00");
|
||||
expect(parseDateTime("-0").toFormat(format)).toBe("2020-01-01 00:00");
|
||||
expect(parseDateTime("+1d").toFormat(format)).toBe("2020-01-02 00:00");
|
||||
expect(parseDateTime("+2w").toFormat(format)).toBe("2020-01-15 00:00");
|
||||
expect(parseDateTime("+3m").toFormat(format)).toBe("2020-04-01 00:00");
|
||||
expect(parseDateTime("+4y").toFormat(format)).toBe("2024-01-01 00:00");
|
||||
expect(parseDateTime("+5").toFormat(format)).toBe("2020-01-06 00:00");
|
||||
expect(parseDateTime("-5").toFormat(format)).toBe("2019-12-27 00:00");
|
||||
expect(parseDateTime("-4y").toFormat(format)).toBe("2016-01-01 00:00");
|
||||
expect(parseDateTime("-3m").toFormat(format)).toBe("2019-10-01 00:00");
|
||||
expect(parseDateTime("-2w").toFormat(format)).toBe("2019-12-18 00:00");
|
||||
expect(parseDateTime("-1d").toFormat(format)).toBe("2019-12-31 00:00");
|
||||
});
|
||||
|
||||
test("parseDateTime ISO8601 Format", async () => {
|
||||
mockTimeZone(+1);
|
||||
expect(parseDateTime("2017-05-15T12:00:00.000+06:00").toISO()).toBe(
|
||||
"2017-05-15T07:00:00.000+01:00"
|
||||
);
|
||||
// without the 'T' separator is not really ISO8601 compliant, but we still support it
|
||||
expect(parseDateTime("2017-05-15 12:00:00.000+06:00").toISO()).toBe(
|
||||
"2017-05-15T07:00:00.000+01:00"
|
||||
);
|
||||
});
|
||||
|
||||
test("parseDateTime SQL Format", async () => {
|
||||
expect(parseDateTime("2017-05-15 09:12:34").toISO()).toBe("2017-05-15T09:12:34.000+01:00");
|
||||
expect(parseDateTime("2017-05-08 09:12:34").toISO()).toBe("2017-05-08T09:12:34.000+01:00");
|
||||
});
|
||||
|
||||
test("serializeDate", async () => {
|
||||
const date = DateTime.utc(2022, 2, 21, 16, 11, 42);
|
||||
expect(date.toFormat("yyyy-MM-dd")).toBe("2022-02-21");
|
||||
expect(serializeDate(date)).toBe("2022-02-21");
|
||||
});
|
||||
|
||||
test("serializeDate, with DateTime.now()", async () => {
|
||||
mockDate("2022-02-21 15:11:42");
|
||||
const date = DateTime.now();
|
||||
expect(date.toFormat("yyyy-MM-dd")).toBe("2022-02-21");
|
||||
expect(serializeDate(date)).toBe("2022-02-21");
|
||||
});
|
||||
|
||||
test("serializeDate, with DateTime.now(), midnight", async () => {
|
||||
mockDate("2022-02-20 23:00:00");
|
||||
const date = DateTime.now();
|
||||
expect(date.toFormat("yyyy-MM-dd")).toBe("2022-02-21");
|
||||
expect(serializeDate(date)).toBe("2022-02-21");
|
||||
});
|
||||
|
||||
test("serializeDate with different numbering system", async () => {
|
||||
patchWithCleanup(Settings, { defaultNumberingSystem: "arab" });
|
||||
const date = DateTime.utc(2022, 2, 21, 16, 11, 42);
|
||||
expect(date.toFormat("yyyy-MM-dd")).toBe("٢٠٢٢-٠٢-٢١");
|
||||
expect(serializeDate(date)).toBe("2022-02-21");
|
||||
});
|
||||
|
||||
test("serializeDateTime", async () => {
|
||||
const date = DateTime.utc(2022, 2, 21, 16, 11, 42);
|
||||
expect(date.toFormat("yyyy-MM-dd HH:mm:ss")).toBe("2022-02-21 16:11:42");
|
||||
expect(serializeDateTime(date)).toBe("2022-02-21 16:11:42");
|
||||
});
|
||||
|
||||
test("serializeDateTime, with DateTime.now()", async () => {
|
||||
mockDate("2022-02-21 15:11:42");
|
||||
const date = DateTime.now();
|
||||
expect(date.toFormat("yyyy-MM-dd HH:mm:ss")).toBe("2022-02-21 16:11:42");
|
||||
expect(serializeDateTime(date)).toBe("2022-02-21 15:11:42");
|
||||
});
|
||||
|
||||
test("serializeDateTime, with DateTime.now(), midnight", async () => {
|
||||
mockDate("2022-02-20 23:00:00");
|
||||
const date = DateTime.now();
|
||||
expect(date.toFormat("yyyy-MM-dd HH:mm:ss")).toBe("2022-02-21 00:00:00");
|
||||
expect(serializeDateTime(date)).toBe("2022-02-20 23:00:00");
|
||||
});
|
||||
|
||||
test("serializeDateTime with different numbering system", async () => {
|
||||
patchWithCleanup(Settings, { defaultNumberingSystem: "arab" });
|
||||
const date = DateTime.utc(2022, 2, 21, 16, 11, 42);
|
||||
expect(date.toFormat("yyyy-MM-dd HH:mm:ss")).toBe("٢٠٢٢-٠٢-٢١ ١٦:١١:٤٢");
|
||||
expect(serializeDateTime(date)).toBe("2022-02-21 16:11:42");
|
||||
});
|
||||
|
||||
test("deserializeDate", async () => {
|
||||
const date = DateTime.local(2022, 2, 21);
|
||||
expect(DateTime.fromFormat("2022-02-21", "yyyy-MM-dd").toMillis()).toBe(date.toMillis());
|
||||
expect(deserializeDate("2022-02-21").toMillis()).toBe(date.toMillis());
|
||||
});
|
||||
|
||||
test("deserializeDate with different numbering system", async () => {
|
||||
patchWithCleanup(Settings, { defaultNumberingSystem: "arab" });
|
||||
const date = DateTime.local(2022, 2, 21);
|
||||
expect(DateTime.fromFormat("٢٠٢٢-٠٢-٢١", "yyyy-MM-dd").toMillis()).toBe(date.toMillis());
|
||||
expect(deserializeDate("2022-02-21").toMillis()).toBe(date.toMillis());
|
||||
});
|
||||
|
||||
test("deserializeDateTime", async () => {
|
||||
const date = DateTime.utc(2022, 2, 21, 16, 11, 42);
|
||||
expect(
|
||||
DateTime.fromFormat("2022-02-21 16:11:42", "yyyy-MM-dd HH:mm:ss", {
|
||||
zone: "utc",
|
||||
}).toMillis()
|
||||
).toBe(date.toMillis());
|
||||
expect(deserializeDateTime("2022-02-21 16:11:42").toMillis()).toBe(date.toMillis());
|
||||
});
|
||||
|
||||
test("deserializeDateTime with different numbering system", async () => {
|
||||
patchWithCleanup(Settings, { defaultNumberingSystem: "arab" });
|
||||
const date = DateTime.utc(2022, 2, 21, 16, 11, 42);
|
||||
expect(
|
||||
DateTime.fromFormat("٢٠٢٢-٠٢-٢١ ١٦:١١:٤٢", "yyyy-MM-dd HH:mm:ss", {
|
||||
zone: "utc",
|
||||
}).toMillis()
|
||||
).toBe(date.toMillis());
|
||||
expect(deserializeDateTime("2022-02-21 16:11:42").toMillis()).toBe(date.toMillis());
|
||||
});
|
||||
|
||||
test("deserializeDateTime with different timezone", async () => {
|
||||
const date = DateTime.utc(2022, 2, 21, 16, 11, 42).setZone("Europe/Brussels");
|
||||
expect(deserializeDateTime("2022-02-21 16:11:42", { tz: "Europe/Brussels" }).c).toEqual(date.c);
|
||||
});
|
||||
|
||||
test("parseDate with short notations", async () => {
|
||||
expect(parseDate("20-10-20", { format: "yyyy-MM-dd" }).toISO()).toBe(
|
||||
"2020-10-20T00:00:00.000+01:00"
|
||||
);
|
||||
expect(parseDate("20/10/20", { format: "yyyy/MM/dd" }).toISO()).toBe(
|
||||
"2020-10-20T00:00:00.000+01:00"
|
||||
);
|
||||
expect(parseDate("10-20-20", { format: "MM-dd-yyyy" }).toISO()).toBe(
|
||||
"2020-10-20T00:00:00.000+01:00"
|
||||
);
|
||||
expect(parseDate("10-20-20", { format: "MM-yyyy-dd" }).toISO()).toBe(
|
||||
"2020-10-20T00:00:00.000+01:00"
|
||||
);
|
||||
expect(parseDate("1-20-2", { format: "MM-yyyy-dd" }).toISO()).toBe(
|
||||
"2020-01-02T00:00:00.000+01:00"
|
||||
);
|
||||
expect(parseDate("20/1/2", { format: "yyyy/MM/dd" }).toISO()).toBe(
|
||||
"2020-01-02T00:00:00.000+01:00"
|
||||
);
|
||||
});
|
||||
|
||||
test("parseDateTime with short notations", async () => {
|
||||
expect(parseDateTime("20-10-20 8:5:3", { format: "yyyy-MM-dd hh:mm:ss" }).toISO()).toBe(
|
||||
"2020-10-20T08:05:03.000+01:00"
|
||||
);
|
||||
});
|
||||
|
||||
test("parseDate with textual month notation", async () => {
|
||||
patchWithCleanup(localization, {
|
||||
dateFormat: "MMM/dd/yyyy",
|
||||
});
|
||||
expect(parseDate("Jan/05/1997").toISO()).toBe("1997-01-05T00:00:00.000+01:00");
|
||||
expect(parseDate("Jan/05/1997", { format: undefined }).toISO()).toBe(
|
||||
"1997-01-05T00:00:00.000+01:00"
|
||||
);
|
||||
expect(parseDate("Jan/05/1997", { format: "MMM/dd/yyyy" }).toISO()).toBe(
|
||||
"1997-01-05T00:00:00.000+01:00"
|
||||
);
|
||||
});
|
||||
|
||||
test("parseDate (various entries)", async () => {
|
||||
mockDate("2020-07-15 12:30:00", 0);
|
||||
patchWithCleanup(localization, {
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
dateTimeFormat: `${dateFormat} ${timeFormat}`,
|
||||
});
|
||||
|
||||
/**
|
||||
* Type of testSet key: string
|
||||
* Type of testSet value: string | undefined
|
||||
*/
|
||||
const testSet = new Map([
|
||||
["10101010101010", undefined],
|
||||
["1191111", "1191-04-21T00:00:00.000Z"], // day 111 of year 1191
|
||||
["11911111", "1191-11-11T00:00:00.000Z"],
|
||||
["3101", "2020-01-31T00:00:00.000Z"],
|
||||
["310160", "2060-01-31T00:00:00.000Z"],
|
||||
["311260", "2060-12-31T00:00:00.000Z"],
|
||||
|
||||
["310161", "1961-01-31T00:00:00.000Z"],
|
||||
["310165", "1965-01-31T00:00:00.000Z"],
|
||||
["310168", "1968-01-31T00:00:00.000Z"],
|
||||
["311268", "1968-12-31T00:00:00.000Z"],
|
||||
|
||||
["310169", "1969-01-31T00:00:00.000Z"],
|
||||
["310170", "1970-01-31T00:00:00.000Z"],
|
||||
["310197", "1997-01-31T00:00:00.000Z"],
|
||||
["310117", "2017-01-31T00:00:00.000Z"],
|
||||
["31011985", "1985-01-31T00:00:00.000Z"],
|
||||
["3101198508", undefined],
|
||||
["310119850833", undefined],
|
||||
|
||||
["1137", undefined],
|
||||
["1197", undefined],
|
||||
["0131", undefined],
|
||||
["0922", undefined],
|
||||
["2020", undefined],
|
||||
|
||||
["199901", "1999-01-01T00:00:00.000Z"],
|
||||
["30100210", "3010-02-10T00:00:00.000Z"],
|
||||
["3010210", "3010-07-29T00:00:00.000Z"],
|
||||
|
||||
["970131", undefined],
|
||||
["31.01", "2020-01-31T00:00:00.000Z"],
|
||||
["31/01/1985 08", undefined],
|
||||
|
||||
["01121934", "1934-12-01T00:00:00.000Z"],
|
||||
["011234", "2034-12-01T00:00:00.000Z"],
|
||||
["011260", "2060-12-01T00:00:00.000Z"],
|
||||
["2", "2020-07-02T00:00:00.000Z"],
|
||||
["02", "2020-07-02T00:00:00.000Z"],
|
||||
["20", "2020-07-20T00:00:00.000Z"],
|
||||
["202", "2020-02-20T00:00:00.000Z"],
|
||||
["2002", "2020-02-20T00:00:00.000Z"],
|
||||
["0202", "2020-02-02T00:00:00.000Z"],
|
||||
["02/02", "2020-02-02T00:00:00.000Z"],
|
||||
["02/13", undefined],
|
||||
["02/1313", undefined],
|
||||
["09990101", undefined],
|
||||
["19990101", "1999-01-01T00:00:00.000Z"],
|
||||
["19990130", "1999-01-30T00:00:00.000Z"],
|
||||
["19991230", "1999-12-30T00:00:00.000Z"],
|
||||
["19993012", undefined],
|
||||
["2016-200", "2016-07-18T00:00:00.000Z"],
|
||||
["2016200", "2016-07-18T00:00:00.000Z"], // day 200 of year 2016
|
||||
["2020-", undefined],
|
||||
["2020-W2", undefined],
|
||||
["2020W23", "2020-06-01T00:00:00.000Z"],
|
||||
["2020-W02", "2020-01-06T00:00:00.000Z"],
|
||||
["2020-W32", "2020-08-03T00:00:00.000Z"],
|
||||
["2020-W32-3", "2020-08-05T00:00:00.000Z"],
|
||||
["2016-W21-3", "2016-05-25T00:00:00.000Z"],
|
||||
["2016W213", "2016-05-25T00:00:00.000Z"],
|
||||
["2209", "2020-09-22T00:00:00.000Z"],
|
||||
["22:09", "2020-09-22T00:00:00.000Z"],
|
||||
["2012", "2020-12-20T00:00:00.000Z"],
|
||||
|
||||
["2016-01-03 09:24:15.123", "2016-01-03T00:00:00.000Z"],
|
||||
["2016-01-03T09:24:15.123", "2016-01-03T00:00:00.000Z"],
|
||||
["2016-01-03T09:24:15.123+06:00", "2016-01-03T00:00:00.000Z"],
|
||||
["2016-01-03T09:24:15.123+16:00", "2016-01-02T00:00:00.000Z"],
|
||||
["2016-01-03T09:24:15.123Z", "2016-01-03T00:00:00.000Z"],
|
||||
["2016-W21-3T09:24:15.123", "2016-05-25T00:00:00.000Z"],
|
||||
["2016-W21-3 09:24:15.123", undefined],
|
||||
|
||||
["2016-03-27T02:00:00.000+02:00", "2016-03-27T00:00:00.000Z"],
|
||||
["2016-03-27T03:00:00.000+02:00", "2016-03-27T00:00:00.000Z"],
|
||||
["2016-03-27T02:00:00.000", "2016-03-27T00:00:00.000Z"],
|
||||
["2016-03-27T03:00:00.000", "2016-03-27T00:00:00.000Z"],
|
||||
["2016-03-27T02:00:00.000Z", "2016-03-27T00:00:00.000Z"],
|
||||
["2016-03-27T03:00:00.000Z", "2016-03-27T00:00:00.000Z"],
|
||||
|
||||
["09:22", undefined],
|
||||
["2013", undefined],
|
||||
["011261", "1961-12-01T00:00:00.000Z"],
|
||||
|
||||
["932-10-10", undefined], // year < 1000 are not supported
|
||||
["1932-10-10", "1932-10-10T00:00:00.000Z"],
|
||||
["2016-01-03 09:24:15.123+06:00", "2016-01-03T00:00:00.000Z"],
|
||||
["2016-01-03 09:24:15.123+16:00", "2016-01-02T00:00:00.000Z"],
|
||||
["2016-01-03 09:24:15.123Z", "2016-01-03T00:00:00.000Z"],
|
||||
]);
|
||||
|
||||
for (const [input, expected] of testSet.entries()) {
|
||||
if (!expected) {
|
||||
expect(() => parseDate(input).toISO()).toThrow(/is not a correct/);
|
||||
} else {
|
||||
expect(parseDate(input).toISO()).toBe(expected);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("parseDateTime (various entries)", async () => {
|
||||
mockDate("2020-07-15 11:30:00", 0);
|
||||
patchWithCleanup(localization, {
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
dateTimeFormat: `${dateFormat} ${timeFormat}`,
|
||||
});
|
||||
|
||||
/**
|
||||
* Type of testSet key: string
|
||||
* Type of testSet value: string | undefined
|
||||
*/
|
||||
const testSet = new Map([
|
||||
["10101010101010", "1010-10-10T10:10:10.000Z"],
|
||||
["1191111", "1191-04-21T00:00:00.000Z"], // day 111 of year 1191
|
||||
["11911111", "1191-11-11T00:00:00.000Z"],
|
||||
["3101", "2020-01-31T00:00:00.000Z"],
|
||||
["310160", "2060-01-31T00:00:00.000Z"],
|
||||
["311260", "2060-12-31T00:00:00.000Z"],
|
||||
["310161", "1961-01-31T00:00:00.000Z"],
|
||||
["310165", "1965-01-31T00:00:00.000Z"],
|
||||
["310168", "1968-01-31T00:00:00.000Z"],
|
||||
["311268", "1968-12-31T00:00:00.000Z"],
|
||||
["310169", "1969-01-31T00:00:00.000Z"],
|
||||
["310170", "1970-01-31T00:00:00.000Z"],
|
||||
["310197", "1997-01-31T00:00:00.000Z"],
|
||||
["310117", "2017-01-31T00:00:00.000Z"],
|
||||
["31011985", "1985-01-31T00:00:00.000Z"],
|
||||
["3101198508", "1985-01-31T08:00:00.000Z"],
|
||||
["310119850833", "1985-01-31T08:33:00.000Z"],
|
||||
["1137", undefined],
|
||||
["1197", undefined],
|
||||
["0131", undefined],
|
||||
["0922", undefined],
|
||||
["2020", undefined],
|
||||
["199901", "1999-01-01T00:00:00.000Z"],
|
||||
["30100210", "3010-02-10T00:00:00.000Z"],
|
||||
["3010210", "3010-07-29T00:00:00.000Z"],
|
||||
["970131", undefined],
|
||||
["31.01", "2020-01-31T00:00:00.000Z"],
|
||||
["31/01/1985 08", "1985-01-31T08:00:00.000Z"],
|
||||
|
||||
["01121934", "1934-12-01T00:00:00.000Z"],
|
||||
["011234", "2034-12-01T00:00:00.000Z"],
|
||||
["011260", "2060-12-01T00:00:00.000Z"],
|
||||
["2", "2020-07-02T00:00:00.000Z"],
|
||||
["02", "2020-07-02T00:00:00.000Z"],
|
||||
["20", "2020-07-20T00:00:00.000Z"],
|
||||
["202", "2020-02-20T00:00:00.000Z"],
|
||||
["2002", "2020-02-20T00:00:00.000Z"],
|
||||
["0202", "2020-02-02T00:00:00.000Z"],
|
||||
["02/02", "2020-02-02T00:00:00.000Z"],
|
||||
["02/13", undefined],
|
||||
["02/1313", undefined],
|
||||
["09990101", undefined],
|
||||
["19990101", "1999-01-01T00:00:00.000Z"],
|
||||
["19990130", "1999-01-30T00:00:00.000Z"],
|
||||
["19991230", "1999-12-30T00:00:00.000Z"],
|
||||
["19993012", undefined],
|
||||
["2016-200", "2016-07-18T00:00:00.000Z"],
|
||||
["2016200", "2016-07-18T00:00:00.000Z"], // day 200 of year 2016
|
||||
["2020-", undefined],
|
||||
["2020-W2", undefined],
|
||||
["2020W23", "2020-06-01T00:00:00.000Z"],
|
||||
["2020-W02", "2020-01-06T00:00:00.000Z"],
|
||||
["2020-W32", "2020-08-03T00:00:00.000Z"],
|
||||
["2020-W32-3", "2020-08-05T00:00:00.000Z"],
|
||||
["2016-W21-3", "2016-05-25T00:00:00.000Z"],
|
||||
["2016W213", "2016-05-25T00:00:00.000Z"],
|
||||
["2209", "2020-09-22T00:00:00.000Z"],
|
||||
["22:09", "2020-09-22T00:00:00.000Z"],
|
||||
["2012", "2020-12-20T00:00:00.000Z"],
|
||||
|
||||
["2016-01-03 09:24:15.123", "2016-01-03T09:24:15.123Z"],
|
||||
["2016-01-03T09:24:15.123", "2016-01-03T09:24:15.123Z"],
|
||||
["2016-01-03T09:24:15.123+06:00", "2016-01-03T03:24:15.123Z"],
|
||||
["2016-01-03T09:24:15.123+16:00", "2016-01-02T17:24:15.123Z"],
|
||||
["2016-01-03T09:24:15.123Z", "2016-01-03T09:24:15.123Z"],
|
||||
["2016-W21-3T09:24:15.123", "2016-05-25T09:24:15.123Z"],
|
||||
["2016-W21-3 09:24:15.123", undefined],
|
||||
|
||||
["2016-03-27T02:00:00.000+02:00", "2016-03-27T00:00:00.000Z"],
|
||||
["2016-03-27T03:00:00.000+02:00", "2016-03-27T01:00:00.000Z"],
|
||||
["2016-03-27T02:00:00.000", "2016-03-27T02:00:00.000Z"],
|
||||
["2016-03-27T03:00:00.000", "2016-03-27T03:00:00.000Z"],
|
||||
["2016-03-27T02:00:00.000Z", "2016-03-27T02:00:00.000Z"],
|
||||
["2016-03-27T03:00:00.000Z", "2016-03-27T03:00:00.000Z"],
|
||||
|
||||
["09:22", undefined],
|
||||
["2013", undefined],
|
||||
["011261", "1961-12-01T00:00:00.000Z"],
|
||||
|
||||
["932-10-10", undefined],
|
||||
["1932-10-10", "1932-10-10T00:00:00.000Z"],
|
||||
["2016-01-03 09:24:15.123+06:00", "2016-01-03T03:24:15.123Z"],
|
||||
["2016-01-03 09:24:15.123+16:00", "2016-01-02T17:24:15.123Z"],
|
||||
["2016-01-03 09:24:15.123Z", "2016-01-03T09:24:15.123Z"],
|
||||
]);
|
||||
|
||||
for (const [input, expected] of testSet.entries()) {
|
||||
if (!expected) {
|
||||
expect(() => parseDateTime(input).toISO()).toThrow(/is not a correct/);
|
||||
} else {
|
||||
expect(parseDateTime(input).toISO()).toBe(expected);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("parseDateTime: arab locale, latin numbering system as input", async () => {
|
||||
defineParams({
|
||||
lang: "ar_001",
|
||||
lang_parameters: {
|
||||
date_format: "%d %b, %Y",
|
||||
time_format: "%H:%M:%S",
|
||||
},
|
||||
});
|
||||
await makeMockEnv();
|
||||
|
||||
// Check it works with arab
|
||||
expect(parseDateTime("١٥ يوليو, ٢٠٢٠ ١٢:٣٠:٤٣").toISO().split(".")[0]).toBe(
|
||||
"2020-07-15T12:30:43"
|
||||
);
|
||||
|
||||
// Check it also works with latin numbers
|
||||
expect(parseDateTime("15 07, 2020 12:30:43").toISO().split(".")[0]).toBe("2020-07-15T12:30:43");
|
||||
expect(parseDateTime("22/01/2023").toISO().split(".")[0]).toBe("2023-01-22T00:00:00");
|
||||
expect(parseDateTime("2023-01-22").toISO().split(".")[0]).toBe("2023-01-22T00:00:00");
|
||||
});
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
import { after, describe, expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
defineParams,
|
||||
makeMockEnv,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
patchTranslations,
|
||||
patchWithCleanup,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { _t, translatedTerms, translationLoaded } from "@web/core/l10n/translation";
|
||||
import { session } from "@web/session";
|
||||
|
||||
import { Component, markup, xml } from "@odoo/owl";
|
||||
const { DateTime } = luxon;
|
||||
|
||||
const frenchTerms = { Hello: "Bonjour" };
|
||||
class TestComponent extends Component {
|
||||
static template = "";
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches the 'lang' of the user session and context.
|
||||
*
|
||||
* @param {string} lang
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function mockLang(lang) {
|
||||
serverState.lang = lang;
|
||||
await makeMockEnv();
|
||||
}
|
||||
|
||||
test("lang is given by the user context", async () => {
|
||||
onRpc("/web/webclient/translations/*", (request) => {
|
||||
const urlParams = new URLSearchParams(new URL(request.url).search);
|
||||
expect.step(urlParams.get("lang"));
|
||||
});
|
||||
await mockLang("fr_FR");
|
||||
expect.verifySteps(["fr_FR"]);
|
||||
});
|
||||
|
||||
test("lang is given by an attribute on the DOM root node", async () => {
|
||||
serverState.lang = null;
|
||||
onRpc("/web/webclient/translations/*", (request) => {
|
||||
const urlParams = new URLSearchParams(new URL(request.url).search);
|
||||
expect.step(urlParams.get("lang"));
|
||||
});
|
||||
document.documentElement.setAttribute("lang", "fr-FR");
|
||||
after(() => {
|
||||
document.documentElement.removeAttribute("lang");
|
||||
});
|
||||
await makeMockEnv();
|
||||
expect.verifySteps(["fr_FR"]);
|
||||
});
|
||||
|
||||
test("url is given by the session", async () => {
|
||||
expect.assertions(1);
|
||||
patchWithCleanup(session, {
|
||||
translationURL: "/get_translations",
|
||||
});
|
||||
onRpc(
|
||||
"/get_translations/*",
|
||||
function (request) {
|
||||
expect(request.url).toInclude("/get_translations/");
|
||||
return this.loadTranslations();
|
||||
},
|
||||
{ pure: true }
|
||||
);
|
||||
await makeMockEnv();
|
||||
});
|
||||
|
||||
test("can translate a text node", async () => {
|
||||
TestComponent.template = xml`<div id="main">Hello</div>`;
|
||||
defineParams({
|
||||
translations: frenchTerms,
|
||||
});
|
||||
await mountWithCleanup(TestComponent);
|
||||
expect("#main").toHaveText("Bonjour");
|
||||
});
|
||||
|
||||
test("can lazy translate", async () => {
|
||||
// Can't use patchWithCleanup cause it doesn't support Symbol
|
||||
translatedTerms[translationLoaded] = false;
|
||||
TestComponent.template = xml`<div id="main"><t t-esc="constructor.someLazyText" /></div>`;
|
||||
TestComponent.someLazyText = _t("Hello");
|
||||
expect(() => TestComponent.someLazyText.toString()).toThrow();
|
||||
expect(() => TestComponent.someLazyText.valueOf()).toThrow();
|
||||
defineParams({
|
||||
translations: frenchTerms,
|
||||
});
|
||||
await mountWithCleanup(TestComponent);
|
||||
expect("#main").toHaveText("Bonjour");
|
||||
});
|
||||
|
||||
test("luxon is configured in the correct lang", async () => {
|
||||
await mockLang("fr_BE");
|
||||
expect(DateTime.utc(2021, 12, 10).toFormat("MMMM")).toBe("décembre");
|
||||
});
|
||||
|
||||
test("arabic has the correct numbering system (generic)", async () => {
|
||||
await mockLang("ar_001");
|
||||
expect(DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss")).toBe("١٠/١٢/٢٠٢١ ١٢:٠٠:٠٠");
|
||||
});
|
||||
|
||||
test("arabic has the correct numbering system (Algeria)", async () => {
|
||||
await mockLang("ar_DZ");
|
||||
expect(DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss")).toBe("10/12/2021 12:00:00");
|
||||
});
|
||||
|
||||
test("arabic has the correct numbering system (Lybia)", async () => {
|
||||
await mockLang("ar_LY");
|
||||
expect(DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss")).toBe("10/12/2021 12:00:00");
|
||||
});
|
||||
|
||||
test("arabic has the correct numbering system (Morocco)", async () => {
|
||||
await mockLang("ar_MA");
|
||||
expect(DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss")).toBe("10/12/2021 12:00:00");
|
||||
});
|
||||
|
||||
test("arabic has the correct numbering system (Saudi Arabia)", async () => {
|
||||
await mockLang("ar_SA");
|
||||
expect(DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss")).toBe("١٠/١٢/٢٠٢١ ١٢:٠٠:٠٠");
|
||||
});
|
||||
|
||||
test("arabic has the correct numbering system (Tunisia)", async () => {
|
||||
await mockLang("ar_TN");
|
||||
expect(DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss")).toBe("10/12/2021 12:00:00");
|
||||
});
|
||||
|
||||
test("bengalese has the correct numbering system", async () => {
|
||||
await mockLang("bn");
|
||||
expect(DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss")).toBe("১০/১২/২০২১ ১২:০০:০০");
|
||||
});
|
||||
|
||||
test("punjabi (gurmukhi) has the correct numbering system", async () => {
|
||||
await mockLang("pa_IN");
|
||||
expect(DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss")).toBe("੧੦/੧੨/੨੦੨੧ ੧੨:੦੦:੦੦");
|
||||
});
|
||||
|
||||
test("tamil has the correct numbering system", async () => {
|
||||
await mockLang("ta");
|
||||
expect(DateTime.utc(2021, 12, 10).toFormat("dd/MM/yyyy hh:mm:ss")).toBe("௧௦/௧௨/௨௦௨௧ ௧௨:௦௦:௦௦");
|
||||
});
|
||||
|
||||
test("_t fills the format specifiers in translated terms with its extra arguments", async () => {
|
||||
patchTranslations({
|
||||
"Due in %s days": "Échéance dans %s jours",
|
||||
});
|
||||
const translatedStr = _t("Due in %s days", 513);
|
||||
expect(translatedStr).toBe("Échéance dans 513 jours");
|
||||
});
|
||||
|
||||
test("_t fills the format specifiers in lazy translated terms with its extra arguments", async () => {
|
||||
translatedTerms[translationLoaded] = false;
|
||||
const translatedStr = _t("Due in %s days", 513);
|
||||
patchTranslations({
|
||||
"Due in %s days": "Échéance dans %s jours",
|
||||
});
|
||||
expect(translatedStr.toString()).toBe("Échéance dans 513 jours");
|
||||
});
|
||||
|
||||
describe("_t with markups", () => {
|
||||
test("non-markup values are escaped", () => {
|
||||
translatedTerms[translationLoaded] = true;
|
||||
const maliciousUserInput = "<script>alert('This should've been escaped')</script>";
|
||||
const translatedStr = _t(
|
||||
"FREE %(blink_start)sROBUX%(blink_end)s, please contact %(email)s",
|
||||
{
|
||||
blink_start: markup("<blink>"),
|
||||
blink_end: markup("</blink>"),
|
||||
email: maliciousUserInput,
|
||||
}
|
||||
);
|
||||
expect(translatedStr).toBeInstanceOf(markup().constructor);
|
||||
expect(translatedStr.valueOf()).toBe(
|
||||
"FREE <blink>ROBUX</blink>, please contact <script>alert('This should've been escaped')</script>"
|
||||
);
|
||||
});
|
||||
test("translations are escaped", () => {
|
||||
translatedTerms[translationLoaded] = true;
|
||||
const maliciousTranslation = "<script>document.write('pizza hawai')</script> %s";
|
||||
patchTranslations({ "I love %s": maliciousTranslation });
|
||||
const translatedStr = _t("I love %s", markup("<blink>Mario Kart</blink>"));
|
||||
expect(translatedStr.valueOf()).toBe(
|
||||
"<script>document.write('pizza hawai')</script> <blink>Mario Kart</blink>"
|
||||
);
|
||||
});
|
||||
});
|
||||
258
odoo-bringout-oca-ocb-web/web/static/tests/core/macro.test.js
Normal file
258
odoo-bringout-oca-ocb-web/web/static/tests/core/macro.test.js
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
import { beforeEach, expect, test } from "@odoo/hoot";
|
||||
import { advanceTime, animationFrame, click, edit, queryOne } from "@odoo/hoot-dom";
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { mountWithCleanup, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Macro } from "@web/core/macro";
|
||||
|
||||
let macro;
|
||||
async function waitForMacro() {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
await animationFrame();
|
||||
await advanceTime(265);
|
||||
if (macro.isComplete) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!macro.isComplete) {
|
||||
throw new Error(`Macro is not complete`);
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
patchWithCleanup(Macro.prototype, {
|
||||
start() {
|
||||
super.start(...arguments);
|
||||
macro = this;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
function onTextChange(element, callback) {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "characterData" || mutation.type === "childList") {
|
||||
callback(element.textContent);
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(element, {
|
||||
characterData: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
return observer;
|
||||
}
|
||||
|
||||
class TestComponent extends Component {
|
||||
static template = xml`
|
||||
<div class="counter">
|
||||
<p><button class="btn inc" t-on-click="() => this.state.value++">increment</button></p>
|
||||
<p><button class="btn dec" t-on-click="() => this.state.value--">decrement</button></p>
|
||||
<p><button class="btn double" t-on-click="() => this.state.value = 2*this.state.value">double</button></p>
|
||||
<span class="value"><t t-esc="state.value"/></span>
|
||||
<input />
|
||||
</div>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({ value: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
test("simple use", async () => {
|
||||
await mountWithCleanup(TestComponent);
|
||||
new Macro({
|
||||
name: "test",
|
||||
steps: [
|
||||
{
|
||||
trigger: "button.inc",
|
||||
async action(trigger) {
|
||||
await click(trigger);
|
||||
},
|
||||
},
|
||||
],
|
||||
}).start(queryOne(".counter"));
|
||||
|
||||
const span = queryOne("span.value");
|
||||
expect(span).toHaveText("0");
|
||||
onTextChange(span, expect.step);
|
||||
await waitForMacro();
|
||||
expect.verifySteps(["1"]);
|
||||
});
|
||||
|
||||
test("multiple steps", async () => {
|
||||
await mountWithCleanup(TestComponent);
|
||||
const span = queryOne("span.value");
|
||||
expect(span).toHaveText("0");
|
||||
|
||||
new Macro({
|
||||
name: "test",
|
||||
steps: [
|
||||
{
|
||||
trigger: "button.inc",
|
||||
async action(trigger) {
|
||||
await click(trigger);
|
||||
},
|
||||
},
|
||||
{
|
||||
trigger: () => (span.textContent === "1" ? span : null),
|
||||
},
|
||||
{
|
||||
trigger: "button.inc",
|
||||
async action(trigger) {
|
||||
await click(trigger);
|
||||
},
|
||||
},
|
||||
],
|
||||
}).start(queryOne(".counter"));
|
||||
onTextChange(span, expect.step);
|
||||
await waitForMacro();
|
||||
expect.verifySteps(["1", "2"]);
|
||||
});
|
||||
|
||||
test("can input values", async () => {
|
||||
await mountWithCleanup(TestComponent);
|
||||
const input = queryOne("input");
|
||||
new Macro({
|
||||
name: "test",
|
||||
steps: [
|
||||
{
|
||||
trigger: "div.counter input",
|
||||
async action(trigger) {
|
||||
await click(trigger);
|
||||
await edit("aaron", { confirm: "blur" });
|
||||
},
|
||||
},
|
||||
],
|
||||
}).start(queryOne(".counter"));
|
||||
expect(input).toHaveValue("");
|
||||
await waitForMacro();
|
||||
expect(input).toHaveValue("aaron");
|
||||
});
|
||||
|
||||
test("a step can have no trigger", async () => {
|
||||
await mountWithCleanup(TestComponent);
|
||||
const input = queryOne("input");
|
||||
new Macro({
|
||||
name: "test",
|
||||
steps: [
|
||||
{ action: () => expect.step("1") },
|
||||
{ action: () => expect.step("2") },
|
||||
{
|
||||
trigger: "div.counter input",
|
||||
async action(trigger) {
|
||||
await click(trigger);
|
||||
await edit("aaron", { confirm: "blur" });
|
||||
},
|
||||
},
|
||||
{ action: () => expect.step("3") },
|
||||
],
|
||||
}).start(queryOne(".counter"));
|
||||
expect(input).toHaveValue("");
|
||||
await waitForMacro();
|
||||
expect(input).toHaveValue("aaron");
|
||||
expect.verifySteps(["1", "2", "3"]);
|
||||
});
|
||||
|
||||
test("onStep function is called at each step", async () => {
|
||||
await mountWithCleanup(TestComponent);
|
||||
const span = queryOne("span.value");
|
||||
expect(span).toHaveText("0");
|
||||
|
||||
new Macro({
|
||||
name: "test",
|
||||
onStep: (el, step, index) => {
|
||||
expect.step(index);
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
action: () => {
|
||||
console.log("brol");
|
||||
},
|
||||
},
|
||||
{
|
||||
trigger: "button.inc",
|
||||
async action(trigger) {
|
||||
await click(trigger);
|
||||
},
|
||||
},
|
||||
],
|
||||
}).start(queryOne(".counter"));
|
||||
await waitForMacro();
|
||||
expect(span).toHaveText("1");
|
||||
expect.verifySteps([0, 1]);
|
||||
});
|
||||
|
||||
test("trigger can be a function returning an htmlelement", async () => {
|
||||
await mountWithCleanup(TestComponent);
|
||||
const span = queryOne("span.value");
|
||||
expect(span).toHaveText("0");
|
||||
|
||||
new Macro({
|
||||
name: "test",
|
||||
steps: [
|
||||
{
|
||||
trigger: () => queryOne("button.inc"),
|
||||
async action(trigger) {
|
||||
await click(trigger);
|
||||
},
|
||||
},
|
||||
],
|
||||
}).start(queryOne(".counter"));
|
||||
expect(span).toHaveText("0");
|
||||
await waitForMacro();
|
||||
expect(span).toHaveText("1");
|
||||
});
|
||||
|
||||
test("macro wait element is visible to do action", async () => {
|
||||
await mountWithCleanup(TestComponent);
|
||||
const span = queryOne("span.value");
|
||||
const button = queryOne("button.inc");
|
||||
button.classList.add("d-none");
|
||||
expect(span).toHaveText("0");
|
||||
new Macro({
|
||||
name: "test",
|
||||
timeout: 1000,
|
||||
steps: [
|
||||
{
|
||||
trigger: "button.inc",
|
||||
action: () => {
|
||||
expect.step("element is now visible");
|
||||
},
|
||||
},
|
||||
],
|
||||
onError: (error) => {
|
||||
expect.step(error);
|
||||
},
|
||||
}).start(queryOne(".counter"));
|
||||
advanceTime(500);
|
||||
button.classList.remove("d-none");
|
||||
await waitForMacro();
|
||||
expect.verifySteps(["element is now visible"]);
|
||||
});
|
||||
|
||||
test("macro timeout if element is not visible", async () => {
|
||||
await mountWithCleanup(TestComponent);
|
||||
const span = queryOne("span.value");
|
||||
const button = queryOne("button.inc");
|
||||
button.classList.add("d-none");
|
||||
expect(span).toHaveText("0");
|
||||
const macro = new Macro({
|
||||
name: "test",
|
||||
timeout: 1000,
|
||||
steps: [
|
||||
{
|
||||
trigger: "button.inc",
|
||||
action: () => {
|
||||
expect.step("element is now visible");
|
||||
},
|
||||
},
|
||||
],
|
||||
onError: (error) => {
|
||||
expect.step(error.message);
|
||||
},
|
||||
});
|
||||
macro.start(queryOne(".counter"));
|
||||
await waitForMacro();
|
||||
expect.verifySteps(["TIMEOUT step failed to complete within 1000 ms."]);
|
||||
});
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
import { beforeEach, expect, onError, test } from "@odoo/hoot";
|
||||
import { animationFrame, Deferred } from "@odoo/hoot-mock";
|
||||
import { clearRegistry, mountWithCleanup, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { Component, onWillStart, useState, xml } from "@odoo/owl";
|
||||
|
||||
const mainComponentsRegistry = registry.category("main_components");
|
||||
|
||||
beforeEach(async () => {
|
||||
clearRegistry(mainComponentsRegistry);
|
||||
});
|
||||
|
||||
test("simple rendering", async () => {
|
||||
class MainComponentA extends Component {
|
||||
static template = xml`<span>MainComponentA</span>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
class MainComponentB extends Component {
|
||||
static template = xml`<span>MainComponentB</span>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
mainComponentsRegistry.add("MainComponentA", { Component: MainComponentA, props: {} });
|
||||
mainComponentsRegistry.add("MainComponentB", { Component: MainComponentB, props: {} });
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
expect("div.o-main-components-container").toHaveCount(1);
|
||||
expect(".o-main-components-container").toHaveInnerHTML(`
|
||||
<span>MainComponentA</span>
|
||||
<span>MainComponentB</span>
|
||||
<div class="o-overlay-container"></div>
|
||||
<div></div>
|
||||
<div class="o_notification_manager"></div>
|
||||
`);
|
||||
});
|
||||
|
||||
test("unmounts erroring main component", async () => {
|
||||
expect.assertions(6);
|
||||
expect.errors(1);
|
||||
onError((error) => {
|
||||
expect.step(error.reason.message);
|
||||
expect.step(error.reason.cause.message);
|
||||
});
|
||||
let compA;
|
||||
class MainComponentA extends Component {
|
||||
static template = xml`<span><t t-if="state.shouldThrow" t-esc="error"/>MainComponentA</span>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
compA = this;
|
||||
this.state = useState({ shouldThrow: false });
|
||||
}
|
||||
get error() {
|
||||
throw new Error("BOOM");
|
||||
}
|
||||
}
|
||||
|
||||
class MainComponentB extends Component {
|
||||
static template = xml`<span>MainComponentB</span>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
mainComponentsRegistry.add("MainComponentA", { Component: MainComponentA, props: {} });
|
||||
mainComponentsRegistry.add("MainComponentB", { Component: MainComponentB, props: {} });
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
expect("div.o-main-components-container").toHaveCount(1);
|
||||
expect(".o-main-components-container").toHaveInnerHTML(`
|
||||
<span>MainComponentA</span><span>MainComponentB</span>
|
||||
<div class="o-overlay-container"></div>
|
||||
<div></div>
|
||||
<div class="o_notification_manager"></div>
|
||||
`);
|
||||
compA.state.shouldThrow = true;
|
||||
await animationFrame();
|
||||
expect.verifySteps([
|
||||
'An error occured in the owl lifecycle (see this Error\'s "cause" property)',
|
||||
"BOOM",
|
||||
]);
|
||||
expect.verifyErrors(["BOOM"]);
|
||||
|
||||
expect(".o-main-components-container span").toHaveCount(1);
|
||||
expect(".o-main-components-container span").toHaveInnerHTML("MainComponentB");
|
||||
});
|
||||
|
||||
test("unmounts erroring main component: variation", async () => {
|
||||
expect.assertions(6);
|
||||
expect.errors(1);
|
||||
onError((error) => {
|
||||
expect.step(error.reason.message);
|
||||
expect.step(error.reason.cause.message);
|
||||
});
|
||||
class MainComponentA extends Component {
|
||||
static template = xml`<span>MainComponentA</span>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
let compB;
|
||||
class MainComponentB extends Component {
|
||||
static template = xml`<span><t t-if="state.shouldThrow" t-esc="error"/>MainComponentB</span>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
compB = this;
|
||||
this.state = useState({ shouldThrow: false });
|
||||
}
|
||||
get error() {
|
||||
throw new Error("BOOM");
|
||||
}
|
||||
}
|
||||
|
||||
mainComponentsRegistry.add("MainComponentA", { Component: MainComponentA, props: {} });
|
||||
mainComponentsRegistry.add("MainComponentB", { Component: MainComponentB, props: {} });
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
expect("div.o-main-components-container").toHaveCount(1);
|
||||
expect(".o-main-components-container").toHaveInnerHTML(`
|
||||
<span>MainComponentA</span><span>MainComponentB</span>
|
||||
<div class="o-overlay-container"></div>
|
||||
<div></div>
|
||||
<div class="o_notification_manager"></div>
|
||||
`);
|
||||
compB.state.shouldThrow = true;
|
||||
await animationFrame();
|
||||
expect.verifySteps([
|
||||
'An error occured in the owl lifecycle (see this Error\'s "cause" property)',
|
||||
"BOOM",
|
||||
]);
|
||||
expect.verifyErrors(["BOOM"]);
|
||||
expect(".o-main-components-container span").toHaveCount(1);
|
||||
expect(".o-main-components-container span").toHaveInnerHTML("MainComponentA");
|
||||
});
|
||||
|
||||
test("MainComponentsContainer re-renders when the registry changes", async () => {
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
|
||||
expect(".myMainComponent").toHaveCount(0);
|
||||
class MyMainComponent extends Component {
|
||||
static template = xml`<div class="myMainComponent" />`;
|
||||
static props = ["*"];
|
||||
}
|
||||
mainComponentsRegistry.add("myMainComponent", { Component: MyMainComponent });
|
||||
await animationFrame();
|
||||
expect(".myMainComponent").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("Should be possible to add a new component when MainComponentContainer is not mounted yet", async () => {
|
||||
const defer = new Deferred();
|
||||
patchWithCleanup(MainComponentsContainer.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
onWillStart(async () => {
|
||||
await defer;
|
||||
});
|
||||
},
|
||||
});
|
||||
mountWithCleanup(MainComponentsContainer);
|
||||
class MyMainComponent extends Component {
|
||||
static template = xml`<div class="myMainComponent" />`;
|
||||
static props = ["*"];
|
||||
}
|
||||
// Wait for the setup of MainComponentsContainer to be completed
|
||||
await animationFrame();
|
||||
mainComponentsRegistry.add("myMainComponent", { Component: MyMainComponent });
|
||||
// Release the component mounting
|
||||
defer.resolve();
|
||||
await animationFrame();
|
||||
expect(".myMainComponent").toHaveCount(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,921 @@
|
|||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import {
|
||||
clickPrev,
|
||||
followRelation,
|
||||
getDisplayedFieldNames,
|
||||
getFocusedFieldName,
|
||||
getModelFieldSelectorValues,
|
||||
getTitle,
|
||||
openModelFieldSelectorPopover,
|
||||
} from "@web/../tests/core/tree_editor/condition_tree_editor_test_helpers";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector";
|
||||
|
||||
class Partner extends models.Model {
|
||||
foo = fields.Char();
|
||||
bar = fields.Boolean();
|
||||
product_id = fields.Many2one({ relation: "product" });
|
||||
json_field = fields.Json();
|
||||
|
||||
_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 },
|
||||
];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char({ string: "Product Name" });
|
||||
|
||||
_records = [
|
||||
{ id: 37, name: "xphone" },
|
||||
{ id: 41, name: "xpad" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner, Product]);
|
||||
|
||||
function addProperties() {
|
||||
Partner._fields.properties = fields.Properties({
|
||||
string: "Properties",
|
||||
definition_record: "product_id",
|
||||
definition_record_field: "definitions",
|
||||
});
|
||||
Product._fields.definitions = fields.PropertiesDefinition({
|
||||
string: "Definitions",
|
||||
});
|
||||
Product._records[0].definitions = [
|
||||
{ name: "xphone_prop_1", string: "P1", type: "boolean" },
|
||||
{ name: "xphone_prop_2", string: "P2", type: "char" },
|
||||
];
|
||||
Product._records[1].definitions = [{ name: "xpad_prop_1", string: "P1", type: "date" }];
|
||||
}
|
||||
|
||||
test("creating a field chain from scratch", async () => {
|
||||
const getValueFromDOM = (root) =>
|
||||
queryAllTexts(".o_model_field_selector_chain_part", { root }).join(" -> ");
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { ModelFieldSelector };
|
||||
static template = xml`
|
||||
<ModelFieldSelector
|
||||
readonly="false"
|
||||
resModel="'partner'"
|
||||
path="path"
|
||||
isDebugMode="false"
|
||||
update="(path) => this.onUpdate(path)"
|
||||
/>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.path = "";
|
||||
}
|
||||
onUpdate(path) {
|
||||
expect.step(`update: ${path}`);
|
||||
this.path = path;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
const fieldSelector = await mountWithCleanup(Parent);
|
||||
|
||||
await openModelFieldSelectorPopover();
|
||||
expect("input.o_input[placeholder='Search...']").toBeFocused();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(1);
|
||||
|
||||
// The field selector popover should contain the list of "partner"
|
||||
// fields. "Bar" should be among them.
|
||||
expect(".o_model_field_selector_popover_item_name:first").toHaveText("Bar");
|
||||
|
||||
// Clicking the "Bar" field should close the popover and set the field
|
||||
// chain to "bar" as it is a basic field
|
||||
await contains(".o_model_field_selector_popover_item_name").click();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
expect(getValueFromDOM()).toBe("Bar");
|
||||
expect(fieldSelector.path).toBe("bar");
|
||||
expect.verifySteps(["update: bar"]);
|
||||
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(1);
|
||||
// The field selector popover should contain the list of "partner"
|
||||
// fields. "Product" should be among them.
|
||||
expect(
|
||||
".o_model_field_selector_popover .o_model_field_selector_popover_relation_icon"
|
||||
).toHaveCount(1, { message: "field selector popover should contain the 'Product' field" });
|
||||
|
||||
// Clicking on the "Product" field should update the popover to show
|
||||
// the product fields (so only "Product Name" and the default fields should be there)
|
||||
await contains(
|
||||
".o_model_field_selector_popover .o_model_field_selector_popover_relation_icon"
|
||||
).click();
|
||||
expect(".o_model_field_selector_popover_item_name").toHaveCount(5);
|
||||
expect(queryAllTexts(".o_model_field_selector_popover_item_name").at(-1)).toBe("Product Name", {
|
||||
message: "the name of the last suggestion should be 'Product Name'",
|
||||
});
|
||||
await contains(".o_model_field_selector_popover_item_name:last").click();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
expect(getValueFromDOM()).toBe("Product -> Product Name");
|
||||
expect.verifySteps(["update: product_id.name"]);
|
||||
|
||||
// Remove the current selection and recreate it again
|
||||
await openModelFieldSelectorPopover();
|
||||
await contains(".o_model_field_selector_popover_prev_page").click();
|
||||
await contains(".o_model_field_selector_popover_close").click();
|
||||
expect.verifySteps(["update: product_id"]);
|
||||
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(
|
||||
".o_model_field_selector_popover .o_model_field_selector_popover_relation_icon"
|
||||
).toHaveCount(1);
|
||||
|
||||
await contains(
|
||||
".o_model_field_selector_popover .o_model_field_selector_popover_relation_icon"
|
||||
).click();
|
||||
await contains(".o_model_field_selector_popover_item_name:last").click();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
expect(getValueFromDOM()).toBe("Product -> Product Name");
|
||||
expect.verifySteps(["update: product_id.name"]);
|
||||
});
|
||||
|
||||
test("default field chain should set the page data correctly", async () => {
|
||||
await mountWithCleanup(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
path: "product_id",
|
||||
resModel: "partner",
|
||||
isDebugMode: false,
|
||||
},
|
||||
});
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(".o_model_field_selector_popover").toHaveCount(1);
|
||||
expect(getDisplayedFieldNames()).toEqual([
|
||||
"Bar",
|
||||
"Created on",
|
||||
"Display name",
|
||||
"Foo",
|
||||
"Id",
|
||||
"Last Modified on",
|
||||
"Product",
|
||||
]);
|
||||
expect(".o_model_field_selector_popover_item:last").toHaveClass("active");
|
||||
});
|
||||
|
||||
test("use the filter option", async () => {
|
||||
await mountWithCleanup(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
path: "",
|
||||
resModel: "partner",
|
||||
filter: (field) => field.type === "many2one" && field.searchable,
|
||||
},
|
||||
});
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(getDisplayedFieldNames()).toEqual(["Product"]);
|
||||
});
|
||||
|
||||
test("default `showSearchInput` option", async () => {
|
||||
await mountWithCleanup(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
path: "",
|
||||
resModel: "partner",
|
||||
},
|
||||
});
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(".o_model_field_selector_popover .o_model_field_selector_popover_search").toHaveCount(1);
|
||||
expect(getDisplayedFieldNames()).toEqual([
|
||||
"Bar",
|
||||
"Created on",
|
||||
"Display name",
|
||||
"Foo",
|
||||
"Id",
|
||||
"Last Modified on",
|
||||
"Product",
|
||||
]);
|
||||
|
||||
// search 'xx'
|
||||
await contains(
|
||||
".o_model_field_selector_popover .o_model_field_selector_popover_search input"
|
||||
).edit("xx", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(getDisplayedFieldNames()).toBeEmpty();
|
||||
|
||||
// search 'Pro'
|
||||
await contains(
|
||||
".o_model_field_selector_popover .o_model_field_selector_popover_search input"
|
||||
).edit("Pro", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(getDisplayedFieldNames()).toEqual(["Product"]);
|
||||
});
|
||||
|
||||
test("false `showSearchInput` option", async () => {
|
||||
await mountWithCleanup(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
showSearchInput: false,
|
||||
path: "",
|
||||
resModel: "partner",
|
||||
},
|
||||
});
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(".o_model_field_selector_popover .o_model_field_selector_popover_search").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("create a field chain with value 1 i.e. TRUE_LEAF", async () => {
|
||||
await mountWithCleanup(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
showSearchInput: false,
|
||||
path: 1,
|
||||
resModel: "partner",
|
||||
},
|
||||
});
|
||||
expect(".o_model_field_selector_chain_part").toHaveText("1");
|
||||
});
|
||||
|
||||
test("create a field chain with value 0 i.e. FALSE_LEAF", async () => {
|
||||
await mountWithCleanup(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
showSearchInput: false,
|
||||
path: 0,
|
||||
resModel: "partner",
|
||||
},
|
||||
});
|
||||
expect(".o_model_field_selector_chain_part").toHaveText("0", {
|
||||
message: "field name value should be 0.",
|
||||
});
|
||||
});
|
||||
|
||||
test("cache fields_get", async () => {
|
||||
Partner._fields.partner_id = fields.Many2one({
|
||||
string: "Partner",
|
||||
relation: "partner",
|
||||
});
|
||||
|
||||
onRpc("fields_get", ({ method }) => expect.step(method));
|
||||
|
||||
await mountWithCleanup(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
path: "partner_id.partner_id.partner_id.foo",
|
||||
resModel: "partner",
|
||||
},
|
||||
});
|
||||
expect.verifySteps(["fields_get"]);
|
||||
});
|
||||
|
||||
test("Using back button in popover", async () => {
|
||||
Partner._fields.partner_id = fields.Many2one({
|
||||
string: "Partner",
|
||||
relation: "partner",
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { ModelFieldSelector };
|
||||
static template = xml`
|
||||
<ModelFieldSelector
|
||||
readonly="false"
|
||||
resModel="'partner'"
|
||||
path="path"
|
||||
update="(path) => this.onUpdate(path)"
|
||||
/>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.path = "partner_id.foo";
|
||||
}
|
||||
onUpdate(path) {
|
||||
this.path = path;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Partner", "Foo"]);
|
||||
expect(".o_model_field_selector i.o_model_field_selector_warning").toHaveCount(0);
|
||||
|
||||
await openModelFieldSelectorPopover();
|
||||
await contains(".o_model_field_selector_popover_prev_page").click();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Partner"]);
|
||||
expect(".o_model_field_selector i.o_model_field_selector_warning").toHaveCount(0);
|
||||
|
||||
await contains(
|
||||
".o_model_field_selector_popover_item:nth-child(1) .o_model_field_selector_popover_item_name"
|
||||
).click();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Bar"]);
|
||||
expect(".o_model_field_selector_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("select a relational field does not follow relation", async () => {
|
||||
await mountWithCleanup(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
path: "",
|
||||
resModel: "partner",
|
||||
update(path) {
|
||||
expect.step(path);
|
||||
},
|
||||
},
|
||||
});
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(
|
||||
".o_model_field_selector_popover_item:last-child .o_model_field_selector_popover_relation_icon"
|
||||
).toHaveCount(1);
|
||||
|
||||
await contains(
|
||||
".o_model_field_selector_popover_item:last-child .o_model_field_selector_popover_item_name"
|
||||
).click();
|
||||
expect.verifySteps(["product_id"]);
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(getDisplayedFieldNames()).toEqual([
|
||||
"Bar",
|
||||
"Created on",
|
||||
"Display name",
|
||||
"Foo",
|
||||
"Id",
|
||||
"Last Modified on",
|
||||
"Product",
|
||||
]);
|
||||
expect(".o_model_field_selector_popover_relation_icon").toHaveCount(1);
|
||||
|
||||
await contains(".o_model_field_selector_popover_relation_icon").click();
|
||||
expect(getDisplayedFieldNames()).toEqual([
|
||||
"Created on",
|
||||
"Display name",
|
||||
"Id",
|
||||
"Last Modified on",
|
||||
"Product Name",
|
||||
]);
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
|
||||
await contains(".o_model_field_selector_popover_item_name").click();
|
||||
expect.verifySteps(["product_id.create_date"]);
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("can follow relations", async () => {
|
||||
await mountWithCleanup(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
path: "",
|
||||
resModel: "partner",
|
||||
followRelations: true, // default
|
||||
update(path) {
|
||||
expect(path).toBe("product_id");
|
||||
},
|
||||
},
|
||||
});
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(getDisplayedFieldNames()).toEqual([
|
||||
"Bar",
|
||||
"Created on",
|
||||
"Display name",
|
||||
"Foo",
|
||||
"Id",
|
||||
"Last Modified on",
|
||||
"Product",
|
||||
]);
|
||||
expect(".o_model_field_selector_popover_relation_icon").toHaveCount(1);
|
||||
|
||||
await contains(".o_model_field_selector_popover_relation_icon").click();
|
||||
expect(getDisplayedFieldNames()).toEqual([
|
||||
"Created on",
|
||||
"Display name",
|
||||
"Id",
|
||||
"Last Modified on",
|
||||
"Product Name",
|
||||
]);
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("cannot follow relations", async () => {
|
||||
await mountWithCleanup(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
path: "",
|
||||
resModel: "partner",
|
||||
followRelations: false,
|
||||
update(path) {
|
||||
expect(path).toBe("product_id");
|
||||
},
|
||||
},
|
||||
});
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(getDisplayedFieldNames()).toEqual([
|
||||
"Bar",
|
||||
"Created on",
|
||||
"Display name",
|
||||
"Foo",
|
||||
"Id",
|
||||
"Last Modified on",
|
||||
"Product",
|
||||
]);
|
||||
expect(".o_model_field_selector_popover_relation_icon").toHaveCount(0);
|
||||
await contains(".o_model_field_selector_popover_item_name:last").click();
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Product"]);
|
||||
});
|
||||
|
||||
test("Edit path in popover debug input", async () => {
|
||||
Partner._fields.partner_id = fields.Many2one({
|
||||
string: "Partner",
|
||||
relation: "partner",
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { ModelFieldSelector };
|
||||
static template = xml`
|
||||
<ModelFieldSelector
|
||||
readonly="false"
|
||||
resModel="'partner'"
|
||||
path="path"
|
||||
isDebugMode="true"
|
||||
update="(pathInfo) => this.onUpdate(pathInfo)"
|
||||
/>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.path = "foo";
|
||||
}
|
||||
onUpdate(path) {
|
||||
this.path = path;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Foo"]);
|
||||
|
||||
await openModelFieldSelectorPopover();
|
||||
await contains(".o_model_field_selector_popover .o_model_field_selector_debug").edit(
|
||||
"partner_id.bar"
|
||||
);
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Partner", "Bar"]);
|
||||
});
|
||||
|
||||
test("title on first four pages", async () => {
|
||||
class Turtle extends models.Model {
|
||||
mother_id = fields.Many2one({
|
||||
string: "Mother",
|
||||
relation: "turtle",
|
||||
});
|
||||
}
|
||||
defineModels([Turtle]);
|
||||
|
||||
await mountWithCleanup(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
path: "mother_id",
|
||||
resModel: "turtle",
|
||||
},
|
||||
});
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(getTitle()).toBe("Select a field");
|
||||
|
||||
await followRelation();
|
||||
expect(getTitle()).toBe("Mother");
|
||||
|
||||
await followRelation();
|
||||
expect(getTitle()).toBe("... > Mother");
|
||||
|
||||
await followRelation();
|
||||
expect(getTitle()).toBe("... > Mother");
|
||||
});
|
||||
|
||||
test("start on complex path and click prev", async () => {
|
||||
class Turtle extends models.Model {
|
||||
mother_id = fields.Many2one({
|
||||
string: "Mother",
|
||||
relation: "turtle",
|
||||
});
|
||||
father_id = fields.Many2one({
|
||||
string: "Father",
|
||||
relation: "turtle",
|
||||
});
|
||||
}
|
||||
defineModels([Turtle]);
|
||||
|
||||
await mountWithCleanup(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
path: "mother_id.father_id.mother_id",
|
||||
resModel: "turtle",
|
||||
},
|
||||
});
|
||||
|
||||
await openModelFieldSelectorPopover();
|
||||
// viewing third page
|
||||
// mother is selected on that page
|
||||
expect(getTitle()).toBe("... > Father");
|
||||
expect(getFocusedFieldName()).toBe("Mother");
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Mother", "Father", "Mother"]);
|
||||
|
||||
// select Father on third page and go to next page
|
||||
// no selection on fourth page --> first item is focused
|
||||
await followRelation();
|
||||
expect(getTitle()).toBe("... > Father");
|
||||
expect(getFocusedFieldName()).toBe("Created on");
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Mother", "Father", "Father"]);
|
||||
|
||||
// go back to third page. Nothing has changed
|
||||
await clickPrev();
|
||||
expect(getTitle()).toBe("... > Father");
|
||||
expect(getFocusedFieldName()).toBe("Father");
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Mother", "Father", "Father"]);
|
||||
|
||||
// go back to second page. Nothing has changed.
|
||||
await clickPrev();
|
||||
expect(getTitle()).toBe("Mother");
|
||||
expect(getFocusedFieldName()).toBe("Father");
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Mother", "Father"]);
|
||||
|
||||
// go back to first page. Nothing has changed.
|
||||
await clickPrev();
|
||||
expect(getTitle()).toBe("Select a field");
|
||||
expect(getFocusedFieldName()).toBe("Mother");
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Mother"]);
|
||||
expect(".o_model_field_selector_popover_prev_page").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("support of invalid paths (allowEmpty=false)", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { ModelFieldSelector };
|
||||
static template = xml`<ModelFieldSelector resModel="'partner'" readonly="false" path="state.path" />`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({ path: `` });
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mountWithCleanup(Parent);
|
||||
expect(getModelFieldSelectorValues()).toEqual(["-"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
|
||||
parent.state.path = undefined;
|
||||
await animationFrame();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["-"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
|
||||
parent.state.path = false;
|
||||
await animationFrame();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["-"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
|
||||
parent.state.path = {};
|
||||
await animationFrame();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["-"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
|
||||
parent.state.path = `a`;
|
||||
await animationFrame();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["a"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
|
||||
parent.state.path = `foo.a`;
|
||||
await animationFrame();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Foo", "a"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
|
||||
parent.state.path = `a.foo`;
|
||||
await animationFrame();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["a", "foo"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("support of invalid paths (allowEmpty=true)", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { ModelFieldSelector };
|
||||
static template = xml`<ModelFieldSelector resModel="'partner'" readonly="false" path="state.path" allowEmpty="true" />`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({ path: `` });
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mountWithCleanup(Parent);
|
||||
expect(getModelFieldSelectorValues()).toEqual([]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(0);
|
||||
|
||||
parent.state.path = undefined;
|
||||
await animationFrame();
|
||||
expect(getModelFieldSelectorValues()).toEqual([]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(0);
|
||||
|
||||
parent.state.path = false;
|
||||
await animationFrame();
|
||||
expect(getModelFieldSelectorValues()).toEqual([]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(0);
|
||||
|
||||
parent.state.path = {};
|
||||
await animationFrame();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["-"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
|
||||
parent.state.path = `a`;
|
||||
await animationFrame();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["a"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
|
||||
parent.state.path = `foo.a`;
|
||||
await animationFrame();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Foo", "a"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
|
||||
parent.state.path = `a.foo`;
|
||||
await animationFrame();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["a", "foo"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("debug input", async () => {
|
||||
expect.assertions(10);
|
||||
let num = 1;
|
||||
class Parent extends Component {
|
||||
static components = { ModelFieldSelector };
|
||||
static template = xml`<ModelFieldSelector resModel="'partner'" readonly="false" isDebugMode="true" path="state.path" update.bind="update"/>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({ path: `` });
|
||||
}
|
||||
update(path, fieldInfo) {
|
||||
if (num === 1) {
|
||||
expect(path).toBe("a");
|
||||
expect(fieldInfo).toEqual({
|
||||
fieldDef: null,
|
||||
resModel: "partner",
|
||||
});
|
||||
num++;
|
||||
} else {
|
||||
expect(path).toBe("foo");
|
||||
expect(fieldInfo).toEqual({
|
||||
resModel: "partner",
|
||||
fieldDef: {
|
||||
string: "Foo",
|
||||
readonly: false,
|
||||
required: false,
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
store: true,
|
||||
groupable: true,
|
||||
type: "char",
|
||||
name: "foo",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(getModelFieldSelectorValues()).toEqual(["-"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
|
||||
await openModelFieldSelectorPopover();
|
||||
await contains(".o_model_field_selector_debug").edit("a", { confirm: false });
|
||||
await contains(".o_model_field_selector_popover_search").click();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["a"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
|
||||
await contains(".o_model_field_selector_popover_close").click();
|
||||
|
||||
await openModelFieldSelectorPopover();
|
||||
await contains(".o_model_field_selector_debug").edit("foo");
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Foo"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(0);
|
||||
|
||||
await contains(".o_model_field_selector_popover_close").click();
|
||||
});
|
||||
|
||||
test("focus on search input", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { ModelFieldSelector };
|
||||
static template = xml`<ModelFieldSelector resModel="'partner'" readonly="false" path="state.path" update.bind="update"/>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({ path: `foo` });
|
||||
}
|
||||
update() {}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(".o_model_field_selector_popover_search .o_input").toBeFocused();
|
||||
|
||||
await followRelation();
|
||||
expect(".o_model_field_selector_popover_search .o_input").toBeFocused();
|
||||
});
|
||||
|
||||
test("support properties", async () => {
|
||||
addProperties();
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { ModelFieldSelector };
|
||||
static template = xml`
|
||||
<ModelFieldSelector
|
||||
readonly="false"
|
||||
resModel="'partner'"
|
||||
path="path"
|
||||
isDebugMode="true"
|
||||
update="(path, fieldInfo) => this.onUpdate(path)"
|
||||
/>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.path = "foo";
|
||||
}
|
||||
onUpdate(path) {
|
||||
this.path = path;
|
||||
expect.step(path);
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(getTitle()).toBe("Select a field");
|
||||
expect('.o_model_field_selector_popover_item[data-name="properties"]').toHaveCount(1);
|
||||
expect(
|
||||
'.o_model_field_selector_popover_item[data-name="properties"] .o_model_field_selector_popover_relation_icon'
|
||||
).toHaveCount(1);
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Foo"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(0);
|
||||
|
||||
await contains(
|
||||
'.o_model_field_selector_popover_item[data-name="properties"] .o_model_field_selector_popover_relation_icon'
|
||||
).click();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Properties"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
expect.verifySteps([]);
|
||||
|
||||
await clickPrev();
|
||||
expect(getTitle()).toBe("Select a field");
|
||||
await contains(
|
||||
'.o_model_field_selector_popover_item[data-name="properties"] .o_model_field_selector_popover_item_name'
|
||||
).click();
|
||||
expect(getTitle()).toBe("Properties");
|
||||
expect(".o_model_field_selector_value").toHaveText("Properties");
|
||||
expect(".o_model_field_selector_popover_item").toHaveCount(3);
|
||||
expect('.o_model_field_selector_popover_item[data-name="xphone_prop_1"]').toHaveCount(1);
|
||||
expect('.o_model_field_selector_popover_item[data-name="xphone_prop_2"]').toHaveCount(1);
|
||||
expect('.o_model_field_selector_popover_item[data-name="xpad_prop_1"]').toHaveCount(1);
|
||||
expect(getDisplayedFieldNames()).toEqual([
|
||||
"P1 (xphone)\nxphone_prop_1 (boolean)",
|
||||
"P1 (xpad)\nxpad_prop_1 (date)",
|
||||
"P2 (xphone)\nxphone_prop_2 (char)",
|
||||
]);
|
||||
|
||||
await contains(
|
||||
'.o_model_field_selector_popover_item[data-name="xphone_prop_2"] .o_model_field_selector_popover_item_name'
|
||||
).click();
|
||||
expect.verifySteps(["properties.xphone_prop_2"]);
|
||||
expect(".o_model_field_selector_value").toHaveText("PropertiesP2");
|
||||
expect(".o_model_field_selector_warning").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("search on field string and name in debug mode", async () => {
|
||||
Partner._fields.ucit = fields.Char({
|
||||
type: "char",
|
||||
string: "Some string",
|
||||
});
|
||||
class Parent extends Component {
|
||||
static components = { ModelFieldSelector };
|
||||
static template = xml`
|
||||
<ModelFieldSelector
|
||||
readonly="false"
|
||||
resModel="'partner'"
|
||||
path="'foo'"
|
||||
isDebugMode="true"
|
||||
/>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
await mountWithCleanup(Parent);
|
||||
await openModelFieldSelectorPopover();
|
||||
await contains(
|
||||
".o_model_field_selector_popover .o_model_field_selector_popover_search input"
|
||||
).edit("uct", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect(getDisplayedFieldNames()).toEqual([
|
||||
"Product\nproduct_id (many2one)",
|
||||
"Some string\nucit (char)",
|
||||
]);
|
||||
});
|
||||
|
||||
test("clear button (allowEmpty=true)", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { ModelFieldSelector };
|
||||
static template = xml`
|
||||
<ModelFieldSelector
|
||||
readonly="false"
|
||||
resModel="'partner'"
|
||||
path="path"
|
||||
allowEmpty="true"
|
||||
isDebugMode="true"
|
||||
update="(path, fieldInfo) => this.onUpdate(path)"
|
||||
/>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.path = "baaarrr";
|
||||
}
|
||||
onUpdate(path) {
|
||||
this.path = path;
|
||||
expect.step(`path is ${JSON.stringify(path)}`);
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
expect(getModelFieldSelectorValues()).toEqual(["baaarrr"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(1);
|
||||
expect(".o_model_field_selector .fa.fa-times").toHaveCount(1);
|
||||
|
||||
// clear when popover is not open
|
||||
await contains(".o_model_field_selector .fa.fa-times").click();
|
||||
expect(getModelFieldSelectorValues()).toEqual([]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(0);
|
||||
expect(".o_model_field_selector .fa.fa-times").toHaveCount(0);
|
||||
expect.verifySteps([`path is ""`]);
|
||||
|
||||
await openModelFieldSelectorPopover();
|
||||
await contains(".o_model_field_selector_popover_item_name").click();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Bar"]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(0);
|
||||
expect(".o_model_field_selector .fa.fa-times").toHaveCount(1);
|
||||
expect.verifySteps([`path is "bar"`]);
|
||||
|
||||
// clear when popover is open
|
||||
await openModelFieldSelectorPopover();
|
||||
await contains(".o_model_field_selector .fa.fa-times").click();
|
||||
expect(getModelFieldSelectorValues()).toEqual([]);
|
||||
expect(".o_model_field_selector_warning").toHaveCount(0);
|
||||
expect(".o_model_field_selector .fa.fa-times").toHaveCount(0);
|
||||
expect.verifySteps([`path is ""`]);
|
||||
});
|
||||
|
||||
test("Modify path in popover debug input and click away", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { ModelFieldSelector };
|
||||
static template = xml`
|
||||
<ModelFieldSelector
|
||||
readonly="false"
|
||||
resModel="'partner'"
|
||||
path="path"
|
||||
isDebugMode="true"
|
||||
update.bind="update"
|
||||
/>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.path = "foo";
|
||||
}
|
||||
update(path) {
|
||||
this.path = path;
|
||||
expect.step(path);
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Foo"]);
|
||||
|
||||
await openModelFieldSelectorPopover();
|
||||
await contains(".o_model_field_selector_popover .o_model_field_selector_debug").edit(
|
||||
"foooooo",
|
||||
{ confirm: false }
|
||||
);
|
||||
expect(getModelFieldSelectorValues()).toEqual(["Foo"]);
|
||||
|
||||
await contains(getFixture()).click();
|
||||
expect(getModelFieldSelectorValues()).toEqual(["foooooo"]);
|
||||
expect.verifySteps(["foooooo"]);
|
||||
});
|
||||
|
||||
test("showDebugInput = false", async () => {
|
||||
await mountWithCleanup(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
path: "product_id",
|
||||
resModel: "partner",
|
||||
isDebugMode: true,
|
||||
showDebugInput: false,
|
||||
},
|
||||
});
|
||||
|
||||
await openModelFieldSelectorPopover();
|
||||
expect(".o_model_field_selector_debug").toHaveCount(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { queryAll } from "@odoo/hoot-dom";
|
||||
import { runAllTimers } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { ModelSelector } from "@web/core/model_selector/model_selector";
|
||||
|
||||
async function mountModelSelector(models = [], value = undefined, onModelSelected = () => {}) {
|
||||
await mountWithCleanup(ModelSelector, {
|
||||
props: {
|
||||
models,
|
||||
value,
|
||||
onModelSelected,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
class IrModel extends models.Model {
|
||||
_name = "ir.model";
|
||||
|
||||
name = fields.Char({ string: "Model Name" });
|
||||
model = fields.Char();
|
||||
_records = [
|
||||
{ id: 1, name: "Model 1", model: "model_1" },
|
||||
{ id: 2, name: "Model 2", model: "model_2" },
|
||||
{ id: 3, name: "Model 3", model: "model_3" },
|
||||
{ id: 4, name: "Model 4", model: "model_4" },
|
||||
{ id: 5, name: "Model 5", model: "model_5" },
|
||||
{ id: 6, name: "Model 6", model: "model_6" },
|
||||
{ id: 7, name: "Model 7", model: "model_7" },
|
||||
{ id: 8, name: "Model 8", model: "model_8" },
|
||||
{ id: 9, name: "Model 9", model: "model_9" },
|
||||
{ id: 10, name: "Model 10", model: "model_10" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([IrModel]);
|
||||
describe.current.tags("desktop");
|
||||
|
||||
onRpc("ir.model", "display_name_for", function ({ args }) {
|
||||
const models = args[0];
|
||||
const records = this.env["ir.model"].filter((rec) => models.includes(rec.model));
|
||||
return records.map((record) => ({
|
||||
model: record.model,
|
||||
display_name: record.name,
|
||||
}));
|
||||
});
|
||||
|
||||
test("model_selector: with no model", async () => {
|
||||
await mountModelSelector();
|
||||
await contains(".o-autocomplete--input").click();
|
||||
expect("li.o-autocomplete--dropdown-item").toHaveCount(1);
|
||||
expect("li.o-autocomplete--dropdown-item").toHaveText("No records");
|
||||
});
|
||||
|
||||
test("model_selector: displays model display names", async () => {
|
||||
await mountModelSelector(["model_1", "model_2", "model_3"]);
|
||||
await contains(".o-autocomplete--input").click();
|
||||
expect("li.o-autocomplete--dropdown-item").toHaveCount(3);
|
||||
const items = queryAll("li.o-autocomplete--dropdown-item");
|
||||
expect(items[0]).toHaveText("Model 1");
|
||||
expect(items[1]).toHaveText("Model 2");
|
||||
expect(items[2]).toHaveText("Model 3");
|
||||
});
|
||||
|
||||
test("model_selector: with 8 models", async () => {
|
||||
await mountModelSelector([
|
||||
"model_1",
|
||||
"model_2",
|
||||
"model_3",
|
||||
"model_4",
|
||||
"model_5",
|
||||
"model_6",
|
||||
"model_7",
|
||||
"model_8",
|
||||
]);
|
||||
await contains(".o-autocomplete--input").click();
|
||||
expect("li.o-autocomplete--dropdown-item").toHaveCount(8);
|
||||
});
|
||||
|
||||
test("model_selector: with more than 8 models", async () => {
|
||||
await mountModelSelector([
|
||||
"model_1",
|
||||
"model_2",
|
||||
"model_3",
|
||||
"model_4",
|
||||
"model_5",
|
||||
"model_6",
|
||||
"model_7",
|
||||
"model_8",
|
||||
"model_9",
|
||||
"model_10",
|
||||
]);
|
||||
await contains(".o-autocomplete--input").click();
|
||||
expect("li.o-autocomplete--dropdown-item").toHaveCount(9);
|
||||
expect("li.o-autocomplete--dropdown-item:eq(8)").toHaveText("Start typing...");
|
||||
});
|
||||
|
||||
test("model_selector: search content is not applied when opening the autocomplete", async () => {
|
||||
await mountModelSelector(["model_1", "model_2"], "_2");
|
||||
await contains(".o-autocomplete--input").click();
|
||||
expect("li.o-autocomplete--dropdown-item").toHaveCount(2);
|
||||
});
|
||||
|
||||
test("model_selector: with search matching some records on technical name", async () => {
|
||||
await mountModelSelector(["model_1", "model_2"]);
|
||||
await contains(".o-autocomplete--input").click();
|
||||
await contains(".o-autocomplete--input").edit("_2", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect("li.o-autocomplete--dropdown-item").toHaveCount(1);
|
||||
expect("li.o-autocomplete--dropdown-item").toHaveText("Model 2");
|
||||
});
|
||||
|
||||
test("model_selector: with search matching some records on business name", async () => {
|
||||
await mountModelSelector(["model_1", "model_2"]);
|
||||
await contains(".o-autocomplete--input").click();
|
||||
await contains(".o-autocomplete--input").edit(" 2", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect("li.o-autocomplete--dropdown-item").toHaveCount(1);
|
||||
expect("li.o-autocomplete--dropdown-item").toHaveText("Model 2");
|
||||
});
|
||||
|
||||
test("model_selector: with search matching no record", async () => {
|
||||
await mountModelSelector(["model_1", "model_2"]);
|
||||
await contains(".o-autocomplete--input").edit("a random search query", { confirm: false });
|
||||
await runAllTimers();
|
||||
expect("li.o-autocomplete--dropdown-item").toHaveCount(1);
|
||||
expect("li.o-autocomplete--dropdown-item").toHaveText("No records");
|
||||
});
|
||||
|
||||
test("model_selector: select a model", async () => {
|
||||
await mountModelSelector(["model_1", "model_2", "model_3"], "Model 1", (selected) => {
|
||||
expect.step("model selected");
|
||||
expect(selected).toEqual({
|
||||
label: "Model 2",
|
||||
technical: "model_2",
|
||||
});
|
||||
});
|
||||
await contains(".o-autocomplete--input").click();
|
||||
await contains(".o_model_selector_model_2").click();
|
||||
expect.verifySteps(["model selected"]);
|
||||
});
|
||||
|
||||
test("model_selector: click on start typing", async () => {
|
||||
await mountModelSelector([
|
||||
"model_1",
|
||||
"model_2",
|
||||
"model_3",
|
||||
"model_4",
|
||||
"model_5",
|
||||
"model_6",
|
||||
"model_7",
|
||||
"model_8",
|
||||
"model_9",
|
||||
"model_10",
|
||||
]);
|
||||
await contains(".o-autocomplete--input").click();
|
||||
await contains("li.o-autocomplete--dropdown-item:eq(8)").click();
|
||||
expect(".o-autocomplete--input").toHaveValue("");
|
||||
expect(".o-autocomplete.dropdown ul").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("model_selector: with an initial value", async () => {
|
||||
await mountModelSelector(["model_1", "model_2", "model_3"], "Model 1");
|
||||
expect(".o-autocomplete--input").toHaveValue("Model 1");
|
||||
});
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { contains, mountWithCleanup, onRpc } from "@web/../tests/web_test_helpers";
|
||||
import { NameAndSignature } from "@web/core/signature/name_and_signature";
|
||||
|
||||
const getNameAndSignatureButtonNames = () => {
|
||||
return queryAllTexts(".card-header .col-auto").filter((text) => text.length);
|
||||
};
|
||||
|
||||
onRpc("/web/sign/get_fonts/", () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
test("test name_and_signature widget", async () => {
|
||||
const props = {
|
||||
signature: {
|
||||
name: "Don Toliver",
|
||||
},
|
||||
};
|
||||
await mountWithCleanup(NameAndSignature, { props });
|
||||
expect(getNameAndSignatureButtonNames()).toEqual(["Auto", "Draw", "Load"]);
|
||||
expect(".o_web_sign_auto_select_style").toHaveCount(1);
|
||||
expect(".card-header .active").toHaveCount(1);
|
||||
expect(".card-header .active").toHaveText("Auto");
|
||||
expect(".o_web_sign_name_group input").toHaveCount(1);
|
||||
expect(".o_web_sign_name_group input").toHaveValue("Don Toliver");
|
||||
|
||||
await contains(".o_web_sign_draw_button").click();
|
||||
expect(getNameAndSignatureButtonNames()).toEqual(["Auto", "Draw", "Load"]);
|
||||
expect(".o_web_sign_draw_clear").toHaveCount(1);
|
||||
expect(".card-header .active").toHaveCount(1);
|
||||
expect(".card-header .active").toHaveText("Draw");
|
||||
|
||||
await contains(".o_web_sign_load_button").click();
|
||||
expect(getNameAndSignatureButtonNames()).toEqual(["Auto", "Draw", "Load"]);
|
||||
expect(".o_web_sign_load_file").toHaveCount(1);
|
||||
expect(".card-header .active").toHaveCount(1);
|
||||
expect(".card-header .active").toHaveText("Load");
|
||||
});
|
||||
|
||||
test("test name_and_signature widget without name", async () => {
|
||||
await mountWithCleanup(NameAndSignature, { props: { signature: {} } });
|
||||
expect(".card-header").toHaveCount(0);
|
||||
expect(".o_web_sign_name_group input").toHaveCount(1);
|
||||
expect(".o_web_sign_name_group input").toHaveValue("");
|
||||
|
||||
await contains(".o_web_sign_name_group input").fill("plop", { instantly: true });
|
||||
expect(getNameAndSignatureButtonNames()).toEqual(["Auto", "Draw", "Load"]);
|
||||
expect(".o_web_sign_auto_select_style").toHaveCount(1);
|
||||
expect(".card-header .active").toHaveText("Auto");
|
||||
expect(".o_web_sign_name_group input").toHaveCount(1);
|
||||
expect(".o_web_sign_name_group input").toHaveValue("plop");
|
||||
|
||||
await contains(".o_web_sign_draw_button").click();
|
||||
expect(".card-header .active").toHaveCount(1);
|
||||
expect(".card-header .active").toHaveText("Draw");
|
||||
});
|
||||
|
||||
test("test name_and_signature widget with noInputName and default name", async function () {
|
||||
const props = {
|
||||
signature: {
|
||||
name: "Don Toliver",
|
||||
},
|
||||
noInputName: true,
|
||||
};
|
||||
await mountWithCleanup(NameAndSignature, { props });
|
||||
expect(getNameAndSignatureButtonNames()).toEqual(["Auto", "Draw", "Load"]);
|
||||
expect(".o_web_sign_auto_select_style").toHaveCount(1);
|
||||
expect(".card-header .active").toHaveCount(1);
|
||||
expect(".card-header .active").toHaveText("Auto");
|
||||
});
|
||||
|
||||
test("test name_and_signature widget with noInputName and without name", async function () {
|
||||
const props = {
|
||||
signature: {},
|
||||
noInputName: true,
|
||||
};
|
||||
await mountWithCleanup(NameAndSignature, { props });
|
||||
expect(getNameAndSignatureButtonNames()).toEqual(["Draw", "Load"]);
|
||||
expect(".o_web_sign_draw_clear").toHaveCount(1);
|
||||
expect(".card-header .active").toHaveCount(1);
|
||||
expect(".card-header .active").toHaveText("Draw");
|
||||
});
|
||||
|
||||
test("test name_and_signature widget default signature", async function () {
|
||||
const props = {
|
||||
signature: {
|
||||
name: "Brandon Freeman",
|
||||
signatureImage:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+BCQAHBQICJmhD1AAAAABJRU5ErkJggg==",
|
||||
},
|
||||
mode: "draw",
|
||||
signatureType: "signature",
|
||||
noInputName: true,
|
||||
};
|
||||
const res = await mountWithCleanup(NameAndSignature, { props });
|
||||
expect(res.isSignatureEmpty).toBe(false);
|
||||
expect(res.props.signature.isSignatureEmpty).toBe(false);
|
||||
});
|
||||
|
||||
test("test name_and_signature widget update signmode with onSignatureChange prop", async function () {
|
||||
let currentSignMode = "";
|
||||
const props = {
|
||||
signature: { name: "Test Owner" },
|
||||
onSignatureChange: function (signMode) {
|
||||
if (currentSignMode !== signMode) {
|
||||
currentSignMode = signMode;
|
||||
}
|
||||
},
|
||||
};
|
||||
await mountWithCleanup(NameAndSignature, { props });
|
||||
await contains(".o_web_sign_draw_button").click();
|
||||
expect(currentSignMode).toBe("draw");
|
||||
});
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
import { after, describe, expect, test } from "@odoo/hoot";
|
||||
import {
|
||||
defineModels,
|
||||
getService,
|
||||
makeMockEnv,
|
||||
models,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { ERROR_INACCESSIBLE_OR_MISSING } from "@web/core/name_service";
|
||||
import { rpcBus } from "@web/core/network/rpc";
|
||||
|
||||
class Dev extends models.Model {
|
||||
_name = "dev";
|
||||
_rec_name = "display_name";
|
||||
_records = [
|
||||
{ id: 1, display_name: "Julien" },
|
||||
{ id: 2, display_name: "Pierre" },
|
||||
];
|
||||
}
|
||||
|
||||
class PO extends models.Model {
|
||||
_name = "po";
|
||||
_rec_name = "display_name";
|
||||
_records = [{ id: 1, display_name: "Damien" }];
|
||||
}
|
||||
|
||||
defineModels([Dev, PO]);
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("single loadDisplayNames", async () => {
|
||||
await makeMockEnv();
|
||||
const displayNames = await getService("name").loadDisplayNames("dev", [1, 2]);
|
||||
expect(displayNames).toEqual({ 1: "Julien", 2: "Pierre" });
|
||||
});
|
||||
|
||||
test("loadDisplayNames is done in silent mode", async () => {
|
||||
await makeMockEnv();
|
||||
|
||||
const onRPCRequest = ({ detail }) => {
|
||||
const silent = detail.settings.silent ? "(silent)" : "";
|
||||
expect.step(`RPC:REQUEST${silent}`);
|
||||
};
|
||||
rpcBus.addEventListener("RPC:REQUEST", onRPCRequest);
|
||||
after(() => rpcBus.removeEventListener("RPC:REQUEST", onRPCRequest));
|
||||
|
||||
await getService("name").loadDisplayNames("dev", [1]);
|
||||
expect.verifySteps(["RPC:REQUEST(silent)"]);
|
||||
});
|
||||
|
||||
test("single loadDisplayNames following addDisplayNames", async () => {
|
||||
await makeMockEnv();
|
||||
onRpc(({ model, method, kwargs }) => {
|
||||
expect.step(`${model}:${method}:${kwargs.domain[0][2]}`);
|
||||
});
|
||||
|
||||
getService("name").addDisplayNames("dev", { 1: "JUM", 2: "PIPU" });
|
||||
const displayNames = await getService("name").loadDisplayNames("dev", [1, 2]);
|
||||
expect(displayNames).toEqual({ 1: "JUM", 2: "PIPU" });
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("single loadDisplayNames following addDisplayNames (2)", async () => {
|
||||
await makeMockEnv();
|
||||
onRpc(({ model, method, kwargs }) => {
|
||||
expect.step(`${model}:${method}:${kwargs.domain[0][2]}`);
|
||||
});
|
||||
|
||||
getService("name").addDisplayNames("dev", { 1: "JUM" });
|
||||
const displayNames = await getService("name").loadDisplayNames("dev", [1, 2]);
|
||||
expect(displayNames).toEqual({ 1: "JUM", 2: "Pierre" });
|
||||
expect.verifySteps(["dev:web_search_read:2"]);
|
||||
});
|
||||
|
||||
test("loadDisplayNames in batch", async () => {
|
||||
await makeMockEnv();
|
||||
onRpc(({ model, method, kwargs }) => {
|
||||
expect.step(`${model}:${method}:${kwargs.domain[0][2]}`);
|
||||
});
|
||||
|
||||
const loadPromise1 = getService("name").loadDisplayNames("dev", [1]);
|
||||
expect.verifySteps([]);
|
||||
const loadPromise2 = getService("name").loadDisplayNames("dev", [2]);
|
||||
expect.verifySteps([]);
|
||||
|
||||
const [displayNames1, displayNames2] = await Promise.all([loadPromise1, loadPromise2]);
|
||||
expect(displayNames1).toEqual({ 1: "Julien" });
|
||||
expect(displayNames2).toEqual({ 2: "Pierre" });
|
||||
expect.verifySteps(["dev:web_search_read:1,2"]);
|
||||
});
|
||||
|
||||
test("loadDisplayNames on different models", async () => {
|
||||
await makeMockEnv();
|
||||
onRpc(({ model, method, kwargs }) => {
|
||||
expect.step(`${model}:${method}:${kwargs.domain[0][2]}`);
|
||||
});
|
||||
|
||||
const loadPromise1 = getService("name").loadDisplayNames("dev", [1]);
|
||||
expect.verifySteps([]);
|
||||
const loadPromise2 = getService("name").loadDisplayNames("po", [1]);
|
||||
expect.verifySteps([]);
|
||||
|
||||
const [displayNames1, displayNames2] = await Promise.all([loadPromise1, loadPromise2]);
|
||||
expect(displayNames1).toEqual({ 1: "Julien" });
|
||||
expect(displayNames2).toEqual({ 1: "Damien" });
|
||||
|
||||
expect.verifySteps(["dev:web_search_read:1", "po:web_search_read:1"]);
|
||||
});
|
||||
|
||||
test("invalid id", async () => {
|
||||
await makeMockEnv();
|
||||
try {
|
||||
await getService("name").loadDisplayNames("dev", ["a"]);
|
||||
} catch (error) {
|
||||
expect(error.message).toBe("Invalid ID: a");
|
||||
}
|
||||
});
|
||||
|
||||
test("inaccessible or missing id", async () => {
|
||||
await makeMockEnv();
|
||||
onRpc(({ model, method, kwargs }) => {
|
||||
expect.step(`${model}:${method}:${kwargs.domain[0][2]}`);
|
||||
});
|
||||
|
||||
const displayNames = await getService("name").loadDisplayNames("dev", [3]);
|
||||
expect(displayNames).toEqual({ 3: ERROR_INACCESSIBLE_OR_MISSING });
|
||||
expect.verifySteps(["dev:web_search_read:3"]);
|
||||
});
|
||||
|
||||
test("batch + inaccessible/missing", async () => {
|
||||
await makeMockEnv();
|
||||
onRpc(({ model, method, kwargs }) => {
|
||||
expect.step(`${model}:${method}:${kwargs.domain[0][2]}`);
|
||||
});
|
||||
|
||||
const loadPromise1 = getService("name").loadDisplayNames("dev", [1, 3]);
|
||||
expect.verifySteps([]);
|
||||
const loadPromise2 = getService("name").loadDisplayNames("dev", [2, 4]);
|
||||
expect.verifySteps([]);
|
||||
|
||||
const [displayNames1, displayNames2] = await Promise.all([loadPromise1, loadPromise2]);
|
||||
expect(displayNames1).toEqual({ 1: "Julien", 3: ERROR_INACCESSIBLE_OR_MISSING });
|
||||
expect(displayNames2).toEqual({ 2: "Pierre", 4: ERROR_INACCESSIBLE_OR_MISSING });
|
||||
expect.verifySteps(["dev:web_search_read:1,3,2,4"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
import { Component, xml } from "@odoo/owl";
|
||||
import { useNavigation } from "@web/core/navigation/navigation";
|
||||
import { useAutofocus } from "@web/core/utils/hooks";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { hover, press } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class BasicHookParent extends Component {
|
||||
static props = [];
|
||||
static template = xml`
|
||||
<button class="outside" t-ref="outsideRef">outside target</button>
|
||||
<div class="container" t-ref="containerRef">
|
||||
<button class="o-navigable one" t-on-click="() => this.onClick(1)">target one</button>
|
||||
<div class="o-navigable two" tabindex="0" t-on-click="() => this.onClick(2)">target two</div>
|
||||
<input class="o-navigable three" t-on-click="() => this.onClick(3)"/><br/>
|
||||
<button class="no-nav-class">skipped</button><br/>
|
||||
<a class="o-navigable four" tabindex="0" t-on-click="() => this.onClick(4)">target four</a>
|
||||
<div class="o-navigable five">
|
||||
<button t-on-click="() => this.onClick(5)">target five</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
setup() {
|
||||
useAutofocus({ refName: "outsideRef" });
|
||||
this.navigation = useNavigation("containerRef", this.navOptions);
|
||||
}
|
||||
|
||||
navOptions = {};
|
||||
onClick(id) {}
|
||||
}
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("default navigation", async () => {
|
||||
async function navigate(hotkey, focused) {
|
||||
await press(hotkey);
|
||||
await animationFrame();
|
||||
|
||||
expect(focused).toBeFocused();
|
||||
expect(focused).toHaveClass("focus");
|
||||
}
|
||||
|
||||
class Parent extends BasicHookParent {
|
||||
onClick(id) {
|
||||
expect.step(id);
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
expect(".one").toBeFocused();
|
||||
|
||||
await navigate("arrowdown", ".two");
|
||||
await navigate("arrowdown", ".three");
|
||||
await navigate("arrowdown", ".four");
|
||||
await navigate("arrowdown", ".five button");
|
||||
await navigate("arrowdown", ".one");
|
||||
|
||||
await navigate("arrowup", ".five button");
|
||||
await navigate("arrowup", ".four");
|
||||
|
||||
await navigate("end", ".five button");
|
||||
await navigate("home", ".one");
|
||||
|
||||
await navigate("tab", ".two");
|
||||
await navigate("shift+tab", ".one");
|
||||
|
||||
await navigate("arrowleft", ".one");
|
||||
await navigate("arrowright", ".one");
|
||||
await navigate("space", ".one");
|
||||
await navigate("escape", ".one");
|
||||
|
||||
await press("enter");
|
||||
await animationFrame();
|
||||
expect.verifySteps([1]);
|
||||
|
||||
await navigate("arrowdown", ".two");
|
||||
await press("enter");
|
||||
await animationFrame();
|
||||
expect.verifySteps([2]);
|
||||
});
|
||||
|
||||
test("hotkey override options", async () => {
|
||||
class Parent extends BasicHookParent {
|
||||
navOptions = {
|
||||
hotkeys: {
|
||||
arrowleft: (index, items) => {
|
||||
expect.step(index);
|
||||
items[(index + 2) % items.length].focus();
|
||||
},
|
||||
escape: (index, items) => {
|
||||
expect.step("escape");
|
||||
items[0].focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
onClick(id) {
|
||||
expect.step(id);
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
expect(".one").toBeFocused();
|
||||
|
||||
await press("arrowleft");
|
||||
await animationFrame();
|
||||
expect(".three").toBeFocused();
|
||||
expect.verifySteps([0]);
|
||||
|
||||
await press("escape");
|
||||
await animationFrame();
|
||||
expect(".one").toBeFocused();
|
||||
expect.verifySteps(["escape"]);
|
||||
});
|
||||
|
||||
test("navigation with virtual focus", async () => {
|
||||
async function navigate(hotkey, expected) {
|
||||
await press(hotkey);
|
||||
await animationFrame();
|
||||
// Focus is kept on button outside container
|
||||
expect(".outside").toBeFocused();
|
||||
// Virtually focused element has "focus" class
|
||||
expect(expected).toHaveClass("focus");
|
||||
}
|
||||
|
||||
class Parent extends BasicHookParent {
|
||||
navOptions = {
|
||||
virtualFocus: true,
|
||||
};
|
||||
|
||||
onClick(id) {
|
||||
expect.step(id);
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
expect(".one").toHaveClass("focus");
|
||||
await navigate("arrowdown", ".two");
|
||||
await navigate("arrowdown", ".three");
|
||||
await navigate("arrowdown", ".four");
|
||||
await navigate("arrowdown", ".five button");
|
||||
await navigate("arrowdown", ".one");
|
||||
|
||||
await navigate("arrowup", ".five button");
|
||||
await navigate("arrowup", ".four");
|
||||
|
||||
await navigate("end", ".five button");
|
||||
await navigate("home", ".one");
|
||||
|
||||
await navigate("tab", ".two");
|
||||
await navigate("shift+tab", ".one");
|
||||
|
||||
await press("enter");
|
||||
await animationFrame();
|
||||
expect.verifySteps([1]);
|
||||
|
||||
await navigate("arrowdown", ".two");
|
||||
await press("enter");
|
||||
await animationFrame();
|
||||
expect.verifySteps([2]);
|
||||
});
|
||||
|
||||
test("hovering an item makes it active but doesn't focus", async () => {
|
||||
await mountWithCleanup(BasicHookParent);
|
||||
|
||||
await press("arrowdown");
|
||||
|
||||
expect(".two").toBeFocused();
|
||||
expect(".two").toHaveClass("focus");
|
||||
|
||||
hover(".three");
|
||||
await animationFrame();
|
||||
|
||||
expect(".two").toBeFocused();
|
||||
expect(".two").not.toHaveClass("focus");
|
||||
|
||||
expect(".three").not.toBeFocused();
|
||||
expect(".three").toHaveClass("focus");
|
||||
|
||||
press("arrowdown");
|
||||
await animationFrame();
|
||||
expect(".four").toBeFocused();
|
||||
expect(".four").toHaveClass("focus");
|
||||
});
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { after, describe, expect, test } from "@odoo/hoot";
|
||||
import { Deferred, mockFetch } from "@odoo/hoot-mock";
|
||||
import { patchTranslations } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { download } from "@web/core/network/download";
|
||||
import { ConnectionLostError, RPCError } from "@web/core/network/rpc";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("handles connection error when behind a server", async () => {
|
||||
mockFetch(() => new Response("", { status: 502 }));
|
||||
|
||||
const error = new ConnectionLostError("/some_url");
|
||||
await expect(download({ data: {}, url: "/some_url" })).rejects.toThrow(error);
|
||||
});
|
||||
|
||||
test("handles connection error when network unavailable", async () => {
|
||||
mockFetch(() => Promise.reject());
|
||||
|
||||
const error = new ConnectionLostError("/some_url");
|
||||
await expect(download({ data: {}, url: "/some_url" })).rejects.toThrow(error);
|
||||
});
|
||||
|
||||
test("handles business error from server", async () => {
|
||||
const serverError = {
|
||||
code: 200,
|
||||
data: {
|
||||
name: "odoo.exceptions.RedirectWarning",
|
||||
arguments: ["Business Error Message", "someArg"],
|
||||
message: "Business Error Message",
|
||||
},
|
||||
message: "Odoo Server Error",
|
||||
};
|
||||
|
||||
mockFetch(() => new Blob([JSON.stringify(serverError)], { type: "text/html" }));
|
||||
|
||||
let error = null;
|
||||
try {
|
||||
await download({
|
||||
data: {},
|
||||
url: "/some_url",
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeInstanceOf(RPCError);
|
||||
expect(error.data).toEqual(serverError.data);
|
||||
});
|
||||
|
||||
test("handles arbitrary error", async () => {
|
||||
const serverError = /* xml */ `<html><body><div>HTML error message</div></body></html>`;
|
||||
|
||||
mockFetch(() => new Blob([JSON.stringify(serverError)], { type: "text/html" }));
|
||||
|
||||
let error = null;
|
||||
try {
|
||||
await download({
|
||||
data: {},
|
||||
url: "/some_url",
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(RPCError);
|
||||
expect(error.message).toBe("Arbitrary Uncaught Python Exception");
|
||||
expect(error.data.debug.trim()).toBe("200\nHTML error message");
|
||||
});
|
||||
|
||||
test("handles success download", async () => {
|
||||
patchTranslations();
|
||||
// This test relies on a implementation detail of the lowest layer of download
|
||||
// That is, a link will be created with the download attribute
|
||||
|
||||
mockFetch((_, { body }) => {
|
||||
expect(body).toBeInstanceOf(FormData);
|
||||
expect(body.get("someKey")).toBe("someValue");
|
||||
expect(body.has("token")).toBe(true);
|
||||
expect(body.has("csrf_token")).toBe(true);
|
||||
expect.step("fetching file");
|
||||
|
||||
return new Blob(["some plain text file"], { type: "text/plain" });
|
||||
});
|
||||
|
||||
const deferred = new Deferred();
|
||||
|
||||
// This part asserts the implementation detail in question
|
||||
const downloadOnClick = (ev) => {
|
||||
const target = ev.target;
|
||||
if (target.tagName === "A" && "download" in target.attributes) {
|
||||
ev.preventDefault();
|
||||
|
||||
expect(target.href).toMatch(/^blob:/);
|
||||
expect.step("file downloaded");
|
||||
document.removeEventListener("click", downloadOnClick);
|
||||
deferred.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", downloadOnClick);
|
||||
after(() => document.removeEventListener("click", downloadOnClick));
|
||||
|
||||
expect("a[download]").toHaveCount(0); // link will be added by download
|
||||
download({ data: { someKey: "someValue" }, url: "/some_url" });
|
||||
await deferred;
|
||||
expect.verifySteps(["fetching file", "file downloaded"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { mockFetch } from "@odoo/hoot-mock";
|
||||
|
||||
import { get, post } from "@web/core/network/http_service";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("method is correctly set", async () => {
|
||||
mockFetch((_, { method }) => expect.step(method));
|
||||
|
||||
await get("/call_get");
|
||||
expect.verifySteps(["GET"]);
|
||||
|
||||
await post("/call_post");
|
||||
expect.verifySteps(["POST"]);
|
||||
});
|
||||
|
||||
test("check status 502", async () => {
|
||||
mockFetch(() => new Response("{}", { status: 502 }));
|
||||
|
||||
await expect(get("/custom_route")).rejects.toThrow(/Failed to fetch/);
|
||||
});
|
||||
|
||||
test("FormData is built by post", async () => {
|
||||
mockFetch((_, { body }) => {
|
||||
expect(body).toBeInstanceOf(FormData);
|
||||
expect(body.get("s")).toBe("1");
|
||||
expect(body.get("a")).toBe("1");
|
||||
expect(body.getAll("a")).toEqual(["1", "2", "3"]);
|
||||
});
|
||||
|
||||
await post("call_post", { s: 1, a: [1, 2, 3] });
|
||||
});
|
||||
|
||||
test("FormData is given to post", async () => {
|
||||
const formData = new FormData();
|
||||
mockFetch((_, { body }) => expect(body).toBe(formData));
|
||||
|
||||
await post("/call_post", formData);
|
||||
});
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import { after, describe, expect, test } from "@odoo/hoot";
|
||||
import { on } from "@odoo/hoot-dom";
|
||||
import { mockFetch } from "@odoo/hoot-mock";
|
||||
|
||||
import {
|
||||
ConnectionAbortedError,
|
||||
ConnectionLostError,
|
||||
RPCError,
|
||||
rpc,
|
||||
rpcBus,
|
||||
} from "@web/core/network/rpc";
|
||||
|
||||
const onRpcRequest = (listener) => after(on(rpcBus, "RPC:REQUEST", listener));
|
||||
const onRpcResponse = (listener) => after(on(rpcBus, "RPC:RESPONSE", listener));
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("can perform a simple rpc", async () => {
|
||||
mockFetch((_, { body }) => {
|
||||
const bodyObject = JSON.parse(body);
|
||||
expect(bodyObject.jsonrpc).toBe("2.0");
|
||||
expect(bodyObject.method).toBe("call");
|
||||
expect(bodyObject.id).toBeOfType("integer");
|
||||
return { result: { action_id: 123 } };
|
||||
});
|
||||
|
||||
expect(await rpc("/test/")).toEqual({ action_id: 123 });
|
||||
});
|
||||
|
||||
test("trigger an error when response has 'error' key", async () => {
|
||||
mockFetch(() => ({
|
||||
error: {
|
||||
message: "message",
|
||||
code: 12,
|
||||
data: {
|
||||
debug: "data_debug",
|
||||
message: "data_message",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const error = new RPCError("message");
|
||||
await expect(rpc("/test/")).rejects.toThrow(error);
|
||||
});
|
||||
|
||||
test("rpc with simple routes", async () => {
|
||||
mockFetch((route, { body }) => ({
|
||||
result: { route, params: JSON.parse(body).params },
|
||||
}));
|
||||
|
||||
expect(await rpc("/my/route")).toEqual({ route: "/my/route", params: {} });
|
||||
expect(await rpc("/my/route", { hey: "there", model: "test" })).toEqual({
|
||||
route: "/my/route",
|
||||
params: { hey: "there", model: "test" },
|
||||
});
|
||||
});
|
||||
|
||||
test("check trigger RPC:REQUEST and RPC:RESPONSE for a simple rpc", async () => {
|
||||
mockFetch(() => ({ result: {} }));
|
||||
|
||||
const rpcIdsRequest = [];
|
||||
const rpcIdsResponse = [];
|
||||
|
||||
onRpcRequest(({ detail }) => {
|
||||
rpcIdsRequest.push(detail.data.id);
|
||||
const silent = detail.settings.silent ? "(silent)" : "";
|
||||
expect.step(`RPC:REQUEST${silent}`);
|
||||
});
|
||||
onRpcResponse(({ detail }) => {
|
||||
rpcIdsResponse.push(detail.data.id);
|
||||
const silent = detail.settings.silent ? "(silent)" : "";
|
||||
const success = "result" in detail ? "(ok)" : "";
|
||||
const fail = "error" in detail ? "(ko)" : "";
|
||||
expect.step(`RPC:RESPONSE${silent}${success}${fail}`);
|
||||
});
|
||||
|
||||
await rpc("/test/");
|
||||
expect(rpcIdsRequest.toString()).toBe(rpcIdsResponse.toString());
|
||||
expect.verifySteps(["RPC:REQUEST", "RPC:RESPONSE(ok)"]);
|
||||
|
||||
await rpc("/test/", {}, { silent: true });
|
||||
expect(rpcIdsRequest.toString()).toBe(rpcIdsResponse.toString());
|
||||
expect.verifySteps(["RPC:REQUEST(silent)", "RPC:RESPONSE(silent)(ok)"]);
|
||||
});
|
||||
|
||||
test("check trigger RPC:REQUEST and RPC:RESPONSE for a rpc with an error", async () => {
|
||||
mockFetch(() => ({
|
||||
error: {
|
||||
message: "message",
|
||||
code: 12,
|
||||
data: {
|
||||
debug: "data_debug",
|
||||
message: "data_message",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const rpcIdsRequest = [];
|
||||
const rpcIdsResponse = [];
|
||||
|
||||
onRpcRequest(({ detail }) => {
|
||||
rpcIdsRequest.push(detail.data.id);
|
||||
const silent = detail.settings.silent ? "(silent)" : "";
|
||||
expect.step(`RPC:REQUEST${silent}`);
|
||||
});
|
||||
onRpcResponse(({ detail }) => {
|
||||
rpcIdsResponse.push(detail.data.id);
|
||||
const silent = detail.settings.silent ? "(silent)" : "";
|
||||
const success = "result" in detail ? "(ok)" : "";
|
||||
const fail = "error" in detail ? "(ko)" : "";
|
||||
expect.step(`RPC:RESPONSE${silent}${success}${fail}`);
|
||||
});
|
||||
|
||||
const error = new RPCError("message");
|
||||
await expect(rpc("/test/")).rejects.toThrow(error);
|
||||
expect.verifySteps(["RPC:REQUEST", "RPC:RESPONSE(ko)"]);
|
||||
});
|
||||
|
||||
test("check connection aborted", async () => {
|
||||
mockFetch(() => new Promise(() => {}));
|
||||
onRpcRequest(() => expect.step("RPC:REQUEST"));
|
||||
onRpcResponse(() => expect.step("RPC:RESPONSE"));
|
||||
|
||||
const connection = rpc();
|
||||
connection.abort();
|
||||
const error = new ConnectionAbortedError();
|
||||
await expect(connection).rejects.toThrow(error);
|
||||
expect.verifySteps(["RPC:REQUEST", "RPC:RESPONSE"]);
|
||||
});
|
||||
|
||||
test("trigger a ConnectionLostError when response isn't json parsable", async () => {
|
||||
mockFetch(() => new Response("<h...", { status: 500 }));
|
||||
|
||||
const error = new ConnectionLostError("/test/");
|
||||
await expect(rpc("/test/")).rejects.toThrow(error);
|
||||
});
|
||||
|
||||
test("rpc can send additional headers", async () => {
|
||||
mockFetch((url, settings) => {
|
||||
expect(settings.headers).toEqual({
|
||||
"Content-Type": "application/json",
|
||||
Hello: "World",
|
||||
});
|
||||
return { result: true };
|
||||
});
|
||||
await rpc("/test/", null, { headers: { Hello: "World" } });
|
||||
});
|
||||
356
odoo-bringout-oca-ocb-web/web/static/tests/core/notebook.test.js
Normal file
356
odoo-bringout-oca-ocb-web/web/static/tests/core/notebook.test.js
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { click, queryFirst } from "@odoo/hoot-dom";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Notebook } from "@web/core/notebook/notebook";
|
||||
|
||||
test("not rendered if empty slots", async () => {
|
||||
await mountWithCleanup(Notebook);
|
||||
expect("div.o_notebook").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("notebook with multiple pages given as slots", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<Notebook>
|
||||
<t t-set-slot="page_about" title="'About'" isVisible="true">
|
||||
<h3>About the bird</h3>
|
||||
<p>Owls are birds from the order Strigiformes which includes over
|
||||
200 species of mostly solitary and nocturnal birds of prey typified by an upright stance, ...</p>
|
||||
</t>
|
||||
<t t-set-slot="page_hunting" title="'Owl Activities'" isVisible="true">
|
||||
<h3>Their favorite activity: hunting</h3>
|
||||
<p>Owls are called raptors, or birds of prey, which means they use sharp talons and curved bills to hunt, kill, and eat other animals.</p>
|
||||
</t>
|
||||
<t t-set-slot="page_secret" title="'Secret about OWLs'" isVisible="false">
|
||||
<p>TODO find a great secret about OWLs.</p>
|
||||
</t>
|
||||
</Notebook>`;
|
||||
static components = { Notebook };
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect("div.o_notebook").toHaveCount(1);
|
||||
expect(".o_notebook").toHaveClass("horizontal", {
|
||||
message: "default orientation is set as horizontal",
|
||||
});
|
||||
expect(".nav").toHaveClass("flex-row", {
|
||||
message: "navigation container uses the right class to display as horizontal tabs",
|
||||
});
|
||||
expect(".o_notebook_headers a.nav-link").toHaveCount(2, {
|
||||
message: "navigation link is present for each visible page",
|
||||
});
|
||||
expect(".o_notebook_headers .nav-item:first-child a").toHaveClass("active", {
|
||||
message: "first page is selected by default",
|
||||
});
|
||||
expect(".active h3").toHaveText("About the bird", {
|
||||
message: "first page content is displayed by the notebook",
|
||||
});
|
||||
|
||||
await click(".o_notebook_headers .nav-item:nth-child(2) a");
|
||||
await animationFrame();
|
||||
expect(".o_notebook_headers .nav-item:nth-child(2) a").toHaveClass("active", {
|
||||
message: "second page is now selected",
|
||||
});
|
||||
expect(".active h3").toHaveText("Their favorite activity: hunting", {
|
||||
message: "second page content is displayed by the notebook",
|
||||
});
|
||||
});
|
||||
|
||||
test("notebook with defaultPage props", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<Notebook defaultPage="'page_hunting'">
|
||||
<t t-set-slot="page_about" title="'About'" isVisible="true">
|
||||
<h3>About the bird</h3>
|
||||
<p>Owls are birds from the order Strigiformes which includes over
|
||||
200 species of mostly solitary and nocturnal birds of prey typified by an upright stance, ...</p>
|
||||
</t>
|
||||
<t t-set-slot="page_hunting" title="'Owl Activities'" isVisible="true">
|
||||
<h3>Their favorite activity: hunting</h3>
|
||||
<p>Owls are called raptors, or birds of prey, which means they use sharp talons and curved bills to hunt, kill, and eat other animals.</p>
|
||||
</t>
|
||||
<t t-set-slot="page_secret" title="'Secret about OWLs'" isVisible="false">
|
||||
<p>TODO find a great secret about OWLs.</p>
|
||||
</t>
|
||||
</Notebook>`;
|
||||
static components = { Notebook };
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect("div.o_notebook").toHaveCount(1);
|
||||
expect(".o_notebook_headers .nav-item:nth-child(2) a").toHaveClass("active", {
|
||||
message: "second page is selected by default",
|
||||
});
|
||||
expect(".active h3").toHaveText("Their favorite activity: hunting", {
|
||||
message: "second page content is displayed by the notebook",
|
||||
});
|
||||
});
|
||||
|
||||
test("notebook with defaultPage set on invisible page", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<Notebook defaultPage="'page_secret'">
|
||||
<t t-set-slot="page_about" title="'About'" isVisible="true">
|
||||
<h3>About the bird</h3>
|
||||
<p>Owls are birds from the order Strigiformes which includes over
|
||||
200 species of mostly solitary and nocturnal birds of prey typified by an upright stance, ...</p>
|
||||
</t>
|
||||
<t t-set-slot="page_hunting" title="'Owl Activities'" isVisible="true">
|
||||
<h3>Their favorite activity: hunting</h3>
|
||||
<p>Owls are called raptors, or birds of prey, which means they use sharp talons and curved bills to hunt, kill, and eat other animals.</p>
|
||||
</t>
|
||||
<t t-set-slot="page_secret" title="'Secret about OWLs'" isVisible="false">
|
||||
<h3>Oooops</h3>
|
||||
<p>TODO find a great secret to reveal about OWLs.</p>
|
||||
</t>
|
||||
</Notebook>`;
|
||||
static components = { Notebook };
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_notebook_headers .nav-item a.active").toHaveText("About", {
|
||||
message: "The first page is selected",
|
||||
});
|
||||
});
|
||||
|
||||
test("notebook set vertically", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<Notebook orientation="'vertical'">
|
||||
<t t-set-slot="page_about" title="'About'" isVisible="true">
|
||||
<h3>About the bird</h3>
|
||||
<p>Owls are birds from the order Strigiformes which includes over
|
||||
200 species of mostly solitary and nocturnal birds of prey typified by an upright stance, ...</p>
|
||||
</t>
|
||||
<t t-set-slot="page_hunting" title="'Owl Activities'" isVisible="true">
|
||||
<h3>Their favorite activity: hunting</h3>
|
||||
<p>Owls are called raptors, or birds of prey, which means they use sharp talons and curved bills to hunt, kill, and eat other animals.</p>
|
||||
</t>
|
||||
</Notebook>`;
|
||||
static components = { Notebook };
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect("div.o_notebook").toHaveCount(1);
|
||||
expect(".o_notebook").toHaveClass("vertical", {
|
||||
message: "orientation is set as vertical",
|
||||
});
|
||||
expect(".nav").toHaveClass("flex-column", {
|
||||
message: "navigation container uses the right class to display as vertical buttons",
|
||||
});
|
||||
});
|
||||
|
||||
test("notebook pages rendered by a template component", async () => {
|
||||
class NotebookPageRenderer extends Component {
|
||||
static template = xml`
|
||||
<h3 t-esc="props.heading"></h3>
|
||||
<p t-esc="props.text" />
|
||||
`;
|
||||
static props = {
|
||||
heading: String,
|
||||
text: String,
|
||||
};
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<Notebook defaultPage="'page_three'" pages="pages">
|
||||
<t t-set-slot="page_one" title="'Page 1'" isVisible="true">
|
||||
<h3>Page 1</h3>
|
||||
<p>First page set directly as a slot</p>
|
||||
</t>
|
||||
<t t-set-slot="page_four" title="'Page 4'" isVisible="true">
|
||||
<h3>Page 4</h3>
|
||||
</t>
|
||||
</Notebook>`;
|
||||
static components = { Notebook };
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.pages = [
|
||||
{
|
||||
Component: NotebookPageRenderer,
|
||||
index: 1,
|
||||
title: "Page 2",
|
||||
props: {
|
||||
heading: "Page 2",
|
||||
text: "Second page rendered by a template component",
|
||||
},
|
||||
},
|
||||
{
|
||||
Component: NotebookPageRenderer,
|
||||
id: "page_three", // required to be set as default page
|
||||
index: 2,
|
||||
title: "Page 3",
|
||||
props: {
|
||||
heading: "Page 3",
|
||||
text: "Third page rendered by a template component",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect("div.o_notebook").toHaveCount(1);
|
||||
expect(".o_notebook_headers .nav-item:nth-child(3) a").toHaveClass("active", {
|
||||
message: "third page is selected by default",
|
||||
});
|
||||
|
||||
await click(".o_notebook_headers .nav-item:nth-child(2) a");
|
||||
await animationFrame();
|
||||
expect(".o_notebook_content p").toHaveText("Second page rendered by a template component", {
|
||||
message: "displayed content corresponds to the current page",
|
||||
});
|
||||
});
|
||||
|
||||
test("each page is different", async () => {
|
||||
class Page extends Component {
|
||||
static template = xml`<h3>Coucou</h3>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<Notebook pages="pages"/>`;
|
||||
static components = { Notebook };
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.pages = [
|
||||
{
|
||||
Component: Page,
|
||||
index: 1,
|
||||
title: "Page 1",
|
||||
},
|
||||
{
|
||||
Component: Page,
|
||||
index: 2,
|
||||
title: "Page 2",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
const firstPage = queryFirst("h3");
|
||||
expect(firstPage).toBeInstanceOf(HTMLElement);
|
||||
|
||||
await click(".o_notebook_headers .nav-item:nth-child(2) a");
|
||||
await animationFrame();
|
||||
const secondPage = queryFirst("h3");
|
||||
expect(secondPage).toBeInstanceOf(HTMLElement);
|
||||
expect(firstPage).not.toBe(secondPage);
|
||||
});
|
||||
|
||||
test("defaultPage recomputed when isVisible is dynamic", async () => {
|
||||
let defaultPageVisible = false;
|
||||
class Parent extends Component {
|
||||
static components = { Notebook };
|
||||
static template = xml`
|
||||
<Notebook defaultPage="'3'">
|
||||
<t t-set-slot="1" title="'page1'" isVisible="true">
|
||||
<div class="page1" />
|
||||
</t>
|
||||
<t t-set-slot="2" title="'page2'" isVisible="true">
|
||||
<div class="page2" />
|
||||
</t>
|
||||
<t t-set-slot="3" title="'page3'" isVisible="defaultPageVisible">
|
||||
<div class="page3" />
|
||||
</t>
|
||||
</Notebook>`;
|
||||
static props = ["*"];
|
||||
get defaultPageVisible() {
|
||||
return defaultPageVisible;
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mountWithCleanup(Parent);
|
||||
expect(".page1").toHaveCount(1);
|
||||
expect(".nav-link.active").toHaveText("page1");
|
||||
defaultPageVisible = true;
|
||||
parent.render(true);
|
||||
|
||||
await animationFrame();
|
||||
expect(".page3").toHaveCount(1);
|
||||
expect(".nav-link.active").toHaveText("page3");
|
||||
|
||||
await click(".o_notebook_headers .nav-item:nth-child(2) a");
|
||||
await animationFrame();
|
||||
expect(".page2").toHaveCount(1);
|
||||
expect(".nav-link.active").toHaveText("page2");
|
||||
|
||||
parent.render(true);
|
||||
await animationFrame();
|
||||
expect(".page2").toHaveCount(1);
|
||||
expect(".nav-link.active").toHaveText("page2");
|
||||
});
|
||||
|
||||
test("disabled pages are greyed out and can't be toggled", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { Notebook };
|
||||
static 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>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".page1").toHaveCount(1);
|
||||
expect(".nav-item:nth-child(2)").toHaveClass("disabled", {
|
||||
message: "tab of the disabled page is greyed out",
|
||||
});
|
||||
|
||||
await click(".nav-item:nth-child(2) .nav-link");
|
||||
await animationFrame();
|
||||
expect(".page1").toHaveCount(1, {
|
||||
message: "the same page is still displayed",
|
||||
});
|
||||
|
||||
await click(".nav-item:nth-child(3) .nav-link");
|
||||
await animationFrame();
|
||||
expect(".page3").toHaveCount(1, {
|
||||
message: "the third page is now displayed",
|
||||
});
|
||||
});
|
||||
|
||||
test("icons can be given for each page tab", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { Notebook };
|
||||
static 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>`;
|
||||
static props = ["*"];
|
||||
get icons() {
|
||||
return {
|
||||
1: "fa-trash",
|
||||
3: "fa-pencil",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".nav-item:nth-child(1) i").toHaveClass("fa-trash");
|
||||
expect(".nav-item:nth-child(1)").toHaveText("page1");
|
||||
expect(".nav-item:nth-child(2) i").toHaveCount(0);
|
||||
expect(".nav-item:nth-child(2)").toHaveText("page2");
|
||||
expect(".nav-item:nth-child(3) i").toHaveClass("fa-pencil");
|
||||
expect(".nav-item:nth-child(3)").toHaveText("page3");
|
||||
});
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, hover, leave } from "@odoo/hoot-dom";
|
||||
import { advanceTime, animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import { markup } from "@odoo/owl";
|
||||
import { getService, makeMockEnv, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
test("can display a basic notification", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
getService("notification").add("I'm a basic notification");
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
expect(".o_notification_content").toHaveText("I'm a basic notification");
|
||||
expect(".o_notification_bar").toHaveClass("bg-warning");
|
||||
});
|
||||
|
||||
test("can display a notification with a className", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
getService("notification").add("I'm a basic notification", { className: "abc" });
|
||||
await animationFrame();
|
||||
expect(".o_notification.abc").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("title and message are escaped by default", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
getService("notification").add("<i>Some message</i>", { title: "<b>Some title</b>" });
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
expect(".o_notification_title").toHaveText("<b>Some title</b>");
|
||||
expect(".o_notification_content").toHaveText("<i>Some message</i>");
|
||||
});
|
||||
|
||||
test("can display a notification with markup content", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
getService("notification").add(markup("<b>I'm a <i>markup</i> notification</b>"));
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
expect(".o_notification_content").toHaveInnerHTML("<b>I'm a <i>markup</i> notification</b>");
|
||||
});
|
||||
|
||||
test("can display a notification of type danger", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
getService("notification").add("I'm a danger notification", { type: "danger" });
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
expect(".o_notification_content").toHaveText("I'm a danger notification");
|
||||
expect(".o_notification_bar").toHaveClass("bg-danger");
|
||||
});
|
||||
|
||||
test("can display a danger notification with a title", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
getService("notification").add("I'm a danger notification", {
|
||||
title: "Some title",
|
||||
type: "danger",
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
expect(".o_notification_title").toHaveText("Some title");
|
||||
expect(".o_notification_content").toHaveText("I'm a danger notification");
|
||||
expect(".o_notification_bar").toHaveClass("bg-danger");
|
||||
});
|
||||
|
||||
test("can display a notification with a button", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
getService("notification").add("I'm a notification with button", {
|
||||
buttons: [
|
||||
{
|
||||
name: "I'm a button",
|
||||
primary: true,
|
||||
onClick: () => {
|
||||
expect.step("Button clicked");
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
expect(".o_notification_buttons").toHaveText("I'm a button");
|
||||
await click(".o_notification .btn-primary");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["Button clicked"]);
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("can display a notification with a callback when closed", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
getService("notification").add("I'm a sticky notification", {
|
||||
sticky: true,
|
||||
onClose: () => {
|
||||
expect.step("Notification closed");
|
||||
},
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
await click(".o_notification .o_notification_close");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["Notification closed"]);
|
||||
expect(".o_notification").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("notifications aren't sticky by default", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
getService("notification").add("I'm a notification");
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
// Wait for the notification to close
|
||||
await runAllTimers();
|
||||
expect(".o_notification").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("can display a sticky notification", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
getService("notification").add("I'm a sticky notification", { sticky: true });
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
await advanceTime(5000);
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("can close sticky notification", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
const closeNotif = getService("notification").add("I'm a sticky notification", {
|
||||
sticky: true,
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
|
||||
// close programmatically
|
||||
closeNotif();
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(0);
|
||||
|
||||
getService("notification").add("I'm a sticky notification", { sticky: true });
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
|
||||
// close by clicking on the close icon
|
||||
await click(".o_notification .o_notification_close");
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(0);
|
||||
});
|
||||
|
||||
// The timeout have to be done by the one that uses the notification service
|
||||
test.skip("can close sticky notification with wait", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
const closeNotif = getService("notification").add("I'm a sticky notification", {
|
||||
sticky: true,
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
|
||||
// close programmatically
|
||||
getService("notification").close(closeNotif, 3000);
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
|
||||
// simulate end of timeout
|
||||
await advanceTime(3000);
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("can close a non-sticky notification", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
const closeNotif = getService("notification").add("I'm a sticky notification");
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
|
||||
// close the notification
|
||||
closeNotif();
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(0);
|
||||
|
||||
// simulate end of timeout, which should try to close the notification as well
|
||||
await runAllTimers();
|
||||
expect(".o_notification").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("can refresh the duration of a non-sticky notification", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
getService("notification").add("I'm a first non-sticky notification");
|
||||
getService("notification").add("I'm a second non-sticky notification");
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(2);
|
||||
|
||||
await advanceTime(3000);
|
||||
await hover(".o_notification:first-child");
|
||||
await advanceTime(5000);
|
||||
// Both notifications should be visible as long as mouse is over one of them
|
||||
expect(".o_notification").toHaveCount(2);
|
||||
await leave();
|
||||
await advanceTime(3000);
|
||||
// Both notifications should be refreshed in duration (4000 ms)
|
||||
expect(".o_notification").toHaveCount(2);
|
||||
await advanceTime(2000);
|
||||
expect(".o_notification").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("close a non-sticky notification while another one remains", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
const closeNotif1 = getService("notification").add("I'm a non-sticky notification");
|
||||
const closeNotif2 = getService("notification").add("I'm a sticky notification", {
|
||||
sticky: true,
|
||||
});
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(2);
|
||||
|
||||
// close the non sticky notification
|
||||
closeNotif1();
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
|
||||
// simulate end of timeout, which should try to close notification 1 as well
|
||||
await runAllTimers();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
|
||||
// close the non sticky notification
|
||||
closeNotif2();
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("notification coming when NotificationManager not mounted yet", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
getService("notification").add("I'm a non-sticky notification");
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("notification autocloses after a specified delay", async () => {
|
||||
await makeMockEnv();
|
||||
const { Component: NotificationContainer, props } = registry
|
||||
.category("main_components")
|
||||
.get("NotificationContainer");
|
||||
|
||||
await mountWithCleanup(NotificationContainer, { props, noMainContainer: true });
|
||||
getService("notification").add("custom autoclose delay notification", {
|
||||
autocloseDelay: 1000,
|
||||
});
|
||||
|
||||
await advanceTime(500);
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(1);
|
||||
|
||||
await advanceTime(500);
|
||||
await animationFrame();
|
||||
expect(".o_notification").toHaveCount(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,511 @@
|
|||
import { after, describe, expect, test } from "@odoo/hoot";
|
||||
import { on } from "@odoo/hoot-dom";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { getService, makeMockEnv, mountWithCleanup, onRpc } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { rpcBus } from "@web/core/network/rpc";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("add user context to a simple read request", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params).toMatchObject({
|
||||
args: [[3], ["id", "descr"]],
|
||||
kwargs: {
|
||||
context: {
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "read",
|
||||
model: "res.partner",
|
||||
});
|
||||
return false; // Don't want to call the actual read method
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.read("res.partner", [3], ["id", "descr"]);
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/res.partner/read"]);
|
||||
});
|
||||
|
||||
test("context is combined with user context in read request", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params).toMatchObject({
|
||||
args: [[3], ["id", "descr"]],
|
||||
kwargs: {
|
||||
context: {
|
||||
allowed_company_ids: [1],
|
||||
earth: "isfucked",
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "read",
|
||||
model: "res.partner",
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.read("res.partner", [3], ["id", "descr"], {
|
||||
context: {
|
||||
earth: "isfucked",
|
||||
},
|
||||
});
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/res.partner/read"]);
|
||||
});
|
||||
|
||||
test("basic method call of model", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params).toMatchObject({
|
||||
args: [],
|
||||
kwargs: {
|
||||
context: {
|
||||
a: 1,
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "test",
|
||||
model: "res.partner",
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.call("res.partner", "test", [], { context: { a: 1 } });
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/res.partner/test"]);
|
||||
});
|
||||
|
||||
test("create method: one record", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params).toMatchObject({
|
||||
args: [[{ color: "red" }]],
|
||||
kwargs: {
|
||||
context: {
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "create",
|
||||
model: "res.partner",
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.create("res.partner", [{ color: "red" }]);
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/res.partner/create"]);
|
||||
});
|
||||
|
||||
test("create method: several records", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params).toMatchObject({
|
||||
args: [[{ color: "red" }, { color: "green" }]],
|
||||
kwargs: {
|
||||
context: {
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "create",
|
||||
model: "res.partner",
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.create("res.partner", [{ color: "red" }, { color: "green" }]);
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/res.partner/create"]);
|
||||
});
|
||||
|
||||
test("read method", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params).toMatchObject({
|
||||
args: [
|
||||
[2, 5],
|
||||
["name", "amount"],
|
||||
],
|
||||
kwargs: {
|
||||
load: "none",
|
||||
context: {
|
||||
abc: 3,
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "read",
|
||||
model: "sale.order",
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.read("sale.order", [2, 5], ["name", "amount"], {
|
||||
load: "none",
|
||||
context: { abc: 3 },
|
||||
});
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/sale.order/read"]);
|
||||
});
|
||||
|
||||
test("unlink method", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params).toMatchObject({
|
||||
args: [[43]],
|
||||
kwargs: {
|
||||
context: {
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "unlink",
|
||||
model: "res.partner",
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.unlink("res.partner", [43]);
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/res.partner/unlink"]);
|
||||
});
|
||||
|
||||
test("write method", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params).toMatchObject({
|
||||
args: [[43, 14], { active: false }],
|
||||
kwargs: {
|
||||
context: {
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "write",
|
||||
model: "res.partner",
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.write("res.partner", [43, 14], { active: false });
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/res.partner/write"]);
|
||||
});
|
||||
|
||||
test("webReadGroup method", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params).toMatchObject({
|
||||
args: [],
|
||||
kwargs: {
|
||||
domain: [["user_id", "=", 2]],
|
||||
fields: ["amount_total:sum"],
|
||||
groupby: ["date_order"],
|
||||
context: {
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
uid: 7,
|
||||
tz: "taht",
|
||||
},
|
||||
offset: 1,
|
||||
},
|
||||
method: "web_read_group",
|
||||
model: "sale.order",
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.webReadGroup(
|
||||
"sale.order",
|
||||
[["user_id", "=", 2]],
|
||||
["amount_total:sum"],
|
||||
["date_order"],
|
||||
{ offset: 1 }
|
||||
);
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/sale.order/web_read_group"]);
|
||||
});
|
||||
|
||||
test("readGroup method", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params).toMatchObject({
|
||||
args: [],
|
||||
kwargs: {
|
||||
domain: [["user_id", "=", 2]],
|
||||
fields: ["amount_total:sum"],
|
||||
groupby: ["date_order"],
|
||||
context: {
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
uid: 7,
|
||||
tz: "taht",
|
||||
},
|
||||
offset: 1,
|
||||
},
|
||||
method: "read_group",
|
||||
model: "sale.order",
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.readGroup(
|
||||
"sale.order",
|
||||
[["user_id", "=", 2]],
|
||||
["amount_total:sum"],
|
||||
["date_order"],
|
||||
{ offset: 1 }
|
||||
);
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/sale.order/read_group"]);
|
||||
});
|
||||
|
||||
test("test readGroup method removes duplicate values from groupby", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params.kwargs.groupby).toMatchObject(["date_order:month"], {
|
||||
message: "Duplicate values should be removed from groupby",
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.readGroup(
|
||||
"sale.order",
|
||||
[["user_id", "=", 2]],
|
||||
["amount_total:sum"],
|
||||
["date_order:month", "date_order:month"],
|
||||
{ offset: 1 }
|
||||
);
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/sale.order/read_group"]);
|
||||
});
|
||||
|
||||
test("search_read method", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params).toMatchObject({
|
||||
args: [],
|
||||
kwargs: {
|
||||
context: {
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
domain: [["user_id", "=", 2]],
|
||||
fields: ["amount_total"],
|
||||
},
|
||||
method: "search_read",
|
||||
model: "sale.order",
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.searchRead("sale.order", [["user_id", "=", 2]], ["amount_total"]);
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/sale.order/search_read"]);
|
||||
});
|
||||
|
||||
test("search_count method", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params).toMatchObject({
|
||||
args: [[["user_id", "=", 2]]],
|
||||
kwargs: {
|
||||
context: {
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "search_count",
|
||||
model: "sale.order",
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.searchCount("sale.order", [["user_id", "=", 2]]);
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/sale.order/search_count"]);
|
||||
});
|
||||
|
||||
test("webRead method", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params).toMatchObject({
|
||||
args: [[2, 5]],
|
||||
kwargs: {
|
||||
specification: { name: {}, amount: {} },
|
||||
context: {
|
||||
abc: 3,
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
},
|
||||
method: "web_read",
|
||||
model: "sale.order",
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.webRead("sale.order", [2, 5], {
|
||||
specification: { name: {}, amount: {} },
|
||||
context: { abc: 3 },
|
||||
});
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/sale.order/web_read"]);
|
||||
});
|
||||
|
||||
test("webSearchRead method", async () => {
|
||||
onRpc(async (params) => {
|
||||
expect.step(params.route);
|
||||
expect(params).toMatchObject({
|
||||
args: [],
|
||||
kwargs: {
|
||||
context: {
|
||||
allowed_company_ids: [1],
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
domain: [["user_id", "=", 2]],
|
||||
specification: { amount_total: {} },
|
||||
},
|
||||
method: "web_search_read",
|
||||
model: "sale.order",
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
await services.orm.webSearchRead("sale.order", [["user_id", "=", 2]], {
|
||||
specification: { amount_total: {} },
|
||||
});
|
||||
|
||||
expect.verifySteps(["/web/dataset/call_kw/sale.order/web_search_read"]);
|
||||
});
|
||||
|
||||
test("orm is specialized for component", async () => {
|
||||
await makeMockEnv();
|
||||
|
||||
class MyComponent extends Component {
|
||||
static props = {};
|
||||
static template = xml`<div />`;
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
}
|
||||
}
|
||||
|
||||
const component = await mountWithCleanup(MyComponent);
|
||||
|
||||
expect(component.orm).not.toBe(getService("orm"));
|
||||
});
|
||||
|
||||
test("silent mode", async () => {
|
||||
onRpc((params) => {
|
||||
expect.step(params.route);
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
after(
|
||||
on(rpcBus, "RPC:RESPONSE", (ev) =>
|
||||
expect.step(`response${ev.detail.settings.silent ? " (silent)" : ""}`)
|
||||
)
|
||||
);
|
||||
|
||||
await services.orm.call("res.partner", "partner_method");
|
||||
await services.orm.silent.call("res.partner", "partner_method");
|
||||
await services.orm.call("res.partner", "partner_method");
|
||||
await services.orm.read("res.partner", [1], []);
|
||||
await services.orm.silent.read("res.partner", [1], []);
|
||||
await services.orm.read("res.partner", [1], []);
|
||||
|
||||
expect.verifySteps([
|
||||
"/web/dataset/call_kw/res.partner/partner_method",
|
||||
"response",
|
||||
"/web/dataset/call_kw/res.partner/partner_method",
|
||||
"response (silent)",
|
||||
"/web/dataset/call_kw/res.partner/partner_method",
|
||||
"response",
|
||||
"/web/dataset/call_kw/res.partner/read",
|
||||
"response",
|
||||
"/web/dataset/call_kw/res.partner/read",
|
||||
"response (silent)",
|
||||
"/web/dataset/call_kw/res.partner/read",
|
||||
"response",
|
||||
]);
|
||||
});
|
||||
|
||||
test("validate some obviously wrong calls", async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
|
||||
expect(() => services.orm.read(false, [3], ["id", "descr"])).toThrow(
|
||||
"Invalid model name: false"
|
||||
);
|
||||
expect(() => services.orm.read("res.res.partner", false, ["id", "descr"])).toThrow(
|
||||
"Invalid ids list: false"
|
||||
);
|
||||
});
|
||||
|
||||
test("optimize read and unlink if no ids", async () => {
|
||||
onRpc((params) => {
|
||||
expect.step(params.route);
|
||||
return false;
|
||||
});
|
||||
|
||||
const { services } = await makeMockEnv();
|
||||
|
||||
await services.orm.read("res.partner", [1], []);
|
||||
expect.verifySteps(["/web/dataset/call_kw/res.partner/read"]);
|
||||
|
||||
await services.orm.read("res.partner", [], []);
|
||||
expect.verifySteps([]);
|
||||
|
||||
await services.orm.unlink("res.partner", [1], {});
|
||||
expect.verifySteps(["/web/dataset/call_kw/res.partner/unlink"]);
|
||||
|
||||
await services.orm.unlink("res.partner", [], {});
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, useSubEnv, xml } from "@odoo/owl";
|
||||
import { getService, makeMockEnv, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
|
||||
test("simple case", async () => {
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
expect(".o-overlay-container").toHaveCount(1);
|
||||
|
||||
class MyComp extends Component {
|
||||
static template = xml`
|
||||
<div class="overlayed"></div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
const remove = getService("overlay").add(MyComp, {});
|
||||
await animationFrame();
|
||||
expect(".o-overlay-container .overlayed").toHaveCount(1);
|
||||
|
||||
remove();
|
||||
await animationFrame();
|
||||
expect(".o-overlay-container .overlayed").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("shadow DOM overlays are visible when registered before main component is mounted", async () => {
|
||||
class MyComp extends Component {
|
||||
static template = xml`
|
||||
<div class="overlayed"></div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("id", "my-root-id");
|
||||
root.attachShadow({ mode: "open" });
|
||||
getFixture().appendChild(root);
|
||||
|
||||
await makeMockEnv();
|
||||
getService("overlay").add(MyComp, {}, { rootId: "my-root-id" });
|
||||
|
||||
await mountWithCleanup(MainComponentsContainer, { target: root.shadowRoot });
|
||||
await animationFrame();
|
||||
|
||||
expect("#my-root-id:shadow .o-overlay-container .overlayed").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("onRemove callback", async () => {
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
class MyComp extends Component {
|
||||
static template = xml``;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
const onRemove = () => expect.step("onRemove");
|
||||
const remove = getService("overlay").add(MyComp, {}, { onRemove });
|
||||
|
||||
expect.verifySteps([]);
|
||||
remove();
|
||||
expect.verifySteps(["onRemove"]);
|
||||
});
|
||||
|
||||
test("multiple overlays", async () => {
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
class MyComp extends Component {
|
||||
static template = xml`
|
||||
<div class="overlayed" t-att-class="props.className"></div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
const remove1 = getService("overlay").add(MyComp, { className: "o1" });
|
||||
const remove2 = getService("overlay").add(MyComp, { className: "o2" });
|
||||
const remove3 = getService("overlay").add(MyComp, { className: "o3" });
|
||||
await animationFrame();
|
||||
expect(".overlayed").toHaveCount(3);
|
||||
expect(".o-overlay-container :nth-child(1) .overlayed").toHaveClass("o1");
|
||||
expect(".o-overlay-container :nth-child(2) .overlayed").toHaveClass("o2");
|
||||
expect(".o-overlay-container :nth-child(3) .overlayed").toHaveClass("o3");
|
||||
|
||||
remove1();
|
||||
await animationFrame();
|
||||
expect(".overlayed").toHaveCount(2);
|
||||
expect(".o-overlay-container :nth-child(1) .overlayed").toHaveClass("o2");
|
||||
expect(".o-overlay-container :nth-child(2) .overlayed").toHaveClass("o3");
|
||||
|
||||
remove2();
|
||||
await animationFrame();
|
||||
expect(".overlayed").toHaveCount(1);
|
||||
expect(".o-overlay-container :nth-child(1) .overlayed").toHaveClass("o3");
|
||||
|
||||
remove3();
|
||||
await animationFrame();
|
||||
expect(".overlayed").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("sequence", async () => {
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
class MyComp extends Component {
|
||||
static template = xml`
|
||||
<div class="overlayed" t-att-class="props.className"></div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
const remove1 = getService("overlay").add(MyComp, { className: "o1" }, { sequence: 50 });
|
||||
const remove2 = getService("overlay").add(MyComp, { className: "o2" }, { sequence: 60 });
|
||||
const remove3 = getService("overlay").add(MyComp, { className: "o3" }, { sequence: 40 });
|
||||
await animationFrame();
|
||||
expect(".overlayed").toHaveCount(3);
|
||||
expect(".o-overlay-container :nth-child(1) .overlayed").toHaveClass("o3");
|
||||
expect(".o-overlay-container :nth-child(2) .overlayed").toHaveClass("o1");
|
||||
expect(".o-overlay-container :nth-child(3) .overlayed").toHaveClass("o2");
|
||||
|
||||
remove1();
|
||||
await animationFrame();
|
||||
expect(".overlayed").toHaveCount(2);
|
||||
expect(".o-overlay-container :nth-child(1) .overlayed").toHaveClass("o3");
|
||||
expect(".o-overlay-container :nth-child(2) .overlayed").toHaveClass("o2");
|
||||
|
||||
remove2();
|
||||
await animationFrame();
|
||||
expect(".overlayed").toHaveCount(1);
|
||||
expect(".o-overlay-container :nth-child(1) .overlayed").toHaveClass("o3");
|
||||
|
||||
remove3();
|
||||
await animationFrame();
|
||||
expect(".overlayed").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("allow env as option", async () => {
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
|
||||
class MyComp extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<ul class="outer">
|
||||
<li>A=<t t-out="env.A"/></li>
|
||||
<li>B=<t t-out="env.B"/></li>
|
||||
</ul>
|
||||
`;
|
||||
setup() {
|
||||
useSubEnv({ A: "blip" });
|
||||
}
|
||||
}
|
||||
|
||||
getService("overlay").add(MyComp, {}, { env: { A: "foo", B: "bar" } });
|
||||
await animationFrame();
|
||||
|
||||
expect(".o-overlay-container li:nth-child(1)").toHaveText("A=blip");
|
||||
expect(".o-overlay-container li:nth-child(2)").toHaveText("B=bar");
|
||||
});
|
||||
|
|
@ -0,0 +1,371 @@
|
|||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import { queryOne, resize, scroll, waitFor } from "@odoo/hoot-dom";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Popover } from "@web/core/popover/popover";
|
||||
import { usePosition } from "@web/core/position/position_hook";
|
||||
|
||||
class Content extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<div id="popover">Popover Content</div>`;
|
||||
}
|
||||
|
||||
test("popover can have custom class", async () => {
|
||||
await mountWithCleanup(Popover, {
|
||||
props: {
|
||||
target: getFixture(),
|
||||
class: "custom-popover",
|
||||
component: Content,
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_popover.custom-popover").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("popover can have more than one custom class", async () => {
|
||||
await mountWithCleanup(Popover, {
|
||||
props: {
|
||||
target: getFixture(),
|
||||
class: "custom-popover popover-custom",
|
||||
component: Content,
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_popover.custom-popover.popover-custom").toHaveCount(1);
|
||||
});
|
||||
|
||||
test("popover is rendered nearby target (default)", async () => {
|
||||
expect.assertions(2);
|
||||
class TestPopover extends Popover {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
expect(direction).toBe("bottom");
|
||||
expect(variant).toBe("middle");
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(TestPopover, {
|
||||
props: {
|
||||
target: getFixture(),
|
||||
class: "custom-popover popover-custom",
|
||||
component: Content,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("popover is rendered nearby target (bottom)", async () => {
|
||||
expect.assertions(2);
|
||||
class TestPopover extends Popover {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
expect(direction).toBe("bottom");
|
||||
expect(variant).toBe("middle");
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(TestPopover, {
|
||||
props: {
|
||||
target: getFixture(),
|
||||
position: "bottom",
|
||||
component: Content,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("popover is rendered nearby target (top)", async () => {
|
||||
expect.assertions(2);
|
||||
class TestPopover extends Popover {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
expect(direction).toBe("top");
|
||||
expect(variant).toBe("middle");
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(TestPopover, {
|
||||
props: {
|
||||
target: getFixture(),
|
||||
position: "top",
|
||||
component: Content,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("popover is rendered nearby target (left)", async () => {
|
||||
expect.assertions(2);
|
||||
class TestPopover extends Popover {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
expect(direction).toBe("left");
|
||||
expect(variant).toBe("middle");
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(TestPopover, {
|
||||
props: {
|
||||
target: getFixture(),
|
||||
position: "left",
|
||||
component: Content,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("popover is rendered nearby target (right)", async () => {
|
||||
expect.assertions(2);
|
||||
class TestPopover extends Popover {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
expect(direction).toBe("right");
|
||||
expect(variant).toBe("middle");
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(TestPopover, {
|
||||
props: {
|
||||
target: getFixture(),
|
||||
position: "right",
|
||||
component: Content,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("popover is rendered nearby target (bottom-start)", async () => {
|
||||
expect.assertions(2);
|
||||
class TestPopover extends Popover {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
expect(direction).toBe("bottom");
|
||||
expect(variant).toBe("start");
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(TestPopover, {
|
||||
props: {
|
||||
target: getFixture(),
|
||||
position: "bottom-start",
|
||||
component: Content,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("popover is rendered nearby target (bottom-middle)", async () => {
|
||||
expect.assertions(2);
|
||||
class TestPopover extends Popover {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
expect(direction).toBe("bottom");
|
||||
expect(variant).toBe("middle");
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(TestPopover, {
|
||||
props: {
|
||||
target: getFixture(),
|
||||
position: "bottom-middle",
|
||||
component: Content,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("popover is rendered nearby target (bottom-end)", async () => {
|
||||
expect.assertions(2);
|
||||
class TestPopover extends Popover {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
expect(direction).toBe("bottom");
|
||||
expect(variant).toBe("end");
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(TestPopover, {
|
||||
props: {
|
||||
target: getFixture(),
|
||||
position: "bottom-end",
|
||||
component: Content,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("popover is rendered nearby target (bottom-fit)", async () => {
|
||||
expect.assertions(2);
|
||||
class TestPopover extends Popover {
|
||||
onPositioned(el, { direction, variant }) {
|
||||
expect(direction).toBe("bottom");
|
||||
expect(variant).toBe("fit");
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(TestPopover, {
|
||||
props: {
|
||||
target: getFixture(),
|
||||
position: "bottom-fit",
|
||||
component: Content,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("reposition popover should properly change classNames", async () => {
|
||||
await resize({ height: 300 });
|
||||
|
||||
class TestPopover extends Popover {
|
||||
setup() {
|
||||
// Don't call super.setup() in order to replace the use of usePosition hook...
|
||||
usePosition("ref", () => this.props.target, {
|
||||
container,
|
||||
onPositioned: this.onPositioned.bind(this),
|
||||
position: this.props.position,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Force some style, to make this test independent of screen size
|
||||
await mountWithCleanup(/* xml */ `
|
||||
<div class="container" style="width: 450px; height: 450px; display: flex; align-items: center; justify-content: center;">
|
||||
<div class="popover-target" style="width: 50px; height: 50px;" />
|
||||
</div>
|
||||
`);
|
||||
|
||||
const container = queryOne(".container");
|
||||
|
||||
await mountWithCleanup(TestPopover, {
|
||||
props: {
|
||||
target: queryOne(".popover-target"),
|
||||
component: Content,
|
||||
},
|
||||
});
|
||||
|
||||
const popover = queryOne("#popover");
|
||||
popover.style.height = "100px";
|
||||
popover.style.width = "100px";
|
||||
|
||||
// Should have classes for a "bottom-middle" placement
|
||||
expect(".o_popover").toHaveClass(
|
||||
"o_popover popover mw-100 o-popover--with-arrow bs-popover-bottom o-popover-bottom o-popover--bm"
|
||||
);
|
||||
expect(".popover-arrow").toHaveClass("popover-arrow start-0 end-0 mx-auto");
|
||||
|
||||
// Change container style and force update
|
||||
container.style.height = "125px"; // height of popper + 1/2 reference
|
||||
container.style.alignItems = "flex-end";
|
||||
await resize();
|
||||
await runAllTimers();
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveClass(
|
||||
"o_popover popover mw-100 o-popover--with-arrow bs-popover-end o-popover-right o-popover--re"
|
||||
);
|
||||
expect(".popover-arrow").toHaveClass("popover-arrow top-auto");
|
||||
});
|
||||
|
||||
test("within iframe", async () => {
|
||||
let popoverEl;
|
||||
class TestPopover extends Popover {
|
||||
onPositioned(el, { direction }) {
|
||||
popoverEl = el;
|
||||
expect.step(direction);
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(/* xml */ `
|
||||
<iframe class="container" style="height: 200px; display: flex" srcdoc="<div id='target' style='height:400px;'>Within iframe</div>" />
|
||||
`);
|
||||
|
||||
await waitFor(":iframe #target");
|
||||
|
||||
const popoverTarget = queryOne(":iframe #target");
|
||||
await mountWithCleanup(TestPopover, {
|
||||
props: {
|
||||
target: popoverTarget,
|
||||
component: Content,
|
||||
animation: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect.verifySteps(["bottom"]);
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(":iframe .o_popover").toHaveCount(0);
|
||||
|
||||
// The popover should be rendered in the correct position
|
||||
const marginTop = parseFloat(getComputedStyle(popoverEl).marginTop);
|
||||
const { top: targetTop, left: targetLeft } = popoverTarget.getBoundingClientRect();
|
||||
const { top: iframeTop, left: iframeLeft } = queryOne("iframe").getBoundingClientRect();
|
||||
let popoverBox = popoverEl.getBoundingClientRect();
|
||||
let expectedTop = iframeTop + targetTop + popoverTarget.offsetHeight + marginTop;
|
||||
const expectedLeft =
|
||||
iframeLeft + targetLeft + (popoverTarget.offsetWidth - popoverBox.width) / 2;
|
||||
expect(Math.floor(popoverBox.top)).toBe(Math.floor(expectedTop));
|
||||
expect(Math.floor(popoverBox.left)).toBe(Math.floor(expectedLeft));
|
||||
|
||||
await scroll(popoverTarget.ownerDocument.documentElement, { y: 100 }, { scrollable: false });
|
||||
await animationFrame();
|
||||
expect.verifySteps(["bottom"]);
|
||||
popoverBox = popoverEl.getBoundingClientRect();
|
||||
expectedTop -= 100;
|
||||
expect(Math.floor(popoverBox.top)).toBe(Math.floor(expectedTop));
|
||||
expect(Math.floor(popoverBox.left)).toBe(Math.floor(expectedLeft));
|
||||
});
|
||||
|
||||
test("within iframe -- wrong element class", async () => {
|
||||
class TestPopover extends Popover {
|
||||
static props = {
|
||||
...Popover.props,
|
||||
target: {
|
||||
validate: (...args) => {
|
||||
const val = Popover.props.target.validate(...args);
|
||||
expect.step(`validate target props: "${val}"`);
|
||||
return val;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await mountWithCleanup(/* xml */ `
|
||||
<iframe class="container" style="height: 200px; display: flex" srcdoc="<div id='target' style='height:400px;'>Within iframe</div>" />
|
||||
`);
|
||||
|
||||
await waitFor(":iframe #target");
|
||||
|
||||
const wrongElement = document.createElement("div");
|
||||
wrongElement.classList.add("wrong-element");
|
||||
queryOne(":iframe body").appendChild(wrongElement);
|
||||
|
||||
await mountWithCleanup(TestPopover, {
|
||||
props: {
|
||||
target: wrongElement,
|
||||
component: Content,
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect.verifySteps(['validate target props: "true"']);
|
||||
});
|
||||
|
||||
test("popover fixed position", async () => {
|
||||
class TestPopover extends Popover {
|
||||
onPositioned() {
|
||||
expect.step("onPositioned");
|
||||
}
|
||||
}
|
||||
|
||||
await resize({ width: 450, height: 450 });
|
||||
await mountWithCleanup(/* xml */ `
|
||||
<div class="container w-100 h-100" style="display: flex">
|
||||
<div class="popover-target" style="width: 50px; height: 50px;" />
|
||||
</div>
|
||||
`);
|
||||
|
||||
const container = queryOne(".container");
|
||||
|
||||
await mountWithCleanup(TestPopover, {
|
||||
props: {
|
||||
target: container,
|
||||
position: "bottom-fit",
|
||||
fixedPosition: true,
|
||||
component: Content,
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect.verifySteps(["onPositioned"]);
|
||||
|
||||
// force the DOM update
|
||||
container.style.alignItems = "flex-end";
|
||||
await resize({ height: 125 });
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import { test, expect, getFixture, destroy } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { contains, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
test("close popover when component is unmounted", async () => {
|
||||
const target = getFixture();
|
||||
class Comp extends Component {
|
||||
static template = xml`<div t-att-id="props.id">in popover</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
class CompWithPopover extends Component {
|
||||
static template = xml`<div />`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.popover = usePopover(Comp);
|
||||
}
|
||||
}
|
||||
|
||||
const comp1 = await mountWithCleanup(CompWithPopover);
|
||||
comp1.popover.open(target, { id: "comp1" });
|
||||
await animationFrame();
|
||||
|
||||
const comp2 = await mountWithCleanup(CompWithPopover, { noMainContainer: true });
|
||||
comp2.popover.open(target, { id: "comp2" });
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(2);
|
||||
expect(".o_popover #comp1").toHaveCount(1);
|
||||
expect(".o_popover #comp2").toHaveCount(1);
|
||||
|
||||
destroy(comp1);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover #comp1").toHaveCount(0);
|
||||
expect(".o_popover #comp2").toHaveCount(1);
|
||||
|
||||
destroy(comp2);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
expect(".o_popover #comp1").toHaveCount(0);
|
||||
expect(".o_popover #comp2").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("popover opened from another", async () => {
|
||||
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>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.popover = usePopover(Comp, {
|
||||
popoverClass: `popover-${++Comp.id}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Comp);
|
||||
|
||||
await contains(".pop-open").click();
|
||||
expect(".popover-1").toHaveCount(1);
|
||||
|
||||
await contains(".popover-1 .pop-open").click();
|
||||
expect(".o_popover").toHaveCount(2);
|
||||
expect(".popover-1").toHaveCount(1);
|
||||
expect(".popover-2").toHaveCount(1);
|
||||
|
||||
await contains(".popover-2 .pop-open").click();
|
||||
expect(".o_popover").toHaveCount(3);
|
||||
expect(".popover-1").toHaveCount(1);
|
||||
expect(".popover-2").toHaveCount(1);
|
||||
expect(".popover-3").toHaveCount(1);
|
||||
|
||||
await contains(".popover-3").click();
|
||||
expect(".o_popover").toHaveCount(3);
|
||||
expect(".popover-1").toHaveCount(1);
|
||||
expect(".popover-2").toHaveCount(1);
|
||||
expect(".popover-3").toHaveCount(1);
|
||||
|
||||
await contains(".popover-2").click();
|
||||
expect(".o_popover").toHaveCount(2);
|
||||
expect(".popover-1").toHaveCount(1);
|
||||
expect(".popover-2").toHaveCount(1);
|
||||
|
||||
await contains(document.body).click();
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
import { Component, onWillStart, xml } from "@odoo/owl";
|
||||
import { test, expect, beforeEach, getFixture } from "@odoo/hoot";
|
||||
import { getService, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { click, press } from "@odoo/hoot-dom";
|
||||
import { Deferred } from "@web/core/utils/concurrency";
|
||||
|
||||
let target;
|
||||
|
||||
beforeEach(async () => {
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
test("simple use", async () => {
|
||||
class Comp extends Component {
|
||||
static template = xml`<div id="comp">in popover</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
|
||||
const remove = getService("popover").add(target, Comp);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover #comp").toHaveCount(1);
|
||||
|
||||
remove();
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
expect(".o_popover #comp").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("close on click away", async () => {
|
||||
class Comp extends Component {
|
||||
static template = xml`<div id="comp">in popover</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
getService("popover").add(target, Comp);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover #comp").toHaveCount(1);
|
||||
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
expect(".o_popover #comp").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("close on click away when loading", async () => {
|
||||
const def = new Deferred();
|
||||
class Comp extends Component {
|
||||
static template = xml`<div id="comp">in popover</div>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
onWillStart(async () => {
|
||||
await def;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getService("popover").add(target, Comp);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
expect(".o_popover #comp").toHaveCount(0);
|
||||
|
||||
click(document.body);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
expect(".o_popover #comp").toHaveCount(0);
|
||||
|
||||
def.resolve();
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
expect(".o_popover #comp").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("close on 'Escape' keydown", async () => {
|
||||
class Comp extends Component {
|
||||
static template = xml`<div id="comp">in popover</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
getService("popover").add(target, Comp);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover #comp").toHaveCount(1);
|
||||
|
||||
await press("Escape");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
expect(".o_popover #comp").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("do not close on click away", async () => {
|
||||
class Comp extends Component {
|
||||
static template = xml`<div id="comp">in popover</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
const remove = getService("popover").add(target, Comp, {}, { closeOnClickAway: false });
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover #comp").toHaveCount(1);
|
||||
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover #comp").toHaveCount(1);
|
||||
|
||||
remove();
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
expect(".o_popover #comp").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("close callback", async () => {
|
||||
class Comp extends Component {
|
||||
static template = xml`<div id="comp">in popover</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
expect.step("close");
|
||||
}
|
||||
|
||||
getService("popover").add(target, Comp, {}, { onClose });
|
||||
await animationFrame();
|
||||
|
||||
await click(document.body);
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps(["close"]);
|
||||
});
|
||||
|
||||
test("sub component triggers close", async () => {
|
||||
class Comp extends Component {
|
||||
static template = xml`<div id="comp" t-on-click="() => this.props.close()">in popover</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
getService("popover").add(target, Comp);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover #comp").toHaveCount(1);
|
||||
|
||||
await click("#comp");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
expect(".o_popover #comp").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("close popover if target is removed", async () => {
|
||||
class Comp extends Component {
|
||||
static template = xml`<div id="comp">in popover</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
getService("popover").add(target, Comp);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover #comp").toHaveCount(1);
|
||||
|
||||
target.remove();
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
expect(".o_popover #comp").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("close and do not crash if target parent does not exist", async () => {
|
||||
// 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");
|
||||
|
||||
class Comp extends Component {
|
||||
static template = xml`<div id="comp">in popover</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
expect.step("close");
|
||||
}
|
||||
|
||||
getService("popover").add(dissapearedTarget, Comp, {}, { onClose });
|
||||
await animationFrame();
|
||||
|
||||
expect.verifySteps(["close"]);
|
||||
});
|
||||
|
||||
test("keep popover if target sibling is removed", async () => {
|
||||
class Comp extends Component {
|
||||
static template = xml`<div id="comp">in popover</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
class Sibling extends Component {
|
||||
static template = xml`<div id="sibling">Sibling</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Sibling, { noMainContainer: true });
|
||||
|
||||
getService("popover").add(target, Comp);
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover #comp").toHaveCount(1);
|
||||
|
||||
target.querySelector("#sibling").remove();
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover #comp").toHaveCount(1);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,67 @@
|
|||
import { describe, expect, getFixture, test } from "@odoo/hoot";
|
||||
import { mockFetch } from "@odoo/hoot-mock";
|
||||
import { getService, makeMockEnv, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
const mountManifestLink = (href) => {
|
||||
const fixture = getFixture();
|
||||
const manifestLink = document.createElement("link");
|
||||
manifestLink.rel = "manifest";
|
||||
manifestLink.href = href;
|
||||
fixture.append(manifestLink);
|
||||
};
|
||||
|
||||
test("PWA service fetches the manifest found in the page", async () => {
|
||||
await makeMockEnv();
|
||||
mountManifestLink("/web/manifest.webmanifest");
|
||||
mockFetch((route) => {
|
||||
expect.step(route);
|
||||
return { name: "Odoo PWA" };
|
||||
});
|
||||
const pwaService = await getService("pwa");
|
||||
let appManifest = await pwaService.getManifest();
|
||||
expect(appManifest).toEqual({ name: "Odoo PWA" });
|
||||
expect.verifySteps(["/web/manifest.webmanifest"]);
|
||||
appManifest = await pwaService.getManifest();
|
||||
expect(appManifest).toEqual({ name: "Odoo PWA" });
|
||||
// manifest is only fetched once to get the app name
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("PWA installation process", async () => {
|
||||
const beforeInstallPromptEvent = new CustomEvent("beforeinstallprompt");
|
||||
beforeInstallPromptEvent.preventDefault = () => {};
|
||||
beforeInstallPromptEvent.prompt = async () => ({ outcome: "accepted" });
|
||||
browser.BeforeInstallPromptEvent = beforeInstallPromptEvent;
|
||||
await makeMockEnv();
|
||||
mountManifestLink("/web/manifest.scoped_app_manifest");
|
||||
mockFetch((route) => {
|
||||
expect.step(route);
|
||||
return { name: "My App", scope: "/scoped_app/myApp", start_url: "/scoped_app/myApp" };
|
||||
});
|
||||
patchWithCleanup(browser.localStorage, {
|
||||
setItem(key, value) {
|
||||
if (key === "pwaService.installationState") {
|
||||
expect.step(value);
|
||||
return null;
|
||||
}
|
||||
return super.setItem(key, value);
|
||||
},
|
||||
});
|
||||
const pwaService = await getService("pwa");
|
||||
expect(pwaService.isAvailable).toBe(false);
|
||||
expect(pwaService.canPromptToInstall).toBe(false);
|
||||
browser.dispatchEvent(beforeInstallPromptEvent);
|
||||
expect(pwaService.isAvailable).toBe(true);
|
||||
expect(pwaService.canPromptToInstall).toBe(true);
|
||||
await pwaService.show({
|
||||
onDone: (res) => {
|
||||
expect.step("onDone call with installation " + res.outcome);
|
||||
},
|
||||
});
|
||||
expect(pwaService.canPromptToInstall).toBe(false);
|
||||
expect.verifySteps(['{"/odoo":"accepted"}', "onDone call with installation accepted"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,420 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { mockDate } from "@odoo/hoot-mock";
|
||||
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
import { PyDate, PyTimeDelta } from "@web/core/py_js/py_date";
|
||||
|
||||
const check = (expr, fn) => {
|
||||
const d0 = new Date();
|
||||
const result = evaluateExpr(expr);
|
||||
const d1 = new Date();
|
||||
return fn(d0) <= result && result <= fn(d1);
|
||||
};
|
||||
|
||||
const format = (n) => String(n).padStart(2, "0");
|
||||
|
||||
const formatDate = (d) => {
|
||||
const year = d.getFullYear();
|
||||
const month = format(d.getMonth() + 1);
|
||||
const day = format(d.getDate());
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
const formatDateTime = (d) => {
|
||||
const h = format(d.getHours());
|
||||
const m = format(d.getMinutes());
|
||||
const s = format(d.getSeconds());
|
||||
return `${formatDate(d)} ${h}:${m}:${s}`;
|
||||
};
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
describe("time", () => {
|
||||
test("strftime", () => {
|
||||
expect(check("time.strftime('%Y')", (d) => String(d.getFullYear()))).toBe(true);
|
||||
expect(
|
||||
check("time.strftime('%Y') + '-01-30'", (d) => String(d.getFullYear()) + "-01-30")
|
||||
).toBe(true);
|
||||
expect(check("time.strftime('%Y-%m-%d %H:%M:%S')", formatDateTime)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("datetime.datetime", () => {
|
||||
test("datetime.datetime.now", () => {
|
||||
expect(check("datetime.datetime.now().year", (d) => d.getFullYear())).toBe(true);
|
||||
expect(check("datetime.datetime.now().month", (d) => d.getMonth() + 1)).toBe(true);
|
||||
expect(check("datetime.datetime.now().day", (d) => d.getDate())).toBe(true);
|
||||
expect(check("datetime.datetime.now().hour", (d) => d.getHours())).toBe(true);
|
||||
expect(check("datetime.datetime.now().minute", (d) => d.getMinutes())).toBe(true);
|
||||
expect(check("datetime.datetime.now().second", (d) => d.getSeconds())).toBe(true);
|
||||
});
|
||||
|
||||
test("various operations", () => {
|
||||
const expr1 = "datetime.datetime(day=3,month=4,year=2001).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr1)).toBe("2001-04-03");
|
||||
const expr2 = "datetime.datetime(2001, 4, 3).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr2)).toBe("2001-04-03");
|
||||
const expr3 =
|
||||
"datetime.datetime(day=3,month=4,second=12, year=2001,minute=32).strftime('%Y-%m-%d %H:%M:%S')";
|
||||
expect(evaluateExpr(expr3)).toBe("2001-04-03 00:32:12");
|
||||
});
|
||||
|
||||
test("to_utc", () => {
|
||||
mockDate("2021-09-17 10:00:00", +6);
|
||||
|
||||
const expr = "datetime.datetime.combine(context_today(), datetime.time(0,0,0)).to_utc()";
|
||||
|
||||
expect(JSON.stringify(evaluateExpr(expr))).toBe(`"2021-09-16 18:00:00"`);
|
||||
});
|
||||
|
||||
test("to_utc in october with winter/summer change", () => {
|
||||
mockDate("2021-10-17 10:00:00", "Europe/Brussels");
|
||||
|
||||
const expr = "datetime.datetime(2022, 10, 17).to_utc()";
|
||||
expect(JSON.stringify(evaluateExpr(expr))).toBe(`"2022-10-16 22:00:00"`);
|
||||
});
|
||||
|
||||
test("datetime.datetime.combine", () => {
|
||||
const expr =
|
||||
"datetime.datetime.combine(context_today(), datetime.time(23,59,59)).strftime('%Y-%m-%d %H:%M:%S')";
|
||||
expect(
|
||||
check(expr, (d) => {
|
||||
return formatDate(d) + " 23:59:59";
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("datetime.datetime.toJSON", () => {
|
||||
expect(
|
||||
JSON.stringify(evaluateExpr("datetime.datetime(day=3,month=4,year=2001,hour=10)"))
|
||||
).toBe(`"2001-04-03 10:00:00"`);
|
||||
});
|
||||
|
||||
test("datetime + timedelta", function () {
|
||||
expect.assertions(6);
|
||||
|
||||
expect(
|
||||
evaluateExpr(
|
||||
"(datetime.datetime(2017, 2, 15, 1, 7, 31) + datetime.timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"
|
||||
)
|
||||
).toBe("2017-02-16 01:07:31");
|
||||
expect(
|
||||
evaluateExpr(
|
||||
"(datetime.datetime(2012, 2, 15, 1, 7, 31) - datetime.timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')"
|
||||
)
|
||||
).toBe("2012-02-15 00:07:31");
|
||||
expect(
|
||||
evaluateExpr(
|
||||
"(datetime.datetime(2012, 2, 15, 1, 7, 31) + datetime.timedelta(hours=-1)).strftime('%Y-%m-%d %H:%M:%S')"
|
||||
)
|
||||
).toBe("2012-02-15 00:07:31");
|
||||
expect(
|
||||
evaluateExpr(
|
||||
"(datetime.datetime(2012, 2, 15, 1, 7, 31) + datetime.timedelta(minutes=100)).strftime('%Y-%m-%d %H:%M:%S')"
|
||||
)
|
||||
).toBe("2012-02-15 02:47:31");
|
||||
expect(
|
||||
evaluateExpr(
|
||||
"(datetime.date(day=3,month=4,year=2001) + datetime.timedelta(days=-1)).strftime('%Y-%m-%d')"
|
||||
)
|
||||
).toBe("2001-04-02");
|
||||
expect(
|
||||
evaluateExpr(
|
||||
"(datetime.timedelta(days=-1) + datetime.date(day=3,month=4,year=2001)).strftime('%Y-%m-%d')"
|
||||
)
|
||||
).toBe("2001-04-02");
|
||||
});
|
||||
});
|
||||
|
||||
describe("datetime.date", () => {
|
||||
test("datetime.date.today", () => {
|
||||
expect(check("(datetime.date.today()).strftime('%Y-%m-%d')", formatDate)).toBe(true);
|
||||
});
|
||||
|
||||
test("various operations", () => {
|
||||
const expr1 = "datetime.date(day=3,month=4,year=2001).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr1)).toBe("2001-04-03");
|
||||
const expr2 = "datetime.date(2001, 4, 3).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr2)).toBe("2001-04-03");
|
||||
});
|
||||
|
||||
test("datetime.date.toJSON", () => {
|
||||
expect(JSON.stringify(evaluateExpr("datetime.date(year=1997,month=5,day=18)"))).toBe(
|
||||
`"1997-05-18"`
|
||||
);
|
||||
});
|
||||
|
||||
test("basic operations with dates", function () {
|
||||
expect.assertions(19);
|
||||
|
||||
let ctx = {
|
||||
d1: PyDate.create(2002, 1, 31),
|
||||
d2: PyDate.create(1956, 1, 31),
|
||||
};
|
||||
|
||||
expect(evaluateExpr("(d1 - d2).days", ctx)).toBe(46 * 365 + 12);
|
||||
expect(evaluateExpr("(d1 - d2).seconds", ctx)).toBe(0);
|
||||
expect(evaluateExpr("(d1 - d2).microseconds", ctx)).toBe(0);
|
||||
|
||||
ctx = {
|
||||
a: PyDate.create(2002, 3, 2),
|
||||
day: PyTimeDelta.create({ days: 1 }),
|
||||
week: PyTimeDelta.create({ days: 7 }),
|
||||
date: PyDate,
|
||||
};
|
||||
|
||||
expect(evaluateExpr("a + day == date(2002, 3, 3)", ctx)).toBe(true);
|
||||
expect(evaluateExpr("day + a == date(2002, 3, 3)", ctx)).toBe(true); // 5
|
||||
expect(evaluateExpr("a - day == date(2002, 3, 1)", ctx)).toBe(true);
|
||||
expect(evaluateExpr("-day + a == date(2002, 3, 1)", ctx)).toBe(true);
|
||||
expect(evaluateExpr("a + week == date(2002, 3, 9)", ctx)).toBe(true);
|
||||
expect(evaluateExpr("a - week == date(2002, 2, 23)", ctx)).toBe(true);
|
||||
expect(evaluateExpr("a + 52*week == date(2003, 3, 1)", ctx)).toBe(true); // 10
|
||||
expect(evaluateExpr("a - 52*week == date(2001, 3, 3)", ctx)).toBe(true);
|
||||
expect(evaluateExpr("(a + week) - a == week", ctx)).toBe(true);
|
||||
expect(evaluateExpr("(a + day) - a == day", ctx)).toBe(true);
|
||||
expect(evaluateExpr("(a - week) - a == -week", ctx)).toBe(true);
|
||||
expect(evaluateExpr("(a - day) - a == -day", ctx)).toBe(true); // 15
|
||||
expect(evaluateExpr("a - (a + week) == -week", ctx)).toBe(true);
|
||||
expect(evaluateExpr("a - (a + day) == -day", ctx)).toBe(true);
|
||||
expect(evaluateExpr("a - (a - week) == week", ctx)).toBe(true);
|
||||
expect(evaluateExpr("a - (a - day) == day", ctx)).toBe(true);
|
||||
|
||||
// expect(() => evaluateExpr("a + 1", ctx)).toThrow(/^Error: TypeError:/); //20
|
||||
// expect(() => evaluateExpr("a - 1", ctx)).toThrow(/^Error: TypeError:/);
|
||||
// expect(() => evaluateExpr("1 + a", ctx)).toThrow(/^Error: TypeError:/);
|
||||
// expect(() => evaluateExpr("1 - a", ctx)).toThrow(/^Error: TypeError:/);
|
||||
|
||||
// // delta - date is senseless.
|
||||
// expect(() => evaluateExpr("day - a", ctx)).toThrow(/^Error: TypeError:/);
|
||||
|
||||
// // mixing date and (delta or date) via * or // is senseless
|
||||
// expect(() => evaluateExpr("day * a", ctx)).toThrow(/^Error: TypeError:/); // 25
|
||||
// expect(() => evaluateExpr("a * day", ctx)).toThrow(/^Error: TypeError:/);
|
||||
// expect(() => evaluateExpr("day // a", ctx)).toThrow(/^Error: TypeError:/);
|
||||
// expect(() => evaluateExpr("a // day", ctx)).toThrow(/^Error: TypeError:/);
|
||||
// expect(() => evaluateExpr("a * a", ctx)).toThrow(/^Error: TypeError:/);
|
||||
// expect(() => evaluateExpr("a // a", ctx)).toThrow(/^Error: TypeError:/); // 30
|
||||
|
||||
// // date + date is senseless
|
||||
// expect(() => evaluateExpr("a + a", ctx)).toThrow(/^Error: TypeError:/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("datetime.time", () => {
|
||||
test("various operations", () => {
|
||||
const expr1 = "datetime.time(hour=3,minute=2. second=1).strftime('%H:%M:%S')";
|
||||
expect(evaluateExpr(expr1)).toBe("03:02:01");
|
||||
});
|
||||
|
||||
test("attributes", () => {
|
||||
const expr1 = "datetime.time(hour=3,minute=2. second=1).hour";
|
||||
expect(evaluateExpr(expr1)).toBe(3);
|
||||
const expr2 = "datetime.time(hour=3,minute=2. second=1).minute";
|
||||
expect(evaluateExpr(expr2)).toBe(2);
|
||||
const expr3 = "datetime.time(hour=3,minute=2. second=1).second";
|
||||
expect(evaluateExpr(expr3)).toBe(1);
|
||||
});
|
||||
|
||||
test("datetime.time.toJSON", () => {
|
||||
expect(JSON.stringify(evaluateExpr("datetime.time(hour=11,minute=45,second=15)"))).toBe(
|
||||
`"11:45:15"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("relativedelta relative : period is plural", () => {
|
||||
test("adding date and relative delta", () => {
|
||||
const expr1 =
|
||||
"(datetime.date(day=3,month=4,year=2001) + relativedelta(days=-1)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr1)).toBe("2001-04-02");
|
||||
const expr2 =
|
||||
"(datetime.date(day=3,month=4,year=2001) + relativedelta(weeks=-1)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr2)).toBe("2001-03-27");
|
||||
});
|
||||
|
||||
test("adding relative delta and date", () => {
|
||||
const expr =
|
||||
"(relativedelta(days=-1) + datetime.date(day=3,month=4,year=2001)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr)).toBe("2001-04-02");
|
||||
});
|
||||
|
||||
test("adding/substracting relative delta and date -- shifts order of magnitude", () => {
|
||||
const expr =
|
||||
"(relativedelta(hours=14) + datetime.datetime(hour=15,day=3,month=4,year=2001)).strftime('%Y-%m-%d %H:%M:%S')";
|
||||
expect(evaluateExpr(expr)).toBe("2001-04-04 05:00:00");
|
||||
|
||||
const expr2 =
|
||||
"(relativedelta(days=32) + datetime.date(day=3,month=4,year=2001)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr2)).toBe("2001-05-05");
|
||||
|
||||
const expr3 =
|
||||
"(relativedelta(months=14) + datetime.date(day=3,month=4,year=2001)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr3)).toBe("2002-06-03");
|
||||
|
||||
const expr4 =
|
||||
"(datetime.datetime(hour=13,day=3,month=4,year=2001) - relativedelta(hours=14)).strftime('%Y-%m-%d %H:%M:%S')";
|
||||
expect(evaluateExpr(expr4)).toBe("2001-04-02 23:00:00");
|
||||
|
||||
const expr5 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(days=4)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr5)).toBe("2001-03-30");
|
||||
|
||||
const expr6 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(months=5)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr6)).toBe("2000-11-03");
|
||||
});
|
||||
|
||||
test("substracting date and relative delta", () => {
|
||||
const expr1 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(days=-1)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr1)).toBe("2001-04-04");
|
||||
const expr2 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(weeks=-1)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr2)).toBe("2001-04-10");
|
||||
const expr3 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(days=1)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr3)).toBe("2001-04-02");
|
||||
const expr4 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(weeks=1)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr4)).toBe("2001-03-27");
|
||||
});
|
||||
});
|
||||
|
||||
describe("relativedelta absolute : period is singular", () => {
|
||||
test("throws when period negative", () => {
|
||||
const expr1 = "relativedelta(day=-1)";
|
||||
expect(() => evaluateExpr(expr1)).toThrow("day -1 is out of range");
|
||||
|
||||
const expr2 = "relativedelta(month=-1)";
|
||||
expect(() => evaluateExpr(expr2)).toThrow("month -1 is out of range");
|
||||
});
|
||||
|
||||
test("adding date and relative delta", () => {
|
||||
const expr1 =
|
||||
"(datetime.date(day=3,month=4,year=2001) + relativedelta(day=1)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr1)).toBe("2001-04-01");
|
||||
|
||||
const expr2 =
|
||||
"(datetime.date(day=3,month=4,year=2001) + relativedelta(month=1)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr2)).toBe("2001-01-03");
|
||||
|
||||
const expr3 =
|
||||
"(datetime.date(2021,10,1) + relativedelta(hours=12)).strftime('%Y-%m-%d %H:%M:%S')";
|
||||
expect(evaluateExpr(expr3)).toBe("2021-10-01 12:00:00");
|
||||
|
||||
const expr4 =
|
||||
"(datetime.date(2021,10,1) + relativedelta(day=15,days=3)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr4)).toBe("2021-10-18");
|
||||
|
||||
const expr5 =
|
||||
"(datetime.date(2021,10,1) - relativedelta(day=15,days=3)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr5)).toBe("2021-10-12");
|
||||
|
||||
const expr6 =
|
||||
"(datetime.date(2021,10,1) + relativedelta(day=15,days=3,hours=24)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr6)).toBe("2021-10-19");
|
||||
});
|
||||
|
||||
test("adding relative delta and date", () => {
|
||||
const expr =
|
||||
"(relativedelta(day=1) + datetime.date(day=3,month=4,year=2001)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr)).toBe("2001-04-01");
|
||||
});
|
||||
|
||||
test("substracting date and relative delta", () => {
|
||||
const expr1 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(day=1)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr1)).toBe("2001-04-01");
|
||||
|
||||
const expr3 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(day=1)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr3)).toBe("2001-04-01");
|
||||
});
|
||||
|
||||
test("type of date + relative delta", () => {
|
||||
const expr1 = "(datetime.date(2021,10,1) + relativedelta(day=15,days=3,hours=24))";
|
||||
expect(evaluateExpr(expr1)).toBeInstanceOf(PyDate);
|
||||
});
|
||||
});
|
||||
|
||||
describe("relative delta weekday", () => {
|
||||
test("add or substract weekday", () => {
|
||||
const expr1 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(day=1, weekday=3)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr1)).toBe("2001-04-05");
|
||||
|
||||
const expr2 =
|
||||
"(datetime.date(day=29,month=4,year=2001) - relativedelta(weekday=4)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr2)).toBe("2001-05-04");
|
||||
|
||||
const expr3 =
|
||||
"(datetime.date(day=6,month=4,year=2001) - relativedelta(weekday=0)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr3)).toBe("2001-04-09");
|
||||
|
||||
const expr4 =
|
||||
"(datetime.date(day=1,month=4,year=2001) + relativedelta(weekday=-2)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr4)).toBe("2001-04-07");
|
||||
|
||||
const expr5 =
|
||||
"(datetime.date(day=11,month=4,year=2001) + relativedelta(weekday=2)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr5)).toBe("2001-04-11");
|
||||
|
||||
const expr6 =
|
||||
"(datetime.date(day=11,month=4,year=2001) + relativedelta(weekday=-2)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr6)).toBe("2001-04-14");
|
||||
|
||||
const expr7 =
|
||||
"(datetime.date(day=11,month=4,year=2001) + relativedelta(weekday=0)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr7)).toBe("2001-04-16");
|
||||
|
||||
const expr8 =
|
||||
"(datetime.date(day=11,month=4,year=2001) + relativedelta(weekday=1)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr8)).toBe("2001-04-17");
|
||||
});
|
||||
});
|
||||
|
||||
describe("relative delta yearday nlyearday", () => {
|
||||
test("yearday", () => {
|
||||
const expr1 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(year=2000, yearday=60)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr1)).toBe("2000-02-29");
|
||||
|
||||
const expr2 =
|
||||
"(datetime.date(day=3,month=4,year=2001) - relativedelta(yearday=60)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr2)).toBe("2001-03-01");
|
||||
|
||||
const expr3 =
|
||||
"(datetime.date(1999,12,31) + relativedelta(days=1, yearday=60)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr3)).toBe("1999-03-02");
|
||||
});
|
||||
|
||||
test("nlyearday", () => {
|
||||
const expr1 =
|
||||
"(datetime.date(day=3,month=4,year=2001) + relativedelta(year=2000, nlyearday=60)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr1)).toBe("2000-03-01");
|
||||
|
||||
const expr2 =
|
||||
"(datetime.date(day=3,month=4,year=2001) + relativedelta(nlyearday=60)).strftime('%Y-%m-%d')";
|
||||
expect(evaluateExpr(expr2)).toBe("2001-03-01");
|
||||
});
|
||||
});
|
||||
|
||||
describe("misc", () => {
|
||||
test("context_today", () => {
|
||||
expect(check("context_today().strftime('%Y-%m-%d')", formatDate)).toBe(true);
|
||||
});
|
||||
|
||||
test("today", () => {
|
||||
expect(check("today", formatDate)).toBe(true);
|
||||
});
|
||||
|
||||
test("now", () => {
|
||||
expect(check("now", formatDateTime)).toBe(true);
|
||||
});
|
||||
|
||||
test("current_date", () => {
|
||||
mockDate("2021-09-20 10:00:00");
|
||||
expect(evaluateExpr("current_date")).toBe("2021-09-20");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,509 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
|
||||
import { evaluateBooleanExpr, evaluateExpr } from "@web/core/py_js/py";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
describe("basic values", () => {
|
||||
test("evaluate simple values", () => {
|
||||
expect(evaluateExpr("12")).toBe(12);
|
||||
expect(evaluateExpr('"foo"')).toBe("foo");
|
||||
});
|
||||
|
||||
test("empty expression", () => {
|
||||
expect(() => evaluateExpr("")).toThrow(/Error: Missing token/);
|
||||
});
|
||||
|
||||
test("numbers", () => {
|
||||
expect(evaluateExpr("1.2")).toBe(1.2);
|
||||
expect(evaluateExpr(".12")).toBe(0.12);
|
||||
expect(evaluateExpr("0")).toBe(0);
|
||||
expect(evaluateExpr("1.0")).toBe(1);
|
||||
expect(evaluateExpr("-1.2")).toBe(-1.2);
|
||||
expect(evaluateExpr("-12")).toBe(-12);
|
||||
expect(evaluateExpr("+12")).toBe(12);
|
||||
});
|
||||
|
||||
test("strings", () => {
|
||||
expect(evaluateExpr('""')).toBe("");
|
||||
expect(evaluateExpr('"foo"')).toBe("foo");
|
||||
expect(evaluateExpr("'foo'")).toBe("foo");
|
||||
expect(evaluateExpr("'FOO'.lower()")).toBe("foo");
|
||||
expect(evaluateExpr("'foo'.upper()")).toBe("FOO");
|
||||
});
|
||||
|
||||
test("boolean", () => {
|
||||
expect(evaluateExpr("True")).toBe(true);
|
||||
expect(evaluateExpr("False")).toBe(false);
|
||||
});
|
||||
|
||||
test("lists", () => {
|
||||
expect(evaluateExpr("[]")).toEqual([]);
|
||||
expect(evaluateExpr("[1]")).toEqual([1]);
|
||||
expect(evaluateExpr("[1,2]")).toEqual([1, 2]);
|
||||
expect(evaluateExpr("[1,False, None, 'foo']")).toEqual([1, false, null, "foo"]);
|
||||
expect(evaluateExpr("[1,2 + 3]")).toEqual([1, 5]);
|
||||
expect(evaluateExpr("[1,2, 3][1]")).toBe(2);
|
||||
});
|
||||
|
||||
test("None", () => {
|
||||
expect(evaluateExpr("None")).toBe(null);
|
||||
});
|
||||
|
||||
test("Tuples", () => {
|
||||
expect(evaluateExpr("()")).toEqual([]);
|
||||
expect(evaluateExpr("(1,)")).toEqual([1]);
|
||||
expect(evaluateExpr("(1,2)")).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
test("strings can be concatenated", () => {
|
||||
expect(evaluateExpr('"foo" + "bar"')).toBe("foobar");
|
||||
});
|
||||
});
|
||||
|
||||
describe("number properties", () => {
|
||||
test("number arithmetic", () => {
|
||||
expect(evaluateExpr("1 + 2")).toBe(3);
|
||||
expect(evaluateExpr("4 - 2")).toBe(2);
|
||||
expect(evaluateExpr("4 * 2")).toBe(8);
|
||||
expect(evaluateExpr("1.5 + 2")).toBe(3.5);
|
||||
expect(evaluateExpr("1 + -1")).toBe(0);
|
||||
expect(evaluateExpr("1 - 1")).toBe(0);
|
||||
expect(evaluateExpr("1.5 - 2")).toBe(-0.5);
|
||||
expect(evaluateExpr("0 * 5")).toBe(0);
|
||||
expect(evaluateExpr("1 + 3 * 5")).toBe(16);
|
||||
expect(evaluateExpr("42 * -2")).toBe(-84);
|
||||
expect(evaluateExpr("1 / 2")).toBe(0.5);
|
||||
expect(evaluateExpr("2 / 1")).toBe(2);
|
||||
expect(evaluateExpr("42 % 5")).toBe(2);
|
||||
expect(evaluateExpr("2 ** 3")).toBe(8);
|
||||
expect(evaluateExpr("a + b", { a: 1, b: 41 })).toBe(42);
|
||||
});
|
||||
|
||||
test("// operator", () => {
|
||||
expect(evaluateExpr("1 // 2")).toBe(0);
|
||||
expect(evaluateExpr("1 // -2")).toBe(-1);
|
||||
expect(evaluateExpr("-1 // 2")).toBe(-1);
|
||||
expect(evaluateExpr("6 // 2")).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("boolean properties", () => {
|
||||
test("boolean arithmetic", () => {
|
||||
expect(evaluateExpr("True and False")).toBe(false);
|
||||
expect(evaluateExpr("True or False")).toBe(true);
|
||||
expect(evaluateExpr("True and (False or True)")).toBe(true);
|
||||
expect(evaluateExpr("not True")).toBe(false);
|
||||
expect(evaluateExpr("not False")).toBe(true);
|
||||
expect(evaluateExpr("not foo", { foo: false })).toBe(true);
|
||||
expect(evaluateExpr("not None")).toBe(true);
|
||||
expect(evaluateExpr("not []")).toBe(true);
|
||||
expect(evaluateExpr("True == False or True == True")).toBe(true);
|
||||
expect(evaluateExpr("False == True and False")).toBe(false);
|
||||
});
|
||||
|
||||
test("get value from context", () => {
|
||||
expect(evaluateExpr("foo == 'foo' or foo == 'bar'", { foo: "bar" })).toBe(true);
|
||||
expect(evaluateExpr("foo == 'foo' and bar == 'bar'", { foo: "foo", bar: "bar" })).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("should be lazy", () => {
|
||||
// second clause should nameerror if evaluated
|
||||
expect(() => evaluateExpr("foo == 'foo' and bar == 'bar'", { foo: "foo" })).toThrow();
|
||||
expect(evaluateExpr("foo == 'foo' and bar == 'bar'", { foo: "bar" })).toBe(false);
|
||||
expect(evaluateExpr("foo == 'foo' or bar == 'bar'", { foo: "foo" })).toBe(true);
|
||||
});
|
||||
|
||||
test("should return the actual object", () => {
|
||||
expect(evaluateExpr('"foo" or "bar"')).toBe("foo");
|
||||
expect(evaluateExpr('None or "bar"')).toBe("bar");
|
||||
expect(evaluateExpr("False or None")).toBe(null);
|
||||
expect(evaluateExpr("0 or 1")).toBe(1);
|
||||
expect(evaluateExpr("[] or False")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("values from context", () => {
|
||||
test("free variable", () => {
|
||||
expect(evaluateExpr("a", { a: 3 })).toBe(3);
|
||||
expect(evaluateExpr("a + b", { a: 3, b: 5 })).toBe(8);
|
||||
expect(evaluateExpr("a", { a: true })).toBe(true);
|
||||
expect(evaluateExpr("a", { a: false })).toBe(false);
|
||||
expect(evaluateExpr("a", { a: null })).toBe(null);
|
||||
expect(evaluateExpr("a", { a: "bar" })).toBe("bar");
|
||||
expect(evaluateExpr("foo", { foo: [1, 2, 3] })).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test("special case for context: the eval context can be accessed as 'context'", () => {
|
||||
expect(evaluateExpr("context.get('b', 54)", { b: 3 })).toBe(3);
|
||||
expect(evaluateExpr("context.get('c', 54)", { b: 3 })).toBe(54);
|
||||
});
|
||||
|
||||
test("true and false available in context", () => {
|
||||
expect(evaluateExpr("true")).toBe(true);
|
||||
expect(evaluateExpr("false")).toBe(false);
|
||||
});
|
||||
|
||||
test("throw error if name is not defined", () => {
|
||||
expect(() => evaluateExpr("a")).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("comparisons", () => {
|
||||
test("equality", () => {
|
||||
expect(evaluateExpr("1 == 1")).toBe(true);
|
||||
expect(evaluateExpr('"foo" == "foo"')).toBe(true);
|
||||
expect(evaluateExpr('"foo" == "bar"')).toBe(false);
|
||||
expect(evaluateExpr("1 == True")).toBe(true);
|
||||
expect(evaluateExpr("True == 1")).toBe(true);
|
||||
expect(evaluateExpr("1 == False")).toBe(false);
|
||||
expect(evaluateExpr("False == 1")).toBe(false);
|
||||
expect(evaluateExpr("0 == False")).toBe(true);
|
||||
expect(evaluateExpr("False == 0")).toBe(true);
|
||||
expect(evaluateExpr("None == None")).toBe(true);
|
||||
expect(evaluateExpr("None == False")).toBe(false);
|
||||
});
|
||||
|
||||
test("equality should work with free variables", () => {
|
||||
expect(evaluateExpr("1 == a", { a: 1 })).toBe(true);
|
||||
expect(evaluateExpr('foo == "bar"', { foo: "bar" })).toBe(true);
|
||||
expect(evaluateExpr('foo == "bar"', { foo: "qux" })).toBe(false);
|
||||
});
|
||||
|
||||
test("inequality", () => {
|
||||
expect(evaluateExpr("1 != 2")).toBe(true);
|
||||
expect(evaluateExpr('"foo" != "foo"')).toBe(false);
|
||||
expect(evaluateExpr('"foo" != "bar"')).toBe(true);
|
||||
});
|
||||
|
||||
test("inequality should work with free variables", () => {
|
||||
expect(evaluateExpr("1 != a", { a: 42 })).toBe(true);
|
||||
expect(evaluateExpr('foo != "bar"', { foo: "bar" })).toBe(false);
|
||||
expect(evaluateExpr('foo != "bar"', { foo: "qux" })).toBe(true);
|
||||
expect(evaluateExpr("foo != bar", { foo: "qux", bar: "quux" })).toBe(true);
|
||||
});
|
||||
|
||||
test("should accept deprecated form", () => {
|
||||
expect(evaluateExpr("1 <> 2")).toBe(true);
|
||||
expect(evaluateExpr('"foo" <> "foo"')).toBe(false);
|
||||
expect(evaluateExpr('"foo" <> "bar"')).toBe(true);
|
||||
});
|
||||
|
||||
test("comparing numbers", () => {
|
||||
expect(evaluateExpr("3 < 5")).toBe(true);
|
||||
expect(evaluateExpr("3 > 5")).toBe(false);
|
||||
expect(evaluateExpr("5 >= 3")).toBe(true);
|
||||
expect(evaluateExpr("3 >= 3")).toBe(true);
|
||||
expect(evaluateExpr("3 <= 5")).toBe(true);
|
||||
expect(evaluateExpr("5 <= 3")).toBe(false);
|
||||
});
|
||||
|
||||
test("should support comparison chains", () => {
|
||||
expect(evaluateExpr("1 < 3 < 5")).toBe(true);
|
||||
expect(evaluateExpr("5 > 3 > 1")).toBe(true);
|
||||
expect(evaluateExpr("1 < 3 > 2 == 2 > -2")).toBe(true);
|
||||
expect(evaluateExpr("1 < 2 < 3 < 4 < 5 < 6")).toBe(true);
|
||||
});
|
||||
|
||||
test("should compare strings", () => {
|
||||
expect(evaluateExpr("date >= current", { date: "2010-06-08", current: "2010-06-05" })).toBe(
|
||||
true
|
||||
);
|
||||
expect(evaluateExpr('state >= "cancel"', { state: "cancel" })).toBe(true);
|
||||
expect(evaluateExpr('state >= "cancel"', { state: "open" })).toBe(true);
|
||||
});
|
||||
|
||||
test("mixed types comparisons", () => {
|
||||
expect(evaluateExpr("None < 42")).toBe(true);
|
||||
expect(evaluateExpr("None > 42")).toBe(false);
|
||||
expect(evaluateExpr("42 > None")).toBe(true);
|
||||
expect(evaluateExpr("None < False")).toBe(true);
|
||||
expect(evaluateExpr("None < True")).toBe(true);
|
||||
expect(evaluateExpr("False > None")).toBe(true);
|
||||
expect(evaluateExpr("True > None")).toBe(true);
|
||||
expect(evaluateExpr("None > False")).toBe(false);
|
||||
expect(evaluateExpr("None > True")).toBe(false);
|
||||
expect(evaluateExpr("0 > True")).toBe(false);
|
||||
expect(evaluateExpr("0 < True")).toBe(true);
|
||||
expect(evaluateExpr("1 <= True")).toBe(true);
|
||||
expect(evaluateExpr('False < ""')).toBe(true);
|
||||
expect(evaluateExpr('"" > False')).toBe(true);
|
||||
expect(evaluateExpr('False > ""')).toBe(false);
|
||||
expect(evaluateExpr('0 < ""')).toBe(true);
|
||||
expect(evaluateExpr('"" > 0')).toBe(true);
|
||||
expect(evaluateExpr('0 > ""')).toBe(false);
|
||||
expect(evaluateExpr("3 < True")).toBe(false);
|
||||
expect(evaluateExpr("3 > True")).toBe(true);
|
||||
expect(evaluateExpr("{} > None")).toBe(true);
|
||||
expect(evaluateExpr("{} < None")).toBe(false);
|
||||
expect(evaluateExpr("{} > False")).toBe(true);
|
||||
expect(evaluateExpr("{} < False")).toBe(false);
|
||||
expect(evaluateExpr("3 < 'foo'")).toBe(true);
|
||||
expect(evaluateExpr("'foo' < 4444")).toBe(false);
|
||||
expect(evaluateExpr("{} < []")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("containment", () => {
|
||||
test("in tuples", () => {
|
||||
expect(evaluateExpr("'bar' in ('foo', 'bar')")).toBe(true);
|
||||
expect(evaluateExpr("'bar' in ('foo', 'qux')")).toBe(false);
|
||||
expect(evaluateExpr("1 in (1,2,3,4)")).toBe(true);
|
||||
expect(evaluateExpr("1 in (2,3,4)")).toBe(false);
|
||||
expect(evaluateExpr("'url' in ('url',)")).toBe(true);
|
||||
expect(evaluateExpr("'ur' in ('url',)")).toBe(false);
|
||||
expect(evaluateExpr("'url' in ('url', 'foo', 'bar')")).toBe(true);
|
||||
});
|
||||
|
||||
test("in strings", () => {
|
||||
expect(evaluateExpr("'bar' in 'bar'")).toBe(true);
|
||||
expect(evaluateExpr("'bar' in 'foobar'")).toBe(true);
|
||||
expect(evaluateExpr("'bar' in 'fooqux'")).toBe(false);
|
||||
});
|
||||
|
||||
test("in lists", () => {
|
||||
expect(evaluateExpr("'bar' in ['foo', 'bar']")).toBe(true);
|
||||
expect(evaluateExpr("'bar' in ['foo', 'qux']")).toBe(false);
|
||||
expect(evaluateExpr("3 in [1,2,3]")).toBe(true);
|
||||
expect(evaluateExpr("None in [1,'foo',None]")).toBe(true);
|
||||
expect(evaluateExpr("not a in b", { a: 3, b: [1, 2, 4, 8] })).toBe(true);
|
||||
});
|
||||
|
||||
test("not in", () => {
|
||||
expect(evaluateExpr("1 not in (2,3,4)")).toBe(true);
|
||||
expect(evaluateExpr('"ur" not in ("url",)')).toBe(true);
|
||||
expect(evaluateExpr("-2 not in (1,2,3)")).toBe(true);
|
||||
expect(evaluateExpr("-2 not in (1,-2,3)")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("conversions", () => {
|
||||
test("to bool", () => {
|
||||
expect(evaluateExpr("bool('')")).toBe(false);
|
||||
expect(evaluateExpr("bool('foo')")).toBe(true);
|
||||
expect(evaluateExpr("bool(date_deadline)", { date_deadline: "2008" })).toBe(true);
|
||||
expect(evaluateExpr("bool(s)", { s: "" })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("callables", () => {
|
||||
test("should not call function from context", () => {
|
||||
expect(() => evaluateExpr("foo()", { foo: () => 3 })).toThrow();
|
||||
expect(() => evaluateExpr("1 + foo()", { foo: () => 3 })).toThrow();
|
||||
});
|
||||
test("min/max", () => {
|
||||
expect(evaluateExpr("max(3, 5)")).toBe(5);
|
||||
expect(evaluateExpr("min(3, 5, 2, 7)")).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dicts", () => {
|
||||
test("dict", () => {
|
||||
expect(evaluateExpr("{}")).toEqual({});
|
||||
expect(evaluateExpr("{'foo': 1 + 2}")).toEqual({ foo: 3 });
|
||||
expect(evaluateExpr("{'foo': 1, 'bar': 4}")).toEqual({ foo: 1, bar: 4 });
|
||||
});
|
||||
|
||||
test("lookup and definition", () => {
|
||||
expect(evaluateExpr("{'a': 1}['a']")).toBe(1);
|
||||
expect(evaluateExpr("{1: 2}[1]")).toBe(2);
|
||||
});
|
||||
|
||||
test("can get values with get method", () => {
|
||||
expect(evaluateExpr("{'a': 1}.get('a')")).toBe(1);
|
||||
expect(evaluateExpr("{'a': 1}.get('b')")).toBe(null);
|
||||
expect(evaluateExpr("{'a': 1}.get('b', 54)")).toBe(54);
|
||||
});
|
||||
|
||||
test("can get values from values 'context'", () => {
|
||||
expect(evaluateExpr("context.get('a')", { context: { a: 123 } })).toBe(123);
|
||||
const values = { context: { a: { b: { c: 321 } } } };
|
||||
expect(evaluateExpr("context.get('a').b.c", values)).toBe(321);
|
||||
expect(evaluateExpr("context.get('a', {'e': 5}).b.c", values)).toBe(321);
|
||||
expect(evaluateExpr("context.get('d', 3)", values)).toBe(3);
|
||||
expect(evaluateExpr("context.get('d', {'e': 5})['e']", values)).toBe(5);
|
||||
});
|
||||
|
||||
test("can check if a key is in the 'context'", () => {
|
||||
expect(evaluateExpr("'a' in context", { context: { a: 123 } })).toBe(true);
|
||||
expect(evaluateExpr("'a' in context", { context: { b: 123 } })).toBe(false);
|
||||
expect(evaluateExpr("'a' not in context", { context: { a: 123 } })).toBe(false);
|
||||
expect(evaluateExpr("'a' not in context", { context: { b: 123 } })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("objects", () => {
|
||||
test("can read values from object", () => {
|
||||
expect(evaluateExpr("obj.a", { obj: { a: 123 } })).toBe(123);
|
||||
expect(evaluateExpr("obj.a.b.c", { obj: { a: { b: { c: 321 } } } })).toBe(321);
|
||||
});
|
||||
|
||||
test("cannot call function in object", () => {
|
||||
expect(() => evaluateExpr("obj.f(3)", { obj: { f: (n) => n + 1 } })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("if expressions", () => {
|
||||
test("simple if expressions", () => {
|
||||
expect(evaluateExpr("1 if True else 2")).toBe(1);
|
||||
expect(evaluateExpr("1 if 3 < 2 else 'greater'")).toBe("greater");
|
||||
});
|
||||
|
||||
test("only evaluate proper branch", () => {
|
||||
// will throw if evaluate wrong branch => name error
|
||||
expect(evaluateExpr("1 if True else boom")).toBe(1);
|
||||
expect(evaluateExpr("boom if False else 222")).toBe(222);
|
||||
});
|
||||
});
|
||||
|
||||
describe("miscellaneous expressions", () => {
|
||||
test("tuple in list", () => {
|
||||
expect(evaluateExpr("[(1 + 2,'foo', True)]")).toEqual([[3, "foo", true]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("evaluate to boolean", () => {
|
||||
test("simple expression", () => {
|
||||
expect(evaluateBooleanExpr("12")).toBe(true);
|
||||
expect(evaluateBooleanExpr("0")).toBe(false);
|
||||
expect(evaluateBooleanExpr("0 + 3 - 1")).toBe(true);
|
||||
expect(evaluateBooleanExpr("0 + 3 - 1 - 2")).toBe(false);
|
||||
expect(evaluateBooleanExpr('"foo"')).toBe(true);
|
||||
expect(evaluateBooleanExpr("[1]")).toBe(true);
|
||||
expect(evaluateBooleanExpr("[]")).toBe(false);
|
||||
});
|
||||
|
||||
test("use contextual values", () => {
|
||||
expect(evaluateBooleanExpr("a", { a: 12 })).toBe(true);
|
||||
expect(evaluateBooleanExpr("a", { a: 0 })).toBe(false);
|
||||
expect(evaluateBooleanExpr("0 + 3 - a", { a: 1 })).toBe(true);
|
||||
expect(evaluateBooleanExpr("0 + 3 - a - 2", { a: 1 })).toBe(false);
|
||||
expect(evaluateBooleanExpr("0 + 3 - a - b", { a: 1, b: 2 })).toBe(false);
|
||||
expect(evaluateBooleanExpr("a", { a: "foo" })).toBe(true);
|
||||
expect(evaluateBooleanExpr("a", { a: [1] })).toBe(true);
|
||||
expect(evaluateBooleanExpr("a", { a: [] })).toBe(false);
|
||||
});
|
||||
|
||||
test("throw if has missing value", () => {
|
||||
expect(() => evaluateBooleanExpr("a", { b: 0 })).toThrow();
|
||||
expect(evaluateBooleanExpr("1 or a")).toBe(true); // do not throw (lazy value)
|
||||
expect(() => evaluateBooleanExpr("0 or a")).toThrow();
|
||||
expect(() => evaluateBooleanExpr("a or b", { b: true })).toThrow();
|
||||
expect(() => evaluateBooleanExpr("a and b", { b: true })).toThrow();
|
||||
expect(() => evaluateBooleanExpr("a()")).toThrow();
|
||||
expect(() => evaluateBooleanExpr("a[0]")).toThrow();
|
||||
expect(() => evaluateBooleanExpr("a.b")).toThrow();
|
||||
expect(() => evaluateBooleanExpr("0 + 3 - a", { b: 1 })).toThrow();
|
||||
expect(() => evaluateBooleanExpr("0 + 3 - a - 2", { b: 1 })).toThrow();
|
||||
expect(() => evaluateBooleanExpr("0 + 3 - a - b", { b: 2 })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sets", () => {
|
||||
test("static set", () => {
|
||||
expect(evaluateExpr("set()")).toEqual(new Set());
|
||||
expect(evaluateExpr("set([])")).toEqual(new Set([]));
|
||||
expect(evaluateExpr("set([0])")).toEqual(new Set([0]));
|
||||
expect(evaluateExpr("set([1])")).toEqual(new Set([1]));
|
||||
expect(evaluateExpr("set([0, 0])")).toEqual(new Set([0]));
|
||||
expect(evaluateExpr("set([0, 1])")).toEqual(new Set([0, 1]));
|
||||
expect(evaluateExpr("set([1, 1])")).toEqual(new Set([1]));
|
||||
|
||||
expect(evaluateExpr("set('')")).toEqual(new Set());
|
||||
expect(evaluateExpr("set('a')")).toEqual(new Set(["a"]));
|
||||
expect(evaluateExpr("set('ab')")).toEqual(new Set(["a", "b"]));
|
||||
|
||||
expect(evaluateExpr("set({})")).toEqual(new Set());
|
||||
expect(evaluateExpr("set({ 'a': 1 })")).toEqual(new Set(["a"]));
|
||||
expect(evaluateExpr("set({ '': 1, 'a': 1 })")).toEqual(new Set(["", "a"]));
|
||||
|
||||
expect(() => evaluateExpr("set(0)")).toThrow();
|
||||
expect(() => evaluateExpr("set(1)")).toThrow();
|
||||
expect(() => evaluateExpr("set(None)")).toThrow();
|
||||
expect(() => evaluateExpr("set(false)")).toThrow();
|
||||
expect(() => evaluateExpr("set(true)")).toThrow();
|
||||
expect(() => evaluateExpr("set(1, 2)")).toThrow();
|
||||
|
||||
expect(() => evaluateExpr("set(expr)", { expr: undefined })).toThrow();
|
||||
expect(() => evaluateExpr("set(expr)", { expr: null })).toThrow();
|
||||
|
||||
expect(() => evaluateExpr("set([], [])")).toThrow(); // valid but not supported by py_js
|
||||
expect(() => evaluateExpr("set({ 'a' })")).toThrow(); // valid but not supported by py_js
|
||||
});
|
||||
|
||||
test("set intersection", () => {
|
||||
expect(evaluateExpr("set([1,2,3]).intersection()")).toEqual(new Set([1, 2, 3]));
|
||||
expect(evaluateExpr("set([1,2,3]).intersection(set([2,3]))")).toEqual(new Set([2, 3]));
|
||||
expect(evaluateExpr("set([1,2,3]).intersection([2,3])")).toEqual(new Set([2, 3]));
|
||||
expect(evaluateExpr("set([1,2,3]).intersection(r)", { r: [2, 3] })).toEqual(
|
||||
new Set([2, 3])
|
||||
);
|
||||
expect(evaluateExpr("r.intersection([2,3])", { r: new Set([1, 2, 3, 2]) })).toEqual(
|
||||
new Set([2, 3])
|
||||
);
|
||||
|
||||
expect(evaluateExpr("set(foo_ids).intersection([2,3])", { foo_ids: [1, 2] })).toEqual(
|
||||
new Set([2])
|
||||
);
|
||||
expect(evaluateExpr("set(foo_ids).intersection([2,3])", { foo_ids: [1] })).toEqual(
|
||||
new Set()
|
||||
);
|
||||
expect(evaluateExpr("set([foo_id]).intersection([2,3])", { foo_id: 1 })).toEqual(new Set());
|
||||
expect(evaluateExpr("set([foo_id]).intersection([2,3])", { foo_id: 2 })).toEqual(
|
||||
new Set([2])
|
||||
);
|
||||
|
||||
expect(() => evaluateExpr("set([]).intersection([], [])")).toThrow(); // valid but not supported by py_js
|
||||
expect(() => evaluateExpr("set([]).intersection([], [], [])")).toThrow(); // valid but not supported by py_js
|
||||
});
|
||||
|
||||
test("set difference", () => {
|
||||
expect(evaluateExpr("set([1,2,3]).difference()")).toEqual(new Set([1, 2, 3]));
|
||||
expect(evaluateExpr("set([1,2,3]).difference(set([2,3]))")).toEqual(new Set([1]));
|
||||
expect(evaluateExpr("set([1,2,3]).difference([2,3])")).toEqual(new Set([1]));
|
||||
expect(evaluateExpr("set([1,2,3]).difference(r)", { r: [2, 3] })).toEqual(new Set([1]));
|
||||
expect(evaluateExpr("r.difference([2,3])", { r: new Set([1, 2, 3, 2, 4]) })).toEqual(
|
||||
new Set([1, 4])
|
||||
);
|
||||
|
||||
expect(evaluateExpr("set(foo_ids).difference([2,3])", { foo_ids: [1, 2] })).toEqual(
|
||||
new Set([1])
|
||||
);
|
||||
expect(evaluateExpr("set(foo_ids).difference([2,3])", { foo_ids: [1] })).toEqual(
|
||||
new Set([1])
|
||||
);
|
||||
expect(evaluateExpr("set([foo_id]).difference([2,3])", { foo_id: 1 })).toEqual(
|
||||
new Set([1])
|
||||
);
|
||||
expect(evaluateExpr("set([foo_id]).difference([2,3])", { foo_id: 2 })).toEqual(new Set());
|
||||
|
||||
expect(() => evaluateExpr("set([]).difference([], [])")).toThrow(); // valid but not supported by py_js
|
||||
expect(() => evaluateExpr("set([]).difference([], [], [])")).toThrow(); // valid but not supported by py_js
|
||||
});
|
||||
|
||||
test("set union", () => {
|
||||
expect(evaluateExpr("set([1,2,3]).union()")).toEqual(new Set([1, 2, 3]));
|
||||
expect(evaluateExpr("set([1,2,3]).union(set([2,3,4]))")).toEqual(new Set([1, 2, 3, 4]));
|
||||
expect(evaluateExpr("set([1,2,3]).union([2,4])")).toEqual(new Set([1, 2, 3, 4]));
|
||||
expect(evaluateExpr("set([1,2,3]).union(r)", { r: [2, 4] })).toEqual(new Set([1, 2, 3, 4]));
|
||||
expect(evaluateExpr("r.union([2,3])", { r: new Set([1, 2, 2, 4]) })).toEqual(
|
||||
new Set([1, 2, 4, 3])
|
||||
);
|
||||
|
||||
expect(evaluateExpr("set(foo_ids).union([2,3])", { foo_ids: [1, 2] })).toEqual(
|
||||
new Set([1, 2, 3])
|
||||
);
|
||||
expect(evaluateExpr("set(foo_ids).union([2,3])", { foo_ids: [1] })).toEqual(
|
||||
new Set([1, 2, 3])
|
||||
);
|
||||
expect(evaluateExpr("set([foo_id]).union([2,3])", { foo_id: 1 })).toEqual(
|
||||
new Set([1, 2, 3])
|
||||
);
|
||||
expect(evaluateExpr("set([foo_id]).union([2,3])", { foo_id: 2 })).toEqual(new Set([2, 3]));
|
||||
|
||||
expect(() => evaluateExpr("set([]).union([], [])")).toThrow(); // valid but not supported by py_js
|
||||
expect(() => evaluateExpr("set([]).union([], [], [])")).toThrow(); // valid but not supported by py_js
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
|
||||
import { parseExpr } from "@web/core/py_js/py";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("can parse basic elements", () => {
|
||||
expect(parseExpr("1")).toEqual({ type: 0 /* Number */, value: 1 });
|
||||
expect(parseExpr('"foo"')).toEqual({ type: 1 /* String */, value: "foo" });
|
||||
expect(parseExpr("foo")).toEqual({ type: 5 /* Name */, value: "foo" });
|
||||
expect(parseExpr("True")).toEqual({ type: 2 /* Boolean */, value: true });
|
||||
expect(parseExpr("False")).toEqual({ type: 2 /* Boolean */, value: false });
|
||||
expect(parseExpr("None")).toEqual({ type: 3 /* None */ });
|
||||
});
|
||||
|
||||
test("cannot parse empty string", () => {
|
||||
expect(() => parseExpr("")).toThrow(/Error: Missing token/);
|
||||
});
|
||||
|
||||
test("can parse unary operator -", () => {
|
||||
expect(parseExpr("-1")).toEqual({
|
||||
type: 6 /* UnaryOperator */,
|
||||
op: "-",
|
||||
right: { type: 0 /* Number */, value: 1 },
|
||||
});
|
||||
expect(parseExpr("-foo")).toEqual({
|
||||
type: 6 /* UnaryOperator */,
|
||||
op: "-",
|
||||
right: { type: 5 /* Name */, value: "foo" },
|
||||
});
|
||||
expect(parseExpr("not True")).toEqual({
|
||||
type: 6 /* UnaryOperator */,
|
||||
op: "not",
|
||||
right: { type: 2 /* Boolean */, value: true },
|
||||
});
|
||||
});
|
||||
|
||||
test("can parse parenthesis", () => {
|
||||
expect(parseExpr("(1 + 2)")).toEqual({
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "+",
|
||||
left: { type: 0 /* Number */, value: 1 },
|
||||
right: { type: 0 /* Number */, value: 2 },
|
||||
});
|
||||
});
|
||||
|
||||
test("can parse binary operators", () => {
|
||||
expect(parseExpr("1 < 2")).toEqual({
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "<",
|
||||
left: { type: 0 /* Number */, value: 1 },
|
||||
right: { type: 0 /* Number */, value: 2 },
|
||||
});
|
||||
expect(parseExpr('a + "foo"')).toEqual({
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "+",
|
||||
left: { type: 5 /* Name */, value: "a" },
|
||||
right: { type: 1 /* String */, value: "foo" },
|
||||
});
|
||||
});
|
||||
|
||||
test("can parse boolean operators", () => {
|
||||
expect(parseExpr('True and "foo"')).toEqual({
|
||||
type: 14 /* BooleanOperator */,
|
||||
op: "and",
|
||||
left: { type: 2 /* Boolean */, value: true },
|
||||
right: { type: 1 /* String */, value: "foo" },
|
||||
});
|
||||
expect(parseExpr('True or "foo"')).toEqual({
|
||||
type: 14 /* BooleanOperator */,
|
||||
op: "or",
|
||||
left: { type: 2 /* Boolean */, value: true },
|
||||
right: { type: 1 /* String */, value: "foo" },
|
||||
});
|
||||
});
|
||||
|
||||
test("expression with == and or", () => {
|
||||
expect(parseExpr("False == True and False")).toEqual({
|
||||
type: 14 /* BooleanOperator */,
|
||||
op: "and",
|
||||
left: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "==",
|
||||
left: { type: 2 /* Boolean */, value: false },
|
||||
right: { type: 2 /* Boolean */, value: true },
|
||||
},
|
||||
right: { type: 2 /* Boolean */, value: false },
|
||||
});
|
||||
});
|
||||
|
||||
test("expression with + and ==", () => {
|
||||
expect(parseExpr("1 + 2 == 3")).toEqual({
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "==",
|
||||
left: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "+",
|
||||
left: { type: 0 /* Number */, value: 1 },
|
||||
right: { type: 0 /* Number */, value: 2 },
|
||||
},
|
||||
right: { type: 0 /* Number */, value: 3 },
|
||||
});
|
||||
});
|
||||
|
||||
test("can parse chained comparisons", () => {
|
||||
expect(parseExpr("1 < 2 <= 3")).toEqual({
|
||||
type: 14 /* BooleanOperator */,
|
||||
op: "and",
|
||||
left: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "<",
|
||||
left: { type: 0 /* Number */, value: 1 },
|
||||
right: { type: 0 /* Number */, value: 2 },
|
||||
},
|
||||
right: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "<=",
|
||||
left: { type: 0 /* Number */, value: 2 },
|
||||
right: { type: 0 /* Number */, value: 3 },
|
||||
},
|
||||
});
|
||||
expect(parseExpr("1 < 2 <= 3 > 33")).toEqual({
|
||||
type: 14 /* BooleanOperator */,
|
||||
op: "and",
|
||||
left: {
|
||||
type: 14 /* BooleanOperator */,
|
||||
op: "and",
|
||||
left: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "<",
|
||||
left: { type: 0 /* Number */, value: 1 },
|
||||
right: { type: 0 /* Number */, value: 2 },
|
||||
},
|
||||
right: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "<=",
|
||||
left: { type: 0 /* Number */, value: 2 },
|
||||
right: { type: 0 /* Number */, value: 3 },
|
||||
},
|
||||
},
|
||||
right: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: ">",
|
||||
left: { type: 0 /* Number */, value: 3 },
|
||||
right: { type: 0 /* Number */, value: 33 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("can parse lists", () => {
|
||||
expect(parseExpr("[]")).toEqual({
|
||||
type: 4 /* List */,
|
||||
value: [],
|
||||
});
|
||||
expect(parseExpr("[1]")).toEqual({
|
||||
type: 4 /* List */,
|
||||
value: [{ type: 0 /* Number */, value: 1 }],
|
||||
});
|
||||
expect(parseExpr("[1,]")).toEqual({
|
||||
type: 4 /* List */,
|
||||
value: [{ type: 0 /* Number */, value: 1 }],
|
||||
});
|
||||
expect(parseExpr("[1, 4]")).toEqual({
|
||||
type: 4 /* List */,
|
||||
value: [
|
||||
{ type: 0 /* Number */, value: 1 },
|
||||
{ type: 0 /* Number */, value: 4 },
|
||||
],
|
||||
});
|
||||
expect(() => parseExpr("[1 1]")).toThrow();
|
||||
});
|
||||
|
||||
test("can parse lists lookup", () => {
|
||||
expect(parseExpr("[1,2][1]")).toEqual({
|
||||
type: 12 /* Lookup */,
|
||||
target: {
|
||||
type: 4 /* List */,
|
||||
value: [
|
||||
{ type: 0 /* Number */, value: 1 },
|
||||
{ type: 0 /* Number */, value: 2 },
|
||||
],
|
||||
},
|
||||
key: { type: 0 /* Number */, value: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
test("can parse tuples", () => {
|
||||
expect(parseExpr("()")).toEqual({
|
||||
type: 10 /* Tuple */,
|
||||
value: [],
|
||||
});
|
||||
expect(parseExpr("(1,)")).toEqual({
|
||||
type: 10 /* Tuple */,
|
||||
value: [{ type: 0 /* Number */, value: 1 }],
|
||||
});
|
||||
expect(parseExpr("(1,4)")).toEqual({
|
||||
type: 10 /* Tuple */,
|
||||
value: [
|
||||
{ type: 0 /* Number */, value: 1 },
|
||||
{ type: 0 /* Number */, value: 4 },
|
||||
],
|
||||
});
|
||||
expect(() => parseExpr("(1 1)")).toThrow();
|
||||
});
|
||||
|
||||
test("can parse dictionary", () => {
|
||||
expect(parseExpr("{}")).toEqual({
|
||||
type: 11 /* Dictionary */,
|
||||
value: {},
|
||||
});
|
||||
expect(parseExpr("{'foo': 1}")).toEqual({
|
||||
type: 11 /* Dictionary */,
|
||||
value: { foo: { type: 0 /* Number */, value: 1 } },
|
||||
});
|
||||
expect(parseExpr("{'foo': 1, 'bar': 3}")).toEqual({
|
||||
type: 11 /* Dictionary */,
|
||||
value: {
|
||||
foo: { type: 0 /* Number */, value: 1 },
|
||||
bar: { type: 0 /* Number */, value: 3 },
|
||||
},
|
||||
});
|
||||
expect(parseExpr("{1: 2}")).toEqual({
|
||||
type: 11 /* Dictionary */,
|
||||
value: { 1: { type: 0 /* Number */, value: 2 } },
|
||||
});
|
||||
});
|
||||
|
||||
test("can parse dictionary lookup", () => {
|
||||
expect(parseExpr("{}['a']")).toEqual({
|
||||
type: 12 /* Lookup */,
|
||||
target: { type: 11 /* Dictionary */, value: {} },
|
||||
key: { type: 1 /* String */, value: "a" },
|
||||
});
|
||||
});
|
||||
|
||||
test("can parse assignment", () => {
|
||||
expect(parseExpr("a=1")).toEqual({
|
||||
type: 9 /* Assignment */,
|
||||
name: { type: 5 /* Name */, value: "a" },
|
||||
value: { type: 0 /* Number */, value: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
test("can parse function calls", () => {
|
||||
expect(parseExpr("f()")).toEqual({
|
||||
type: 8 /* FunctionCall */,
|
||||
fn: { type: 5 /* Name */, value: "f" },
|
||||
args: [],
|
||||
kwargs: {},
|
||||
});
|
||||
expect(parseExpr("f() + 2")).toEqual({
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "+",
|
||||
left: {
|
||||
type: 8 /* FunctionCall */,
|
||||
fn: { type: 5 /* Name */, value: "f" },
|
||||
args: [],
|
||||
kwargs: {},
|
||||
},
|
||||
right: { type: 0 /* Number */, value: 2 },
|
||||
});
|
||||
expect(parseExpr("f(1)")).toEqual({
|
||||
type: 8 /* FunctionCall */,
|
||||
fn: { type: 5 /* Name */, value: "f" },
|
||||
args: [{ type: 0 /* Number */, value: 1 }],
|
||||
kwargs: {},
|
||||
});
|
||||
expect(parseExpr("f(1, 2)")).toEqual({
|
||||
type: 8 /* FunctionCall */,
|
||||
fn: { type: 5 /* Name */, value: "f" },
|
||||
args: [
|
||||
{ type: 0 /* Number */, value: 1 },
|
||||
{ type: 0 /* Number */, value: 2 },
|
||||
],
|
||||
kwargs: {},
|
||||
});
|
||||
});
|
||||
|
||||
test("can parse function calls with kwargs", () => {
|
||||
expect(parseExpr("f(a = 1)")).toEqual({
|
||||
type: 8 /* FunctionCall */,
|
||||
fn: { type: 5 /* Name */, value: "f" },
|
||||
args: [],
|
||||
kwargs: { a: { type: 0 /* Number */, value: 1 } },
|
||||
});
|
||||
expect(parseExpr("f(3, a = 1)")).toEqual({
|
||||
type: 8 /* FunctionCall */,
|
||||
fn: { type: 5 /* Name */, value: "f" },
|
||||
args: [{ type: 0 /* Number */, value: 3 }],
|
||||
kwargs: { a: { type: 0 /* Number */, value: 1 } },
|
||||
});
|
||||
});
|
||||
|
||||
test("can parse not a in b", () => {
|
||||
expect(parseExpr("not a in b")).toEqual({
|
||||
type: 6 /* UnaryOperator */,
|
||||
op: "not",
|
||||
right: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "in",
|
||||
left: { type: 5 /* Name */, value: "a" },
|
||||
right: { type: 5 /* Name */, value: "b" },
|
||||
},
|
||||
});
|
||||
expect(parseExpr("a.b.c")).toEqual({
|
||||
type: 15 /* ObjLookup */,
|
||||
obj: {
|
||||
type: 15 /* ObjLookup */,
|
||||
obj: { type: 5 /* Name */, value: "a" },
|
||||
key: "b",
|
||||
},
|
||||
key: "c",
|
||||
});
|
||||
});
|
||||
|
||||
test("can parse if statement", () => {
|
||||
expect(parseExpr("1 if True else 2")).toEqual({
|
||||
type: 13 /* If */,
|
||||
condition: { type: 2 /* Boolean */, value: true },
|
||||
ifTrue: { type: 0 /* Number */, value: 1 },
|
||||
ifFalse: { type: 0 /* Number */, value: 2 },
|
||||
});
|
||||
expect(parseExpr("1 + 1 if True else 2")).toEqual({
|
||||
type: 13 /* If */,
|
||||
condition: { type: 2 /* Boolean */, value: true },
|
||||
ifTrue: {
|
||||
type: 7 /* BinaryOperator */,
|
||||
op: "+",
|
||||
left: { type: 0 /* Number */, value: 1 },
|
||||
right: { type: 0 /* Number */, value: 1 },
|
||||
},
|
||||
ifFalse: { type: 0 /* Number */, value: 2 },
|
||||
});
|
||||
});
|
||||
|
||||
test("tuple in list", () => {
|
||||
expect(parseExpr("[(1,2)]")).toEqual({
|
||||
type: 4 /* List */,
|
||||
value: [
|
||||
{
|
||||
type: 10 /* Tuple */,
|
||||
value: [
|
||||
{ type: 0 /* Number */, value: 1 },
|
||||
{ type: 0 /* Number */, value: 2 },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("cannot parse []a", () => {
|
||||
expect(() => parseExpr("[]a")).toThrow(/Error: Token\(s\) unused/);
|
||||
expect(() => parseExpr("[]a b")).toThrow(/Error: Token\(s\) unused/);
|
||||
});
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
import { PyTimeDelta } from "@web/core/py_js/py_date";
|
||||
|
||||
const expectDelta = (expr, res) => {
|
||||
const timedelta = evaluateExpr(expr, { td: PyTimeDelta });
|
||||
expect(`${timedelta.days}, ${timedelta.seconds}, ${timedelta.microseconds}`).toBe(res);
|
||||
};
|
||||
|
||||
const expectEquality = (expr1, expr2, ctx) => {
|
||||
const equality = `${expr1} == ${expr2}`;
|
||||
expect(evaluateExpr(equality, Object.assign({ td: PyTimeDelta }, ctx))).toBe(true, {
|
||||
message: `evaluating ${equality}`,
|
||||
});
|
||||
};
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("create", () => {
|
||||
expectDelta("td(weeks=1)", "7, 0, 0");
|
||||
expectDelta("td(days=1)", "1, 0, 0");
|
||||
expectDelta("td(hours=1)", "0, 3600, 0");
|
||||
expectDelta("td(minutes=1)", "0, 60, 0");
|
||||
expectDelta("td(seconds=1)", "0, 1, 0");
|
||||
expectDelta("td(milliseconds=1)", "0, 0, 1000");
|
||||
expectDelta("td(microseconds=1)", "0, 0, 1");
|
||||
|
||||
expectDelta("td(days=-1.25)", "-2, 64800, 0");
|
||||
expectDelta("td(seconds=129600.4)", "1, 43200, 400000");
|
||||
expectDelta("td(hours=24.5,milliseconds=1400)", "1, 1801, 400000");
|
||||
|
||||
expectEquality(
|
||||
"td()",
|
||||
"td(weeks=0, days=0, hours=0, minutes=0, seconds=0, milliseconds=0, microseconds=0)"
|
||||
);
|
||||
expectEquality("td(1)", "td(days=1)");
|
||||
expectEquality("td(0, 1)", "td(seconds=1)");
|
||||
expectEquality("td(0, 0, 1)", "td(microseconds=1)");
|
||||
expectEquality("td(weeks=1)", "td(days=7)");
|
||||
expectEquality("td(days=1)", "td(hours=24)");
|
||||
expectEquality("td(hours=1)", "td(minutes=60)");
|
||||
expectEquality("td(minutes=1)", "td(seconds=60)");
|
||||
expectEquality("td(seconds=1)", "td(milliseconds=1000)");
|
||||
expectEquality("td(milliseconds=1)", "td(microseconds=1000)");
|
||||
|
||||
expectEquality("td(weeks=1.0/7)", "td(days=1)");
|
||||
expectEquality("td(days=1.0/24)", "td(hours=1)");
|
||||
expectEquality("td(hours=1.0/60)", "td(minutes=1)");
|
||||
expectEquality("td(minutes=1.0/60)", "td(seconds=1)");
|
||||
expectEquality("td(seconds=0.001)", "td(milliseconds=1)");
|
||||
expectEquality("td(milliseconds=0.001)", "td(microseconds=1)");
|
||||
});
|
||||
|
||||
test("massive normalization", () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const td = PyTimeDelta.create({ microseconds: -1 });
|
||||
|
||||
expect(td.days).toBe(-1);
|
||||
expect(td.seconds).toBe(24 * 3600 - 1);
|
||||
expect(td.microseconds).toBe(999999);
|
||||
});
|
||||
|
||||
test("attributes", () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const ctx = { td: PyTimeDelta };
|
||||
|
||||
expect(evaluateExpr("td(1, 7, 31).days", ctx)).toBe(1);
|
||||
expect(evaluateExpr("td(1, 7, 31).seconds", ctx)).toBe(7);
|
||||
expect(evaluateExpr("td(1, 7, 31).microseconds", ctx)).toBe(31);
|
||||
});
|
||||
|
||||
test("basic operations: +, -, *, //", () => {
|
||||
expect.assertions(28);
|
||||
|
||||
const ctx = {
|
||||
a: new PyTimeDelta(7, 0, 0),
|
||||
b: new PyTimeDelta(0, 60, 0),
|
||||
c: new PyTimeDelta(0, 0, 1000),
|
||||
};
|
||||
|
||||
expectEquality("a+b+c", "td(7, 60, 1000)", ctx);
|
||||
expectEquality("a-b", "td(6, 24*3600 - 60)", ctx);
|
||||
expectEquality("-a", "td(-7)", ctx);
|
||||
expectEquality("+a", "td(7)", ctx);
|
||||
expectEquality("-b", "td(-1, 24*3600 - 60)", ctx);
|
||||
expectEquality("-c", "td(-1, 24*3600 - 1, 999000)", ctx);
|
||||
expectEquality("td(6, 24*3600)", "a", ctx);
|
||||
expectEquality("td(0, 0, 60*1000000)", "b", ctx);
|
||||
expectEquality("a*10", "td(70)", ctx);
|
||||
expectEquality("a*10", "10*a", ctx);
|
||||
// expectEquality('a*10L', '10*a', ctx);
|
||||
expectEquality("b*10", "td(0, 600)", ctx);
|
||||
expectEquality("10*b", "td(0, 600)", ctx);
|
||||
// expectEquality('b*10L', 'td(0, 600)', ctx);
|
||||
expectEquality("c*10", "td(0, 0, 10000)", ctx);
|
||||
expectEquality("10*c", "td(0, 0, 10000)", ctx);
|
||||
// expectEquality('c*10L', 'td(0, 0, 10000)', ctx);
|
||||
expectEquality("a*-1", "-a", ctx);
|
||||
expectEquality("b*-2", "-b-b", ctx);
|
||||
expectEquality("c*-2", "-c+-c", ctx);
|
||||
expectEquality("b*(60*24)", "(b*60)*24", ctx);
|
||||
expectEquality("b*(60*24)", "(60*b)*24", ctx);
|
||||
expectEquality("c*1000", "td(0, 1)", ctx);
|
||||
expectEquality("1000*c", "td(0, 1)", ctx);
|
||||
expectEquality("a//7", "td(1)", ctx);
|
||||
expectEquality("b//10", "td(0, 6)", ctx);
|
||||
expectEquality("c//1000", "td(0, 0, 1)", ctx);
|
||||
expectEquality("a//10", "td(0, 7*24*360)", ctx);
|
||||
expectEquality("a//3600000", "td(0, 0, 7*24*1000)", ctx);
|
||||
expectEquality("td(999999999, 86399, 999999) - td(999999999, 86399, 999998)", "td(0, 0, 1)");
|
||||
expectEquality("td(999999999, 1, 1) - td(999999999, 1, 0)", "td(0, 0, 1)");
|
||||
});
|
||||
|
||||
test("total_seconds", () => {
|
||||
expect.assertions(6);
|
||||
|
||||
const ctx = { td: PyTimeDelta };
|
||||
|
||||
expect(evaluateExpr("td(365).total_seconds()", ctx)).toBe(31536000);
|
||||
expect(evaluateExpr("td(seconds=123456.789012).total_seconds()", ctx)).toBe(123456.789012);
|
||||
expect(evaluateExpr("td(seconds=-123456.789012).total_seconds()", ctx)).toBe(-123456.789012);
|
||||
expect(evaluateExpr("td(seconds=0.123456).total_seconds()", ctx)).toBe(0.123456);
|
||||
expect(evaluateExpr("td().total_seconds()", ctx)).toBe(0);
|
||||
expect(evaluateExpr("td(seconds=1000000).total_seconds()", ctx)).toBe(1e6);
|
||||
});
|
||||
|
||||
test("bool", () => {
|
||||
expect.assertions(5);
|
||||
|
||||
const ctx = { td: PyTimeDelta };
|
||||
|
||||
expect(evaluateExpr("bool(td(1))", ctx)).toBe(true);
|
||||
expect(evaluateExpr("bool(td(0, 1))", ctx)).toBe(true);
|
||||
expect(evaluateExpr("bool(td(0, 0, 1))", ctx)).toBe(true);
|
||||
expect(evaluateExpr("bool(td(microseconds=1))", ctx)).toBe(true);
|
||||
expect(evaluateExpr("bool(not td(0))", ctx)).toBe(true);
|
||||
});
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
|
||||
import { tokenize } from "@web/core/py_js/py";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("can tokenize simple expressions with spaces", () => {
|
||||
expect(tokenize("1")).toEqual([{ type: 0 /* Number */, value: 1 }]);
|
||||
expect(tokenize(" 1")).toEqual([{ type: 0 /* Number */, value: 1 }]);
|
||||
expect(tokenize(" 1 ")).toEqual([{ type: 0 /* Number */, value: 1 }]);
|
||||
});
|
||||
|
||||
test("can tokenize numbers", () => {
|
||||
/* Without exponent */
|
||||
expect(tokenize("1")).toEqual([{ type: 0 /* Number */, value: 1 }]);
|
||||
expect(tokenize("13")).toEqual([{ type: 0 /* Number */, value: 13 }]);
|
||||
expect(tokenize("-1")).toEqual([
|
||||
{ type: 2 /* Symbol */, value: "-" },
|
||||
{ type: 0 /* Number */, value: 1 },
|
||||
]);
|
||||
|
||||
/* With exponent */
|
||||
expect(tokenize("1e2")).toEqual([{ type: 0 /* Number */, value: 100 }]);
|
||||
expect(tokenize("13E+02")).toEqual([{ type: 0 /* Number */, value: 1300 }]);
|
||||
expect(tokenize("15E-2")).toEqual([{ type: 0 /* Number */, value: 0.15 }]);
|
||||
expect(tokenize("-30e+002")).toEqual([
|
||||
{ type: 2 /* Symbol */, value: "-" },
|
||||
{ type: 0 /* Number */, value: 3000 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("can tokenize floats", () => {
|
||||
/* Without exponent */
|
||||
expect(tokenize("12.0")).toEqual([{ type: 0 /* Number */, value: 12 }]);
|
||||
expect(tokenize("1.2")).toEqual([{ type: 0 /* Number */, value: 1.2 }]);
|
||||
expect(tokenize(".42")).toEqual([{ type: 0 /* Number */, value: 0.42 }]);
|
||||
expect(tokenize("12.")).toEqual([{ type: 0 /* Number */, value: 12 }]);
|
||||
expect(tokenize("-1.23")).toEqual([
|
||||
{ type: 2 /* Symbol */, value: "-" },
|
||||
{ type: 0 /* Number */, value: 1.23 },
|
||||
]);
|
||||
|
||||
/* With exponent */
|
||||
expect(tokenize("1234e-3")).toEqual([{ type: 0 /* Number */, value: 1.234 }]);
|
||||
expect(tokenize("1.23E-03")).toEqual([{ type: 0 /* Number */, value: 0.00123 }]);
|
||||
expect(tokenize(".23e-3")).toEqual([{ type: 0 /* Number */, value: 0.00023 }]);
|
||||
expect(tokenize("23.e-03")).toEqual([{ type: 0 /* Number */, value: 0.023 }]);
|
||||
|
||||
expect(tokenize("12.1E2")).toEqual([{ type: 0 /* Number */, value: 1210 }]);
|
||||
expect(tokenize("1.23e+03")).toEqual([{ type: 0 /* Number */, value: 1230 }]);
|
||||
expect(tokenize(".23e2")).toEqual([{ type: 0 /* Number */, value: 23 }]);
|
||||
expect(tokenize("15.E+02")).toEqual([{ type: 0 /* Number */, value: 1500 }]);
|
||||
|
||||
expect(tokenize("-23E02")).toEqual([
|
||||
{ type: 2 /* Symbol */, value: "-" },
|
||||
{ type: 0 /* Number */, value: 2300 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("can tokenize strings", () => {
|
||||
expect(tokenize('"foo"')).toEqual([{ type: 1 /* String */, value: "foo" }]);
|
||||
});
|
||||
|
||||
test("can tokenize bare names", () => {
|
||||
expect(tokenize("foo")).toEqual([{ type: 3 /* Name */, value: "foo" }]);
|
||||
});
|
||||
|
||||
test("can tokenize misc operators", () => {
|
||||
expect(tokenize("in")).toEqual([{ type: 2 /* Symbol */, value: "in" }]);
|
||||
expect(tokenize("not in")).toEqual([{ type: 2 /* Symbol */, value: "not in" }]);
|
||||
expect(tokenize("3 ** 2")[1]).toEqual({ type: 2 /* Symbol */, value: "**" });
|
||||
});
|
||||
|
||||
test("can tokenize constants", () => {
|
||||
expect(tokenize("None")).toEqual([{ type: 4 /* Constant */, value: "None" }]);
|
||||
expect(tokenize("True")).toEqual([{ type: 4 /* Constant */, value: "True" }]);
|
||||
expect(tokenize("False")).toEqual([{ type: 4 /* Constant */, value: "False" }]);
|
||||
});
|
||||
|
||||
test("can tokenize parenthesis", () => {
|
||||
expect(tokenize("()")).toEqual([
|
||||
{ type: 2 /* Symbol */, value: "(" },
|
||||
{ type: 2 /* Symbol */, value: ")" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("can tokenize function with kwargs", () => {
|
||||
expect(tokenize('foo(bar=3, qux="4")')).toEqual([
|
||||
{ type: 3 /* Name */, value: "foo" },
|
||||
{ type: 2 /* Symbol */, value: "(" },
|
||||
{ type: 3 /* Name */, value: "bar" },
|
||||
{ type: 2 /* Symbol */, value: "=" },
|
||||
{ type: 0 /* Number */, value: 3 },
|
||||
{ type: 2 /* Symbol */, value: "," },
|
||||
{ type: 3 /* Name */, value: "qux" },
|
||||
{ type: 2 /* Symbol */, value: "=" },
|
||||
{ type: 1 /* String */, value: "4" },
|
||||
{ type: 2 /* Symbol */, value: ")" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("can tokenize if statement", () => {
|
||||
expect(tokenize("1 if True else 2")).toEqual([
|
||||
{ type: 0 /* Number */, value: 1 },
|
||||
{ type: 2 /* Symbol */, value: "if" },
|
||||
{ type: 4 /* Constant */, value: "True" },
|
||||
{ type: 2 /* Symbol */, value: "else" },
|
||||
{ type: 0 /* Number */, value: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("sanity check: throw some errors", () => {
|
||||
expect(() => tokenize("'asdf")).toThrow();
|
||||
});
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
|
||||
import { evaluateExpr, formatAST, parseExpr } from "@web/core/py_js/py";
|
||||
import { PyDate, PyDateTime } from "@web/core/py_js/py_date";
|
||||
import { toPyValue } from "@web/core/py_js/py_utils";
|
||||
|
||||
const checkAST = (expr, message = expr) => {
|
||||
const ast = parseExpr(expr);
|
||||
const str = formatAST(ast);
|
||||
if (str !== expr) {
|
||||
throw new Error(`mismatch: ${str} !== ${expr} (${message});`);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
describe("formatAST", () => {
|
||||
test("basic values", () => {
|
||||
expect(checkAST("1", "number value")).toBe(true);
|
||||
expect(checkAST("1.4", "float value")).toBe(true);
|
||||
expect(checkAST("-12", "negative number value")).toBe(true);
|
||||
expect(checkAST("True", "boolean")).toBe(true);
|
||||
expect(checkAST(`"some string"`, "a string")).toBe(true);
|
||||
expect(checkAST("None", "None")).toBe(true);
|
||||
});
|
||||
|
||||
test("dictionary", () => {
|
||||
expect(checkAST("{}", "empty dictionary")).toBe(true);
|
||||
expect(checkAST(`{"a": 1}`, "dictionary with a single key")).toBe(true);
|
||||
expect(checkAST(`d["a"]`, "get a value in a dictionary")).toBe(true);
|
||||
});
|
||||
|
||||
test("list", () => {
|
||||
expect(checkAST("[]", "empty list")).toBe(true);
|
||||
expect(checkAST("[1]", "list with one value")).toBe(true);
|
||||
expect(checkAST("[1, 2]", "list with two values")).toBe(true);
|
||||
});
|
||||
|
||||
test("tuple", () => {
|
||||
expect(checkAST("()", "empty tuple")).toBe(true);
|
||||
expect(checkAST("(1, 2)", "basic tuple")).toBe(true);
|
||||
});
|
||||
|
||||
test("simple arithmetic", () => {
|
||||
expect(checkAST("1 + 2", "addition")).toBe(true);
|
||||
expect(checkAST("+(1 + 2)", "other addition, prefix")).toBe(true);
|
||||
expect(checkAST("1 - 2", "substraction")).toBe(true);
|
||||
expect(checkAST("-1 - 2", "other substraction")).toBe(true);
|
||||
expect(checkAST("-(1 + 2)", "other substraction")).toBe(true);
|
||||
expect(checkAST("1 + 2 + 3", "addition of 3 integers")).toBe(true);
|
||||
expect(checkAST("a + b", "addition of two variables")).toBe(true);
|
||||
expect(checkAST("42 % 5", "modulo operator")).toBe(true);
|
||||
expect(checkAST("a * 10", "multiplication")).toBe(true);
|
||||
expect(checkAST("a ** 10", "**")).toBe(true);
|
||||
expect(checkAST("~10", "bitwise not")).toBe(true);
|
||||
expect(checkAST("~(10 + 3)", "bitwise not")).toBe(true);
|
||||
expect(checkAST("a * (1 + 2)", "multiplication and addition")).toBe(true);
|
||||
expect(checkAST("(a + b) * 43", "addition and multiplication")).toBe(true);
|
||||
expect(checkAST("a // 10", "number division")).toBe(true);
|
||||
});
|
||||
|
||||
test("boolean operators", () => {
|
||||
expect(checkAST("True and False", "boolean operator")).toBe(true);
|
||||
expect(checkAST("True or False", "boolean operator or")).toBe(true);
|
||||
expect(checkAST("(True or False) and False", "boolean operators and and or")).toBe(true);
|
||||
expect(checkAST("not False", "not prefix")).toBe(true);
|
||||
expect(checkAST("not foo", "not prefix with variable")).toBe(true);
|
||||
expect(checkAST("not a in b", "not prefix with expression")).toBe(true);
|
||||
});
|
||||
|
||||
test("conditional expression", () => {
|
||||
expect(checkAST("1 if a else 2")).toBe(true);
|
||||
expect(checkAST("[] if a else 2")).toBe(true);
|
||||
});
|
||||
|
||||
test("other operators", () => {
|
||||
expect(checkAST("x == y", "== operator")).toBe(true);
|
||||
expect(checkAST("x != y", "!= operator")).toBe(true);
|
||||
expect(checkAST("x < y", "< operator")).toBe(true);
|
||||
expect(checkAST("x is y", "is operator")).toBe(true);
|
||||
expect(checkAST("x is not y", "is and not operator")).toBe(true);
|
||||
expect(checkAST("x in y", "in operator")).toBe(true);
|
||||
expect(checkAST("x not in y", "not in operator")).toBe(true);
|
||||
});
|
||||
|
||||
test("equality", () => {
|
||||
expect(checkAST("a == b", "simple equality")).toBe(true);
|
||||
});
|
||||
|
||||
test("strftime", () => {
|
||||
expect(checkAST(`time.strftime("%Y")`, "strftime with year")).toBe(true);
|
||||
expect(checkAST(`time.strftime("%Y") + "-01-30"`, "strftime with year")).toBe(true);
|
||||
expect(checkAST(`time.strftime("%Y-%m-%d %H:%M:%S")`, "strftime with year")).toBe(true);
|
||||
});
|
||||
|
||||
test("context_today", () => {
|
||||
expect(checkAST(`context_today().strftime("%Y-%m-%d")`, "context today call")).toBe(true);
|
||||
});
|
||||
|
||||
test("function call", () => {
|
||||
expect(checkAST("td()", "simple call")).toBe(true);
|
||||
expect(checkAST("td(a, b, c)", "simple call with args")).toBe(true);
|
||||
expect(checkAST("td(days = 1)", "simple call with kwargs")).toBe(true);
|
||||
expect(checkAST("f(1, 2, days = 1)", "mixing args and kwargs")).toBe(true);
|
||||
expect(checkAST("str(td(2))", "function call in function call")).toBe(true);
|
||||
});
|
||||
|
||||
test("various expressions", () => {
|
||||
expect(checkAST("(a - b).days", "substraction and .days")).toBe(true);
|
||||
expect(checkAST("a + day == date(2002, 3, 3)")).toBe(true);
|
||||
const expr = `[("type", "=", "in"), ("day", "<=", time.strftime("%Y-%m-%d")), ("day", ">", (context_today() - datetime.timedelta(days = 15)).strftime("%Y-%m-%d"))]`;
|
||||
expect(checkAST(expr)).toBe(true);
|
||||
});
|
||||
|
||||
test("escaping support", () => {
|
||||
expect(evaluateExpr(String.raw`"\x61"`)).toBe("a", { message: "hex escapes" });
|
||||
expect(evaluateExpr(String.raw`"\\abc"`)).toBe(String.raw`\abc`, {
|
||||
message: "escaped backslash",
|
||||
});
|
||||
expect(checkAST(String.raw`"\\abc"`, "escaped backslash AST check")).toBe(true);
|
||||
const a = String.raw`'foo\\abc"\''`;
|
||||
const b = formatAST(parseExpr(formatAST(parseExpr(a))));
|
||||
// Our repr uses JSON.stringify which always uses double quotes,
|
||||
// whereas Python's repr is single-quote-biased: strings are repr'd
|
||||
// using single quote delimiters *unless* they contain single quotes and
|
||||
// no double quotes, then they're delimited with double quotes.
|
||||
expect(b).toBe(String.raw`"foo\\abc\"'"`);
|
||||
});
|
||||
|
||||
test("null value", () => {
|
||||
expect(formatAST(toPyValue(null))).toBe("None");
|
||||
});
|
||||
});
|
||||
|
||||
describe("toPyValue", () => {
|
||||
test("toPyValue a string", () => {
|
||||
const ast = toPyValue("test");
|
||||
expect(ast.type).toBe(1);
|
||||
expect(ast.value).toBe("test");
|
||||
expect(formatAST(ast)).toBe('"test"');
|
||||
});
|
||||
|
||||
test("toPyValue a number", () => {
|
||||
const ast = toPyValue(1);
|
||||
expect(ast.type).toBe(0);
|
||||
expect(ast.value).toBe(1);
|
||||
expect(formatAST(ast)).toBe("1");
|
||||
});
|
||||
|
||||
test("toPyValue a boolean", () => {
|
||||
let ast = toPyValue(true);
|
||||
expect(ast.type).toBe(2);
|
||||
expect(ast.value).toBe(true);
|
||||
expect(formatAST(ast)).toBe("True");
|
||||
|
||||
ast = toPyValue(false);
|
||||
expect(ast.type).toBe(2);
|
||||
expect(ast.value).toBe(false);
|
||||
expect(formatAST(ast)).toBe("False");
|
||||
});
|
||||
|
||||
test("toPyValue a object", () => {
|
||||
const ast = toPyValue({ a: 1 });
|
||||
expect(ast.type).toBe(11);
|
||||
expect("a" in ast.value).toBe(true);
|
||||
expect(["type", "value"].every((prop) => prop in ast.value.a)).toBe(true);
|
||||
expect(ast.value.a.type).toBe(0);
|
||||
expect(ast.value.a.value).toBe(1);
|
||||
expect(formatAST(ast)).toBe('{"a": 1}');
|
||||
});
|
||||
|
||||
test("toPyValue a date", () => {
|
||||
const date = new Date(Date.UTC(2000, 0, 1));
|
||||
const ast = toPyValue(date);
|
||||
expect(ast.type).toBe(1);
|
||||
const expectedValue = PyDateTime.convertDate(date);
|
||||
expect(ast.value.isEqual(expectedValue)).toBe(true);
|
||||
expect(formatAST(ast)).toBe(JSON.stringify(expectedValue));
|
||||
});
|
||||
|
||||
test("toPyValue a dateime", () => {
|
||||
const datetime = new Date(Date.UTC(2000, 0, 1, 1, 0, 0, 0));
|
||||
const ast = toPyValue(datetime);
|
||||
expect(ast.type).toBe(1);
|
||||
const expectedValue = PyDateTime.convertDate(datetime);
|
||||
expect(ast.value.isEqual(expectedValue)).toBe(true);
|
||||
expect(formatAST(ast)).toBe(JSON.stringify(expectedValue));
|
||||
});
|
||||
|
||||
test("toPyValue a PyDate", () => {
|
||||
const value = new PyDate(2000, 1, 1);
|
||||
const ast = toPyValue(value);
|
||||
expect(ast.type).toBe(1);
|
||||
expect(ast.value).toBe(value);
|
||||
expect(formatAST(ast)).toBe(JSON.stringify(value));
|
||||
});
|
||||
|
||||
test("toPyValue a PyDateTime", () => {
|
||||
const value = new PyDateTime(2000, 1, 1, 1, 0, 0, 0);
|
||||
const ast = toPyValue(value);
|
||||
expect(ast.type).toBe(1);
|
||||
expect(ast.value).toBe(value);
|
||||
expect(formatAST(ast)).toBe(JSON.stringify(value));
|
||||
});
|
||||
});
|
||||
195
odoo-bringout-oca-ocb-web/web/static/tests/core/reactive.test.js
Normal file
195
odoo-bringout-oca-ocb-web/web/static/tests/core/reactive.test.js
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { EventBus, reactive } from "@odoo/owl";
|
||||
import { Reactive, effect, withComputedProperties } from "@web/core/utils/reactive";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
describe("class", () => {
|
||||
test("callback registered without Reactive class constructor will not notify", async () => {
|
||||
// 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(), () => {
|
||||
expect.step(`counter: ${obj.counter}`);
|
||||
});
|
||||
|
||||
obj.counter; // initial subscription to counter
|
||||
obj.counter++;
|
||||
expect.verifySteps(["counter: 1"]);
|
||||
bus.trigger("change");
|
||||
expect(obj.counter).toBe(2);
|
||||
expect.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
|
||||
]);
|
||||
});
|
||||
|
||||
test("callback registered in Reactive class constructor will notify", async () => {
|
||||
const bus = new EventBus();
|
||||
class MyReactiveClass extends Reactive {
|
||||
constructor() {
|
||||
super();
|
||||
this.counter = 0;
|
||||
bus.addEventListener("change", () => this.counter++);
|
||||
}
|
||||
}
|
||||
const obj = reactive(new MyReactiveClass(), () => {
|
||||
expect.step(`counter: ${obj.counter}`);
|
||||
});
|
||||
obj.counter; // initial subscription to counter
|
||||
obj.counter++;
|
||||
expect.verifySteps(["counter: 1"]);
|
||||
bus.trigger("change");
|
||||
expect(obj.counter).toBe(2);
|
||||
expect.verifySteps(["counter: 2"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("effect", () => {
|
||||
test("effect runs once immediately", async () => {
|
||||
const state = reactive({ counter: 0 });
|
||||
expect.verifySteps([]);
|
||||
effect(
|
||||
(state) => {
|
||||
expect.step(`counter: ${state.counter}`);
|
||||
},
|
||||
[state]
|
||||
);
|
||||
expect.verifySteps(["counter: 0"]);
|
||||
});
|
||||
|
||||
test("effect runs when reactive deps change", async () => {
|
||||
const state = reactive({ counter: 0 });
|
||||
expect.verifySteps([]);
|
||||
effect(
|
||||
(state) => {
|
||||
expect.step(`counter: ${state.counter}`);
|
||||
},
|
||||
[state]
|
||||
);
|
||||
// effect runs immediately
|
||||
expect.verifySteps(["counter: 0"]);
|
||||
|
||||
state.counter++;
|
||||
// first mutation runs the effect
|
||||
expect.verifySteps(["counter: 1"]);
|
||||
|
||||
state.counter++;
|
||||
// subsequent mutations run the effect
|
||||
expect.verifySteps(["counter: 2"]);
|
||||
});
|
||||
|
||||
test("Original reactive callback is not subscribed to keys observed by effect", async () => {
|
||||
let reactiveCallCount = 0;
|
||||
const state = reactive(
|
||||
{
|
||||
counter: 0,
|
||||
},
|
||||
() => reactiveCallCount++
|
||||
);
|
||||
expect.verifySteps([]);
|
||||
expect(reactiveCallCount).toBe(0);
|
||||
effect(
|
||||
(state) => {
|
||||
expect.step(`counter: ${state.counter}`);
|
||||
},
|
||||
[state]
|
||||
);
|
||||
expect.verifySteps(["counter: 0"]);
|
||||
expect(reactiveCallCount).toBe(0, {
|
||||
message: "did not call the original reactive's callback",
|
||||
});
|
||||
state.counter = 1;
|
||||
expect.verifySteps(["counter: 1"]);
|
||||
expect(reactiveCallCount).toBe(0, {
|
||||
message: "did not call the original reactive's callback",
|
||||
});
|
||||
state.counter; // subscribe the original reactive
|
||||
state.counter = 2;
|
||||
expect.verifySteps(["counter: 2"]);
|
||||
expect(reactiveCallCount).toBe(1, {
|
||||
message: "the original callback was called because it is subscribed independently",
|
||||
});
|
||||
});
|
||||
|
||||
test("mutating keys not observed by the effect doesn't cause it to run", async () => {
|
||||
const state = reactive({ counter: 0, unobserved: 0 });
|
||||
effect(
|
||||
(state) => {
|
||||
expect.step(`counter: ${state.counter}`);
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
expect.verifySteps(["counter: 0"]);
|
||||
state.counter = 1;
|
||||
expect.verifySteps(["counter: 1"]);
|
||||
state.unobserved = 1;
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withComputedProperties", () => {
|
||||
test("computed properties are set immediately", async () => {
|
||||
const source = reactive({ counter: 1 });
|
||||
const derived = withComputedProperties(reactive({}), [source], {
|
||||
doubleCounter(source) {
|
||||
return source.counter * 2;
|
||||
},
|
||||
});
|
||||
expect(derived.doubleCounter).toBe(2);
|
||||
});
|
||||
|
||||
test("computed properties are recomputed when dependencies change", async () => {
|
||||
const source = reactive({ counter: 1 });
|
||||
const derived = withComputedProperties(reactive({}), [source], {
|
||||
doubleCounter(source) {
|
||||
return source.counter * 2;
|
||||
},
|
||||
});
|
||||
expect(derived.doubleCounter).toBe(2);
|
||||
source.counter++;
|
||||
expect(derived.doubleCounter).toBe(4);
|
||||
});
|
||||
|
||||
test("can observe computed properties", async () => {
|
||||
const source = reactive({ counter: 1 });
|
||||
const derived = withComputedProperties(reactive({}), [source], {
|
||||
doubleCounter(source) {
|
||||
return source.counter * 2;
|
||||
},
|
||||
});
|
||||
const observed = reactive(derived, () => {
|
||||
expect.step(`doubleCounter: ${observed.doubleCounter}`);
|
||||
});
|
||||
observed.doubleCounter; // subscribe to doubleCounter
|
||||
expect.verifySteps([]);
|
||||
source.counter++;
|
||||
expect.verifySteps(["doubleCounter: 4"]);
|
||||
});
|
||||
|
||||
test("computed properties can use nested objects", async () => {
|
||||
const source = reactive({ subObj: { counter: 1 } });
|
||||
const derived = withComputedProperties(reactive({}), [source], {
|
||||
doubleCounter(source) {
|
||||
return source.subObj.counter * 2;
|
||||
},
|
||||
});
|
||||
const observed = reactive(derived, () => {
|
||||
expect.step(`doubleCounter: ${observed.doubleCounter}`);
|
||||
});
|
||||
observed.doubleCounter; // subscribe to doubleCounter
|
||||
expect(derived.doubleCounter).toBe(2);
|
||||
expect.verifySteps([]);
|
||||
source.subObj.counter++;
|
||||
expect(derived.doubleCounter).toBe(4);
|
||||
// reactive gets notified even for computed properties dervied from nested objects
|
||||
expect.verifySteps(["doubleCounter: 4"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { MultiRecordSelector } from "@web/core/record_selectors/multi_record_selector";
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import {
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { click, fill, press, queryAllTexts } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "partner";
|
||||
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "Alice" },
|
||||
{ id: 2, name: "Bob" },
|
||||
{ id: 3, name: "Charlie" },
|
||||
];
|
||||
}
|
||||
|
||||
// Required for the select create dialog
|
||||
class Users extends models.Model {
|
||||
_name = "res.users";
|
||||
has_group = () => true;
|
||||
}
|
||||
|
||||
defineModels([Partner, Users]);
|
||||
|
||||
async function mountMultiRecordSelector(props) {
|
||||
class Parent extends Component {
|
||||
static components = { MultiRecordSelector };
|
||||
static template = xml`<MultiRecordSelector t-props="recordProps" />`;
|
||||
static props = ["*"];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
}
|
||||
|
||||
test("Can be renderer with no values", async () => {
|
||||
await mountMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [],
|
||||
});
|
||||
|
||||
expect(".o_multi_record_selector input").toHaveValue("");
|
||||
expect(".o_multi_record_selector input").toHaveClass("o_input");
|
||||
});
|
||||
|
||||
test("Can be renderer with a value", async () => {
|
||||
await mountMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1],
|
||||
});
|
||||
|
||||
expect(".o_multi_record_selector input").toHaveValue("");
|
||||
expect(".o_tag").toHaveCount(1);
|
||||
expect(".o_tag").toHaveText("Alice");
|
||||
});
|
||||
|
||||
test("Can be renderer with multiple values", async () => {
|
||||
await mountMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1, 2],
|
||||
});
|
||||
|
||||
expect(".o_multi_record_selector input").toHaveValue("");
|
||||
expect(".o_tag").toHaveCount(2);
|
||||
expect(queryAllTexts(".o_tag")).toEqual(["Alice", "Bob"]);
|
||||
});
|
||||
|
||||
test("Can be updated from autocomplete", async () => {
|
||||
await mountMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [],
|
||||
});
|
||||
|
||||
expect(".o_multi_record_selector input").toHaveValue("");
|
||||
expect(".o_tag").toHaveCount(0);
|
||||
expect(".o-autocomplete--dropdown-menu").toHaveCount(0);
|
||||
await click(".o_multi_record_selector input");
|
||||
await animationFrame();
|
||||
expect(".o-autocomplete--dropdown-menu").toHaveCount(1);
|
||||
await click("li.o-autocomplete--dropdown-item:eq(1)");
|
||||
await animationFrame();
|
||||
expect(".o_tag").toHaveCount(1);
|
||||
expect(".o_tag").toHaveText("Bob");
|
||||
});
|
||||
|
||||
test("Display name is correctly fetched", async () => {
|
||||
expect.assertions(4);
|
||||
onRpc("partner", "web_search_read", ({ kwargs }) => {
|
||||
expect.step("web_search_read");
|
||||
expect(kwargs.domain).toEqual([["id", "in", [1]]]);
|
||||
});
|
||||
|
||||
await mountMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1],
|
||||
});
|
||||
|
||||
expect(".o_tag").toHaveCount(1);
|
||||
expect(".o_tag").toHaveText("Alice");
|
||||
expect.verifySteps(["web_search_read"]);
|
||||
});
|
||||
|
||||
test("Can give domain and context props for the name search", async () => {
|
||||
expect.assertions(4);
|
||||
onRpc("partner", "name_search", ({ kwargs }) => {
|
||||
expect.step("name_search");
|
||||
expect(kwargs.args).toEqual(["&", ["display_name", "=", "Bob"], "!", ["id", "in", [1]]]);
|
||||
expect(kwargs.context.blip).toBe("blop");
|
||||
});
|
||||
|
||||
await mountMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1],
|
||||
domain: [["display_name", "=", "Bob"]],
|
||||
context: { blip: "blop" },
|
||||
});
|
||||
|
||||
expect.verifySteps([]);
|
||||
await click(".o_multi_record_selector input");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["name_search"]);
|
||||
});
|
||||
|
||||
test("Support placeholder", async () => {
|
||||
await mountMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [],
|
||||
placeholder: "Select a partner",
|
||||
});
|
||||
expect(".o_multi_record_selector input").toHaveAttribute("placeholder", "Select a partner");
|
||||
await click(".o_multi_record_selector input");
|
||||
await animationFrame();
|
||||
await contains("li.o-autocomplete--dropdown-item:eq(0)").click();
|
||||
expect(".o_multi_record_selector input").toHaveAttribute("placeholder", "");
|
||||
});
|
||||
|
||||
test("Placeholder is not set if values are selected", async () => {
|
||||
await mountMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1],
|
||||
placeholder: "Select a partner",
|
||||
});
|
||||
expect(".o_multi_record_selector input").toHaveAttribute("placeholder", "");
|
||||
});
|
||||
|
||||
test("Can delete a tag with Backspace", async () => {
|
||||
await mountMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1, 2],
|
||||
});
|
||||
await click(".o_multi_record_selector input");
|
||||
await animationFrame();
|
||||
await press("Backspace");
|
||||
await animationFrame();
|
||||
expect(".o_tag").toHaveCount(1);
|
||||
expect(".o_tag").toHaveText("Alice");
|
||||
});
|
||||
|
||||
test("Can focus tags with arrow right and left", async () => {
|
||||
await mountMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1, 2],
|
||||
});
|
||||
// Click twice because to get the focus and make disappear the autocomplete popover
|
||||
await click(".o_multi_record_selector input");
|
||||
await click(".o_multi_record_selector input");
|
||||
await animationFrame();
|
||||
await press("arrowleft");
|
||||
await animationFrame();
|
||||
expect(document.activeElement).toHaveText("Bob");
|
||||
await press("arrowleft");
|
||||
await animationFrame();
|
||||
expect(document.activeElement).toHaveText("Alice");
|
||||
await press("arrowleft");
|
||||
await animationFrame();
|
||||
expect(document.activeElement).toHaveClass("o-autocomplete--input");
|
||||
await press("arrowright");
|
||||
await animationFrame();
|
||||
expect(document.activeElement).toHaveText("Alice");
|
||||
await press("arrowright");
|
||||
await animationFrame();
|
||||
expect(document.activeElement).toHaveText("Bob");
|
||||
await press("arrowright");
|
||||
await animationFrame();
|
||||
expect(document.activeElement).toHaveClass("o-autocomplete--input");
|
||||
});
|
||||
|
||||
test("Delete the focused element", async () => {
|
||||
await mountMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1, 2],
|
||||
});
|
||||
// Click twice because to get the focus and make disappear the autocomplete popover
|
||||
await click(".o_multi_record_selector input");
|
||||
await click(".o_multi_record_selector input");
|
||||
await animationFrame();
|
||||
|
||||
await press("arrowright");
|
||||
await animationFrame();
|
||||
expect(document.activeElement).toHaveText("Alice");
|
||||
|
||||
await press("Backspace");
|
||||
await animationFrame();
|
||||
expect(".o_tag").toHaveCount(1);
|
||||
expect(".o_tag").toHaveText("Bob");
|
||||
});
|
||||
|
||||
test("Backspace do nothing when the input is currently edited", async () => {
|
||||
await mountMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [1, 2],
|
||||
});
|
||||
await click(".o-autocomplete input");
|
||||
await animationFrame();
|
||||
|
||||
await fill("a");
|
||||
await animationFrame();
|
||||
expect(document.activeElement).toHaveValue("a");
|
||||
|
||||
await press("Backspace");
|
||||
await animationFrame();
|
||||
expect(".o_tag").toHaveCount(2);
|
||||
});
|
||||
|
||||
// Desktop only because a kanban view is used instead of a list in mobile
|
||||
test.tags("desktop");
|
||||
test("Can pass domain to search more", async () => {
|
||||
Partner._records.push(
|
||||
{ id: 4, name: "David" },
|
||||
{ id: 5, name: "Eve" },
|
||||
{ id: 6, name: "Frank" },
|
||||
{ id: 7, name: "Grace" },
|
||||
{ id: 8, name: "Helen" },
|
||||
{ id: 9, name: "Ivy" }
|
||||
);
|
||||
Partner._views["list"] = /* xml */ `<list><field name="name"/></list>`;
|
||||
await mountMultiRecordSelector({
|
||||
resModel: "partner",
|
||||
resIds: [],
|
||||
domain: [["id", "not in", [1]]],
|
||||
});
|
||||
await click(".o-autocomplete input");
|
||||
await animationFrame();
|
||||
|
||||
await click(".o_multi_record_selector .o_m2o_dropdown_option");
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_data_row").toHaveCount(8, { message: "should contain 8 records" });
|
||||
});
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { RecordSelector } from "@web/core/record_selectors/record_selector";
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import {
|
||||
defineModels,
|
||||
fields,
|
||||
models,
|
||||
mountWithCleanup,
|
||||
onRpc,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { click } from "@odoo/hoot-dom";
|
||||
|
||||
class Partner extends models.Model {
|
||||
_name = "partner";
|
||||
|
||||
name = fields.Char();
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "Alice" },
|
||||
{ id: 2, name: "Bob" },
|
||||
{ id: 3, name: "Charlie" },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels([Partner]);
|
||||
|
||||
async function mountRecordSelector(props) {
|
||||
class Parent extends Component {
|
||||
static components = { RecordSelector };
|
||||
static template = xml`<RecordSelector t-props="recordProps" />`;
|
||||
static props = ["*"];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
}
|
||||
|
||||
test("Can be renderer with no values", async () => {
|
||||
await mountRecordSelector({
|
||||
resModel: "partner",
|
||||
resId: false,
|
||||
});
|
||||
|
||||
expect(".o_record_selector input").toHaveValue("");
|
||||
expect(".o_record_selector input").toHaveClass("o_input");
|
||||
});
|
||||
|
||||
test("Can be renderer with a value", async () => {
|
||||
await mountRecordSelector({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_record_selector input").toHaveValue("Alice");
|
||||
});
|
||||
|
||||
test("Can be updated from autocomplete", async () => {
|
||||
await mountRecordSelector({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_record_selector input").toHaveValue("Alice");
|
||||
expect(".o-autocomplete--dropdown-menu").toHaveCount(0);
|
||||
await click(".o_record_selector input");
|
||||
await animationFrame();
|
||||
expect(".o-autocomplete--dropdown-menu").toHaveCount(1);
|
||||
await click("li.o-autocomplete--dropdown-item:eq(1)");
|
||||
await animationFrame();
|
||||
expect(".o_record_selector input").toHaveValue("Bob");
|
||||
});
|
||||
|
||||
test("Display name is correctly fetched", async () => {
|
||||
expect.assertions(3);
|
||||
onRpc("partner", "web_search_read", ({ kwargs }) => {
|
||||
expect.step("web_search_read");
|
||||
expect(kwargs.domain).toEqual([["id", "in", [1]]]);
|
||||
});
|
||||
await mountRecordSelector({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(".o_record_selector input").toHaveValue("Alice");
|
||||
expect.verifySteps(["web_search_read"]);
|
||||
});
|
||||
|
||||
test("Can give domain and context props for the name search", async () => {
|
||||
expect.assertions(5);
|
||||
onRpc("partner", "name_search", ({ kwargs }) => {
|
||||
expect.step("name_search");
|
||||
expect(kwargs.args).toEqual(["&", ["display_name", "=", "Bob"], "!", ["id", "in", []]]);
|
||||
expect(kwargs.context.blip).toBe("blop");
|
||||
});
|
||||
await mountRecordSelector({
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
domain: [["display_name", "=", "Bob"]],
|
||||
context: { blip: "blop" },
|
||||
});
|
||||
|
||||
expect(".o_record_selector input").toHaveValue("Alice");
|
||||
expect.verifySteps([]);
|
||||
await click(".o_record_selector input");
|
||||
await animationFrame();
|
||||
expect.verifySteps(["name_search"]);
|
||||
});
|
||||
|
||||
test("Support placeholder", async () => {
|
||||
await mountRecordSelector({
|
||||
resModel: "partner",
|
||||
resId: false,
|
||||
placeholder: "Select a partner",
|
||||
});
|
||||
expect(".o_record_selector input").toHaveAttribute("placeholder", "Select a partner");
|
||||
});
|
||||
207
odoo-bringout-oca-ocb-web/web/static/tests/core/registry.test.js
Normal file
207
odoo-bringout-oca-ocb-web/web/static/tests/core/registry.test.js
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Registry } from "@web/core/registry";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("key set and get", () => {
|
||||
const registry = new Registry();
|
||||
const foo = {};
|
||||
|
||||
registry.add("foo", foo);
|
||||
|
||||
expect(registry.get("foo")).toBe(foo);
|
||||
});
|
||||
|
||||
test("can set and get falsy values", () => {
|
||||
const registry = new Registry();
|
||||
registry.add("foo1", false);
|
||||
registry.add("foo2", 0);
|
||||
registry.add("foo3", "");
|
||||
registry.add("foo4", undefined);
|
||||
registry.add("foo5", null);
|
||||
|
||||
expect(registry.get("foo1")).toBe(false);
|
||||
expect(registry.get("foo2")).toBe(0);
|
||||
expect(registry.get("foo3")).toBe("");
|
||||
expect(registry.get("foo4")).toBe(undefined);
|
||||
expect(registry.get("foo5")).toBe(null);
|
||||
});
|
||||
|
||||
test("can set and get falsy values with default value", () => {
|
||||
const registry = new Registry();
|
||||
registry.add("foo1", false);
|
||||
registry.add("foo2", 0);
|
||||
registry.add("foo3", "");
|
||||
registry.add("foo4", undefined);
|
||||
registry.add("foo5", null);
|
||||
|
||||
expect(registry.get("foo1", 1)).toBe(false);
|
||||
expect(registry.get("foo2", 1)).toBe(0);
|
||||
expect(registry.get("foo3", 1)).toBe("");
|
||||
expect(registry.get("foo4", 1)).toBe(undefined);
|
||||
expect(registry.get("foo5", 1)).toBe(null);
|
||||
});
|
||||
|
||||
test("can get a default value when missing key", () => {
|
||||
const registry = new Registry();
|
||||
|
||||
expect(registry.get("missing", "default")).toBe("default");
|
||||
expect(registry.get("missing", null)).toBe(null);
|
||||
expect(registry.get("missing", false)).toBe(false);
|
||||
});
|
||||
|
||||
test("throws if key is missing", () => {
|
||||
const registry = new Registry();
|
||||
expect(() => registry.get("missing")).toThrow();
|
||||
});
|
||||
|
||||
test("contains method", () => {
|
||||
const registry = new Registry();
|
||||
|
||||
registry.add("foo", 1);
|
||||
|
||||
expect(registry.contains("foo")).toBe(true);
|
||||
expect(registry.contains("bar")).toBe(false);
|
||||
});
|
||||
|
||||
test("can set and get a value, with an order arg", () => {
|
||||
const registry = new Registry();
|
||||
const foo = {};
|
||||
|
||||
registry.add("foo", foo, { sequence: 24 });
|
||||
|
||||
expect(registry.get("foo")).toBe(foo);
|
||||
});
|
||||
|
||||
test("can get ordered list of elements", () => {
|
||||
const registry = new Registry();
|
||||
|
||||
registry
|
||||
.add("foo1", "foo1", { sequence: 1 })
|
||||
.add("foo2", "foo2", { sequence: 2 })
|
||||
.add("foo5", "foo5", { sequence: 5 })
|
||||
.add("foo3", "foo3", { sequence: 3 });
|
||||
|
||||
expect(registry.getAll()).toEqual(["foo1", "foo2", "foo3", "foo5"]);
|
||||
});
|
||||
|
||||
test("can get ordered list of entries", () => {
|
||||
const registry = new Registry();
|
||||
|
||||
registry
|
||||
.add("foo1", "foo1", { sequence: 1 })
|
||||
.add("foo2", "foo2", { sequence: 2 })
|
||||
.add("foo5", "foo5", { sequence: 5 })
|
||||
.add("foo3", "foo3", { sequence: 3 });
|
||||
|
||||
expect(registry.getEntries()).toEqual([
|
||||
["foo1", "foo1"],
|
||||
["foo2", "foo2"],
|
||||
["foo3", "foo3"],
|
||||
["foo5", "foo5"],
|
||||
]);
|
||||
});
|
||||
|
||||
test("getAll and getEntries returns shallow copies", () => {
|
||||
const registry = new Registry();
|
||||
|
||||
registry.add("foo1", "foo1");
|
||||
|
||||
const all = registry.getAll();
|
||||
const entries = registry.getEntries();
|
||||
|
||||
expect(all).toEqual(["foo1"]);
|
||||
expect(entries).toEqual([["foo1", "foo1"]]);
|
||||
|
||||
all.push("foo2");
|
||||
entries.push(["foo2", "foo2"]);
|
||||
|
||||
expect(all).toEqual(["foo1", "foo2"]);
|
||||
expect(entries).toEqual([
|
||||
["foo1", "foo1"],
|
||||
["foo2", "foo2"],
|
||||
]);
|
||||
expect(registry.getAll()).toEqual(["foo1"]);
|
||||
expect(registry.getEntries()).toEqual([["foo1", "foo1"]]);
|
||||
});
|
||||
|
||||
test("can override element with sequence", () => {
|
||||
const registry = new Registry();
|
||||
|
||||
registry
|
||||
.add("foo1", "foo1", { sequence: 1 })
|
||||
.add("foo2", "foo2", { sequence: 2 })
|
||||
.add("foo1", "foo3", { force: true });
|
||||
|
||||
expect(registry.getEntries()).toEqual([
|
||||
["foo1", "foo3"],
|
||||
["foo2", "foo2"],
|
||||
]);
|
||||
});
|
||||
|
||||
test("can override element with sequence 2 ", () => {
|
||||
const registry = new Registry();
|
||||
|
||||
registry
|
||||
.add("foo1", "foo1", { sequence: 1 })
|
||||
.add("foo2", "foo2", { sequence: 2 })
|
||||
.add("foo1", "foo3", { force: true, sequence: 3 });
|
||||
|
||||
expect(registry.getEntries()).toEqual([
|
||||
["foo2", "foo2"],
|
||||
["foo1", "foo3"],
|
||||
]);
|
||||
});
|
||||
|
||||
test("can recursively open sub registry", () => {
|
||||
const registry = new Registry();
|
||||
|
||||
registry.category("sub").add("a", "b");
|
||||
expect(registry.category("sub").get("a")).toBe("b");
|
||||
});
|
||||
|
||||
test("can validate the values from a schema", () => {
|
||||
serverState.debug = "1";
|
||||
const schema = { name: String, age: { type: Number, optional: true } };
|
||||
const friendsRegistry = new Registry();
|
||||
friendsRegistry.addValidation(schema);
|
||||
expect(() => friendsRegistry.add("jean", { name: "Jean" })).not.toThrow();
|
||||
expect(friendsRegistry.get("jean")).toEqual({ name: "Jean" });
|
||||
expect(() => friendsRegistry.add("luc", { name: "Luc", age: 32 })).not.toThrow();
|
||||
expect(friendsRegistry.get("luc")).toEqual({ name: "Luc", age: 32 });
|
||||
expect(() => friendsRegistry.add("adrien", { name: 23 })).toThrow();
|
||||
expect(() => friendsRegistry.add("hubert", { age: 54 })).toThrow();
|
||||
expect(() => friendsRegistry.add("chris", { name: "chris", city: "Namur" })).toThrow();
|
||||
expect(() => friendsRegistry.addValidation({ something: Number })).toThrow();
|
||||
});
|
||||
|
||||
test("can validate by adding a schema after the registry is filled", async () => {
|
||||
serverState.debug = "1";
|
||||
const schema = { name: String };
|
||||
const friendsRegistry = new Registry();
|
||||
expect(() => friendsRegistry.add("jean", { name: 999 })).not.toThrow();
|
||||
expect(() => friendsRegistry.addValidation(schema)).toThrow();
|
||||
});
|
||||
|
||||
test("can validate subclassess", async () => {
|
||||
serverState.debug = "1";
|
||||
const schema = { component: { validate: (c) => c.prototype instanceof Component } };
|
||||
const widgetRegistry = new Registry();
|
||||
widgetRegistry.addValidation(schema);
|
||||
class Widget extends Component {} // eslint-disable-line
|
||||
expect(() => widgetRegistry.add("calculator", { component: Widget })).not.toThrow({
|
||||
message: "Support subclasses",
|
||||
});
|
||||
});
|
||||
|
||||
test("only validate in debug", async () => {
|
||||
const schema = { name: String };
|
||||
const registry = new Registry();
|
||||
registry.addValidation(schema);
|
||||
expect(() => registry.add("jean", { name: 50 })).not.toThrow({
|
||||
message: "There is no validation if not in debug mode",
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { drag, queryOne, queryRect, resize } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, reactive, xml } from "@odoo/owl";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { ResizablePanel } from "@web/core/resizable_panel/resizable_panel";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("Width cannot exceed viewport width", async () => {
|
||||
class Parent extends Component {
|
||||
static components = { ResizablePanel };
|
||||
static template = xml`
|
||||
<ResizablePanel>
|
||||
<p>A</p>
|
||||
<p>Cool</p>
|
||||
<p>Paragraph</p>
|
||||
</ResizablePanel>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_resizable_panel").toHaveCount(1);
|
||||
expect(".o_resizable_panel_handle").toHaveCount(1);
|
||||
|
||||
const vw = window.innerWidth;
|
||||
queryOne(".o_resizable_panel").style.width = `${vw + 100}px`;
|
||||
|
||||
expect(queryRect(".o_resizable_panel").width).toBeWithin(vw * 0.95, vw);
|
||||
});
|
||||
|
||||
test("handles right-to-left", async () => {
|
||||
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>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_resizable_panel").toHaveRect({ width: 30 });
|
||||
await (
|
||||
await drag(".o_resizable_panel_handle")
|
||||
).drop(".o_resizable_panel_handle", {
|
||||
position: {
|
||||
x: 10,
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryRect(".o_resizable_panel").width).toBeGreaterThan(
|
||||
queryOne(".parent-el").offsetWidth - 10 - 50
|
||||
);
|
||||
});
|
||||
|
||||
test("handles resize handle at start in fixed position", async () => {
|
||||
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>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
const resizablePanelEl = queryOne(".o_resizable_panel");
|
||||
resizablePanelEl.style.setProperty("right", "100px");
|
||||
expect(resizablePanelEl).toHaveRect({ width: 30 });
|
||||
|
||||
await (
|
||||
await drag(".o_resizable_panel_handle")
|
||||
).drop(".o_resizable_panel_handle", {
|
||||
position: {
|
||||
x: window.innerWidth - 200,
|
||||
},
|
||||
});
|
||||
expect(resizablePanelEl).toHaveRect({
|
||||
width: 100 + queryRect(".o_resizable_panel_handle").width / 2,
|
||||
});
|
||||
});
|
||||
|
||||
test("resizing the window adapts the panel", async () => {
|
||||
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>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
await (
|
||||
await drag(".o_resizable_panel_handle")
|
||||
).drop(".o_resizable_panel_handle", {
|
||||
position: {
|
||||
x: 99999,
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryOne(".o_resizable_panel").offsetWidth).toBe(398);
|
||||
queryOne(".parent-el").style.width = "200px";
|
||||
await resize();
|
||||
expect(queryOne(".o_resizable_panel").offsetWidth).toBe(198);
|
||||
});
|
||||
|
||||
test("minWidth props can be updated", async () => {
|
||||
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>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
const state = reactive({ minWidth: 20 });
|
||||
await mountWithCleanup(Parent, {
|
||||
props: { state },
|
||||
});
|
||||
await (
|
||||
await drag(".o_resizable_panel_handle")
|
||||
).drop(".o_resizable_panel_handle", {
|
||||
position: {
|
||||
x: 15,
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o_resizable_panel").toHaveRect({ width: 20 });
|
||||
state.minWidth = 40;
|
||||
await animationFrame();
|
||||
await (
|
||||
await drag(".o_resizable_panel_handle")
|
||||
).drop(".o_resizable_panel_handle", {
|
||||
position: {
|
||||
x: 15,
|
||||
},
|
||||
});
|
||||
expect(".o_resizable_panel").toHaveRect({ width: 40 });
|
||||
});
|
||||
1930
odoo-bringout-oca-ocb-web/web/static/tests/core/router.test.js
Normal file
1930
odoo-bringout-oca-ocb-web/web/static/tests/core/router.test.js
Normal file
File diff suppressed because it is too large
Load diff
543
odoo-bringout-oca-ocb-web/web/static/tests/core/scroller.test.js
Normal file
543
odoo-bringout-oca-ocb-web/web/static/tests/core/scroller.test.js
Normal file
|
|
@ -0,0 +1,543 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { getService, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { click, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { scrollTo } from "@web/core/utils/scrolling";
|
||||
import { WebClient } from "@web/webclient/webclient";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { redirect } from "@web/core/utils/urls";
|
||||
|
||||
test("Ignore empty hrefs", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="my_component">
|
||||
<a href="#" class="inactive_link">This link does nothing</a>
|
||||
<button class="btn btn-secondary">
|
||||
<a href="#">
|
||||
<i class="fa fa-trash"/>
|
||||
</a>
|
||||
</button>
|
||||
</div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
browser.location.hash = "#testscroller";
|
||||
|
||||
await click(".inactive_link");
|
||||
await animationFrame();
|
||||
|
||||
await click(".fa.fa-trash");
|
||||
await animationFrame();
|
||||
|
||||
expect(browser.location.hash).toBe("#testscroller");
|
||||
});
|
||||
|
||||
test("Simple rendering with a scroll", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div id="scroller" style="overflow: scroll; width: 400px; height: 150px">
|
||||
<div class="o_content">
|
||||
<a href="#scrollToHere" class="btn btn-primary">sroll to ...</a>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.
|
||||
Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,
|
||||
dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper
|
||||
congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est
|
||||
eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu
|
||||
massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut
|
||||
in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent
|
||||
egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue
|
||||
blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices
|
||||
posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque
|
||||
fermentum. Maecenas adipiscing ante non diam sodales hendrerit.
|
||||
</p>
|
||||
<p>
|
||||
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa
|
||||
suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc
|
||||
turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus
|
||||
nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis
|
||||
metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing
|
||||
elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor
|
||||
tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis
|
||||
vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus,
|
||||
et tristique ligula justo vitae magna.
|
||||
</p>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus,
|
||||
felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
|
||||
</p>
|
||||
<div id="scrollToHere">sroll here!</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect(queryOne("#scroller").scrollTop).toBe(0);
|
||||
await click(".btn.btn-primary");
|
||||
await animationFrame();
|
||||
expect(queryOne("#scroller").scrollTop).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("clicking to scroll on a web client shouldn't open the default app", async (assert) => {
|
||||
expect.assertions(2);
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="o_content" style="overflow:scroll;height:150px;width:400px">
|
||||
<a href="#scrollToHere" class="alert-link" role="button">sroll to ...</a>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.
|
||||
Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,
|
||||
dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper
|
||||
congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est
|
||||
eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu
|
||||
massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut
|
||||
in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent
|
||||
egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue
|
||||
blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices
|
||||
posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque
|
||||
fermentum. Maecenas adipiscing ante non diam sodales hendrerit.
|
||||
</p>
|
||||
<p>
|
||||
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa
|
||||
suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc
|
||||
turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus
|
||||
nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis
|
||||
metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing
|
||||
elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor
|
||||
tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis
|
||||
vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus,
|
||||
et tristique ligula justo vitae magna.
|
||||
</p>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus,
|
||||
felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
|
||||
</p>
|
||||
<div id="scrollToHere">sroll here!</div>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
static path = "my_component";
|
||||
}
|
||||
registry.category("actions").add("my_component", MyComponent);
|
||||
await mountWithCleanup(WebClient);
|
||||
await getService("action").doAction("my_component");
|
||||
|
||||
const scrollableParent = document.querySelector(".o_content");
|
||||
expect(scrollableParent.scrollTop).toBe(0);
|
||||
await click(".alert-link");
|
||||
expect(scrollableParent.scrollTop).not.toBe(0);
|
||||
});
|
||||
|
||||
test("Rendering with multiple anchors and scrolls", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div id="scroller" style="overflow: scroll; width: 400px; height: 150px">
|
||||
<div class="o_content">
|
||||
<h2 id="anchor3">ANCHOR 3</h2>
|
||||
<a href="#anchor1" class="link1">sroll to ...</a>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.
|
||||
Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,
|
||||
dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper
|
||||
congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est
|
||||
eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu
|
||||
massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut
|
||||
in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent
|
||||
egestas leo in pede. Praesent blandit odio eu enim.
|
||||
</p>
|
||||
<p>
|
||||
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa
|
||||
suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc
|
||||
turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus
|
||||
nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis
|
||||
metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing
|
||||
elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor
|
||||
tellus, aliquam faucibus, convallis id, congue eu, quam.
|
||||
</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h2 id="anchor2">ANCHOR 2</h2>
|
||||
<a href="#anchor3" class="link3">TO ANCHOR 3</a>
|
||||
<p>
|
||||
The table forces you to get the precise position of the element.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue.
|
||||
</p>
|
||||
<div id="anchor1">sroll here!</div>
|
||||
<a href="#anchor2" class="link2">TO ANCHOR 2</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
const scrollableParent = queryOne("#scroller");
|
||||
expect(scrollableParent.scrollTop).toBe(0);
|
||||
await click(".link1");
|
||||
|
||||
// The element must be contained in the scrollable parent (top and bottom)
|
||||
const isVisible = (selector) => {
|
||||
const el = queryOne(selector);
|
||||
return (
|
||||
el.getBoundingClientRect().bottom <= scrollableParent.getBoundingClientRect().bottom &&
|
||||
el.getBoundingClientRect().top >= scrollableParent.getBoundingClientRect().top
|
||||
);
|
||||
};
|
||||
|
||||
expect(isVisible("#anchor1")).toBe(true);
|
||||
await click(".link2");
|
||||
await animationFrame();
|
||||
expect(isVisible("#anchor2")).toBe(true);
|
||||
await click(".link3");
|
||||
await animationFrame();
|
||||
expect(isVisible("#anchor3")).toBe(true);
|
||||
});
|
||||
|
||||
test("clicking anchor when no scrollable", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div id="scroller" style="overflow: auto; width: 400px; height: 150px">
|
||||
<div class="o_content">
|
||||
<a href="#scrollToHere" class="btn btn-primary">scroll to ...</a>
|
||||
<div class="active-container">
|
||||
<p>There is no scrollable with only the height of this element</p>
|
||||
</div>
|
||||
<div class="inactive-container" style="max-height: 0; 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,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus,
|
||||
felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
|
||||
</p>
|
||||
<div id="scrollToHere">should try to scroll here only if scrollable!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
const scrollableParent = queryOne("#scroller");
|
||||
expect(scrollableParent.scrollTop).toBe(0);
|
||||
await click(".btn.btn-primary");
|
||||
await animationFrame();
|
||||
expect(scrollableParent.scrollTop).toBe(0, { message: "no scroll happened" });
|
||||
queryOne(".inactive-container").style.maxHeight = "unset";
|
||||
await click(".btn.btn-primary");
|
||||
await animationFrame();
|
||||
expect(scrollableParent.scrollTop).toBeGreaterThan(0, { message: "a scroll happened" });
|
||||
});
|
||||
|
||||
test("clicking anchor when multi levels scrollables", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div id="scroller" style="overflow: auto; width: 400px; height: 150px">
|
||||
<div class="o_content scrollable-1">
|
||||
<a href="#scroll1" class="btn1 btn btn-primary">go to level 2 anchor</a>
|
||||
<div>
|
||||
<p>This is some content</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.
|
||||
Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,
|
||||
dolor. Cras elementum ultrices diam. Maecenas
|
||||
</p>
|
||||
</div>
|
||||
<div class="scrollable-2" style="background: green; overflow: auto; height: 100px;">
|
||||
<h2>This is level 1 of scrollable</h2>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus,
|
||||
felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
|
||||
</p>
|
||||
<div style="background: lime;">
|
||||
<h2>This is level 2 of scrollable</h2>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus,
|
||||
felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
|
||||
</p>
|
||||
<div id="scroll1" style="background: orange;">this element is contained in a scrollable metaverse!</div>
|
||||
<a href="#scroll2" class="btn2 btn btn-primary">go to level 1 anchor</a>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.
|
||||
Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,
|
||||
dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper
|
||||
congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est
|
||||
eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu
|
||||
massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut
|
||||
in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent
|
||||
egestas leo in pede. Praesent blandit odio eu enim.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="scroll2" style="background: orange;">this is an anchor at level 1!</div>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus,
|
||||
felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
const scrollableParent = queryOne("#scroller");
|
||||
|
||||
const border = (selector) => {
|
||||
const el = queryOne(selector);
|
||||
// Returns the state of the element in relation to the borders
|
||||
const element = el.getBoundingClientRect();
|
||||
const scrollable = scrollableParent.getBoundingClientRect();
|
||||
return {
|
||||
top: parseInt(element.top - scrollable.top),
|
||||
bottom: parseInt(scrollable.bottom - element.bottom),
|
||||
};
|
||||
};
|
||||
|
||||
expect(scrollableParent.scrollTop).toBe(0);
|
||||
await click(".btn1");
|
||||
await animationFrame();
|
||||
expect(border("#scroll1").top).toBeLessThan(10, {
|
||||
message: "the element must be near the top border",
|
||||
});
|
||||
await click(".btn2");
|
||||
await animationFrame();
|
||||
expect(border("#scroll2").top).toBeLessThan(10, {
|
||||
message: "the element must be near the top border",
|
||||
});
|
||||
});
|
||||
|
||||
test("Simple scroll to HTML elements", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div id="scroller" style="overflow: auto; width: 400px; height: 150px">
|
||||
<div class="o_content">
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus.
|
||||
</p>
|
||||
<div id="o-div-1">A div is an HTML element</div>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.
|
||||
Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,
|
||||
dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper
|
||||
congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est
|
||||
eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu
|
||||
massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut
|
||||
in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent
|
||||
egestas leo in pede. Praesent blandit odio eu enim.
|
||||
</p>
|
||||
<p>
|
||||
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa
|
||||
suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc
|
||||
turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus
|
||||
nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis
|
||||
metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing
|
||||
elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor
|
||||
tellus, aliquam faucibus, convallis id, congue eu, quam.
|
||||
</p>
|
||||
<div id="o-div-2">A div is an HTML element</div>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue.
|
||||
</p>
|
||||
<div 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>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
const scrollableParent = queryOne("#scroller");
|
||||
|
||||
// The element must be contained in the scrollable parent (top and bottom)
|
||||
const isVisible = (selector) => {
|
||||
const el = queryOne(selector);
|
||||
return (
|
||||
el.getBoundingClientRect().bottom <= scrollableParent.getBoundingClientRect().bottom &&
|
||||
el.getBoundingClientRect().top >= scrollableParent.getBoundingClientRect().top
|
||||
);
|
||||
};
|
||||
|
||||
const border = (selector) => {
|
||||
const el = queryOne(selector);
|
||||
// Returns the state of the element in relation to the borders
|
||||
const element = el.getBoundingClientRect();
|
||||
const scrollable = scrollableParent.getBoundingClientRect();
|
||||
return {
|
||||
top: parseInt(element.top - scrollable.top),
|
||||
bottom: parseInt(scrollable.bottom - element.bottom),
|
||||
};
|
||||
};
|
||||
|
||||
// When using scrollTo to an element, this should just scroll
|
||||
// until the element is visible in the scrollable parent
|
||||
const subScrollable = queryOne("#sub-scrollable");
|
||||
subScrollable.style.overflowY = "scroll";
|
||||
subScrollable.style.height = getComputedStyle(subScrollable)["line-height"];
|
||||
subScrollable.style.width = "300px";
|
||||
|
||||
expect(isVisible("#o-div-1")).toBe(true);
|
||||
expect(isVisible("#o-div-2")).toBe(false);
|
||||
expect(border("#o-div-1").top).not.toBe(0);
|
||||
|
||||
scrollTo(queryOne("#o-div-2"));
|
||||
expect(isVisible("#o-div-1")).toBe(false);
|
||||
expect(isVisible("#o-div-2")).toBe(true);
|
||||
expect(border("#o-div-2").bottom).toBe(0);
|
||||
|
||||
scrollTo(queryOne("#o-div-1"));
|
||||
expect(isVisible("#o-div-3")).toBe(false);
|
||||
expect(isVisible("#o-div-4")).toBe(false);
|
||||
expect(border("#o-div-1").top).toBe(0);
|
||||
|
||||
// Specify a scrollable which can not be scrolled, the effective scrollable
|
||||
// should be its closest actually scrollable parent.
|
||||
scrollTo(queryOne("#o-div-3"), { scrollable: queryOne("#fake-scrollable") });
|
||||
expect(isVisible("#o-div-3")).toBe(true);
|
||||
expect(isVisible("#o-div-4")).toBe(false);
|
||||
expect(border("#o-div-3").bottom).toBe(0);
|
||||
|
||||
// Reset the position
|
||||
scrollTo(queryOne("#o-div-1"));
|
||||
expect(isVisible("#o-div-1")).toBe(true);
|
||||
expect(isVisible("#o-div-3")).toBe(false);
|
||||
expect(isVisible("#o-div-4")).toBe(false);
|
||||
|
||||
// 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(queryOne("#o-div-4"), { isAnchor: true });
|
||||
expect(isVisible("#o-div-4")).toBe(true);
|
||||
expect(border("#o-div-4").top).toBe(0);
|
||||
expect(border("#sub-scrollable").top).toBe(0);
|
||||
});
|
||||
|
||||
test("scroll to anchor from load", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml/* xml */ `
|
||||
<div class="o_content" style="overflow:scroll;height:150px;width:400px">
|
||||
<a href="#scrollToHere" class="alert-link" role="button">sroll to ...</a>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.
|
||||
Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed,
|
||||
dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper
|
||||
congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est
|
||||
eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu
|
||||
massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut
|
||||
in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent
|
||||
egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue
|
||||
blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices
|
||||
posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque
|
||||
fermentum. Maecenas adipiscing ante non diam sodales hendrerit.
|
||||
</p>
|
||||
<p>
|
||||
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa
|
||||
suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc
|
||||
turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus
|
||||
nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis
|
||||
metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing
|
||||
elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor
|
||||
tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis
|
||||
vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus,
|
||||
et tristique ligula justo vitae magna.
|
||||
</p>
|
||||
<p>
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis,
|
||||
ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem
|
||||
at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo
|
||||
placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum
|
||||
augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus,
|
||||
felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
|
||||
</p>
|
||||
<div id="scrollToHere">sroll here!</div>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
static path = "my_component";
|
||||
}
|
||||
registry.category("actions").add("my_component", MyComponent);
|
||||
redirect("/odoo/my_component#scrollToHere");
|
||||
await mountWithCleanup(WebClient);
|
||||
await animationFrame();
|
||||
|
||||
const scrollableParent = document.querySelector(".o_content");
|
||||
expect(scrollableParent.scrollTop).not.toBe(0);
|
||||
});
|
||||
1173
odoo-bringout-oca-ocb-web/web/static/tests/core/select_menu.test.js
Normal file
1173
odoo-bringout-oca-ocb-web/web/static/tests/core/select_menu.test.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,201 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, queryAttribute } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { TagsList } from "@web/core/tags_list/tags_list";
|
||||
|
||||
test("Can be rendered with different tags", async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { TagsList };
|
||||
static template = xml`<TagsList tags="tags" />`;
|
||||
setup() {
|
||||
this.tags = [
|
||||
{
|
||||
id: "tag1",
|
||||
text: "Earth",
|
||||
},
|
||||
{
|
||||
colorIndex: 1,
|
||||
id: "tag2",
|
||||
text: "Wind",
|
||||
onDelete: () => {
|
||||
expect.step(`tag2 delete button has been clicked`);
|
||||
},
|
||||
},
|
||||
{
|
||||
colorIndex: 2,
|
||||
id: "tag3",
|
||||
text: "Fire",
|
||||
onClick: () => {
|
||||
expect.step(`tag3 has been clicked`);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_tag").toHaveCount(3);
|
||||
|
||||
await click(".o_tag:nth-of-type(2) .o_delete");
|
||||
expect.verifySteps(["tag2 delete button has been clicked"]);
|
||||
|
||||
await click(".o_tag:nth-of-type(3)");
|
||||
expect.verifySteps(["tag3 has been clicked"]);
|
||||
});
|
||||
|
||||
test("Tags can be displayed with an image", async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { TagsList };
|
||||
static template = xml`<TagsList tags="tags" />`;
|
||||
setup() {
|
||||
this.tags = [
|
||||
{
|
||||
img: "fake/url",
|
||||
id: "tag1",
|
||||
text: "Earth",
|
||||
},
|
||||
{
|
||||
img: "fake/url/2",
|
||||
id: "tag2",
|
||||
text: "Wind",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_tag").toHaveCount(2);
|
||||
expect(".o_tag:nth-of-type(1) img").toHaveAttribute("data-src", "fake/url");
|
||||
expect(".o_tag:nth-of-type(2) img").toHaveAttribute("data-src", "fake/url/2");
|
||||
});
|
||||
|
||||
test("Tags can be displayed with an icon", async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { TagsList };
|
||||
static template = xml`<TagsList tags="tags" />`;
|
||||
setup() {
|
||||
this.tags = [
|
||||
{
|
||||
icon: "fa-trash",
|
||||
id: "tag1",
|
||||
text: "Bad",
|
||||
},
|
||||
{
|
||||
icon: "fa-check",
|
||||
id: "tag2",
|
||||
text: "Good",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_tag").toHaveCount(2);
|
||||
expect(".o_tag:nth-of-type(1) i").toHaveClass("fa fa-trash");
|
||||
expect(".o_tag:nth-of-type(2) i").toHaveClass("fa fa-check");
|
||||
});
|
||||
|
||||
test("Limiting the visible tags displays a counter", async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { TagsList };
|
||||
static template = xml`<TagsList tags="tags" visibleItemsLimit="state.visibleItemsLimit" />`;
|
||||
setup() {
|
||||
this.state = useState({
|
||||
visibleItemsLimit: 3,
|
||||
});
|
||||
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",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mountWithCleanup(Parent);
|
||||
// visibleItemsLimit = 3 -> displays 2 tags + 1 counter (4 tags left)
|
||||
expect(".o_tag").toHaveCount(2);
|
||||
expect(".rounded").toHaveText("+4", {
|
||||
message: "the counter displays 4 more items",
|
||||
});
|
||||
expect(JSON.parse(queryAttribute(".rounded", "data-tooltip-info"))).toEqual(
|
||||
{
|
||||
tags: [
|
||||
{ text: "Fire", id: "tag3" },
|
||||
{ text: "Earth", id: "tag4" },
|
||||
{ text: "Wind", id: "tag5" },
|
||||
{ text: "Dust", id: "tag6" },
|
||||
],
|
||||
},
|
||||
{ message: "the counter has a tooltip displaying other items" }
|
||||
);
|
||||
|
||||
parent.state.visibleItemsLimit = 5;
|
||||
await animationFrame();
|
||||
// visibleItemsLimit = 5 -> displays 4 tags + 1 counter (2 tags left)
|
||||
expect(".o_tag").toHaveCount(4);
|
||||
expect(".rounded").toHaveText("+2");
|
||||
|
||||
parent.state.visibleItemsLimit = 6;
|
||||
await animationFrame();
|
||||
// visibleItemsLimit = 6 -> displays 6 tags + 0 counter (0 tag left)
|
||||
expect(".o_tag").toHaveCount(6);
|
||||
expect(".rounded").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Tags with img have a backdrop only if they can be deleted", async () => {
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static components = { TagsList };
|
||||
static template = xml`<TagsList tags="tags" />`;
|
||||
setup() {
|
||||
this.tags = [
|
||||
{
|
||||
id: "tag1",
|
||||
text: "Earth",
|
||||
img: "fake/url",
|
||||
},
|
||||
{
|
||||
colorIndex: 1,
|
||||
id: "tag2",
|
||||
text: "Wind",
|
||||
img: "fake/url",
|
||||
onDelete: () => {},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(".o_tag").toHaveCount(2);
|
||||
expect(".o_tag:nth-of-type(1) .o_avatar_backdrop").toHaveCount(0);
|
||||
expect(".o_tag:nth-of-type(2) .o_avatar_backdrop").toHaveCount(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,372 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { applyInheritance } from "@web/core/template_inheritance";
|
||||
import { serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
const parser = new DOMParser();
|
||||
const serializer = new XMLSerializer();
|
||||
|
||||
function _applyInheritance(arch, inherits) {
|
||||
const archXmlDoc = parser.parseFromString(arch, "text/xml");
|
||||
const inheritsDoc = parser.parseFromString(inherits, "text/xml");
|
||||
const modifiedTemplate = applyInheritance(
|
||||
archXmlDoc.documentElement,
|
||||
inheritsDoc.documentElement,
|
||||
"test/url"
|
||||
);
|
||||
return serializer.serializeToString(modifiedTemplate);
|
||||
}
|
||||
|
||||
test("no operation", async () => {
|
||||
const arch = `<t t-name="web.A"> <div><h2>Title</h2>text</div> </t>`;
|
||||
const operations = `<t/>`;
|
||||
expect(_applyInheritance(arch, operations)).toBe(arch);
|
||||
});
|
||||
|
||||
test("single operation: replace", async () => {
|
||||
const toTest = [
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div><h2>Title</h2>text</div> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div/h2" position="replace"><h3>Other title</h3></xpath>
|
||||
</t>`,
|
||||
result: `<t t-name="web.A"> <div><h3>Other title</h3>text</div> </t>`,
|
||||
// TODO check if text should be there? (I think there is a bug in python code)
|
||||
},
|
||||
];
|
||||
for (const { arch, operations, result } of toTest) {
|
||||
expect(_applyInheritance(arch, operations)).toBe(result);
|
||||
}
|
||||
});
|
||||
|
||||
test("single operation: replace (debug mode)", async () => {
|
||||
serverState.debug = "1";
|
||||
const toTest = [
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div><h2>Title</h2>text</div> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div/h2" position="replace"><h3>Other title</h3></xpath>
|
||||
</t>`,
|
||||
result: `<t t-name="web.A"> <div><!-- From file: test/url ; expr="./div/h2" ; position="replace" --><h3>Other title</h3>text</div> </t>`,
|
||||
},
|
||||
];
|
||||
for (const { arch, operations, result } of toTest) {
|
||||
expect(_applyInheritance(arch, operations)).toBe(result);
|
||||
}
|
||||
});
|
||||
|
||||
test("single operation: replace root (and use a $0)", async () => {
|
||||
const toTest = [
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div>I was petrified</div> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="." position="replace"><div>At first I was afraid</div>$0</xpath>
|
||||
</t>`,
|
||||
result: `<div t-name="web.A">At first I was afraid</div>`,
|
||||
// in outer mode with no parent only first child of operation is kept
|
||||
},
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div>I was petrified</div> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="." position="replace"> <div>$0</div><div>At first I was afraid</div> </xpath>
|
||||
</t>`,
|
||||
result: `<div t-name="web.A"><t t-name="web.A"> <div>I was petrified</div> </t></div>`,
|
||||
},
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div>I was petrified</div> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="." position="replace"> <t><t t-if="cond"><div>At first I was afraid</div></t><t t-else="">$0</t></t> </xpath>
|
||||
</t>`,
|
||||
result: `<t t-name="web.A"><t t-if="cond"><div>At first I was afraid</div></t><t t-else=""><t t-name="web.A"> <div>I was petrified</div> </t></t></t>`,
|
||||
},
|
||||
{
|
||||
arch: `<form t-name="template_1_1" random-attr="gloria"> <div>At first I was afraid</div> <form>Inner Form</form> </form>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="//form" position="replace">
|
||||
<div> Form replacer </div>
|
||||
</xpath>
|
||||
</t>`,
|
||||
result: `<div t-name="template_1_1"> Form replacer </div>`,
|
||||
},
|
||||
{
|
||||
arch: `<form t-name="template_1_1" random-attr="gloria"> <div>At first I was afraid</div> </form>`,
|
||||
operations: `
|
||||
<t t-name="template_1_2">
|
||||
<xpath expr="." position="replace">
|
||||
<div overriden-attr="overriden">And I grew strong</div>
|
||||
</xpath>
|
||||
</t>
|
||||
`,
|
||||
result: `<div overriden-attr="overriden" t-name="template_1_1">And I grew strong</div>`,
|
||||
},
|
||||
];
|
||||
for (const { arch, operations, result } of toTest) {
|
||||
expect(_applyInheritance(arch, operations)).toBe(result);
|
||||
}
|
||||
});
|
||||
|
||||
test("single operation: replace (mode inner)", async () => {
|
||||
const toTest = [
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div> A <span/> B <span/> C </div> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div" position="replace" mode="inner"> E <div/> F <span attr1="12"/> </xpath>
|
||||
</t>`,
|
||||
result: `<t t-name="web.A"> <div> E <div/> F <span attr1="12"/> </div> </t>`,
|
||||
},
|
||||
];
|
||||
for (const { arch, operations, result } of toTest) {
|
||||
expect(_applyInheritance(arch, operations)).toBe(result);
|
||||
}
|
||||
});
|
||||
|
||||
test("single operation: replace (mode inner) (debug mode)", async () => {
|
||||
serverState.debug = "1";
|
||||
const toTest = [
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div> A <span/> B <span/> C </div> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div" position="replace" mode="inner"> E <div/> F <span attr1="12"/> </xpath>
|
||||
</t>`,
|
||||
result: `<t t-name="web.A"> <div><!-- From file: test/url ; expr="./div" ; position="replace" ; mode="inner" --> E <div/> F <span attr1="12"/> </div> </t>`,
|
||||
},
|
||||
];
|
||||
for (const { arch, operations, result } of toTest) {
|
||||
expect(_applyInheritance(arch, operations, "/test/url")).toBe(result);
|
||||
}
|
||||
});
|
||||
|
||||
test("single operation: before", async () => {
|
||||
const toTest = [
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div>AAB is the best<h2>Title</h2>text</div> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div/h2" position="before"> <h3>Other title</h3>Yooplahoo!<h4>Yet another title</h4> </xpath>
|
||||
</t>`,
|
||||
result: `<t t-name="web.A"> <div>AAB is the best <h3>Other title</h3>Yooplahoo!<h4>Yet another title</h4> <h2>Title</h2>text</div> </t>`,
|
||||
},
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div>AAB is the best<h2>Title</h2><div><span>Ola</span></div></div> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div/h2" position="before"> <xpath expr="./div/div/span" position="move" /> </xpath>
|
||||
</t>`,
|
||||
result: `<t t-name="web.A"> <div>AAB is the best <span>Ola</span> <h2>Title</h2><div/></div> </t>`,
|
||||
},
|
||||
{
|
||||
arch: `<t t-name="web.A"> a <div/> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div" position="before"> 4 </xpath>
|
||||
</t>`,
|
||||
result: `<t t-name="web.A"> a 4 <div/> </t>`,
|
||||
},
|
||||
];
|
||||
for (const { arch, operations, result } of toTest) {
|
||||
expect(_applyInheritance(arch, operations)).toBe(result);
|
||||
}
|
||||
});
|
||||
|
||||
test("single operation: inside", async () => {
|
||||
const toTest = [
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div>AAB is the best <h2>Title</h2> <div/> </div> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div/div" position="inside"> Hop! <xpath expr="./div/h2" position="move" /> <span>Yellow</span> </xpath>
|
||||
</t>`,
|
||||
result: `<t t-name="web.A"> <div>AAB is the best <div> Hop! <h2>Title</h2> <span>Yellow</span> </div> </div> </t>`,
|
||||
},
|
||||
{
|
||||
arch: `<t><div/></t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div" position="inside">4</xpath>
|
||||
</t>`,
|
||||
result: `<t><div>4</div></t>`,
|
||||
},
|
||||
{
|
||||
arch: `<t><div>\na \n </div></t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div" position="inside">4</xpath>
|
||||
</t>`,
|
||||
result: `<t><div>\na4</div></t>`,
|
||||
},
|
||||
{
|
||||
arch: `<t>a<div></div><span/></t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div" position="inside"><span/></xpath>
|
||||
</t>`,
|
||||
result: `<t>a<div><span/></div><span/></t>`,
|
||||
},
|
||||
];
|
||||
for (const { arch, operations, result } of toTest) {
|
||||
expect(_applyInheritance(arch, operations)).toBe(result);
|
||||
}
|
||||
});
|
||||
|
||||
test("single operation: after", async () => {
|
||||
const toTest = [
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div> AAB is the best <h2>Title</h2> <div id="1"/> <div id="2"/> </div> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div/h2" position="after"> Hop! <xpath expr="./div/div[2]" position="move" /> <span>Yellow</span> </xpath>
|
||||
</t>`,
|
||||
result: `<t t-name="web.A"> <div> AAB is the best <h2>Title</h2> Hop! <div id="2"/> <span>Yellow</span> <div id="1"/> </div> </t>`,
|
||||
},
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div/>a </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div" position="after">4</xpath>
|
||||
</t>`,
|
||||
result: `<t t-name="web.A"> <div/>4a </t>`,
|
||||
},
|
||||
];
|
||||
for (const { arch, operations, result } of toTest) {
|
||||
expect(_applyInheritance(arch, operations)).toBe(result);
|
||||
}
|
||||
});
|
||||
|
||||
test("single operation: attributes", async (assert) => {
|
||||
const toTest = [
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div attr1="12" attr2="a b" attr3="to remove" /> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div" position="attributes">
|
||||
<attribute name="attr1">45</attribute>
|
||||
<attribute name="attr3"></attribute>
|
||||
<attribute name="attr2" add="c" separator=" "></attribute>
|
||||
<attribute name="attr2" remove="a" separator=" "></attribute>
|
||||
<attribute name="attr4">new</attribute>
|
||||
</xpath>
|
||||
</t>`,
|
||||
result: `<t t-name="web.A"> <div attr1="45" attr2="b c" attr4="new"/> </t>`,
|
||||
},
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div><a href="1"/><div><a href="2"/></div></div> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="//a[@href='2']" position="attributes">
|
||||
<attribute name="found">1</attribute>
|
||||
</xpath>
|
||||
</t>`,
|
||||
result: `<t t-name="web.A"> <div><a href="1"/><div><a href="2" found="1"/></div></div> </t>`,
|
||||
},
|
||||
];
|
||||
for (const { arch, operations, result } of toTest) {
|
||||
expect(_applyInheritance(arch, operations)).toBe(result);
|
||||
}
|
||||
});
|
||||
|
||||
test("single operation: attributes (debug mode)", async () => {
|
||||
serverState.debug = "1";
|
||||
const toTest = [
|
||||
{
|
||||
arch: `<t t-name="web.A"> <div attr1="12" attr2="a b" attr3="to remove" /> </t>`,
|
||||
operations: `
|
||||
<t>
|
||||
<xpath expr="./div" position="attributes">
|
||||
<attribute name="attr1">45</attribute>
|
||||
<attribute name="attr3"></attribute>
|
||||
<attribute name="attr2" add="c" separator=" "></attribute>
|
||||
<attribute name="attr2" remove="a" separator=" "></attribute>
|
||||
<attribute name="attr4">new</attribute>
|
||||
</xpath>
|
||||
</t>`,
|
||||
result: `<t t-name="web.A"> <!-- From file: test/url ; expr="./div" ; position="attributes" --><div attr1="45" attr2="b c" attr4="new"/> </t>`,
|
||||
},
|
||||
];
|
||||
for (const { arch, operations, result } of toTest) {
|
||||
expect(_applyInheritance(arch, operations)).toBe(result);
|
||||
}
|
||||
});
|
||||
|
||||
test("xpath with hasclass", async () => {
|
||||
const toTest = [
|
||||
{
|
||||
arch: `<t><div class="abc"/></t>`,
|
||||
operations: `<t><xpath expr="./div[hasclass('abc')]" position="replace"></xpath></t>`,
|
||||
result: `<t/>`,
|
||||
},
|
||||
{
|
||||
arch: `<t><div class="abc "/></t>`,
|
||||
operations: `<t><xpath expr="./div[hasclass('abc')]" position="replace"></xpath></t>`,
|
||||
result: `<t/>`,
|
||||
},
|
||||
{
|
||||
arch: `<t><div class=" abc"/></t>`,
|
||||
operations: `<t><xpath expr="./div[hasclass('abc')]" position="replace"></xpath></t>`,
|
||||
result: `<t/>`,
|
||||
},
|
||||
{
|
||||
arch: `<t><div class=" abc "/></t>`,
|
||||
operations: `<t><xpath expr="./div[hasclass('abc')]" position="replace"></xpath></t>`,
|
||||
result: `<t/>`,
|
||||
},
|
||||
{
|
||||
arch: `<t><div class="d abc e"/></t>`,
|
||||
operations: `<t><xpath expr="./div[hasclass('abc')]" position="replace"></xpath></t>`,
|
||||
result: `<t/>`,
|
||||
},
|
||||
{
|
||||
arch: `<t><div class="d abc"/></t>`,
|
||||
operations: `<t><xpath expr="./div[hasclass('abc')]" position="replace"></xpath></t>`,
|
||||
result: `<t/>`,
|
||||
},
|
||||
{
|
||||
arch: `<t><div class="abc d"/></t>`,
|
||||
operations: `<t><xpath expr="./div[hasclass('abc')]" position="replace"></xpath></t>`,
|
||||
result: `<t/>`,
|
||||
},
|
||||
{
|
||||
arch: `<t><div class="abcd e abc"/></t>`,
|
||||
operations: `<t><xpath expr="./div[hasclass('abc')]" position="replace"></xpath></t>`,
|
||||
result: `<t/>`,
|
||||
},
|
||||
{
|
||||
arch: `<t><div class="abcd e abc"/></t>`,
|
||||
operations: `<t><xpath expr="./div[hasclass('abc', 'abcd' )]" position="replace"></xpath></t>`,
|
||||
result: `<t/>`,
|
||||
},
|
||||
{
|
||||
arch: `<t><div class="abcd e abc"/></t>`,
|
||||
operations: `<t><xpath expr="./div[hasclass('abc') and hasclass('abcd')]" position="replace"></xpath></t>`,
|
||||
result: `<t/>`,
|
||||
},
|
||||
{
|
||||
arch: `<t><div class="abcd"/></t>`,
|
||||
operations: `<t><xpath expr="./div[hasclass('abc')]" position="replace"></xpath></t>`,
|
||||
isError: true,
|
||||
},
|
||||
{
|
||||
arch: `<t><div class="dabc"/></t>`,
|
||||
operations: `<t><xpath expr="./div[hasclass('abc')]" position="replace"></xpath></t>`,
|
||||
isError: true,
|
||||
},
|
||||
{
|
||||
arch: `<t><div class="dabc"/></t>`,
|
||||
operations: `<t><xpath expr="./div[ends-with(@class, 'bc')]" position="replace"></xpath></t>`,
|
||||
isError: true,
|
||||
},
|
||||
];
|
||||
for (const { arch, operations, result, isError } of toTest) {
|
||||
if (isError) {
|
||||
expect(() => _applyInheritance(arch, operations)).toThrow();
|
||||
} else {
|
||||
expect(_applyInheritance(arch, operations)).toBe(result);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,388 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { click, drag, hover, leave, pointerDown, pointerUp, queryOne } from "@odoo/hoot-dom";
|
||||
import { advanceTime, animationFrame, mockTouch, runAllTimers } from "@odoo/hoot-mock";
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { makeMockEnv, mockService, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { OPEN_DELAY, SHOW_AFTER_DELAY } from "@web/core/tooltip/tooltip_service";
|
||||
|
||||
test.tags("desktop");
|
||||
test("basic rendering", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<button class="mybtn" data-tooltip="hello">Action</button>`;
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
await hover(".mybtn");
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover").toHaveText("hello");
|
||||
|
||||
await leave();
|
||||
await animationFrame();
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("basic rendering 2", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<span data-tooltip="hello" class="outer_span"><span class="inner_span">Action</span></span>`;
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
await hover(".inner_span");
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover").toHaveText("hello");
|
||||
|
||||
await hover(".outer_span");
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
|
||||
await leave();
|
||||
await animationFrame();
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("remove element with opened tooltip", async () => {
|
||||
let compState;
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<div>
|
||||
<button t-if="state.visible" data-tooltip="hello">Action</button>
|
||||
</div>`;
|
||||
setup() {
|
||||
this.state = useState({ visible: true });
|
||||
compState = this.state;
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect("button").toHaveCount(1);
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
await hover("button");
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
|
||||
compState.visible = false;
|
||||
await animationFrame();
|
||||
expect("button").toHaveCount(0);
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("rendering with several tooltips", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<div>
|
||||
<button class="button_1" data-tooltip="tooltip 1">Action 1</button>
|
||||
<button class="button_2" data-tooltip="tooltip 2">Action 2</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
|
||||
await hover("button.button_1");
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover").toHaveText("tooltip 1");
|
||||
|
||||
await hover("button.button_2");
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover").toHaveText("tooltip 2");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("positioning", async () => {
|
||||
mockService("popover", (...kargs) => {
|
||||
const popover = popoverService.start(...kargs);
|
||||
return {
|
||||
add(...args) {
|
||||
const { position } = args[3];
|
||||
if (position) {
|
||||
expect.step(`popover added with position: ${position}`);
|
||||
} else {
|
||||
expect.step(`popover added with default positioning`);
|
||||
}
|
||||
return popover.add(...args);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<div style="height: 400px; padding: 40px">
|
||||
<button class="default" data-tooltip="default">Default</button>
|
||||
<button class="top" data-tooltip="top" data-tooltip-position="top">Top</button>
|
||||
<button class="right" data-tooltip="right" data-tooltip-position="right">Right</button>
|
||||
<button class="bottom" data-tooltip="bottom" data-tooltip-position="bottom">Bottom</button>
|
||||
<button class="left" data-tooltip="left" data-tooltip-position="left">Left</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
// default
|
||||
await hover("button.default");
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover").toHaveText("default");
|
||||
expect.verifySteps(["popover added with default positioning"]);
|
||||
|
||||
// top
|
||||
await hover("button.top");
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover").toHaveText("top");
|
||||
expect.verifySteps(["popover added with position: top"]);
|
||||
|
||||
// right
|
||||
await hover("button.right");
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover").toHaveText("right");
|
||||
expect.verifySteps(["popover added with position: right"]);
|
||||
|
||||
// bottom
|
||||
await hover("button.bottom");
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover").toHaveText("bottom");
|
||||
expect.verifySteps(["popover added with position: bottom"]);
|
||||
|
||||
// left
|
||||
await hover("button.left");
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover").toHaveText("left");
|
||||
expect.verifySteps(["popover added with position: left"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("tooltip with a template, no info", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<button data-tooltip-template="my_tooltip_template">Action</button>
|
||||
`;
|
||||
}
|
||||
|
||||
await makeMockEnv({ tooltip_text: "tooltip" });
|
||||
await mountWithCleanup(MyComponent, {
|
||||
templates: {
|
||||
my_tooltip_template: /* xml */ `<i t-esc='env.tooltip_text'/>`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o-tooltip").toHaveCount(0);
|
||||
|
||||
await hover("button");
|
||||
await runAllTimers();
|
||||
|
||||
expect(".o-tooltip").toHaveCount(1);
|
||||
expect(".o-tooltip").toHaveInnerHTML("<i>tooltip</i>");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("tooltip with a template and info", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<button
|
||||
data-tooltip-template="my_tooltip_template"
|
||||
t-att-data-tooltip-info="info">
|
||||
Action
|
||||
</button>
|
||||
`;
|
||||
get info() {
|
||||
return JSON.stringify({ x: 3, y: "abc" });
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent, {
|
||||
templates: {
|
||||
my_tooltip_template: /* xml */ `
|
||||
<ul>
|
||||
<li>X: <t t-esc="x"/></li>
|
||||
<li>Y: <t t-esc="y"/></li>
|
||||
</ul>
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(".o-tooltip").toHaveCount(0);
|
||||
|
||||
await hover("button");
|
||||
await runAllTimers();
|
||||
|
||||
expect(".o-tooltip").toHaveCount(1);
|
||||
expect(".o-tooltip").toHaveInnerHTML("<ul><li>X: 3</li><li>Y: abc</li></ul>");
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("empty tooltip, no template", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<button t-att-data-tooltip="tooltip">Action</button>`;
|
||||
get tooltip() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
expect(".o-tooltip").toHaveCount(0);
|
||||
await hover("button");
|
||||
await runAllTimers();
|
||||
expect(".o-tooltip").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("tooltip with a delay", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<button class="myBtn" data-tooltip="'helpful tooltip'" data-tooltip-delay="2000">Action</button>`;
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
expect(".o-tooltip").toHaveCount(0);
|
||||
|
||||
await hover("button.myBtn");
|
||||
await advanceTime(OPEN_DELAY);
|
||||
expect(".o-tooltip").toHaveCount(0);
|
||||
await advanceTime(2000 - OPEN_DELAY);
|
||||
expect(".o-tooltip").toHaveCount(1);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("tooltip does not crash with disappearing target", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<button class="mybtn" data-tooltip="hello">Action</button>`;
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
|
||||
await hover(".mybtn");
|
||||
await animationFrame();
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
|
||||
// the element disappeared from the DOM during the setTimeout
|
||||
queryOne(".mybtn").remove();
|
||||
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("tooltip using touch enabled device", async () => {
|
||||
mockTouch(true);
|
||||
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<button class="mybtn" data-tooltip="hello">Action</button>`;
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
|
||||
await drag(".mybtn");
|
||||
await animationFrame();
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
|
||||
await advanceTime(SHOW_AFTER_DELAY);
|
||||
await advanceTime(OPEN_DELAY);
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover").toHaveText("hello");
|
||||
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover").toHaveText("hello");
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("touch rendering - hold-to-show", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<button data-tooltip="hello">Action</button>`;
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
await pointerDown("button");
|
||||
await animationFrame();
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
|
||||
await advanceTime(SHOW_AFTER_DELAY);
|
||||
await advanceTime(OPEN_DELAY);
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover").toHaveText("hello");
|
||||
|
||||
await pointerUp("button");
|
||||
await animationFrame();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
|
||||
await pointerDown(document.body);
|
||||
await animationFrame();
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
});
|
||||
|
||||
test.tags("mobile");
|
||||
test("touch rendering - tap-to-show", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<button data-tooltip="hello" data-tooltip-touch-tap-to-show="true">Action</button>`;
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
await pointerDown("button[data-tooltip]");
|
||||
await animationFrame();
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
|
||||
await advanceTime(SHOW_AFTER_DELAY);
|
||||
await advanceTime(OPEN_DELAY);
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
expect(".o_popover").toHaveText("hello");
|
||||
|
||||
await pointerUp("button");
|
||||
await animationFrame();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
await runAllTimers();
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
|
||||
// The tooltip should be closed if you click on the button itself
|
||||
await click("button[data-tooltip]");
|
||||
await animationFrame();
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
|
||||
// Reopen it
|
||||
await pointerDown("button[data-tooltip]");
|
||||
await advanceTime(SHOW_AFTER_DELAY);
|
||||
await advanceTime(OPEN_DELAY);
|
||||
expect(".o_popover").toHaveCount(1);
|
||||
|
||||
// The tooltip should be also closed if you click anywhere else
|
||||
await pointerDown(document.body);
|
||||
await animationFrame();
|
||||
expect(".o_popover").toHaveCount(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { Transition, useTransition, config as transitionConfig } from "@web/core/transition";
|
||||
import { mountWithCleanup, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Component, xml, useState } from "@odoo/owl";
|
||||
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
|
||||
|
||||
test("useTransition hook (default params)", async () => {
|
||||
patchWithCleanup(transitionConfig, {
|
||||
disabled: false,
|
||||
});
|
||||
class Parent extends Component {
|
||||
static template = xml`<div t-if="transition.shouldMount" t-att-class="transition.className"/>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.transition = useTransition({
|
||||
name: "test",
|
||||
onLeave: () => expect.step("leave"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// noMainContainer, because the await for the mount of the main container
|
||||
// will already change the transition
|
||||
const parent = await mountWithCleanup(Parent, { noMainContainer: true });
|
||||
|
||||
expect(".test.test-enter-active:not(.test-enter)").toHaveCount(1);
|
||||
parent.transition.shouldMount = false;
|
||||
await animationFrame();
|
||||
|
||||
// Leaving: -leave but not -enter-active
|
||||
expect(".test.test-leave:not(.test-enter-active)").toHaveCount(1);
|
||||
expect.verifySteps([]);
|
||||
await runAllTimers();
|
||||
expect.verifySteps(["leave"]);
|
||||
await animationFrame();
|
||||
expect(".test").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("useTransition hook (initially visible and immediate=true)", async () => {
|
||||
patchWithCleanup(transitionConfig, {
|
||||
disabled: false,
|
||||
});
|
||||
class Parent extends Component {
|
||||
static template = xml`<div t-if="transition.shouldMount" t-att-class="transition.className"/>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.transition = useTransition({
|
||||
name: "test",
|
||||
immediate: true,
|
||||
onLeave: () => expect.step("leave"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// noMainContainer, because the await for the mount of the main container
|
||||
// will already change the transition
|
||||
const parent = await mountWithCleanup(Parent, { noMainContainer: true });
|
||||
|
||||
// Mounted with -enter but not -enter-active
|
||||
expect(".test.test-enter:not(.test-enter-active)").toHaveCount(1);
|
||||
await animationFrame();
|
||||
// No longer has -enter class but now has -enter-active
|
||||
expect(".test.test-enter-active:not(.test-enter)").toHaveCount(1);
|
||||
parent.transition.shouldMount = false;
|
||||
await animationFrame();
|
||||
|
||||
// Leaving: -leave but not -enter-active
|
||||
expect(".test.test-leave:not(.test-enter-active)").toHaveCount(1);
|
||||
expect.verifySteps([]);
|
||||
await runAllTimers();
|
||||
expect.verifySteps(["leave"]);
|
||||
await animationFrame();
|
||||
expect(".test").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("useTransition hook (initially not visible)", async () => {
|
||||
patchWithCleanup(transitionConfig, {
|
||||
disabled: false,
|
||||
});
|
||||
class Parent extends Component {
|
||||
static template = xml`<div t-if="transition.shouldMount" t-att-class="transition.className"/>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.transition = useTransition({
|
||||
name: "test",
|
||||
initialVisibility: false,
|
||||
onLeave: () => expect.step("leave"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// noMainContainer, because the await for the mount of the main container
|
||||
// will already change the transition
|
||||
const parent = await mountWithCleanup(Parent, { noMainContainer: true });
|
||||
expect(".test").toHaveCount(0);
|
||||
|
||||
parent.transition.shouldMount = true;
|
||||
await animationFrame();
|
||||
|
||||
// Leaving: -leave but not -enter-active
|
||||
expect(".test.test-enter:not(.test-enter-active)").toHaveCount(1);
|
||||
await animationFrame();
|
||||
// No longer has -enter class but now has -enter-active
|
||||
expect(".test.test-enter-active:not(.test-enter)").toHaveCount(1);
|
||||
await runAllTimers();
|
||||
expect.verifySteps([]);
|
||||
await animationFrame();
|
||||
});
|
||||
|
||||
test("Transition HOC", async () => {
|
||||
patchWithCleanup(transitionConfig, {
|
||||
disabled: false,
|
||||
});
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Transition name="'test'" visible="state.show" immediate="true" t-slot-scope="transition" onLeave="onLeave">
|
||||
<div t-att-class="transition.className"/>
|
||||
</Transition>
|
||||
`;
|
||||
static components = { Transition };
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState({ show: true });
|
||||
}
|
||||
onLeave() {
|
||||
expect.step("leave");
|
||||
}
|
||||
}
|
||||
|
||||
// noMainContainer, because the await for the mount of the main container
|
||||
// will already change the transition
|
||||
const parent = await mountWithCleanup(Parent, { noMainContainer: true });
|
||||
|
||||
// Mounted with -enter but not -enter-active
|
||||
expect(".test.test-enter:not(.test-enter-active)").toHaveCount(1);
|
||||
await animationFrame();
|
||||
// No longer has -enter class but now has -enter-active
|
||||
expect(".test.test-enter-active:not(.test-enter)").toHaveCount(1);
|
||||
parent.state.show = false;
|
||||
await animationFrame();
|
||||
|
||||
// Leaving: -leave but not -enter-active
|
||||
expect(".test.test-leave:not(.test-enter-active)").toHaveCount(1);
|
||||
expect.verifySteps([]);
|
||||
await runAllTimers();
|
||||
expect.verifySteps(["leave"]);
|
||||
await animationFrame();
|
||||
expect(".test").toHaveCount(0);
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,398 @@
|
|||
import { queryAll, queryAllTexts, queryOne, queryText, queryValue } from "@odoo/hoot-dom";
|
||||
import { contains, fields, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
/**
|
||||
* @typedef {import("@odoo/hoot-dom").FillOptions} FillOptions
|
||||
* @typedef {import("@odoo/hoot-dom").Target} Target
|
||||
*/
|
||||
|
||||
function getValue(root) {
|
||||
if (!root) {
|
||||
return null;
|
||||
}
|
||||
const el = queryOne("input,select,span:not(.o_tag):not(.o_dropdown_button)", { root });
|
||||
switch (el.tagName) {
|
||||
case "INPUT":
|
||||
return queryValue(el);
|
||||
case "SELECT":
|
||||
return el.options[el.selectedIndex].label;
|
||||
default:
|
||||
return queryText(el);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Target} selector
|
||||
* @param {number} [index]
|
||||
* @param {Target} [root]
|
||||
*/
|
||||
function queryAt(selector, index, root) {
|
||||
return queryAll(selector, { root }).at(index || 0);
|
||||
}
|
||||
|
||||
export class Partner extends models.Model {
|
||||
foo = fields.Char();
|
||||
bar = fields.Boolean();
|
||||
name = fields.Char({ string: "Partner Name" });
|
||||
product_id = fields.Many2one({ relation: "product" });
|
||||
int = fields.Integer();
|
||||
date = fields.Date();
|
||||
datetime = fields.Datetime();
|
||||
json_field = fields.Json({ string: "Json Field" });
|
||||
state = fields.Selection({
|
||||
selection: [
|
||||
["abc", "ABC"],
|
||||
["def", "DEF"],
|
||||
["ghi", "GHI"],
|
||||
],
|
||||
});
|
||||
|
||||
_records = [
|
||||
{ id: 1, foo: "yop", bar: true, product_id: 37, name: "first record" },
|
||||
{ id: 2, foo: "blip", bar: true, product_id: false, name: "second record" },
|
||||
{ id: 4, foo: "abc", bar: false, product_id: 41, name: "aaa" },
|
||||
];
|
||||
}
|
||||
|
||||
export class Product extends models.Model {
|
||||
name = fields.Char({ string: "Product Name" });
|
||||
bar = fields.Boolean({ string: "Product Bar" });
|
||||
team_id = fields.Many2one({
|
||||
string: "Product Team",
|
||||
relation: "team",
|
||||
searchable: true,
|
||||
});
|
||||
|
||||
_records = [
|
||||
{ id: 37, name: "xphone" },
|
||||
{ id: 41, name: "xpad" },
|
||||
];
|
||||
}
|
||||
|
||||
export class Team extends models.Model {
|
||||
name = fields.Char({ string: "Team Name", searchable: true });
|
||||
player_ids = fields.One2many({ relation: "player", string: "Players" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, display_name: "Mancester City" },
|
||||
{ id: 2, display_name: "Arsenal" },
|
||||
];
|
||||
}
|
||||
|
||||
export class Player extends models.Model {
|
||||
name = fields.Char({ string: "Player Name", searchable: true });
|
||||
country_id = fields.Many2one({ string: "Country", relation: "country" });
|
||||
_records = [
|
||||
{ id: 1, name: "Kevin De Bruyne" },
|
||||
{ id: 2, name: "Jeremy Doku" },
|
||||
];
|
||||
}
|
||||
|
||||
export class Country extends models.Model {
|
||||
foo = fields.Char();
|
||||
stage_id = fields.Many2one({ relation: "stage" });
|
||||
}
|
||||
|
||||
export class Stage extends models.Model {
|
||||
bar = fields.Boolean();
|
||||
}
|
||||
|
||||
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(options = {}) {
|
||||
const content = [];
|
||||
const nodes = queryAll(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(0, node);
|
||||
} else if (className.includes("complex_condition")) {
|
||||
nodeValue.value = getCurrentComplexCondition(0, node);
|
||||
} else {
|
||||
nodeValue.value = getCurrentCondition(0, node);
|
||||
}
|
||||
if (options.node) {
|
||||
nodeValue.node = node;
|
||||
}
|
||||
content.push(nodeValue);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export function getCurrentPath(index, target) {
|
||||
const pathEditor = queryAt(SELECTORS.pathEditor, index, target);
|
||||
if (pathEditor) {
|
||||
if (pathEditor.querySelector(".o_model_field_selector")) {
|
||||
return getModelFieldSelectorValues(pathEditor).join(" > ");
|
||||
}
|
||||
return queryText(pathEditor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export function getCurrentOperator(index, target) {
|
||||
const operatorEditor = queryAt(SELECTORS.operatorEditor, index, target);
|
||||
return getValue(operatorEditor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export function getCurrentValue(index, target) {
|
||||
const valueEditor = queryAt(SELECTORS.valueEditor, index, target);
|
||||
const value = getValue(valueEditor);
|
||||
if (valueEditor) {
|
||||
const texts = queryAllTexts(`.o_tag`, { root: valueEditor });
|
||||
if (texts.length) {
|
||||
if (value) {
|
||||
texts.push(value);
|
||||
}
|
||||
return texts.join(" ");
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export function getOperatorOptions(index, target) {
|
||||
const el = queryAt(SELECTORS.operatorEditor, index, target);
|
||||
if (el) {
|
||||
return queryAll(`select:only option`, { root: el }).map((o) => o.label);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export function getValueOptions(index, target) {
|
||||
const el = queryAt(SELECTORS.valueEditor, index, target);
|
||||
if (el) {
|
||||
return queryAll(`select:only option`, { root: el }).map((o) => o.label);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
function getCurrentComplexCondition(index, target) {
|
||||
const input = queryAt(SELECTORS.complexConditionInput, index, target);
|
||||
return input?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export function getConditionText(index, target) {
|
||||
const condition = queryAt(SELECTORS.condition, index, target);
|
||||
return queryText(condition).replace(/\n/g, " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
function getCurrentCondition(index, target) {
|
||||
const values = [getCurrentPath(index, target), getCurrentOperator(index, target)];
|
||||
const valueEditor = queryAt(SELECTORS.valueEditor, index, target);
|
||||
if (valueEditor) {
|
||||
values.push(getCurrentValue(index, target));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
function getCurrentConnector(index, target) {
|
||||
const connectorText = queryAllTexts(
|
||||
`${SELECTORS.connector} .dropdown-toggle, ${SELECTORS.connector} > span:nth-child(2), ${SELECTORS.connector} > span > strong`,
|
||||
{ root: target }
|
||||
).at(index);
|
||||
return connectorText.includes("all") ? "all" : connectorText;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export function isNotSupportedPath(index, target) {
|
||||
const pathEditor = queryAt(SELECTORS.pathEditor, index, target);
|
||||
return Boolean(queryOne(SELECTORS.clearNotSupported, { root: pathEditor }));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export function isNotSupportedOperator(index, target) {
|
||||
const operatorEditor = queryAt(SELECTORS.operatorEditor, index, target);
|
||||
return Boolean(queryOne(SELECTORS.clearNotSupported, { root: operatorEditor }));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export function isNotSupportedValue(index, target) {
|
||||
const valueEditor = queryAt(SELECTORS.valueEditor, index, target);
|
||||
return Boolean(queryOne(SELECTORS.clearNotSupported, { root: valueEditor }));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* @param {any} operator
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export async function selectOperator(operator, index, target) {
|
||||
await contains(`${SELECTORS.operatorEditor}:eq(${index || 0}) select`, { root: target }).select(
|
||||
JSON.stringify(operator)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export async function selectValue(value, index, target) {
|
||||
await contains(`${SELECTORS.valueEditor}:eq(${index || 0}) select`, { root: target }).select(
|
||||
JSON.stringify(value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
* @param {FillOptions} [options]
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export async function editValue(value, options, index, target) {
|
||||
await contains(`${SELECTORS.valueEditor}:eq(${index || 0}) input`, { root: target }).edit(
|
||||
value,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export async function clickOnButtonAddNewRule(index, target) {
|
||||
await contains(queryAt(SELECTORS.buttonAddNewRule, index, target)).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export async function clickOnButtonAddBranch(index, target) {
|
||||
await contains(queryAt(SELECTORS.buttonAddBranch, index, target)).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export async function clickOnButtonDeleteNode(index, target) {
|
||||
await contains(queryAt(SELECTORS.buttonDeleteNode, index, target)).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
* @param {Target} [target]
|
||||
*/
|
||||
export async function clearNotSupported(index, target) {
|
||||
await contains(queryAt(SELECTORS.clearNotSupported, index, target)).click();
|
||||
}
|
||||
|
||||
export async function addNewRule() {
|
||||
await contains(SELECTORS.addNewRule).click();
|
||||
}
|
||||
|
||||
export async function toggleArchive() {
|
||||
await contains(SELECTORS.toggleArchive).click();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
*/
|
||||
export async function openModelFieldSelectorPopover(index = 0) {
|
||||
await contains(`.o_model_field_selector:eq(${index})`).click();
|
||||
}
|
||||
|
||||
export function getModelFieldSelectorValues(root) {
|
||||
return queryAllTexts("span.o_model_field_selector_chain_part", { root });
|
||||
}
|
||||
|
||||
export function getDisplayedFieldNames() {
|
||||
return queryAllTexts(".o_model_field_selector_popover_item_name");
|
||||
}
|
||||
|
||||
export function getTitle() {
|
||||
return queryText(".o_model_field_selector_popover .o_model_field_selector_popover_title");
|
||||
}
|
||||
|
||||
export async function clickPrev() {
|
||||
await contains(".o_model_field_selector_popover_prev_page").click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [index=0]
|
||||
*/
|
||||
export async function followRelation(index = 0) {
|
||||
await contains(`.o_model_field_selector_popover_item_relation:eq(${index})`).click();
|
||||
}
|
||||
|
||||
export function getFocusedFieldName() {
|
||||
return queryText(".o_model_field_selector_popover_item.active");
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { press, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
import { getService, mountWithCleanup } from "../web_test_helpers";
|
||||
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { useActiveElement } from "@web/core/ui/ui_service";
|
||||
import { useAutofocus } from "@web/core/utils/hooks";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
|
||||
test("block and unblock once ui with ui service", async () => {
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
expect(".o_blockUI").toHaveCount(0);
|
||||
getService("ui").block();
|
||||
await animationFrame();
|
||||
expect(".o_blockUI").toHaveCount(1);
|
||||
getService("ui").unblock();
|
||||
await animationFrame();
|
||||
expect(".o_blockUI").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("use block and unblock several times to block ui with ui service", async () => {
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
expect(".o_blockUI").toHaveCount(0);
|
||||
getService("ui").block();
|
||||
getService("ui").block();
|
||||
getService("ui").block();
|
||||
await animationFrame();
|
||||
expect(".o_blockUI").toHaveCount(1);
|
||||
getService("ui").unblock();
|
||||
getService("ui").unblock();
|
||||
await animationFrame();
|
||||
expect(".o_blockUI").toHaveCount(1);
|
||||
getService("ui").unblock();
|
||||
await animationFrame();
|
||||
expect(".o_blockUI").toHaveCount(0);
|
||||
});
|
||||
|
||||
test("a component can be the UI active element: simple usage", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<h1>My Component</h1>
|
||||
<div t-if="hasRef" id="owner" t-ref="delegatedRef">
|
||||
<input type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
useActiveElement("delegatedRef");
|
||||
this.hasRef = true;
|
||||
}
|
||||
}
|
||||
|
||||
const comp = await mountWithCleanup(MyComponent);
|
||||
|
||||
expect(getService("ui").activeElement).toBe(queryOne("#owner"));
|
||||
expect("#owner input").toBeFocused();
|
||||
comp.hasRef = false;
|
||||
comp.render();
|
||||
await animationFrame();
|
||||
expect(getService("ui").activeElement).toBe(document);
|
||||
expect(document.body).toBeFocused();
|
||||
});
|
||||
|
||||
test("UI active element: trap focus", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<h1>My Component</h1>
|
||||
<input type="text" placeholder="outerUIActiveElement"/>
|
||||
<div t-ref="delegatedRef">
|
||||
<input type="text" placeholder="withFocus"/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
useActiveElement("delegatedRef");
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect("input[placeholder=withFocus]").toBeFocused();
|
||||
let [firstEvent] = await press("Tab", { shiftKey: false });
|
||||
await animationFrame();
|
||||
expect(firstEvent.defaultPrevented).toBe(true);
|
||||
expect("input[placeholder=withFocus]").toBeFocused();
|
||||
|
||||
[firstEvent] = await press("Tab", { shiftKey: true });
|
||||
await animationFrame();
|
||||
expect(firstEvent.defaultPrevented).toBe(true);
|
||||
expect("input[placeholder=withFocus]").toBeFocused();
|
||||
});
|
||||
|
||||
test("UI active element: trap focus - default focus with autofocus", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<h1>My Component</h1>
|
||||
<input type="text" placeholder="outerUIActiveElement"/>
|
||||
<div t-ref="delegatedRef">
|
||||
<input type="text" placeholder="withoutFocus"/>
|
||||
<input type="text" t-ref="autofocus" placeholder="withAutoFocus"/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
useActiveElement("delegatedRef");
|
||||
useAutofocus();
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect("input[placeholder=withAutoFocus]").toBeFocused();
|
||||
let [firstEvent] = await press("Tab", { shiftKey: false });
|
||||
await animationFrame();
|
||||
expect(firstEvent.defaultPrevented).toBe(true);
|
||||
expect("input[placeholder=withoutFocus]").toBeFocused();
|
||||
|
||||
[firstEvent] = await press("Tab", { shiftKey: true });
|
||||
await animationFrame();
|
||||
expect(firstEvent.defaultPrevented).toBe(true);
|
||||
expect("input[placeholder=withAutoFocus]").toBeFocused();
|
||||
|
||||
[firstEvent] = await press("Tab", { shiftKey: true });
|
||||
await animationFrame();
|
||||
expect(firstEvent.defaultPrevented).toBe(false);
|
||||
});
|
||||
|
||||
test("do not become UI active element if no element to focus", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<h1>My Component</h1>
|
||||
<input type="text" placeholder="outerUIActiveElement"/>
|
||||
<div id="idActiveElement" t-ref="delegatedRef">
|
||||
<div>
|
||||
<span> No focus element </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
useActiveElement("delegatedRef");
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
expect(getService("ui").activeElement).toBe(document);
|
||||
});
|
||||
|
||||
test("UI active element: trap focus - first or last tabable changes", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<h1>My Component</h1>
|
||||
<input type="text" name="outer"/>
|
||||
<div id="idActiveElement" t-ref="delegatedRef">
|
||||
<div>
|
||||
<input type="text" name="a" t-if="show.a"/>
|
||||
<input type="text" name="b"/>
|
||||
<input type="text" name="c" t-if="show.c"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.show = useState({ a: true, c: false });
|
||||
useActiveElement("delegatedRef");
|
||||
}
|
||||
}
|
||||
|
||||
const comp = await mountWithCleanup(MyComponent);
|
||||
|
||||
expect("input[name=a]").toBeFocused();
|
||||
|
||||
let [firstEvent] = await press("Tab", { shiftKey: true });
|
||||
await animationFrame();
|
||||
expect(firstEvent.defaultPrevented).toBe(true);
|
||||
expect("input[name=b]").toBeFocused();
|
||||
|
||||
comp.show.a = false;
|
||||
comp.show.c = true;
|
||||
await animationFrame();
|
||||
expect("input[name=b]").toBeFocused();
|
||||
|
||||
[firstEvent] = await press("Tab", { shiftKey: true });
|
||||
await animationFrame();
|
||||
expect(firstEvent.defaultPrevented).toBe(true);
|
||||
expect("input[name=c]").toBeFocused();
|
||||
});
|
||||
|
||||
test("UI active element: trap focus is not bypassed using invisible elements", async () => {
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<h1>My Component</h1>
|
||||
<input type="text" placeholder="outerUIActiveElement"/>
|
||||
<div t-ref="delegatedRef">
|
||||
<input type="text" placeholder="withFocus"/>
|
||||
<input class="d-none" type="text" placeholder="withFocusNotDisplayed"/>
|
||||
<div class="d-none">
|
||||
<input type="text" placeholder="withFocusNotDisplayedToo"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
useActiveElement("delegatedRef");
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect("input[placeholder=withFocus]").toBeFocused();
|
||||
|
||||
let [firstEvent] = await press("Tab", { shiftKey: false });
|
||||
await animationFrame();
|
||||
expect(firstEvent.defaultPrevented).toBe(true);
|
||||
expect("input[placeholder=withFocus]").toBeFocused();
|
||||
|
||||
[firstEvent] = await press("Tab", { shiftKey: true });
|
||||
await animationFrame();
|
||||
expect(firstEvent.defaultPrevented).toBe(true);
|
||||
expect("input[placeholder=withFocus]").toBeFocused();
|
||||
});
|
||||
39
odoo-bringout-oca-ocb-web/web/static/tests/core/user.test.js
Normal file
39
odoo-bringout-oca-ocb-web/web/static/tests/core/user.test.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { test, expect, describe } from "@odoo/hoot";
|
||||
import { _makeUser, user } from "@web/core/user";
|
||||
import { makeMockEnv, onRpc, patchWithCleanup, serverState } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("successive calls to hasGroup", async () => {
|
||||
serverState.uid = 7;
|
||||
await makeMockEnv();
|
||||
const groups = ["x"];
|
||||
onRpc("has_group", (args) => {
|
||||
expect.step(`${args.model}/${args.method}/${args.args[1]}`);
|
||||
return groups.includes(args.args[1]);
|
||||
});
|
||||
|
||||
const hasGroupX = await user.hasGroup("x");
|
||||
const hasGroupY = await user.hasGroup("y");
|
||||
expect(hasGroupX).toBe(true);
|
||||
expect(hasGroupY).toBe(false);
|
||||
const hasGroupXAgain = await user.hasGroup("x");
|
||||
expect(hasGroupXAgain).toBe(true);
|
||||
|
||||
expect.verifySteps(["res.users/has_group/x", "res.users/has_group/y"]);
|
||||
});
|
||||
|
||||
test("set user settings do not override old valid keys", async () => {
|
||||
await makeMockEnv();
|
||||
patchWithCleanup(user, _makeUser({ user_settings: { a: 1, b: 2 } }));
|
||||
onRpc("set_res_users_settings", (args) => {
|
||||
expect.step(JSON.stringify(args.kwargs.new_settings));
|
||||
return { a: 3, c: 4 };
|
||||
});
|
||||
|
||||
expect(user.settings).toEqual({ a: 1, b: 2 });
|
||||
|
||||
await user.setUserSettings("a", 3);
|
||||
expect.verifySteps(['{"a":3}']);
|
||||
expect(user.settings).toEqual({ a: 3, b: 2, c: 4 });
|
||||
});
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
|
||||
import {
|
||||
cartesian,
|
||||
ensureArray,
|
||||
groupBy,
|
||||
intersection,
|
||||
shallowEqual,
|
||||
slidingWindow,
|
||||
sortBy,
|
||||
unique,
|
||||
zip,
|
||||
zipWith,
|
||||
} from "@web/core/utils/arrays";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
describe("groupby", () => {
|
||||
test("groupBy parameter validations", () => {
|
||||
// Safari: TypeError: undefined is not a function
|
||||
// Other navigator: array is not iterable
|
||||
expect(() => groupBy({})).toThrow(/TypeError: \w+ is not iterable/);
|
||||
expect(() => groupBy([], true)).toThrow(
|
||||
/Expected criterion of type 'string' or 'function' and got 'boolean'/
|
||||
);
|
||||
expect(() => groupBy([], 3)).toThrow(
|
||||
/Expected criterion of type 'string' or 'function' and got 'number'/
|
||||
);
|
||||
expect(() => groupBy([], {})).toThrow(
|
||||
/Expected criterion of type 'string' or 'function' and got 'object'/
|
||||
);
|
||||
});
|
||||
|
||||
test("groupBy (no criterion)", () => {
|
||||
// criterion = default
|
||||
expect(groupBy(["a", "b", 1, true])).toEqual({
|
||||
1: [1],
|
||||
a: ["a"],
|
||||
b: ["b"],
|
||||
true: [true],
|
||||
});
|
||||
});
|
||||
|
||||
test("groupBy by property", () => {
|
||||
expect(groupBy([{ x: "a" }, { x: "a" }, { x: "b" }], "x")).toEqual({
|
||||
a: [{ x: "a" }, { x: "a" }],
|
||||
b: [{ x: "b" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("groupBy", () => {
|
||||
expect(groupBy(["a", "b", 1, true], (x) => `el${x}`)).toEqual({
|
||||
ela: ["a"],
|
||||
elb: ["b"],
|
||||
el1: [1],
|
||||
eltrue: [true],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortby", () => {
|
||||
test("sortBy parameter validation", () => {
|
||||
expect(() => sortBy({})).toThrow(/TypeError: \w+ is not iterable/);
|
||||
expect(() => sortBy([Symbol("b"), Symbol("a")])).toThrow(
|
||||
/(Cannot convert a (Symbol value)|(symbol) to a number)|(can't convert symbol to number)/
|
||||
);
|
||||
expect(() => sortBy([2, 1, 5], true)).toThrow(
|
||||
/Expected criterion of type 'string' or 'function' and got 'boolean'/
|
||||
);
|
||||
expect(() => sortBy([2, 1, 5], 3)).toThrow(
|
||||
/Expected criterion of type 'string' or 'function' and got 'number'/
|
||||
);
|
||||
expect(() => sortBy([2, 1, 5], {})).toThrow(
|
||||
/Expected criterion of type 'string' or 'function' and got 'object'/
|
||||
);
|
||||
});
|
||||
|
||||
test("sortBy do not sort in place", () => {
|
||||
const toSort = [2, 3, 1];
|
||||
sortBy(toSort);
|
||||
expect(toSort).toEqual([2, 3, 1]);
|
||||
});
|
||||
|
||||
test("sortBy (no criterion)", () => {
|
||||
expect(sortBy([])).toEqual([]);
|
||||
expect(sortBy([2, 1, 5])).toEqual([1, 2, 5]);
|
||||
expect(sortBy([true, false, true])).toEqual([false, true, true]);
|
||||
expect(sortBy(["b", "a", "z"])).toEqual(["a", "b", "z"]);
|
||||
expect(sortBy([{ x: true }, { x: false }, { x: true }])).toEqual([
|
||||
{ x: true },
|
||||
{ x: false },
|
||||
{ x: true },
|
||||
]);
|
||||
expect(sortBy([{ x: 2 }, { x: 1 }, { x: 5 }])).toEqual([{ x: 2 }, { x: 1 }, { x: 5 }]);
|
||||
expect(sortBy([{ x: "b" }, { x: "a" }, { x: "z" }])).toEqual([
|
||||
{ x: "b" },
|
||||
{ x: "a" },
|
||||
{ x: "z" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("sortBy property", () => {
|
||||
expect(sortBy([], "x")).toEqual([]);
|
||||
expect(sortBy([2, 1, 5], "x")).toEqual([2, 1, 5]);
|
||||
expect(sortBy([true, false, true], "x")).toEqual([true, false, true]);
|
||||
expect(sortBy(["b", "a", "z"], "x")).toEqual(["b", "a", "z"]);
|
||||
expect(sortBy([{ x: true }, { x: false }, { x: true }], "x")).toEqual([
|
||||
{ x: false },
|
||||
{ x: true },
|
||||
{ x: true },
|
||||
]);
|
||||
expect(sortBy([{ x: 2 }, { x: 1 }, { x: 5 }], "x")).toEqual([{ x: 1 }, { x: 2 }, { x: 5 }]);
|
||||
expect(sortBy([{ x: "b" }, { x: "a" }, { x: "z" }], "x")).toEqual([
|
||||
{ x: "a" },
|
||||
{ x: "b" },
|
||||
{ x: "z" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("sortBy getter", () => {
|
||||
const getter = (obj) => obj.x;
|
||||
expect(sortBy([], getter)).toEqual([]);
|
||||
expect(sortBy([2, 1, 5], getter)).toEqual([2, 1, 5]);
|
||||
expect(sortBy([true, false, true], getter)).toEqual([true, false, true]);
|
||||
expect(sortBy(["b", "a", "z"], getter)).toEqual(["b", "a", "z"]);
|
||||
expect(sortBy([{ x: true }, { x: false }, { x: true }], getter)).toEqual([
|
||||
{ x: false },
|
||||
{ x: true },
|
||||
{ x: true },
|
||||
]);
|
||||
expect(sortBy([{ x: 2 }, { x: 1 }, { x: 5 }], getter)).toEqual([
|
||||
{ x: 1 },
|
||||
{ x: 2 },
|
||||
{ x: 5 },
|
||||
]);
|
||||
expect(sortBy([{ x: "b" }, { x: "a" }, { x: "z" }], getter)).toEqual([
|
||||
{ x: "a" },
|
||||
{ x: "b" },
|
||||
{ x: "z" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("sortBy descending order", () => {
|
||||
expect(sortBy([2, 1, 5], null, "desc")).toEqual([5, 2, 1]);
|
||||
expect(sortBy([{ x: "b" }, { x: "a" }, { x: "z" }], "x", "desc")).toEqual([
|
||||
{ x: "z" },
|
||||
{ x: "b" },
|
||||
{ x: "a" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("intersection", () => {
|
||||
test("intersection of arrays", () => {
|
||||
expect(intersection([], [1, 2])).toEqual([]);
|
||||
expect(intersection([1, 2], [])).toEqual([]);
|
||||
expect(intersection([1], [2])).toEqual([]);
|
||||
expect(intersection([1, 2], [2, 3])).toEqual([2]);
|
||||
expect(intersection([1, 2, 3], [1, 2, 3])).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cartesian", () => {
|
||||
test("cartesian product of zero arrays", () => {
|
||||
expect(cartesian()).toEqual([undefined], {
|
||||
message: "the unit of the product is a singleton",
|
||||
});
|
||||
});
|
||||
|
||||
test("cartesian product of a single array", () => {
|
||||
expect(cartesian([])).toEqual([]);
|
||||
expect(cartesian([1])).toEqual([1], { message: "we don't want unecessary brackets" });
|
||||
expect(cartesian([1, 2])).toEqual([1, 2]);
|
||||
expect(cartesian([[1, 2]])).toEqual([[1, 2]], {
|
||||
message: "the internal structure of elements should be preserved",
|
||||
});
|
||||
expect(
|
||||
cartesian([
|
||||
[1, 2],
|
||||
[3, [2]],
|
||||
])
|
||||
).toEqual(
|
||||
[
|
||||
[1, 2],
|
||||
[3, [2]],
|
||||
],
|
||||
{ message: "the internal structure of elements should be preserved" }
|
||||
);
|
||||
});
|
||||
|
||||
test("cartesian product of two arrays", () => {
|
||||
expect(cartesian([], [])).toEqual([]);
|
||||
expect(cartesian([1], [])).toEqual([]);
|
||||
expect(cartesian([1], [2])).toEqual([[1, 2]]);
|
||||
expect(cartesian([1, 2], [3])).toEqual([
|
||||
[1, 3],
|
||||
[2, 3],
|
||||
]);
|
||||
expect(cartesian([[1], 4], [2, [3]])).toEqual(
|
||||
[
|
||||
[[1], 2],
|
||||
[[1], [3]],
|
||||
[4, 2],
|
||||
[4, [3]],
|
||||
],
|
||||
{ message: "the internal structure of elements should be preserved" }
|
||||
);
|
||||
});
|
||||
|
||||
test("cartesian product of three arrays", () => {
|
||||
expect(cartesian([], [], [])).toEqual([]);
|
||||
expect(cartesian([1], [], [2, 5])).toEqual([]);
|
||||
expect(cartesian([1], [2], [3])).toEqual([[1, 2, 3]], {
|
||||
message: "we should have no unecessary brackets, we want elements to be 'triples'",
|
||||
});
|
||||
expect(cartesian([[1], 2], [3], [4])).toEqual(
|
||||
[
|
||||
[[1], 3, 4],
|
||||
[2, 3, 4],
|
||||
],
|
||||
{ message: "the internal structure of elements should be preserved" }
|
||||
);
|
||||
});
|
||||
|
||||
test("cartesian product of four arrays", () => {
|
||||
expect(cartesian([1], [2], [3], [4])).toEqual([[1, 2, 3, 4]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureArray", () => {
|
||||
test("ensure array", () => {
|
||||
const arrayRef = [];
|
||||
expect(ensureArray(arrayRef)).not.toBe(arrayRef, {
|
||||
message: "Should be a different array",
|
||||
});
|
||||
expect(ensureArray([])).toEqual([]);
|
||||
expect(ensureArray()).toEqual([undefined]);
|
||||
expect(ensureArray(null)).toEqual([null]);
|
||||
expect(ensureArray({ a: 1 })).toEqual([{ a: 1 }]);
|
||||
expect(ensureArray("foo")).toEqual(["foo"]);
|
||||
expect(ensureArray([1, 2, "3"])).toEqual([1, 2, "3"]);
|
||||
expect(ensureArray(new Set([1, 2, 3]))).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unique", () => {
|
||||
test("unique array", () => {
|
||||
expect(unique([1, 2, 3, 2, 4, 3, 1, 4])).toEqual([1, 2, 3, 4]);
|
||||
expect(unique("a d c a b c d b".split(" "))).toEqual("a d c b".split(" "));
|
||||
});
|
||||
});
|
||||
|
||||
describe("shallowEqual", () => {
|
||||
test("simple valid cases", () => {
|
||||
expect(shallowEqual([], [])).toBe(true);
|
||||
expect(shallowEqual([1], [1])).toBe(true);
|
||||
expect(shallowEqual([1, "a"], [1, "a"])).toBe(true);
|
||||
});
|
||||
|
||||
test("simple invalid cases", () => {
|
||||
expect(shallowEqual([1], [])).not.toBe(true);
|
||||
expect(shallowEqual([], [1])).not.toBe(true);
|
||||
expect(shallowEqual([1, "b"], [1, "a"])).not.toBe(true);
|
||||
});
|
||||
|
||||
test("arrays with non primitive values", () => {
|
||||
const obj = { b: 3 };
|
||||
expect(shallowEqual([obj], [obj])).toBe(true);
|
||||
expect(shallowEqual([{ b: 3 }], [{ b: 3 }])).not.toBe(true);
|
||||
|
||||
const arr = ["x", "y", "z"];
|
||||
expect(shallowEqual([arr], [arr])).toBe(true);
|
||||
expect(shallowEqual([["x", "y", "z"]], [["x", "y", "z"]])).not.toBe(true);
|
||||
|
||||
const fn = () => {};
|
||||
expect(shallowEqual([fn], [fn])).toBe(true);
|
||||
expect(shallowEqual([() => {}], [() => {}])).not.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("zip", () => {
|
||||
test("zip", () => {
|
||||
expect(zip([1, 2], [])).toEqual([]);
|
||||
expect(zip([1, 2], ["a"])).toEqual([[1, "a"]]);
|
||||
expect(zip([1, 2], ["a", "b"])).toEqual([
|
||||
[1, "a"],
|
||||
[2, "b"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("zipWith", () => {
|
||||
test("zipWith", () => {
|
||||
expect(zipWith([{ a: 1 }, { b: 2 }], ["a", "b"], (o, k) => o[k])).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("slidingWindow", () => {
|
||||
test("slidingWindow", () => {
|
||||
expect(slidingWindow([1, 2, 3, 4], 2)).toEqual([
|
||||
[1, 2],
|
||||
[2, 3],
|
||||
[3, 4],
|
||||
]);
|
||||
expect(slidingWindow([1, 2, 3, 4], 4)).toEqual([[1, 2, 3, 4]]);
|
||||
expect(slidingWindow([1, 2, 3, 4], 5)).toEqual([]);
|
||||
expect(slidingWindow([], 1)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { manuallyDispatchProgrammaticEvent } from "@odoo/hoot-dom";
|
||||
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { assets, loadCSS, loadJS } from "@web/core/assets";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
/**
|
||||
* @param {(node: Node) => void} callback
|
||||
*/
|
||||
const mockHeadAppendChild = (callback) =>
|
||||
patchWithCleanup(document.head, {
|
||||
appendChild: callback,
|
||||
});
|
||||
|
||||
test("loadJS: load invalid JS lib", async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
mockHeadAppendChild((node) => {
|
||||
expect(node).toBeInstanceOf(HTMLScriptElement);
|
||||
expect(node).toHaveAttribute("type", "text/javascript");
|
||||
expect(node).toHaveAttribute("src", "/some/invalid/file.js");
|
||||
|
||||
// Simulates a failed request to an invalid file.
|
||||
manuallyDispatchProgrammaticEvent(node, "error");
|
||||
});
|
||||
|
||||
await expect(loadJS("/some/invalid/file.js")).rejects.toThrow(
|
||||
/The loading of \/some\/invalid\/file.js failed/,
|
||||
{ message: "Trying to load an invalid file rejects the promise" }
|
||||
);
|
||||
});
|
||||
|
||||
test("loadCSS: load invalid CSS lib", async () => {
|
||||
expect.assertions(4 * 4 + 1);
|
||||
|
||||
assets.retries = { count: 3, delay: 1, extraDelay: 1 }; // Fail fast.
|
||||
|
||||
mockHeadAppendChild((node) => {
|
||||
expect(node).toBeInstanceOf(HTMLLinkElement);
|
||||
expect(node).toHaveAttribute("rel", "stylesheet");
|
||||
expect(node).toHaveAttribute("type", "text/css");
|
||||
expect(node).toHaveAttribute("href", "/some/invalid/file.css");
|
||||
|
||||
// Simulates a failed request to an invalid file.
|
||||
manuallyDispatchProgrammaticEvent(node, "error");
|
||||
});
|
||||
|
||||
await expect(loadCSS("/some/invalid/file.css")).rejects.toThrow(
|
||||
/The loading of \/some\/invalid\/file.css failed/,
|
||||
{ message: "Trying to load an invalid file rejects the promise" }
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryRect, queryOne } from "@odoo/hoot-dom";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { Component, useRef, xml } from "@odoo/owl";
|
||||
import { contains, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { useAutoresize } from "@web/core/utils/autoresize";
|
||||
|
||||
test(`resizable input`, async () => {
|
||||
class ResizableInput extends Component {
|
||||
static template = xml`<input class="resizable-input" t-ref="input"/>`;
|
||||
static props = ["*"];
|
||||
|
||||
setup() {
|
||||
useAutoresize(useRef("input"));
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(ResizableInput);
|
||||
const initialWidth = queryRect(`.resizable-input`).width;
|
||||
|
||||
await contains(`.resizable-input`).edit("new value");
|
||||
expect(`.resizable-input`).not.toHaveRect({ width: initialWidth });
|
||||
});
|
||||
|
||||
test(`resizable textarea`, async () => {
|
||||
class ResizableTextArea extends Component {
|
||||
static template = xml`<textarea class="resizable-textarea" t-ref="textarea"/>`;
|
||||
static props = ["*"];
|
||||
|
||||
setup() {
|
||||
useAutoresize(useRef("textarea"));
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(ResizableTextArea);
|
||||
const initialHeight = queryRect(`.resizable-textarea`).height;
|
||||
|
||||
await contains(`.resizable-textarea`).edit("new value\n".repeat(5));
|
||||
expect(`.resizable-textarea`).not.toHaveRect({ height: initialHeight });
|
||||
});
|
||||
|
||||
test(`resizable textarea with minimum height`, async () => {
|
||||
class ResizableTextArea extends Component {
|
||||
static template = xml`<textarea class="resizable-textarea" t-ref="textarea"/>`;
|
||||
static props = ["*"];
|
||||
|
||||
setup() {
|
||||
useAutoresize(useRef("textarea"), { minimumHeight: 100 });
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(ResizableTextArea);
|
||||
const initialHeight = queryRect(`.resizable-textarea`).height;
|
||||
expect(initialHeight).toBe(100);
|
||||
|
||||
await contains(`.resizable-textarea`).edit("new value\n".repeat(5));
|
||||
expect(`.resizable-textarea`).not.toHaveRect({ height: initialHeight });
|
||||
});
|
||||
|
||||
test(`call onResize callback`, async () => {
|
||||
class ResizableInput extends Component {
|
||||
static template = xml`<input class="resizable-input" t-ref="input"/>`;
|
||||
static props = ["*"];
|
||||
|
||||
setup() {
|
||||
const inputRef = useRef("input");
|
||||
useAutoresize(inputRef, {
|
||||
randomParam: true,
|
||||
onResize(el, options) {
|
||||
expect.step("onResize");
|
||||
expect(el).toBe(inputRef.el);
|
||||
expect(options).toInclude("randomParam");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(ResizableInput);
|
||||
expect.verifySteps(["onResize"]);
|
||||
|
||||
await contains(`.resizable-input`).edit("new value", { instantly: true });
|
||||
expect.verifySteps(["onResize"]);
|
||||
});
|
||||
|
||||
test(`call onResize callback after resizing text area`, async () => {
|
||||
class ResizableTextArea extends Component {
|
||||
static template = xml`<textarea class="resizable-textarea" t-ref="textarea"/>`;
|
||||
static props = ["*"];
|
||||
|
||||
setup() {
|
||||
const textareaRef = useRef("textarea");
|
||||
useAutoresize(textareaRef, {
|
||||
onResize(el, options) {
|
||||
expect.step("onResizeTextArea");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(ResizableTextArea);
|
||||
expect.verifySteps(["onResizeTextArea"]);
|
||||
|
||||
const target = queryOne(".resizable-textarea");
|
||||
target.style.width = `500px`;
|
||||
await animationFrame();
|
||||
expect.verifySteps(["onResizeTextArea"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { patchTranslations } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { humanSize } from "@web/core/utils/binary";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("humanSize", () => {
|
||||
patchTranslations();
|
||||
expect(humanSize(0)).toBe("0.00 Bytes");
|
||||
expect(humanSize(3)).toBe("3.00 Bytes");
|
||||
expect(humanSize(2048)).toBe("2.00 Kb");
|
||||
expect(humanSize(2645000)).toBe("2.52 Mb");
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { ErrorHandler } from "@web/core/utils/components";
|
||||
|
||||
test("ErrorHandler component", async () => {
|
||||
class Boom extends Component {
|
||||
static template = xml`<div><t t-esc="this.will.throw"/></div>`;
|
||||
static props = ["*"];
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t t-if="flag">
|
||||
<ErrorHandler onError="() => this.handleError()">
|
||||
<Boom/>
|
||||
</ErrorHandler>
|
||||
</t>
|
||||
<t t-else="">not boom</t>
|
||||
</div>
|
||||
`;
|
||||
static components = { Boom, ErrorHandler };
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.flag = true;
|
||||
}
|
||||
handleError() {
|
||||
this.flag = false;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent, { noMainContainer: true });
|
||||
expect(getFixture()).toHaveText("not boom");
|
||||
});
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { tick } from "@odoo/hoot-mock";
|
||||
|
||||
import { Deferred, Mutex, KeepLast, Race } from "@web/core/utils/concurrency";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
describe("Deferred", () => {
|
||||
test("basic use", async () => {
|
||||
const def1 = new Deferred();
|
||||
def1.then((v) => expect.step(`ok (${v})`));
|
||||
def1.resolve(44);
|
||||
await tick();
|
||||
expect.verifySteps(["ok (44)"]);
|
||||
|
||||
const def2 = new Deferred();
|
||||
def2.catch((v) => expect.step(`ko (${v})`));
|
||||
def2.reject(44);
|
||||
await tick();
|
||||
expect.verifySteps(["ko (44)"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Mutex", () => {
|
||||
test("simple scheduling", async () => {
|
||||
const mutex = new Mutex();
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
|
||||
mutex.exec(() => def1).then(() => expect.step("ok [1]"));
|
||||
mutex.exec(() => def2).then(() => expect.step("ok [2]"));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def1.resolve();
|
||||
await tick();
|
||||
expect.verifySteps(["ok [1]"]);
|
||||
|
||||
def2.resolve();
|
||||
await tick();
|
||||
expect.verifySteps(["ok [2]"]);
|
||||
});
|
||||
|
||||
test("simple scheduling (2)", async () => {
|
||||
const mutex = new Mutex();
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
|
||||
mutex.exec(() => def1).then(() => expect.step("ok [1]"));
|
||||
mutex.exec(() => def2).then(() => expect.step("ok [2]"));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def2.resolve();
|
||||
await tick();
|
||||
expect.verifySteps([]);
|
||||
|
||||
def1.resolve();
|
||||
await tick();
|
||||
expect.verifySteps(["ok [1]", "ok [2]"]);
|
||||
});
|
||||
|
||||
test("reject", async () => {
|
||||
const mutex = new Mutex();
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
const def3 = new Deferred();
|
||||
|
||||
mutex.exec(() => def1).then(() => expect.step("ok [1]"));
|
||||
mutex.exec(() => def2).catch(() => expect.step("ko [2]"));
|
||||
mutex.exec(() => def3).then(() => expect.step("ok [3]"));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def1.resolve();
|
||||
await tick();
|
||||
expect.verifySteps(["ok [1]"]);
|
||||
|
||||
def2.reject({ name: "sdkjfmqsjdfmsjkdfkljsdq" });
|
||||
await tick();
|
||||
expect.verifySteps(["ko [2]"]);
|
||||
|
||||
def3.resolve();
|
||||
await tick();
|
||||
expect.verifySteps(["ok [3]"]);
|
||||
});
|
||||
|
||||
test("getUnlockedDef checks", async () => {
|
||||
const mutex = new Mutex();
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
|
||||
mutex.getUnlockedDef().then(() => expect.step("mutex unlocked (1)"));
|
||||
await tick();
|
||||
expect.verifySteps(["mutex unlocked (1)"]);
|
||||
|
||||
mutex.exec(() => def1).then(() => expect.step("ok [1]"));
|
||||
await tick();
|
||||
mutex.getUnlockedDef().then(function () {
|
||||
expect.step("mutex unlocked (2)");
|
||||
});
|
||||
expect.verifySteps([]);
|
||||
|
||||
mutex.exec(() => def2).then(() => expect.step("ok [2]"));
|
||||
await tick();
|
||||
expect.verifySteps([]);
|
||||
|
||||
def1.resolve();
|
||||
await tick();
|
||||
expect.verifySteps(["ok [1]"]);
|
||||
|
||||
def2.resolve();
|
||||
await tick();
|
||||
expect.verifySteps(["mutex unlocked (2)", "ok [2]"]);
|
||||
});
|
||||
|
||||
test("error and getUnlockedDef", async () => {
|
||||
const mutex = new Mutex();
|
||||
const action = async () => {
|
||||
await Promise.resolve();
|
||||
throw new Error("boom");
|
||||
};
|
||||
mutex.exec(action).catch(() => expect.step("prom rejected"));
|
||||
await tick();
|
||||
expect.verifySteps(["prom rejected"]);
|
||||
|
||||
mutex.getUnlockedDef().then(() => expect.step("mutex unlocked"));
|
||||
await tick();
|
||||
expect.verifySteps(["mutex unlocked"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("KeepLast", () => {
|
||||
test("basic use", async () => {
|
||||
const keepLast = new KeepLast();
|
||||
const def = new Deferred();
|
||||
|
||||
keepLast.add(def).then(() => expect.step("ok"));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def.resolve();
|
||||
await tick();
|
||||
expect.verifySteps(["ok"]);
|
||||
});
|
||||
|
||||
test("rejected promise", async () => {
|
||||
const keepLast = new KeepLast();
|
||||
const def = new Deferred();
|
||||
|
||||
keepLast.add(def).catch(() => expect.step("ko"));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def.reject();
|
||||
await tick();
|
||||
expect.verifySteps(["ko"]);
|
||||
});
|
||||
|
||||
test("two promises resolved in order", async () => {
|
||||
const keepLast = new KeepLast();
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
|
||||
keepLast.add(def1).then(() => {
|
||||
throw new Error("should not be executed");
|
||||
});
|
||||
keepLast.add(def2).then(() => expect.step("ok [2]"));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def1.resolve();
|
||||
await tick();
|
||||
expect.verifySteps([]);
|
||||
|
||||
def2.resolve();
|
||||
await tick();
|
||||
expect.verifySteps(["ok [2]"]);
|
||||
});
|
||||
|
||||
test("two promises resolved in reverse order", async () => {
|
||||
const keepLast = new KeepLast();
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
|
||||
keepLast.add(def1).then(() => {
|
||||
throw new Error("should not be executed");
|
||||
});
|
||||
keepLast.add(def2).then(() => expect.step("ok [2]"));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def2.resolve();
|
||||
await tick();
|
||||
expect.verifySteps(["ok [2]"]);
|
||||
|
||||
def1.resolve();
|
||||
await tick();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Race", () => {
|
||||
test("basic use", async () => {
|
||||
const race = new Race();
|
||||
const def = new Deferred();
|
||||
|
||||
race.add(def).then((v) => expect.step(`ok (${v})`));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def.resolve(44);
|
||||
await tick();
|
||||
expect.verifySteps(["ok (44)"]);
|
||||
});
|
||||
|
||||
test("two promises resolved in order", async () => {
|
||||
const race = new Race();
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
|
||||
race.add(def1).then((v) => expect.step(`ok (${v}) [1]`));
|
||||
race.add(def2).then((v) => expect.step(`ok (${v}) [2]`));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def1.resolve(44);
|
||||
await tick();
|
||||
expect.verifySteps(["ok (44) [1]", "ok (44) [2]"]);
|
||||
|
||||
def2.resolve();
|
||||
await tick();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("two promises resolved in reverse order", async () => {
|
||||
const race = new Race();
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
|
||||
race.add(def1).then((v) => expect.step(`ok (${v}) [1]`));
|
||||
race.add(def2).then((v) => expect.step(`ok (${v}) [2]`));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def2.resolve(44);
|
||||
await tick();
|
||||
expect.verifySteps(["ok (44) [1]", "ok (44) [2]"]);
|
||||
|
||||
def1.resolve();
|
||||
await tick();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("multiple resolutions", async () => {
|
||||
const race = new Race();
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
const def3 = new Deferred();
|
||||
|
||||
race.add(def1).then((v) => expect.step(`ok (${v}) [1]`));
|
||||
def1.resolve(44);
|
||||
await tick();
|
||||
expect.verifySteps(["ok (44) [1]"]);
|
||||
|
||||
race.add(def2).then((v) => expect.step(`ok (${v}) [2]`));
|
||||
race.add(def3).then((v) => expect.step(`ok (${v}) [3]`));
|
||||
def2.resolve(44);
|
||||
await tick();
|
||||
expect.verifySteps(["ok (44) [2]", "ok (44) [3]"]);
|
||||
});
|
||||
|
||||
test("catch rejected promise", async () => {
|
||||
const race = new Race();
|
||||
const def = new Deferred();
|
||||
|
||||
race.add(def).catch((v) => expect.step(`not ok (${v})`));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def.reject(44);
|
||||
await tick();
|
||||
expect.verifySteps(["not ok (44)"]);
|
||||
});
|
||||
|
||||
test("first promise rejects first", async () => {
|
||||
const race = new Race();
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
|
||||
race.add(def1).catch((v) => expect.step(`not ok (${v}) [1]`));
|
||||
race.add(def2).catch((v) => expect.step(`not ok (${v}) [2]`));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def1.reject(44);
|
||||
await tick();
|
||||
expect.verifySteps(["not ok (44) [1]", "not ok (44) [2]"]);
|
||||
|
||||
def2.resolve();
|
||||
await tick();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("second promise rejects after", async () => {
|
||||
const race = new Race();
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
|
||||
race.add(def1).then((v) => expect.step(`ok (${v}) [1]`));
|
||||
race.add(def2).then((v) => expect.step(`ok (${v}) [2]`));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def1.resolve(44);
|
||||
await tick();
|
||||
expect.verifySteps(["ok (44) [1]", "ok (44) [2]"]);
|
||||
|
||||
def2.reject();
|
||||
await tick();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("second promise rejects first", async () => {
|
||||
const race = new Race();
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
|
||||
race.add(def1).catch((v) => expect.step(`not ok (${v}) [1]`));
|
||||
race.add(def2).catch((v) => expect.step(`not ok (${v}) [2]`));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def2.reject(44);
|
||||
await tick();
|
||||
expect.verifySteps(["not ok (44) [1]", "not ok (44) [2]"]);
|
||||
|
||||
def1.resolve();
|
||||
await tick();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("first promise rejects after", async () => {
|
||||
const race = new Race();
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
|
||||
race.add(def1).then((v) => expect.step(`ok (${v}) [1]`));
|
||||
race.add(def2).then((v) => expect.step(`ok (${v}) [2]`));
|
||||
expect.verifySteps([]);
|
||||
|
||||
def2.resolve(44);
|
||||
await tick();
|
||||
expect.verifySteps(["ok (44) [1]", "ok (44) [2]"]);
|
||||
|
||||
def1.reject();
|
||||
await tick();
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("getCurrentProm", async () => {
|
||||
const race = new Race();
|
||||
const def1 = new Deferred();
|
||||
const def2 = new Deferred();
|
||||
const def3 = new Deferred();
|
||||
expect(race.getCurrentProm()).toBe(null);
|
||||
|
||||
race.add(def1);
|
||||
race.getCurrentProm().then((v) => expect.step(`ok (${v})`));
|
||||
def1.resolve(44);
|
||||
await tick();
|
||||
expect.verifySteps(["ok (44)"]);
|
||||
expect(race.getCurrentProm()).toBe(null);
|
||||
|
||||
race.add(def2);
|
||||
race.getCurrentProm().then((v) => expect.step(`ok (${v})`));
|
||||
race.add(def3);
|
||||
def3.resolve(44);
|
||||
await tick();
|
||||
expect.verifySteps(["ok (44)"]);
|
||||
expect(race.getCurrentProm()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,406 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { queryRect } from "@odoo/hoot-dom";
|
||||
import { animationFrame, mockTouch } from "@odoo/hoot-mock";
|
||||
import { Component, reactive, useRef, useState, xml } from "@odoo/owl";
|
||||
import { contains, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { useDraggable } from "@web/core/utils/draggable";
|
||||
|
||||
test("Parameters error handling", async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const mountList = async (setupList) => {
|
||||
class List extends Component {
|
||||
static 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>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
setupList();
|
||||
}
|
||||
}
|
||||
await mountWithCleanup(List);
|
||||
};
|
||||
|
||||
// Incorrect params
|
||||
await mountList(() => {
|
||||
expect(() => useDraggable({})).toThrow(
|
||||
`Error in hook useDraggable: missing required property "ref" in parameter`
|
||||
);
|
||||
});
|
||||
await mountList(() => {
|
||||
expect(() =>
|
||||
useDraggable({
|
||||
elements: ".item",
|
||||
})
|
||||
).toThrow(`Error in hook useDraggable: missing required property "ref" in parameter`);
|
||||
});
|
||||
|
||||
// Correct params
|
||||
await mountList(() => {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
});
|
||||
});
|
||||
await mountList(() => {
|
||||
useDraggable({
|
||||
ref: {},
|
||||
elements: ".item",
|
||||
enable: false,
|
||||
});
|
||||
});
|
||||
await mountList(() => {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("Simple dragging in single group", async () => {
|
||||
expect.assertions(11);
|
||||
|
||||
class List extends Component {
|
||||
static 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>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
onDragStart({ element }) {
|
||||
expect.step("start");
|
||||
expect(element).toHaveText("1");
|
||||
},
|
||||
onDragEnd({ element }) {
|
||||
expect.step("end");
|
||||
expect(element).toHaveText("1");
|
||||
expect(".item").toHaveCount(3);
|
||||
expect(".item.o_dragged").toHaveCount(1);
|
||||
},
|
||||
onDrop({ element }) {
|
||||
expect.step("drop");
|
||||
expect(element).toHaveText("1");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(List);
|
||||
|
||||
expect(".item").toHaveCount(3);
|
||||
expect(".o_dragged").toHaveCount(0);
|
||||
expect.verifySteps([]);
|
||||
|
||||
// First item after 2nd item
|
||||
await contains(".item:first-child").dragAndDrop(".item:nth-child(2)");
|
||||
|
||||
expect(".item").toHaveCount(3);
|
||||
expect(".o_dragged").toHaveCount(0);
|
||||
expect.verifySteps(["start", "drop", "end"]);
|
||||
});
|
||||
|
||||
test("Dynamically disable draggable feature", async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const state = reactive({ enableDrag: true });
|
||||
class List extends Component {
|
||||
static 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>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.state = useState(state);
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
enable: () => this.state.enableDrag,
|
||||
onDragStart() {
|
||||
expect.step("start");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(List);
|
||||
|
||||
expect.verifySteps([]);
|
||||
|
||||
// First item before last item
|
||||
await contains(".item:first-child").dragAndDrop(".item:last-child");
|
||||
|
||||
// Drag should have occurred
|
||||
expect.verifySteps(["start"]);
|
||||
|
||||
state.enableDrag = false;
|
||||
await animationFrame();
|
||||
|
||||
// First item before last item
|
||||
await contains(".item:first-child").dragAndDrop(".item:last-child");
|
||||
|
||||
// Drag shouldn't have occurred
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("Ignore specified elements", async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
class List extends Component {
|
||||
static 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>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
ignore: ".ignored",
|
||||
onDragStart() {
|
||||
expect.step("start");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(List);
|
||||
|
||||
expect.verifySteps([]);
|
||||
|
||||
// Drag root item element
|
||||
await contains(".item:first-child").dragAndDrop(".item:nth-child(2)");
|
||||
|
||||
expect.verifySteps(["start"]);
|
||||
|
||||
// Drag ignored element
|
||||
await contains(".item:first-child .not-ignored").dragAndDrop(".item:nth-child(2)");
|
||||
|
||||
expect.verifySteps(["start"]);
|
||||
|
||||
// Drag non-ignored element
|
||||
await contains(".item:first-child .ignored").dragAndDrop(".item:nth-child(2)");
|
||||
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
|
||||
test("Ignore specific elements in a nested draggable", async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
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>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
preventDrag: (el) => el.classList.contains("ignored"),
|
||||
onDragStart() {
|
||||
expect.step("start");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(List);
|
||||
|
||||
expect.verifySteps([]);
|
||||
|
||||
// Drag ignored under non-ignored -> block
|
||||
await contains(".not-ignored.parent .ignored.child").dragAndDrop(
|
||||
".not-ignored.parent .not-ignored.child"
|
||||
);
|
||||
expect.verifySteps([]);
|
||||
|
||||
// Drag not-ignored-under not-ignored -> succeed
|
||||
await contains(".not-ignored.parent .not-ignored.child").dragAndDrop(
|
||||
".not-ignored.parent .ignored.child"
|
||||
);
|
||||
expect.verifySteps(["start"]);
|
||||
|
||||
// Drag ignored under ignored -> block
|
||||
await contains(".ignored.parent .ignored.child").dragAndDrop(
|
||||
".ignored.parent .not-ignored.child"
|
||||
);
|
||||
expect.verifySteps([]);
|
||||
|
||||
// Drag not-ignored under ignored -> succeed
|
||||
await contains(".ignored.parent .not-ignored.child").dragAndDrop(
|
||||
".ignored.parent .ignored.child"
|
||||
);
|
||||
expect.verifySteps(["start"]);
|
||||
});
|
||||
|
||||
test("Dragging element with touch event", async () => {
|
||||
expect.assertions(4);
|
||||
mockTouch(true);
|
||||
class List extends Component {
|
||||
static 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>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
onDragStart({ element }) {
|
||||
expect.step("start");
|
||||
expect(".item.o_dragged").toHaveCount(1);
|
||||
},
|
||||
onDragEnd() {
|
||||
expect.step("end");
|
||||
},
|
||||
onDrop() {
|
||||
expect.step("drop");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(List);
|
||||
|
||||
expect.verifySteps([]);
|
||||
|
||||
// Should DnD, if the timing value is higher then the default delay value (300ms)
|
||||
await contains(".item:first-child").dragAndDrop(".item:nth-child(2)");
|
||||
|
||||
expect(".item.o_touch_bounce").toHaveCount(0, {
|
||||
message: "element no longer has the animation class applied",
|
||||
});
|
||||
expect.verifySteps(["start", "drop", "end"]);
|
||||
});
|
||||
|
||||
test("Dragging element with touch event: initiation delay can be overrided", async () => {
|
||||
mockTouch(true);
|
||||
class List extends Component {
|
||||
static 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>`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
delay: 1000,
|
||||
elements: ".item",
|
||||
onDragStart() {
|
||||
expect.step("start");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(List);
|
||||
await contains(".item:first-child").dragAndDrop(".item:nth-child(2)", {
|
||||
pointerDownDuration: 700,
|
||||
});
|
||||
|
||||
// Shouldn't DnD, if the timing value is below then the delay value (1000ms)
|
||||
expect.verifySteps([]);
|
||||
|
||||
await contains(".item:first-child").dragAndDrop(".item:nth-child(2)", {
|
||||
pointerDownDuration: 1200,
|
||||
});
|
||||
|
||||
// Should DnD, if the timing value is higher then the delay value (1000ms)
|
||||
expect.verifySteps(["start"]);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("Elements are confined within their container", async () => {
|
||||
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>
|
||||
`;
|
||||
static props = ["*"];
|
||||
|
||||
setup() {
|
||||
useDraggable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(List);
|
||||
|
||||
const containerRect = queryRect(".root");
|
||||
|
||||
const { moveTo, drop } = await contains(".item:first").drag({
|
||||
initialPointerMoveDistance: 0,
|
||||
position: { x: 0, y: 0 },
|
||||
});
|
||||
|
||||
expect(".item:first").toHaveRect({
|
||||
x: containerRect.x,
|
||||
y: containerRect.y,
|
||||
width: containerRect.width / 2,
|
||||
});
|
||||
|
||||
await moveTo(".item:last-child", {
|
||||
position: { x: 0, y: 9999 },
|
||||
});
|
||||
|
||||
expect(".item:first").toHaveRect({
|
||||
x: containerRect.x,
|
||||
y: containerRect.y + containerRect.height - queryRect(".item:first").height,
|
||||
});
|
||||
|
||||
await moveTo(".item:last-child", {
|
||||
position: { x: 9999, y: 9999 },
|
||||
});
|
||||
|
||||
expect(".item:first").toHaveRect({
|
||||
x: containerRect.x + containerRect.width - queryRect(".item:first").width,
|
||||
y: containerRect.y + containerRect.height - queryRect(".item:first").height,
|
||||
});
|
||||
|
||||
await moveTo(".item:last-child", {
|
||||
position: { x: -9999, y: -9999 },
|
||||
});
|
||||
|
||||
expect(".item:first").toHaveRect({
|
||||
x: containerRect.x,
|
||||
y: containerRect.y,
|
||||
});
|
||||
|
||||
await drop();
|
||||
});
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { memoize, uniqueId } from "@web/core/utils/functions";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("memoize", () => {
|
||||
let callCount = 0;
|
||||
let lastReceivedArgs;
|
||||
const func = function () {
|
||||
lastReceivedArgs = [...arguments];
|
||||
return callCount++;
|
||||
};
|
||||
const memoized = memoize(func);
|
||||
const firstValue = memoized("first");
|
||||
expect(callCount).toBe(1);
|
||||
expect(lastReceivedArgs).toEqual(["first"]);
|
||||
const secondValue = memoized("first");
|
||||
// Subsequent calls to memoized function with the same argument do not call the original function again
|
||||
expect(callCount).toBe(1);
|
||||
// Subsequent call to memoized function with the same argument returns the same value
|
||||
expect(firstValue).toBe(secondValue);
|
||||
const thirdValue = memoized();
|
||||
// Subsequent calls to memoized function with a different argument call the original function again
|
||||
expect(callCount).toBe(2);
|
||||
const fourthValue = memoized();
|
||||
// Memoization also works with no first argument as a key
|
||||
expect(thirdValue).toBe(fourthValue);
|
||||
// Subsequent calls to memoized function with no first argument do not call the original function again
|
||||
expect(callCount).toBe(2);
|
||||
memoized(1, 2, 3);
|
||||
expect(callCount).toBe(3);
|
||||
// Arguments after the first one are passed through correctly
|
||||
expect(lastReceivedArgs).toEqual([1, 2, 3]);
|
||||
memoized(1, 20, 30);
|
||||
// Subsequent calls to memoized function with more than one argument do not call the original function again even if the arguments other than the first have changed
|
||||
expect(callCount).toBe(3);
|
||||
});
|
||||
|
||||
test("memoized function inherit function name if possible", () => {
|
||||
const memoized1 = memoize(function test() {});
|
||||
expect(memoized1.name).toBe("test (memoized)");
|
||||
const memoized2 = memoize(function () {});
|
||||
expect(memoized2.name).toBe("memoized");
|
||||
});
|
||||
|
||||
test("uniqueId", () => {
|
||||
patchWithCleanup(uniqueId, { nextId: 0 });
|
||||
expect(uniqueId("test_")).toBe("test_1");
|
||||
expect(uniqueId("bla")).toBe("bla2");
|
||||
expect(uniqueId("test_")).toBe("test_3");
|
||||
expect(uniqueId("bla")).toBe("bla4");
|
||||
expect(uniqueId("test_")).toBe("test_5");
|
||||
expect(uniqueId("test_")).toBe("test_6");
|
||||
expect(uniqueId("bla")).toBe("bla7");
|
||||
});
|
||||
|
|
@ -0,0 +1,677 @@
|
|||
import { describe, expect, getFixture, test } from "@odoo/hoot";
|
||||
import { click, queryOne } from "@odoo/hoot-dom";
|
||||
import { Deferred, animationFrame, mockTouch } from "@odoo/hoot-mock";
|
||||
import {
|
||||
contains,
|
||||
getService,
|
||||
makeMockEnv,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { Component, onMounted, reactive, useState, xml } from "@odoo/owl";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { CommandPalette } from "@web/core/commands/command_palette";
|
||||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
useAutofocus,
|
||||
useBus,
|
||||
useChildRef,
|
||||
useForwardRefToParent,
|
||||
useService,
|
||||
useServiceProtectMethodHandling,
|
||||
useSpellCheck,
|
||||
} from "@web/core/utils/hooks";
|
||||
|
||||
describe("useAutofocus", () => {
|
||||
test.tags("desktop");
|
||||
test("simple usecase", async () => {
|
||||
const state = reactive({ text: "" });
|
||||
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<span>
|
||||
<input type="text" t-ref="autofocus" t-att-value="state.text" />
|
||||
</span>
|
||||
`;
|
||||
setup() {
|
||||
useAutofocus();
|
||||
|
||||
this.state = useState(state);
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect("input").toBeFocused();
|
||||
|
||||
state.text = "a";
|
||||
await animationFrame();
|
||||
|
||||
expect("input").toBeFocused();
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("simple usecase when input type is number", async () => {
|
||||
const state = reactive({ counter: 0 });
|
||||
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<span>
|
||||
<input type="number" t-ref="autofocus" t-att-value="state.counter" />
|
||||
</span>
|
||||
`;
|
||||
setup() {
|
||||
useAutofocus();
|
||||
|
||||
this.state = useState(state);
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect("input").toBeFocused();
|
||||
|
||||
state.counter++;
|
||||
await animationFrame();
|
||||
|
||||
expect("input").toBeFocused();
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("conditional autofocus", async () => {
|
||||
const state = reactive({ showInput: true });
|
||||
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<span>
|
||||
<input t-if="state.showInput" type="text" t-ref="autofocus" />
|
||||
</span>
|
||||
`;
|
||||
setup() {
|
||||
useAutofocus();
|
||||
|
||||
this.state = useState(state);
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect("input").toBeFocused();
|
||||
|
||||
state.showInput = false;
|
||||
await animationFrame();
|
||||
|
||||
expect(document.body).toBeFocused();
|
||||
|
||||
state.showInput = true;
|
||||
await animationFrame();
|
||||
|
||||
expect("input").toBeFocused();
|
||||
});
|
||||
|
||||
test("returns also a ref when screen has touch but it does not focus", async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
mockTouch(true);
|
||||
|
||||
class MyComponent extends Component {
|
||||
static template = xml`
|
||||
<span>
|
||||
<input type="text" t-ref="autofocus" />
|
||||
</span>
|
||||
`;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
const inputRef = useAutofocus();
|
||||
onMounted(() => {
|
||||
expect(inputRef.el).toBeInstanceOf(HTMLInputElement);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
expect("input").not.toBeFocused();
|
||||
});
|
||||
|
||||
test("works when screen has touch and you provide mobile param", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<span>
|
||||
<input type="text" t-ref="autofocus" />
|
||||
</span>
|
||||
`;
|
||||
setup() {
|
||||
useAutofocus({ mobile: true });
|
||||
}
|
||||
}
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
matchMedia: (media) => {
|
||||
if (media === "(pointer:coarse)") {
|
||||
return { matches: true };
|
||||
}
|
||||
this._super();
|
||||
},
|
||||
});
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect("input").toBeFocused();
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("supports different ref names", async () => {
|
||||
const state = reactive({ showSecond: true });
|
||||
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<span>
|
||||
<input type="text" t-ref="first" />
|
||||
<input t-if="state.showSecond" type="text" t-ref="second" />
|
||||
</span>
|
||||
`;
|
||||
setup() {
|
||||
useAutofocus({ refName: "second" });
|
||||
useAutofocus({ refName: "first" }); // test requires this at second position
|
||||
|
||||
this.state = useState(state);
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
// "first" is focused first since it has the last call to "useAutofocus"
|
||||
expect("input:first").toBeFocused();
|
||||
|
||||
// We now remove and add again the second input, which triggers the useEffect of the hook and and apply focus
|
||||
state.showSecond = false;
|
||||
await animationFrame();
|
||||
|
||||
expect("input:first").toBeFocused();
|
||||
|
||||
state.showSecond = true;
|
||||
await animationFrame();
|
||||
|
||||
expect("input:last").toBeFocused();
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("can select its content", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<span>
|
||||
<input type="text" value="input content" t-ref="autofocus" />
|
||||
</span>
|
||||
`;
|
||||
setup() {
|
||||
useAutofocus({ selectAll: true });
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect("input").toBeFocused();
|
||||
expect("input").toHaveProperty("selectionStart", 0);
|
||||
expect("input").toHaveProperty("selectionEnd", 13);
|
||||
});
|
||||
|
||||
test.tags("desktop");
|
||||
test("autofocus outside of active element doesn't work (CommandPalette)", async () => {
|
||||
const state = reactive({
|
||||
showPalette: true,
|
||||
text: "",
|
||||
});
|
||||
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<div>
|
||||
<input type="text" t-ref="autofocus" t-att-value="state.text" />
|
||||
</div>
|
||||
`;
|
||||
setup() {
|
||||
useAutofocus();
|
||||
|
||||
this.state = useState(state);
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect("input:first").toBeFocused();
|
||||
|
||||
getService("dialog").add(CommandPalette, {
|
||||
config: { providers: [] },
|
||||
});
|
||||
await animationFrame();
|
||||
|
||||
expect(".o_command_palette").toHaveCount(1);
|
||||
expect("input:first").not.toBeFocused();
|
||||
|
||||
state.text = "a";
|
||||
await animationFrame();
|
||||
|
||||
expect("input:first").not.toBeFocused();
|
||||
});
|
||||
});
|
||||
|
||||
describe("useBus", () => {
|
||||
test("simple usecase", async () => {
|
||||
const state = reactive({ child: true });
|
||||
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<div/>`;
|
||||
setup() {
|
||||
useBus(this.env.bus, "test-event", this.myCallback);
|
||||
}
|
||||
myCallback() {
|
||||
expect.step("callback");
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { MyComponent };
|
||||
static props = ["*"];
|
||||
static template = xml`<MyComponent t-if="state.child" />`;
|
||||
|
||||
setup() {
|
||||
this.state = useState(state);
|
||||
}
|
||||
}
|
||||
|
||||
const { bus } = await makeMockEnv();
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
bus.trigger("test-event");
|
||||
expect.verifySteps(["callback"]);
|
||||
|
||||
state.child = false;
|
||||
await animationFrame();
|
||||
|
||||
bus.trigger("test-event");
|
||||
expect.verifySteps([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("useService", () => {
|
||||
test("unavailable service", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<div/>`;
|
||||
setup() {
|
||||
useService("toy_service");
|
||||
}
|
||||
}
|
||||
|
||||
await expect(mountWithCleanup(MyComponent)).rejects.toThrow(
|
||||
"Service toy_service is not available"
|
||||
);
|
||||
});
|
||||
|
||||
test("service that returns null", async () => {
|
||||
let toyService;
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<div/>`;
|
||||
setup() {
|
||||
toyService = useService("toy_service");
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("services").add("toy_service", {
|
||||
name: "toy_service",
|
||||
start: () => null,
|
||||
});
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect(toyService).toBe(null);
|
||||
});
|
||||
|
||||
test("async service with protected methods", async () => {
|
||||
useServiceProtectMethodHandling.fn = useServiceProtectMethodHandling.original;
|
||||
const state = reactive({ child: true });
|
||||
let nbCalls = 0;
|
||||
let def = new Deferred();
|
||||
let objectService;
|
||||
let functionService;
|
||||
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<div/>`;
|
||||
|
||||
setup() {
|
||||
objectService = useService("object_service");
|
||||
functionService = useService("function_service");
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { MyComponent };
|
||||
static props = ["*"];
|
||||
static template = xml`<MyComponent t-if="state.child" />`;
|
||||
|
||||
setup() {
|
||||
this.state = useState(state);
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("services").add("object_service", {
|
||||
name: "object_service",
|
||||
async: ["asyncMethod"],
|
||||
start() {
|
||||
return {
|
||||
async asyncMethod() {
|
||||
nbCalls++;
|
||||
await def;
|
||||
return this;
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
registry.category("services").add("function_service", {
|
||||
name: "function_service",
|
||||
async: true,
|
||||
start() {
|
||||
return async function asyncFunc() {
|
||||
nbCalls++;
|
||||
await def;
|
||||
return this;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
|
||||
// Functions and methods have the correct this
|
||||
def.resolve();
|
||||
await expect(objectService.asyncMethod()).resolves.toBe(objectService);
|
||||
await expect(objectService.asyncMethod.call("boundThis")).resolves.toBe("boundThis");
|
||||
await expect(functionService()).resolves.toBe(undefined);
|
||||
await expect(functionService.call("boundThis")).resolves.toBe("boundThis");
|
||||
expect(nbCalls).toBe(4);
|
||||
|
||||
// Functions that were called before the component is destroyed but resolved after never resolve
|
||||
def = new Deferred();
|
||||
objectService.asyncMethod().then(() => expect.step("resolved"));
|
||||
objectService.asyncMethod.call("boundThis").then(() => expect.step("resolved"));
|
||||
functionService().then(() => expect.step("resolved"));
|
||||
functionService.call("boundThis").then(() => expect.step("resolved"));
|
||||
expect(nbCalls).toBe(8);
|
||||
|
||||
state.child = false;
|
||||
await animationFrame();
|
||||
def.resolve();
|
||||
expect.verifySteps([]);
|
||||
|
||||
// Calling the functions after the destruction rejects the promise
|
||||
await expect(objectService.asyncMethod()).rejects.toThrow("Component is destroyed");
|
||||
await expect(objectService.asyncMethod.call("boundThis")).rejects.toThrow(
|
||||
"Component is destroyed"
|
||||
);
|
||||
await expect(functionService()).rejects.toThrow("Component is destroyed");
|
||||
await expect(functionService.call("boundThis")).rejects.toThrow("Component is destroyed");
|
||||
expect(nbCalls).toBe(8);
|
||||
useServiceProtectMethodHandling.fn = useServiceProtectMethodHandling.mocked;
|
||||
});
|
||||
});
|
||||
|
||||
describe("useSpellCheck", () => {
|
||||
test("ref is on the textarea", async () => {
|
||||
// To understand correctly the test, refer to the MDN documentation of spellcheck.
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<div><textarea t-ref="spellcheck" class="textArea"/></div>`;
|
||||
setup() {
|
||||
useSpellCheck();
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect(".textArea").toHaveProperty("spellcheck", true);
|
||||
expect(".textArea").not.toHaveAttribute("spellcheck");
|
||||
|
||||
// Focus textarea
|
||||
await click(".textArea");
|
||||
expect(".textArea").toBeFocused();
|
||||
|
||||
// Click out to trigger blur
|
||||
await click(getFixture());
|
||||
|
||||
expect(".textArea").toHaveProperty("spellcheck", false);
|
||||
expect(".textArea").toHaveAttribute("spellcheck", "false");
|
||||
|
||||
// Focus textarea
|
||||
await click(".textArea");
|
||||
|
||||
expect(".textArea").toHaveProperty("spellcheck", true);
|
||||
expect(".textArea").toHaveAttribute("spellcheck", "true");
|
||||
});
|
||||
|
||||
test("use a different refName", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<div><textarea t-ref="myreference" class="textArea"/></div>`;
|
||||
setup() {
|
||||
useSpellCheck({ refName: "myreference" });
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect(".textArea").toHaveProperty("spellcheck", true);
|
||||
expect(".textArea").not.toHaveAttribute("spellcheck");
|
||||
|
||||
await click(".textArea");
|
||||
|
||||
expect(".textArea").toBeFocused();
|
||||
|
||||
// Click out to trigger blur
|
||||
await click(getFixture());
|
||||
|
||||
// Once these assertions pass, it means that the hook is working.
|
||||
expect(".textArea").toHaveProperty("spellcheck", false);
|
||||
expect(".textArea").toHaveAttribute("spellcheck", "false");
|
||||
});
|
||||
|
||||
test("ref is on the root element and two editable elements", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<div t-ref="spellcheck">
|
||||
<textarea class="textArea"/>
|
||||
<div contenteditable="true" class="editableDiv"/>
|
||||
</div>`;
|
||||
setup() {
|
||||
useSpellCheck();
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect(".textArea").toHaveProperty("spellcheck", true);
|
||||
expect(".editableDiv").toHaveProperty("spellcheck", true);
|
||||
expect(".textArea").not.toHaveAttribute("spellcheck");
|
||||
expect(".editableDiv").not.toHaveAttribute("spellcheck");
|
||||
|
||||
// Focus textarea
|
||||
await click(".textArea");
|
||||
expect(".textArea").toBeFocused();
|
||||
|
||||
// Focus editable div
|
||||
await click(".editableDiv");
|
||||
expect(".editableDiv").toBeFocused();
|
||||
|
||||
// Click out to trigger blur
|
||||
await click(getFixture());
|
||||
|
||||
expect(".textArea").toHaveProperty("spellcheck", false);
|
||||
expect(".editableDiv").toHaveProperty("spellcheck", false);
|
||||
expect(".textArea").toHaveAttribute("spellcheck", "false");
|
||||
expect(".editableDiv").toHaveAttribute("spellcheck", "false");
|
||||
|
||||
// Focus textarea
|
||||
await click(".textArea");
|
||||
|
||||
expect(".textArea").toHaveProperty("spellcheck", true);
|
||||
expect(".textArea").toHaveAttribute("spellcheck", "true");
|
||||
expect(".editableDiv").toHaveProperty("spellcheck", false);
|
||||
expect(".editableDiv").toHaveAttribute("spellcheck", "false");
|
||||
|
||||
// Focus editable div
|
||||
await click(".editableDiv");
|
||||
|
||||
expect(".textArea").toHaveProperty("spellcheck", false);
|
||||
expect(".textArea").toHaveAttribute("spellcheck", "false");
|
||||
expect(".editableDiv").toHaveProperty("spellcheck", true);
|
||||
expect(".editableDiv").toHaveAttribute("spellcheck", "true");
|
||||
});
|
||||
|
||||
test("ref is on the root element and one element has disabled the spellcheck", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<div t-ref="spellcheck">
|
||||
<textarea class="textArea"/>
|
||||
<div contenteditable="true" spellcheck="false" class="editableDiv"/>
|
||||
</div>`;
|
||||
setup() {
|
||||
useSpellCheck();
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
|
||||
expect(".textArea").toHaveProperty("spellcheck", true);
|
||||
expect(".editableDiv").toHaveProperty("spellcheck", false);
|
||||
expect(".textArea").not.toHaveAttribute("spellcheck");
|
||||
expect(".editableDiv").toHaveAttribute("spellcheck", "false");
|
||||
|
||||
// Focus textarea
|
||||
await click(".textArea");
|
||||
expect(".textArea").toBeFocused();
|
||||
|
||||
// Focus editable div
|
||||
await click(".editableDiv");
|
||||
expect(".editableDiv").toBeFocused();
|
||||
|
||||
// Click out to trigger blur
|
||||
await click(getFixture());
|
||||
|
||||
expect(".textArea").toHaveProperty("spellcheck", false);
|
||||
expect(".textArea").toHaveAttribute("spellcheck", "false");
|
||||
expect(".editableDiv").toHaveProperty("spellcheck", false);
|
||||
expect(".editableDiv").toHaveAttribute("spellcheck", "false");
|
||||
|
||||
// Focus textarea
|
||||
await click(".textArea");
|
||||
|
||||
expect(".textArea").toHaveProperty("spellcheck", true);
|
||||
expect(".textArea").toHaveAttribute("spellcheck", "true");
|
||||
expect(".editableDiv").toHaveProperty("spellcheck", false);
|
||||
expect(".editableDiv").toHaveAttribute("spellcheck", "false");
|
||||
|
||||
// Focus editable div
|
||||
await click(".editableDiv");
|
||||
|
||||
expect(".textArea").toHaveProperty("spellcheck", false);
|
||||
expect(".textArea").toHaveAttribute("spellcheck", "false");
|
||||
expect(".editableDiv").toHaveProperty("spellcheck", false);
|
||||
expect(".editableDiv").toHaveAttribute("spellcheck", "false");
|
||||
});
|
||||
|
||||
test("ref is on an element with contenteditable attribute", async () => {
|
||||
class MyComponent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`
|
||||
<div t-ref="spellcheck" contenteditable="true" class="editableDiv" />`;
|
||||
setup() {
|
||||
useSpellCheck();
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(MyComponent);
|
||||
expect(".editableDiv").toHaveProperty("spellcheck", true);
|
||||
await contains(".editableDiv").click();
|
||||
expect(".editableDiv").toBeFocused();
|
||||
expect(".editableDiv").toHaveAttribute("spellcheck", "true");
|
||||
await click(getFixture());
|
||||
expect(".editableDiv").toHaveAttribute("spellcheck", "false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("useChildRef and useForwardRefToParent", () => {
|
||||
test("simple usecase", async () => {
|
||||
let childRef;
|
||||
let parentRef;
|
||||
|
||||
class Child extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<span t-ref="someRef" class="my_span">Hello</span>`;
|
||||
setup() {
|
||||
childRef = useForwardRefToParent("someRef");
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<div><Child someRef="someRef"/></div>`;
|
||||
static components = { Child };
|
||||
setup() {
|
||||
this.someRef = useChildRef();
|
||||
parentRef = this.someRef;
|
||||
}
|
||||
}
|
||||
|
||||
await mountWithCleanup(Parent);
|
||||
expect(childRef.el).toBe(queryOne(".my_span"));
|
||||
expect(parentRef.el).toBe(queryOne(".my_span"));
|
||||
});
|
||||
|
||||
test("in a conditional child", async () => {
|
||||
class Child extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<span t-ref="someRef" class="my_span">Hello</span>`;
|
||||
setup() {
|
||||
useForwardRefToParent("someRef");
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static props = ["*"];
|
||||
static template = xml`<div><Child t-if="state.hasChild" someRef="someRef"/></div>`;
|
||||
static components = { Child };
|
||||
setup() {
|
||||
this.someRef = useChildRef();
|
||||
this.state = useState({ hasChild: true });
|
||||
}
|
||||
}
|
||||
|
||||
const parentComponent = await mountWithCleanup(Parent);
|
||||
|
||||
expect(".my_span").toHaveCount(1);
|
||||
expect(parentComponent.someRef.el).toBe(queryOne(".my_span"));
|
||||
|
||||
parentComponent.state.hasChild = false;
|
||||
await animationFrame();
|
||||
|
||||
expect(".my_span").toHaveCount(0);
|
||||
expect(parentComponent.someRef.el).toBe(null);
|
||||
|
||||
parentComponent.state.hasChild = true;
|
||||
await animationFrame();
|
||||
|
||||
expect(".my_span").toHaveCount(1);
|
||||
expect(parentComponent.someRef.el).toBe(queryOne(".my_span"));
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { markup } from "@odoo/owl";
|
||||
|
||||
const Markup = markup().constructor;
|
||||
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { htmlEscape, isHtmlEmpty, setElementContent } from "@web/core/utils/html";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("htmlEscape escapes text", () => {
|
||||
const res = htmlEscape("<p>test</p>");
|
||||
expect(res.toString()).toBe("<p>test</p>");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
});
|
||||
|
||||
test("htmlEscape keeps html markup", () => {
|
||||
const res = htmlEscape(markup("<p>test</p>"));
|
||||
expect(res.toString()).toBe("<p>test</p>");
|
||||
expect(res).toBeInstanceOf(Markup);
|
||||
});
|
||||
|
||||
test("isHtmlEmpty does not consider text as empty", () => {
|
||||
expect(isHtmlEmpty("<p></p>")).toBe(false);
|
||||
});
|
||||
|
||||
test("isHtmlEmpty considers empty html markup as empty", () => {
|
||||
expect(isHtmlEmpty(markup("<p></p>"))).toBe(true);
|
||||
});
|
||||
|
||||
test("setElementContent escapes text", () => {
|
||||
const div = document.createElement("div");
|
||||
setElementContent(div, "<p>test</p>");
|
||||
expect(div.innerHTML).toBe("<p>test</p>");
|
||||
});
|
||||
|
||||
test("setElementContent keeps html markup", () => {
|
||||
const div = document.createElement("div");
|
||||
setElementContent(div, markup("<p>test</p>"));
|
||||
expect(div.innerHTML).toBe("<p>test</p>");
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,337 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { patchTranslations, patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import {
|
||||
clamp,
|
||||
floatIsZero,
|
||||
formatFloat,
|
||||
range,
|
||||
roundDecimals,
|
||||
roundPrecision,
|
||||
} from "@web/core/utils/numbers";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("clamp", () => {
|
||||
expect(clamp(-5, 0, 10)).toBe(0);
|
||||
expect(clamp(0, 0, 10)).toBe(0);
|
||||
expect(clamp(2, 0, 10)).toBe(2);
|
||||
expect(clamp(5, 0, 10)).toBe(5);
|
||||
expect(clamp(7, 0, 10)).toBe(7);
|
||||
expect(clamp(10, 0, 10)).toBe(10);
|
||||
expect(clamp(15, 0, 10)).toBe(10);
|
||||
});
|
||||
|
||||
test("range", () => {
|
||||
expect(range(0, 10)).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
expect(range(0, 35, 5)).toEqual([0, 5, 10, 15, 20, 25, 30]);
|
||||
expect(range(-10, 6, 2)).toEqual([-10, -8, -6, -4, -2, 0, 2, 4]);
|
||||
expect(range(0, -10, -1)).toEqual([0, -1, -2, -3, -4, -5, -6, -7, -8, -9]);
|
||||
expect(range(4, -4, -1)).toEqual([4, 3, 2, 1, 0, -1, -2, -3]);
|
||||
expect(range(1, 4, -1)).toEqual([]);
|
||||
expect(range(1, -4, 1)).toEqual([]);
|
||||
});
|
||||
|
||||
describe("roundPrecision", () => {
|
||||
test("default method (HALF-UP)", () => {
|
||||
expect(roundPrecision(1.0, 1)).toBe(1);
|
||||
expect(roundPrecision(1.0, 0.1)).toBe(1);
|
||||
expect(roundPrecision(1.0, 0.01)).toBe(1);
|
||||
expect(roundPrecision(1.0, 0.001)).toBe(1);
|
||||
expect(roundPrecision(1.0, 0.0001)).toBe(1);
|
||||
expect(roundPrecision(1.0, 0.00001)).toBe(1);
|
||||
expect(roundPrecision(1.0, 0.000001)).toBe(1);
|
||||
expect(roundPrecision(1.0, 0.0000001)).toBe(1);
|
||||
expect(roundPrecision(1.0, 0.00000001)).toBe(1);
|
||||
expect(roundPrecision(0.5, 1)).toBe(1);
|
||||
expect(roundPrecision(-0.5, 1)).toBe(-1);
|
||||
expect(roundPrecision(2.6745, 0.001)).toBe(2.675);
|
||||
expect(roundPrecision(-2.6745, 0.001)).toBe(-2.675);
|
||||
expect(roundPrecision(2.6744, 0.001)).toBe(2.674);
|
||||
expect(roundPrecision(-2.6744, 0.001)).toBe(-2.674);
|
||||
expect(roundPrecision(0.0004, 0.001)).toBe(0);
|
||||
expect(roundPrecision(-0.0004, 0.001)).toBe(0);
|
||||
expect(roundPrecision(357.4555, 0.001)).toBe(357.456);
|
||||
expect(roundPrecision(-357.4555, 0.001)).toBe(-357.456);
|
||||
expect(roundPrecision(457.4554, 0.001)).toBe(457.455);
|
||||
expect(roundPrecision(-457.4554, 0.001)).toBe(-457.455);
|
||||
expect(roundPrecision(-457.4554, 0.05)).toBe(-457.45);
|
||||
expect(roundPrecision(457.444, 0.5)).toBe(457.5);
|
||||
expect(roundPrecision(457.3, 5)).toBe(455);
|
||||
expect(roundPrecision(457.5, 5)).toBe(460);
|
||||
expect(roundPrecision(457.1, 3)).toBe(456);
|
||||
|
||||
expect(roundPrecision(2.6735, 0.001)).toBe(2.674);
|
||||
expect(roundPrecision(-2.6735, 0.001)).toBe(-2.674);
|
||||
expect(roundPrecision(2.6745, 0.001)).toBe(2.675);
|
||||
expect(roundPrecision(-2.6745, 0.001)).toBe(-2.675);
|
||||
expect(roundPrecision(2.6744, 0.001)).toBe(2.674);
|
||||
expect(roundPrecision(-2.6744, 0.001)).toBe(-2.674);
|
||||
expect(roundPrecision(0.0004, 0.001)).toBe(0);
|
||||
expect(roundPrecision(-0.0004, 0.001)).toBe(-0);
|
||||
expect(roundPrecision(357.4555, 0.001)).toBe(357.456);
|
||||
expect(roundPrecision(-357.4555, 0.001)).toBe(-357.456);
|
||||
expect(roundPrecision(457.4554, 0.001)).toBe(457.455);
|
||||
expect(roundPrecision(-457.4554, 0.001)).toBe(-457.455);
|
||||
});
|
||||
|
||||
test("DOWN", () => {
|
||||
// We use 2.425 because when normalizing 2.425 with precision=0.001 it gives
|
||||
// us 2424.9999999999995 as value, and if not handle correctly the rounding DOWN
|
||||
// value will be incorrect (should be 2.425 and not 2.424)
|
||||
expect(roundPrecision(2.425, 0.001, "DOWN")).toBe(2.425);
|
||||
expect(roundPrecision(2.4249, 0.001, "DOWN")).toBe(2.424);
|
||||
expect(roundPrecision(-2.425, 0.001, "DOWN")).toBe(-2.425);
|
||||
expect(roundPrecision(-2.4249, 0.001, "DOWN")).toBe(-2.424);
|
||||
expect(roundPrecision(-2.5, 0.001, "DOWN")).toBe(-2.5);
|
||||
expect(roundPrecision(1.8, 1, "DOWN")).toBe(1);
|
||||
expect(roundPrecision(-1.8, 1, "DOWN")).toBe(-1);
|
||||
});
|
||||
|
||||
test("HALF-DOWN", () => {
|
||||
expect(roundPrecision(2.6735, 0.001, "HALF-DOWN")).toBe(2.673);
|
||||
expect(roundPrecision(-2.6735, 0.001, "HALF-DOWN")).toBe(-2.673);
|
||||
expect(roundPrecision(2.6745, 0.001, "HALF-DOWN")).toBe(2.674);
|
||||
expect(roundPrecision(-2.6745, 0.001, "HALF-DOWN")).toBe(-2.674);
|
||||
expect(roundPrecision(2.6744, 0.001, "HALF-DOWN")).toBe(2.674);
|
||||
expect(roundPrecision(-2.6744, 0.001, "HALF-DOWN")).toBe(-2.674);
|
||||
expect(roundPrecision(0.0004, 0.001, "HALF-DOWN")).toBe(0);
|
||||
expect(roundPrecision(-0.0004, 0.001, "HALF-DOWN")).toBe(-0);
|
||||
expect(roundPrecision(357.4555, 0.001, "HALF-DOWN")).toBe(357.455);
|
||||
expect(roundPrecision(-357.4555, 0.001, "HALF-DOWN")).toBe(-357.455);
|
||||
expect(roundPrecision(457.4554, 0.001, "HALF-DOWN")).toBe(457.455);
|
||||
expect(roundPrecision(-457.4554, 0.001, "HALF-DOWN")).toBe(-457.455);
|
||||
});
|
||||
|
||||
test("HALF-UP", () => {
|
||||
expect(roundPrecision(2.6735, 0.001, "HALF-UP")).toBe(2.674);
|
||||
expect(roundPrecision(-2.6735, 0.001, "HALF-UP")).toBe(-2.674);
|
||||
expect(roundPrecision(2.6745, 0.001, "HALF-UP")).toBe(2.675);
|
||||
expect(roundPrecision(-2.6745, 0.001, "HALF-UP")).toBe(-2.675);
|
||||
expect(roundPrecision(2.6744, 0.001, "HALF-UP")).toBe(2.674);
|
||||
expect(roundPrecision(-2.6744, 0.001, "HALF-UP")).toBe(-2.674);
|
||||
expect(roundPrecision(0.0004, 0.001, "HALF-UP")).toBe(0);
|
||||
expect(roundPrecision(-0.0004, 0.001, "HALF-UP")).toBe(-0);
|
||||
expect(roundPrecision(357.4555, 0.001, "HALF-UP")).toBe(357.456);
|
||||
expect(roundPrecision(-357.4555, 0.001, "HALF-UP")).toBe(-357.456);
|
||||
expect(roundPrecision(457.4554, 0.001, "HALF-UP")).toBe(457.455);
|
||||
expect(roundPrecision(-457.4554, 0.001, "HALF-UP")).toBe(-457.455);
|
||||
});
|
||||
|
||||
test("HALF-EVEN", () => {
|
||||
expect(roundPrecision(5.015, 0.01, "HALF-EVEN")).toBe(5.02);
|
||||
expect(roundPrecision(-5.015, 0.01, "HALF-EVEN")).toBe(-5.02);
|
||||
expect(roundPrecision(5.025, 0.01, "HALF-EVEN")).toBe(5.02);
|
||||
expect(roundPrecision(-5.025, 0.01, "HALF-EVEN")).toBe(-5.02);
|
||||
expect(roundPrecision(2.6735, 0.001, "HALF-EVEN")).toBe(2.674);
|
||||
expect(roundPrecision(-2.6735, 0.001, "HALF-EVEN")).toBe(-2.674);
|
||||
expect(roundPrecision(2.6745, 0.001, "HALF-EVEN")).toBe(2.674);
|
||||
expect(roundPrecision(-2.6745, 0.001, "HALF-EVEN")).toBe(-2.674);
|
||||
expect(roundPrecision(2.6744, 0.001, "HALF-EVEN")).toBe(2.674);
|
||||
expect(roundPrecision(-2.6744, 0.001, "HALF-EVEN")).toBe(-2.674);
|
||||
expect(roundPrecision(0.0004, 0.001, "HALF-EVEN")).toBe(0);
|
||||
expect(roundPrecision(-0.0004, 0.001, "HALF-EVEN")).toBe(-0);
|
||||
expect(roundPrecision(357.4555, 0.001, "HALF-EVEN")).toBe(357.456);
|
||||
expect(roundPrecision(-357.4555, 0.001, "HALF-EVEN")).toBe(-357.456);
|
||||
expect(roundPrecision(457.4554, 0.001, "HALF-EVEN")).toBe(457.455);
|
||||
expect(roundPrecision(-457.4554, 0.001, "HALF-EVEN")).toBe(-457.455);
|
||||
});
|
||||
|
||||
test("UP", () => {
|
||||
// We use 8.175 because when normalizing 8.175 with precision=0.001 it gives
|
||||
// us 8175,0000000001234 as value, and if not handle correctly the rounding UP
|
||||
// value will be incorrect (should be 8,175 and not 8,176)
|
||||
expect(roundPrecision(8.175, 0.001, "UP")).toBe(8.175);
|
||||
expect(roundPrecision(8.1751, 0.001, "UP")).toBe(8.176);
|
||||
expect(roundPrecision(-8.175, 0.001, "UP")).toBe(-8.175);
|
||||
expect(roundPrecision(-8.1751, 0.001, "UP")).toBe(-8.176);
|
||||
expect(roundPrecision(-6.0, 0.001, "UP")).toBe(-6);
|
||||
expect(roundPrecision(1.8, 1, "UP")).toBe(2);
|
||||
expect(roundPrecision(-1.8, 1, "UP")).toBe(-2);
|
||||
});
|
||||
});
|
||||
|
||||
test("roundDecimals", () => {
|
||||
expect(roundDecimals(1.0, 0)).toBe(1);
|
||||
expect(roundDecimals(1.0, 1)).toBe(1);
|
||||
expect(roundDecimals(1.0, 2)).toBe(1);
|
||||
expect(roundDecimals(1.0, 3)).toBe(1);
|
||||
expect(roundDecimals(1.0, 4)).toBe(1);
|
||||
expect(roundDecimals(1.0, 5)).toBe(1);
|
||||
expect(roundDecimals(1.0, 6)).toBe(1);
|
||||
expect(roundDecimals(1.0, 7)).toBe(1);
|
||||
expect(roundDecimals(1.0, 8)).toBe(1);
|
||||
expect(roundDecimals(0.5, 0)).toBe(1);
|
||||
expect(roundDecimals(-0.5, 0)).toBe(-1);
|
||||
expect(roundDecimals(2.6745, 3)).toBe(2.675);
|
||||
expect(roundDecimals(-2.6745, 3)).toBe(-2.675);
|
||||
expect(roundDecimals(2.6744, 3)).toBe(2.674);
|
||||
expect(roundDecimals(-2.6744, 3)).toBe(-2.674);
|
||||
expect(roundDecimals(0.0004, 3)).toBe(0);
|
||||
expect(roundDecimals(-0.0004, 3)).toBe(0);
|
||||
expect(roundDecimals(357.4555, 3)).toBe(357.456);
|
||||
expect(roundDecimals(-357.4555, 3)).toBe(-357.456);
|
||||
expect(roundDecimals(457.4554, 3)).toBe(457.455);
|
||||
expect(roundDecimals(-457.4554, 3)).toBe(-457.455);
|
||||
});
|
||||
|
||||
test("floatIsZero", () => {
|
||||
expect(floatIsZero(1, 0)).toBe(false);
|
||||
expect(floatIsZero(0.9999, 0)).toBe(false);
|
||||
expect(floatIsZero(0.50001, 0)).toBe(false);
|
||||
expect(floatIsZero(0.5, 0)).toBe(false);
|
||||
expect(floatIsZero(0.49999, 0)).toBe(true);
|
||||
expect(floatIsZero(0, 0)).toBe(true);
|
||||
expect(floatIsZero(-0.49999, 0)).toBe(true);
|
||||
expect(floatIsZero(-0.50001, 0)).toBe(false);
|
||||
expect(floatIsZero(-0.5, 0)).toBe(false);
|
||||
expect(floatIsZero(-0.9999, 0)).toBe(false);
|
||||
expect(floatIsZero(-1, 0)).toBe(false);
|
||||
|
||||
expect(floatIsZero(0.1, 1)).toBe(false);
|
||||
expect(floatIsZero(0.099999, 1)).toBe(false);
|
||||
expect(floatIsZero(0.050001, 1)).toBe(false);
|
||||
expect(floatIsZero(0.05, 1)).toBe(false);
|
||||
expect(floatIsZero(0.049999, 1)).toBe(true);
|
||||
expect(floatIsZero(0, 1)).toBe(true);
|
||||
expect(floatIsZero(-0.049999, 1)).toBe(true);
|
||||
expect(floatIsZero(-0.05, 1)).toBe(false);
|
||||
expect(floatIsZero(-0.050001, 1)).toBe(false);
|
||||
expect(floatIsZero(-0.099999, 1)).toBe(false);
|
||||
expect(floatIsZero(-0.1, 1)).toBe(false);
|
||||
|
||||
expect(floatIsZero(0.01, 2)).toBe(false);
|
||||
expect(floatIsZero(0.0099999, 2)).toBe(false);
|
||||
expect(floatIsZero(0.005, 2)).toBe(false);
|
||||
expect(floatIsZero(0.0050001, 2)).toBe(false);
|
||||
expect(floatIsZero(0.0049999, 2)).toBe(true);
|
||||
expect(floatIsZero(0, 2)).toBe(true);
|
||||
expect(floatIsZero(-0.0049999, 2)).toBe(true);
|
||||
expect(floatIsZero(-0.0050001, 2)).toBe(false);
|
||||
expect(floatIsZero(-0.005, 2)).toBe(false);
|
||||
expect(floatIsZero(-0.0099999, 2)).toBe(false);
|
||||
expect(floatIsZero(-0.01, 2)).toBe(false);
|
||||
|
||||
// 4 and 5 decimal places are mentioned as special cases in `roundDecimals` method.
|
||||
expect(floatIsZero(0.0001, 4)).toBe(false);
|
||||
expect(floatIsZero(0.000099999, 4)).toBe(false);
|
||||
expect(floatIsZero(0.00005, 4)).toBe(false);
|
||||
expect(floatIsZero(0.000050001, 4)).toBe(false);
|
||||
expect(floatIsZero(0.000049999, 4)).toBe(true);
|
||||
expect(floatIsZero(0, 4)).toBe(true);
|
||||
expect(floatIsZero(-0.000049999, 4)).toBe(true);
|
||||
expect(floatIsZero(-0.000050001, 4)).toBe(false);
|
||||
expect(floatIsZero(-0.00005, 4)).toBe(false);
|
||||
expect(floatIsZero(-0.000099999, 4)).toBe(false);
|
||||
expect(floatIsZero(-0.0001, 4)).toBe(false);
|
||||
|
||||
expect(floatIsZero(0.00001, 5)).toBe(false);
|
||||
expect(floatIsZero(0.0000099999, 5)).toBe(false);
|
||||
expect(floatIsZero(0.000005, 5)).toBe(false);
|
||||
expect(floatIsZero(0.0000050001, 5)).toBe(false);
|
||||
expect(floatIsZero(0.0000049999, 5)).toBe(true);
|
||||
expect(floatIsZero(0, 5)).toBe(true);
|
||||
expect(floatIsZero(-0.0000049999, 5)).toBe(true);
|
||||
expect(floatIsZero(-0.0000050001, 5)).toBe(false);
|
||||
expect(floatIsZero(-0.000005, 5)).toBe(false);
|
||||
expect(floatIsZero(-0.0000099999, 5)).toBe(false);
|
||||
expect(floatIsZero(-0.00001, 5)).toBe(false);
|
||||
|
||||
expect(floatIsZero(0.0000001, 7)).toBe(false);
|
||||
expect(floatIsZero(0.000000099999, 7)).toBe(false);
|
||||
expect(floatIsZero(0.00000005, 7)).toBe(false);
|
||||
expect(floatIsZero(0.000000050001, 7)).toBe(false);
|
||||
expect(floatIsZero(0.000000049999, 7)).toBe(true);
|
||||
expect(floatIsZero(0, 7)).toBe(true);
|
||||
expect(floatIsZero(-0.000000049999, 7)).toBe(true);
|
||||
expect(floatIsZero(-0.000000050001, 7)).toBe(false);
|
||||
expect(floatIsZero(-0.00000005, 7)).toBe(false);
|
||||
expect(floatIsZero(-0.000000099999, 7)).toBe(false);
|
||||
expect(floatIsZero(-0.0000001, 7)).toBe(false);
|
||||
});
|
||||
|
||||
describe("formatFloat", () => {
|
||||
test("localized", () => {
|
||||
patchWithCleanup(localization, {
|
||||
decimalPoint: ".",
|
||||
grouping: [3, 0],
|
||||
thousandsSep: ",",
|
||||
});
|
||||
expect(formatFloat(1000000)).toBe("1,000,000.00");
|
||||
|
||||
const options = { grouping: [3, 2, -1], decimalPoint: "?", thousandsSep: "€" };
|
||||
expect(formatFloat(106500, options)).toBe("1€06€500?00");
|
||||
|
||||
expect(formatFloat(1500, { thousandsSep: "" })).toBe("1500.00");
|
||||
expect(formatFloat(-1.01)).toBe("-1.01");
|
||||
expect(formatFloat(-0.01)).toBe("-0.01");
|
||||
|
||||
expect(formatFloat(38.0001, { trailingZeros: false })).toBe("38");
|
||||
expect(formatFloat(38.1, { trailingZeros: false })).toBe("38.1");
|
||||
expect(formatFloat(38.0001, { digits: [16, 0], trailingZeros: false })).toBe("38");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 3, 3, 3] });
|
||||
expect(formatFloat(1000000)).toBe("1,000,000.00");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [3, 2, -1] });
|
||||
expect(formatFloat(106500)).toBe("1,06,500.00");
|
||||
|
||||
patchWithCleanup(localization, { grouping: [1, 2, -1] });
|
||||
expect(formatFloat(106500)).toBe("106,50,0.00");
|
||||
|
||||
patchWithCleanup(localization, {
|
||||
decimalPoint: "!",
|
||||
grouping: [2, 0],
|
||||
thousandsSep: "@",
|
||||
});
|
||||
expect(formatFloat(6000)).toBe("60@00!00");
|
||||
});
|
||||
|
||||
test("humanReadable", () => {
|
||||
patchTranslations();
|
||||
patchWithCleanup(localization, {
|
||||
decimalPoint: ".",
|
||||
grouping: [3, 0],
|
||||
thousandsSep: ",",
|
||||
});
|
||||
|
||||
const options = { humanReadable: true };
|
||||
expect(formatFloat(1e18, options)).toBe("1E");
|
||||
expect(formatFloat(-1e18, options)).toBe("-1E");
|
||||
|
||||
Object.assign(options, { decimals: 2, minDigits: 1 });
|
||||
expect(formatFloat(1020, options)).toBe("1.02k");
|
||||
expect(formatFloat(1002, options)).toBe("1.00k");
|
||||
expect(formatFloat(101, options)).toBe("101.00");
|
||||
expect(formatFloat(64.2, options)).toBe("64.20");
|
||||
expect(formatFloat(1020, options)).toBe("1.02k");
|
||||
expect(formatFloat(1e21, options)).toBe("1e+21");
|
||||
expect(formatFloat(1.0045e22, options)).toBe("1e+22");
|
||||
expect(formatFloat(1.012e43, options)).toBe("1.01e+43");
|
||||
expect(formatFloat(-1020, options)).toBe("-1.02k");
|
||||
expect(formatFloat(-1020, options)).toBe("-1.02k");
|
||||
expect(formatFloat(-1002, options)).toBe("-1.00k");
|
||||
expect(formatFloat(-101, options)).toBe("-101.00");
|
||||
expect(formatFloat(-64.2, options)).toBe("-64.20");
|
||||
expect(formatFloat(-1e21, options)).toBe("-1e+21");
|
||||
expect(formatFloat(-1.0045e22, options)).toBe("-1e+22");
|
||||
expect(formatFloat(-1.012e43, options)).toBe("-1.01e+43");
|
||||
expect(formatFloat(-0.0000001, options)).toBe("0.00");
|
||||
|
||||
Object.assign(options, { decimals: 2, minDigits: 2 });
|
||||
expect(formatFloat(1020000, options)).toBe("1,020k");
|
||||
expect(formatFloat(10200000, options)).toBe("10.20M");
|
||||
expect(formatFloat(1.012e43, options)).toBe("1.01e+43");
|
||||
expect(formatFloat(-1020000, options)).toBe("-1,020k");
|
||||
expect(formatFloat(-10200000, options)).toBe("-10.20M");
|
||||
expect(formatFloat(-1.012e43, options)).toBe("-1.01e+43");
|
||||
|
||||
Object.assign(options, { decimals: 3, minDigits: 1 });
|
||||
expect(formatFloat(1.0045e22, options)).toBe("1.005e+22");
|
||||
expect(formatFloat(-1.0045e22, options)).toBe("-1.004e+22");
|
||||
|
||||
Object.assign(options, { humanReadable: false });
|
||||
expect(formatFloat(-0.0000001, options)).toBe("0.00");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
|
||||
import {
|
||||
deepCopy,
|
||||
deepEqual,
|
||||
isObject,
|
||||
omit,
|
||||
pick,
|
||||
shallowEqual,
|
||||
deepMerge,
|
||||
} from "@web/core/utils/objects";
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
describe("shallowEqual", () => {
|
||||
test("simple valid cases", () => {
|
||||
expect(shallowEqual({}, {})).toBe(true);
|
||||
expect(shallowEqual({ a: 1 }, { a: 1 })).toBe(true);
|
||||
expect(shallowEqual({ a: 1, b: "x" }, { b: "x", a: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
test("simple invalid cases", () => {
|
||||
expect(shallowEqual({ a: 1 }, { a: 2 })).toBe(false);
|
||||
expect(shallowEqual({}, { a: 2 })).toBe(false);
|
||||
expect(shallowEqual({ a: 1 }, {})).toBe(false);
|
||||
});
|
||||
|
||||
test("objects with non primitive values", () => {
|
||||
const obj = { x: "y" };
|
||||
expect(shallowEqual({ a: obj }, { a: obj })).toBe(true);
|
||||
expect(shallowEqual({ a: { x: "y" } }, { a: { x: "y" } })).toBe(false);
|
||||
|
||||
const arr = ["x", "y", "z"];
|
||||
expect(shallowEqual({ a: arr }, { a: arr })).toBe(true);
|
||||
expect(shallowEqual({ a: ["x", "y", "z"] }, { a: ["x", "y", "z"] })).toBe(false);
|
||||
|
||||
const fn = () => {};
|
||||
expect(shallowEqual({ a: fn }, { a: fn })).toBe(true);
|
||||
expect(shallowEqual({ a: () => {} }, { a: () => {} })).toBe(false);
|
||||
});
|
||||
|
||||
test("custom comparison function", () => {
|
||||
const dateA = new Date();
|
||||
const dateB = new Date(dateA);
|
||||
|
||||
expect(shallowEqual({ a: 1, date: dateA }, { a: 1, date: dateB })).toBe(false);
|
||||
expect(
|
||||
shallowEqual({ a: 1, date: dateA }, { a: 1, date: dateB }, (a, b) =>
|
||||
a instanceof Date ? Number(a) === Number(b) : a === b
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("deepEqual", () => {
|
||||
const obj1 = {
|
||||
a: ["a", "b", "c"],
|
||||
o: {
|
||||
b: true,
|
||||
n: 10,
|
||||
},
|
||||
};
|
||||
const obj2 = Object.assign({}, obj1);
|
||||
const obj3 = Object.assign({}, obj2, { some: "thing" });
|
||||
expect(deepEqual(obj1, obj2)).toBe(true);
|
||||
expect(deepEqual(obj1, obj3)).toBe(false);
|
||||
expect(deepEqual(obj2, obj3)).toBe(false);
|
||||
});
|
||||
|
||||
test("deepCopy", () => {
|
||||
const obj = {
|
||||
a: ["a", "b", "c"],
|
||||
o: {
|
||||
b: true,
|
||||
n: 10,
|
||||
},
|
||||
};
|
||||
const copy = deepCopy(obj);
|
||||
expect(copy).not.toBe(obj);
|
||||
expect(copy).toEqual(obj);
|
||||
expect(copy.a).not.toBe(obj.a);
|
||||
expect(copy.o).not.toBe(obj.o);
|
||||
|
||||
expect(deepCopy(new Date())).not.toBeInstanceOf(Date);
|
||||
expect(deepCopy(new Set(["a"]))).not.toBeInstanceOf(Set);
|
||||
expect(deepCopy(new Map([["a", 1]]))).not.toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
test("isObject", () => {
|
||||
expect(isObject(null)).toBe(false);
|
||||
expect(isObject(undefined)).toBe(false);
|
||||
|
||||
expect(isObject("a")).toBe(false);
|
||||
|
||||
expect(isObject(true)).toBe(false);
|
||||
expect(isObject(false)).toBe(false);
|
||||
|
||||
expect(isObject(10)).toBe(false);
|
||||
expect(isObject(10.01)).toBe(false);
|
||||
|
||||
expect(isObject([])).toBe(true);
|
||||
expect(isObject([1, 2])).toBe(true);
|
||||
|
||||
expect(isObject({})).toBe(true);
|
||||
expect(isObject({ a: 1 })).toBe(true);
|
||||
|
||||
expect(isObject(() => {})).toBe(true);
|
||||
expect(isObject(new Set())).toBe(true);
|
||||
expect(isObject(new Map())).toBe(true);
|
||||
expect(isObject(new Date())).toBe(true);
|
||||
});
|
||||
|
||||
test("omit", () => {
|
||||
expect(omit({})).toEqual({});
|
||||
expect(omit({}, "a")).toEqual({});
|
||||
expect(omit({ a: 1 })).toEqual({ a: 1 });
|
||||
expect(omit({ a: 1 }, "a")).toEqual({});
|
||||
expect(omit({ a: 1, b: 2 }, "c", "a")).toEqual({ b: 2 });
|
||||
expect(omit({ a: 1, b: 2 }, "b", "c")).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
test("pick", () => {
|
||||
expect(pick({})).toEqual({});
|
||||
expect(pick({}, "a")).toEqual({});
|
||||
expect(pick({ a: 3, b: "a", c: [] }, "a")).toEqual({ a: 3 });
|
||||
expect(pick({ a: 3, b: "a", c: [] }, "a", "c")).toEqual({ a: 3, c: [] });
|
||||
expect(pick({ a: 3, b: "a", c: [] }, "a", "b", "c")).toEqual({ a: 3, b: "a", c: [] });
|
||||
|
||||
// Non enumerable property
|
||||
class MyClass {
|
||||
get a() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
const myClass = new MyClass();
|
||||
Object.defineProperty(myClass, "b", { enumerable: false, value: 2 });
|
||||
expect(pick(myClass, "a", "b")).toEqual({ a: 1, b: 2 });
|
||||
});
|
||||
|
||||
test("deepMerge", () => {
|
||||
expect(
|
||||
deepMerge(
|
||||
{
|
||||
a: 1,
|
||||
b: {
|
||||
b_a: 1,
|
||||
b_b: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
a: 2,
|
||||
b: {
|
||||
b_b: 3,
|
||||
b_c: 4,
|
||||
},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
a: 2,
|
||||
b: {
|
||||
b_a: 1,
|
||||
b_b: 3,
|
||||
b_c: 4,
|
||||
},
|
||||
});
|
||||
|
||||
expect(deepMerge({}, {})).toEqual({});
|
||||
|
||||
expect(deepMerge({ a: 1 }, {})).toEqual({ a: 1 });
|
||||
expect(deepMerge({}, { a: 1 })).toEqual({ a: 1 });
|
||||
expect(deepMerge({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 });
|
||||
expect(deepMerge({ a: 1 }, { a: 2 })).toEqual({ a: 2 });
|
||||
|
||||
expect(deepMerge(undefined, { a: 1 })).toEqual({ a: 1 });
|
||||
expect(deepMerge({ a: 1 }, undefined)).toEqual({ a: 1 });
|
||||
expect(deepMerge(undefined, undefined)).toBe(undefined);
|
||||
expect(deepMerge({ a: undefined, b: undefined }, { a: { foo: "bar" } })).toEqual({
|
||||
a: { foo: "bar" },
|
||||
b: undefined,
|
||||
});
|
||||
|
||||
expect(deepMerge("foo", 1)).toBe(undefined);
|
||||
expect(deepMerge(null, null)).toBe(undefined);
|
||||
|
||||
const f = () => {};
|
||||
expect(deepMerge({ a: undefined }, { a: f })).toEqual({ a: f });
|
||||
|
||||
// There's no current use for arrays, support can be added if needed
|
||||
expect(deepMerge({ a: [1, 2, 3] }, { a: [4] })).toEqual({ a: [4] });
|
||||
|
||||
const symbolA = Symbol("A");
|
||||
const symbolB = Symbol("B");
|
||||
expect(
|
||||
deepMerge(
|
||||
{
|
||||
[symbolA]: 1,
|
||||
},
|
||||
{
|
||||
[symbolA]: 3,
|
||||
[symbolB]: 2,
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
[symbolA]: 3,
|
||||
[symbolB]: 2,
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,871 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
class BaseClass {
|
||||
static staticStr = "base";
|
||||
static staticObj = { base: "base" };
|
||||
static staticArr = ["base"];
|
||||
static staticFn() {
|
||||
expect.step("base.staticFn");
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.setup();
|
||||
}
|
||||
setup() {
|
||||
this._dynamic = "base";
|
||||
|
||||
this.str = "base";
|
||||
this.obj = { base: "base" };
|
||||
this.arr = ["base"];
|
||||
|
||||
expect.step("base.setup");
|
||||
}
|
||||
fn() {
|
||||
expect.step("base.fn");
|
||||
}
|
||||
async asyncFn() {
|
||||
// also check this binding
|
||||
expect.step(`base.${this.str}`);
|
||||
}
|
||||
get dynamic() {
|
||||
return this._dynamic;
|
||||
}
|
||||
set dynamic(value) {
|
||||
this._dynamic = value;
|
||||
}
|
||||
}
|
||||
|
||||
function applyGenericPatch(Klass, tag) {
|
||||
return patch(Klass.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
expect.step(`${tag}.setup`);
|
||||
},
|
||||
fn() {
|
||||
super.fn();
|
||||
expect.step(`${tag}.fn`);
|
||||
},
|
||||
async asyncFn() {
|
||||
await Promise.resolve();
|
||||
await super.asyncFn(...arguments);
|
||||
// also check this binding
|
||||
expect.step(`${tag}.${this.str}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function applyGenericStaticPatch(Klass, tag) {
|
||||
return patch(Klass, {
|
||||
staticStr: Klass.staticStr + tag,
|
||||
staticArr: [...Klass.staticArr, tag],
|
||||
staticObj: { ...Klass.staticObj, patch: tag },
|
||||
staticFn() {
|
||||
super.staticFn();
|
||||
expect.step(`${tag}.staticFn`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function createGenericExtension() {
|
||||
return class Extension extends BaseClass {
|
||||
static staticStr = BaseClass.staticStr + "extension";
|
||||
static staticArr = [...BaseClass.staticArr, "extension"];
|
||||
static staticObj = { ...BaseClass.staticObj, extension: "extension" };
|
||||
static staticFn() {
|
||||
super.staticFn();
|
||||
expect.step("extension.staticFn");
|
||||
}
|
||||
setup() {
|
||||
super.setup();
|
||||
expect.step("extension.setup");
|
||||
}
|
||||
fn() {
|
||||
super.fn();
|
||||
expect.step("extension.fn");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe.current.tags("headless");
|
||||
|
||||
test("one patch/unpatch", () => {
|
||||
new BaseClass().fn();
|
||||
expect.verifySteps(["base.setup", "base.fn"]);
|
||||
|
||||
const unpatch = applyGenericPatch(BaseClass, "patch");
|
||||
new BaseClass().fn();
|
||||
expect.verifySteps(["base.setup", "patch.setup", "base.fn", "patch.fn"]);
|
||||
|
||||
unpatch();
|
||||
new BaseClass().fn();
|
||||
expect.verifySteps(["base.setup", "base.fn"]);
|
||||
});
|
||||
|
||||
test("two patch/unpatch (unpatch 1 > 2)", () => {
|
||||
new BaseClass().fn();
|
||||
expect.verifySteps(["base.setup", "base.fn"]);
|
||||
|
||||
const unpatch1 = applyGenericPatch(BaseClass, "patch1");
|
||||
new BaseClass().fn();
|
||||
expect.verifySteps(["base.setup", "patch1.setup", "base.fn", "patch1.fn"]);
|
||||
|
||||
const unpatch2 = applyGenericPatch(BaseClass, "patch2");
|
||||
new BaseClass().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch1.setup",
|
||||
"patch2.setup",
|
||||
"base.fn",
|
||||
"patch1.fn",
|
||||
"patch2.fn",
|
||||
]);
|
||||
|
||||
unpatch1();
|
||||
new BaseClass().fn();
|
||||
expect.verifySteps(["base.setup", "patch2.setup", "base.fn", "patch2.fn"]);
|
||||
|
||||
unpatch2();
|
||||
new BaseClass().fn();
|
||||
expect.verifySteps(["base.setup", "base.fn"]);
|
||||
});
|
||||
|
||||
test("two patch/unpatch (unpatch 2 > 1)", () => {
|
||||
new BaseClass().fn();
|
||||
expect.verifySteps(["base.setup", "base.fn"]);
|
||||
|
||||
const unpatch1 = applyGenericPatch(BaseClass, "patch1");
|
||||
new BaseClass().fn();
|
||||
expect.verifySteps(["base.setup", "patch1.setup", "base.fn", "patch1.fn"]);
|
||||
|
||||
const unpatch2 = applyGenericPatch(BaseClass, "patch2");
|
||||
new BaseClass().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch1.setup",
|
||||
"patch2.setup",
|
||||
"base.fn",
|
||||
"patch1.fn",
|
||||
"patch2.fn",
|
||||
]);
|
||||
|
||||
unpatch2();
|
||||
new BaseClass().fn();
|
||||
expect.verifySteps(["base.setup", "patch1.setup", "base.fn", "patch1.fn"]);
|
||||
|
||||
unpatch1();
|
||||
new BaseClass().fn();
|
||||
expect.verifySteps(["base.setup", "base.fn"]);
|
||||
});
|
||||
|
||||
test("patch for specialization", () => {
|
||||
let args = [];
|
||||
class A {
|
||||
constructor() {
|
||||
this.setup(...arguments);
|
||||
}
|
||||
setup() {
|
||||
args = ["A", ...arguments];
|
||||
}
|
||||
}
|
||||
|
||||
const unpatch = patch(A.prototype, {
|
||||
setup() {
|
||||
super.setup("patch", ...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
new A("instantiation");
|
||||
expect(args).toEqual(["A", "patch", "instantiation"]);
|
||||
unpatch();
|
||||
});
|
||||
|
||||
test("instance fields", () => {
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.str += "patch";
|
||||
this.arr.push("patch");
|
||||
this.obj.patch = "patch";
|
||||
},
|
||||
});
|
||||
|
||||
const instance = new BaseClass();
|
||||
expect.verifySteps(["base.setup"]);
|
||||
expect(instance.str).toBe("basepatch");
|
||||
expect(instance.arr).toEqual(["base", "patch"]);
|
||||
expect(instance.obj).toEqual({ base: "base", patch: "patch" });
|
||||
|
||||
unpatch();
|
||||
// unpatch does not change instance fields' values
|
||||
expect(instance.str).toBe("basepatch");
|
||||
expect(instance.arr).toEqual(["base", "patch"]);
|
||||
expect(instance.obj).toEqual({ base: "base", patch: "patch" });
|
||||
});
|
||||
|
||||
test("call instance method defined in patch", () => {
|
||||
const instance = new BaseClass();
|
||||
expect.verifySteps(["base.setup"]);
|
||||
expect(instance).not.toInclude("f");
|
||||
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
f() {
|
||||
expect.step("patch.f");
|
||||
},
|
||||
});
|
||||
instance.f();
|
||||
expect(instance).toInclude("f");
|
||||
expect.verifySteps(["patch.f"]);
|
||||
|
||||
unpatch();
|
||||
expect(instance).not.toInclude("f");
|
||||
});
|
||||
|
||||
test("class methods", () => {
|
||||
BaseClass.staticFn();
|
||||
expect.verifySteps(["base.staticFn"]);
|
||||
|
||||
const unpatch = applyGenericStaticPatch(BaseClass, "patch");
|
||||
BaseClass.staticFn();
|
||||
expect.verifySteps(["base.staticFn", "patch.staticFn"]);
|
||||
|
||||
unpatch();
|
||||
BaseClass.staticFn();
|
||||
expect.verifySteps(["base.staticFn"]);
|
||||
});
|
||||
|
||||
test("class fields", () => {
|
||||
expect(BaseClass.staticStr).toBe("base");
|
||||
expect(BaseClass.staticArr).toEqual(["base"]);
|
||||
expect(BaseClass.staticObj).toEqual({ base: "base" });
|
||||
|
||||
const unpatch = applyGenericStaticPatch(BaseClass, "patch");
|
||||
expect(BaseClass.staticStr).toBe("basepatch");
|
||||
expect(BaseClass.staticArr).toEqual(["base", "patch"]);
|
||||
expect(BaseClass.staticObj).toEqual({ base: "base", patch: "patch" });
|
||||
|
||||
unpatch();
|
||||
expect(BaseClass.staticStr).toBe("base");
|
||||
expect(BaseClass.staticArr).toEqual(["base"]);
|
||||
expect(BaseClass.staticObj).toEqual({ base: "base" });
|
||||
});
|
||||
|
||||
test("lazy patch", () => {
|
||||
const instance = new BaseClass();
|
||||
const unpatch = applyGenericPatch(BaseClass, "patch");
|
||||
instance.fn();
|
||||
expect.verifySteps(["base.setup", "base.fn", "patch.fn"]);
|
||||
|
||||
unpatch();
|
||||
instance.fn();
|
||||
expect.verifySteps(["base.fn"]);
|
||||
});
|
||||
|
||||
test("getter", () => {
|
||||
const instance = new BaseClass();
|
||||
expect.verifySteps(["base.setup"]);
|
||||
expect(instance.dynamic).toBe("base");
|
||||
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
get dynamic() {
|
||||
return super.dynamic + "patch";
|
||||
},
|
||||
});
|
||||
expect(instance.dynamic).toBe("basepatch");
|
||||
|
||||
unpatch();
|
||||
expect(instance.dynamic).toBe("base");
|
||||
});
|
||||
|
||||
test("setter", () => {
|
||||
const instance = new BaseClass();
|
||||
expect.verifySteps(["base.setup"]);
|
||||
expect(instance.dynamic).toBe("base");
|
||||
instance.dynamic = "1";
|
||||
expect(instance.dynamic).toBe("1");
|
||||
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
set dynamic(value) {
|
||||
super.dynamic = "patch:" + value;
|
||||
},
|
||||
});
|
||||
expect(instance.dynamic).toBe("1"); // nothing changed
|
||||
|
||||
instance.dynamic = "2";
|
||||
expect(instance.dynamic).toBe("patch:2");
|
||||
|
||||
unpatch();
|
||||
instance.dynamic = "3";
|
||||
expect(instance.dynamic).toBe("3");
|
||||
});
|
||||
|
||||
test("patch getter/setter with value", () => {
|
||||
const originalDescriptor = Object.getOwnPropertyDescriptor(BaseClass.prototype, "dynamic");
|
||||
|
||||
const unpatch = patch(BaseClass.prototype, { dynamic: "patched" });
|
||||
const instance = new BaseClass();
|
||||
expect.verifySteps(["base.setup"]);
|
||||
expect(Object.getOwnPropertyDescriptor(BaseClass.prototype, "dynamic")).toEqual({
|
||||
value: "patched",
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: false, // class properties are not enumerable
|
||||
});
|
||||
expect(instance.dynamic).toBe("patched");
|
||||
|
||||
unpatch();
|
||||
instance.dynamic = "base";
|
||||
expect(Object.getOwnPropertyDescriptor(BaseClass.prototype, "dynamic")).toEqual(
|
||||
originalDescriptor
|
||||
);
|
||||
expect(instance.dynamic).toBe("base");
|
||||
});
|
||||
|
||||
test("async function", async () => {
|
||||
const instance = new BaseClass();
|
||||
instance.str = "async1";
|
||||
await instance.asyncFn();
|
||||
expect.verifySteps(["base.setup", "base.async1"]);
|
||||
|
||||
const unpatch = applyGenericPatch(BaseClass, "patch");
|
||||
instance.str = "async2";
|
||||
await instance.asyncFn();
|
||||
expect.verifySteps(["base.async2", "patch.async2"]);
|
||||
|
||||
unpatch();
|
||||
instance.str = "async3";
|
||||
await instance.asyncFn();
|
||||
expect.verifySteps(["base.async3"]);
|
||||
});
|
||||
|
||||
test("async function (multiple patches)", async () => {
|
||||
const instance = new BaseClass();
|
||||
instance.str = "async1";
|
||||
await instance.asyncFn();
|
||||
expect.verifySteps(["base.setup", "base.async1"]);
|
||||
|
||||
const unpatch1 = applyGenericPatch(BaseClass, "patch1");
|
||||
const unpatch2 = applyGenericPatch(BaseClass, "patch2");
|
||||
instance.str = "async2";
|
||||
await instance.asyncFn();
|
||||
expect.verifySteps(["base.async2", "patch1.async2", "patch2.async2"]);
|
||||
|
||||
unpatch1();
|
||||
unpatch2();
|
||||
instance.str = "async3";
|
||||
await instance.asyncFn();
|
||||
expect.verifySteps(["base.async3"]);
|
||||
});
|
||||
|
||||
test("call another super method", () => {
|
||||
new BaseClass();
|
||||
expect.verifySteps(["base.setup"]);
|
||||
|
||||
const unpatch = patch(BaseClass.prototype, {
|
||||
setup() {
|
||||
expect.step("patch.setup");
|
||||
super.fn();
|
||||
},
|
||||
fn() {
|
||||
expect.step("patch.fn"); // should not called
|
||||
},
|
||||
});
|
||||
|
||||
new BaseClass();
|
||||
expect.verifySteps(["patch.setup", "base.fn"]);
|
||||
|
||||
unpatch();
|
||||
new BaseClass();
|
||||
expect.verifySteps(["base.setup"]);
|
||||
});
|
||||
|
||||
describe("inheritance", () => {
|
||||
test("extend > patch base > unpatch base", () => {
|
||||
const Extension = createGenericExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
|
||||
const unpatch = applyGenericPatch(BaseClass, "patch");
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
]);
|
||||
|
||||
unpatch();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
});
|
||||
|
||||
test("patch base > extend > unpatch base", () => {
|
||||
const unpatch = applyGenericPatch(BaseClass, "patch");
|
||||
const Extension = createGenericExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
]);
|
||||
|
||||
unpatch();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
});
|
||||
|
||||
test("extend > patch extension > unpatch extension", () => {
|
||||
const Extension = createGenericExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
|
||||
const unpatch = applyGenericPatch(Extension, "patch.extension");
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"extension.setup",
|
||||
"patch.extension.setup",
|
||||
"base.fn",
|
||||
"extension.fn",
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatch();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
});
|
||||
|
||||
test("extend > patch base > patch extension > unpatch base > unpatch extension", () => {
|
||||
const Extension = createGenericExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
|
||||
const unpatchBase = applyGenericPatch(BaseClass, "patch");
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
]);
|
||||
|
||||
const unpatchExtension = applyGenericPatch(Extension, "patch.extension");
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"patch.extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatchBase();
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"extension.setup",
|
||||
"patch.extension.setup",
|
||||
"base.fn",
|
||||
"extension.fn",
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatchExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
});
|
||||
|
||||
test("extend > patch base > patch extension > unpatch extension > unpatch base", () => {
|
||||
const Extension = createGenericExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
|
||||
const unpatchBase = applyGenericPatch(BaseClass, "patch");
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
]);
|
||||
|
||||
const unpatchExtension = applyGenericPatch(Extension, "patch.extension");
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"patch.extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatchExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
]);
|
||||
|
||||
unpatchBase();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
});
|
||||
|
||||
test("extend > patch extension > patch base > unpatch base > unpatch extension", () => {
|
||||
const Extension = createGenericExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
|
||||
const unpatchExtension = applyGenericPatch(Extension, "patch.extension");
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"extension.setup",
|
||||
"patch.extension.setup",
|
||||
"base.fn",
|
||||
"extension.fn",
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
const unpatchBase = applyGenericPatch(BaseClass, "patch");
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"patch.extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatchBase();
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"extension.setup",
|
||||
"patch.extension.setup",
|
||||
"base.fn",
|
||||
"extension.fn",
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatchExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
});
|
||||
|
||||
test("extend > patch extension > patch base > unpatch extension > unpatch base", () => {
|
||||
const Extension = createGenericExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
|
||||
const unpatchExtension = applyGenericPatch(Extension, "patch.extension");
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"extension.setup",
|
||||
"patch.extension.setup",
|
||||
"base.fn",
|
||||
"extension.fn",
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
const unpatchBase = applyGenericPatch(BaseClass, "patch");
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"patch.extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatchExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
]);
|
||||
|
||||
unpatchBase();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
});
|
||||
|
||||
test("patch base > extend > patch extension > unpatch base > unpatch extension", () => {
|
||||
const unpatchBase = applyGenericPatch(BaseClass, "patch");
|
||||
const Extension = createGenericExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
]);
|
||||
|
||||
const unpatchExtension = applyGenericPatch(Extension, "patch.extension");
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"patch.extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatchBase();
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"extension.setup",
|
||||
"patch.extension.setup",
|
||||
"base.fn",
|
||||
"extension.fn",
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatchExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
});
|
||||
|
||||
test("patch base > extend > patch extension > unpatch extension > unpatch base", () => {
|
||||
const unpatchBase = applyGenericPatch(BaseClass, "patch");
|
||||
const Extension = createGenericExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
]);
|
||||
|
||||
const unpatchExtension = applyGenericPatch(Extension, "patch.extension");
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"patch.extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
"patch.extension.fn",
|
||||
]);
|
||||
|
||||
unpatchExtension();
|
||||
new Extension().fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"patch.setup",
|
||||
"extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
]);
|
||||
|
||||
unpatchBase();
|
||||
new Extension().fn();
|
||||
expect.verifySteps(["base.setup", "extension.setup", "base.fn", "extension.fn"]);
|
||||
});
|
||||
|
||||
test("class methods", () => {
|
||||
const Extension = createGenericExtension();
|
||||
Extension.staticFn();
|
||||
expect.verifySteps(["base.staticFn", "extension.staticFn"]);
|
||||
|
||||
const unpatchBase = applyGenericStaticPatch(BaseClass, "patch");
|
||||
Extension.staticFn();
|
||||
expect.verifySteps(["base.staticFn", "patch.staticFn", "extension.staticFn"]);
|
||||
|
||||
const unpatchExtension = applyGenericStaticPatch(Extension, "patch.extension");
|
||||
Extension.staticFn();
|
||||
expect.verifySteps([
|
||||
"base.staticFn",
|
||||
"patch.staticFn",
|
||||
"extension.staticFn",
|
||||
"patch.extension.staticFn",
|
||||
]);
|
||||
|
||||
unpatchBase();
|
||||
Extension.staticFn();
|
||||
expect.verifySteps(["base.staticFn", "extension.staticFn", "patch.extension.staticFn"]);
|
||||
|
||||
unpatchExtension();
|
||||
Extension.staticFn();
|
||||
expect.verifySteps(["base.staticFn", "extension.staticFn"]);
|
||||
});
|
||||
|
||||
test("class fields (patch before inherit)", () => {
|
||||
const unpatch = applyGenericStaticPatch(BaseClass, "patch");
|
||||
const Extension = createGenericExtension();
|
||||
expect(Extension.staticStr).toBe("basepatchextension");
|
||||
expect(Extension.staticArr).toEqual(["base", "patch", "extension"]);
|
||||
expect(Extension.staticObj).toEqual({
|
||||
base: "base",
|
||||
patch: "patch",
|
||||
extension: "extension",
|
||||
});
|
||||
|
||||
// /!\ WARNING /!\
|
||||
// If inherit comes after the patch then extension will still have
|
||||
// the patched data when unpatching.
|
||||
unpatch();
|
||||
expect(Extension.staticStr).toBe("basepatchextension");
|
||||
expect(Extension.staticArr).toEqual(["base", "patch", "extension"]);
|
||||
expect(Extension.staticObj).toEqual({
|
||||
base: "base",
|
||||
patch: "patch",
|
||||
extension: "extension",
|
||||
});
|
||||
});
|
||||
|
||||
test("class fields (inherit before patch)", () => {
|
||||
const Extension = createGenericExtension();
|
||||
expect(Extension.staticStr).toBe("baseextension");
|
||||
expect(Extension.staticArr).toEqual(["base", "extension"]);
|
||||
expect(Extension.staticObj).toEqual({ base: "base", extension: "extension" });
|
||||
|
||||
// /!\ WARNING /!\
|
||||
// If patch comes after the inherit then extension won't have
|
||||
// the patched data.
|
||||
const unpatch = applyGenericStaticPatch(BaseClass, "patch");
|
||||
expect(Extension.staticStr).toBe("baseextension");
|
||||
expect(Extension.staticArr).toEqual(["base", "extension"]);
|
||||
expect(Extension.staticObj).toEqual({ base: "base", extension: "extension" });
|
||||
|
||||
unpatch();
|
||||
expect(Extension.staticStr).toBe("baseextension");
|
||||
expect(Extension.staticArr).toEqual(["base", "extension"]);
|
||||
expect(Extension.staticObj).toEqual({ base: "base", extension: "extension" });
|
||||
});
|
||||
|
||||
test("lazy patch", () => {
|
||||
const Extension = createGenericExtension();
|
||||
const instance = new Extension();
|
||||
const unpatch = applyGenericPatch(BaseClass, "patch");
|
||||
|
||||
instance.fn();
|
||||
expect.verifySteps([
|
||||
"base.setup",
|
||||
"extension.setup",
|
||||
"base.fn",
|
||||
"patch.fn",
|
||||
"extension.fn",
|
||||
]);
|
||||
|
||||
unpatch();
|
||||
instance.fn();
|
||||
expect.verifySteps(["base.fn", "extension.fn"]);
|
||||
});
|
||||
|
||||
test("keep original descriptor details", () => {
|
||||
class Klass {
|
||||
// getter declared in classes are not enumerable
|
||||
get getter() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let descriptor = Object.getOwnPropertyDescriptor(Klass.prototype, "getter");
|
||||
const getterFn = descriptor.get;
|
||||
expect(descriptor.configurable).toBe(true);
|
||||
expect(descriptor.enumerable).toBe(false);
|
||||
|
||||
patch(Klass.prototype, {
|
||||
// getter declared in object are enumerable
|
||||
get getter() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
descriptor = Object.getOwnPropertyDescriptor(Klass.prototype, "getter");
|
||||
expect(descriptor.configurable).toBe(true);
|
||||
expect(descriptor.enumerable).toBe(false);
|
||||
expect(getterFn).not.toBe(descriptor.get);
|
||||
});
|
||||
});
|
||||
|
||||
describe("other", () => {
|
||||
test("patch an object", () => {
|
||||
const obj = {
|
||||
var: "obj",
|
||||
fn() {
|
||||
expect.step("obj");
|
||||
},
|
||||
};
|
||||
|
||||
const unpatch = patch(obj, {
|
||||
var: obj.var + "patch",
|
||||
fn() {
|
||||
super.fn();
|
||||
expect.step("patch");
|
||||
},
|
||||
});
|
||||
expect(obj.var).toBe("objpatch");
|
||||
|
||||
obj.fn();
|
||||
expect.verifySteps(["obj", "patch"]);
|
||||
|
||||
unpatch();
|
||||
expect(obj.var).toBe("obj");
|
||||
|
||||
obj.fn();
|
||||
expect.verifySteps(["obj"]);
|
||||
});
|
||||
|
||||
test("can call a non bound patched method", () => {
|
||||
// use case: patching a function on window (e.g. setTimeout)
|
||||
|
||||
const obj = {
|
||||
fn() {
|
||||
expect.step("original");
|
||||
},
|
||||
};
|
||||
|
||||
const originalFn = obj.fn;
|
||||
patch(obj, {
|
||||
fn() {
|
||||
expect.step("patched");
|
||||
originalFn();
|
||||
},
|
||||
});
|
||||
|
||||
const fn = obj.fn; // purposely not bound
|
||||
fn();
|
||||
expect.verifySteps(["patched", "original"]);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue