mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-19 09:11:59 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
|
|
@ -0,0 +1,973 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ActionSwiper } from "@web/core/action_swiper/action_swiper";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
|
||||
import {
|
||||
mount,
|
||||
nextTick,
|
||||
triggerEvent,
|
||||
getFixture,
|
||||
mockTimeout,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { swipeRight } from "@web/../tests/mobile/helpers";
|
||||
import { Deferred } from "@web/core/utils/concurrency";
|
||||
|
||||
import { Component, xml, onPatched } from "@odoo/owl";
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let env;
|
||||
let target;
|
||||
|
||||
QUnit.module("ActionSwiper", ({ beforeEach }) => {
|
||||
beforeEach(async () => {
|
||||
env = await makeTestEnv();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
// Tests marked as [REQUIRE TOUCHEVENT] will fail on browsers that don't support
|
||||
// TouchEvent by default. It might be an option to activate on some browser.
|
||||
|
||||
QUnit.test("render only its target if no props is given", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { ActionSwiper };
|
||||
Parent.template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper>
|
||||
<div class="target-component"/>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
await mount(Parent, target, { env });
|
||||
assert.containsNone(target, "div.o_actionswiper");
|
||||
assert.containsOnce(target, "div.target-component");
|
||||
});
|
||||
|
||||
QUnit.test("only render the necessary divs", async (assert) => {
|
||||
await mount(ActionSwiper, target, {
|
||||
env,
|
||||
props: {
|
||||
onRightSwipe: {
|
||||
action: () => {},
|
||||
icon: "fa-circle",
|
||||
bgColor: "bg-warning",
|
||||
},
|
||||
slots: {},
|
||||
},
|
||||
});
|
||||
assert.containsOnce(target, "div.o_actionswiper_right_swipe_area");
|
||||
assert.containsNone(target, "div.o_actionswiper_left_swipe_area");
|
||||
await mount(ActionSwiper, target, {
|
||||
env,
|
||||
props: {
|
||||
onLeftSwipe: {
|
||||
action: () => {},
|
||||
icon: "fa-circle",
|
||||
bgColor: "bg-warning",
|
||||
},
|
||||
slots: {},
|
||||
},
|
||||
});
|
||||
assert.containsOnce(target, "div.o_actionswiper_right_swipe_area");
|
||||
assert.containsOnce(target, "div.o_actionswiper_left_swipe_area");
|
||||
});
|
||||
|
||||
QUnit.test("render with the height of its content", async (assert) => {
|
||||
assert.expect(2);
|
||||
class Parent extends Component {
|
||||
onRightSwipe() {
|
||||
assert.step("onRightSwipe");
|
||||
}
|
||||
}
|
||||
Parent.components = { ActionSwiper };
|
||||
Parent.template = xml`
|
||||
<div class="o-container d-flex" style="width: 200px; height: 200px; overflow: auto">
|
||||
<ActionSwiper onRightSwipe = "{
|
||||
action: 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>
|
||||
`;
|
||||
await mount(Parent, target, { env });
|
||||
assert.ok(
|
||||
target.querySelector(".o_actionswiper").scrollHeight ===
|
||||
target.querySelector(".target-component").scrollHeight,
|
||||
"the swiper has the height of its content"
|
||||
);
|
||||
assert.ok(
|
||||
target.querySelector(".o_actionswiper").scrollHeight >
|
||||
target.querySelector(".o_actionswiper").clientHeight,
|
||||
"the height of the swiper must make the parent div scrollable"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"can perform actions by swiping to the right [REQUIRE TOUCHEVENT]",
|
||||
async (assert) => {
|
||||
assert.expect(5);
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
class Parent extends Component {
|
||||
onRightSwipe() {
|
||||
assert.step("onRightSwipe");
|
||||
}
|
||||
}
|
||||
Parent.components = { ActionSwiper };
|
||||
Parent.template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper onRightSwipe = "{
|
||||
action: onRightSwipe,
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning'
|
||||
}">
|
||||
<div class="target-component" style="width: 200px; height: 80px">Test</div>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
await mount(Parent, target, { env });
|
||||
const swiper = target.querySelector(".o_actionswiper");
|
||||
const targetContainer = target.querySelector(".o_actionswiper_target_container");
|
||||
await triggerEvent(target, ".o_actionswiper", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: (3 * swiper.clientWidth) / 4,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(
|
||||
targetContainer.style.transform.includes("translateX"),
|
||||
"target has translateX"
|
||||
);
|
||||
// Touch ends before the half of the distance has been reached
|
||||
await triggerEvent(target, ".o_actionswiper", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: swiper.clientWidth / 2 - 1,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchend", {});
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
assert.ok(
|
||||
!targetContainer.style.transform.includes("translateX"),
|
||||
"target does not have a translate value"
|
||||
);
|
||||
// Touch ends once the half of the distance has been crossed
|
||||
await triggerEvent(target, ".o_actionswiper", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: swiper.clientWidth / 2,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: swiper.clientWidth + 1,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchend", {});
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
// The action is performed AND the component is reset
|
||||
assert.ok(
|
||||
!targetContainer.style.transform.includes("translateX"),
|
||||
"target doesn't have translateX after action is performed"
|
||||
);
|
||||
assert.verifySteps(["onRightSwipe"]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"can perform actions by swiping in both directions [REQUIRE TOUCHEVENT]",
|
||||
async (assert) => {
|
||||
assert.expect(7);
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
class Parent extends Component {
|
||||
onRightSwipe() {
|
||||
assert.step("onRightSwipe");
|
||||
}
|
||||
onLeftSwipe() {
|
||||
assert.step("onLeftSwipe");
|
||||
}
|
||||
}
|
||||
Parent.components = { ActionSwiper };
|
||||
Parent.template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper
|
||||
onRightSwipe = "{
|
||||
action: onRightSwipe,
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning'
|
||||
}"
|
||||
onLeftSwipe = "{
|
||||
action: onLeftSwipe,
|
||||
icon: 'fa-check',
|
||||
bgColor: 'bg-success'
|
||||
}">
|
||||
<div class="target-component" style="width: 250px; height: 80px">Swipe in both directions</div>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
await mount(Parent, target, { env });
|
||||
const swiper = target.querySelector(".o_actionswiper");
|
||||
const targetContainer = target.querySelector(".o_actionswiper_target_container");
|
||||
await triggerEvent(target, ".o_actionswiper", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: (3 * swiper.clientWidth) / 4,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(
|
||||
targetContainer.style.transform.includes("translateX"),
|
||||
"target has translateX"
|
||||
);
|
||||
// Touch ends before the half of the distance has been reached to the left
|
||||
await triggerEvent(target, ".o_actionswiper", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: -swiper.clientWidth / 2 + 1,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchend", {});
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
assert.ok(
|
||||
!targetContainer.style.transform.includes("translateX"),
|
||||
"target does not have a translate value"
|
||||
);
|
||||
// Touch ends once the half of the distance has been crossed to the left
|
||||
await triggerEvent(target, ".o_actionswiper", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: swiper.clientWidth / 2,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: -swiper.clientWidth - 1,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchend", {});
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
assert.verifySteps(["onLeftSwipe"], "the onLeftSwipe props action has been performed");
|
||||
// Touch ends once the half of the distance has been crossed to the right
|
||||
await triggerEvent(target, ".o_actionswiper", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: swiper.clientWidth / 2,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: swiper.clientWidth + 1,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchend", {});
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
assert.ok(
|
||||
!targetContainer.style.transform.includes("translateX"),
|
||||
"target doesn't have translateX after all actions are performed"
|
||||
);
|
||||
assert.verifySteps(
|
||||
["onRightSwipe"],
|
||||
"the onRightSwipe props action has been performed"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"invert the direction of swipes when language is rtl [REQUIRE TOUCHEVENT]",
|
||||
async (assert) => {
|
||||
assert.expect(7);
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
class Parent extends Component {
|
||||
onRightSwipe() {
|
||||
assert.step("onRightSwipe");
|
||||
}
|
||||
onLeftSwipe() {
|
||||
assert.step("onLeftSwipe");
|
||||
}
|
||||
}
|
||||
Parent.components = { ActionSwiper };
|
||||
Parent.template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper
|
||||
onRightSwipe = "{
|
||||
action: onRightSwipe,
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning'
|
||||
}"
|
||||
onLeftSwipe = "{
|
||||
action: onLeftSwipe,
|
||||
icon: 'fa-check',
|
||||
bgColor: 'bg-success'
|
||||
}">
|
||||
<div class="target-component" style="width: 250px; height: 80px">Swipe in both directions</div>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService({ direction: "rtl" }));
|
||||
await mount(Parent, target, { env });
|
||||
const swiper = target.querySelector(".o_actionswiper");
|
||||
const targetContainer = target.querySelector(".o_actionswiper_target_container");
|
||||
await triggerEvent(target, ".o_actionswiper", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: (3 * swiper.clientWidth) / 4,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(
|
||||
targetContainer.style.transform.includes("translateX"),
|
||||
"target has translateX"
|
||||
);
|
||||
// Touch ends before the half of the distance has been reached to the left
|
||||
await triggerEvent(target, ".o_actionswiper", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: -swiper.clientWidth / 2 + 1,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchend", {});
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
assert.ok(
|
||||
!targetContainer.style.transform.includes("translateX"),
|
||||
"target does not have a translate value"
|
||||
);
|
||||
// Touch ends once the half of the distance has been crossed to the left
|
||||
await triggerEvent(target, ".o_actionswiper", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: swiper.clientWidth / 2,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: -swiper.clientWidth - 1,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchend", {});
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
// In rtl languages, actions are permuted
|
||||
assert.verifySteps(
|
||||
["onRightSwipe"],
|
||||
"the onRightSwipe props action has been performed"
|
||||
);
|
||||
await triggerEvent(target, ".o_actionswiper", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: swiper.clientWidth / 2,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: swiper.clientWidth + 1,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchend", {});
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
assert.ok(
|
||||
!targetContainer.style.transform.includes("translateX"),
|
||||
"target doesn't have translateX after all actions are performed"
|
||||
);
|
||||
// In rtl languages, actions are permuted
|
||||
assert.verifySteps(["onLeftSwipe"], "the onLeftSwipe props action has been performed");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"swiping when the swiper contains scrollable areas [REQUIRE TOUCHEVENT]",
|
||||
async (assert) => {
|
||||
assert.expect(9);
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
class Parent extends Component {
|
||||
onRightSwipe() {
|
||||
assert.step("onRightSwipe");
|
||||
}
|
||||
onLeftSwipe() {
|
||||
assert.step("onLeftSwipe");
|
||||
}
|
||||
}
|
||||
Parent.components = { ActionSwiper };
|
||||
Parent.template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper
|
||||
onRightSwipe = "{
|
||||
action: onRightSwipe,
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning'
|
||||
}"
|
||||
onLeftSwipe = "{
|
||||
action: 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" style="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>
|
||||
`;
|
||||
await mount(Parent, target, { env });
|
||||
const swiper = target.querySelector(".o_actionswiper");
|
||||
const targetContainer = target.querySelector(".o_actionswiper_target_container");
|
||||
const scrollable = target.querySelector(".large-content");
|
||||
// The scrollable element is set as scrollable
|
||||
scrollable.scrollLeft = 100;
|
||||
await triggerEvent(target, ".o_actionswiper", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: (3 * swiper.clientWidth) / 4,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(
|
||||
targetContainer.style.transform.includes("translateX"),
|
||||
"the swiper can swipe if the scrollable area is not under touch pressure"
|
||||
);
|
||||
await triggerEvent(target, ".o_actionswiper", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchend", {});
|
||||
await triggerEvent(scrollable, ".large-text", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(scrollable, ".large-text", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(
|
||||
!targetContainer.style.transform.includes("translateX"),
|
||||
"the swiper has not swiped to the right because the scrollable element was scrollable to the left"
|
||||
);
|
||||
// The scrollable element is set at its left limit
|
||||
scrollable.scrollLeft = 0;
|
||||
await triggerEvent(scrollable, ".large-text", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(scrollable, ".large-text", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(
|
||||
targetContainer.style.transform.includes("translateX"),
|
||||
"the swiper has swiped to the right because the scrollable element couldn't scroll anymore to the left"
|
||||
);
|
||||
await triggerEvent(target, ".o_actionswiper", "touchend", {});
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
["onRightSwipe"],
|
||||
"the onRightSwipe props action has been performed"
|
||||
);
|
||||
await triggerEvent(scrollable, ".large-text", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(scrollable, ".large-text", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(
|
||||
!targetContainer.style.transform.includes("translateX"),
|
||||
"the swiper has not swiped to the left because the scrollable element was scrollable to the right"
|
||||
);
|
||||
// The scrollable element is set at its right limit
|
||||
scrollable.scrollLeft =
|
||||
scrollable.scrollWidth - scrollable.getBoundingClientRect().right;
|
||||
await triggerEvent(scrollable, ".large-text", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(scrollable, ".large-text", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(
|
||||
targetContainer.style.transform.includes("translateX"),
|
||||
"the swiper has swiped to the left because the scrollable element couldn't scroll anymore to the right"
|
||||
);
|
||||
await triggerEvent(target, ".o_actionswiper", "touchend", {});
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
assert.verifySteps(["onLeftSwipe"], "the onLeftSwipe props action has been performed");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"preventing swipe on scrollable areas when language is rtl [REQUIRE TOUCHEVENT]",
|
||||
async (assert) => {
|
||||
assert.expect(8);
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
class Parent extends Component {
|
||||
onRightSwipe() {
|
||||
assert.step("onRightSwipe");
|
||||
}
|
||||
onLeftSwipe() {
|
||||
assert.step("onLeftSwipe");
|
||||
}
|
||||
}
|
||||
Parent.components = { ActionSwiper };
|
||||
Parent.template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper
|
||||
onRightSwipe = "{
|
||||
action: onRightSwipe,
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning'
|
||||
}"
|
||||
onLeftSwipe = "{
|
||||
action: 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" style="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>
|
||||
`;
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService({ direction: "rtl" }));
|
||||
await mount(Parent, target, { env });
|
||||
const targetContainer = target.querySelector(".o_actionswiper_target_container");
|
||||
const scrollable = target.querySelector(".large-content");
|
||||
// The scrollable element is set as scrollable
|
||||
scrollable.scrollLeft = 100;
|
||||
await triggerEvent(scrollable, ".large-text", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(scrollable, ".large-text", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(
|
||||
!targetContainer.style.transform.includes("translateX"),
|
||||
"the swiper has not swiped to the right because the scrollable element was scrollable to the left"
|
||||
);
|
||||
// The scrollable element is set at its left limit
|
||||
scrollable.scrollLeft = 0;
|
||||
await triggerEvent(scrollable, ".large-text", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(scrollable, ".large-text", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(
|
||||
targetContainer.style.transform.includes("translateX"),
|
||||
"the swiper has swiped to the right because the scrollable element couldn't scroll anymore to the left"
|
||||
);
|
||||
await triggerEvent(target, ".o_actionswiper", "touchend", {});
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
// In rtl languages, actions are permuted
|
||||
assert.verifySteps(["onLeftSwipe"], "the onLeftSwipe props action has been performed");
|
||||
await triggerEvent(scrollable, ".large-text", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(scrollable, ".large-text", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(
|
||||
!targetContainer.style.transform.includes("translateX"),
|
||||
"the swiper has not swiped to the left because the scrollable element was scrollable to the right"
|
||||
);
|
||||
// The scrollable element is set at its right limit
|
||||
scrollable.scrollLeft =
|
||||
scrollable.scrollWidth - scrollable.getBoundingClientRect().right;
|
||||
await triggerEvent(scrollable, ".large-text", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientWidth,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(scrollable, ".large-text", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: scrollable.clientLeft,
|
||||
clientY:
|
||||
scrollable.getBoundingClientRect().top +
|
||||
scrollable.getBoundingClientRect().height / 2,
|
||||
target: scrollable.querySelector(".large-text"),
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.ok(
|
||||
targetContainer.style.transform.includes("translateX"),
|
||||
"the swiper has swiped to the left because the scrollable element couldn't scroll anymore to the right"
|
||||
);
|
||||
await triggerEvent(target, ".o_actionswiper", "touchend", {});
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
// In rtl languages, actions are permuted
|
||||
assert.verifySteps(
|
||||
["onRightSwipe"],
|
||||
"the onRightSwipe props action has been performed"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("swipeInvalid prop prevents swiping", async (assert) => {
|
||||
assert.expect(3);
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
class Parent extends Component {
|
||||
onRightSwipe() {
|
||||
assert.step("onRightSwipe");
|
||||
}
|
||||
swipeInvalid() {
|
||||
assert.step("swipeInvalid");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Parent.components = { ActionSwiper };
|
||||
Parent.template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper onRightSwipe = "{
|
||||
action: onRightSwipe,
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning',
|
||||
}" swipeInvalid = "swipeInvalid">
|
||||
<div class="target-component" style="width: 200px; height: 80px">Test</div>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
await mount(Parent, target, { env });
|
||||
const swiper = target.querySelector(".o_actionswiper");
|
||||
const targetContainer = target.querySelector(".o_actionswiper_target_container");
|
||||
// Touch ends once the half of the distance has been crossed
|
||||
await triggerEvent(target, ".o_actionswiper", "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: swiper.clientWidth / 2,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: swiper.clientWidth + 1,
|
||||
clientY: 0,
|
||||
target: target,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, ".o_actionswiper", "touchend", {});
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
assert.ok(
|
||||
!targetContainer.style.transform.includes("translateX"),
|
||||
"target doesn't have translateX after action is performed"
|
||||
);
|
||||
assert.verifySteps(["swipeInvalid"]);
|
||||
});
|
||||
|
||||
QUnit.test("action should be done before a new render", async (assert) => {
|
||||
let executingAction = false;
|
||||
const prom = new Deferred();
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
patchWithCleanup(ActionSwiper.prototype, {
|
||||
setup() {
|
||||
this._super(...arguments);
|
||||
onPatched(() => {
|
||||
if (executingAction) {
|
||||
assert.step("ActionSwiper patched");
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
class Parent extends Component {
|
||||
async onRightSwipe() {
|
||||
await nextTick();
|
||||
assert.step("action done");
|
||||
prom.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
Parent.props = [];
|
||||
Parent.components = { ActionSwiper };
|
||||
Parent.template = xml`
|
||||
<div class="d-flex">
|
||||
<ActionSwiper animationType="'forwards'" onRightSwipe = "{
|
||||
action: onRightSwipe.bind(this),
|
||||
icon: 'fa-circle',
|
||||
bgColor: 'bg-warning',
|
||||
}">
|
||||
<span>test</span>
|
||||
</ActionSwiper>
|
||||
</div>
|
||||
`;
|
||||
|
||||
await mount(Parent, target, { env });
|
||||
await swipeRight(target, ".o_actionswiper");
|
||||
executingAction = true;
|
||||
execRegisteredTimeouts();
|
||||
await prom;
|
||||
await nextTick();
|
||||
assert.verifySteps(["action done", "ActionSwiper patched"]);
|
||||
});
|
||||
});
|
||||
124
odoo-bringout-oca-ocb-web/web/static/tests/mobile/helpers.js
Normal file
124
odoo-bringout-oca-ocb-web/web/static/tests/mobile/helpers.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { findElement, triggerEvent } from "../helpers/utils";
|
||||
|
||||
async function swipe(target, selector, direction) {
|
||||
const touchTarget = findElement(target, selector);
|
||||
if (direction === "left") {
|
||||
// The scrollable element is set at its right limit
|
||||
touchTarget.scrollLeft = touchTarget.scrollWidth - touchTarget.offsetWidth;
|
||||
} else {
|
||||
// The scrollable element is set at its left limit
|
||||
touchTarget.scrollLeft = 0;
|
||||
}
|
||||
|
||||
await triggerEvent(target, selector, "touchstart", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
target: touchTarget,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, selector, "touchmove", {
|
||||
touches: [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: (direction === "left" ? -1 : 1) * touchTarget.clientWidth,
|
||||
clientY: 0,
|
||||
target: touchTarget,
|
||||
},
|
||||
],
|
||||
});
|
||||
await triggerEvent(target, selector, "touchend", {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Will simulate a swipe right on the target element with the given selector.
|
||||
*
|
||||
* @param {HTMLElement} target
|
||||
* @param {DOMSelector} [selector]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function swipeRight(target, selector) {
|
||||
return swipe(target, selector, "right");
|
||||
}
|
||||
|
||||
/**
|
||||
* Will simulate a swipe left on the target element with the given selector.
|
||||
*
|
||||
* @param {HTMLElement} target
|
||||
* @param {DOMSelector} [selector]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function swipeLeft(target, selector) {
|
||||
return swipe(target, selector, "left");
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a "TAP" (touch) on the target element with the given selector.
|
||||
*
|
||||
* @param {HTMLElement} target
|
||||
* @param {DOMSelector} [selector]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function tap(target, selector) {
|
||||
const touchTarget = findElement(target, selector);
|
||||
const box = touchTarget.getBoundingClientRect();
|
||||
const x = box.left + box.width / 2;
|
||||
const y = box.top + box.height / 2;
|
||||
const touch = {
|
||||
identifier: 0,
|
||||
target: touchTarget,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
pageX: x,
|
||||
pageY: y,
|
||||
};
|
||||
await triggerEvent(touchTarget, null, "touchstart", {
|
||||
touches: [touch],
|
||||
});
|
||||
await triggerEvent(touchTarget, null, "touchend", {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a "TAP" (touch) on the target element with the given selector.
|
||||
*
|
||||
* @param {HTMLElement} target
|
||||
* @param {DOMSelector} startSelector
|
||||
* @param {DOMSelector} endSelector
|
||||
* @param {{start: "center"|"top", end: "center"|"bottom"}} [positions]
|
||||
* Specify where the touches will occur in the start and end elements.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function tapAndMove(
|
||||
target,
|
||||
startSelector,
|
||||
endSelector,
|
||||
positions = { start: "center", end: "center" }
|
||||
) {
|
||||
const startTarget = findElement(target, startSelector);
|
||||
const startBox = startTarget.getBoundingClientRect();
|
||||
|
||||
const touch = {
|
||||
identifier: 0,
|
||||
target: startTarget,
|
||||
pageX: startBox.x + startBox.width / 2,
|
||||
pageY: positions.start === "center" ? startBox.y + startBox.height / 2 : startBox.y + 1,
|
||||
};
|
||||
await triggerEvent(startTarget, null, "touchstart", {
|
||||
touches: [touch],
|
||||
});
|
||||
const endTarget = findElement(target, endSelector);
|
||||
const endBox = endTarget.getBoundingClientRect();
|
||||
touch.pageX = endBox.x + endBox.width / 2;
|
||||
touch.pageY = positions.end === "center" ? endBox.y + endBox.height / 2 : endBox.y - 1;
|
||||
await triggerEvent(startTarget, null, "touchmove", {
|
||||
touches: [touch],
|
||||
});
|
||||
await triggerEvent(startTarget, null, "touchend", {
|
||||
touches: [touch],
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture, triggerEvent, nextTick } from "@web/../tests/helpers/utils";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import {
|
||||
editSearch,
|
||||
makeWithSearch,
|
||||
setupControlPanelServiceRegistry,
|
||||
} from "@web/../tests/search/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Search", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
setupControlPanelServiceRegistry();
|
||||
target = getFixture();
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
serverData = {
|
||||
models: {
|
||||
foo: {
|
||||
fields: {
|
||||
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
|
||||
date_field: { string: "Date", type: "date", store: true, sortable: true },
|
||||
},
|
||||
records: [{ date_field: "2022-02-14" }],
|
||||
},
|
||||
},
|
||||
views: {
|
||||
"foo,false,search": `
|
||||
<search>
|
||||
<filter name="birthday" date="birthday"/>
|
||||
<filter name="date_field" date="date_field"/>
|
||||
</search>
|
||||
`,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
QUnit.module("Control Panel (mobile)");
|
||||
|
||||
QUnit.test("Display control panel mobile", async (assert) => {
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchMenuTypes: ["filter"],
|
||||
searchViewId: false,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".breadcrumb");
|
||||
assert.containsOnce(target, ".o_enable_searchview");
|
||||
assert.containsNone(target, ".o_searchview");
|
||||
assert.containsNone(target, ".o_toggle_searchview_full");
|
||||
|
||||
await click(target, ".o_enable_searchview");
|
||||
|
||||
assert.containsNone(target, ".breadcrumb");
|
||||
assert.containsOnce(target, ".o_enable_searchview");
|
||||
assert.containsOnce(target, ".o_searchview");
|
||||
assert.containsOnce(target, ".o_toggle_searchview_full");
|
||||
|
||||
await click(target, ".o_toggle_searchview_full");
|
||||
|
||||
assert.containsOnce(document.body, ".o_searchview.o_mobile_search");
|
||||
assert.containsN(document.body, ".o_mobile_search .o_mobile_search_button", 2);
|
||||
assert.strictEqual(
|
||||
document.body.querySelector(".o_mobile_search_header").textContent.trim(),
|
||||
"FILTER CLEAR"
|
||||
);
|
||||
assert.containsOnce(document.body, ".o_searchview.o_mobile_search .o_cp_searchview");
|
||||
assert.containsOnce(document.body, ".o_searchview.o_mobile_search .o_mobile_search_footer");
|
||||
|
||||
await click(document.body.querySelector(".o_mobile_search_button"));
|
||||
|
||||
assert.containsNone(target, ".breadcrumb");
|
||||
assert.containsOnce(target, ".o_enable_searchview");
|
||||
assert.containsOnce(target, ".o_searchview");
|
||||
assert.containsOnce(target, ".o_toggle_searchview_full");
|
||||
|
||||
await click(target, ".o_enable_searchview");
|
||||
|
||||
assert.containsOnce(target, ".breadcrumb");
|
||||
assert.containsOnce(target, ".o_enable_searchview");
|
||||
assert.containsNone(target, ".o_searchview");
|
||||
assert.containsNone(target, ".o_toggle_searchview_full");
|
||||
});
|
||||
|
||||
QUnit.test("Make a simple search in mobile mode", async (assert) => {
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchMenuTypes: ["filter"],
|
||||
searchViewFields: {
|
||||
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
|
||||
},
|
||||
searchViewArch: `
|
||||
<search>
|
||||
<field name="birthday"/>
|
||||
</search>
|
||||
`,
|
||||
});
|
||||
assert.containsNone(target, ".o_searchview");
|
||||
|
||||
await click(target, ".o_enable_searchview");
|
||||
assert.containsOnce(target, ".o_searchview");
|
||||
const input = target.querySelector(".o_searchview input");
|
||||
assert.containsNone(target, ".o_searchview_autocomplete");
|
||||
|
||||
await editSearch(target, "2022-02-14");
|
||||
assert.strictEqual(input.value, "2022-02-14", "input value should be updated");
|
||||
assert.containsOnce(target, ".o_searchview_autocomplete");
|
||||
|
||||
await triggerEvent(input, null, "keydown", { key: "Escape" });
|
||||
assert.containsNone(target, ".o_searchview_autocomplete");
|
||||
|
||||
await click(target, ".o_enable_searchview");
|
||||
assert.containsNone(target, ".o_searchview");
|
||||
});
|
||||
|
||||
QUnit.test("Control panel is shown/hide on top when scrolling", async (assert) => {
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
const contentHeight = 200;
|
||||
const sampleContent = document.createElement("div");
|
||||
sampleContent.style.minHeight = `${2 * contentHeight}px`;
|
||||
target.appendChild(sampleContent);
|
||||
const { maxHeight, overflow } = target.style;
|
||||
target.style.maxHeight = `${contentHeight}px`;
|
||||
target.style.overflow = "auto";
|
||||
target.scrollTo({ top: 50 });
|
||||
await nextTick();
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_control_panel"),
|
||||
"o_mobile_sticky",
|
||||
"control panel becomes sticky when the target is not on top"
|
||||
);
|
||||
target.scrollTo({ top: -50 });
|
||||
await nextTick();
|
||||
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelector(".o_control_panel"),
|
||||
"o_mobile_sticky",
|
||||
"control panel is not sticky anymore"
|
||||
);
|
||||
target.style.maxHeight = maxHeight;
|
||||
target.style.overflow = overflow;
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { SearchPanel } from "@web/search/search_panel/search_panel";
|
||||
import { makeWithSearch, setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Search", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
setupControlPanelServiceRegistry();
|
||||
target = getFixture();
|
||||
registry.category("services").add("ui", uiService);
|
||||
|
||||
serverData = {
|
||||
models: {
|
||||
foo: {
|
||||
fields: {
|
||||
tag_id: {
|
||||
string: "Many2One",
|
||||
type: "many2one",
|
||||
relation: "tag",
|
||||
store: true,
|
||||
sortable: true,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, tag_id: 2 },
|
||||
{ id: 2, tag_id: 1 },
|
||||
{ id: 3, tag_id: 1 },
|
||||
],
|
||||
},
|
||||
tag: {
|
||||
fields: {
|
||||
name: { string: "Name", type: "string" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "Gold" },
|
||||
{ id: 2, name: "Silver" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
QUnit.module("Search Panel (mobile)");
|
||||
|
||||
QUnit.test("basic search panel rendering", async (assert) => {
|
||||
class Parent extends Component {}
|
||||
Parent.components = { SearchPanel };
|
||||
Parent.template = xml`<SearchPanel/>`;
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: Parent,
|
||||
searchViewFields: serverData.models.foo.fields,
|
||||
searchViewArch: `
|
||||
<search>
|
||||
<searchpanel>
|
||||
<field name="tag_id" icon="fa-bars" string="Tags"/>
|
||||
</searchpanel>
|
||||
</search>`,
|
||||
});
|
||||
assert.containsOnce(target, ".o_search_panel.o_search_panel_summary");
|
||||
|
||||
await click(target, ".o_search_panel .o_search_panel_current_selection");
|
||||
assert.containsOnce(document.body, ".o_search_panel.o_mobile_search");
|
||||
assert.containsN(document.body, ".o_search_panel_category_value", 3); // All, Gold, Silver
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,393 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { CalendarCommonRenderer } from "@web/views/calendar/calendar_common/calendar_common_renderer";
|
||||
import { CalendarYearRenderer } from "@web/views/calendar/calendar_year/calendar_year_renderer";
|
||||
import { click, getFixture, nextTick, patchDate, patchWithCleanup } from "../../helpers/utils";
|
||||
import { changeScale, clickEvent, toggleSectionFilter } from "../../views/calendar/helpers";
|
||||
import { makeView, setupViewRegistries } from "../../views/helpers";
|
||||
import { tap, swipeRight, tapAndMove } from "../helpers";
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
QUnit.module("Views", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
// 2016-12-12 08:00:00
|
||||
patchDate(2016, 11, 12, 8, 0, 0);
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
clearTimeout: () => {},
|
||||
});
|
||||
|
||||
target = getFixture();
|
||||
|
||||
setupViewRegistries();
|
||||
|
||||
serverData = {
|
||||
models: {
|
||||
event: {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
name: { string: "name", type: "char" },
|
||||
start: { string: "start datetime", type: "datetime" },
|
||||
stop: { string: "stop datetime", type: "datetime" },
|
||||
partner_id: {
|
||||
string: "user",
|
||||
type: "many2one",
|
||||
relation: "partner",
|
||||
related: "user_id.partner_id",
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
partner_id: 1,
|
||||
name: "event 1",
|
||||
start: "2016-12-11 00:00:00",
|
||||
stop: "2016-12-11 00:00:00",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
partner_id: 2,
|
||||
name: "event 2",
|
||||
start: "2016-12-12 10:55:05",
|
||||
stop: "2016-12-12 14:55:05",
|
||||
},
|
||||
],
|
||||
methods: {
|
||||
check_access_rights() {
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
},
|
||||
},
|
||||
partner: {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
image: { string: "Image", type: "binary" },
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "partner 1", image: "AAA" },
|
||||
{ id: 2, display_name: "partner 2", image: "BBB" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
QUnit.module("CalendarView - Mobile");
|
||||
|
||||
QUnit.test("simple calendar rendering in mobile", async function (assert) {
|
||||
await makeView({
|
||||
type: "calendar",
|
||||
resModel: "event",
|
||||
arch: `
|
||||
<calendar date_start="start" date_stop="stop">
|
||||
<field name="name"/>
|
||||
</calendar>`,
|
||||
serverData,
|
||||
});
|
||||
|
||||
assert.containsNone(target, ".o_calendar_button_prev", "prev button should be hidden");
|
||||
assert.containsNone(target, ".o_calendar_button_next", "next button should be hidden");
|
||||
assert.isVisible(
|
||||
target.querySelector(
|
||||
".o_cp_bottom_left .o_calendar_buttons .o_calendar_scale_buttons + button.o_cp_today_button"
|
||||
),
|
||||
"today button should be visible near the calendar buttons (bottom left corner)"
|
||||
);
|
||||
|
||||
// Test all views
|
||||
// displays month mode by default
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".fc-view-container > .fc-timeGridWeek-view",
|
||||
"should display the current week"
|
||||
);
|
||||
assert.equal(
|
||||
target.querySelector(".breadcrumb-item").textContent,
|
||||
"undefined (Dec 11 – 17, 2016)"
|
||||
);
|
||||
|
||||
// switch to day mode
|
||||
await click(target, ".o_control_panel .scale_button_selection");
|
||||
await click(target, ".o_control_panel .o_calendar_button_day");
|
||||
await nextTick();
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".fc-view-container > .fc-timeGridDay-view",
|
||||
"should display the current day"
|
||||
);
|
||||
assert.equal(
|
||||
target.querySelector(".breadcrumb-item").textContent,
|
||||
"undefined (December 12, 2016)"
|
||||
);
|
||||
|
||||
// switch to month mode
|
||||
await click(target, ".o_control_panel .scale_button_selection");
|
||||
await click(target, ".o_control_panel .o_calendar_button_month");
|
||||
await nextTick();
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".fc-view-container > .fc-dayGridMonth-view",
|
||||
"should display the current month"
|
||||
);
|
||||
assert.equal(
|
||||
target.querySelector(".breadcrumb-item").textContent,
|
||||
"undefined (December 2016)"
|
||||
);
|
||||
|
||||
// switch to year mode
|
||||
await click(target, ".o_control_panel .scale_button_selection");
|
||||
await click(target, ".o_control_panel .o_calendar_button_year");
|
||||
await nextTick();
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".fc-view-container > .fc-dayGridYear-view",
|
||||
"should display the current year"
|
||||
);
|
||||
assert.equal(target.querySelector(".breadcrumb-item").textContent, "undefined (2016)");
|
||||
});
|
||||
|
||||
QUnit.test("calendar: popover is rendered as dialog in mobile", async function (assert) {
|
||||
// Legacy name of this test: "calendar: popover rendering in mobile"
|
||||
await makeView({
|
||||
type: "calendar",
|
||||
resModel: "event",
|
||||
serverData,
|
||||
arch: `
|
||||
<calendar date_start="start" date_stop="stop">
|
||||
<field name="name"/>
|
||||
</calendar>`,
|
||||
});
|
||||
|
||||
await clickEvent(target, 1);
|
||||
assert.containsNone(target, ".o_cw_popover");
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.hasClass(target.querySelector(".modal"), "o_modal_full");
|
||||
|
||||
assert.containsN(target, ".modal-footer .btn", 2);
|
||||
assert.containsOnce(target, ".modal-footer .btn.btn-primary.o_cw_popover_edit");
|
||||
assert.containsOnce(target, ".modal-footer .btn.btn-secondary.o_cw_popover_delete");
|
||||
});
|
||||
|
||||
QUnit.test("calendar: today button", async function (assert) {
|
||||
await makeView({
|
||||
type: "calendar",
|
||||
resModel: "event",
|
||||
serverData,
|
||||
arch: `<calendar mode="day" date_start="start" date_stop="stop"></calendar>`,
|
||||
});
|
||||
assert.equal(target.querySelector(".fc-day-header[data-date]").dataset.date, "2016-12-12");
|
||||
|
||||
// Swipe right
|
||||
await swipeRight(target, ".o_calendar_widget");
|
||||
assert.equal(target.querySelector(".fc-day-header[data-date]").dataset.date, "2016-12-11");
|
||||
|
||||
await click(target, ".o_calendar_button_today");
|
||||
assert.equal(target.querySelector(".fc-day-header[data-date]").dataset.date, "2016-12-12");
|
||||
});
|
||||
|
||||
QUnit.test("calendar: show and change other calendar", async function (assert) {
|
||||
await makeView({
|
||||
type: "calendar",
|
||||
resModel: "event",
|
||||
serverData,
|
||||
arch: `
|
||||
<calendar date_start="start" date_stop="stop" color="partner_id">
|
||||
<filter name="user_id" avatar_field="image"/>
|
||||
<field name="partner_id" filters="1" invisible="1"/>
|
||||
</calendar>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, ".o_other_calendar_panel");
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_other_calendar_panel .o_filter > *",
|
||||
3,
|
||||
"should contains 3 child nodes -> 1 label (USER) + 2 resources (user 1/2)"
|
||||
);
|
||||
assert.containsNone(target, ".o_calendar_sidebar");
|
||||
assert.containsOnce(target, ".o_calendar_renderer");
|
||||
|
||||
// Toggle the other calendar panel should hide the calendar view and show the sidebar
|
||||
await click(target, ".o_other_calendar_panel");
|
||||
assert.containsOnce(target, ".o_calendar_sidebar");
|
||||
assert.containsNone(target, ".o_calendar_renderer");
|
||||
assert.containsOnce(target, ".o_calendar_filter");
|
||||
assert.containsOnce(target, ".o_calendar_filter[data-name=partner_id]");
|
||||
|
||||
// Toggle the whole section filters by unchecking the all items checkbox
|
||||
await toggleSectionFilter(target, "partner_id");
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_other_calendar_panel .o_filter > *",
|
||||
1,
|
||||
"should contains 1 child node -> 1 label (USER)"
|
||||
);
|
||||
|
||||
// Toggle again the other calendar panel should hide the sidebar and show the calendar view
|
||||
await click(target, ".o_other_calendar_panel");
|
||||
assert.containsNone(target, ".o_calendar_sidebar");
|
||||
assert.containsOnce(target, ".o_calendar_renderer");
|
||||
});
|
||||
|
||||
QUnit.test('calendar: tap on "Free Zone" opens quick create', async function (assert) {
|
||||
patchWithCleanup(CalendarCommonRenderer.prototype, {
|
||||
onDateClick(...args) {
|
||||
assert.step("dateClick");
|
||||
return this._super(...args);
|
||||
},
|
||||
onSelect(...args) {
|
||||
assert.step("select");
|
||||
return this._super(...args);
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "calendar",
|
||||
resModel: "event",
|
||||
serverData,
|
||||
arch: `<calendar mode="day" date_start="start" date_stop="stop"/>`,
|
||||
});
|
||||
|
||||
// Simulate a "TAP" (touch)
|
||||
await tap(
|
||||
target,
|
||||
".fc-time-grid .fc-minor[data-time='00:30:00'] .fc-widget-content:last-child"
|
||||
);
|
||||
await nextTick();
|
||||
|
||||
// should open a Quick create modal view in mobile on short tap
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.verifySteps(["dateClick"]);
|
||||
});
|
||||
|
||||
QUnit.test('calendar: select range on "Free Zone" opens quick create', async function (assert) {
|
||||
patchWithCleanup(CalendarCommonRenderer.prototype, {
|
||||
get options() {
|
||||
return Object.assign({}, this._super(), {
|
||||
selectLongPressDelay: 0,
|
||||
});
|
||||
},
|
||||
onDateClick(info) {
|
||||
assert.step("dateClick");
|
||||
return this._super(info);
|
||||
},
|
||||
onSelect(info) {
|
||||
assert.step("select");
|
||||
const { startStr, endStr } = info;
|
||||
assert.equal(startStr, "2016-12-12T01:00:00+01:00");
|
||||
assert.equal(endStr, "2016-12-12T02:00:00+01:00");
|
||||
return this._super(info);
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "calendar",
|
||||
resModel: "event",
|
||||
serverData,
|
||||
arch: `<calendar mode="day" date_start="start" date_stop="stop"/>`,
|
||||
});
|
||||
|
||||
// Simulate a "TAP" (touch)
|
||||
await tapAndMove(
|
||||
target,
|
||||
".fc-time-grid [data-time='01:00:00'] .fc-widget-content:last-child",
|
||||
".fc-time-grid [data-time='02:00:00'] .fc-widget-content:last-child",
|
||||
{ start: "top", end: "bottom" }
|
||||
);
|
||||
await nextTick();
|
||||
|
||||
// should open a Quick create modal view in mobile on short tap
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.verifySteps(["select"]);
|
||||
});
|
||||
|
||||
QUnit.test("calendar (year): select date range opens quick create", async function (assert) {
|
||||
patchWithCleanup(CalendarYearRenderer.prototype, {
|
||||
get options() {
|
||||
return Object.assign({}, this._super(), {
|
||||
longPressDelay: 0,
|
||||
selectLongPressDelay: 0,
|
||||
});
|
||||
},
|
||||
onDateClick(info) {
|
||||
assert.step("dateClick");
|
||||
return this._super(info);
|
||||
},
|
||||
onSelect(info) {
|
||||
assert.step("select");
|
||||
const { startStr, endStr } = info;
|
||||
assert.equal(startStr, "2016-02-02");
|
||||
assert.equal(endStr, "2016-02-06"); // end date is exclusive
|
||||
return this._super(info);
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "calendar",
|
||||
resModel: "event",
|
||||
serverData,
|
||||
arch: `<calendar mode="year" date_start="start" date_stop="stop"/>`,
|
||||
});
|
||||
|
||||
// Tap on a date
|
||||
await tapAndMove(
|
||||
target,
|
||||
".fc-day-top[data-date='2016-02-02']",
|
||||
".fc-day-top[data-date='2016-02-05']"
|
||||
);
|
||||
await nextTick();
|
||||
|
||||
// should open a Quick create modal view in mobile on short tap
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.verifySteps(["select"]);
|
||||
});
|
||||
|
||||
QUnit.test("calendar (year): tap on date switch to day scale", async function (assert) {
|
||||
await makeView({
|
||||
type: "calendar",
|
||||
resModel: "event",
|
||||
serverData,
|
||||
arch: `<calendar mode="year" date_start="start" date_stop="stop"/>`,
|
||||
});
|
||||
|
||||
// Should display year view
|
||||
assert.containsOnce(target, ".fc-dayGridYear-view");
|
||||
assert.containsN(target, ".fc-month-container", 12);
|
||||
assert.equal(target.querySelector(".breadcrumb-item").textContent, "undefined (2016)");
|
||||
|
||||
// Tap on a date
|
||||
await tap(target, ".fc-day-top[data-date='2016-02-05']");
|
||||
await nextTick(); // switch renderer
|
||||
await nextTick(); // await breadcrumb update
|
||||
|
||||
// Should display day view
|
||||
assert.containsNone(target, ".fc-dayGridYear-view");
|
||||
assert.containsOnce(target, ".fc-timeGridDay-view");
|
||||
assert.equal(
|
||||
target.querySelector(".breadcrumb-item").textContent,
|
||||
"undefined (February 5, 2016)"
|
||||
);
|
||||
|
||||
// Change scale to month
|
||||
await changeScale(target, "month");
|
||||
assert.containsNone(target, ".fc-timeGridDay-view");
|
||||
assert.containsOnce(target, ".fc-dayGridMonth-view");
|
||||
assert.equal(
|
||||
target.querySelector(".breadcrumb-item").textContent,
|
||||
"undefined (February 2016)"
|
||||
);
|
||||
|
||||
// Tap on a date
|
||||
await tap(target, ".fc-day-top[data-date='2016-02-10']");
|
||||
await nextTick(); // await reload & render
|
||||
await nextTick(); // await breadcrumb update
|
||||
|
||||
// should open a Quick create modal view in mobile on short tap on date in monthly view
|
||||
assert.containsOnce(target, ".modal");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
timmy: { string: "pokemon", type: "many2many", relation: "partner_type" },
|
||||
},
|
||||
},
|
||||
partner_type: {
|
||||
fields: {
|
||||
name: { string: "Partner Type", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{ id: 12, display_name: "gold" },
|
||||
{ id: 14, display_name: "silver" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("Many2ManyTagsField");
|
||||
|
||||
QUnit.test("Many2ManyTagsField placeholder should be correct", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_tags" placeholder="foo"/>
|
||||
</form>`,
|
||||
});
|
||||
assert.strictEqual(target.querySelector("#timmy").placeholder, "foo");
|
||||
});
|
||||
|
||||
QUnit.test("Many2ManyTagsField placeholder should be empty", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="timmy" widget="many2many_tags"/>
|
||||
</form>`,
|
||||
});
|
||||
assert.strictEqual(target.querySelector("#timmy").placeholder, "");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { click, clickSave, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
import * as BarcodeScanner from "@web/webclient/barcode/barcode_scanner";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
const CREATE = "create";
|
||||
const NAME_SEARCH = "name_search";
|
||||
const PRODUCT_PRODUCT = "product.product";
|
||||
const SALE_ORDER_LINE = "sale_order_line";
|
||||
const PRODUCT_FIELD_NAME = "product_id";
|
||||
|
||||
// MockRPC to allow the search in barcode too
|
||||
async function barcodeMockRPC(route, args, performRPC) {
|
||||
if (args.method === NAME_SEARCH && args.model === PRODUCT_PRODUCT) {
|
||||
const result = await performRPC(route, args);
|
||||
const records = serverData.models[PRODUCT_PRODUCT].records
|
||||
.filter((record) => record.barcode === args.kwargs.name)
|
||||
.map((record) => [record.id, record.name]);
|
||||
return records.concat(result);
|
||||
}
|
||||
}
|
||||
|
||||
QUnit.module("Fields", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
[PRODUCT_PRODUCT]: {
|
||||
fields: {
|
||||
id: { type: "integer" },
|
||||
name: {},
|
||||
barcode: {},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 111,
|
||||
name: "product_cable_management_box",
|
||||
barcode: "601647855631",
|
||||
},
|
||||
{
|
||||
id: 112,
|
||||
name: "product_n95_mask",
|
||||
barcode: "601647855632",
|
||||
},
|
||||
{
|
||||
id: 113,
|
||||
name: "product_surgical_mask",
|
||||
barcode: "601647855633",
|
||||
},
|
||||
],
|
||||
},
|
||||
[SALE_ORDER_LINE]: {
|
||||
fields: {
|
||||
id: { type: "integer" },
|
||||
[PRODUCT_FIELD_NAME]: {
|
||||
string: PRODUCT_FIELD_NAME,
|
||||
type: "many2one",
|
||||
relation: PRODUCT_PRODUCT,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
views: {
|
||||
"product.product,false,kanban": `
|
||||
<kanban><templates><t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<field name="id"/>
|
||||
<field name="name"/>
|
||||
<field name="barcode"/>
|
||||
</div>
|
||||
</t></templates></kanban>
|
||||
`,
|
||||
"product.product,false,search": "<search></search>",
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
|
||||
patchWithCleanup(AutoComplete, {
|
||||
delay: 0,
|
||||
});
|
||||
|
||||
// simulate a environment with a camera/webcam
|
||||
patchWithCleanup(
|
||||
browser,
|
||||
Object.assign({}, browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
navigator: {
|
||||
userAgent: "Chrome/0.0.0 (Linux; Android 13; Odoo TestSuite)",
|
||||
mediaDevices: {
|
||||
getUserMedia: () => [],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.module("Many2OneField Barcode (Small)");
|
||||
|
||||
QUnit.test("barcode button with multiple results", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
// The product selected (mock) for the barcode scanner
|
||||
const selectedRecordTest = serverData.models[PRODUCT_PRODUCT].records[1];
|
||||
|
||||
patchWithCleanup(BarcodeScanner, {
|
||||
scanBarcode: async () => "mask",
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: SALE_ORDER_LINE,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="${PRODUCT_FIELD_NAME}" options="{'can_scan_barcode': True}"/>
|
||||
</form>`,
|
||||
async mockRPC(route, args, performRPC) {
|
||||
if (args.method === CREATE && args.model === SALE_ORDER_LINE) {
|
||||
const selectedId = args.args[0][PRODUCT_FIELD_NAME];
|
||||
assert.equal(
|
||||
selectedId,
|
||||
selectedRecordTest.id,
|
||||
`product id selected ${selectedId}, should be ${selectedRecordTest.id} (${selectedRecordTest.barcode})`
|
||||
);
|
||||
return performRPC(route, args, performRPC);
|
||||
}
|
||||
return barcodeMockRPC(route, args, performRPC);
|
||||
},
|
||||
});
|
||||
|
||||
const scanButton = target.querySelector(".o_barcode");
|
||||
assert.containsOnce(target, scanButton, "has scanner barcode button");
|
||||
|
||||
await click(target, ".o_barcode");
|
||||
|
||||
const modal = target.querySelector(".modal-dialog.modal-lg");
|
||||
assert.containsOnce(target, modal, "there should be one modal opened in full screen");
|
||||
|
||||
assert.containsN(
|
||||
modal,
|
||||
".o_kanban_record .oe_kanban_global_click",
|
||||
2,
|
||||
"there should be 2 records displayed"
|
||||
);
|
||||
|
||||
await click(modal, ".o_kanban_record:nth-child(1)");
|
||||
await clickSave(target);
|
||||
});
|
||||
|
||||
QUnit.test("many2one with barcode show all records", async function (assert) {
|
||||
// The product selected (mock) for the barcode scanner
|
||||
const selectedRecordTest = serverData.models[PRODUCT_PRODUCT].records[0];
|
||||
|
||||
patchWithCleanup(BarcodeScanner, {
|
||||
scanBarcode: async () => selectedRecordTest.barcode,
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: SALE_ORDER_LINE,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="${PRODUCT_FIELD_NAME}" options="{'can_scan_barcode': True}"/>
|
||||
</form>`,
|
||||
mockRPC: barcodeMockRPC,
|
||||
});
|
||||
|
||||
// Select one product
|
||||
await click(target, ".o_barcode");
|
||||
|
||||
// Click on the input to show all records
|
||||
await click(target, ".o_input_dropdown > input");
|
||||
|
||||
const modal = target.querySelector(".modal-dialog.modal-lg");
|
||||
assert.containsOnce(target, modal, "there should be one modal opened in full screen");
|
||||
|
||||
assert.containsN(
|
||||
modal,
|
||||
".o_kanban_record .oe_kanban_global_click",
|
||||
3,
|
||||
"there should be 3 records displayed"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let fixture;
|
||||
let serverData;
|
||||
|
||||
QUnit.module("Mobile Fields", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
setupViewRegistries();
|
||||
fixture = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
trululu: { string: "Trululu", type: "many2one", relation: "partner" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "first record", trululu: 4 },
|
||||
{ id: 2, display_name: "second record", trululu: 1 },
|
||||
{ id: 4, display_name: "aaa" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
QUnit.module("StatusBarField");
|
||||
|
||||
QUnit.test("statusbar is rendered correclty on small devices", async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" />
|
||||
</header>
|
||||
<field name="display_name" />
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_status > button",
|
||||
"should have only one visible status in mobile, the active one"
|
||||
);
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_status .dropdown",
|
||||
"should have a dropdown containing all status"
|
||||
);
|
||||
assert.containsNone(
|
||||
fixture,
|
||||
".o_statusbar_status .dropdown-menu",
|
||||
"dropdown should be hidden"
|
||||
);
|
||||
assert.strictEqual(
|
||||
fixture.querySelector(".o_statusbar_status button.dropdown-toggle").textContent.trim(),
|
||||
"aaa",
|
||||
"statusbar button should display current field value"
|
||||
);
|
||||
|
||||
// open the dropdown
|
||||
await click(fixture, ".o_statusbar_status > button");
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_status .dropdown-menu",
|
||||
"dropdown should be visible"
|
||||
);
|
||||
assert.containsN(
|
||||
fixture,
|
||||
".o_statusbar_status .dropdown-menu .btn",
|
||||
3,
|
||||
"should have 3 status"
|
||||
);
|
||||
assert.containsN(
|
||||
fixture,
|
||||
".o_statusbar_status .btn.disabled",
|
||||
3,
|
||||
"all status should be disabled"
|
||||
);
|
||||
assert.hasClass(
|
||||
fixture.querySelector(".o_statusbar_status .btn:nth-child(3)"),
|
||||
"btn-primary",
|
||||
"active status should be btn-primary"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("statusbar with no status on extra small screens", async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 4,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
assert.doesNotHaveClass(
|
||||
fixture.querySelector(".o_field_statusbar"),
|
||||
"o_field_empty",
|
||||
"statusbar widget should have class o_field_empty in edit"
|
||||
);
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_status button.dropdown-toggle",
|
||||
"statusbar widget should have a button"
|
||||
);
|
||||
assert.strictEqual(
|
||||
fixture.querySelector(".o_statusbar_status button.dropdown-toggle").textContent.trim(),
|
||||
"",
|
||||
"statusbar button shouldn't have text for null field value"
|
||||
);
|
||||
|
||||
await click(fixture, ".o_statusbar_status button.dropdown-toggle");
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_status .dropdown-menu",
|
||||
"statusbar widget should have a dropdown menu"
|
||||
);
|
||||
assert.containsN(
|
||||
fixture,
|
||||
".o_statusbar_status .dropdown-menu .btn",
|
||||
3,
|
||||
"statusbar widget dropdown menu should have 3 buttons"
|
||||
);
|
||||
assert.strictEqual(
|
||||
fixture
|
||||
.querySelectorAll(".o_statusbar_status .dropdown-menu .btn")[0]
|
||||
.textContent.trim(),
|
||||
"first record",
|
||||
"statusbar widget dropdown first button should display the first record display_name"
|
||||
);
|
||||
assert.strictEqual(
|
||||
fixture
|
||||
.querySelectorAll(".o_statusbar_status .dropdown-menu .btn")[1]
|
||||
.textContent.trim(),
|
||||
"second record",
|
||||
"statusbar widget dropdown second button should display the second record display_name"
|
||||
);
|
||||
assert.strictEqual(
|
||||
fixture
|
||||
.querySelectorAll(".o_statusbar_status .dropdown-menu .btn")[2]
|
||||
.textContent.trim(),
|
||||
"aaa",
|
||||
"statusbar widget dropdown third button should display the third record display_name"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("clickable statusbar widget on mobile view", async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<field name="trululu" widget="statusbar" options="{'clickable': '1'}" />
|
||||
</header>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await click(fixture, ".o_statusbar_status .dropdown-toggle");
|
||||
assert.hasClass(
|
||||
fixture.querySelector(".o_statusbar_status .dropdown-menu .btn:nth-child(3)"),
|
||||
"btn-primary"
|
||||
);
|
||||
assert.hasClass(
|
||||
fixture.querySelector(".o_statusbar_status .dropdown-menu .btn:nth-child(3)"),
|
||||
"disabled"
|
||||
);
|
||||
|
||||
assert.containsN(
|
||||
fixture,
|
||||
".o_statusbar_status .btn-secondary:not(.dropdown-toggle):not(.disabled)",
|
||||
2,
|
||||
"other status should be btn-secondary and not disabled"
|
||||
);
|
||||
|
||||
await click(
|
||||
fixture.querySelector(
|
||||
".o_statusbar_status .btn-secondary:not(.dropdown-toggle):not(.disabled)"
|
||||
)
|
||||
);
|
||||
|
||||
await click(fixture, ".o_statusbar_status .dropdown-toggle");
|
||||
assert.hasClass(
|
||||
fixture.querySelector(".o_statusbar_status .dropdown-menu .btn:nth-child(1)"),
|
||||
"btn-primary"
|
||||
);
|
||||
assert.hasClass(
|
||||
fixture.querySelector(".o_statusbar_status .dropdown-menu .btn:nth-child(1)"),
|
||||
"disabled"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,482 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
click,
|
||||
clickSave,
|
||||
editInput,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { AttachDocumentWidget } from "@web/views/widgets/attach_document/attach_document";
|
||||
|
||||
let fixture;
|
||||
let serverData;
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("Mobile Views", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
setupViewRegistries();
|
||||
fixture = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { type: "char", string: "Display Name" },
|
||||
trululu: { type: "many2one", string: "Trululu", relation: "partner" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "first record", trululu: 4 },
|
||||
{ id: 2, display_name: "second record", trululu: 1 },
|
||||
{ id: 4, display_name: "aaa" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
QUnit.module("FormView");
|
||||
|
||||
QUnit.test(`statusbar buttons are correctly rendered in mobile`, async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<button string="Confirm" />
|
||||
<button string="Do it" />
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<button name="display_name" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_buttons .dropdown",
|
||||
"statusbar should contain a button 'Action'"
|
||||
);
|
||||
assert.containsNone(
|
||||
fixture,
|
||||
".o_statusbar_buttons .dropdown-menu",
|
||||
"statusbar should contain a dropdown"
|
||||
);
|
||||
assert.containsNone(
|
||||
fixture,
|
||||
".o_statusbar_buttons .dropdown-menu:visible",
|
||||
"dropdown should be hidden"
|
||||
);
|
||||
|
||||
// open the dropdown
|
||||
await click(fixture, ".o_statusbar_buttons .dropdown-toggle");
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_buttons .dropdown-menu:visible",
|
||||
"dropdown should be visible"
|
||||
);
|
||||
assert.containsN(
|
||||
fixture,
|
||||
".o_statusbar_buttons .dropdown-menu button",
|
||||
2,
|
||||
"dropdown should contain 2 buttons"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
`statusbar "Action" button should be displayed only if there are multiple visible buttons`,
|
||||
async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<button string="Confirm" attrs="{'invisible': [['display_name', '=', 'first record']]}" />
|
||||
<button string="Do it" attrs="{'invisible': [['display_name', '=', 'first record']]}" />
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="display_name" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
// if all buttons are invisible then there should be no action button
|
||||
assert.containsNone(
|
||||
fixture,
|
||||
".o_statusbar_buttons > btn-group > .dropdown-toggle",
|
||||
"'Action' dropdown is not displayed as there are no visible buttons"
|
||||
);
|
||||
|
||||
// change display_name to update buttons modifiers and make it visible
|
||||
await editInput(fixture, ".o_field_widget[name=display_name] input", "test");
|
||||
await clickSave(fixture);
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_buttons .dropdown",
|
||||
"statusbar should contain a dropdown"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
`statusbar "Action" button shouldn't be displayed for only one visible button`,
|
||||
async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<button string="Hola" attrs="{'invisible': [['display_name', '=', 'first record']]}" />
|
||||
<button string="Ciao" />
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="display_name" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
// There should be a simple statusbar button and no action dropdown
|
||||
assert.containsNone(
|
||||
fixture,
|
||||
".o_statusbar_buttons .dropdown",
|
||||
"should have no 'Action' dropdown"
|
||||
);
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_buttons > button",
|
||||
"should have a simple statusbar button"
|
||||
);
|
||||
|
||||
// change display_name to update buttons modifiers and make both buttons visible
|
||||
await editInput(fixture, ".o_field_widget[name=display_name] input", "test");
|
||||
|
||||
// Now there should an action dropdown, because there are two visible buttons
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_buttons .dropdown",
|
||||
"should have no 'Action' dropdown"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
`statusbar widgets should appear in the statusbar dropdown only if there are multiple items`,
|
||||
async (assert) => {
|
||||
serviceRegistry.add("http", {
|
||||
start: () => ({}),
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
resId: 2,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<widget name="attach_document" string="Attach document" />
|
||||
<button string="Ciao" attrs="{'invisible': [['display_name', '=', 'first record']]}" />
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="display_name" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
// Now there should an action dropdown, because there are two visible buttons
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_buttons .dropdown",
|
||||
"should have 'Action' dropdown"
|
||||
);
|
||||
|
||||
await click(fixture, ".o_statusbar_buttons .dropdown-toggle");
|
||||
assert.containsN(
|
||||
fixture,
|
||||
".o_statusbar_buttons .dropdown-menu button",
|
||||
2,
|
||||
"should have 2 buttons in the dropdown"
|
||||
);
|
||||
|
||||
// change display_name to update buttons modifiers and make one button visible
|
||||
await editInput(fixture, ".o_field_widget[name=display_name] input", "first record");
|
||||
|
||||
// There should be a simple statusbar button and no action dropdown
|
||||
assert.containsNone(
|
||||
fixture,
|
||||
".o_statusbar_buttons .dropdown",
|
||||
"shouldn't have 'Action' dropdown"
|
||||
);
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_buttons button:visible",
|
||||
"should have 1 button visible in the statusbar"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
`statusbar "Action" dropdown should keep its open/close state`,
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<button string="Just more than one" />
|
||||
<button string="Confirm" attrs="{'invisible': [['display_name', '=', '']]}" />
|
||||
<button string="Do it" attrs="{'invisible': [['display_name', '!=', '']]}" />
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="display_name" />
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_buttons .dropdown",
|
||||
"statusbar should contain a dropdown"
|
||||
);
|
||||
assert.doesNotHaveClass(
|
||||
fixture.querySelector(".o_statusbar_buttons .dropdown"),
|
||||
"show",
|
||||
"dropdown should be closed"
|
||||
);
|
||||
|
||||
// open the dropdown
|
||||
await click(fixture, ".o_statusbar_buttons .dropdown-toggle");
|
||||
assert.hasClass(
|
||||
fixture.querySelector(".o_statusbar_buttons .dropdown"),
|
||||
"show",
|
||||
"dropdown should be opened"
|
||||
);
|
||||
|
||||
// change display_name to update buttons' modifiers
|
||||
await editInput(fixture, ".o_field_widget[name=display_name] input", "test");
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_buttons .dropdown",
|
||||
"statusbar should contain a dropdown"
|
||||
);
|
||||
assert.hasClass(
|
||||
fixture.querySelector(".o_statusbar_buttons .dropdown"),
|
||||
"show",
|
||||
"dropdown should still be opened"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
`statusbar "Action" dropdown's open/close state shouldn't be modified after 'onchange'`,
|
||||
async function (assert) {
|
||||
serverData.models.partner.onchanges = {
|
||||
display_name: async () => {},
|
||||
};
|
||||
|
||||
const onchangeDef = makeDeferred();
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<button name="create" string="Create Invoice" type="action" />
|
||||
<button name="send" string="Send by Email" type="action" />
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="display_name" />
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
mockRPC(route, { method, args: [, , changedField] }) {
|
||||
if (method === "onchange" && changedField === "display_name") {
|
||||
return onchangeDef;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".o_statusbar_buttons .dropdown",
|
||||
"statusbar should contain a dropdown"
|
||||
);
|
||||
assert.doesNotHaveClass(
|
||||
fixture.querySelector(".o_statusbar_buttons .dropdown"),
|
||||
"show",
|
||||
"dropdown should be closed"
|
||||
);
|
||||
|
||||
await editInput(fixture, ".o_field_widget[name=display_name] input", "before onchange");
|
||||
await click(fixture, ".o_statusbar_buttons .dropdown-toggle");
|
||||
assert.hasClass(
|
||||
fixture.querySelector(".o_statusbar_buttons .dropdown"),
|
||||
"show",
|
||||
"dropdown should be opened"
|
||||
);
|
||||
|
||||
onchangeDef.resolve({ value: { display_name: "after onchange" } });
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
fixture.querySelector(".o_field_widget[name=display_name] input").value,
|
||||
"after onchange"
|
||||
);
|
||||
assert.hasClass(
|
||||
fixture.querySelector(".o_statusbar_buttons .dropdown"),
|
||||
"show",
|
||||
"dropdown should still be opened"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
`preserve current scroll position on form view while closing dialog`,
|
||||
async function (assert) {
|
||||
serverData.views = {
|
||||
"partner,false,kanban": `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<field name="display_name" />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
`,
|
||||
"partner,false,search": `
|
||||
<search />
|
||||
`,
|
||||
};
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 2,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<p style="height:500px" />
|
||||
<field name="trululu" />
|
||||
<p style="height:500px" />
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
let position = { top: 0, left: 0 };
|
||||
patchWithCleanup(window, {
|
||||
scrollTo(newPosition) {
|
||||
position = newPosition;
|
||||
},
|
||||
get scrollX() {
|
||||
return position.left;
|
||||
},
|
||||
get scrollY() {
|
||||
return position.top;
|
||||
},
|
||||
});
|
||||
|
||||
window.scrollTo({ top: 265, left: 0 });
|
||||
assert.strictEqual(window.scrollY, 265, "Should have scrolled 265 px vertically");
|
||||
assert.strictEqual(window.scrollX, 0, "Should be 0 px from left as it is");
|
||||
|
||||
// click on m2o field
|
||||
await click(fixture, ".o_field_many2one input");
|
||||
// assert.strictEqual(window.scrollY, 0, "Should have scrolled to top (0) px");
|
||||
assert.containsOnce(
|
||||
fixture,
|
||||
".modal.o_modal_full",
|
||||
"there should be a many2one modal opened in full screen"
|
||||
);
|
||||
|
||||
// click on back button
|
||||
await click(fixture, ".modal .modal-header .fa-arrow-left");
|
||||
assert.strictEqual(
|
||||
window.scrollY,
|
||||
265,
|
||||
"Should have scrolled back to 265 px vertically"
|
||||
);
|
||||
assert.strictEqual(window.scrollX, 0, "Should be 0 px from left as it is");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("attach_document widget also works inside a dropdown", async (assert) => {
|
||||
let fileInput;
|
||||
patchWithCleanup(AttachDocumentWidget.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
fileInput = this.fileInput;
|
||||
},
|
||||
});
|
||||
|
||||
serviceRegistry.add("http", {
|
||||
start: () => ({
|
||||
post: (route, params) => {
|
||||
assert.step("post");
|
||||
assert.strictEqual(route, "/web/binary/upload_attachment");
|
||||
assert.strictEqual(params.model, "partner");
|
||||
assert.strictEqual(params.id, 1);
|
||||
return '[{ "id": 5 }, { "id": 2 }]';
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<button string="Confirm" />
|
||||
<widget name="attach_document" string="Attach Document"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<button name="display_name" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
await click(fixture, ".o_statusbar_buttons .dropdown-toggle");
|
||||
await click(fixture, ".o_attach_document");
|
||||
fileInput.dispatchEvent(new Event("change"));
|
||||
await nextTick();
|
||||
assert.verifySteps(["post"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeFakeUserService } from "@web/../tests/helpers/mock_services";
|
||||
import { click, getFixture, patchWithCleanup, triggerEvents } from "@web/../tests/helpers/utils";
|
||||
import { getMenuItemTexts, toggleActionMenu } from "@web/../tests/search/helpers";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData;
|
||||
let fixture;
|
||||
|
||||
QUnit.module("Mobile Views", ({ beforeEach }) => {
|
||||
beforeEach(() => {
|
||||
setupViewRegistries();
|
||||
fixture = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
foo: {
|
||||
fields: {
|
||||
foo: { string: "Foo", type: "char" },
|
||||
bar: { string: "Bar", type: "boolean" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, bar: true, foo: "yop" },
|
||||
{ id: 2, bar: true, foo: "blip" },
|
||||
{ id: 3, bar: true, foo: "gnap" },
|
||||
{ id: 4, bar: false, foo: "blip" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn() || true,
|
||||
clearTimeout: () => {},
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("ListView");
|
||||
|
||||
QUnit.test("selection is properly displayed (single page)", async function (assert) {
|
||||
registry.category("services").add(
|
||||
"user",
|
||||
makeFakeUserService(() => false),
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="foo"/>
|
||||
<field name="bar"/>
|
||||
</tree>
|
||||
`,
|
||||
loadActionMenus: true,
|
||||
});
|
||||
|
||||
assert.containsN(fixture, ".o_data_row", 4);
|
||||
assert.containsNone(fixture, ".o_list_selection_box");
|
||||
assert.containsOnce(fixture, ".o_control_panel .o_cp_bottom_right");
|
||||
|
||||
// select a record
|
||||
await triggerEvents(fixture, ".o_data_row:nth-child(1)", ["touchstart", "touchend"]);
|
||||
assert.containsOnce(fixture, ".o_list_selection_box");
|
||||
assert.containsNone(fixture, ".o_list_selection_box .o_list_select_domain");
|
||||
assert.containsNone(fixture, ".o_control_panel .o_cp_bottom_right");
|
||||
assert.ok(
|
||||
fixture.querySelector(".o_list_selection_box").textContent.includes("1 selected")
|
||||
);
|
||||
|
||||
// unselect a record
|
||||
await triggerEvents(fixture, ".o_data_row:nth-child(1)", ["touchstart", "touchend"]);
|
||||
assert.containsNone(fixture, ".o_list_selection_box .o_list_select_domain");
|
||||
|
||||
// select 2 records
|
||||
await triggerEvents(fixture, ".o_data_row:nth-child(1)", ["touchstart", "touchend"]);
|
||||
await triggerEvents(fixture, ".o_data_row:nth-child(2)", ["touchstart", "touchend"]);
|
||||
assert.ok(
|
||||
fixture.querySelector(".o_list_selection_box").textContent.includes("2 selected")
|
||||
);
|
||||
assert.containsOnce(fixture, "div.o_control_panel .o_cp_action_menus");
|
||||
|
||||
await toggleActionMenu(fixture);
|
||||
assert.deepEqual(
|
||||
getMenuItemTexts(fixture.querySelector(".o_cp_action_menus")),
|
||||
["Delete"],
|
||||
"action menu should contain the Delete action"
|
||||
);
|
||||
|
||||
// unselect all
|
||||
await click(fixture, ".o_discard_selection");
|
||||
assert.containsNone(fixture, ".o_list_selection_box");
|
||||
assert.containsOnce(fixture, ".o_control_panel .o_cp_bottom_right");
|
||||
});
|
||||
|
||||
QUnit.test("selection box is properly displayed (multi pages)", async function (assert) {
|
||||
registry.category("services").add(
|
||||
"user",
|
||||
makeFakeUserService(() => false),
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree limit="3">
|
||||
<field name="foo"/>
|
||||
<field name="bar"/>
|
||||
</tree>
|
||||
`,
|
||||
loadActionMenus: true,
|
||||
});
|
||||
|
||||
assert.containsN(fixture, ".o_data_row", 3);
|
||||
assert.containsNone(fixture, ".o_list_selection_box");
|
||||
|
||||
// select a record
|
||||
await triggerEvents(fixture, ".o_data_row:nth-child(1)", ["touchstart", "touchend"]);
|
||||
|
||||
assert.containsOnce(fixture, ".o_list_selection_box");
|
||||
assert.containsNone(fixture, ".o_list_selection_box .o_list_select_domain");
|
||||
assert.ok(
|
||||
fixture.querySelector(".o_list_selection_box").textContent.includes("1 selected")
|
||||
);
|
||||
assert.containsOnce(fixture, ".o_list_selection_box");
|
||||
assert.containsOnce(fixture, "div.o_control_panel .o_cp_action_menus");
|
||||
|
||||
await toggleActionMenu(fixture);
|
||||
assert.deepEqual(
|
||||
getMenuItemTexts(fixture.querySelector(".o_cp_action_menus")),
|
||||
["Delete"],
|
||||
"action menu should contain the Delete action"
|
||||
);
|
||||
|
||||
// select all records of first page
|
||||
await triggerEvents(fixture, ".o_data_row:nth-child(2)", ["touchstart", "touchend"]);
|
||||
await triggerEvents(fixture, ".o_data_row:nth-child(3)", ["touchstart", "touchend"]);
|
||||
assert.containsOnce(fixture, ".o_list_selection_box");
|
||||
assert.containsOnce(fixture, ".o_list_selection_box .o_list_select_domain");
|
||||
assert.ok(
|
||||
fixture.querySelector(".o_list_selection_box").textContent.includes("3 selected")
|
||||
);
|
||||
assert.containsOnce(fixture, ".o_list_select_domain");
|
||||
|
||||
// select all domain
|
||||
await click(fixture, ".o_list_selection_box .o_list_select_domain");
|
||||
assert.containsOnce(fixture, ".o_list_selection_box");
|
||||
assert.ok(
|
||||
fixture.querySelector(".o_list_selection_box").textContent.includes("All 4 selected")
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("export button is properly hidden", async (assert) => {
|
||||
registry.category("services").add(
|
||||
"user",
|
||||
makeFakeUserService((group) => group === "base.group_allow_export"),
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="foo"/>
|
||||
<field name="bar"/>
|
||||
</tree>
|
||||
`,
|
||||
});
|
||||
|
||||
assert.containsN(fixture, ".o_data_row", 4);
|
||||
assert.isNotVisible(fixture.querySelector(".o_list_export_xlsx"));
|
||||
});
|
||||
|
||||
QUnit.test("editable readonly list view is disabled", async (assert) => {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="foo" />
|
||||
</tree>
|
||||
`,
|
||||
});
|
||||
|
||||
await triggerEvents(fixture, ".o_data_row:nth-child(1)", ["touchstart", "touchend"]);
|
||||
await click(fixture, ".o_data_row:nth-child(1) .o_data_cell:nth-child(1)");
|
||||
assert.containsNone(
|
||||
fixture,
|
||||
".o_selected_row .o_field_widget[name=foo]",
|
||||
"The listview should not contains an edit field"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("add custom field button not shown in mobile (with opt. col.)", async (assert) => {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="foo" />
|
||||
<field name="bar" optional="hide" />
|
||||
</tree>
|
||||
`,
|
||||
});
|
||||
assert.containsOnce(fixture, "table .o_optional_columns_dropdown_toggle");
|
||||
await click(fixture, "table .o_optional_columns_dropdown_toggle");
|
||||
assert.containsOnce(fixture, "div.o_optional_columns_dropdown .dropdown-item");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"add custom field button not shown to non-system users (wo opt. col.)",
|
||||
async (assert) => {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: `
|
||||
<tree>
|
||||
<field name="foo" />
|
||||
<field name="bar" />
|
||||
</tree>
|
||||
`,
|
||||
});
|
||||
|
||||
assert.containsNone(fixture, "table .o_optional_columns_dropdown_toggle");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { click, getFixture } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
QUnit.module("ViewDialogs", (hooks) => {
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
product: {
|
||||
fields: {
|
||||
id: { type: "integer" },
|
||||
name: {},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 111,
|
||||
name: "product_cable_management_box",
|
||||
},
|
||||
],
|
||||
},
|
||||
sale_order_line: {
|
||||
fields: {
|
||||
id: { type: "integer" },
|
||||
product_id: {
|
||||
string: "product_id",
|
||||
type: "many2one",
|
||||
relation: "product",
|
||||
},
|
||||
linked_sale_order_line: {
|
||||
string: "linked_sale_order_line",
|
||||
type: "many2many",
|
||||
relation: "sale_order_line",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
views: {
|
||||
"product,false,kanban": `
|
||||
<kanban><templates><t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<field name="id"/>
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</t></templates></kanban>
|
||||
`,
|
||||
"sale_order_line,false,kanban": `
|
||||
<kanban><templates><t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<field name="id"/>
|
||||
</div>
|
||||
</t></templates></kanban>
|
||||
`,
|
||||
"product,false,search": "<search></search>",
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("SelectCreateDialog - Mobile");
|
||||
|
||||
QUnit.test("SelectCreateDialog: clear selection in mobile", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "sale_order_line",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="product_id"/>
|
||||
<field name="linked_sale_order_line" widget="many2many_tags"/>
|
||||
</form>`,
|
||||
async mockRPC(route, args) {
|
||||
if (args.method === "create" && args.model === "sale_order_line") {
|
||||
const { product_id: selectedId } = args.args[0];
|
||||
assert.strictEqual(selectedId, false, `there should be no product selected`);
|
||||
}
|
||||
},
|
||||
});
|
||||
const clearBtnSelector = ".btn.o_clear_button";
|
||||
|
||||
await click(target, '.o_field_widget[name="linked_sale_order_line"] input');
|
||||
let modal = target.querySelector(".modal-dialog.modal-lg");
|
||||
assert.containsNone(modal, clearBtnSelector, "there shouldn't be a Clear button");
|
||||
await click(modal, ".o_form_button_cancel");
|
||||
|
||||
// Select a product
|
||||
await click(target, '.o_field_widget[name="product_id"] input');
|
||||
modal = target.querySelector(".modal-dialog.modal-lg");
|
||||
await click(modal, ".o_kanban_record:nth-child(1)");
|
||||
|
||||
// Remove the product
|
||||
await click(target, '.o_field_widget[name="product_id"] input');
|
||||
modal = target.querySelector(".modal-dialog.modal-lg");
|
||||
assert.containsOnce(modal, clearBtnSelector, "there should be a Clear button");
|
||||
await click(modal, clearBtnSelector);
|
||||
|
||||
await click(target, ".o_form_button_save");
|
||||
});
|
||||
|
||||
QUnit.test("SelectCreateDialog: selection_mode should be true", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
serverData.views["product,false,kanban"] = `
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="o_primary" t-if="!selection_mode">
|
||||
<a type="object" name="some_action">
|
||||
<field name="name"/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="o_primary" t-if="selection_mode">
|
||||
<field name="name"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "sale_order_line",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="product_id"/>
|
||||
<field name="linked_sale_order_line" widget="many2many_tags"/>
|
||||
</form>`,
|
||||
async mockRPC(route, args) {
|
||||
if (args.method === "create" && args.model === "sale_order_line") {
|
||||
const { product_id: selectedId } = args.args[0];
|
||||
assert.strictEqual(selectedId, 111, `the product should be selected`);
|
||||
}
|
||||
if (args.method === "some_action") {
|
||||
assert.step("action should not be called");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, '.o_field_widget[name="product_id"] input');
|
||||
await click(target, ".modal-dialog.modal-lg .o_kanban_record:nth-child(1) .o_primary span");
|
||||
assert.containsNone(target, ".modal-dialog.modal-lg");
|
||||
await click(target, ".o_form_button_save");
|
||||
assert.verifySteps([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/** @odoo-module **/
|
||||
import { click, getFixture, patchWithCleanup, editInput, nextTick } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { SignatureWidget } from "@web/views/widgets/signature/signature";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Widgets", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Name", type: "char" },
|
||||
product_id: {
|
||||
string: "Product Name",
|
||||
type: "many2one",
|
||||
relation: "product",
|
||||
},
|
||||
signature: { string: "", type: "string" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "Pop's Chock'lit",
|
||||
product_id: 7,
|
||||
},
|
||||
],
|
||||
onchanges: {},
|
||||
},
|
||||
product: {
|
||||
fields: {
|
||||
name: { string: "Product Name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 7,
|
||||
display_name: "Veggie Burger",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("Signature Widget");
|
||||
|
||||
QUnit.test("Signature widget works inside of a dropdown", async (assert) => {
|
||||
assert.expect(7);
|
||||
patchWithCleanup(SignatureWidget.prototype, {
|
||||
async onClickSignature() {
|
||||
await this._super.apply(this, arguments);
|
||||
assert.step("onClickSignature");
|
||||
},
|
||||
async uploadSignature({signatureImage}) {
|
||||
await this._super.apply(this, arguments);
|
||||
assert.step("uploadSignature");
|
||||
},
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<header>
|
||||
<button string="Dummy"/>
|
||||
<widget name="signature" string="Sign" full_name="display_name"/>
|
||||
</header>
|
||||
<field name="display_name" />
|
||||
</form>
|
||||
`,
|
||||
mockRPC: async (route, args) => {
|
||||
if (route === "/web/sign/get_fonts/") {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// change display_name to enable auto-sign feature
|
||||
await editInput(target, ".o_field_widget[name=display_name] input", "test");
|
||||
|
||||
// open the signature dialog
|
||||
await click(target, ".o_statusbar_buttons .dropdown-toggle");
|
||||
await click(target, ".o_widget_signature button.o_sign_button");
|
||||
|
||||
assert.containsOnce(target, ".modal-dialog", "Should have one modal opened");
|
||||
|
||||
// use auto-sign feature, might take a while
|
||||
await click(target, ".o_web_sign_auto_button");
|
||||
|
||||
assert.containsOnce(target, ".modal-footer button.btn-primary");
|
||||
|
||||
let maxDelay = 100;
|
||||
while (target.querySelector(".modal-footer button.btn-primary")["disabled"] && maxDelay > 0) {
|
||||
await nextTick();
|
||||
maxDelay--;
|
||||
}
|
||||
|
||||
assert.equal(maxDelay > 0, true, "Timeout exceeded");
|
||||
|
||||
// close the dialog and save the signature
|
||||
await click(target, ".modal-footer button.btn-primary:enabled");
|
||||
|
||||
assert.containsNone(target, ".modal-dialog", "Should have no modal opened");
|
||||
|
||||
assert.verifySteps(["onClickSignature", "uploadSignature"], "An error has occurred while signing");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
/** @odoo-module **/
|
||||
import { click, legacyExtraNextTick } from "@web/../tests/helpers/utils";
|
||||
import {
|
||||
createWebClient,
|
||||
doAction,
|
||||
getActionManagerServerData,
|
||||
} from "@web/../tests/webclient/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { BurgerMenu } from "@web/webclient/burger_menu/burger_menu";
|
||||
import { companyService } from "@web/webclient/company_service";
|
||||
|
||||
/**
|
||||
* Note: The asserts are all based on document.body (instead of getFixture() by example) because
|
||||
* the burger menu is porteled into the dom and is not part of the qunit fixture.
|
||||
*/
|
||||
|
||||
let serverData;
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("Burger Menu", {
|
||||
beforeEach() {
|
||||
serverData = getActionManagerServerData();
|
||||
|
||||
serviceRegistry.add("company", companyService);
|
||||
|
||||
registry.category("systray").add("burger_menu", {
|
||||
Component: BurgerMenu,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("Burger menu can be opened and closed", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
await createWebClient({ serverData });
|
||||
|
||||
await click(document.body, ".o_mobile_menu_toggle");
|
||||
assert.containsOnce(document.body, ".o_burger_menu");
|
||||
|
||||
await click(document.body, ".o_burger_menu_close");
|
||||
assert.containsNone(document.body, ".o_burger_menu");
|
||||
});
|
||||
|
||||
QUnit.test("Burger Menu on an App", async (assert) => {
|
||||
assert.expect(7);
|
||||
|
||||
serverData.menus[1].children = [99];
|
||||
serverData.menus[99] = {
|
||||
id: 99,
|
||||
children: [],
|
||||
name: "SubMenu",
|
||||
appID: 1,
|
||||
actionID: 1002,
|
||||
xmlid: "",
|
||||
webIconData: undefined,
|
||||
webIcon: false,
|
||||
};
|
||||
|
||||
await createWebClient({ serverData });
|
||||
await click(document.body, ".o_navbar_apps_menu .dropdown-toggle");
|
||||
await legacyExtraNextTick();
|
||||
await click(document.body, ".o_app:nth-of-type(2)");
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsNone(document.body, ".o_burger_menu");
|
||||
|
||||
await click(document.body, ".o_mobile_menu_toggle");
|
||||
assert.containsOnce(document.body, ".o_burger_menu");
|
||||
assert.containsOnce(document.body, ".o_burger_menu nav.o_burger_menu_content li");
|
||||
assert.strictEqual(
|
||||
document.body.querySelector(".o_burger_menu nav.o_burger_menu_content li").textContent,
|
||||
"SubMenu"
|
||||
);
|
||||
assert.hasClass(document.body.querySelector(".o_burger_menu_content"), "o_burger_menu_dark");
|
||||
|
||||
await click(document.body, ".o_burger_menu_topbar");
|
||||
assert.doesNotHaveClass(
|
||||
document.body.querySelector(".o_burger_menu_content"),
|
||||
"o_burger_menu_dark"
|
||||
);
|
||||
|
||||
await click(document.body, ".o_burger_menu_topbar");
|
||||
assert.hasClass(document.body.querySelector(".o_burger_menu_content"), "o_burger_menu_dark");
|
||||
});
|
||||
|
||||
QUnit.test("Burger Menu on an App without SubMenu", async (assert) => {
|
||||
assert.expect(4);
|
||||
|
||||
await createWebClient({ serverData });
|
||||
await click(document.body, ".o_navbar_apps_menu .dropdown-toggle");
|
||||
await legacyExtraNextTick();
|
||||
await click(document.body, ".o_app:nth-of-type(2)");
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsNone(document.body, ".o_burger_menu");
|
||||
|
||||
await click(document.body, ".o_mobile_menu_toggle");
|
||||
assert.containsOnce(document.body, ".o_burger_menu");
|
||||
assert.containsOnce(document.body, ".o_user_menu_mobile");
|
||||
await click(document.body, ".o_burger_menu_close");
|
||||
assert.containsNone(document.body, ".o_burger_menu");
|
||||
});
|
||||
|
||||
QUnit.test("Burger menu closes when an action is requested", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
const wc = await createWebClient({ serverData });
|
||||
|
||||
await click(document.body, ".o_mobile_menu_toggle");
|
||||
assert.containsOnce(document.body, ".o_burger_menu");
|
||||
|
||||
await doAction(wc, 1);
|
||||
await legacyExtraNextTick();
|
||||
assert.containsNone(document.body, ".o_burger_menu");
|
||||
assert.containsOnce(document.body, ".o_kanban_view");
|
||||
});
|
||||
|
||||
QUnit.test("Burger menu closes when click on menu item", async (assert) => {
|
||||
serverData.actions[1].target = "new";
|
||||
serverData.menus[1].children = [99];
|
||||
serverData.menus[99] = {
|
||||
id: 99,
|
||||
children: [],
|
||||
name: "SubMenu",
|
||||
appID: 1,
|
||||
actionID: 1,
|
||||
xmlid: "",
|
||||
webIconData: undefined,
|
||||
webIcon: false,
|
||||
};
|
||||
await createWebClient({ serverData });
|
||||
await click(document.body, ".o_navbar_apps_menu .dropdown-toggle");
|
||||
await legacyExtraNextTick();
|
||||
await click(document.body, ".o_app:nth-of-type(2)");
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsNone(document.body, ".o_burger_menu");
|
||||
|
||||
await click(document.body, ".o_mobile_menu_toggle");
|
||||
assert.containsOnce(document.body, ".o_burger_menu");
|
||||
assert.strictEqual(
|
||||
document.body.querySelector(".o_burger_menu nav.o_burger_menu_content li").textContent,
|
||||
"SubMenu"
|
||||
);
|
||||
await click(document.body, ".o_burger_menu nav.o_burger_menu_content li");
|
||||
await legacyExtraNextTick();
|
||||
await legacyExtraNextTick();
|
||||
assert.containsNone(document.body, ".o_burger_menu");
|
||||
});
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { BurgerUserMenu } from "@web/webclient/burger_menu/burger_user_menu/burger_user_menu";
|
||||
import { preferencesItem } from "@web/webclient/user_menu/user_menu_items";
|
||||
import { userService } from "@web/core/user_service";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
|
||||
import { click, getFixture, mount } from "@web/../tests/helpers/utils";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
const userMenuRegistry = registry.category("user_menuitems");
|
||||
let target;
|
||||
let env;
|
||||
|
||||
QUnit.module("BurgerUserMenu", {
|
||||
async beforeEach() {
|
||||
serviceRegistry.add("user", userService);
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
target = getFixture();
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test("can be rendered", async (assert) => {
|
||||
env = await makeTestEnv();
|
||||
userMenuRegistry.add("bad_item", function () {
|
||||
return {
|
||||
type: "item",
|
||||
id: "bad",
|
||||
description: "Bad",
|
||||
callback: () => {
|
||||
assert.step("callback bad_item");
|
||||
},
|
||||
sequence: 10,
|
||||
};
|
||||
});
|
||||
userMenuRegistry.add("ring_item", function () {
|
||||
return {
|
||||
type: "item",
|
||||
id: "ring",
|
||||
description: "Ring",
|
||||
callback: () => {
|
||||
assert.step("callback ring_item");
|
||||
},
|
||||
sequence: 5,
|
||||
};
|
||||
});
|
||||
userMenuRegistry.add("frodo_item", function () {
|
||||
return {
|
||||
type: "switch",
|
||||
id: "frodo",
|
||||
description: "Frodo",
|
||||
callback: () => {
|
||||
assert.step("callback frodo_item");
|
||||
},
|
||||
sequence: 11,
|
||||
};
|
||||
});
|
||||
userMenuRegistry.add("separator", function () {
|
||||
return {
|
||||
type: "separator",
|
||||
sequence: 15,
|
||||
};
|
||||
});
|
||||
userMenuRegistry.add("invisible_item", function () {
|
||||
return {
|
||||
type: "item",
|
||||
id: "hidden",
|
||||
description: "Hidden Power",
|
||||
callback: () => {},
|
||||
sequence: 5,
|
||||
hide: true,
|
||||
};
|
||||
});
|
||||
userMenuRegistry.add("eye_item", function () {
|
||||
return {
|
||||
type: "item",
|
||||
id: "eye",
|
||||
description: "Eye",
|
||||
callback: () => {
|
||||
assert.step("callback eye_item");
|
||||
},
|
||||
};
|
||||
});
|
||||
await mount(BurgerUserMenu, target, { env });
|
||||
assert.containsN(target, ".o_user_menu_mobile .dropdown-item", 4);
|
||||
assert.containsOnce(target, ".o_user_menu_mobile .dropdown-item input.form-check-input");
|
||||
assert.containsOnce(target, "div.dropdown-divider");
|
||||
const children = [...(target.querySelector(".o_user_menu_mobile").children || [])];
|
||||
assert.deepEqual(
|
||||
children.map((el) => el.tagName),
|
||||
["A", "A", "DIV", "DIV", "A"]
|
||||
);
|
||||
const items = [...target.querySelectorAll(".dropdown-item")] || [];
|
||||
assert.deepEqual(
|
||||
items.map((el) => el.textContent),
|
||||
["Ring", "Bad", "Frodo", "Eye"]
|
||||
);
|
||||
for (const item of items) {
|
||||
click(item);
|
||||
}
|
||||
assert.verifySteps([
|
||||
"callback ring_item",
|
||||
"callback bad_item",
|
||||
"callback frodo_item",
|
||||
"callback eye_item",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("can execute the callback of settings", async (assert) => {
|
||||
const mockRPC = (route) => {
|
||||
if (route === "/web/dataset/call_kw/res.users/action_get") {
|
||||
return Promise.resolve({
|
||||
name: "Change My Preferences",
|
||||
res_id: 0,
|
||||
});
|
||||
}
|
||||
};
|
||||
const testConfig = { mockRPC };
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
serviceRegistry.add("orm", ormService);
|
||||
const fakeActionService = {
|
||||
name: "action",
|
||||
start() {
|
||||
return {
|
||||
doAction(actionId) {
|
||||
assert.step("" + actionId.res_id);
|
||||
assert.step(actionId.name);
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("action", fakeActionService, { force: true });
|
||||
|
||||
env = await makeTestEnv(testConfig);
|
||||
userMenuRegistry.add("profile", preferencesItem);
|
||||
await mount(BurgerUserMenu, target, { env });
|
||||
assert.containsOnce(target, ".o_user_menu_mobile .dropdown-item");
|
||||
const item = target.querySelector(".o_user_menu_mobile .dropdown-item");
|
||||
assert.strictEqual(item.textContent, "Preferences");
|
||||
await click(item);
|
||||
assert.verifySteps(["7", "Change My Preferences"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { getFixture, mockTimeout, nextTick } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { swipeLeft, swipeRight } from "@web/../tests/mobile/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { EventBus } from "@odoo/owl";
|
||||
|
||||
let serverData, target;
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
QUnit.module("Mobile SettingsFormView", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
project: {
|
||||
fields: {
|
||||
foo: { string: "Foo", type: "boolean" },
|
||||
bar: { string: "Bar", type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
target = getFixture();
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
QUnit.module("BaseSettings Mobile");
|
||||
|
||||
QUnit.test("swipe settings in mobile [REQUIRE TOUCHEVENT]", async function (assert) {
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
serviceRegistry.add("ui", {
|
||||
start(env) {
|
||||
Object.defineProperty(env, "isSmall", {
|
||||
value: true,
|
||||
});
|
||||
return {
|
||||
bus: new EventBus(),
|
||||
size: 0,
|
||||
isSmall: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "project",
|
||||
serverData,
|
||||
arch: `
|
||||
<form string="Settings" class="oe_form_configuration o_base_settings" js_class="base_settings">
|
||||
<div class="o_setting_container">
|
||||
<div class="settings">
|
||||
<div class="app_settings_block" string="CRM" data-key="crm">
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="bar"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="bar"/>
|
||||
<div class="text-muted">this is bar</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app_settings_block" string="Project" data-key="project">
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="foo"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="foo"/>
|
||||
<div class="text-muted">this is foo</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await swipeLeft(target, ".settings");
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".selected"),
|
||||
"data-key",
|
||||
"project",
|
||||
"current setting should be project"
|
||||
);
|
||||
|
||||
await swipeRight(target, ".settings");
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".selected"),
|
||||
"data-key",
|
||||
"crm",
|
||||
"current setting should be crm"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"swipe settings on larger screen sizes has no effect [REQUIRE TOUCHEVENT]",
|
||||
async function (assert) {
|
||||
const { execRegisteredTimeouts } = mockTimeout();
|
||||
serviceRegistry.add("ui", {
|
||||
start(env) {
|
||||
Object.defineProperty(env, "isSmall", {
|
||||
value: false,
|
||||
});
|
||||
return {
|
||||
bus: new EventBus(),
|
||||
size: 9,
|
||||
isSmall: false,
|
||||
};
|
||||
},
|
||||
});
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "project",
|
||||
serverData,
|
||||
arch: `
|
||||
<form string="Settings" class="oe_form_configuration o_base_settings" js_class="base_settings">
|
||||
<div class="o_setting_container">
|
||||
<div class="settings">
|
||||
<div class="app_settings_block" string="CRM" data-key="crm">
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="bar"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="bar"/>
|
||||
<div class="text-muted">this is bar</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app_settings_block" string="Project" data-key="project">
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="foo"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="foo"/>
|
||||
<div class="text-muted">this is foo</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await swipeLeft(target, ".settings");
|
||||
execRegisteredTimeouts();
|
||||
await nextTick();
|
||||
assert.hasAttrValue(
|
||||
target.querySelector(".selected"),
|
||||
"data-key",
|
||||
"crm",
|
||||
"current setting should still be crm"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue