vanilla 19.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:49:46 +02:00
parent 991d2234ca
commit d1963a3c3a
3066 changed files with 1651266 additions and 922560 deletions

View file

@ -0,0 +1,293 @@
import {
contains,
defineModels,
fields,
models,
mountView,
onRpc,
stepAllNetworkCalls,
} from "../web_test_helpers";
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { queryAllTexts } from "@odoo/hoot-dom";
import { registry } from "@web/core/registry";
/** Foo is dummy model to test `action.report` with domain of its field `value`. **/
class Foo extends models.Model {
_name = "foo";
value = fields.Boolean();
_records = [
{
id: 1,
value: true,
},
{
id: 2,
value: false,
},
];
}
class IrActionsReport extends models.Model {
_name = "ir.actions.report";
get_valid_action_reports(self, model, recordIds) {
const validActionIds = [1];
if (recordIds.includes(1)) {
validActionIds.push(2);
}
if (recordIds.includes(2)) {
validActionIds.push(3);
}
if (!recordIds.includes(1) && !recordIds.includes(2)) {
// new record are initialized with value=False so domain of action 3 is satisfied
validActionIds.push(3);
}
return validActionIds;
}
}
defineModels([Foo, IrActionsReport]);
describe.current.tags("desktop");
beforeEach(() => {
onRpc("has_group", () => true);
});
const printItems = [
{
id: 1,
name: "Some Report always visible",
type: "ir.actions.action_report",
domain: "",
},
{
id: 2,
name: "Some Report with domain 1",
type: "ir.actions.action_report",
domain: [["value", "=", "True"]],
},
{
id: 3,
name: "Some Report with domain 2",
type: "ir.actions.action_report",
domain: [["value", "=", "False"]],
},
];
test("render ActionMenus in list view", async () => {
stepAllNetworkCalls();
await mountView({
type: "list",
resModel: "foo",
actionMenus: {
action: [],
print: printItems,
},
loadActionMenus: true,
arch: /* xml */ `
<list>
<field name="value"/>
</list>
`,
});
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_search_read",
"has_group",
]);
// select all records
await contains(`thead .o_list_record_selector input`).click();
expect(`div.o_control_panel .o_cp_action_menus`).toHaveCount(1);
expect(queryAllTexts(`div.o_control_panel .o_cp_action_menus .dropdown-toggle`)).toEqual([
"Print",
"Actions",
]);
// select Print dropdown
await contains(`.o_cp_action_menus .dropdown-toggle:eq(0)`).click();
expect(`.o-dropdown--menu .o-dropdown-item`).toHaveCount(3);
expect(queryAllTexts(`.o-dropdown--menu .o-dropdown-item`)).toEqual([
"Some Report always visible",
"Some Report with domain 1",
"Some Report with domain 2",
]);
// the last RPC call to retrieve print items only happens when the dropdown is clicked
expect.verifySteps(["get_valid_action_reports"]);
// select only the record that satisfies domain 1
await contains(`.o_data_row:eq(1) input`).click();
await contains(`.o_cp_action_menus .dropdown-toggle:eq(0)`).click();
expect(`.o-dropdown--menu .o-dropdown-item`).toHaveCount(2);
expect(queryAllTexts(`.o-dropdown--menu .o-dropdown-item`)).toEqual([
"Some Report always visible",
"Some Report with domain 1",
]);
expect.verifySteps(["get_valid_action_reports"]);
});
test("render ActionMenus in form view", async () => {
stepAllNetworkCalls();
await mountView({
type: "form",
resModel: "foo",
resId: 1,
actionMenus: {
action: [],
print: printItems,
},
loadActionMenus: true,
arch: /* xml */ `
<form>
<field name="value"/>
</form>
`,
});
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_read",
]);
// select CogMenu
await contains(`div.o_control_panel_breadcrumbs_actions i.fa-cog`).click();
// select Print dropdown
await contains(`button.o-dropdown:contains(Print)`).click();
expect(queryAllTexts(`.o-dropdown--menu-submenu span.o-dropdown-item`)).toEqual([
"Some Report always visible",
"Some Report with domain 1",
]);
// the RPC call to retrieve print items only happens when the dropdown is clicked
expect.verifySteps(["get_valid_action_reports"]);
// create a new record
await contains(`button.o_form_button_create`).click();
await contains(`button.o_form_button_save`).click();
expect(`.o_pager_counter`).toHaveText("2 / 2");
expect.verifySteps(["onchange", "web_save"]);
await contains(`div.o_control_panel_breadcrumbs_actions i.fa-cog`).click();
await contains(`button.o-dropdown:contains(Print)`).click();
expect(queryAllTexts(`.o-dropdown--menu-submenu span.o-dropdown-item`)).toEqual([
"Some Report always visible",
"Some Report with domain 2",
]);
expect.verifySteps(["get_valid_action_reports"]);
// switch back to first record
await contains(`.o_pager_previous`).click();
expect(`.o_pager_counter`).toHaveText("1 / 2");
await contains(`div.o_control_panel_breadcrumbs_actions i.fa-cog`).click();
await contains(`button.o-dropdown:contains(Print)`).click();
expect(queryAllTexts(`.o-dropdown--menu-submenu span.o-dropdown-item`)).toEqual([
"Some Report always visible",
"Some Report with domain 1",
]);
expect.verifySteps(["web_read", "get_valid_action_reports"]);
});
test("render ActionMenus in list view with extraPrintItems", async () => {
stepAllNetworkCalls();
const listView = registry.category("views").get("list");
class ExtraPrintController extends listView.Controller {
get actionMenuProps() {
return {
...super.actionMenuProps,
loadExtraPrintItems: () => [
{
key: "extra_print_key",
description: "Extra Print Item",
class: "o_menu_item",
},
],
};
}
}
registry.category("views").add("extra_print", {
...listView,
Controller: ExtraPrintController,
});
await mountView({
resModel: "foo",
type: "list",
arch: `<list js_class="extra_print"><field name="value"/></list>`,
actionMenus: {
action: [],
print: printItems,
},
});
expect.verifySteps([
"/web/webclient/translations",
"/web/webclient/load_menus",
"get_views",
"web_search_read",
"has_group",
]);
// select all records
await contains(`thead .o_list_record_selector input`).click();
expect(`div.o_control_panel .o_cp_action_menus`).toHaveCount(1);
expect(queryAllTexts(`div.o_control_panel .o_cp_action_menus .dropdown-toggle`)).toEqual([
"Print",
"Actions",
]);
// select Print dropdown
await contains(`.o_cp_action_menus .dropdown-toggle:eq(0)`).click();
expect(`.o-dropdown--menu .o-dropdown-item`).toHaveCount(4);
expect(queryAllTexts(`.o-dropdown--menu .o-dropdown-item`)).toEqual([
"Extra Print Item",
"Some Report always visible",
"Some Report with domain 1",
"Some Report with domain 2",
]);
// the last RPC call to retrieve print items only happens when the dropdown is clicked
expect.verifySteps(["get_valid_action_reports"]);
// select only the record that satisfies domain 1
await contains(`.o_data_row:eq(1) input`).click();
await contains(`.o_cp_action_menus .dropdown-toggle:eq(0)`).click();
expect(`.o-dropdown--menu .o-dropdown-item`).toHaveCount(3);
expect(queryAllTexts(`.o-dropdown--menu .o-dropdown-item`)).toEqual([
"Extra Print Item",
"Some Report always visible",
"Some Report with domain 1",
]);
expect.verifySteps(["get_valid_action_reports"]);
});
test("static action items are properly ordered and styled", async () => {
await mountView({
type: "list",
resModel: "foo",
actionMenus: {
action: [],
print: [],
},
loadActionMenus: true,
arch: /* xml */ `
<list>
<field name="value"/>
</list>
`,
});
// select all records
await contains(`thead .o_list_record_selector input`).click();
expect(`div.o_control_panel .o_cp_action_menus .dropdown-toggle`).toHaveCount(1);
await contains(`div.o_control_panel .o_cp_action_menus .dropdown-toggle`).click();
expect(queryAllTexts(`.o_menu_item`)).toEqual(["Export", "Duplicate", "Delete"]);
expect(`.o_menu_item:last`).toHaveClass("text-danger");
});

View file

@ -1,136 +0,0 @@
/** @odoo-module **/
import { getFixture, patchDate, patchWithCleanup } from "@web/../tests/helpers/utils";
import { browser } from "@web/core/browser/browser";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import {
getFacetTexts,
makeWithSearch,
removeFacet,
setupControlPanelServiceRegistry,
toggleComparisonMenu,
toggleFilterMenu,
toggleMenuItem,
toggleMenuItemOption,
} from "./helpers";
let target;
let serverData;
QUnit.module("Search", (hooks) => {
hooks.beforeEach(async () => {
serverData = {
models: {
foo: {
fields: {
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
date_field: { string: "Date", type: "date", store: true, sortable: true },
},
},
},
views: {
"foo,false,search": `
<search>
<filter name="birthday" date="birthday"/>
<filter name="date_field" date="date_field"/>
</search>
`,
},
};
setupControlPanelServiceRegistry();
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout: () => {},
});
target = getFixture();
});
QUnit.module("Comparison");
QUnit.test("simple rendering", async function (assert) {
patchDate(1997, 0, 9, 12, 0, 0);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["filter", "comparison"],
searchViewId: false,
});
assert.containsOnce(target, ".dropdown.o_filter_menu");
assert.containsNone(target, ".dropdown.o_comparison_menu");
await toggleFilterMenu(target);
await toggleMenuItem(target, "Birthday");
await toggleMenuItemOption(target, "Birthday", "January");
assert.containsOnce(target, "div.o_comparison_menu > button i.fa.fa-adjust");
assert.strictEqual(
target
.querySelector("div.o_comparison_menu > button span")
.innerText.trim()
.toUpperCase() /** @todo why do I need to upperCase */,
"COMPARISON"
);
await toggleComparisonMenu(target);
assert.containsN(target, ".o_comparison_menu .dropdown-item", 2);
assert.containsN(target, ".o_comparison_menu .dropdown-item[role=menuitemcheckbox]", 2);
const comparisonOptions = [...target.querySelectorAll(".o_comparison_menu .dropdown-item")];
assert.deepEqual(
comparisonOptions.map((e) => e.innerText.trim()),
["Birthday: Previous Period", "Birthday: Previous Year"]
);
assert.deepEqual(
comparisonOptions.map((e) => e.ariaChecked),
["false", "false"]
);
});
QUnit.test("activate a comparison works", async function (assert) {
patchDate(1997, 0, 9, 12, 0, 0);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["filter", "comparison"],
searchViewId: false,
});
await toggleFilterMenu(target);
await toggleMenuItem(target, "Birthday");
await toggleMenuItemOption(target, "Birthday", "January");
await toggleComparisonMenu(target);
await toggleMenuItem(target, "Birthday: Previous Period");
assert.deepEqual(getFacetTexts(target), [
"Birthday: January 1997",
"Birthday: Previous Period",
]);
await toggleFilterMenu(target);
await toggleMenuItem(target, "Date");
await toggleMenuItemOption(target, "Date", "December");
await toggleComparisonMenu(target);
await toggleMenuItem(target, "Date: Previous Year");
assert.deepEqual(getFacetTexts(target), [
["Birthday: January 1997", "Date: December 1996"].join("or"),
"Date: Previous Year",
]);
await toggleFilterMenu(target);
await toggleMenuItem(target, "Date");
await toggleMenuItemOption(target, "Date", "1996");
assert.deepEqual(getFacetTexts(target), ["Birthday: January 1997"]);
await toggleComparisonMenu(target);
await toggleMenuItem(target, "Birthday: Previous Year");
assert.containsN(target, ".o_comparison_menu .dropdown-item", 2);
assert.containsN(target, ".o_comparison_menu .dropdown-item[role=menuitemcheckbox]", 2);
const comparisonOptions = [...target.querySelectorAll(".o_comparison_menu .dropdown-item")];
assert.deepEqual(
comparisonOptions.map((e) => e.innerText.trim()),
["Birthday: Previous Period", "Birthday: Previous Year"]
);
assert.deepEqual(
comparisonOptions.map((e) => e.ariaChecked),
["false", "true"]
);
assert.deepEqual(getFacetTexts(target), [
"Birthday: January 1997",
"Birthday: Previous Year",
]);
await removeFacet(target);
assert.deepEqual(getFacetTexts(target), []);
});
});

View file

@ -1,143 +0,0 @@
/** @odoo-module **/
import { click, getFixture, nextTick } from "@web/../tests/helpers/utils";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { makeWithSearch, setupControlPanelServiceRegistry } from "./helpers";
let target;
let serverData;
QUnit.module("Search", (hooks) => {
hooks.beforeEach(async () => {
serverData = {
models: {
foo: {
fields: {},
},
},
views: {
"foo,false,search": `<search/>`,
},
};
setupControlPanelServiceRegistry();
target = getFixture();
});
QUnit.module("ControlPanel");
QUnit.test("simple rendering", async (assert) => {
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
componentProps: {
display: {
"top-right": false,
},
},
searchMenuTypes: [],
});
assert.containsOnce(target, ".o_cp_top");
assert.containsOnce(target, ".o_cp_top_left");
assert.strictEqual(target.querySelector(".o_cp_top_right").innerHTML, "");
assert.containsOnce(target, ".o_cp_bottom");
assert.containsOnce(target, ".o_cp_bottom_left");
assert.containsOnce(target, ".o_cp_bottom_right");
assert.containsNone(target, ".o_cp_switch_buttons");
assert.containsOnce(target, ".breadcrumb");
});
QUnit.test("breadcrumbs", async (assert) => {
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
config: {
breadcrumbs: [
{ jsId: "controller_7", name: "Previous" },
{ jsId: "controller_9", name: "Current" },
],
},
searchMenuTypes: [],
});
assert.containsN(target, ".breadcrumb li.breadcrumb-item", 2);
const breadcrumbItems = target.querySelectorAll("li.breadcrumb-item");
assert.strictEqual(breadcrumbItems[0].innerText, "Previous");
assert.hasClass(breadcrumbItems[1], "active");
assert.strictEqual(breadcrumbItems[1].innerText, "Current");
controlPanel.env.services.action.restore = (jsId) => {
assert.step(jsId);
};
await click(breadcrumbItems[0]);
assert.verifySteps(["controller_7"]);
});
QUnit.test("view switcher", async (assert) => {
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
config: {
viewSwitcherEntries: [
{
type: "list",
active: true,
icon: "oi-view-list",
name: "List",
accessKey: "l",
},
{ type: "kanban", icon: "oi-view-kanban", name: "Kanban", accessKey: "k" },
],
},
searchMenuTypes: [],
});
assert.containsOnce(target, ".o_cp_switch_buttons");
assert.containsN(target, ".o_switch_view", 2);
const views = target.querySelectorAll(".o_switch_view");
assert.strictEqual(views[0].getAttribute("data-tooltip"), "List");
assert.strictEqual(views[0].getAttribute("data-hotkey"), "l");
assert.hasClass(views[0], "active");
assert.strictEqual(views[1].getAttribute("data-tooltip"), "Kanban");
assert.strictEqual(views[1].getAttribute("data-hotkey"), "k");
assert.hasClass(views[1], "oi-view-kanban");
controlPanel.env.services.action.switchView = (viewType) => {
assert.step(viewType);
};
await click(views[1]);
assert.verifySteps(["kanban"]);
});
QUnit.test("pager", async (assert) => {
const pagerProps = {
offset: 0,
limit: 10,
total: 50,
onUpdate: () => {},
};
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
config: {
pagerProps: pagerProps,
},
searchMenuTypes: [],
});
assert.containsOnce(target, ".o_pager");
pagerProps.total = 0;
controlPanel.render();
await nextTick();
assert.containsNone(target, ".o_pager");
});
});

View file

@ -0,0 +1,295 @@
import { expect, test, getFixture } from "@odoo/hoot";
import { click, press, keyDown, keyUp, queryAll, queryFirst } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { reactive } from "@odoo/owl";
import {
contains,
defineModels,
fields,
getService,
models,
mountWithCleanup,
mountWithSearch,
onRpc,
} from "@web/../tests/web_test_helpers";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { WebClient } from "@web/webclient/webclient";
class Foo extends models.Model {
_views = {
kanban: `<kanban><t t-name="card"></t></kanban>`,
};
}
defineModels([Foo]);
test("simple rendering", async () => {
await mountWithSearch(ControlPanel, { resModel: "foo" });
expect(`.o_control_panel_breadcrumbs`).toHaveCount(1);
expect(`.o_control_panel_actions`).toHaveCount(1);
expect(`.o_control_panel_actions > *`).toHaveCount(0);
expect(`.o_control_panel_navigation`).toHaveCount(1);
expect(`.o_control_panel_navigation > *`).toHaveCount(0);
expect(`.o_cp_switch_buttons`).toHaveCount(0);
expect(`.o_breadcrumb`).toHaveCount(1);
});
test.tags("desktop");
test("breadcrumbs", async () => {
await mountWithSearch(
ControlPanel,
{ resModel: "foo" },
{
breadcrumbs: [
{
jsId: "controller_7",
name: "Previous",
onSelected: () => expect.step("controller_7"),
},
{
jsId: "controller_9",
name: "Current",
onSelected: () => expect.step("controller_9"),
},
],
}
);
const breadcrumbItems = queryAll(`.o_breadcrumb li.breadcrumb-item, .o_breadcrumb .active`);
expect(breadcrumbItems).toHaveCount(2);
expect(breadcrumbItems[0]).toHaveText("Previous");
expect(breadcrumbItems[1]).toHaveText("Current");
expect(breadcrumbItems[1]).toHaveClass("active");
await click(breadcrumbItems[0]);
expect.verifySteps(["controller_7"]);
});
test.tags("desktop");
test("view switcher", async () => {
await mountWithSearch(
ControlPanel,
{ resModel: "foo" },
{
viewSwitcherEntries: [
{ type: "list", active: true, icon: "oi-view-list", name: "List" },
{ type: "kanban", icon: "oi-view-kanban", name: "Kanban" },
],
}
);
expect(`.o_control_panel_navigation .o_cp_switch_buttons`).toHaveCount(1);
expect(`.o_switch_view`).toHaveCount(2);
const views = queryAll`.o_switch_view`;
expect(views[0]).toHaveAttribute("data-tooltip", "List");
expect(views[0]).toHaveClass("active");
expect(`.o_switch_view:eq(0) .oi-view-list`).toHaveCount(1);
expect(views[1]).toHaveAttribute("data-tooltip", "Kanban");
expect(views[1]).not.toHaveClass("active");
expect(`.o_switch_view:eq(1) .oi-view-kanban`).toHaveCount(1);
getService("action").switchView = (viewType) => expect.step(viewType);
await click(views[1]);
expect.verifySteps(["kanban"]);
});
test.tags("desktop");
test("view switcher (middle click)", async () => {
await mountWithSearch(
ControlPanel,
{ resModel: "foo" },
{
viewSwitcherEntries: [
{ type: "list", active: true, icon: "oi-view-list", name: "List" },
{ type: "kanban", icon: "oi-view-kanban", name: "Kanban" },
],
}
);
expect(`.o_control_panel_navigation .o_cp_switch_buttons`).toHaveCount(1);
expect(`.o_switch_view`).toHaveCount(2);
getService("action").switchView = (viewType, props, options) =>
expect.step(`${viewType} -- ${JSON.stringify(props)} -- ${JSON.stringify(options)}`);
await contains(".o_switch_view.o_kanban").click({ ctrlKey: true });
expect.verifySteps([`kanban -- {} -- {"newWindow":true}`]);
});
test.tags("desktop");
test("views aria labels", async () => {
await mountWithSearch(
ControlPanel,
{ resModel: "foo" },
{
viewSwitcherEntries: [
{ type: "list", active: true, icon: "oi-view-list", name: "List" },
{ type: "kanban", icon: "oi-view-kanban", name: "Kanban" },
],
}
);
const views = queryAll`.o_switch_view`;
expect(views[0]).toHaveAttribute("aria-label", "List View");
expect(views[1]).toHaveAttribute("aria-label", "Kanban View");
});
test.tags("mobile");
test("view switcher on mobile", async () => {
await mountWithSearch(
ControlPanel,
{ resModel: "foo" },
{
viewSwitcherEntries: [
{ type: "list", active: true, icon: "oi-view-list", name: "List" },
{ type: "kanban", icon: "oi-view-kanban", name: "Kanban" },
],
}
);
expect(`.o_control_panel_navigation .o_cp_switch_buttons`).toHaveCount(1);
await click(".o_control_panel_navigation .o_cp_switch_buttons .dropdown-toggle");
await animationFrame();
expect(`.dropdown-item`).toHaveCount(2);
const views = queryAll`.dropdown-item`;
expect(views[0]).toHaveText("List");
expect(views[0]).toHaveClass("selected");
expect(queryAll(`.oi-view-list`, { root: views[0] })).toHaveCount(1);
expect(views[1]).toHaveText("Kanban");
expect(views[1]).not.toHaveClass("selected");
expect(queryAll(`.oi-view-kanban`, { root: views[1] })).toHaveCount(1);
getService("action").switchView = (viewType) => expect.step(viewType);
await click(views[1]);
expect.verifySteps(["kanban"]);
});
test("pager", async () => {
const pagerProps = reactive({
offset: 0,
limit: 10,
total: 50,
onUpdate: () => {},
});
await mountWithSearch(ControlPanel, { resModel: "foo" }, { pagerProps });
expect(`.o_pager`).toHaveCount(1);
pagerProps.total = 0;
await animationFrame();
expect(`.o_pager`).toHaveCount(0);
});
test("view switcher hotkey cycles through views", async () => {
onRpc("has_group", () => true);
await mountWithCleanup(WebClient);
await getService("action").doAction({
res_model: "foo",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "kanban"],
],
});
expect(`.o_list_view`).toHaveCount(1);
await press(["alt", "shift", "v"]);
await animationFrame();
expect(`.o_kanban_view`).toHaveCount(1);
await press(["alt", "shift", "v"]);
await animationFrame();
expect(`.o_list_view`).toHaveCount(1);
});
test.tags("desktop");
test("hotkey overlay not overlapped by active view button", async () => {
onRpc("has_group", () => true);
await mountWithCleanup(WebClient);
await getService("action").doAction({
res_model: "foo",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "kanban"],
],
});
await keyDown("alt");
expect(`.o_cp_switch_buttons .o_web_hotkey_overlay`).toHaveCount(1);
expect(`.o_switch_view.active`).toHaveCount(1);
const hotkeyZIndex = Number(
getComputedStyle(queryFirst(`.o_cp_switch_buttons .o_web_hotkey_overlay`)).zIndex
);
const buttonZIndex = Number(getComputedStyle(queryFirst(`.o_switch_view.active`)).zIndex);
expect(hotkeyZIndex).toBeGreaterThan(buttonZIndex);
await keyUp("alt");
expect(`.o_cp_switch_buttons .o_web_hotkey_overlay`).toHaveCount(0);
});
test.tags("desktop");
test("control panel layout buttons in dialog", async () => {
onRpc("has_group", () => true);
Foo._fields.char = fields.Char();
Foo._records = [
{
char: "a",
},
{
char: "b",
},
];
Foo._views["list"] = `<list editable="top"><field name="char"/></list>`;
await mountWithCleanup(WebClient);
await getService("action").doAction({
res_model: "foo",
type: "ir.actions.act_window",
target: "new",
views: [[false, "list"]],
});
expect(`.o_list_view`).toHaveCount(1);
await contains(".o_data_cell").click();
expect(".modal-footer button:visible").toHaveCount(2);
expect(".o_control_panel_main_buttons button").toHaveCount(0, {
message: "layout buttons are not replicated in the control panel when inside a dialog",
});
});
test.tags("mobile");
test("Control panel is shown/hide on top when scrolling", async () => {
await mountWithSearch(
ControlPanel,
{ resModel: "foo" },
{
viewSwitcherEntries: [
{ type: "list", active: true, icon: "oi-view-list", name: "List" },
{ type: "kanban", icon: "oi-view-kanban", name: "Kanban" },
],
}
);
const contentHeight = 200;
const sampleContent = document.createElement("div");
sampleContent.style.minHeight = `${2 * contentHeight}px`;
const target = getFixture();
target.appendChild(sampleContent);
target.style.maxHeight = `${contentHeight}px`;
target.style.overflow = "auto";
target.scrollTo({ top: 50 });
await animationFrame();
expect(".o_control_panel").toHaveClass("o_mobile_sticky", {
message: "control panel becomes sticky when the target is not on top",
});
target.scrollTo({ top: -50 });
await animationFrame();
expect(".o_control_panel").not.toHaveClass("o_mobile_sticky", {
message: "control panel is not sticky anymore",
});
});

View file

@ -0,0 +1,224 @@
import { after, expect, test } from "@odoo/hoot";
import { Component, xml } from "@odoo/owl";
import {
defineModels,
editFavoriteName,
editSearch,
fields,
getFacetTexts,
mockService,
models,
mountWithSearch,
onRpc,
saveFavorite,
saveAndEditFavorite,
toggleSaveFavorite,
toggleSearchBarMenu,
validateSearch,
} from "@web/../tests/web_test_helpers";
import { useSetupAction } from "@web/search/action_hook";
import { SearchBar } from "@web/search/search_bar/search_bar";
import { SearchBarMenu } from "@web/search/search_bar_menu/search_bar_menu";
import { rpcBus } from "@web/core/network/rpc";
class Foo extends models.Model {
bar = fields.Many2one({ relation: "partner" });
birthday = fields.Date();
date_field = fields.Date();
float_field = fields.Float();
foo = fields.Char();
}
class Partner extends models.Model {}
defineModels([Foo, Partner]);
test("simple rendering", async () => {
await mountWithSearch(
SearchBar,
{
resModel: "foo",
searchMenuTypes: ["favorite"],
searchViewId: false,
},
{
getDisplayName: () => "Action Name",
}
);
await toggleSearchBarMenu();
await toggleSaveFavorite();
expect(`.o_add_favorite + .o_accordion_values input[type="text"]`).toHaveValue("Action Name");
expect(`.o_add_favorite + .o_accordion_values input[type="checkbox"]`).toHaveCount(1);
expect(`.o_add_favorite + .o_accordion_values .form-check label`).toHaveText("Default filter");
});
test("save filter", async () => {
class TestComponent extends Component {
static components = { SearchBarMenu };
static template = xml`<div><SearchBarMenu/></div>`;
static props = ["*"];
setup() {
useSetupAction({
getContext: () => ({ someKey: "foo" }),
});
}
}
onRpc("create_filter", ({ args, route }) => {
expect.step(route);
const irFilter = args[0];
expect(irFilter.context).toEqual({ group_by: [], someKey: "foo" });
return [7]; // fake serverSideId
});
await mountWithSearch(TestComponent, {
resModel: "foo",
context: { someOtherKey: "bar" }, // should not end up in filter's context
searchViewId: false,
});
const clearCacheListener = () => expect.step("CLEAR-CACHES");
rpcBus.addEventListener("CLEAR-CACHES", clearCacheListener);
after(() => rpcBus.removeEventListener("CLEAR-CACHES", clearCacheListener));
expect.verifySteps([]);
await toggleSearchBarMenu();
await toggleSaveFavorite();
await editFavoriteName("aaa");
await saveFavorite();
expect.verifySteps(["/web/dataset/call_kw/ir.filters/create_filter", "CLEAR-CACHES"]);
});
test("save and edit filter", async () => {
class TestComponent extends Component {
static components = { SearchBarMenu };
static template = xml`<div><SearchBarMenu/></div>`;
static props = ["*"];
setup() {
useSetupAction({
getContext: () => ({ someKey: "foo" }),
});
}
}
onRpc("create_filter", ({ args, route }) => {
expect.step(route);
const irFilter = args[0];
expect(irFilter.context).toEqual({ group_by: [], someKey: "foo" });
return [7]; // fake serverSideId
});
mockService("action", {
doAction(action) {
expect(action).toEqual({
context: {
form_view_ref: "base.ir_filters_view_edit_form",
},
res_id: 7,
res_model: "ir.filters",
type: "ir.actions.act_window",
views: [[false, "form"]],
});
expect.step("Edit favorite");
},
});
await mountWithSearch(TestComponent, {
resModel: "foo",
context: { someOtherKey: "bar" }, // should not end up in filter's context
searchViewId: false,
});
const clearCacheListener = () => expect.step("CLEAR-CACHES");
rpcBus.addEventListener("CLEAR-CACHES", clearCacheListener);
after(() => rpcBus.removeEventListener("CLEAR-CACHES", clearCacheListener));
expect.verifySteps([]);
await toggleSearchBarMenu();
await toggleSaveFavorite();
await editFavoriteName("aaa");
await saveAndEditFavorite();
expect.verifySteps([
"/web/dataset/call_kw/ir.filters/create_filter",
"CLEAR-CACHES",
"Edit favorite",
]);
});
test("dynamic filters are saved dynamic", async () => {
onRpc("create_filter", ({ args, route }) => {
expect.step(route);
const irFilter = args[0];
expect(irFilter.domain).toBe(
`[("date_field", ">=", (context_today() + relativedelta()).strftime("%Y-%m-%d"))]`
);
return [7]; // fake serverSideId
});
await mountWithSearch(SearchBar, {
resModel: "foo",
context: { search_default_filter: 1 },
searchMenuTypes: ["filter", "favorite"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Filter" name="filter" domain="[('date_field', '>=', (context_today() + relativedelta()).strftime('%Y-%m-%d'))]"/>
</search>
`,
});
expect(getFacetTexts()).toEqual(["Filter"]);
await toggleSearchBarMenu();
await toggleSaveFavorite();
await editFavoriteName("My favorite");
await saveFavorite();
expect(getFacetTexts()).toEqual(["My favorite"]);
expect.verifySteps(["/web/dataset/call_kw/ir.filters/create_filter"]);
});
test("save filters created via autocompletion works", async () => {
onRpc("create_filter", ({ args, route }) => {
expect.step(route);
const irFilter = args[0];
expect(irFilter.domain).toBe(`[("foo", "ilike", "a")]`);
return [7]; // fake serverSideId
});
await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["favorite"],
searchViewId: false,
searchViewArch: `<search><field name="foo"/></search>`,
});
expect(getFacetTexts()).toEqual([]);
await editSearch("a");
await validateSearch();
expect(getFacetTexts()).toEqual(["Foo\na"]);
await toggleSearchBarMenu();
await toggleSaveFavorite();
await editFavoriteName("My favorite");
await saveFavorite();
expect(getFacetTexts()).toEqual(["My favorite"]);
expect.verifySteps(["/web/dataset/call_kw/ir.filters/create_filter"]);
});
test("undefined name for filter shows notification and not error", async () => {
mockService("notification", {
add(message, options) {
expect.step("notification");
expect(message).toBe("A name for your favorite filter is required.");
expect(options).toEqual({ type: "danger" });
},
});
onRpc("create_filter", () => [7]); // fake serverSideId
await mountWithSearch(SearchBarMenu, {
resModel: "foo",
searchViewId: false,
});
await toggleSearchBarMenu();
await toggleSaveFavorite();
await saveFavorite();
expect.verifySteps(["notification"]);
});

View file

@ -1,562 +0,0 @@
/** @odoo-module **/
import { getFixture, patchWithCleanup, triggerEvent } from "@web/../tests/helpers/utils";
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { FavoriteMenu } from "@web/search/favorite_menu/favorite_menu";
import { useSetupAction } from "@web/webclient/actions/action_hook";
import {
editFavoriteName,
editSearch,
getFacetTexts,
makeWithSearch,
saveFavorite,
setupControlPanelFavoriteMenuRegistry,
setupControlPanelServiceRegistry,
toggleFavoriteMenu,
toggleSaveFavorite,
validateSearch,
} from "./helpers";
import { Component, xml } from "@odoo/owl";
const serviceRegistry = registry.category("services");
/**
* @param {HTMLElement} target
*/
async function toggleDefaultCheckBox(target) {
const checkbox = target.querySelector("input[type='checkbox']");
checkbox.checked = !checkbox.checked;
await triggerEvent(checkbox, null, "change");
}
/**
* @param {HTMLElement} target
*/
async function toggleShareCheckBox(target) {
const checkbox = target.querySelectorAll("input[type='checkbox']")[1];
checkbox.checked = !checkbox.checked;
await triggerEvent(checkbox, null, "change");
}
let target;
let serverData;
QUnit.module("Search", (hooks) => {
hooks.beforeEach(async () => {
serverData = {
models: {
foo: {
fields: {
bar: { string: "Bar", type: "many2one", relation: "partner" },
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
date_field: { string: "Date", type: "date", store: true, sortable: true },
float_field: { string: "Float", type: "float", group_operator: "sum" },
foo: { string: "Foo", type: "char", store: true, sortable: true },
},
records: {},
},
},
views: {
"foo,false,search": `<search/>`,
},
};
setupControlPanelFavoriteMenuRegistry();
setupControlPanelServiceRegistry();
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout: () => {},
});
target = getFixture();
});
QUnit.module("CustomFavoriteItem");
QUnit.test("simple rendering", async function (assert) {
assert.expect(3);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["favorite"],
searchViewId: false,
config: {
getDisplayName: () => "Action Name",
},
});
await toggleFavoriteMenu(target);
await toggleSaveFavorite(target);
assert.strictEqual(
target.querySelector('.o_add_favorite input[type="text"]').value,
"Action Name"
);
assert.containsN(target, '.o_add_favorite .form-check input[type="checkbox"]', 2);
const labelEls = target.querySelectorAll(".o_add_favorite .form-check label");
assert.deepEqual(
[...labelEls].map((e) => e.innerText.trim()),
["Use by default", "Share with all users"]
);
});
QUnit.test("favorites use by default and share are exclusive", async function (assert) {
assert.expect(11);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["favorite"],
searchViewId: false,
});
await toggleFavoriteMenu(target);
await toggleSaveFavorite(target);
const checkboxes = target.querySelectorAll('input[type="checkbox"]');
assert.strictEqual(checkboxes.length, 2, "2 checkboxes are present");
assert.notOk(checkboxes[0].checked, "Start: None of the checkboxes are checked (1)");
assert.notOk(checkboxes[1].checked, "Start: None of the checkboxes are checked (2)");
await toggleDefaultCheckBox(target);
assert.ok(checkboxes[0].checked, "The first checkbox is checked");
assert.notOk(checkboxes[1].checked, "The second checkbox is not checked");
await toggleShareCheckBox(target);
assert.notOk(
checkboxes[0].checked,
"Clicking on the second checkbox checks it, and unchecks the first (1)"
);
assert.ok(
checkboxes[1].checked,
"Clicking on the second checkbox checks it, and unchecks the first (2)"
);
await toggleDefaultCheckBox(target);
assert.ok(
checkboxes[0].checked,
"Clicking on the first checkbox checks it, and unchecks the second (1)"
);
assert.notOk(
checkboxes[1].checked,
"Clicking on the first checkbox checks it, and unchecks the second (2)"
);
await toggleDefaultCheckBox(target);
assert.notOk(checkboxes[0].checked, "End: None of the checkboxes are checked (1)");
assert.notOk(checkboxes[1].checked, "End: None of the checkboxes are checked (2)");
});
QUnit.test("save filter", async function (assert) {
assert.expect(4);
class TestComponent extends Component {
setup() {
useSetupAction({
getContext: () => {
return { someKey: "foo" };
},
});
}
}
TestComponent.components = { FavoriteMenu };
TestComponent.template = xml`<div><FavoriteMenu/></div>`;
const comp = await makeWithSearch({
serverData,
mockRPC: (_, args) => {
if (args.model === "ir.filters" && args.method === "create_or_replace") {
const irFilter = args.args[0];
assert.deepEqual(irFilter.context, { group_by: [], someKey: "foo" });
return 7; // fake serverSideId
}
},
resModel: "foo",
context: { someOtherKey: "bar" }, // should not end up in filter's context
Component: TestComponent,
searchViewId: false,
});
comp.env.bus.on("CLEAR-CACHES", comp, () => assert.step("CLEAR-CACHES"));
assert.verifySteps([]);
await toggleFavoriteMenu(target);
await toggleSaveFavorite(target);
await editFavoriteName(target, "aaa");
await saveFavorite(target);
assert.verifySteps(["CLEAR-CACHES"]);
});
QUnit.test("dynamic filters are saved dynamic", async function (assert) {
assert.expect(3);
await makeWithSearch({
serverData,
mockRPC: (_, args) => {
if (args.model === "ir.filters" && args.method === "create_or_replace") {
const irFilter = args.args[0];
assert.deepEqual(
irFilter.domain,
'[("date_field", ">=", (context_today() + relativedelta()).strftime("%Y-%m-%d"))]'
);
return 7; // fake serverSideId
}
},
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["favorite"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Filter" name="filter" domain="[('date_field', '>=', (context_today() + relativedelta()).strftime('%Y-%m-%d'))]"/>
</search>
`,
context: { search_default_filter: 1 },
});
assert.deepEqual(getFacetTexts(target), ["Filter"]);
await toggleFavoriteMenu(target);
await toggleSaveFavorite(target);
await editFavoriteName(target, "My favorite");
await saveFavorite(target);
assert.deepEqual(getFacetTexts(target), ["My favorite"]);
});
QUnit.test("save filters created via autocompletion works", async function (assert) {
assert.expect(4);
await makeWithSearch({
serverData,
mockRPC: (_, args) => {
if (args.model === "ir.filters" && args.method === "create_or_replace") {
const irFilter = args.args[0];
assert.deepEqual(irFilter.domain, '[("foo", "ilike", "a")]');
return 7; // fake serverSideId
}
},
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["favorite"],
searchViewId: false,
searchViewArch: `
<search>
<field name="foo"/>
</search>
`,
});
assert.deepEqual(getFacetTexts(target), []);
await editSearch(target, "a");
await validateSearch(target);
assert.deepEqual(getFacetTexts(target), ["Foo\na"]);
await toggleFavoriteMenu(target);
await toggleSaveFavorite(target);
await editFavoriteName(target, "My favorite");
await saveFavorite(target);
assert.deepEqual(getFacetTexts(target), ["My favorite"]);
});
QUnit.test(
"favorites have unique descriptions (the submenus of the favorite menu are correctly updated)",
async function (assert) {
assert.expect(5);
serviceRegistry.add(
"notification",
{
start() {
return {
add(message, options) {
assert.strictEqual(
message,
"A filter with same name already exists."
);
assert.deepEqual(options, { type: "danger" });
},
};
},
},
{ force: true }
);
await makeWithSearch({
serverData,
mockRPC: (route, args) => {
if (args.model === "ir.filters" && args.method === "create_or_replace") {
const irFilter = args.args[0];
assert.deepEqual(irFilter, {
action_id: false,
context: { group_by: [] },
domain: "[]",
is_default: false,
model_id: "foo",
name: "My favorite 2",
sort: "[]",
user_id: 7,
});
return 2; // serverSideId
}
},
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["favorite"],
searchViewId: false,
irFilters: [
{
context: "{}",
domain: "[]",
id: 1,
is_default: false,
name: "My favorite",
sort: "[]",
user_id: [2, "Mitchell Admin"],
},
],
});
await toggleFavoriteMenu(target);
await toggleSaveFavorite(target);
// first try: should fail
await editFavoriteName(target, "My favorite");
await saveFavorite(target);
// second try: should succeed
await editFavoriteName(target, "My favorite 2");
await saveFavorite(target);
// third try: should fail
await editFavoriteName(target, "My favorite 2");
await saveFavorite(target);
}
);
QUnit.test(
"undefined name for filter shows notification and not error",
async function (assert) {
assert.expect(2);
serviceRegistry.add(
"notification",
{
start() {
return {
add(message, options) {
assert.strictEqual(
message,
"A name for your favorite filter is required.",
"The notification should match: A name for your favorite filter is required."
);
assert.deepEqual(options, { type: "danger" });
},
};
},
},
{ force: true }
);
await makeWithSearch({
serverData,
mockRPC: (_, args) => {
if (args.model === "ir.filters" && args.method === "create_or_replace") {
return 7; // fake serverSideId
}
},
resModel: "foo",
Component: FavoriteMenu,
searchViewId: false,
});
await toggleFavoriteMenu(target);
await toggleSaveFavorite(target);
await saveFavorite(target);
}
);
QUnit.test("add favorite with enter which already exists", async function (assert) {
serviceRegistry.add(
"notification",
{
start() {
return {
add(message, options) {
assert.strictEqual(message, "A filter with same name already exists.");
assert.deepEqual(options, { type: "danger" });
assert.step("warning dialog");
},
};
},
},
{ force: true }
);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["favorite"],
searchViewId: false,
config: {
displayName: "Action Name",
},
irFilters: [
{
context: "{}",
domain: "[]",
id: 1,
is_default: false,
name: "My favorite",
sort: "[]",
user_id: [2, "Mitchell Admin"],
},
],
});
await toggleFavoriteMenu(target);
await toggleSaveFavorite(target);
await editFavoriteName(target, "My favorite");
triggerEvent(
target,
`.o_favorite_menu .o_add_favorite .dropdown-menu input[type="text"]`,
"keydown",
{ key: "Enter" }
);
assert.verifySteps(["warning dialog"]);
});
QUnit.skip("save search filter in modal", async function (assert) {
/** @todo I don't know yet how to convert this test */
// assert.expect(5);
// serverData.models = {
// partner: {
// fields: {
// date_field: {
// string: "Date",
// type: "date",
// store: true,
// sortable: true,
// searchable: true,
// },
// birthday: { string: "Birthday", type: "date", store: true, sortable: true },
// foo: { string: "Foo", type: "char", store: true, sortable: true },
// bar: { string: "Bar", type: "many2one", relation: "partner" },
// float_field: { string: "Float", type: "float", group_operator: "sum" },
// },
// records: [
// {
// id: 1,
// display_name: "First record",
// foo: "yop",
// bar: 2,
// date_field: "2017-01-25",
// birthday: "1983-07-15",
// float_field: 1,
// },
// {
// id: 2,
// display_name: "Second record",
// foo: "blip",
// bar: 1,
// date_field: "2017-01-24",
// birthday: "1982-06-04",
// float_field: 2,
// },
// {
// id: 3,
// display_name: "Third record",
// foo: "gnap",
// bar: 1,
// date_field: "2017-01-13",
// birthday: "1985-09-13",
// float_field: 1.618,
// },
// {
// id: 4,
// display_name: "Fourth record",
// foo: "plop",
// bar: 2,
// date_field: "2017-02-25",
// birthday: "1983-05-05",
// float_field: -1,
// },
// {
// id: 5,
// display_name: "Fifth record",
// foo: "zoup",
// bar: 2,
// date_field: "2016-01-25",
// birthday: "1800-01-01",
// float_field: 13,
// },
// { id: 7, display_name: "Partner 6" },
// { id: 8, display_name: "Partner 7" },
// { id: 9, display_name: "Partner 8" },
// { id: 10, display_name: "Partner 9" },
// ],
// },
// };
// const form = await createView({
// arch: `
// <form string="Partners">
// <sheet>
// <group>
// <field name="bar"/>
// </group>
// </sheet>
// </form>`,
// archs: {
// "partner,false,list": '<tree><field name="display_name"/></tree>',
// "partner,false,search": '<search><field name="date_field"/></search>',
// },
// data,
// model: "partner",
// res_id: 1,
// View: FormView,
// env: {
// dataManager: {
// create_filter(filter) {
// assert.strictEqual(
// filter.name,
// "Awesome Test Customer Filter",
// "filter name should be correct"
// );
// },
// },
// },
// });
// await testUtils.form.clickEdit(form);
// await testUtils.fields.many2one.clickOpenDropdown("bar");
// await testUtils.fields.many2one.clickItem("bar", "Search");
// assert.containsN(document.body, "tr.o_data_row", 9, "should display 9 records");
// await toggleFilterMenu(".modal");
// await toggleAddCustomFilter(".modal");
// assert.strictEqual(
// document.querySelector(".o_filter_condition select.o_generator_menu_field").value,
// "date_field",
// "date field should be selected"
// );
// await applyFilter(".modal");
// assert.containsNone(document.body, "tr.o_data_row", "should display 0 records");
// // Save this search
// await toggleFavoriteMenu(".modal");
// await toggleSaveFavorite(".modal");
// const filterNameInput = document.querySelector('.o_add_favorite input[type="text"]');
// assert.isVisible(filterNameInput, "should display an input field for the filter name");
// await testUtils.fields.editInput(filterNameInput, "Awesome Test Customer Filter");
// await click(document.querySelector(".o_add_favorite button.btn-primary"));
// form.destroy();
});
});

View file

@ -1,863 +0,0 @@
/** @odoo-module **/
import {
click,
getFixture,
patchDate,
patchTimeZone,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import { localization } from "@web/core/l10n/localization";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { browser } from "@web/core/browser/browser";
import {
addCondition,
applyFilter,
editConditionField,
editConditionOperator,
editConditionValue,
getFacetTexts,
isItemSelected,
makeWithSearch,
removeFacet,
setupControlPanelServiceRegistry,
toggleAddCustomFilter,
toggleFilterMenu,
toggleMenuItem,
} from "./helpers";
function getDomain(controlPanel) {
return controlPanel.env.searchModel.domain;
}
let target;
let serverData;
QUnit.module("Search", (hooks) => {
hooks.beforeEach(async () => {
serverData = {
models: {
foo: {
fields: {
date_field: {
name: "date_field",
string: "A date",
type: "date",
searchable: true,
},
date_time_field: {
name: "date_time_field",
string: "DateTime",
type: "datetime",
searchable: true,
},
boolean_field: {
name: "boolean_field",
string: "Boolean Field",
type: "boolean",
default: true,
searchable: true,
},
char_field: {
name: "char_field",
string: "Char Field",
type: "char",
default: "foo",
trim: true,
searchable: true,
},
float_field: {
name: "float_field",
string: "Floaty McFloatface",
type: "float",
digits: [999, 1],
searchable: true,
},
color: {
name: "color",
string: "Color",
type: "selection",
selection: [
["black", "Black"],
["white", "White"],
],
searchable: true,
},
image: {
name: "image",
string: "Binary field",
type: "binary",
searchable: true,
},
json_field: {
name: "json_field",
string: "Jason",
type: "json",
searchable: true,
},
},
records: {},
},
},
views: {
"foo,false,search": `<search/>`,
},
};
setupControlPanelServiceRegistry();
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout: () => {},
});
target = getFixture();
});
QUnit.module("CustomFilterItem");
QUnit.test("basic rendering", async function (assert) {
assert.expect(14);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
});
await toggleFilterMenu(target);
const customFilterItem = target.querySelector(".o_add_custom_filter_menu");
assert.strictEqual(customFilterItem.innerText.trim(), "Add Custom Filter");
await toggleAddCustomFilter(target);
// Single condition
assert.containsOnce(customFilterItem, ".o_filter_condition");
assert.containsOnce(
customFilterItem,
".o_filter_condition > select.o_generator_menu_field"
);
assert.containsOnce(
customFilterItem,
".o_filter_condition > select.o_generator_menu_operator"
);
assert.containsOnce(customFilterItem, ".o_filter_condition > span.o_generator_menu_value");
assert.containsNone(customFilterItem, ".o_filter_condition .o_or_filter");
assert.containsNone(customFilterItem, ".o_filter_condition .o_generator_menu_delete");
// no deletion allowed on single condition
assert.containsNone(customFilterItem, ".o_filter_condition > i.o_generator_menu_delete");
// Buttons
assert.containsOnce(customFilterItem, "button.o_apply_filter");
assert.containsOnce(customFilterItem, "button.o_add_condition");
assert.containsOnce(customFilterItem, ".o_filter_condition");
await click(customFilterItem, "button.o_add_condition");
assert.containsN(customFilterItem, ".o_filter_condition", 2);
assert.containsOnce(customFilterItem, ".o_filter_condition .o_or_filter");
assert.containsN(customFilterItem, ".o_filter_condition .o_generator_menu_delete", 2);
});
QUnit.test(
'should have Date and ID field proposed in that order in "Add custom Filter" submenu',
async function (assert) {
assert.expect(2);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewFields: {
date_field: {
name: "date_field",
string: "Date",
type: "date",
store: true,
sortable: true,
searchable: true,
},
foo: { string: "Foo", type: "char", store: true, sortable: true },
},
});
await toggleFilterMenu(target);
await toggleAddCustomFilter(target);
const optionEls = target.querySelectorAll(
".o_filter_condition > select.o_generator_menu_field option"
);
assert.strictEqual(optionEls[0].innerText.trim(), "Date");
assert.strictEqual(optionEls[1].innerText.trim(), "ID");
}
);
QUnit.test("deactivate a new custom filter works", async function (assert) {
assert.expect(4);
patchDate(2020, 1, 5, 12, 20, 0);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewFields: {
date_field: {
name: "date_field",
string: "Date",
type: "date",
store: true,
sortable: true,
searchable: true,
},
},
});
await toggleFilterMenu(target);
await toggleAddCustomFilter(target);
await applyFilter(target);
assert.ok(isItemSelected(target, 'Date is equal to "02/05/2020"'));
assert.deepEqual(getFacetTexts(target), ['Date is equal to "02/05/2020"']);
await toggleMenuItem(target, 'Date is equal to "02/05/2020"');
assert.notOk(isItemSelected(target, 'Date is equal to "02/05/2020"'));
assert.deepEqual(getFacetTexts(target), []);
});
QUnit.test("custom OR filter presets new condition from preceding", async function (assert) {
assert.expect(4);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
});
await toggleFilterMenu(target);
await toggleAddCustomFilter(target);
// Retrieve second selectable values for field and operator dropdowns
const secondFieldName = target.querySelector(
".o_generator_menu_field option:nth-of-type(2)"
).value;
const secondOperator = target.querySelector(
".o_generator_menu_operator option:nth-of-type(2)"
).value;
// Check if they really exist…
assert.ok(!!secondFieldName);
assert.ok(!!secondOperator);
// Add first filter condition
await editConditionField(target, 0, secondFieldName);
await editConditionOperator(target, 0, secondOperator);
// Add a second conditon on the filter being created
await addCondition(target);
// Check the defaults for field and operator dropdowns
assert.strictEqual(
target.querySelector(".o_filter_condition:nth-of-type(2) .o_generator_menu_field")
.value,
secondFieldName
);
assert.strictEqual(
target.querySelector(".o_filter_condition:nth-of-type(2) .o_generator_menu_operator")
.value,
secondOperator
);
});
QUnit.test("add a custom filter works", async function (assert) {
assert.expect(2);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewFields: {},
});
await toggleFilterMenu(target);
await toggleAddCustomFilter(target);
// choose ID field in 'Add Custom filter' menu and value 1
await editConditionField(target, 0, "id");
await editConditionValue(target, 0, 1);
await applyFilter(target);
assert.deepEqual(getFacetTexts(target), ['ID is "1"']);
assert.deepEqual(getDomain(controlPanel), [["id", "=", 1]]);
});
QUnit.test("adding a simple filter works", async function (assert) {
assert.expect(11);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewFields: {
boolean_field: {
name: "boolean_field",
string: "Boolean Field",
type: "boolean",
default: true,
searchable: true,
},
},
});
await toggleFilterMenu(target);
assert.deepEqual(getFacetTexts(target), []);
assert.deepEqual(getDomain(controlPanel), []);
assert.containsNone(target, ".o_menu_item");
assert.containsOnce(target, ".o_add_custom_filter_menu button.dropdown-toggle");
// the 'Add Custom Filter' menu should be closed;
assert.containsNone(target, ".o_add_custom_filter_menu .dropdown-menu");
await toggleAddCustomFilter(target);
// the 'Add Custom Filter' menu should be open;
assert.containsOnce(target, ".o_add_custom_filter_menu .dropdown-menu");
await applyFilter(target);
assert.deepEqual(getFacetTexts(target), ["Boolean Field is Yes"]);
assert.deepEqual(getDomain(controlPanel), [["boolean_field", "=", true]]);
assert.containsOnce(target, ".o_menu_item");
assert.containsOnce(target, ".o_add_custom_filter_menu button.dropdown-toggle");
// the 'Add Custom Filter' menu should still be opened;
assert.containsOnce(target, ".o_add_custom_filter_menu .dropdown-menu");
});
QUnit.test("binary field is available", async function (assert) {
assert.expect(11);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewFields: {
image: {
name: "image",
string: "Binary Field",
type: "binary",
default: true,
searchable: true,
},
},
});
await toggleFilterMenu(target);
assert.deepEqual(getFacetTexts(target), []);
assert.deepEqual(getDomain(controlPanel), []);
assert.containsNone(target, ".o_menu_item");
assert.containsOnce(target, ".o_add_custom_filter_menu button.dropdown-toggle");
// the 'Add Custom Filter' menu should be closed;
assert.containsNone(target, ".o_add_custom_filter_menu .dropdown-menu");
await toggleAddCustomFilter(target);
// the 'Add Custom Filter' menu should be open;
assert.containsOnce(target, ".o_add_custom_filter_menu .dropdown-menu");
await applyFilter(target);
assert.deepEqual(getFacetTexts(target), ["Binary Field is set"]);
assert.deepEqual(getDomain(controlPanel), [["image", "!=", false]]);
assert.containsOnce(target, ".o_menu_item");
assert.containsOnce(target, ".o_add_custom_filter_menu button.dropdown-toggle");
// the 'Add Custom Filter' menu should still be opened;
assert.containsOnce(target, ".o_add_custom_filter_menu .dropdown-menu");
});
QUnit.test("json field is available", async function (assert) {
assert.expect(10);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewFields: {
json_field: {
name: "json_field",
string: "Json",
type: "json",
default: "{'1': 50}",
searchable: true,
},
},
});
await toggleFilterMenu(target);
assert.deepEqual(getFacetTexts(target), []);
assert.deepEqual(getDomain(controlPanel), []);
assert.containsNone(target, ".o_menu_item");
assert.containsOnce(target, ".o_add_custom_filter_menu button.dropdown-toggle");
// the 'Add Custom Filter' menu should be closed;
assert.containsNone(target, ".o_add_custom_filter_menu .dropdown-menu");
await toggleAddCustomFilter(target);
// the 'Add Custom Filter' menu should be open;
assert.containsOnce(target, ".o_add_custom_filter_menu .dropdown-menu");
await editConditionField(target, 0, "json_field");
await applyFilter(target);
assert.deepEqual(getFacetTexts(target), ['Json contains ""']);
assert.deepEqual(getDomain(controlPanel), [["json_field", "ilike", ""]]);
await removeFacet(target);
await toggleFilterMenu(target);
await toggleAddCustomFilter(target);
await editConditionField(target, 0, "json_field");
await editConditionOperator(target, 0, "!=");
await applyFilter(target);
assert.deepEqual(getFacetTexts(target), ['Json is not equal to ""']);
assert.deepEqual(getDomain(controlPanel), [["json_field", "!=", ""]]);
});
QUnit.test("selection field: default and updated value", async function (assert) {
assert.expect(11);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
});
// Default value
await toggleFilterMenu(target);
assert.containsN(target, ".o_menu_item", 0);
assert.containsN(target, ".dropdown-divider", 0);
await toggleAddCustomFilter(target);
await editConditionField(target, 0, "color");
await applyFilter(target);
assert.deepEqual(getFacetTexts(target), ['Color is "Black"']);
assert.deepEqual(getDomain(controlPanel), [["color", "=", "black"]]);
assert.containsN(target, ".o_menu_item", 1);
assert.containsN(target, ".dropdown-divider", 1);
// deactivate custom filter
await removeFacet(target);
// Updated value
await toggleFilterMenu(target);
await toggleAddCustomFilter(target);
await editConditionField(target, 0, "color");
await editConditionValue(target, 0, "white");
assert.strictEqual(
target.querySelector(".o_generator_menu_value input,.o_generator_menu_value select")
.value,
"white"
);
await applyFilter(target);
assert.deepEqual(getFacetTexts(target), ['Color is "White"']);
assert.deepEqual(getDomain(controlPanel), [["color", "=", "white"]]);
assert.containsN(target, ".o_menu_item", 2);
assert.containsN(target, ".dropdown-divider", 2);
});
QUnit.test(
"commit search with an extended proposition with field char does not cause a crash",
async function (assert) {
assert.expect(12);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewFields: {
many2one_field: {
name: "many2one_field",
string: "AAA",
type: "many2one",
searchable: true,
},
},
});
const steps = [
{
value: `a`,
domain: [["many2one_field", "ilike", `a`]],
facetContent: `AAA contains "a"`,
},
{
value: `"a"`,
domain: [["many2one_field", "ilike", `"a"`]],
facetContent: `AAA contains ""a""`,
},
{
value: `'a'`,
domain: [["many2one_field", "ilike", `'a'`]],
facetContent: `AAA contains "'a'"`,
},
{
value: `'`,
domain: [["many2one_field", "ilike", `'`]],
facetContent: `AAA contains "'"`,
},
{
value: `"`,
domain: [["many2one_field", "ilike", `"`]],
facetContent: `AAA contains """`,
},
{
value: `\\`,
domain: [["many2one_field", "ilike", `\\`]],
facetContent: `AAA contains "\\"`,
},
];
for (const step of steps) {
await toggleFilterMenu(target);
await toggleAddCustomFilter(target);
await editConditionValue(target, 0, step.value);
await applyFilter(target);
assert.deepEqual(getFacetTexts(target), [step.facetContent]);
assert.deepEqual(getDomain(controlPanel), step.domain);
await removeFacet(target);
}
}
);
QUnit.test("custom filter date with equal operator", async function (assert) {
patchTimeZone(-240);
patchDate(2017, 1, 22, 12, 30, 0);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
});
await toggleFilterMenu(target);
await toggleAddCustomFilter(target);
await editConditionField(target, 0, "date_field");
await editConditionOperator(target, 0, "=");
await editConditionValue(target, 0, "01/01/2017");
await applyFilter(target);
assert.deepEqual(getFacetTexts(target), ['A date is equal to "01/01/2017"']);
assert.deepEqual(getDomain(controlPanel), [["date_field", "=", "2017-01-01"]]);
});
QUnit.test("custom filter datetime with equal operator", async function (assert) {
assert.expect(5);
patchTimeZone(-240);
patchDate(2017, 1, 22, 12, 30, 0);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
});
await toggleFilterMenu(target);
await toggleAddCustomFilter(target);
await editConditionField(target, 0, "date_time_field");
assert.strictEqual(
target.querySelector(".o_generator_menu_field").value,
"date_time_field"
);
assert.strictEqual(target.querySelector(".o_generator_menu_operator").value, "between");
assert.deepEqual(
[...target.querySelectorAll(".o_generator_menu_value input[type=text]")].map(
(v) => v.value
),
["02/22/2017 00:00:00", "02/22/2017 23:59:59"]
);
await editConditionOperator(target, 0, "=");
await editConditionValue(target, 0, "02/22/2017 11:00:00"); // in TZ
await applyFilter(target);
assert.deepEqual(
getFacetTexts(target),
['DateTime is equal to "02/22/2017 11:00:00"'],
"description should be in localized format"
);
assert.deepEqual(
getDomain(controlPanel),
[["date_time_field", "=", "2017-02-22 15:00:00"]],
"domain should be in UTC format"
);
});
QUnit.test("custom filter datetime between operator", async function (assert) {
assert.expect(5);
patchTimeZone(-240);
patchDate(2017, 1, 22, 12, 30, 0);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
});
await toggleFilterMenu(target);
await toggleAddCustomFilter(target);
await editConditionField(target, 0, "date_time_field");
assert.strictEqual(
target.querySelector(".o_generator_menu_field").value,
"date_time_field"
);
assert.strictEqual(target.querySelector(".o_generator_menu_operator").value, "between");
assert.deepEqual(
[...target.querySelectorAll(".o_generator_menu_value input[type=text]")].map(
(v) => v.value
),
["02/22/2017 00:00:00", "02/22/2017 23:59:59"]
);
await editConditionValue(target, 0, "02/22/2017 11:00:00", 0); // in TZ
await editConditionValue(target, 0, "02-22-2017 17:00:00", 1); // in TZ
await applyFilter(target);
assert.deepEqual(
getFacetTexts(target),
['DateTime is between "02/22/2017 11:00:00 and 02/22/2017 17:00:00"'],
"description should be in localized format"
);
assert.deepEqual(
getDomain(controlPanel),
[
"&",
["date_time_field", ">=", "2017-02-22 15:00:00"],
["date_time_field", "<=", "2017-02-22 21:00:00"],
],
"domain should be in UTC format"
);
});
QUnit.test("input value parsing", async function (assert) {
assert.expect(7);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
});
await toggleFilterMenu(target);
await toggleAddCustomFilter(target);
await addCondition(target);
await editConditionField(target, 0, "float_field");
await editConditionField(target, 1, "id");
const [floatInput, idInput] = target.querySelectorAll(".o_generator_menu_value .o_input");
// Default values
await editConditionValue(target, 0, "0.0");
assert.strictEqual(floatInput.value, "0.0");
await editConditionValue(target, 1, "0");
assert.strictEqual(idInput.value, "0");
// Float parsing
await editConditionValue(target, 0, "4.2");
assert.strictEqual(floatInput.value, "4.2");
await editConditionValue(target, 0, "DefinitelyValidFloat");
// "DefinitelyValidFloat" cannot be entered in a input type number so that the input value is reset to 0
assert.strictEqual(floatInput.value, "4.2");
// Number parsing
await editConditionValue(target, 1, "4");
assert.strictEqual(idInput.value, "4");
await editConditionValue(target, 1, "4.2");
assert.strictEqual(idInput.value, "4");
await editConditionValue(target, 1, "DefinitelyValidID");
// "DefinitelyValidID" cannot be entered in a input type number so that the input value is reset to 0
assert.strictEqual(idInput.value, "0");
});
QUnit.test("input value parsing with language", async function (assert) {
assert.expect(5);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
});
// Needs to be done after services have been started
patchWithCleanup(localization, {
decimalPoint: ",",
thousandsSep: "",
grouping: [3, 0],
});
await toggleFilterMenu(target);
await toggleAddCustomFilter(target);
await editConditionField(target, 0, "float_field");
const [floatInput] = target.querySelectorAll(".o_generator_menu_value .o_input");
// Default values
assert.strictEqual(floatInput.value, "0,0");
// Float parsing
await editConditionValue(target, 0, "4,");
assert.strictEqual(floatInput.value, "4,0");
await editConditionValue(target, 0, "4,2");
assert.strictEqual(floatInput.value, "4,2");
await editConditionValue(target, 0, "4,2,");
assert.strictEqual(floatInput.value, "42,0"); // because of the en localization fallback in parsers.
await editConditionValue(target, 0, "DefinitelyValidFloat");
// The input here is a string, resulting in a parsing error instead of 0
assert.strictEqual(floatInput.value, "42,0");
});
QUnit.test("add custom filter with multiple values", async function (assert) {
assert.expect(2);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
});
await toggleFilterMenu(target);
await toggleAddCustomFilter(target);
for (let i = 0; i < 4; i++) {
await addCondition(target);
}
await editConditionField(target, 0, "date_field");
await editConditionValue(target, 0, "01/09/1997");
await editConditionField(target, 1, "boolean_field");
await editConditionOperator(target, 1, "!=");
await editConditionField(target, 2, "char_field");
await editConditionValue(target, 2, "I will be deleted anyway");
await editConditionField(target, 3, "float_field");
await editConditionValue(target, 3, 7.2);
await editConditionField(target, 4, "id");
await editConditionValue(target, 4, 9);
const thirdcondition = target.querySelectorAll(".o_filter_condition")[2];
await click(thirdcondition, ".o_generator_menu_delete");
await applyFilter(target);
assert.deepEqual(getFacetTexts(target), [
[
'A date is equal to "01/09/1997"',
"Boolean Field is No",
'Floaty McFloatface is equal to "7.2"',
'ID is "9"',
].join("or"),
]);
assert.deepEqual(getDomain(controlPanel), [
"|",
["date_field", "=", "1997-01-09"],
"|",
["boolean_field", "!=", true],
"|",
["float_field", "=", 7.2],
["id", "=", 9],
]);
});
QUnit.test("delete button is visible", async function (assert) {
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
});
await toggleFilterMenu(target);
await toggleAddCustomFilter(target);
assert.containsNone(
target,
".o_generator_menu_delete",
"There is no delete button by default"
);
await addCondition(target);
assert.containsN(
target,
".o_generator_menu_delete",
2,
"A delete button has been added to each condition"
);
assert.containsN(
target,
"i.o_generator_menu_delete.fa-trash-o",
2,
"The delete button is shown as a trash icon"
);
});
});

View file

@ -0,0 +1,168 @@
import { expect, test } from "@odoo/hoot";
import { queryAllTexts } from "@odoo/hoot-dom";
import {
defineModels,
fields,
getFacetTexts,
isItemSelected,
isOptionSelected,
models,
mountWithSearch,
selectGroup,
toggleMenuItem,
toggleSearchBarMenu,
} from "@web/../tests/web_test_helpers";
import { SearchBar } from "@web/search/search_bar/search_bar";
class Foo extends models.Model {
bar = fields.Many2one({ relation: "partner", groupable: false });
birthday = fields.Date();
date = fields.Date();
float = fields.Float({ groupable: false });
foo = fields.Char();
}
class Partner extends models.Model {}
defineModels([Foo, Partner]);
test(`simple rendering`, async () => {
await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
});
await toggleSearchBarMenu();
expect(`.o_group_by_menu option[disabled]`).toHaveText(`Custom Group`);
expect(queryAllTexts`.o_add_custom_group_menu option:not([disabled])`).toEqual([
"Birthday",
"Created on",
"Date",
"Display name",
"Foo",
"Last Modified on",
]);
});
test(`the ID field should not be proposed in "Custom Group" menu`, async () => {
await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewFields: {
foo: { string: "Foo", type: "char", store: true, sortable: true, groupable: true },
id: { string: "ID", type: "integer", sortable: true, groupable: true },
},
});
await toggleSearchBarMenu();
expect(queryAllTexts`.o_add_custom_group_menu option:not([disabled])`).toEqual(["Foo"]);
});
test(`stored many2many should be proposed in "Custom Group" menu`, async () => {
await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewFields: {
char_a: {
string: "Char A",
type: "char",
store: true,
sortable: true,
groupable: true,
},
m2m_no_stored: { string: "M2M Not Stored", type: "many2many" },
m2m_stored: {
string: "M2M Stored",
type: "many2many",
store: true,
groupable: true,
},
},
});
await toggleSearchBarMenu();
expect(queryAllTexts`.o_add_custom_group_menu option:not([disabled])`).toEqual([
"Char A",
"M2M Stored",
]);
});
test(`add a date field in "Custom Group" activate a groupby with global default option "month"`, async () => {
const component = await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewFields: {
date: {
string: "Date",
type: "date",
store: true,
sortable: true,
groupable: true,
},
id: { sortable: true, string: "ID", type: "integer", groupable: true },
},
});
await toggleSearchBarMenu();
expect(component.env.searchModel.groupBy).toEqual([]);
expect(`.o_add_custom_group_menu`).toHaveCount(1); // Custom Group
await selectGroup("date");
expect(component.env.searchModel.groupBy).toEqual(["date:month"]);
expect(getFacetTexts()).toEqual(["Date: Month"]);
expect(isItemSelected("Date")).toBe(true);
await toggleMenuItem("Date");
expect(isOptionSelected("Date", "Month")).toBe(true);
});
test(`click on add custom group toggle group selector`, async () => {
await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewFields: {
date: {
sortable: true,
name: "date",
string: "Super Date",
type: "date",
groupable: true,
},
},
});
await toggleSearchBarMenu();
expect(`.o_add_custom_group_menu option[disabled]`).toHaveText("Custom Group");
// Single select node with a single option
expect(`.o_add_custom_group_menu option:not([disabled])`).toHaveCount(1);
expect(`.o_add_custom_group_menu option:not([disabled])`).toHaveText("Super Date");
});
test(`select a field name in Custom Group menu properly trigger the corresponding field`, async () => {
await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewFields: {
candle_light: {
sortable: true,
groupable: true,
string: "Candlelight",
type: "boolean",
},
},
});
await toggleSearchBarMenu();
await selectGroup("candle_light");
expect(`.o_group_by_menu .o_menu_item`).toHaveCount(2);
expect(`.o_add_custom_group_menu`).toHaveCount(1);
expect(getFacetTexts()).toEqual(["Candlelight"]);
});

View file

@ -1,244 +0,0 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { getFixture, patchWithCleanup } from "../helpers/utils";
import {
applyGroup,
getFacetTexts,
isItemSelected,
isOptionSelected,
makeWithSearch,
setupControlPanelServiceRegistry,
toggleAddCustomGroup,
toggleGroupByMenu,
toggleMenuItem,
} from "./helpers";
let target;
let serverData;
QUnit.module("Search", (hooks) => {
hooks.beforeEach(async () => {
serverData = {
models: {
foo: {
fields: {
bar: { string: "Bar", type: "many2one", relation: "partner" },
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
date_field: { string: "Date", type: "date", store: true, sortable: true },
float_field: { string: "Float", type: "float", group_operator: "sum" },
foo: { string: "Foo", type: "char", store: true, sortable: true },
},
records: {},
},
},
views: {
"foo,false,search": `<search/>`,
},
};
setupControlPanelServiceRegistry();
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout: () => {},
});
target = getFixture();
});
QUnit.module("CustomGroupByItem");
QUnit.test("simple rendering", async function (assert) {
assert.expect(5);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
});
await toggleGroupByMenu(target);
const customGroupByItem = target.querySelector(".o_add_custom_group_menu");
assert.strictEqual(customGroupByItem.innerText.trim(), "Add Custom Group");
assert.containsOnce(customGroupByItem, "button.dropdown-toggle");
assert.containsNone(customGroupByItem, ".dropdown-menu");
await toggleAddCustomGroup(target);
assert.containsOnce(customGroupByItem, ".dropdown-menu");
assert.deepEqual(
[...target.querySelectorAll(".o_add_custom_group_menu select option")].map(
(el) => el.innerText
),
["Birthday", "Date", "Foo"]
);
});
QUnit.test(
'the ID field should not be proposed in "Add Custom Group" menu',
async function (assert) {
assert.expect(1);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewFields: {
foo: { string: "Foo", type: "char", store: true, sortable: true },
id: { sortable: true, string: "ID", type: "integer" },
},
});
await toggleGroupByMenu(target);
await toggleAddCustomGroup(target);
assert.deepEqual(
[
...target.querySelectorAll(
".o_add_custom_group_menu .dropdown-menu select option"
),
].map((el) => el.innerText),
["Foo"]
);
}
);
QUnit.test(
'stored many2many should be proposed in "Add Custom Group" menu',
async function (assert) {
assert.expect(1);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewFields: {
char_a: { string: "Char A", type: "char", store: true, sortable: true },
m2m_no_stored: { string: "M2M Not Stored", type: "many2many" },
m2m_stored: {
string: "M2M Stored",
type: "many2many",
store: true,
},
},
});
await toggleGroupByMenu(target);
await toggleAddCustomGroup(target);
assert.deepEqual(
[
...target.querySelectorAll(
".o_add_custom_group_menu .dropdown-menu select option"
),
].map((el) => el.innerText),
["Char A", "M2M Stored"]
);
}
);
QUnit.test(
'add a date field in "Add Custom Group" activate a groupby with global default option "month"',
async function (assert) {
assert.expect(6);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewFields: {
date_field: { string: "Date", type: "date", store: true, sortable: true },
id: { sortable: true, string: "ID", type: "integer" },
},
});
await toggleGroupByMenu(target);
assert.deepEqual(controlPanel.env.searchModel.groupBy, []);
assert.containsNone(target, ".o_menu_item");
await toggleAddCustomGroup(target);
await applyGroup(target);
assert.deepEqual(controlPanel.env.searchModel.groupBy, ["date_field:month"]);
assert.deepEqual(getFacetTexts(target), ["Date: Month"]);
assert.ok(isItemSelected(target, "Date"));
await toggleMenuItem(target, "Date");
assert.ok(isOptionSelected(target, "Date", "Month"));
}
);
QUnit.test("click on add custom group toggle group selector", async function (assert) {
assert.expect(4);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewFields: {
date: { sortable: true, name: "date", string: "Super Date", type: "date" },
},
});
await toggleGroupByMenu(target);
const addCustomGroupMenu = target.querySelector(".o_add_custom_group_menu");
assert.strictEqual(addCustomGroupMenu.innerText.trim(), "Add Custom Group");
await toggleAddCustomGroup(target);
// Single select node with a single option
assert.containsOnce(target, ".o_add_custom_group_menu .dropdown-menu select");
assert.strictEqual(
target
.querySelector(".o_add_custom_group_menu .dropdown-menu select option")
.innerText.trim(),
"Super Date"
);
// Button apply
assert.containsOnce(target, ".o_add_custom_group_menu .dropdown-menu .btn");
});
QUnit.test(
"select a field name in Add Custom Group menu properly trigger the corresponding field",
async function (assert) {
assert.expect(4);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewFields: {
candle_light: {
sortable: true,
string: "Candlelight",
type: "boolean",
},
},
});
await toggleGroupByMenu(target);
await toggleAddCustomGroup(target);
await applyGroup(target);
assert.containsOnce(target, ".o_group_by_menu .o_menu_item");
assert.containsOnce(target, ".o_add_custom_group_menu .dropdown-toggle");
assert.containsOnce(target, ".o_add_custom_group_menu .dropdown-menu");
assert.deepEqual(getFacetTexts(target), ["Candlelight"]);
}
);
});

View file

@ -1,391 +0,0 @@
/** @odoo-module **/
import { click, getFixture, patchDate } from "@web/../tests/helpers/utils";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { registry } from "@web/core/registry";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { FavoriteMenu } from "@web/search/favorite_menu/favorite_menu";
import { SearchBar } from "@web/search/search_bar/search_bar";
import {
deleteFavorite,
getFacetTexts,
isItemSelected,
makeWithSearch,
setupControlPanelFavoriteMenuRegistry,
setupControlPanelServiceRegistry,
toggleComparisonMenu,
toggleFavoriteMenu,
toggleMenuItem,
} from "@web/../tests/search/helpers";
import { Component, onWillUpdateProps, xml } from "@odoo/owl";
const viewRegistry = registry.category("views");
const favoriteMenuRegistry = registry.category("favoriteMenu");
function getDomain(comp) {
return comp.env.searchModel.domain;
}
let serverData;
let target;
QUnit.module("Search", (hooks) => {
hooks.beforeEach(async () => {
target = getFixture();
serverData = {
models: {
foo: {
fields: {
bar: { string: "Bar", type: "many2one", relation: "partner" },
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
date_field: { string: "Date", type: "date", store: true, sortable: true },
float_field: { string: "Float", type: "float", group_operator: "sum" },
foo: { string: "Foo", type: "char", store: true, sortable: true },
},
records: {},
},
},
views: {
"foo,false,search": `<search/>`,
},
};
setupControlPanelFavoriteMenuRegistry();
setupControlPanelServiceRegistry();
});
QUnit.module("FavoriteMenu");
QUnit.test(
"simple rendering with no favorite (without ability to save)",
async function (assert) {
assert.expect(4);
favoriteMenuRegistry.remove("custom-favorite-item");
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["favorite"],
searchViewId: false,
config: {
getDisplayName: () => "Action Name",
},
});
assert.containsOnce(target, "div.o_favorite_menu > button i.fa.fa-star");
assert.strictEqual(
target
.querySelector("div.o_favorite_menu > button span")
.innerText.trim()
.toUpperCase(),
"FAVORITES"
);
await toggleFavoriteMenu(target);
assert.containsOnce(
target,
"div.o_favorite_menu > .dropdown-menu",
"the menu should be opened"
);
assert.containsNone(target, ".dropdown-menu *", "the menu should be empty");
}
);
QUnit.test("simple rendering with no favorite", async function (assert) {
assert.expect(5);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["favorite"],
searchViewId: false,
config: {
getDisplayName: () => "Action Name",
},
});
assert.containsOnce(target, "div.o_favorite_menu > button i.fa.fa-star");
assert.strictEqual(
target
.querySelector("div.o_favorite_menu > button span")
.innerText.trim()
.toUpperCase(),
"FAVORITES"
);
await toggleFavoriteMenu(target);
assert.containsOnce(
target,
"div.o_favorite_menu > .dropdown-menu",
"the menu should be opened"
);
assert.containsNone(target, ".dropdown-menu .dropdown-divider");
assert.containsOnce(target, ".dropdown-menu .o_add_favorite");
});
QUnit.test("delete an active favorite", async function (assert) {
assert.expect(14);
class ToyController extends Component {
setup() {
assert.deepEqual(this.props.domain, [["foo", "=", "qsdf"]]);
onWillUpdateProps((nextProps) => {
assert.deepEqual(nextProps.domain, []);
});
}
}
ToyController.components = { FavoriteMenu, SearchBar };
ToyController.template = xml`
<div>
<SearchBar/>
<FavoriteMenu/>
</div>
`;
viewRegistry.add("toy", {
type: "toy",
display_name: "Toy",
Controller: ToyController,
});
serverData.views["foo,false,toy"] = `<toy />`;
serverData.models.foo.filters = [
{
context: "{}",
domain: "[['foo', '=', 'qsdf']]",
id: 7,
is_default: true,
name: "My favorite",
sort: "[]",
user_id: [2, "Mitchell Admin"],
},
];
const webClient = await createWebClient({
serverData,
mockRPC: async (_, args) => {
if (args.model === "ir.filters" && args.method === "unlink") {
assert.step("deleteFavorite");
return { result: true }; // mocked unlink result
}
},
});
webClient.env.bus.on("CLEAR-CACHES", webClient, () => assert.step("CLEAR-CACHES"));
await doAction(webClient, {
name: "Action",
res_model: "foo",
type: "ir.actions.act_window",
views: [[false, "toy"]],
});
await toggleFavoriteMenu(target);
const favorite = target.querySelector(".o_favorite_menu .dropdown-item");
assert.equal(favorite.innerText, "My favorite");
assert.deepEqual(favorite.getAttribute("role"), "menuitemcheckbox");
assert.deepEqual(favorite.ariaChecked, "true");
assert.deepEqual(getFacetTexts(target), ["My favorite"]);
assert.hasClass(target.querySelector(".o_favorite_menu .o_menu_item"), "selected");
await deleteFavorite(target, 0);
assert.verifySteps([]);
await click(document.querySelector("div.o_dialog footer button"));
assert.deepEqual(getFacetTexts(target), []);
assert.containsNone(target, ".o_favorite_menu .o_menu_item");
assert.containsOnce(target, ".o_favorite_menu .o_add_favorite");
assert.verifySteps(["deleteFavorite", "CLEAR-CACHES"]);
});
QUnit.test(
"default favorite is not activated if activateFavorite is set to false",
async function (assert) {
assert.expect(3);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["favorite"],
searchViewId: false,
irFilters: [
{
context: "{}",
domain: "[('foo', '=', 'a')]",
id: 7,
is_default: true,
name: "My favorite",
sort: "[]",
user_id: [2, "Mitchell Admin"],
},
],
activateFavorite: false,
});
await toggleFavoriteMenu(target);
assert.notOk(isItemSelected(target, "My favorite"));
assert.deepEqual(getDomain(controlPanel), []);
assert.deepEqual(getFacetTexts(target), []);
}
);
QUnit.test(
'toggle favorite correctly clears filter, groupbys, comparison and field "options"',
async function (assert) {
patchDate(2019, 6, 31, 13, 43, 0);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["filter", "groupBy", "comparison", "favorite"],
searchViewId: false,
irFilters: [
{
context: `
{
"group_by": ["foo"],
"comparison": {
"favorite comparison content": "bla bla..."
},
}
`,
domain: "['!', ['foo', '=', 'qsdf']]",
id: 7,
is_default: false,
name: "My favorite",
sort: "[]",
user_id: [2, "Mitchell Admin"],
},
],
searchViewArch: `
<search>
<field string="Foo" name="foo"/>
<filter string="Date Field Filter" name="positive" date="date_field" default_period="this_year"/>
<filter string="Date Field Groupby" name="coolName" context="{'group_by': 'date_field'}"/>
</search>
`,
context: {
search_default_positive: true,
search_default_coolName: true,
search_default_foo: "a",
},
});
let domain = controlPanel.env.searchModel.domain;
let groupBy = controlPanel.env.searchModel.groupBy;
let comparison = controlPanel.env.searchModel.getFullComparison();
assert.deepEqual(domain, [
"&",
["foo", "ilike", "a"],
"&",
["date_field", ">=", "2019-01-01"],
["date_field", "<=", "2019-12-31"],
]);
assert.deepEqual(groupBy, ["date_field:month"]);
assert.deepEqual(comparison, null);
assert.deepEqual(getFacetTexts(target), [
"Foo\na",
"Date Field Filter: 2019",
"Date Field Groupby: Month",
]);
// activate a comparison
await toggleComparisonMenu(target);
await toggleMenuItem(target, "Date Field Filter: Previous Period");
domain = controlPanel.env.searchModel.domain;
groupBy = controlPanel.env.searchModel.groupBy;
comparison = controlPanel.env.searchModel.getFullComparison();
assert.deepEqual(domain, [["foo", "ilike", "a"]]);
assert.deepEqual(groupBy, ["date_field:month"]);
assert.deepEqual(comparison, {
comparisonId: "previous_period",
comparisonRange: [
"&",
["date_field", ">=", "2018-01-01"],
["date_field", "<=", "2018-12-31"],
],
comparisonRangeDescription: "2018",
fieldDescription: "Date Field Filter",
fieldName: "date_field",
range: [
"&",
["date_field", ">=", "2019-01-01"],
["date_field", "<=", "2019-12-31"],
],
rangeDescription: "2019",
});
// activate the unique existing favorite
await toggleFavoriteMenu(target);
const favorite = target.querySelector(".o_favorite_menu .dropdown-item");
assert.equal(favorite.innerText, "My favorite");
assert.deepEqual(favorite.getAttribute("role"), "menuitemcheckbox");
assert.deepEqual(favorite.ariaChecked, "false");
await toggleMenuItem(target, 0);
assert.deepEqual(favorite.ariaChecked, "true");
domain = controlPanel.env.searchModel.domain;
groupBy = controlPanel.env.searchModel.groupBy;
comparison = controlPanel.env.searchModel.getFullComparison();
assert.deepEqual(domain, ["!", ["foo", "=", "qsdf"]]);
assert.deepEqual(groupBy, ["foo"]);
assert.deepEqual(comparison, {
"favorite comparison content": "bla bla...",
});
assert.deepEqual(getFacetTexts(target), ["My favorite"]);
}
);
QUnit.skip("modal loads saved search filters", async function (assert) {
/** @todo I don't know yet how to convert this test */
// assert.expect(1);
// const data = {
// partner: {
// fields: {
// bar: { string: "Bar", type: "many2one", relation: "partner" },
// },
// // 10 records so that the Search button shows
// records: Array.apply(null, Array(10)).map(function (_, i) {
// return { id: i, display_name: "Record " + i, bar: 1 };
// }),
// },
// };
// const form = await createView({
// arch: `
// <form string="Partners">
// <sheet>
// <group>
// <field name="bar"/>
// </group>
// </sheet>
// </form>`,
// data,
// model: "partner",
// res_id: 1,
// View: FormView,
// interceptsPropagate: {
// load_views: function (ev) {
// assert.ok(
// ev.data.options.load_filters,
// "opening dialog should load the filters"
// );
// },
// },
// });
// await testUtils.form.clickEdit(form);
// await testUtils.fields.many2one.clickOpenDropdown("bar");
// await testUtils.fields.many2one.clickItem("bar", "Search");
// form.destroy();
});
});

View file

@ -1,587 +0,0 @@
/** @odoo-module **/
import { getFixture, patchDate, patchWithCleanup } from "@web/../tests/helpers/utils";
import { browser } from "@web/core/browser/browser";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import {
getFacetTexts,
isItemSelected,
isOptionSelected,
makeWithSearch,
setupControlPanelServiceRegistry,
toggleFilterMenu,
toggleMenuItem,
toggleMenuItemOption,
} from "./helpers";
function getDomain(controlPanel) {
return controlPanel.env.searchModel.domain;
}
function getContext(controlPanel) {
return controlPanel.env.searchModel.context;
}
let target;
let serverData;
QUnit.module("Search", (hooks) => {
hooks.beforeEach(async () => {
serverData = {
models: {
foo: {
fields: {
date_field: {
string: "Date",
type: "date",
store: true,
sortable: true,
searchable: true,
},
foo: { string: "Foo", type: "char", store: true, sortable: true },
},
records: {},
},
},
views: {
"foo,false,search": `<search/>`,
},
};
setupControlPanelServiceRegistry();
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout: () => {},
});
target = getFixture();
});
QUnit.module("FilterMenu");
QUnit.test("simple rendering with no filter", async function (assert) {
assert.expect(3);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["filter"],
});
await toggleFilterMenu(target);
assert.containsNone(target, ".o_menu_item");
assert.containsNone(target, ".dropdown-divider");
assert.containsOnce(target, ".o_add_custom_filter_menu");
});
QUnit.test("simple rendering with a single filter", async function (assert) {
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewArch: `
<search>
<filter string="Foo" name="foo" domain="[]"/>
</search>
`,
});
await toggleFilterMenu(target);
assert.containsOnce(target, ".o_menu_item");
assert.containsOnce(target, ".o_menu_item[role=menuitemcheckbox]");
assert.deepEqual(target.querySelector(".o_menu_item").ariaChecked, "false");
assert.containsOnce(target, ".dropdown-divider");
assert.containsOnce(target, ".o_add_custom_filter_menu");
});
QUnit.test('toggle a "simple" filter in filter menu works', async function (assert) {
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewArch: `
<search>
<filter string="Foo" name="foo" domain="[('foo', '=', 'qsdf')]"/>
</search>
`,
});
await toggleFilterMenu(target);
assert.deepEqual(getFacetTexts(target), []);
assert.notOk(isItemSelected(target, "Foo"));
assert.deepEqual(getDomain(controlPanel), []);
assert.containsOnce(target, ".o_menu_item[role=menuitemcheckbox]");
assert.deepEqual(target.querySelector(".o_menu_item").ariaChecked, "false");
await toggleMenuItem(target, "Foo");
assert.deepEqual(target.querySelector(".o_menu_item").ariaChecked, "true");
assert.deepEqual(getFacetTexts(target), ["Foo"]);
assert.containsOnce(
target.querySelector(".o_searchview .o_searchview_facet"),
"span.fa.fa-filter.o_searchview_facet_label"
);
assert.ok(isItemSelected(target, "Foo"));
assert.deepEqual(getDomain(controlPanel), [["foo", "=", "qsdf"]]);
await toggleMenuItem(target, "Foo");
assert.deepEqual(getFacetTexts(target), []);
assert.notOk(isItemSelected(target, "Foo"));
assert.deepEqual(getDomain(controlPanel), []);
});
QUnit.test("filter by a date field using period works", async function (assert) {
assert.expect(57);
patchDate(2017, 2, 22, 1, 0, 0);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewArch: `
<search>
<filter string="Date" name="date_field" date="date_field"/>
</search>
`,
context: { search_default_date_field: 1 },
});
await toggleFilterMenu(target);
await toggleMenuItem(target, "Date");
const optionEls = target.querySelectorAll(".dropdown .o_item_option");
// default filter should be activated with the global default period 'this_month'
assert.deepEqual(getDomain(controlPanel), [
"&",
["date_field", ">=", "2017-03-01"],
["date_field", "<=", "2017-03-31"],
]);
assert.ok(isItemSelected(target, "Date"));
assert.ok(isOptionSelected(target, "Date", "March"));
// check option descriptions
const optionDescriptions = [...optionEls].map((e) => e.innerText.trim());
const expectedDescriptions = [
"March",
"February",
"January",
"Q4",
"Q3",
"Q2",
"Q1",
"2017",
"2016",
"2015",
];
assert.deepEqual(optionDescriptions, expectedDescriptions);
// check generated domains
const steps = [
{
toggledOption: "March",
resultingFacet: "Date: 2017",
selectedoptions: ["2017"],
domain: [
"&",
["date_field", ">=", "2017-01-01"],
["date_field", "<=", "2017-12-31"],
],
},
{
toggledOption: "February",
resultingFacet: "Date: February 2017",
selectedoptions: ["February", "2017"],
domain: [
"&",
["date_field", ">=", "2017-02-01"],
["date_field", "<=", "2017-02-28"],
],
},
{
toggledOption: "February",
resultingFacet: "Date: 2017",
selectedoptions: ["2017"],
domain: [
"&",
["date_field", ">=", "2017-01-01"],
["date_field", "<=", "2017-12-31"],
],
},
{
toggledOption: "January",
resultingFacet: "Date: January 2017",
selectedoptions: ["January", "2017"],
domain: [
"&",
["date_field", ">=", "2017-01-01"],
["date_field", "<=", "2017-01-31"],
],
},
{
toggledOption: "Q4",
resultingFacet: "Date: January 2017/Q4 2017",
selectedoptions: ["January", "Q4", "2017"],
domain: [
"|",
"&",
["date_field", ">=", "2017-01-01"],
["date_field", "<=", "2017-01-31"],
"&",
["date_field", ">=", "2017-10-01"],
["date_field", "<=", "2017-12-31"],
],
},
{
toggledOption: "January",
resultingFacet: "Date: Q4 2017",
selectedoptions: ["Q4", "2017"],
domain: [
"&",
["date_field", ">=", "2017-10-01"],
["date_field", "<=", "2017-12-31"],
],
},
{
toggledOption: "Q4",
resultingFacet: "Date: 2017",
selectedoptions: ["2017"],
domain: [
"&",
["date_field", ">=", "2017-01-01"],
["date_field", "<=", "2017-12-31"],
],
},
{
toggledOption: "Q1",
resultingFacet: "Date: Q1 2017",
selectedoptions: ["Q1", "2017"],
domain: [
"&",
["date_field", ">=", "2017-01-01"],
["date_field", "<=", "2017-03-31"],
],
},
{
toggledOption: "Q1",
resultingFacet: "Date: 2017",
selectedoptions: ["2017"],
domain: [
"&",
["date_field", ">=", "2017-01-01"],
["date_field", "<=", "2017-12-31"],
],
},
{
toggledOption: "2017",
selectedoptions: [],
domain: [],
},
{
toggledOption: "2017",
resultingFacet: "Date: 2017",
selectedoptions: ["2017"],
domain: [
"&",
["date_field", ">=", "2017-01-01"],
["date_field", "<=", "2017-12-31"],
],
},
{
toggledOption: "2016",
resultingFacet: "Date: 2016/2017",
selectedoptions: ["2017", "2016"],
domain: [
"|",
"&",
["date_field", ">=", "2016-01-01"],
["date_field", "<=", "2016-12-31"],
"&",
["date_field", ">=", "2017-01-01"],
["date_field", "<=", "2017-12-31"],
],
},
{
toggledOption: "2015",
resultingFacet: "Date: 2015/2016/2017",
selectedoptions: ["2017", "2016", "2015"],
domain: [
"|",
"&",
["date_field", ">=", "2015-01-01"],
["date_field", "<=", "2015-12-31"],
"|",
"&",
["date_field", ">=", "2016-01-01"],
["date_field", "<=", "2016-12-31"],
"&",
["date_field", ">=", "2017-01-01"],
["date_field", "<=", "2017-12-31"],
],
},
{
toggledOption: "March",
resultingFacet: "Date: March 2015/March 2016/March 2017",
selectedoptions: ["March", "2017", "2016", "2015"],
domain: [
"|",
"&",
["date_field", ">=", "2015-03-01"],
["date_field", "<=", "2015-03-31"],
"|",
"&",
["date_field", ">=", "2016-03-01"],
["date_field", "<=", "2016-03-31"],
"&",
["date_field", ">=", "2017-03-01"],
["date_field", "<=", "2017-03-31"],
],
},
];
for (const s of steps) {
await toggleMenuItemOption(target, "Date", s.toggledOption);
assert.deepEqual(getDomain(controlPanel), s.domain);
if (s.resultingFacet) {
assert.deepEqual(getFacetTexts(target), [s.resultingFacet]);
} else {
assert.deepEqual(getFacetTexts(target), []);
}
s.selectedoptions.forEach((option) => {
assert.ok(
isOptionSelected(target, "Date", option),
`at step ${steps.indexOf(s) + 1}, ${option} should be selected`
);
});
}
});
QUnit.test(
"filter by a date field using period works even in January",
async function (assert) {
assert.expect(5);
patchDate(2017, 0, 7, 3, 0, 0);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewArch: `
<search>
<filter string="Date" name="some_filter" date="date_field" default_period="last_month"/>
</search>
`,
context: { search_default_some_filter: 1 },
});
assert.deepEqual(getDomain(controlPanel), [
"&",
["date_field", ">=", "2016-12-01"],
["date_field", "<=", "2016-12-31"],
]);
assert.deepEqual(getFacetTexts(target), ["Date: December 2016"]);
await toggleFilterMenu(target);
await toggleMenuItem(target, "Date");
assert.ok(isItemSelected(target, "Date"));
assert.ok(isOptionSelected(target, "Date", "December"));
assert.ok(isOptionSelected(target, "Date", "2016"));
}
);
QUnit.test("`context` key in <filter> is used", async function (assert) {
assert.expect(1);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewArch: `
<search>
<filter string="Filter" name="some_filter" domain="[]" context="{'coucou_1': 1}"/>
</search>
`,
context: { search_default_some_filter: 1 },
});
assert.deepEqual(getContext(controlPanel), {
coucou_1: 1,
lang: "en",
tz: "taht",
uid: 7,
});
});
QUnit.test("Filter with JSON-parsable domain works", async function (assert) {
assert.expect(1);
const xml_domain = "[[&quot;foo&quot;,&quot;=&quot;,&quot;Gently Weeps&quot;]]";
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewArch: `
<search>
<filter string="Foo" name="gently_weeps" domain="${xml_domain}"/>
</search>
`,
context: { search_default_gently_weeps: 1 },
});
assert.deepEqual(
getDomain(controlPanel),
[["foo", "=", "Gently Weeps"]],
"A JSON parsable xml domain should be handled just like any other"
);
});
QUnit.test("filter with date attribute set as search_default", async function (assert) {
assert.expect(1);
patchDate(2019, 6, 31, 13, 43, 0);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewArch: `
<search>
<filter string="Date" name="date_field" date="date_field" default_period="last_month"/>
</search>
`,
context: { search_default_date_field: true },
});
assert.deepEqual(getFacetTexts(target), ["Date: June 2019"]);
});
QUnit.test(
"filter with multiple values in default_period date attribute set as search_default",
async function (assert) {
assert.expect(3);
patchDate(2019, 6, 31, 13, 43, 0);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewArch: `
<search>
<filter string="Date" name="date_field" date="date_field" default_period="this_year,last_year"/>
</search>
`,
context: { search_default_date_field: true },
});
await toggleFilterMenu(target);
await toggleMenuItem(target, "Date");
assert.ok(isItemSelected(target, "Date"));
assert.ok(isOptionSelected(target, "Date", "2019"));
assert.ok(isOptionSelected(target, "Date", "2018"));
}
);
QUnit.test("filter domains are correcly combined by OR and AND", async function (assert) {
assert.expect(2);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewArch: `
<search>
<filter string="Filter Group 1" name="f_1_g1" domain="[['foo', '=', 'f1_g1']]"/>
<separator/>
<filter string="Filter 1 Group 2" name="f1_g2" domain="[['foo', '=', 'f1_g2']]"/>
<filter string="Filter 2 GROUP 2" name="f2_g2" domain="[['foo', '=', 'f2_g2']]"/>
</search>
`,
context: {
search_default_f_1_g1: true,
search_default_f1_g2: true,
search_default_f2_g2: true,
},
});
assert.deepEqual(getDomain(controlPanel), [
"&",
["foo", "=", "f1_g1"],
"|",
["foo", "=", "f1_g2"],
["foo", "=", "f2_g2"],
]);
assert.deepEqual(getFacetTexts(target), [
"Filter Group 1",
"Filter 1 Group 2orFilter 2 GROUP 2",
]);
});
QUnit.test("arch order of groups of filters preserved", async function (assert) {
assert.expect(12);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchMenuTypes: ["filter"],
searchViewArch: `
<search>
<filter string="1" name="coolName1" date="date_field"/>
<separator/>
<filter string="2" name="coolName2" date="date_field"/>
<separator/>
<filter string="3" name="coolName3" domain="[]"/>
<separator/>
<filter string="4" name="coolName4" domain="[]"/>
<separator/>
<filter string="5" name="coolName5" domain="[]"/>
<separator/>
<filter string="6" name="coolName6" domain="[]"/>
<separator/>
<filter string="7" name="coolName7" domain="[]"/>
<separator/>
<filter string="8" name="coolName8" domain="[]"/>
<separator/>
<filter string="9" name="coolName9" domain="[]"/>
<separator/>
<filter string="10" name="coolName10" domain="[]"/>
<separator/>
<filter string="11" name="coolName11" domain="[]"/>
</search>
`,
});
await toggleFilterMenu(target);
assert.containsN(target, ".o_filter_menu .o_menu_item", 11);
const menuItemEls = target.querySelectorAll(".o_filter_menu .o_menu_item");
[...menuItemEls].forEach((e, index) => {
assert.strictEqual(e.innerText.trim(), String(index + 1));
});
});
});

View file

@ -0,0 +1,69 @@
import { describe, expect, test } from "@odoo/hoot";
import { DEFAULT_INTERVAL } from "@web/search/utils/dates";
import { getGroupBy } from "@web/search/utils/group_by";
const fields = {
display_name: { string: "Displayed name", type: "char" },
foo: {
string: "Foo",
type: "char",
default: "My little Foo Value",
store: true,
sortable: true,
},
date_field: { string: "Date", type: "date", store: true, sortable: true },
float_field: { string: "Float", type: "float" },
bar: { string: "Bar", type: "many2one", relation: "partner" },
};
describe("Without field validation", () => {
test("simple valid group by", async () => {
let groupBy = getGroupBy("display_name");
expect(groupBy.fieldName).toBe("display_name");
expect(groupBy.interval).toBe(null);
expect(groupBy.spec).toBe("display_name");
groupBy = getGroupBy("display_name:quarter");
expect(groupBy.fieldName).toBe("display_name");
expect(groupBy.interval).toBe("quarter");
expect(groupBy.spec).toBe("display_name:quarter");
});
test("simple invalid group by", async () => {
expect(() => getGroupBy(":day")).toThrow();
expect(() => getGroupBy("diay_name:yar")).toThrow();
});
});
describe("With field validation", () => {
test("simple valid group by", async () => {
const groupBy = getGroupBy("display_name", fields);
expect(groupBy.fieldName).toBe("display_name");
expect(groupBy.interval).toBe(null);
expect(groupBy.spec).toBe("display_name");
});
test("simple invalid group by", async () => {
expect(() => getGroupBy("", fields)).toThrow();
expect(() => getGroupBy("display_name:day", fields)).toThrow();
expect(() => getGroupBy("diay_name:year", fields)).toThrow();
expect(() => getGroupBy("diay_name:yar", fields)).toThrow();
});
test("simple valid date group by", async () => {
let groupBy = getGroupBy("date_field:year", fields);
expect(groupBy.fieldName).toBe("date_field");
expect(groupBy.interval).toBe("year");
expect(groupBy.spec).toBe("date_field:year");
groupBy = getGroupBy("date_field", fields);
expect(groupBy.fieldName).toBe("date_field");
expect(groupBy.interval).toBe(DEFAULT_INTERVAL);
expect(groupBy.spec).toBe(`date_field:${DEFAULT_INTERVAL}`);
});
test("simple invalid date group by", async () => {
expect(() => getGroupBy("date_field:yar", fields)).toThrow();
});
});

View file

@ -1,534 +0,0 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { getFixture, patchWithCleanup } from "../helpers/utils";
import {
getFacetTexts,
isItemSelected,
isOptionSelected,
makeWithSearch,
removeFacet,
setupControlPanelServiceRegistry,
toggleGroupByMenu,
toggleMenuItem,
toggleMenuItemOption,
} from "./helpers";
let target;
let serverData;
QUnit.module("Search", (hooks) => {
hooks.beforeEach(async () => {
serverData = {
models: {
foo: {
fields: {
bar: { string: "Bar", type: "many2one", relation: "partner" },
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
date_field: { string: "Date", type: "date", store: true, sortable: true },
float_field: { string: "Float", type: "float", group_operator: "sum" },
foo: { string: "Foo", type: "char", store: true, sortable: true },
},
records: {},
},
},
views: {
"foo,false,search": `<search/>`,
},
};
setupControlPanelServiceRegistry();
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout: () => {},
});
target = getFixture();
});
QUnit.module("GroupByMenu");
QUnit.test(
"simple rendering with neither groupbys nor groupable fields",
async function (assert) {
assert.expect(3);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewFields: {},
});
await toggleGroupByMenu(target);
assert.containsNone(target, ".o_menu_item");
assert.containsNone(target, ".dropdown-divider");
assert.containsNone(target, ".o_add_custom_group_menu");
}
);
QUnit.test("simple rendering with no groupby", async function (assert) {
assert.expect(3);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
});
await toggleGroupByMenu(target);
assert.containsNone(target, ".o_menu_item");
assert.containsNone(target, ".dropdown-divider");
assert.containsOnce(target, ".o_add_custom_group_menu");
});
QUnit.test("simple rendering with a single groupby", async function (assert) {
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Foo" name="group_by_foo" context="{'group_by': 'foo'}"/>
</search>
`,
});
await toggleGroupByMenu(target);
assert.containsOnce(target, ".o_menu_item");
const menuItem = target.querySelector(".o_menu_item");
assert.strictEqual(menuItem.innerText.trim(), "Foo");
assert.strictEqual(menuItem.getAttribute("role"), "menuitemcheckbox");
assert.strictEqual(menuItem.ariaChecked, "false");
assert.containsOnce(target, ".dropdown-divider");
assert.containsOnce(target, ".o_add_custom_group_menu");
});
QUnit.test('toggle a "simple" groupby in groupby menu works', async function (assert) {
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Foo" name="group_by_foo" context="{'group_by': 'foo'}"/>
</search>
`,
});
await toggleGroupByMenu(target);
assert.deepEqual(controlPanel.env.searchModel.groupBy, []);
assert.deepEqual(getFacetTexts(target), []);
assert.notOk(isItemSelected(target, "Foo"));
const menuItem = target.querySelector(".o_menu_item");
assert.strictEqual(menuItem.innerText.trim(), "Foo");
assert.strictEqual(menuItem.getAttribute("role"), "menuitemcheckbox");
assert.strictEqual(menuItem.ariaChecked, "false");
await toggleMenuItem(target, "Foo");
assert.strictEqual(menuItem.ariaChecked, "true");
assert.deepEqual(controlPanel.env.searchModel.groupBy, ["foo"]);
assert.deepEqual(getFacetTexts(target), ["Foo"]);
assert.containsOnce(
target.querySelector(".o_searchview .o_searchview_facet"),
"span.oi.oi-group.o_searchview_facet_label"
);
assert.ok(isItemSelected(target, "Foo"));
await toggleMenuItem(target, "Foo");
assert.deepEqual(controlPanel.env.searchModel.groupBy, []);
assert.deepEqual(getFacetTexts(target), []);
assert.notOk(isItemSelected(target, "Foo"));
});
QUnit.test('toggle a "simple" groupby quickly does not crash', async function (assert) {
assert.expect(1);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Foo" name="group_by_foo" context="{'group_by': 'foo'}"/>
</search>
`,
});
await toggleGroupByMenu(target);
toggleMenuItem(target, "Foo");
toggleMenuItem(target, "Foo");
assert.ok(true);
});
QUnit.test(
'remove a "Group By" facet properly unchecks groupbys in groupby menu',
async function (assert) {
assert.expect(6);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Foo" name="group_by_foo" context="{'group_by': 'foo'}"/>
</search>
`,
context: { search_default_group_by_foo: 1 },
});
await toggleGroupByMenu(target);
assert.deepEqual(getFacetTexts(target), ["Foo"]);
assert.deepEqual(controlPanel.env.searchModel.groupBy, ["foo"]);
assert.ok(isItemSelected(target, "Foo"));
await removeFacet(target, "Foo");
assert.deepEqual(getFacetTexts(target), []);
assert.deepEqual(controlPanel.env.searchModel.groupBy, []);
await toggleGroupByMenu(target);
assert.notOk(isItemSelected(target, "Foo"));
}
);
QUnit.test("group by a date field using interval works", async function (assert) {
assert.expect(21);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Date" name="date" context="{'group_by': 'date_field:week'}"/>
</search>
`,
context: { search_default_date: 1 },
});
await toggleGroupByMenu(target);
assert.deepEqual(controlPanel.env.searchModel.groupBy, ["date_field:week"]);
await toggleMenuItem(target, "Date");
assert.ok(isOptionSelected(target, "Date", "Week"));
assert.deepEqual(
[...target.querySelectorAll(".o_item_option")].map((el) => el.innerText),
["Year", "Quarter", "Month", "Week", "Day"]
);
const steps = [
{
description: "Year",
facetTexts: ["Date: Year>Date: Week"],
selectedoptions: ["Year", "Week"],
groupBy: ["date_field:year", "date_field:week"],
},
{
description: "Month",
facetTexts: ["Date: Year>Date: Month>Date: Week"],
selectedoptions: ["Year", "Month", "Week"],
groupBy: ["date_field:year", "date_field:month", "date_field:week"],
},
{
description: "Week",
facetTexts: ["Date: Year>Date: Month"],
selectedoptions: ["Year", "Month"],
groupBy: ["date_field:year", "date_field:month"],
},
{
description: "Month",
facetTexts: ["Date: Year"],
selectedoptions: ["Year"],
groupBy: ["date_field:year"],
},
{ description: "Year", facetTexts: [], selectedoptions: [], groupBy: [] },
];
for (const s of steps) {
await toggleMenuItemOption(target, "Date", s.description);
assert.deepEqual(controlPanel.env.searchModel.groupBy, s.groupBy);
assert.deepEqual(getFacetTexts(target), s.facetTexts);
s.selectedoptions.forEach((description) => {
assert.ok(isOptionSelected(target, "Date", description));
});
}
});
QUnit.test("interval options are correctly grouped and ordered", async function (assert) {
assert.expect(8);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Bar" name="bar" context="{'group_by': 'bar'}"/>
<filter string="Date" name="date" context="{'group_by': 'date_field'}"/>
<filter string="Foo" name="foo" context="{'group_by': 'foo'}"/>
</search>
`,
context: { search_default_bar: 1 },
});
assert.deepEqual(getFacetTexts(target), ["Bar"]);
// open menu 'Group By'
await toggleGroupByMenu(target);
// Open the groupby 'Date'
await toggleMenuItem(target, "Date");
// select option 'week'
await toggleMenuItemOption(target, "Date", "Week");
assert.deepEqual(getFacetTexts(target), ["Bar>Date: Week"]);
// select option 'day'
await toggleMenuItemOption(target, "Date", "Day");
assert.deepEqual(getFacetTexts(target), ["Bar>Date: Week>Date: Day"]);
// select option 'year'
await toggleMenuItemOption(target, "Date", "Year");
assert.deepEqual(getFacetTexts(target), ["Bar>Date: Year>Date: Week>Date: Day"]);
// select 'Foo'
await toggleMenuItem(target, "Foo");
assert.deepEqual(getFacetTexts(target), ["Bar>Date: Year>Date: Week>Date: Day>Foo"]);
// select option 'quarter'
await toggleMenuItem(target, "Date");
await toggleMenuItemOption(target, "Date", "Quarter");
assert.deepEqual(getFacetTexts(target), [
"Bar>Date: Year>Date: Quarter>Date: Week>Date: Day>Foo",
]);
// unselect 'Bar'
await toggleMenuItem(target, "Bar");
assert.deepEqual(getFacetTexts(target), [
"Date: Year>Date: Quarter>Date: Week>Date: Day>Foo",
]);
// unselect option 'week'
await toggleMenuItem(target, "Date");
await toggleMenuItemOption(target, "Date", "Week");
assert.deepEqual(getFacetTexts(target), ["Date: Year>Date: Quarter>Date: Day>Foo"]);
});
QUnit.test("default groupbys can be ordered", async function (assert) {
assert.expect(2);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/>
<filter string="Date" name="date" context="{'group_by': 'date_field:week'}"/>
</search>
`,
context: { search_default_birthday: 2, search_default_date: 1 },
});
// the default groupbys should be activated in the right order
assert.deepEqual(controlPanel.env.searchModel.groupBy, [
"date_field:week",
"birthday:month",
]);
assert.deepEqual(getFacetTexts(target), ["Date: Week>Birthday: Month"]);
});
QUnit.test("a separator in groupbys does not cause problems", async function (assert) {
assert.expect(23);
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Date" name="coolName" context="{'group_by': 'date_field'}"/>
<separator/>
<filter string="Bar" name="superName" context="{'group_by': 'bar'}"/>
</search>
`,
});
await toggleGroupByMenu(target);
await toggleMenuItem(target, "Date");
await toggleMenuItemOption(target, "Date", "Day");
assert.ok(isItemSelected(target, "Date"));
assert.notOk(isItemSelected(target, "Bar"));
assert.ok(isOptionSelected(target, "Date", "Day"), "selected");
assert.deepEqual(getFacetTexts(target), ["Date: Day"]);
await toggleMenuItem(target, "Bar");
await toggleMenuItem(target, "Date");
assert.ok(isItemSelected(target, "Date"));
assert.ok(isItemSelected(target, "Bar"));
assert.ok(isOptionSelected(target, "Date", "Day"), "selected");
assert.deepEqual(getFacetTexts(target), ["Date: Day>Bar"]);
await toggleMenuItemOption(target, "Date", "Quarter");
assert.ok(isItemSelected(target, "Date"));
assert.ok(isItemSelected(target, "Bar"));
assert.ok(isOptionSelected(target, "Date", "Quarter"), "selected");
assert.ok(isOptionSelected(target, "Date", "Day"), "selected");
assert.deepEqual(getFacetTexts(target), ["Date: Quarter>Date: Day>Bar"]);
await toggleMenuItem(target, "Bar");
await toggleMenuItem(target, "Date");
assert.ok(isItemSelected(target, "Date"));
assert.notOk(isItemSelected(target, "Bar"));
assert.ok(isOptionSelected(target, "Date", "Quarter"), "selected");
assert.ok(isOptionSelected(target, "Date", "Day"), "selected");
assert.deepEqual(getFacetTexts(target), ["Date: Quarter>Date: Day"]);
await removeFacet(target);
assert.deepEqual(getFacetTexts(target), []);
await toggleGroupByMenu(target);
await toggleMenuItem(target, "Date");
assert.notOk(isItemSelected(target, "Date"));
assert.notOk(isItemSelected(target, "Bar"));
assert.notOk(isOptionSelected(target, "Date", "Quarter"), "selected");
assert.notOk(isOptionSelected(target, "Date", "Day"), "selected");
});
QUnit.test("falsy search default groupbys are not activated", async function (assert) {
assert.expect(2);
const controlPanel = await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/>
<filter string="Date" name="date" context="{'group_by': 'foo'}"/>
</search>
`,
context: { search_default_birthday: false, search_default_foo: 0 },
});
assert.deepEqual(controlPanel.env.searchModel.groupBy, []);
assert.deepEqual(getFacetTexts(target), []);
});
QUnit.test(
"Custom group by menu is displayed when hideCustomGroupBy is not set",
async function (assert) {
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchViewArch: `
<search>
<filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/>
<filter string="Date" name="date" context="{'group_by': 'foo'}"/>
</search>
`,
searchMenuTypes: ["groupBy"],
});
await toggleGroupByMenu(target);
assert.containsOnce(target, ".o_add_custom_group_menu");
}
);
QUnit.test(
"Custom group by menu is displayed when hideCustomGroupBy is false",
async function (assert) {
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchViewArch: `
<search>
<filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/>
<filter string="Date" name="date" context="{'group_by': 'foo'}"/>
</search>
`,
hideCustomGroupBy: false,
searchMenuTypes: ["groupBy"],
});
await toggleGroupByMenu(target);
assert.containsOnce(target, ".o_add_custom_group_menu");
}
);
QUnit.test(
"Custom group by menu is displayed when hideCustomGroupBy is true",
async function (assert) {
await makeWithSearch({
serverData,
resModel: "foo",
Component: ControlPanel,
searchViewId: false,
searchViewArch: `
<search>
<filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/>
<filter string="Date" name="date" context="{'group_by': 'foo'}"/>
</search>
`,
hideCustomGroupBy: true,
searchMenuTypes: ["groupBy"],
});
await toggleGroupByMenu(target);
assert.containsNone(target, ".o_add_custom_group_menu");
}
);
});

View file

@ -1,99 +0,0 @@
/** @odoo-module **/
import { getGroupBy } from "@web/search/utils/group_by";
import { DEFAULT_INTERVAL } from "@web/search/utils/dates";
const fields = {
display_name: { string: "Displayed name", type: "char" },
foo: {
string: "Foo",
type: "char",
default: "My little Foo Value",
store: true,
sortable: true,
},
date_field: { string: "Date", type: "date", store: true, sortable: true },
float_field: { string: "Float", type: "float" },
bar: { string: "Bar", type: "many2one", relation: "partner" },
};
QUnit.module("GroupBy Class", {}, () => {
QUnit.module("Without field validation");
QUnit.test("simple valid group by", async function (assert) {
assert.expect(6);
let groupBy = getGroupBy("display_name");
assert.strictEqual(groupBy.fieldName, "display_name");
assert.strictEqual(groupBy.interval, null);
assert.strictEqual(groupBy.spec, "display_name");
groupBy = getGroupBy("display_name:quarter");
assert.strictEqual(groupBy.fieldName, "display_name");
assert.strictEqual(groupBy.interval, "quarter");
assert.strictEqual(groupBy.spec, "display_name:quarter");
});
QUnit.test("simple invalid group by", async function (assert) {
assert.expect(3);
try {
getGroupBy(":day");
} catch (_e) {
assert.step("Error 1");
}
try {
getGroupBy("diay_name:yar");
} catch (_e) {
assert.step("Error 2");
}
assert.verifySteps(["Error 1", "Error 2"]);
});
QUnit.module("With field validation");
QUnit.test("simple valid group by", async function (assert) {
assert.expect(3);
const groupBy = getGroupBy("display_name", fields);
assert.strictEqual(groupBy.fieldName, "display_name");
assert.strictEqual(groupBy.interval, null);
assert.strictEqual(groupBy.spec, "display_name");
});
QUnit.test("simple invalid group by", async function (assert) {
assert.expect(5);
try {
getGroupBy("", fields);
} catch (_e) {
assert.step("Error 1");
}
try {
getGroupBy("display_name:day", fields);
} catch (_e) {
assert.step("Error 2");
}
try {
getGroupBy("diay_name:year", fields);
} catch (_e) {
assert.step("Error 3");
}
try {
getGroupBy("diay_name:yar", fields);
} catch (_e) {
assert.step("Error 4");
}
assert.verifySteps(["Error 1", "Error 2", "Error 3", "Error 4"]);
});
QUnit.test("simple valid date group by", async function (assert) {
assert.expect(6);
let groupBy = getGroupBy("date_field:year", fields);
assert.strictEqual(groupBy.fieldName, "date_field");
assert.strictEqual(groupBy.interval, "year");
assert.strictEqual(groupBy.spec, "date_field:year");
groupBy = getGroupBy("date_field", fields);
assert.strictEqual(groupBy.fieldName, "date_field");
assert.strictEqual(groupBy.interval, DEFAULT_INTERVAL);
assert.strictEqual(groupBy.spec, `date_field:${DEFAULT_INTERVAL}`);
});
QUnit.test("simple invalid date group by", async function (assert) {
assert.expect(2);
try {
getGroupBy("date_field:yar", fields);
} catch (_e) {
assert.step("Error");
}
assert.verifySteps(["Error"]);
});
});

View file

@ -1,336 +0,0 @@
/** @odoo-module **/
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import {
click,
editInput,
getFixture,
mount,
mouseEnter,
triggerEvent,
triggerEvents,
} from "@web/../tests/helpers/utils";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { notificationService } from "@web/core/notifications/notification_service";
import { ormService } from "@web/core/orm_service";
import { registry } from "@web/core/registry";
import { CustomFavoriteItem } from "@web/search/favorite_menu/custom_favorite_item";
import { WithSearch } from "@web/search/with_search/with_search";
import { getDefaultConfig } from "@web/views/view";
import { viewService } from "@web/views/view_service";
import { actionService } from "@web/webclient/actions/action_service";
import { dialogService } from "@web/core/dialog/dialog_service";
import { MainComponentsContainer } from "@web/core/main_components_container";
import { Component, xml } from "@odoo/owl";
const serviceRegistry = registry.category("services");
const favoriteMenuRegistry = registry.category("favoriteMenu");
export function setupControlPanelServiceRegistry() {
serviceRegistry.add("action", actionService);
serviceRegistry.add("hotkey", hotkeyService);
serviceRegistry.add("notification", notificationService);
serviceRegistry.add("orm", ormService);
serviceRegistry.add("view", viewService);
serviceRegistry.add("dialog", dialogService);
}
export function setupControlPanelFavoriteMenuRegistry() {
favoriteMenuRegistry.add(
"custom-favorite-item",
{ Component: CustomFavoriteItem, groupNumber: 3 },
{ sequence: 0 }
);
}
export async function makeWithSearch(params) {
const props = { ...params };
const serverData = props.serverData || undefined;
const mockRPC = props.mockRPC || undefined;
const config = {
...getDefaultConfig(),
...props.config,
};
delete props.serverData;
delete props.mockRPC;
delete props.config;
const componentProps = props.componentProps || {};
delete props.componentProps;
delete props.Component;
class Parent extends Component {
setup() {
this.withSearchProps = props;
this.componentProps = componentProps;
}
getDisplay(display) {
return Object.assign({}, display, componentProps.display);
}
}
Parent.template = xml`
<WithSearch t-props="withSearchProps" t-slot-scope="search">
<Component
t-props="componentProps"
context="search.context"
domain="search.domain"
groupBy="search.groupBy"
orderBy="search.orderBy"
comparison="search.comparison"
display="getDisplay(search.display)"/>
</WithSearch>
<MainComponentsContainer />
`;
Parent.components = { Component: params.Component, WithSearch, MainComponentsContainer };
const env = await makeTestEnv({ serverData, mockRPC });
const searchEnv = Object.assign(Object.create(env), { config });
const parent = await mount(Parent, getFixture(), { env: searchEnv, props });
const parentNode = parent.__owl__;
const withSearchNode = getUniqueChild(parentNode);
const componentNode = getUniqueChild(withSearchNode);
const component = componentNode.component;
return component;
}
function getUniqueChild(node) {
return Object.values(node.children)[0];
}
function getNode(target) {
return target instanceof Component ? target.el : target;
}
function findItem(target, selector, finder = 0) {
const el = getNode(target);
const elems = [...el.querySelectorAll(selector)];
if (Number.isInteger(finder)) {
return elems[finder];
}
return elems.find((el) => el.innerText.trim().toLowerCase() === String(finder).toLowerCase());
}
/** Menu (generic) */
export async function toggleMenu(el, menuFinder) {
const menu = findItem(el, `.dropdown button.dropdown-toggle`, menuFinder);
await click(menu);
}
export async function toggleMenuItem(el, itemFinder) {
const item = findItem(el, `.o_menu_item`, itemFinder);
if (item.classList.contains("dropdown-toggle")) {
await mouseEnter(item);
} else {
await click(item);
}
}
export async function toggleMenuItemOption(el, itemFinder, optionFinder) {
const item = findItem(el, `.o_menu_item`, itemFinder);
const option = findItem(item.parentNode, ".o_item_option", optionFinder);
if (option.classList.contains("dropdown-toggle")) {
await mouseEnter(option);
} else {
await click(option);
}
}
export function isItemSelected(el, itemFinder) {
const item = findItem(el, `.o_menu_item`, itemFinder);
return item.classList.contains("selected");
}
export function isOptionSelected(el, itemFinder, optionFinder) {
const item = findItem(el, `.o_menu_item`, itemFinder);
const option = findItem(item.parentNode, ".o_item_option", optionFinder);
return option.classList.contains("selected");
}
export function getMenuItemTexts(target) {
const el = getNode(target);
return [...el.querySelectorAll(`.dropdown-menu .o_menu_item`)].map((e) => e.innerText.trim());
}
export function getButtons(el) {
return [...el.querySelector(`div.o_cp_bottom div.o_cp_buttons`).children];
}
/** Filter menu */
export async function toggleFilterMenu(el) {
await click(findItem(el, `.o_filter_menu button.dropdown-toggle`));
}
export async function toggleAddCustomFilter(el) {
await mouseEnter(findItem(el, `.o_add_custom_filter_menu .dropdown-toggle`));
}
export async function editConditionField(el, index, fieldName) {
const condition = findItem(el, `.o_filter_condition`, index);
const select = findItem(condition, "select", 0);
select.value = fieldName;
await triggerEvent(select, null, "change");
}
export async function editConditionOperator(el, index, operator) {
const condition = findItem(el, `.o_filter_condition`, index);
const select = findItem(condition, "select", 1);
select.value = operator;
await triggerEvent(select, null, "change");
}
export async function editConditionValue(el, index, value, valueIndex = 0) {
const condition = findItem(el, `.o_filter_condition`, index);
const target = findItem(
condition,
".o_generator_menu_value input:not([type=hidden]),.o_generator_menu_value select",
valueIndex
);
target.value = value;
await triggerEvent(target, null, "change");
}
export async function applyFilter(el) {
await click(findItem(el, `.o_add_custom_filter_menu .dropdown-menu button.o_apply_filter`));
}
export async function addCondition(el) {
await click(findItem(el, `.o_add_custom_filter_menu .dropdown-menu button.o_add_condition`));
}
export async function removeCondition(el, index) {
const condition = findItem(el, `.o_filter_condition`, index);
await click(findItem(condition, ".o_generator_menu_delete"));
}
/** Group by menu */
export async function toggleGroupByMenu(el) {
await click(findItem(el, `.o_group_by_menu .dropdown-toggle`));
}
export async function toggleAddCustomGroup(el) {
await mouseEnter(findItem(el, `.o_add_custom_group_menu .dropdown-toggle`));
}
export async function selectGroup(el, fieldName) {
const select = findItem(el, `.o_add_custom_group_menu .dropdown-menu select`);
select.value = fieldName;
await triggerEvent(select, null, "change");
}
export async function applyGroup(el) {
await click(findItem(el, `.o_add_custom_group_menu .dropdown-menu .btn`));
}
export async function groupByMenu(el, fieldName) {
await toggleGroupByMenu(el);
await toggleAddCustomGroup(el);
await selectGroup(el, fieldName);
await applyGroup(el);
}
/** Favorite menu */
export async function toggleFavoriteMenu(el) {
await click(findItem(el, `.o_favorite_menu .dropdown-toggle`));
}
export async function deleteFavorite(el, favoriteFinder) {
const favorite = findItem(el, `.o_favorite_menu .o_menu_item`, favoriteFinder);
await click(findItem(favorite, "i.fa-trash-o"));
}
export async function toggleSaveFavorite(el) {
await mouseEnter(findItem(el, `.o_favorite_menu .o_add_favorite .dropdown-toggle`));
}
export async function editFavoriteName(el, name) {
const input = findItem(
el,
`.o_favorite_menu .o_add_favorite .dropdown-menu input[type="text"]`
);
input.value = name;
await triggerEvents(input, null, ["input", "change"]);
}
export async function saveFavorite(el) {
await click(findItem(el, `.o_favorite_menu .o_add_favorite .dropdown-menu button`));
}
/** Comparison menu */
export async function toggleComparisonMenu(el) {
await click(findItem(el, `.o_comparison_menu button.dropdown-toggle`));
}
/** Search bar */
export function getFacetTexts(target) {
const el = getNode(target);
return [...el.querySelectorAll(`div.o_searchview_facet`)].map((facet) =>
facet.innerText.trim()
);
}
export async function removeFacet(el, facetFinder = 0) {
const facet = findItem(el, `div.o_searchview_facet`, facetFinder);
await click(facet.querySelector("i.o_facet_remove"));
}
export async function editSearch(el, value) {
const input = findItem(el, `.o_searchview input`);
input.value = value;
await triggerEvent(input, null, "input");
}
export async function validateSearch(el) {
const input = findItem(el, `.o_searchview input`);
await triggerEvent(input, null, "keydown", { key: "Enter" });
}
/** Switch View */
export async function switchView(el, viewType) {
await click(findItem(el, `button.o_switch_view.o_${viewType}`));
}
/** Pager */
export function getPagerValue(el) {
const valueEl = findItem(el, ".o_pager .o_pager_value");
return valueEl.innerText.trim().split("-").map(Number);
}
export function getPagerLimit(el) {
const limitEl = findItem(el, ".o_pager .o_pager_limit");
return Number(limitEl.innerText.trim());
}
export async function pagerNext(el) {
await click(findItem(el, ".o_pager button.o_pager_next"));
}
export async function pagerPrevious(el) {
await click(findItem(el, ".o_pager button.o_pager_previous"));
}
export async function editPager(el, value) {
await click(findItem(el, ".o_pager .o_pager_value"));
await editInput(getNode(el), ".o_pager .o_pager_value.o_input", value);
}
/////////////////////////////////////
// Action Menu
/////////////////////////////////////
/**
* @param {EventTarget} el
* @param {string} [menuFinder="Action"]
* @returns {Promise}
*/
export async function toggleActionMenu(el, menuFinder = "Action") {
const dropdown = findItem(el, `.o_cp_action_menus button`, menuFinder);
await click(dropdown);
}

View file

@ -0,0 +1,141 @@
import { expect, test } from "@odoo/hoot";
import { Component, useState, xml } from "@odoo/owl";
import {
defineModels,
getPagerLimit,
getPagerValue,
models,
mountWithSearch,
pagerNext,
} from "@web/../tests/web_test_helpers";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { usePager } from "@web/search/pager_hook";
import { animationFrame } from "@odoo/hoot-mock";
class Foo extends models.Model {}
defineModels([Foo]);
test("pager is correctly displayed", async () => {
class TestComponent extends Component {
static components = { ControlPanel };
static template = xml`<ControlPanel />`;
static props = ["*"];
setup() {
usePager(() => ({
offset: 0,
limit: 10,
total: 50,
onUpdate: () => {},
}));
}
}
await mountWithSearch(TestComponent, {
resModel: "foo",
searchMenuTypes: [],
});
expect(`.o_pager`).toHaveCount(1);
expect(".o_pager button.o_pager_next").toHaveCount(1);
expect(".o_pager button.o_pager_previous").toHaveCount(1);
});
test.tags("desktop");
test("pager is correctly displayed on desktop", async () => {
class TestComponent extends Component {
static components = { ControlPanel };
static template = xml`<ControlPanel />`;
static props = ["*"];
setup() {
usePager(() => ({
offset: 0,
limit: 10,
total: 50,
onUpdate: () => {},
}));
}
}
await mountWithSearch(TestComponent, {
resModel: "foo",
searchMenuTypes: [],
});
expect(`.o_pager`).toHaveCount(1);
expect(getPagerValue()).toEqual([1, 10]);
expect(getPagerLimit()).toBe(50);
});
test("pager is correctly updated", async () => {
class TestComponent extends Component {
static components = { ControlPanel };
static template = xml`<ControlPanel />`;
static props = ["*"];
setup() {
this.state = useState({ offset: 0, limit: 10 });
usePager(() => ({
offset: this.state.offset,
limit: this.state.limit,
total: 50,
onUpdate: (newState) => {
Object.assign(this.state, newState);
},
}));
}
}
const component = await mountWithSearch(TestComponent, {
resModel: "foo",
searchMenuTypes: [],
});
expect(`.o_pager`).toHaveCount(1);
expect(component.state).toEqual({ offset: 0, limit: 10 });
await pagerNext();
expect(`.o_pager`).toHaveCount(1);
expect(component.state).toEqual({ offset: 10, limit: 10 });
component.state.offset = 20;
await animationFrame();
expect(`.o_pager`).toHaveCount(1);
expect(component.state).toEqual({ offset: 20, limit: 10 });
});
test.tags("desktop");
test("pager is correctly updated on desktop", async () => {
class TestComponent extends Component {
static components = { ControlPanel };
static template = xml`<ControlPanel />`;
static props = ["*"];
setup() {
this.state = useState({ offset: 0, limit: 10 });
usePager(() => ({
offset: this.state.offset,
limit: this.state.limit,
total: 50,
onUpdate: (newState) => {
Object.assign(this.state, newState);
},
}));
}
}
const component = await mountWithSearch(TestComponent, {
resModel: "foo",
searchMenuTypes: [],
});
expect(`.o_pager`).toHaveCount(1);
expect(getPagerValue()).toEqual([1, 10]);
expect(getPagerLimit()).toBe(50);
await pagerNext();
expect(`.o_pager`).toHaveCount(1);
expect(getPagerValue()).toEqual([11, 20]);
expect(getPagerLimit()).toBe(50);
component.state.offset = 20;
await animationFrame();
expect(`.o_pager`).toHaveCount(1);
expect(getPagerValue()).toEqual([21, 30]);
expect(getPagerLimit()).toBe(50);
});

View file

@ -1,133 +0,0 @@
/** @odoo-module **/
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { usePager } from "@web/search/pager_hook";
import { click, getFixture, nextTick } from "../helpers/utils";
import { makeWithSearch, setupControlPanelServiceRegistry } from "./helpers";
import { Component, useState, xml } from "@odoo/owl";
let target;
let serverData;
QUnit.module("Search", (hooks) => {
hooks.beforeEach(async () => {
serverData = {
models: {
foo: {
fields: {},
},
},
views: {
"foo,false,search": `<search/>`,
},
};
setupControlPanelServiceRegistry();
target = getFixture();
});
QUnit.module("usePager");
QUnit.test("pager is correctly displayed", async (assert) => {
class TestComponent extends Component {
setup() {
usePager(() => ({
offset: 0,
limit: 10,
total: 50,
onUpdate: () => {},
}));
}
}
TestComponent.components = { ControlPanel };
TestComponent.template = xml`<ControlPanel />`;
await makeWithSearch({
serverData,
resModel: "foo",
Component: TestComponent,
searchMenuTypes: [],
});
assert.containsOnce(target, ".o_pager");
assert.strictEqual(
target.querySelector(`.o_pager_counter .o_pager_value`).textContent.trim(),
"1-10"
);
assert.strictEqual(
target.querySelector(`.o_pager_counter span.o_pager_limit`).textContent.trim(),
"50"
);
});
QUnit.test("pager is correctly updated", async (assert) => {
class TestComponent extends Component {
setup() {
this.state = useState({ offset: 0, limit: 10 });
usePager(() => ({
offset: this.state.offset,
limit: this.state.limit,
total: 50,
onUpdate: (newState) => {
Object.assign(this.state, newState);
},
}));
}
}
TestComponent.components = { ControlPanel };
TestComponent.template = xml`<ControlPanel />`;
const comp = await makeWithSearch({
serverData,
resModel: "foo",
Component: TestComponent,
searchMenuTypes: [],
});
assert.containsOnce(target, ".o_pager");
assert.strictEqual(
target.querySelector(`.o_pager_counter .o_pager_value`).textContent.trim(),
"1-10"
);
assert.strictEqual(
target.querySelector(`.o_pager_counter span.o_pager_limit`).textContent.trim(),
"50"
);
assert.deepEqual(comp.state, {
offset: 0,
limit: 10,
});
await click(target.querySelector(`.o_pager button.o_pager_next`));
assert.containsOnce(target, ".o_pager");
assert.strictEqual(
target.querySelector(`.o_pager_counter .o_pager_value`).textContent.trim(),
"11-20"
);
assert.strictEqual(
target.querySelector(`.o_pager_counter span.o_pager_limit`).textContent.trim(),
"50"
);
assert.deepEqual(comp.state, {
offset: 10,
limit: 10,
});
comp.state.offset = 20;
await nextTick();
assert.containsOnce(target, ".o_pager");
assert.strictEqual(
target.querySelector(`.o_pager_counter .o_pager_value`).textContent.trim(),
"21-30"
);
assert.strictEqual(
target.querySelector(`.o_pager_counter span.o_pager_limit`).textContent.trim(),
"50"
);
assert.deepEqual(comp.state, {
offset: 20,
limit: 10,
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,267 @@
import { after, expect, test } from "@odoo/hoot";
import { queryFirst } from "@odoo/hoot-dom";
import { mockDate } from "@odoo/hoot-mock";
import { editValue } from "@web/../tests/core/tree_editor/condition_tree_editor_test_helpers";
import {
contains,
editFavorite,
getFacetTexts,
isItemSelected,
mockService,
mountWithSearch,
onRpc,
toggleMenuItem,
toggleSearchBarMenu,
} from "@web/../tests/web_test_helpers";
import { defineSearchBarModels } from "./models";
import { registry } from "@web/core/registry";
import { SearchBar } from "@web/search/search_bar/search_bar";
import { SearchBarMenu } from "@web/search/search_bar_menu/search_bar_menu";
const favoriteMenuRegistry = registry.category("favoriteMenu");
defineSearchBarModels();
test("simple rendering with no favorite (without ability to save)", async () => {
const registryItem = favoriteMenuRegistry.content["custom-favorite-item"];
favoriteMenuRegistry.remove("custom-favorite-item");
after(() => {
favoriteMenuRegistry.add("custom-favorite-item", registryItem[1], {
sequence: registryItem[1],
});
});
await mountWithSearch(
SearchBarMenu,
{
resModel: "foo",
searchMenuTypes: ["favorite"],
searchViewId: false,
},
{ getDisplayName: () => "Action Name" }
);
await toggleSearchBarMenu();
expect(`.o_favorite_menu .fa.fa-star`).toHaveCount(1);
expect(`.o_favorite_menu .o_dropdown_title`).toHaveText(/^favorites$/i);
expect(`.o_favorite_menu`).toHaveCount(1);
expect(`.o_favorite_menu .o_menu_item`).toHaveCount(0);
});
test("simple rendering with no favorite", async () => {
await mountWithSearch(
SearchBarMenu,
{
resModel: "foo",
searchMenuTypes: ["favorite"],
searchViewId: false,
},
{
getDisplayName: () => "Action Name",
}
);
await toggleSearchBarMenu();
expect(`.o_favorite_menu .fa.fa-star`).toHaveCount(1);
expect(`.o_favorite_menu .o_dropdown_title`).toHaveText(/^favorites$/i);
expect(`.o_favorite_menu`).toHaveCount(1);
expect(`.o_favorite_menu .dropdown-divider`).toHaveCount(0);
expect(`.o_favorite_menu .o_add_favorite`).toHaveCount(1);
});
test("edit an active favorite", async () => {
const irFilters = [
{
context: "{}",
domain: "[['foo', '=', 'qsdf']]",
id: 7,
is_default: true,
name: "My favorite",
sort: "[]",
user_ids: [2],
},
];
mockService("action", {
doAction(action) {
expect.step("edit favorite");
expect(action).toEqual({
context: {
form_view_ref: "base.ir_filters_view_edit_form",
},
type: "ir.actions.act_window",
res_model: "ir.filters",
views: [[false, "form"]],
res_id: 7,
});
},
});
await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["favorite"],
searchViewId: false,
searchViewArch: `<search/>`,
irFilters,
});
expect(getFacetTexts()).toEqual(["My favorite"]);
await toggleSearchBarMenu();
const favorite = queryFirst`.o_favorite_menu .dropdown-item`;
expect(favorite).toHaveText("My favorite");
expect(favorite).toHaveAttribute("role", "menuitemcheckbox");
expect(favorite).toHaveProperty("ariaChecked", "true");
expect(getFacetTexts()).toEqual(["My favorite"]);
expect(queryFirst`.o_favorite_menu .o_menu_item`).toHaveClass("selected");
await editFavorite("My favorite");
expect.verifySteps(["edit favorite"]);
});
test("default favorite is not activated if activateFavorite is set to false", async () => {
const searchBarMenu = await mountWithSearch(SearchBarMenu, {
resModel: "foo",
searchMenuTypes: ["favorite"],
searchViewId: false,
irFilters: [
{
context: "{}",
domain: "[('foo', '=', 'a')]",
id: 7,
is_default: true,
name: "My favorite",
sort: "[]",
user_ids: [2],
},
],
activateFavorite: false,
});
await toggleSearchBarMenu();
expect(isItemSelected("My favorite")).toBe(false);
expect(searchBarMenu.env.searchModel.domain).toEqual([]);
expect(getFacetTexts()).toEqual([]);
});
test(`toggle favorite correctly clears filter, groupbys and field "options"`, async () => {
mockDate("2019-07-31T13:43:00");
const searchBar = await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["filter", "groupBy", "favorite"],
searchViewId: false,
irFilters: [
{
context: `{"group_by": ["foo"]}`,
domain: "['!', ['foo', '=', 'qsdf']]",
id: 7,
is_default: false,
name: "My favorite",
sort: "[]",
user_ids: [2],
},
],
searchViewArch: `
<search>
<field string="Foo" name="foo"/>
<filter string="Date Field Filter" name="positive" date="date_field" default_period="year"/>
<filter string="Date Field Groupby" name="coolName" context="{'group_by': 'date_field'}"/>
</search>
`,
context: {
search_default_positive: true,
search_default_coolName: true,
search_default_foo: "a",
},
});
expect(searchBar.env.searchModel.domain).toEqual([
"&",
["foo", "ilike", "a"],
"&",
["date_field", ">=", "2019-01-01"],
["date_field", "<=", "2019-12-31"],
]);
expect(searchBar.env.searchModel.groupBy).toEqual(["date_field:month"]);
expect(getFacetTexts()).toEqual([
"Foo\na",
"Date Field Filter: 2019",
"Date Field Groupby: Month",
]);
// activate the unique existing favorite
await toggleSearchBarMenu();
const favorite = queryFirst`.o_favorite_menu .dropdown-item`;
expect(favorite).toHaveText("My favorite");
expect(favorite).toHaveAttribute("role", "menuitemcheckbox");
expect(favorite).toHaveProperty("ariaChecked", "false");
await toggleMenuItem("My favorite");
expect(favorite).toHaveProperty("ariaChecked", "true");
expect(searchBar.env.searchModel.domain).toEqual(["!", ["foo", "=", "qsdf"]]);
expect(searchBar.env.searchModel.groupBy).toEqual(["foo"]);
expect(getFacetTexts()).toEqual(["My favorite"]);
});
test("edit a favorite with a groupby", async () => {
const irFilters = [
{
context: "{ 'some_key': 'some_value', 'group_by': ['bar'] }",
domain: "[('foo', 'ilike', 'abc')]",
id: 1,
is_default: true,
name: "My favorite",
sort: "[]",
user_ids: [2],
},
];
onRpc("/web/domain/validate", () => true);
await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["groupBy"], // we need it to have facet (see facets getter in search_model)
searchViewId: false,
searchViewArch: `<search/>`,
irFilters,
});
expect(getFacetTexts()).toEqual(["My favorite"]);
await toggleSearchBarMenu();
expect(`.o_group_by_menu .o_menu_item:not(.o_add_custom_group_menu)`).toHaveCount(0);
await contains(`.o_searchview_facet_label`).click();
expect(`.modal`).toHaveCount(1);
await editValue("abcde");
await contains(`.modal footer button`).click();
expect(`.modal`).toHaveCount(0);
expect(getFacetTexts()).toEqual(["Foo contains abcde", "Bar"]);
await toggleSearchBarMenu();
expect(`.o_group_by_menu .o_menu_item:not(.o_add_custom_group_menu)`).toHaveCount(0);
});
test("shared favorites are partially shown if there is more than 4", async () => {
const irFilters = [];
for (let i = 1; i < 6; i++) {
irFilters.push({
context: "{}",
domain: "[('foo', '=', 'a')]",
id: i,
is_default: false,
name: "My favorite" + i,
sort: "[]",
user_ids: [],
});
}
await mountWithSearch(SearchBarMenu, {
resModel: "foo",
searchMenuTypes: ["favorite"],
searchViewId: false,
irFilters,
activateFavorite: false,
});
await toggleSearchBarMenu();
expect(".o_favorite_menu .o_favorite_item").toHaveCount(3);
expect(".o_favorite_menu .o_expand_shared_favorites").toHaveCount(1);
await contains(".o_favorite_menu .o_expand_shared_favorites").click();
expect(".o_favorite_menu .o_expand_shared_favorites").toHaveCount(0);
expect(".o_favorite_menu .o_favorite_item").toHaveCount(5);
});

View file

@ -0,0 +1,382 @@
import { expect, test } from "@odoo/hoot";
import { queryAllTexts, queryFirst } from "@odoo/hoot-dom";
import {
contains,
getFacetTexts,
isItemSelected,
isOptionSelected,
mountWithSearch,
removeFacet,
toggleMenuItem,
toggleMenuItemOption,
toggleSearchBarMenu,
} from "@web/../tests/web_test_helpers";
import { defineSearchBarModels } from "./models";
import { animationFrame } from "@odoo/hoot-mock";
import { SearchBar } from "@web/search/search_bar/search_bar";
import { SearchBarMenu } from "@web/search/search_bar_menu/search_bar_menu";
defineSearchBarModels();
test("simple rendering with neither groupbys nor groupable fields", async () => {
await mountWithSearch(SearchBarMenu, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewArch: `<search />`,
searchViewId: false,
searchViewFields: {},
});
await toggleSearchBarMenu();
expect(`.o_menu_item`).toHaveCount(0);
expect(`.dropdown-divider`).toHaveCount(0);
expect(`.o_add_custom_group_menu`).toHaveCount(0);
});
test("simple rendering with no groupby", async () => {
await mountWithSearch(SearchBarMenu, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
});
await toggleSearchBarMenu();
expect(`.o_menu_item`).toHaveCount(1);
expect(`.dropdown-divider`).toHaveCount(0);
expect(`.o_add_custom_group_menu`).toHaveCount(1);
});
test("simple rendering with a single groupby", async () => {
await mountWithSearch(SearchBarMenu, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Foo" name="group_by_foo" context="{'group_by': 'foo'}"/>
</search>
`,
});
await toggleSearchBarMenu();
expect(`.o_menu_item`).toHaveCount(2);
const menuItem = queryFirst`.o_menu_item`;
expect(menuItem).toHaveText("Foo");
expect(menuItem).toHaveAttribute("role", "menuitemcheckbox");
expect(menuItem).toHaveProperty("ariaChecked", "false");
expect(".dropdown-divider").toHaveCount(1);
expect(".o_add_custom_group_menu").toHaveCount(1);
});
test(`toggle a "simple" groupby in groupby menu works`, async () => {
const searchBar = await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Foo" name="group_by_foo" context="{'group_by': 'foo'}"/>
</search>
`,
});
await toggleSearchBarMenu();
expect(searchBar.env.searchModel.groupBy).toEqual([]);
expect(getFacetTexts()).toEqual([]);
expect(isItemSelected("Foo")).toBe(false);
const menuItem = queryFirst`.o_menu_item`;
expect(menuItem).toHaveText("Foo");
expect(menuItem).toHaveAttribute("role", "menuitemcheckbox");
expect(menuItem).toHaveProperty("ariaChecked", "false");
await toggleMenuItem("Foo");
expect(menuItem).toHaveProperty("ariaChecked", "true");
expect(searchBar.env.searchModel.groupBy).toEqual(["foo"]);
expect(getFacetTexts()).toEqual(["Foo"]);
expect(`.o_searchview .o_searchview_facet .o_searchview_facet_label`).toHaveCount(1);
expect(isItemSelected("Foo")).toBe(true);
await toggleMenuItem("Foo");
expect(searchBar.env.searchModel.groupBy).toEqual([]);
expect(getFacetTexts()).toEqual([]);
expect(isItemSelected("Foo")).toBe(false);
});
test(`toggle a "simple" groupby quickly does not crash`, async () => {
await mountWithSearch(SearchBarMenu, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Foo" name="group_by_foo" context="{'group_by': 'foo'}"/>
</search>
`,
});
await toggleSearchBarMenu();
await toggleMenuItem("Foo");
await toggleMenuItem("Foo");
await animationFrame();
expect(isItemSelected("Foo")).toBe(false);
});
test(`remove a "Group By" facet properly unchecks groupbys in groupby menu`, async () => {
const searchBar = await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Foo" name="group_by_foo" context="{'group_by': 'foo'}"/>
</search>
`,
context: { search_default_group_by_foo: 1 },
});
await toggleSearchBarMenu();
expect(getFacetTexts()).toEqual(["Foo"]);
expect(searchBar.env.searchModel.groupBy).toEqual(["foo"]);
expect(isItemSelected("Foo")).toBe(true);
await removeFacet("Foo");
expect(getFacetTexts()).toEqual([]);
expect(searchBar.env.searchModel.groupBy).toEqual([]);
await toggleSearchBarMenu();
expect(isItemSelected("Foo")).toBe(false);
});
test("group by a date field using interval works", async () => {
const searchBar = await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Date" name="date" context="{'group_by': 'date_field:week'}"/>
</search>
`,
context: { search_default_date: 1 },
});
await toggleSearchBarMenu();
expect(searchBar.env.searchModel.groupBy).toEqual(["date_field:week"]);
await toggleMenuItem("Date");
expect(isOptionSelected("Date", "Week")).toBe(true);
expect(queryAllTexts`.o_item_option`).toEqual(["Year", "Quarter", "Month", "Week", "Day"]);
await toggleMenuItemOption("Date", "Year");
expect(searchBar.env.searchModel.groupBy).toEqual(["date_field:year", "date_field:week"]);
expect(getFacetTexts()).toEqual(["Date: Year\n>\nDate: Week"]);
expect(isOptionSelected("Date", "Year")).toBe(true);
expect(isOptionSelected("Date", "Week")).toBe(true);
await toggleMenuItemOption("Date", "Month");
expect(searchBar.env.searchModel.groupBy).toEqual([
"date_field:year",
"date_field:month",
"date_field:week",
]);
expect(getFacetTexts()).toEqual(["Date: Year\n>\nDate: Month\n>\nDate: Week"]);
expect(isOptionSelected("Date", "Year")).toBe(true);
expect(isOptionSelected("Date", "Month")).toBe(true);
expect(isOptionSelected("Date", "Week")).toBe(true);
await toggleMenuItemOption("Date", "Week");
expect(searchBar.env.searchModel.groupBy).toEqual(["date_field:year", "date_field:month"]);
expect(getFacetTexts()).toEqual(["Date: Year\n>\nDate: Month"]);
expect(isOptionSelected("Date", "Year")).toBe(true);
expect(isOptionSelected("Date", "Month")).toBe(true);
await toggleMenuItemOption("Date", "Month");
expect(searchBar.env.searchModel.groupBy).toEqual(["date_field:year"]);
expect(getFacetTexts()).toEqual(["Date: Year"]);
expect(isOptionSelected("Date", "Year")).toBe(true);
await toggleMenuItemOption("Date", "Year");
expect(searchBar.env.searchModel.groupBy).toEqual([]);
expect(getFacetTexts()).toEqual([]);
});
test("interval options are correctly grouped and ordered", async () => {
await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Bar" name="bar" context="{'group_by': 'bar'}"/>
<filter string="Date" name="date" context="{'group_by': 'date_field'}"/>
<filter string="Foo" name="foo" context="{'group_by': 'foo'}"/>
</search>
`,
context: { search_default_bar: 1 },
});
expect(getFacetTexts()).toEqual(["Bar"]);
await toggleSearchBarMenu();
await toggleMenuItem("Date");
await toggleMenuItemOption("Date", "Week");
expect(getFacetTexts()).toEqual(["Bar\n>\nDate: Week"]);
await toggleMenuItemOption("Date", "Day");
expect(getFacetTexts()).toEqual(["Bar\n>\nDate: Week\n>\nDate: Day"]);
await toggleMenuItemOption("Date", "Year");
expect(getFacetTexts()).toEqual(["Bar\n>\nDate: Year\n>\nDate: Week\n>\nDate: Day"]);
await toggleMenuItem("Foo");
expect(getFacetTexts()).toEqual(["Bar\n>\nDate: Year\n>\nDate: Week\n>\nDate: Day\n>\nFoo"]);
await toggleMenuItemOption("Date", "Quarter");
expect(getFacetTexts()).toEqual([
"Bar\n>\nDate: Year\n>\nDate: Quarter\n>\nDate: Week\n>\nDate: Day\n>\nFoo",
]);
await toggleMenuItem("Bar");
expect(getFacetTexts()).toEqual([
"Date: Year\n>\nDate: Quarter\n>\nDate: Week\n>\nDate: Day\n>\nFoo",
]);
await toggleMenuItemOption("Date", "Week");
expect(getFacetTexts()).toEqual(["Date: Year\n>\nDate: Quarter\n>\nDate: Day\n>\nFoo"]);
});
test("default groupbys can be ordered", async () => {
const searchBar = await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/>
<filter string="Date" name="date" context="{'group_by': 'date_field:week'}"/>
</search>
`,
context: { search_default_birthday: 2, search_default_date: 1 },
});
// the default groupbys should be activated in the right order
expect(searchBar.env.searchModel.groupBy).toEqual(["date_field:week", "birthday:month"]);
expect(getFacetTexts()).toEqual(["Date: Week\n>\nBirthday: Month"]);
});
test("a separator in groupbys does not cause problems", async () => {
await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Date" name="coolName" context="{'group_by': 'date_field'}"/>
<separator/>
<filter string="Bar" name="superName" context="{'group_by': 'bar'}"/>
</search>
`,
});
await toggleSearchBarMenu();
await toggleMenuItem("Date");
await toggleMenuItemOption("Date", "Day");
expect(isItemSelected("Date")).toBe(true);
expect(isItemSelected("Bar")).toBe(false);
expect(isOptionSelected("Date", "Day")).toBe(true);
expect(getFacetTexts()).toEqual(["Date: Day"]);
await toggleMenuItem("Bar");
expect(isItemSelected("Date")).toBe(true);
expect(isItemSelected("Bar")).toBe(true);
expect(isOptionSelected("Date", "Day")).toBe(true);
expect(getFacetTexts()).toEqual(["Date: Day\n>\nBar"]);
await toggleMenuItemOption("Date", "Quarter");
expect(isItemSelected("Date")).toBe(true);
expect(isItemSelected("Bar")).toBe(true);
expect(isOptionSelected("Date", "Quarter")).toBe(true);
expect(isOptionSelected("Date", "Day")).toBe(true);
expect(getFacetTexts()).toEqual(["Date: Quarter\n>\nDate: Day\n>\nBar"]);
await toggleMenuItem("Bar");
expect(isItemSelected("Date")).toBe(true);
expect(isItemSelected("Bar")).toBe(false);
expect(isOptionSelected("Date", "Quarter")).toBe(true);
expect(isOptionSelected("Date", "Day")).toBe(true);
expect(getFacetTexts()).toEqual(["Date: Quarter\n>\nDate: Day"]);
await contains(`.o_facet_remove`).click();
expect(getFacetTexts()).toEqual([]);
await toggleSearchBarMenu();
await toggleMenuItem("Date");
expect(isItemSelected("Date")).toBe(false);
expect(isItemSelected("Bar")).toBe(false);
expect(isOptionSelected("Date", "Quarter")).toBe(false);
expect(isOptionSelected("Date", "Day")).toBe(false);
});
test("falsy search default groupbys are not activated", async () => {
const searchBar = await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["groupBy"],
searchViewId: false,
searchViewArch: `
<search>
<filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/>
<filter string="Date" name="date" context="{'group_by': 'foo'}"/>
</search>
`,
context: { search_default_birthday: false, search_default_foo: 0 },
});
expect(searchBar.env.searchModel.groupBy).toEqual([]);
expect(getFacetTexts()).toEqual([]);
});
test("Custom group by menu is displayed when hideCustomGroupBy is not set", async () => {
await mountWithSearch(SearchBarMenu, {
resModel: "foo",
searchViewId: false,
searchViewArch: `
<search>
<filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/>
<filter string="Date" name="date" context="{'group_by': 'foo'}"/>
</search>
`,
searchMenuTypes: ["groupBy"],
});
await toggleSearchBarMenu();
expect(`.o_add_custom_group_menu`).toHaveCount(1);
});
test("Custom group by menu is displayed when hideCustomGroupBy is false", async () => {
await mountWithSearch(SearchBarMenu, {
resModel: "foo",
searchViewId: false,
searchViewArch: `
<search>
<filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/>
<filter string="Date" name="date" context="{'group_by': 'foo'}"/>
</search>
`,
hideCustomGroupBy: false,
searchMenuTypes: ["groupBy"],
});
await toggleSearchBarMenu();
expect(`.o_add_custom_group_menu`).toHaveCount(1);
});
test("Custom group by menu is displayed when hideCustomGroupBy is true", async () => {
await mountWithSearch(SearchBarMenu, {
resModel: "foo",
searchViewId: false,
searchViewArch: `
<search>
<filter string="Birthday" name="birthday" context="{'group_by': 'birthday'}"/>
<filter string="Date" name="date" context="{'group_by': 'foo'}"/>
</search>
`,
hideCustomGroupBy: true,
searchMenuTypes: ["groupBy"],
});
await toggleSearchBarMenu();
expect(`.o_add_custom_group_menu`).toHaveCount(0);
});

View file

@ -0,0 +1,37 @@
import { defineModels, fields, models } from "@web/../tests/web_test_helpers";
export class Foo extends models.Model {
bar = fields.Many2one({ relation: "partner" });
foo = fields.Char();
birthday = fields.Date();
date_field = fields.Date({ string: "Date" });
parent_id = fields.Many2one({ string: "Parent", relation: "parent.model" });
properties = fields.Properties({
definition_record: "parent_id",
definition_record_field: "properties_definition",
});
_views = {
search: `
<search>
<filter name="birthday" date="birthday"/>
<filter name="date_field" date="date_field"/>
</search>
`,
};
}
export class Partner extends models.Model {
name = fields.Char();
}
export class ParentModel extends models.Model {
_name = "parent.model";
name = fields.Char();
properties_definition = fields.PropertiesDefinition();
}
export function defineSearchBarModels() {
defineModels([Foo, Partner, ParentModel]);
}

File diff suppressed because it is too large Load diff

View file

@ -1,980 +0,0 @@
/** @odoo-module **/
import { patchDate, patchWithCleanup } from "@web/../tests/helpers/utils";
import { makeWithSearch, setupControlPanelServiceRegistry } from "./helpers";
import { Component, xml } from "@odoo/owl";
class TestComponent extends Component {}
TestComponent.template = xml`<div class="o_test_component"/>`;
async function makeSearchModel(params) {
const component = await makeWithSearch({
Component: TestComponent,
resModel: "foo",
searchViewId: false,
...params,
});
return component.env.searchModel;
}
function sanitizeSearchItems(model) {
// We should not access searchItems but there is a problem with getSearchItems:
// comparisons are not sent back in some cases
const searchItems = Object.values(model.searchItems);
return searchItems.map((searchItem) => {
const copy = Object.assign({}, searchItem);
delete copy.groupId;
delete copy.groupNumber;
delete copy.id;
return copy;
});
}
let serverData;
QUnit.module("Search", (hooks) => {
hooks.beforeEach(() => {
serverData = {
models: {
foo: {
fields: {
display_name: { string: "Displayed name", type: "char" },
foo: {
string: "Foo",
type: "char",
default: "My little Foo Value",
store: true,
sortable: true,
},
date_field: { string: "Date", type: "date", store: true, sortable: true },
float_field: { string: "Float", type: "float" },
bar: { string: "Bar", type: "many2one", relation: "partner" },
},
records: [],
},
partner: {
fields: {
foo: { string: "Foo", type: "char" },
bar: { string: "Bar", type: "boolean" },
int_field: { string: "Int Field", type: "integer", group_operator: "sum" },
company_id: { string: "company", type: "many2one", relation: "company" },
company_ids: {
string: "Companies",
type: "many2many",
relation: "company",
},
category_id: { string: "category", type: "many2one", relation: "category" },
state: {
string: "State",
type: "selection",
selection: [
["abc", "ABC"],
["def", "DEF"],
["ghi", "GHI"],
],
},
},
records: [
{
id: 1,
bar: true,
foo: "yop",
int_field: 1,
company_ids: [3],
company_id: 3,
state: "abc",
category_id: 6,
},
{
id: 2,
bar: true,
foo: "blip",
int_field: 2,
company_ids: [3],
company_id: 5,
state: "def",
category_id: 7,
},
{
id: 3,
bar: true,
foo: "gnap",
int_field: 4,
company_ids: [],
company_id: 3,
state: "ghi",
category_id: 7,
},
{
id: 4,
bar: false,
foo: "blip",
int_field: 8,
company_ids: [5],
company_id: 5,
state: "ghi",
category_id: 7,
},
],
},
company: {
fields: {
name: { string: "Display Name", type: "char" },
parent_id: {
string: "Parent company",
type: "many2one",
relation: "company",
},
category_id: { string: "Category", type: "many2one", relation: "category" },
},
records: [
{ id: 3, name: "asustek", category_id: 6 },
{ id: 5, name: "agrolait", category_id: 7 },
],
},
category: {
fields: {
name: { string: "Category Name", type: "char" },
},
records: [
{ id: 6, name: "gold" },
{ id: 7, name: "silver" },
],
},
},
views: {
"foo,false,search": `<search/>`,
"partner,false,search": `<search/>`,
},
};
setupControlPanelServiceRegistry();
});
QUnit.module("SearchModel");
QUnit.test("parsing empty arch", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({ serverData });
assert.deepEqual(sanitizeSearchItems(model), []);
});
QUnit.test("parsing one field tag", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<field name="bar"/>
</search>
`,
});
assert.deepEqual(sanitizeSearchItems(model), [
{
description: "Bar",
fieldName: "bar",
fieldType: "many2one",
type: "field",
},
]);
});
QUnit.test("parsing one separator tag", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<separator/>
</search>
`,
});
assert.deepEqual(sanitizeSearchItems(model), []);
});
QUnit.test("parsing one separator tag and one field tag", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<separator/>
<field name="bar"/>
</search>
`,
});
assert.deepEqual(sanitizeSearchItems(model), [
{
description: "Bar",
fieldName: "bar",
fieldType: "many2one",
type: "field",
},
]);
});
QUnit.test("parsing one filter tag", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="filter" string="Hello" domain="[]"/>
</search>
`,
});
assert.deepEqual(sanitizeSearchItems(model), [
{
description: "Hello",
domain: "[]",
name: "filter",
type: "filter",
},
]);
});
QUnit.test(
"parsing one filter tag with default_period date attribute",
async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="date_filter" string="Date" date="date_field" default_period="this_year,last_year"/>
</search>
`,
});
const dateFilterId = model.getSearchItems((f) => f.type === "dateFilter")[0].id;
assert.deepEqual(sanitizeSearchItems(model), [
{
defaultGeneratorIds: ["this_year", "last_year"],
description: "Date",
fieldName: "date_field",
fieldType: "date",
type: "dateFilter",
name: "date_filter",
},
{
comparisonOptionId: "previous_period",
dateFilterId,
description: "Date: Previous Period",
type: "comparison",
},
{
comparisonOptionId: "previous_year",
dateFilterId,
description: "Date: Previous Year",
type: "comparison",
},
]);
}
);
QUnit.test("parsing one filter tag with date attribute ", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="date_filter" string="Date" date="date_field"/>
</search>
`,
});
const dateFilterId = model.getSearchItems((f) => f.type === "dateFilter")[0].id;
assert.deepEqual(sanitizeSearchItems(model), [
{
defaultGeneratorIds: ["this_month"],
description: "Date",
fieldName: "date_field",
fieldType: "date",
name: "date_filter",
type: "dateFilter",
},
{
comparisonOptionId: "previous_period",
dateFilterId,
description: "Date: Previous Period",
type: "comparison",
},
{
comparisonOptionId: "previous_year",
dateFilterId,
description: "Date: Previous Year",
type: "comparison",
},
]);
});
QUnit.test("parsing one groupBy tag", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="groupby" string="Hi" context="{ 'group_by': 'date_field:day'}"/>
</search>
`,
});
assert.deepEqual(sanitizeSearchItems(model), [
{
defaultIntervalId: "day",
description: "Hi",
fieldName: "date_field",
fieldType: "date",
name: "groupby",
type: "dateGroupBy",
},
]);
});
QUnit.test("parsing two filter tags", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="filter_1" string="Hello One" domain="[]"/>
<filter name="filter_2" string="Hello Two" domain="[('bar', '=', 3)]"/>
</search>
`,
});
assert.deepEqual(sanitizeSearchItems(model), [
{
description: "Hello One",
domain: "[]",
name: "filter_1",
type: "filter",
},
{
description: "Hello Two",
domain: "[('bar', '=', 3)]",
name: "filter_2",
type: "filter",
},
]);
});
QUnit.test("parsing two filter tags separated by a separator", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="filter_1" string="Hello One" domain="[]"/>
<separator/>
<filter name="filter_2" string="Hello Two" domain="[('bar', '=', 3)]"/>
</search>
`,
});
assert.deepEqual(sanitizeSearchItems(model), [
{
description: "Hello One",
domain: "[]",
name: "filter_1",
type: "filter",
},
{
description: "Hello Two",
domain: "[('bar', '=', 3)]",
name: "filter_2",
type: "filter",
},
]);
});
QUnit.test("parsing one filter tag and one field", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="filter" string="Hello" domain="[]"/>
<field name="bar"/>
</search>
`,
});
assert.deepEqual(sanitizeSearchItems(model), [
{
description: "Hello",
domain: "[]",
name: "filter",
type: "filter",
},
{
description: "Bar",
fieldName: "bar",
fieldType: "many2one",
type: "field",
},
]);
});
QUnit.test("parsing two field tags", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<field name="foo"/>
<field name="bar"/>
</search>
`,
});
assert.deepEqual(sanitizeSearchItems(model), [
{
description: "Foo",
fieldName: "foo",
fieldType: "char",
type: "field",
},
{
description: "Bar",
fieldName: "bar",
fieldType: "many2one",
type: "field",
},
]);
});
QUnit.test("parsing a searchpanel tag", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<searchpanel/>
</search>
`,
config: { viewType: "kanban" },
});
assert.deepEqual(model.getSections(), []);
});
QUnit.test("parsing a searchpanel field select one", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<searchpanel>
<field name="company_id"/>
</searchpanel>
</search>
`,
resModel: "partner",
config: { viewType: "kanban" },
});
const sections = model.getSections();
for (const section of sections) {
section.values = [...section.values];
}
assert.deepEqual(sections, [
{
activeValueId: false,
color: null,
description: "company",
empty: false,
enableCounters: false,
expand: false,
fieldName: "company_id",
hierarchize: true,
icon: "fa-folder",
id: 1,
limit: 200,
parentField: "parent_id",
rootIds: [false, 3, 5],
type: "category",
values: [
[
false,
{
bold: true,
childrenIds: [],
display_name: "All",
id: false,
parentId: false,
},
],
[
3,
{
childrenIds: [],
display_name: "asustek",
id: 3,
parentId: false,
parent_id: false,
},
],
[
5,
{
childrenIds: [],
display_name: "agrolait",
id: 5,
parentId: false,
parent_id: false,
},
],
],
},
]);
});
QUnit.test("parsing a searchpanel field select multi", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<searchpanel>
<field name="company_id" select="multi"/>
</searchpanel>
</search>
`,
resModel: "partner",
config: { viewType: "kanban" },
});
const sections = model.getSections();
for (const section of sections) {
section.values = [...section.values];
}
assert.deepEqual(sections, [
{
color: null,
description: "company",
domain: "[]",
empty: false,
enableCounters: false,
expand: false,
fieldName: "company_id",
groupBy: null,
icon: "fa-filter",
id: 1,
limit: 200,
type: "filter",
values: [
[
3,
{
checked: false,
display_name: "asustek",
id: 3,
},
],
[
5,
{
checked: false,
display_name: "agrolait",
id: 5,
},
],
],
},
]);
});
QUnit.test("parsing a filter and a dateFilter", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="filter" string="Filter" domain="[['foo', '=', 'a']]"/>
<filter name="date_filter" string="Date" date="date_field"/>
</search>
`,
});
const groupNumbers = model.getSearchItems(() => true).map((i) => i.groupNumber);
assert.deepEqual(groupNumbers, [1, 1]);
});
QUnit.test("parsing a groupBy and a dateGroupBy", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="group_by" context="{ 'group_by': 'foo'}"/>
<filter name="date_groupBy" string="DateGroupBy" context="{'group_by': 'date_field:day'}"/>
</search>
`,
});
const groupNumbers = model.getSearchItems(() => true).map((i) => i.groupNumber);
assert.deepEqual(groupNumbers, [1, 1]);
});
QUnit.test("parsing a filter and a groupBy", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="filter" string="Filter" domain="[['foo', '=', 'a']]"/>
<filter name="group_by" context="{ 'group_by': 'foo'}"/>
</search>
`,
});
const groupNumbers = model.getSearchItems(() => true).map((i) => i.groupNumber);
assert.deepEqual(groupNumbers, [1, 2]);
});
QUnit.test("parsing a groupBy and a filter", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="group_by" context="{ 'group_by': 'foo'}"/>
<filter name="filter" string="Filter" domain="[['foo', '=', 'a']]"/>
</search>
`,
});
const groupNumbers = model.getSearchItems(() => true).map((i) => i.groupNumber);
assert.deepEqual(groupNumbers, [2, 1]);
});
QUnit.test("process search default group by", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="group_by" context="{ 'group_by': 'foo'}"/>
</search>
`,
context: { search_default_group_by: 14 },
});
assert.deepEqual(sanitizeSearchItems(model), [
{
defaultRank: 14,
description: "Foo",
fieldName: "foo",
fieldType: "char",
name: "group_by",
type: "groupBy",
isDefault: true,
},
]);
});
QUnit.test("process and toggle a field with a context to evaluate", async function (assert) {
assert.expect(2);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<field name="foo" context="{ 'a': self }"/>
</search>
`,
});
assert.deepEqual(sanitizeSearchItems(model), [
{
context: "{ 'a': self }",
description: "Foo",
fieldName: "foo",
fieldType: "char",
type: "field",
},
]);
model.addAutoCompletionValues(1, { label: "7", operator: "=", value: 7 });
assert.deepEqual(model.context, { a: [7], lang: "en", tz: "taht", uid: 7 });
});
QUnit.test("process favorite filters", async function (assert) {
assert.expect(1);
const model = await makeSearchModel({
serverData,
irFilters: [
{
user_id: [2, "Mitchell Admin"],
name: "Sorted filter",
id: 5,
context: `{"group_by":["foo","bar"]}`,
sort: '["foo", "-bar"]',
domain: "[('user_id', '=', uid)]",
is_default: false,
model_id: "res.partner",
action_id: false,
},
],
});
assert.deepEqual(sanitizeSearchItems(model), [
{
context: {},
description: "Sorted filter",
domain: "[('user_id', '=', uid)]",
groupBys: ["foo", "bar"],
orderBy: [
{
asc: true,
name: "foo",
},
{
asc: false,
name: "bar",
},
],
removable: true,
serverSideId: 5,
type: "favorite",
userId: 2,
},
]);
});
QUnit.test("process dynamic filters", async function (assert) {
assert.expect(1);
assert.expect(1);
const model = await makeSearchModel({
serverData,
dynamicFilters: [
{
description: "Quick search",
domain: [["id", "in", [1, 3, 4]]],
},
],
});
assert.deepEqual(sanitizeSearchItems(model), [
{
description: "Quick search",
domain: [["id", "in", [1, 3, 4]]],
isDefault: true,
type: "filter",
},
]);
});
QUnit.test("process a dynamic filter with a isDefault key to false", async function (assert) {
const model = await makeSearchModel({
serverData,
dynamicFilters: [
{
description: "Quick search",
domain: [],
is_default: false,
},
],
});
assert.deepEqual(sanitizeSearchItems(model), [
{
description: "Quick search",
domain: [],
isDefault: false,
type: "filter",
},
]);
});
QUnit.test("toggle a filter", async function (assert) {
assert.expect(1);
assert.expect(3);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="filter" string="Filter" domain="[['foo', '=', 'a']]"/>
</search>
`,
});
const filterId = Object.keys(model.searchItems).map((key) => Number(key))[0];
assert.deepEqual([], model.domain);
model.toggleSearchItem(filterId);
assert.deepEqual([["foo", "=", "a"]], model.domain);
model.toggleSearchItem(filterId);
assert.deepEqual([], model.domain);
});
QUnit.test("toggle a date filter", async function (assert) {
assert.expect(3);
patchDate(2019, 0, 6, 15, 0, 0);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="date_filter" date="date_field" string="DateFilter"/>
</search>
`,
});
const filterId = Object.keys(model.searchItems).map((key) => Number(key))[0];
model.toggleDateFilter(filterId);
assert.deepEqual(
["&", ["date_field", ">=", "2019-01-01"], ["date_field", "<=", "2019-01-31"]],
model.domain
);
model.toggleDateFilter(filterId, "first_quarter");
assert.deepEqual(
[
"|",
"&",
["date_field", ">=", "2019-01-01"],
["date_field", "<=", "2019-01-31"],
"&",
["date_field", ">=", "2019-01-01"],
["date_field", "<=", "2019-03-31"],
],
model.domain
);
model.toggleDateFilter(filterId, "this_year");
assert.deepEqual([], model.domain);
});
QUnit.test("toggle a groupBy", async function (assert) {
assert.expect(3);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="groupBy" string="GroupBy" context="{'group_by': 'foo'}"/>
</search>
`,
});
const filterId = Object.keys(model.searchItems).map((key) => Number(key))[0];
assert.deepEqual(model.groupBy, []);
model.toggleSearchItem(filterId);
assert.deepEqual(model.groupBy, ["foo"]);
model.toggleSearchItem(filterId);
assert.deepEqual(model.groupBy, []);
});
QUnit.test("toggle a date groupBy", async function (assert) {
assert.expect(5);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="date_groupBy" string="DateGroupBy" context="{'group_by': 'date_field:day'}"/>
</search>
`,
});
const filterId = Object.keys(model.searchItems).map((key) => Number(key))[0];
assert.deepEqual(model.groupBy, []);
model.toggleDateGroupBy(filterId);
assert.deepEqual(model.groupBy, ["date_field:day"]);
model.toggleDateGroupBy(filterId, "week");
assert.deepEqual(model.groupBy, ["date_field:week", "date_field:day"]);
model.toggleDateGroupBy(filterId);
assert.deepEqual(model.groupBy, ["date_field:week"]);
model.toggleDateGroupBy(filterId, "week");
assert.deepEqual(model.groupBy, []);
});
QUnit.test("create a new groupBy", async function (assert) {
assert.expect(2);
const model = await makeSearchModel({ serverData });
model.createNewGroupBy("foo");
assert.deepEqual(sanitizeSearchItems(model), [
{
custom: true,
description: "Foo",
fieldName: "foo",
fieldType: "char",
type: "groupBy",
},
]);
assert.deepEqual(model.groupBy, ["foo"]);
});
QUnit.test("create a new dateGroupBy", async function (assert) {
assert.expect(2);
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<filter name="foo" string="Foo" context="{'group_by': 'foo'}"/>
</search>
`,
});
model.createNewGroupBy("date_field");
assert.deepEqual(sanitizeSearchItems(model), [
{
description: "Foo",
fieldName: "foo",
fieldType: "char",
name: "foo",
type: "groupBy",
},
{
custom: true,
defaultIntervalId: "month",
description: "Date",
fieldName: "date_field",
fieldType: "date",
type: "dateGroupBy",
},
]);
assert.deepEqual(model.groupBy, ["date_field:month"]);
});
QUnit.test("dynamic domains evaluation", async function (assert) {
patchDate(2021, 8, 17, 10, 0, 0);
patchWithCleanup(Date.prototype, {
getTimezoneOffset() {
return -120;
},
});
const searchViewArch = `
<search>
<filter name="filter_0" domain="[('datetime', '=', (datetime.datetime.combine(context_today(), datetime.time(0,0,0)).to_utc()))]"/>
<filter name="filter_1" domain="[('date', '=', context_today() + relativedelta(days=-365))]"/>
<filter name="filter_2" domain="[('create_date', '&gt;', (context_today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d'))]"/>
<filter name="filter_3" domain="[('date_deadline', '&lt;', current_date)]"/>
</search>
`;
const evaluatedDomains = [
[["datetime", "=", "2021-09-16 22:00:00"]],
[["date", "=", "2020-09-17"]],
[["create_date", ">", "2021-09-16"]],
[["date_deadline", "<", "2021-09-17"]],
];
const model = await makeSearchModel({ serverData, searchViewArch });
for (let i = 0; i < evaluatedDomains.length; i++) {
model.toggleSearchItem(i + 1);
assert.deepEqual(model.domain, evaluatedDomains[i]);
model.toggleSearchItem(i + 1);
}
});
QUnit.test("dynamic domains evaluation using global context", async function (assert) {
const searchViewArch = `
<search>
<filter name="filter" domain="[('date_deadline', '&lt;', context.get('my_date'))]"/>
</search>
`;
const model = await makeSearchModel({
serverData,
searchViewArch,
context: {
my_date: "2021-09-17",
},
});
model.toggleSearchItem(1);
assert.deepEqual(model.domain, [["date_deadline", "<", "2021-09-17"]]);
});
QUnit.test("no search items created for search panel sections", async function (assert) {
const model = await makeSearchModel({
serverData,
searchViewArch: `
<search>
<searchpanel>
<field name="company_id"/>
<field name="company_id" select="multi"/>
</searchpanel>
</search>
`,
resModel: "partner",
config: { viewType: "kanban" },
});
const sections = model.getSections();
assert.strictEqual(sections.length, 2);
assert.deepEqual(sanitizeSearchItems(model), []);
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,136 @@
import { describe, expect, test } from "@odoo/hoot";
import { queryAllTexts } from "@odoo/hoot-dom";
import { Component, xml } from "@odoo/owl";
import {
contains,
defineModels,
fields,
models,
mountWithSearch,
} from "@web/../tests/web_test_helpers";
import { SearchPanel } from "@web/search/search_panel/search_panel";
class Partner extends models.Model {
name = fields.Char();
foo = fields.Char();
bar = fields.Boolean();
int_field = fields.Integer({ string: "Int Field", aggregator: "sum" });
category_id = fields.Many2one({ string: "category", relation: "category" });
state = fields.Selection({
selection: [
["abc", "ABC"],
["def", "DEF"],
["ghi", "GHI"],
],
});
_records = [
{
id: 1,
bar: true,
foo: "yop",
int_field: 1,
state: "abc",
category_id: 6,
},
{
id: 2,
bar: true,
foo: "blip",
int_field: 2,
state: "def",
category_id: 7,
},
{
id: 3,
bar: true,
foo: "gnap",
int_field: 4,
state: "ghi",
category_id: 7,
},
{
id: 4,
bar: false,
foo: "blip",
int_field: 8,
state: "ghi",
category_id: 7,
},
];
_views = {
search: /* xml */ `
<search>
<filter name="false_domain" string="False Domain" domain="[(0, '=', 1)]"/>
<filter name="filter" string="Filter" domain="[('bar', '=', true)]"/>
<filter name="true_domain" string="True Domain" domain="[(1, '=', 1)]"/>
<filter name="group_by_bar" string="Bar" context="{ 'group_by': 'bar' }"/>
<searchpanel view_types="kanban,list,toy">
<field name="category_id" expand="1"/>
</searchpanel>
</search>
`,
};
}
class Category extends models.Model {
name = fields.Char({ string: "Category Name" });
_records = [
{ id: 6, name: "gold" },
{ id: 7, name: "silver" },
];
}
defineModels([Partner, Category]);
describe.current.tags("mobile");
test("basic search panel rendering", async () => {
class Parent extends Component {
static components = { SearchPanel };
static template = xml`<SearchPanel/>`;
static props = ["*"];
}
await mountWithSearch(Parent, {
resModel: "partner",
searchViewId: false,
});
expect(".o_search_panel .o-dropdown").toHaveCount(1);
expect(".o_search_panel .o-dropdown").toHaveText("category");
await contains(".o_search_panel .o-dropdown").click();
expect(".o_search_panel_section.o_search_panel_category").toHaveCount(1);
expect(".o_search_panel_category_value").toHaveCount(3);
expect(queryAllTexts(".o_search_panel_field li")).toEqual(["All", "gold", "silver"]);
await contains(".o_search_panel_category_value:nth-of-type(2) header").click();
expect(".o_search_panel .o-dropdown").toHaveText("gold");
expect(".o_search_panel a").toHaveCount(1);
await contains(".o_search_panel a").click();
expect(".o_search_panel .o-dropdown").toHaveText("category");
});
test("Dropdown closes on category selection", async () => {
class Parent extends Component {
static components = { SearchPanel };
static template = xml`<SearchPanel/>`;
static props = ["*"];
}
await mountWithSearch(Parent, {
resModel: "partner",
searchViewId: false,
});
expect(".o-dropdown--menu").toHaveCount(0);
await contains(".o_search_panel .o-dropdown").click();
expect(".o-dropdown--menu").toHaveCount(1);
await contains(".o_search_panel_category_value:nth-of-type(2) header").click();
expect(".o-dropdown--menu").toHaveCount(0);
});

View file

@ -0,0 +1,307 @@
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { mockDate, mockTimeZone } from "@odoo/hoot-mock";
import {
allowTranslations,
patchTranslations,
patchWithCleanup,
} from "@web/../tests/web_test_helpers";
import { Domain } from "@web/core/domain";
import { localization } from "@web/core/l10n/localization";
import { constructDateDomain } from "@web/search/utils/dates";
describe.current.tags("headless");
const dateSearchItem = {
fieldName: "date_field",
fieldType: "date",
optionsParams: {
customOptions: [],
endMonth: 0,
endYear: 0,
startMonth: -2,
startYear: -2,
},
type: "dateFilter",
};
const dateTimeSearchItem = {
...dateSearchItem,
fieldType: "datetime",
};
beforeEach(() => {
mockTimeZone(0);
patchWithCleanup(localization, { direction: "ltr" });
allowTranslations();
});
test("construct simple domain based on date field (no comparisonOptionId)", () => {
mockDate("2020-06-01T13:00:00");
const referenceMoment = luxon.DateTime.local();
let domain = constructDateDomain(referenceMoment, dateSearchItem, []);
expect(domain).toEqual({
domain: new Domain(`[]`),
description: "",
});
domain = constructDateDomain(referenceMoment, dateSearchItem, ["month", "year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2020-06-01"), ("date_field", "<=", "2020-06-30")]`
),
description: "June 2020",
});
domain = constructDateDomain(referenceMoment, dateSearchItem, ["second_quarter", "year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2020-04-01"), ("date_field", "<=", "2020-06-30")]`
),
description: "Q2 2020",
});
domain = constructDateDomain(referenceMoment, dateSearchItem, ["year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2020-01-01"), ("date_field", "<=", "2020-12-31")]`
),
description: "2020",
});
});
test("construct simple domain based on date field (no comparisonOptionId) - UTC+2", () => {
mockTimeZone(2);
mockDate("2020-06-01T00:00:00");
const referenceMoment = luxon.DateTime.local();
let domain = constructDateDomain(referenceMoment, dateSearchItem, []);
expect(domain).toEqual({
domain: new Domain(`[]`),
description: "",
});
domain = constructDateDomain(referenceMoment, dateSearchItem, ["month", "year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2020-06-01"), ("date_field", "<=", "2020-06-30")]`
),
description: "June 2020",
});
domain = constructDateDomain(referenceMoment, dateSearchItem, ["second_quarter", "year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2020-04-01"), ("date_field", "<=", "2020-06-30")]`
),
description: "Q2 2020",
});
domain = constructDateDomain(referenceMoment, dateSearchItem, ["year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2020-01-01"), ("date_field", "<=", "2020-12-31")]`
),
description: "2020",
});
});
test("construct simple domain based on datetime field (no comparisonOptionId)", () => {
mockDate("2020-06-01T13:00:00");
const referenceMoment = luxon.DateTime.local();
let domain = constructDateDomain(referenceMoment, dateTimeSearchItem, ["month", "year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2020-06-01 00:00:00"), ("date_field", "<=", "2020-06-30 23:59:59")]`
),
description: "June 2020",
});
domain = constructDateDomain(referenceMoment, dateTimeSearchItem, ["second_quarter", "year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2020-04-01 00:00:00"), ("date_field", "<=", "2020-06-30 23:59:59")]`
),
description: "Q2 2020",
});
domain = constructDateDomain(referenceMoment, dateTimeSearchItem, ["year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2020-01-01 00:00:00"), ("date_field", "<=", "2020-12-31 23:59:59")]`
),
description: "2020",
});
});
test("construct simple domain based on datetime field (no comparisonOptionId) - UTC+2", () => {
mockTimeZone(2);
mockDate("2020-06-01T00:00:00");
const referenceMoment = luxon.DateTime.local();
let domain = constructDateDomain(referenceMoment, dateTimeSearchItem, ["month", "year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2020-05-31 22:00:00"), ("date_field", "<=", "2020-06-30 21:59:59")]`
),
description: "June 2020",
});
domain = constructDateDomain(referenceMoment, dateTimeSearchItem, ["second_quarter", "year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2020-03-31 22:00:00"), ("date_field", "<=", "2020-06-30 21:59:59")]`
),
description: "Q2 2020",
});
domain = constructDateDomain(referenceMoment, dateTimeSearchItem, ["year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2019-12-31 22:00:00"), ("date_field", "<=", "2020-12-31 21:59:59")]`
),
description: "2020",
});
});
test("construct domain based on date field (no comparisonOptionId)", () => {
mockDate("2020-01-01T12:00:00");
const referenceMoment = luxon.DateTime.local();
let domain = constructDateDomain(referenceMoment, dateSearchItem, [
"month",
"first_quarter",
"year",
]);
expect(domain).toEqual({
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2020-01-01"), ("date_field", "<=", "2020-01-31"), ` +
`"&", ("date_field", ">=", "2020-01-01"), ("date_field", "<=", "2020-03-31")` +
"]"
),
description: "January 2020/Q1 2020",
});
domain = constructDateDomain(referenceMoment, dateSearchItem, [
"second_quarter",
"year",
"year-1",
]);
expect(domain).toEqual({
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2019-04-01"), ("date_field", "<=", "2019-06-30"), ` +
`"&", ("date_field", ">=", "2020-04-01"), ("date_field", "<=", "2020-06-30")` +
"]"
),
description: "Q2 2019/Q2 2020",
});
domain = constructDateDomain(referenceMoment, dateSearchItem, ["year", "month", "month-2"]);
expect(domain).toEqual({
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2020-01-01"), ("date_field", "<=", "2020-01-31"), ` +
`"&", ("date_field", ">=", "2020-11-01"), ("date_field", "<=", "2020-11-30")` +
"]"
),
description: "January 2020/November 2020",
});
});
test("construct domain based on datetime field (no comparisonOptionId)", () => {
mockDate("2020-01-01T12:00:00");
const referenceMoment = luxon.DateTime.local();
let domain = constructDateDomain(referenceMoment, dateTimeSearchItem, [
"month",
"first_quarter",
"year",
]);
expect(domain).toEqual({
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2020-01-01 00:00:00"), ("date_field", "<=", "2020-01-31 23:59:59"), ` +
`"&", ("date_field", ">=", "2020-01-01 00:00:00"), ("date_field", "<=", "2020-03-31 23:59:59")` +
"]"
),
description: "January 2020/Q1 2020",
});
domain = constructDateDomain(referenceMoment, dateTimeSearchItem, [
"second_quarter",
"year",
"year-1",
]);
expect(domain).toEqual({
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2019-04-01 00:00:00"), ("date_field", "<=", "2019-06-30 23:59:59"), ` +
`"&", ("date_field", ">=", "2020-04-01 00:00:00"), ("date_field", "<=", "2020-06-30 23:59:59")` +
"]"
),
description: "Q2 2019/Q2 2020",
});
domain = constructDateDomain(referenceMoment, dateTimeSearchItem, ["year", "month", "month-2"]);
expect(domain).toEqual({
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2020-01-01 00:00:00"), ("date_field", "<=", "2020-01-31 23:59:59"), ` +
`"&", ("date_field", ">=", "2020-11-01 00:00:00"), ("date_field", "<=", "2020-11-30 23:59:59")` +
"]"
),
description: "January 2020/November 2020",
});
});
test("Quarter option: custom translation", async () => {
mockDate("2020-06-01T13:00:00");
const referenceMoment = luxon.DateTime.local().setLocale("en");
patchTranslations({ web: { Q2: "Deuxième trimestre de l'an de grâce" } });
const domain = constructDateDomain(referenceMoment, dateSearchItem, ["second_quarter", "year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2020-04-01"), ("date_field", "<=", "2020-06-30")]`
),
description: "Deuxième trimestre de l'an de grâce 2020",
});
});
test("Quarter option: right to left", async () => {
mockDate("2020-06-01T13:00:00");
const referenceMoment = luxon.DateTime.local().setLocale("en");
patchWithCleanup(localization, { direction: "rtl" });
const domain = constructDateDomain(referenceMoment, dateSearchItem, ["second_quarter", "year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2020-04-01"), ("date_field", "<=", "2020-06-30")]`
),
description: "2020 Q2",
});
});
test("Quarter option: custom translation and right to left", async () => {
mockDate("2020-06-01T13:00:00");
const referenceMoment = luxon.DateTime.local().setLocale("en");
patchWithCleanup(localization, { direction: "rtl" });
patchTranslations({ web: { Q2: "2e Trimestre" } });
const domain = constructDateDomain(referenceMoment, dateSearchItem, ["second_quarter", "year"]);
expect(domain).toEqual({
domain: new Domain(
`["&", ("date_field", ">=", "2020-04-01"), ("date_field", "<=", "2020-06-30")]`
),
description: "2020 2e Trimestre",
});
});

View file

@ -1,593 +0,0 @@
/** @odoo-module **/
import { constructDateDomain } from "@web/search/utils/dates";
import { defaultLocalization } from "@web/../tests/helpers/mock_services";
import { Domain } from "@web/core/domain";
import { localization } from "@web/core/l10n/localization";
import { patch, unpatch } from "@web/core/utils/patch";
import { patchDate } from "@web/../tests/helpers/utils";
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import { translatedTerms } from "@web/core/l10n/translation";
const { DateTime } = luxon;
function patchTimeZone(offset) {
const fixedZone = new luxon.FixedOffsetZone.instance(offset);
const originalZone = luxon.Settings.defaultZone;
luxon.Settings.defaultZone = fixedZone.name;
registerCleanup(() => {
luxon.Settings.defaultZone = originalZone;
});
}
QUnit.module("Search", () => {
QUnit.module("SearchUtils", {
beforeEach() {
patchTimeZone(0);
},
});
QUnit.test(
"construct simple domain based on date field (no comparisonOptionId)",
function (assert) {
patchDate(2020, 5, 1, 13, 0, 0);
const referenceMoment = DateTime.local();
assert.deepEqual(constructDateDomain(referenceMoment, "date_field", "date", []), {
domain: new Domain(`[]`),
description: "",
});
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "date", [
"this_month",
"this_year",
]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2020-06-01"), ("date_field", "<=", "2020-06-30")]`
),
description: "June 2020",
}
);
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "date", [
"second_quarter",
"this_year",
]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2020-04-01"), ("date_field", "<=", "2020-06-30")]`
),
description: "Q2 2020",
}
);
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "date", ["this_year"]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2020-01-01"), ("date_field", "<=", "2020-12-31")]`
),
description: "2020",
}
);
}
);
QUnit.test(
"construct simple domain based on date field (no comparisonOptionId) - UTC+2",
(assert) => {
patchTimeZone(120);
patchDate(2020, 5, 1, 0, 0, 0);
const referenceMoment = DateTime.local();
assert.deepEqual(constructDateDomain(referenceMoment, "date_field", "date", []), {
domain: new Domain(`[]`),
description: "",
});
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "date", [
"this_month",
"this_year",
]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2020-06-01"), ("date_field", "<=", "2020-06-30")]`
),
description: "June 2020",
}
);
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "date", [
"second_quarter",
"this_year",
]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2020-04-01"), ("date_field", "<=", "2020-06-30")]`
),
description: "Q2 2020",
}
);
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "date", ["this_year"]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2020-01-01"), ("date_field", "<=", "2020-12-31")]`
),
description: "2020",
}
);
}
);
QUnit.test(
"construct simple domain based on datetime field (no comparisonOptionId)",
function (assert) {
patchDate(2020, 5, 1, 13, 0, 0);
const referenceMoment = DateTime.local();
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "datetime", [
"this_month",
"this_year",
]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2020-06-01 00:00:00"), ("date_field", "<=", "2020-06-30 23:59:59")]`
),
description: "June 2020",
}
);
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "datetime", [
"second_quarter",
"this_year",
]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2020-04-01 00:00:00"), ("date_field", "<=", "2020-06-30 23:59:59")]`
),
description: "Q2 2020",
}
);
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "datetime", ["this_year"]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2020-01-01 00:00:00"), ("date_field", "<=", "2020-12-31 23:59:59")]`
),
description: "2020",
}
);
}
);
QUnit.test(
"construct simple domain based on datetime field (no comparisonOptionId) - UTC+2",
(assert) => {
patchTimeZone(120);
patchDate(2020, 5, 1, 0, 0, 0);
const referenceMoment = DateTime.local();
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "datetime", [
"this_month",
"this_year",
]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2020-05-31 22:00:00"), ("date_field", "<=", "2020-06-30 21:59:59")]`
),
description: "June 2020",
}
);
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "datetime", [
"second_quarter",
"this_year",
]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2020-03-31 22:00:00"), ("date_field", "<=", "2020-06-30 21:59:59")]`
),
description: "Q2 2020",
}
);
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "datetime", ["this_year"]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2019-12-31 22:00:00"), ("date_field", "<=", "2020-12-31 21:59:59")]`
),
description: "2020",
}
);
}
);
QUnit.test("construct domain based on date field (no comparisonOptionId)", function (assert) {
patchDate(2020, 0, 1, 12, 0, 0);
const referenceMoment = DateTime.local();
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "date", [
"this_month",
"first_quarter",
"this_year",
]),
{
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2020-01-01"), ("date_field", "<=", "2020-01-31"), ` +
`"&", ("date_field", ">=", "2020-01-01"), ("date_field", "<=", "2020-03-31")` +
"]"
),
description: "January 2020/Q1 2020",
}
);
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "date", [
"second_quarter",
"this_year",
"last_year",
]),
{
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2019-04-01"), ("date_field", "<=", "2019-06-30"), ` +
`"&", ("date_field", ">=", "2020-04-01"), ("date_field", "<=", "2020-06-30")` +
"]"
),
description: "Q2 2019/Q2 2020",
}
);
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "date", [
"this_year",
"this_month",
"antepenultimate_month",
]),
{
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2020-01-01"), ("date_field", "<=", "2020-01-31"), ` +
`"&", ("date_field", ">=", "2020-11-01"), ("date_field", "<=", "2020-11-30")` +
"]"
),
description: "January 2020/November 2020",
}
);
});
QUnit.test(
"construct domain based on datetime field (no comparisonOptionId)",
function (assert) {
patchDate(2020, 0, 1, 12, 0, 0);
const referenceMoment = DateTime.local();
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "datetime", [
"this_month",
"first_quarter",
"this_year",
]),
{
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2020-01-01 00:00:00"), ("date_field", "<=", "2020-01-31 23:59:59"), ` +
`"&", ("date_field", ">=", "2020-01-01 00:00:00"), ("date_field", "<=", "2020-03-31 23:59:59")` +
"]"
),
description: "January 2020/Q1 2020",
}
);
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "datetime", [
"second_quarter",
"this_year",
"last_year",
]),
{
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2019-04-01 00:00:00"), ("date_field", "<=", "2019-06-30 23:59:59"), ` +
`"&", ("date_field", ">=", "2020-04-01 00:00:00"), ("date_field", "<=", "2020-06-30 23:59:59")` +
"]"
),
description: "Q2 2019/Q2 2020",
}
);
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "datetime", [
"this_year",
"this_month",
"antepenultimate_month",
]),
{
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2020-01-01 00:00:00"), ("date_field", "<=", "2020-01-31 23:59:59"), ` +
`"&", ("date_field", ">=", "2020-11-01 00:00:00"), ("date_field", "<=", "2020-11-30 23:59:59")` +
"]"
),
description: "January 2020/November 2020",
}
);
}
);
QUnit.test(
'construct comparison domain based on date field and option "previous_period"',
function (assert) {
patchDate(2020, 0, 1, 12, 0, 0);
const referenceMoment = DateTime.local();
assert.deepEqual(
constructDateDomain(
referenceMoment,
"date_field",
"date",
["this_month", "first_quarter", "this_year"],
"previous_period"
),
{
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2019-10-01"), ("date_field", "<=", "2019-10-31"), ` +
`"|", ` +
`"&", ("date_field", ">=", "2019-11-01"), ("date_field", "<=", "2019-11-30"), ` +
`"&", ("date_field", ">=", "2019-12-01"), ("date_field", "<=", "2019-12-31")` +
"]"
),
description: "October 2019/November 2019/December 2019",
}
);
assert.deepEqual(
constructDateDomain(
referenceMoment,
"date_field",
"date",
["second_quarter", "this_year", "last_year"],
"previous_period"
),
{
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2018-01-01"), ("date_field", "<=", "2018-03-31"), ` +
`"&", ("date_field", ">=", "2019-01-01"), ("date_field", "<=", "2019-03-31")` +
"]"
),
description: "Q1 2018/Q1 2019",
}
);
assert.deepEqual(
constructDateDomain(
referenceMoment,
"date_field",
"date",
["this_year", "antepenultimate_year", "this_month", "antepenultimate_month"],
"previous_period"
),
{
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2015-02-01"), ("date_field", "<=", "2015-02-28"), ` +
`"|", ` +
`"&", ("date_field", ">=", "2015-12-01"), ("date_field", "<=", "2015-12-31"), ` +
`"|", ` +
`"&", ("date_field", ">=", "2017-02-01"), ("date_field", "<=", "2017-02-28"), ` +
`"&", ("date_field", ">=", "2017-12-01"), ("date_field", "<=", "2017-12-31")` +
"]"
),
description: "February 2015/December 2015/February 2017/December 2017",
}
);
assert.deepEqual(
constructDateDomain(
referenceMoment,
"date_field",
"date",
["this_year", "last_year"],
"previous_period"
),
{
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2017-01-01"), ("date_field", "<=", "2017-12-31"), ` +
`"&", ("date_field", ">=", "2018-01-01"), ("date_field", "<=", "2018-12-31")` +
"]"
),
description: "2017/2018",
}
);
assert.deepEqual(
constructDateDomain(
referenceMoment,
"date_field",
"date",
["second_quarter", "third_quarter", "last_year"],
"previous_period"
),
{
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2018-10-01"), ("date_field", "<=", "2018-12-31"), ` +
`"&", ("date_field", ">=", "2019-01-01"), ("date_field", "<=", "2019-03-31")` +
"]"
),
description: "Q4 2018/Q1 2019",
}
);
}
);
QUnit.test(
'construct comparison domain based on datetime field and option "previous_year"',
function (assert) {
patchDate(2020, 5, 1, 13, 0, 0);
const referenceMoment = DateTime.local();
assert.deepEqual(
constructDateDomain(
referenceMoment,
"date_field",
"datetime",
["this_month", "first_quarter", "this_year"],
"previous_year"
),
{
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2019-06-01 00:00:00"), ("date_field", "<=", "2019-06-30 23:59:59"), ` +
`"&", ("date_field", ">=", "2019-01-01 00:00:00"), ("date_field", "<=", "2019-03-31 23:59:59")` +
"]"
),
description: "June 2019/Q1 2019",
}
);
assert.deepEqual(
constructDateDomain(
referenceMoment,
"date_field",
"datetime",
["second_quarter", "this_year", "last_year"],
"previous_year"
),
{
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2018-04-01 00:00:00"), ("date_field", "<=", "2018-06-30 23:59:59"), ` +
`"&", ("date_field", ">=", "2019-04-01 00:00:00"), ("date_field", "<=", "2019-06-30 23:59:59")` +
"]"
),
description: "Q2 2018/Q2 2019",
}
);
assert.deepEqual(
constructDateDomain(
referenceMoment,
"date_field",
"datetime",
["this_year", "antepenultimate_year", "this_month", "antepenultimate_month"],
"previous_year"
),
{
domain: new Domain(
"[" +
`"|", ` +
`"&", ("date_field", ">=", "2017-04-01 00:00:00"), ("date_field", "<=", "2017-04-30 23:59:59"), ` +
`"|", ` +
`"&", ("date_field", ">=", "2017-06-01 00:00:00"), ("date_field", "<=", "2017-06-30 23:59:59"), ` +
`"|", ` +
`"&", ("date_field", ">=", "2019-04-01 00:00:00"), ("date_field", "<=", "2019-04-30 23:59:59"), ` +
`"&", ("date_field", ">=", "2019-06-01 00:00:00"), ("date_field", "<=", "2019-06-30 23:59:59")` +
"]"
),
description: "April 2017/June 2017/April 2019/June 2019",
}
);
}
);
QUnit.test("Quarter option: custom translation", async function (assert) {
patchDate(2020, 5, 1, 13, 0, 0);
const referenceMoment = DateTime.local().setLocale("en");
patch(translatedTerms, "add_translations", { Q2: "Deuxième trimestre de l'an de grâce" });
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "date", [
"second_quarter",
"this_year",
]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2020-04-01"), ("date_field", "<=", "2020-06-30")]`
),
description: "Deuxième trimestre de l'an de grâce 2020",
},
"Quarter term should be translated"
);
unpatch(translatedTerms, "add_translations");
});
QUnit.test("Quarter option: right to left", async function (assert) {
patchDate(2020, 5, 1, 13, 0, 0);
const referenceMoment = DateTime.local().setLocale("en");
patch(
localization,
"rtl_localization",
Object.assign({}, defaultLocalization, { direction: "rtl" })
);
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "date", [
"second_quarter",
"this_year",
]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2020-04-01"), ("date_field", "<=", "2020-06-30")]`
),
description: "2020 Q2",
},
"Notation should be right to left"
);
unpatch(localization, "rtl_localization");
});
QUnit.test("Quarter option: custom translation and right to left", async function (assert) {
patchDate(2020, 5, 1, 13, 0, 0);
const referenceMoment = DateTime.local().setLocale("en");
patch(
localization,
"rtl_localization",
Object.assign({}, defaultLocalization, { direction: "rtl" })
);
patch(translatedTerms, "add_translations", { Q2: "2e Trimestre" });
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "date", [
"second_quarter",
"this_year",
]),
{
domain: new Domain(
`["&", ("date_field", ">=", "2020-04-01"), ("date_field", "<=", "2020-06-30")]`
),
description: "2020 2e Trimestre",
},
"Quarter term should be translated and notation should be right to left"
);
unpatch(localization, "rtl_localization");
unpatch(translatedTerms, "add_translations");
});
QUnit.skip(
"Moment.js localization does not affect formatted domain dates",
async function (assert) {
patchDate(2020, 5, 1, 13, 0, 0);
const initialLocale = moment.locale();
moment.defineLocale("addoneForTest", {
postformat: function (string) {
return string.replace(/\d/g, (match) => (1 + parseInt(match)) % 10);
},
});
const referenceMoment = moment().locale("addoneForTest");
assert.deepEqual(
constructDateDomain(referenceMoment, "date_field", "date", [
"this_month",
"this_year",
]),
{
domain: `["&", ["date_field", ">=", "2020-06-01"], ["date_field", "<=", "2020-06-30"]]`,
description: "June 3131",
},
"Numbers in domain should not use addoneForTest locale"
);
moment.locale(initialLocale);
moment.updateLocale("addoneForTest", null);
}
);
});

View file

@ -0,0 +1,347 @@
import { expect, test } from "@odoo/hoot";
import { animationFrame } from "@odoo/hoot-mock";
import { Component, onWillStart, onWillUpdateProps, useState, useSubEnv, xml } from "@odoo/owl";
import {
defineModels,
fields,
getMenuItemTexts,
models,
mountWithCleanup,
mountWithSearch,
onRpc,
toggleMenuItem,
toggleSearchBarMenu,
} from "@web/../tests/web_test_helpers";
import { SearchBarMenu } from "@web/search/search_bar_menu/search_bar_menu";
import { WithSearch } from "@web/search/with_search/with_search";
class Animal extends models.Model {
name = fields.Char();
birthday = fields.Date({ groupable: false });
type = fields.Selection({
groupable: false,
selection: [
["omnivorous", "Omnivorous"],
["herbivorous", "Herbivorous"],
["carnivorous", "Carnivorous"],
],
});
_views = {
[["search", 1]]: `
<search>
<filter name="filter" string="True domain" domain="[(1, '=', 1)]"/>
<filter name="group_by" context="{ 'group_by': 'name' }"/>
</search>
`,
};
}
defineModels([Animal]);
test("simple rendering", async () => {
class TestComponent extends Component {
static props = ["*"];
static template = xml`<div class="o_test_component">Test component content</div>`;
}
await mountWithSearch(TestComponent, {
resModel: "animal",
});
expect(".o_test_component").toHaveCount(1);
expect(".o_test_component").toHaveText("Test component content");
});
test("search model in sub env", async () => {
class TestComponent extends Component {
static props = ["*"];
static template = xml`<div class="o_test_component">Test component content</div>`;
}
const component = await mountWithSearch(TestComponent, {
resModel: "animal",
});
expect(component.env.searchModel).not.toBeEmpty();
});
test("search query props are passed as props to concrete component", async () => {
class TestComponent extends Component {
static props = ["*"];
static template = xml`<div class="o_test_component">Test component content</div>`;
setup() {
expect.step("setup");
const { context, domain, groupBy, orderBy } = this.props;
expect(context).toEqual({
allowed_company_ids: [1],
lang: "en",
tz: "taht",
uid: 7,
key: "val",
});
expect(domain).toEqual([[0, "=", 1]]);
expect(groupBy).toEqual(["birthday"]);
expect(orderBy).toEqual([{ name: "bar", asc: true }]);
}
}
await mountWithSearch(TestComponent, {
resModel: "animal",
domain: [[0, "=", 1]],
groupBy: ["birthday"],
context: { key: "val" },
orderBy: [{ name: "bar", asc: true }],
});
expect.verifySteps(["setup"]);
});
test("do not load search view description by default", async () => {
class TestComponent extends Component {
static props = ["*"];
static template = xml`<div class="o_test_component">Test component content</div>`;
}
onRpc("get_views", ({ method }) => {
expect.step(method);
throw new Error("No get_views should be done");
});
await mountWithSearch(TestComponent, {
resModel: "animal",
});
expect.verifySteps([]);
});
test("load search view description if not provided and loadSearchView=true", async () => {
class TestComponent extends Component {
static props = ["*"];
static template = xml`<div class="o_test_component">Test component content</div>`;
}
onRpc("get_views", ({ method, kwargs }) => {
expect.step(method);
delete kwargs.options.mobile;
expect(kwargs).toMatchObject({
options: {
action_id: false,
load_filters: false,
toolbar: false,
embedded_action_id: false,
embedded_parent_res_id: false,
},
views: [[false, "search"]],
});
});
await mountWithSearch(TestComponent, {
resModel: "animal",
searchViewId: false,
});
expect.verifySteps(["get_views"]);
});
test("do not load the search view description if provided even if loadSearchView=true", async () => {
class TestComponent extends Component {
static props = ["*"];
static template = xml`<div class="o_test_component">Test component content</div>`;
}
onRpc("get_views", ({ method }) => {
expect.step(method);
throw new Error("No get_views should be done");
});
await mountWithSearch(TestComponent, {
resModel: "animal",
searchViewArch: "<search/>",
searchViewFields: {},
searchViewId: false,
});
expect.verifySteps([]);
});
test("load view description if it is not complete and loadSearchView=true", async () => {
class TestComponent extends Component {
static props = ["*"];
static template = xml`<div class="o_test_component">Test component content</div>`;
}
onRpc("get_views", ({ method, kwargs }) => {
expect.step(method);
delete kwargs.options.mobile;
expect(kwargs.options).toEqual({
action_id: false,
load_filters: true,
toolbar: false,
embedded_action_id: false,
embedded_parent_res_id: false,
});
});
await mountWithSearch(TestComponent, {
resModel: "animal",
searchViewArch: "<search/>",
searchViewFields: {},
searchViewId: true,
loadIrFilters: true,
});
expect.verifySteps(["get_views"]);
});
test("load view description with given id if it is not provided and loadSearchView=true", async () => {
class TestComponent extends Component {
static props = ["*"];
static components = { SearchBarMenu };
static template = xml`<div class="o_test_component"><SearchBarMenu/></div>`;
}
onRpc("get_views", ({ method, kwargs }) => {
expect.step(method);
expect(kwargs.views).toEqual([[1, "search"]]);
});
await mountWithSearch(TestComponent, {
resModel: "animal",
searchViewId: 1,
});
expect.verifySteps(["get_views"]);
await toggleSearchBarMenu();
expect(getMenuItemTexts()).toEqual([
"True domain",
"Custom Filter...",
"Name",
"Custom Group\nCreated on\nDisplay name\nLast Modified on\nName",
"Save current search",
]);
});
test("toggle a filter render the underlying component with an updated domain", async () => {
class TestComponent extends Component {
static props = ["*"];
static components = { SearchBarMenu };
static template = xml`<div class="o_test_component"><SearchBarMenu/></div>`;
setup() {
onWillStart(() => {
expect.step("willStart");
expect(this.props.domain).toEqual([]);
});
onWillUpdateProps((nextProps) => {
expect.step("willUpdateProps");
expect(nextProps.domain).toEqual([[1, "=", 1]]);
});
}
}
await mountWithSearch(TestComponent, {
resModel: "animal",
searchViewId: 1,
});
expect.verifySteps(["willStart"]);
await toggleSearchBarMenu();
await toggleMenuItem("True domain");
expect.verifySteps(["willUpdateProps"]);
});
test("react to prop 'domain' changes", async () => {
class TestComponent extends Component {
static props = ["*"];
static template = xml`<div class="o_test_component">Test component content</div>`;
setup() {
onWillStart(() => {
expect.step("willStart");
expect(this.props.domain).toEqual([["type", "=", "carnivorous"]]);
});
onWillUpdateProps((nextProps) => {
expect.step("willUpdateProps");
expect(nextProps.domain).toEqual([["type", "=", "herbivorous"]]);
});
}
}
class Parent extends Component {
static props = ["*"];
static template = xml`
<WithSearch t-props="searchState" t-slot-scope="search">
<TestComponent domain="search.domain"/>
</WithSearch>
`;
static components = { WithSearch, TestComponent };
setup() {
useSubEnv({ config: {} });
this.searchState = useState({
resModel: "animal",
domain: [["type", "=", "carnivorous"]],
});
}
}
const parent = await mountWithCleanup(Parent);
expect.verifySteps(["willStart"]);
parent.searchState.domain = [["type", "=", "herbivorous"]];
await animationFrame();
expect.verifySteps(["willUpdateProps"]);
});
test("search defaults are removed from context at reload", async function () {
const context = {
search_default_x: true,
searchpanel_default_y: true,
};
class TestComponent extends Component {
static template = xml`<div class="o_test_component">Test component content</div>`;
static props = { context: Object };
setup() {
onWillStart(() => {
expect.step("willStart");
expect(this.props.context).toEqual({
lang: "en",
tz: "taht",
uid: 7,
allowed_company_ids: [1],
});
});
onWillUpdateProps((nextProps) => {
expect.step("willUpdateProps");
expect(nextProps.context).toEqual({
lang: "en",
tz: "taht",
uid: 7,
allowed_company_ids: [1],
});
});
}
}
class Parent extends Component {
static props = ["*"];
static template = xml`
<WithSearch t-props="searchState" t-slot-scope="search">
<TestComponent
context="search.context"
/>
</WithSearch>
`;
static components = { WithSearch, TestComponent };
setup() {
useSubEnv({ config: {} });
this.searchState = useState({
resModel: "animal",
domain: [["type", "=", "carnivorous"]],
context,
});
}
}
const parent = await mountWithCleanup(Parent);
expect.verifySteps(["willStart"]);
expect(parent.searchState.context).toEqual(context);
parent.searchState.domain = [["type", "=", "herbivorous"]];
await animationFrame();
expect.verifySteps(["willUpdateProps"]);
expect(parent.searchState.context).toEqual(context);
});

View file

@ -1,391 +0,0 @@
/** @odoo-module **/
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { getFixture, nextTick } from "@web/../tests/helpers/utils";
import { FilterMenu } from "@web/search/filter_menu/filter_menu";
import { GroupByMenu } from "@web/search/group_by_menu/group_by_menu";
import { WithSearch } from "@web/search/with_search/with_search";
import { mount } from "../helpers/utils";
import {
getMenuItemTexts,
makeWithSearch,
setupControlPanelServiceRegistry,
toggleFilterMenu,
toggleGroupByMenu,
toggleMenuItem,
} from "./helpers";
import { Component, onWillUpdateProps, onWillStart, useState, xml } from "@odoo/owl";
let target;
let serverData;
QUnit.module("Search", (hooks) => {
hooks.beforeEach(async () => {
serverData = {
models: {
animal: {
fields: {
birthday: { string: "Birthday", type: "date", store: true },
type: {
string: "Type",
type: "selection",
selection: [
["omnivorous", "Omnivorous"],
["herbivorous", "Herbivorous"],
["carnivorous", "Carnivorous"],
],
store: true,
},
},
},
},
views: {
"animal,false,search": `<search/>`,
"animal,1,search": `
<search>
<filter name="filter" string="True domain" domain="[(1, '=', 1)]"/>
<filter name="group_by" context="{ 'group_by': 'name' }"/>
</search>
`,
},
};
setupControlPanelServiceRegistry();
target = getFixture();
});
QUnit.module("WithSearch");
QUnit.test("simple rendering", async function (assert) {
assert.expect(2);
class TestComponent extends Component {}
TestComponent.template = xml`<div class="o_test_component">Test component content</div>`;
await makeWithSearch({
serverData,
resModel: "animal",
Component: TestComponent,
});
assert.containsOnce(target, ".o_test_component");
assert.strictEqual(
target.querySelector(".o_test_component").innerText,
"Test component content"
);
});
QUnit.test("search model in sub env", async function (assert) {
assert.expect(1);
class TestComponent extends Component {}
TestComponent.template = xml`<div class="o_test_component">Test component content</div>`;
const component = await makeWithSearch({
serverData,
resModel: "animal",
Component: TestComponent,
});
assert.ok(component.env.searchModel);
});
QUnit.test(
"search query props are passed as props to concrete component",
async function (assert) {
assert.expect(4);
class TestComponent extends Component {
setup() {
const { context, domain, groupBy, orderBy } = this.props;
assert.deepEqual(context, {
lang: "en",
tz: "taht",
uid: 7,
key: "val",
});
assert.deepEqual(domain, [[0, "=", 1]]);
assert.deepEqual(groupBy, ["birthday"]);
assert.deepEqual(orderBy, [{ name: "bar", asc: true }]);
}
}
TestComponent.template = xml`<div class="o_test_component">Test component content</div>`;
await makeWithSearch({
serverData,
resModel: "animal",
Component: TestComponent,
domain: [[0, "=", 1]],
groupBy: ["birthday"],
context: { key: "val" },
orderBy: [{ name: "bar", asc: true }],
});
}
);
QUnit.test("do not load search view description by default", async function (assert) {
assert.expect(1);
class TestComponent extends Component {}
TestComponent.template = xml`<div class="o_test_component">Test component content</div>`;
await makeWithSearch({
serverData,
mockRPC: function (_, args) {
if (args.method === "get_views") {
throw new Error("No get_views should be done");
}
},
resModel: "animal",
Component: TestComponent,
});
assert.ok(true);
});
QUnit.test(
"load search view description if not provided and loadSearchView=true",
async function (assert) {
assert.expect(1);
class TestComponent extends Component {}
TestComponent.template = xml`<div class="o_test_component">Test component content</div>`;
await makeWithSearch({
serverData,
mockRPC: function (_, args) {
if (args.method === "get_views") {
assert.deepEqual(args.kwargs, {
context: {
lang: "en",
tz: "taht",
uid: 7,
},
options: {
action_id: false,
load_filters: false,
toolbar: false,
},
views: [[false, "search"]],
});
}
},
resModel: "animal",
Component: TestComponent,
searchViewId: false,
});
}
);
QUnit.test(
"do not load the search view description if provided even if loadSearchView=true",
async function (assert) {
assert.expect(1);
class TestComponent extends Component {}
TestComponent.template = xml`<div class="o_test_component">Test component content</div>`;
await makeWithSearch({
serverData,
mockRPC: function (_, args) {
if (args.method === "get_views") {
throw new Error("No get_views should be done");
}
},
resModel: "animal",
Component: TestComponent,
searchViewArch: "<search/>",
searchViewFields: {},
searchViewId: false,
});
assert.ok(true);
}
);
QUnit.test(
"load view description if it is not complete and loadSearchView=true",
async function (assert) {
assert.expect(1);
class TestComponent extends Component {}
TestComponent.template = xml`<div class="o_test_component">Test component content</div>`;
await makeWithSearch({
serverData,
mockRPC: function (_, args) {
if (args.method === "get_views") {
assert.deepEqual(args.kwargs.options, {
action_id: false,
load_filters: true,
toolbar: false,
});
}
},
resModel: "animal",
Component: TestComponent,
searchViewArch: "<search/>",
searchViewFields: {},
searchViewId: true,
loadIrFilters: true,
});
}
);
QUnit.test(
"load view description with given id if it is not provided and loadSearchView=true",
async function (assert) {
assert.expect(3);
class TestComponent extends Component {}
TestComponent.components = { FilterMenu, GroupByMenu };
TestComponent.template = xml`
<div class="o_test_component">
<FilterMenu/>
<GroupByMenu/>
</div>
`;
await makeWithSearch({
serverData,
mockRPC: function (_, args) {
if (args.method === "get_views") {
assert.deepEqual(args.kwargs.views, [[1, "search"]]);
}
},
resModel: "animal",
Component: TestComponent,
searchViewId: 1,
});
await toggleFilterMenu(target);
await assert.ok(getMenuItemTexts(target), ["True Domain"]);
await toggleGroupByMenu(target);
await assert.ok(getMenuItemTexts(target), ["Name"]);
}
);
QUnit.test(
"toggle a filter render the underlying component with an updated domain",
async function (assert) {
assert.expect(2);
class TestComponent extends Component {
setup() {
owl.onWillStart(() => {
assert.deepEqual(this.props.domain, []);
});
owl.onWillUpdateProps((nextProps) => {
assert.deepEqual(nextProps.domain, [[1, "=", 1]]);
});
}
}
TestComponent.components = { FilterMenu };
TestComponent.template = xml`
<div class="o_test_component">
<FilterMenu/>
</div>
`;
await makeWithSearch({
serverData,
resModel: "animal",
Component: TestComponent,
searchViewId: 1,
});
await toggleFilterMenu(target);
await toggleMenuItem(target, "True domain");
}
);
QUnit.test("react to prop 'domain' changes", async function (assert) {
assert.expect(2);
class TestComponent extends Component {
setup() {
onWillStart(() => {
assert.deepEqual(this.props.domain, [["type", "=", "carnivorous"]]);
});
onWillUpdateProps((nextProps) => {
assert.deepEqual(nextProps.domain, [["type", "=", "herbivorous"]]);
});
}
}
TestComponent.template = xml`<div class="o_test_component">Test component content</div>`;
const env = await makeTestEnv(serverData);
const target = getFixture();
class Parent extends Component {
setup() {
owl.useSubEnv({ config: {} });
this.searchState = useState({
resModel: "animal",
domain: [["type", "=", "carnivorous"]],
});
}
}
Parent.template = xml`
<WithSearch t-props="searchState" t-slot-scope="search">
<TestComponent
domain="search.domain"
/>
</WithSearch>
`;
Parent.components = { WithSearch, TestComponent };
const parent = await mount(Parent, target, { env });
parent.searchState.domain = [["type", "=", "herbivorous"]];
await nextTick();
});
QUnit.test("search defaults are removed from context at reload", async function (assert) {
assert.expect(4);
const context = {
search_default_x: true,
searchpanel_default_y: true,
};
class TestComponent extends Component {
setup() {
onWillStart(() => {
assert.deepEqual(this.props.context, { lang: "en", tz: "taht", uid: 7 });
});
onWillUpdateProps((nextProps) => {
assert.deepEqual(nextProps.context, { lang: "en", tz: "taht", uid: 7 });
});
}
}
TestComponent.template = xml`<div class="o_test_component">Test component content</div>`;
TestComponent.props = { context: Object };
const env = await makeTestEnv(serverData);
const target = getFixture();
class Parent extends Component {
setup() {
owl.useSubEnv({ config: {} });
this.searchState = useState({
resModel: "animal",
domain: [["type", "=", "carnivorous"]],
context,
});
}
}
Parent.template = xml`
<WithSearch t-props="searchState" t-slot-scope="search">
<TestComponent
context="search.context"
/>
</WithSearch>
`;
Parent.components = { WithSearch, TestComponent };
const parent = await mount(Parent, target, { env });
assert.deepEqual(parent.searchState.context, context);
parent.searchState.domain = [["type", "=", "herbivorous"]];
await nextTick();
assert.deepEqual(parent.searchState.context, context);
});
});