vanilla 18.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:48:09 +02:00
parent 5454004ff9
commit d7f6d2725e
979 changed files with 428093 additions and 0 deletions

View file

@ -0,0 +1,271 @@
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: () => {
return [
{
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"]);
});

View file

@ -0,0 +1,255 @@
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("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 .o_list_buttons button").toHaveCount(2);
expect(".o_control_panel .o_list_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,299 @@
import { after, expect, test } from "@odoo/hoot";
import { press, queryAllTexts } from "@odoo/hoot-dom";
import { Component, xml } from "@odoo/owl";
import {
contains,
defineModels,
editFavoriteName,
editSearch,
fields,
getFacetTexts,
mockService,
models,
mountWithSearch,
onRpc,
saveFavorite,
toggleSaveFavorite,
toggleSearchBarMenu,
validateSearch,
} from "@web/../tests/web_test_helpers";
import { SearchBar } from "@web/search/search_bar/search_bar";
import { SearchBarMenu } from "@web/search/search_bar_menu/search_bar_menu";
import { useSetupAction } from "@web/search/action_hook";
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(2);
expect(queryAllTexts(`.o_add_favorite + .o_accordion_values .form-check label`)).toEqual([
"Default filter",
"Shared",
]);
});
test("favorites use by default and share are exclusive", async () => {
await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["favorite"],
searchViewId: false,
});
await toggleSearchBarMenu();
await toggleSaveFavorite();
expect(`input[type="checkbox"]`).toHaveCount(2);
expect(`input[type="checkbox"]:checked`).toHaveCount(0);
await contains(`input[type="checkbox"]:eq(0)`).check();
expect(`input[type="checkbox"]:eq(0)`).toBeChecked();
expect(`input[type="checkbox"]:eq(1)`).not.toBeChecked();
await contains(`input[type="checkbox"]:eq(1)`).check();
expect(`input[type="checkbox"]:eq(0)`).not.toBeChecked();
expect(`input[type="checkbox"]:eq(1)`).toBeChecked();
await contains(`input[type="checkbox"]:eq(0)`).check();
expect(`input[type="checkbox"]:eq(0)`).toBeChecked();
expect(`input[type="checkbox"]:eq(1)`).not.toBeChecked();
await contains(`input[type="checkbox"]:eq(0)`).uncheck();
expect(`input[type="checkbox"]:eq(0)`).not.toBeChecked();
expect(`input[type="checkbox"]:eq(1)`).not.toBeChecked();
});
test("save filter", async () => {
class TestComponent extends Component {
static components = { SearchBarMenu };
static template = xml`<div><SearchBarMenu/></div>`;
static props = ["*"];
setup() {
useSetupAction({
getContext: () => {
return { someKey: "foo" };
},
});
}
}
onRpc("create_or_replace", ({ args, route }) => {
expect.step(route);
const irFilter = args[0];
expect(irFilter.context).toEqual({ group_by: [], someKey: "foo" });
return 7; // fake serverSideId
});
const component = await mountWithSearch(TestComponent, {
resModel: "foo",
context: { someOtherKey: "bar" }, // should not end up in filter's context
searchViewId: false,
});
const clearCacheListener = () => expect.step("CLEAR-CACHES");
component.env.bus.addEventListener("CLEAR-CACHES", clearCacheListener);
after(() => component.env.bus.removeEventListener("CLEAR-CACHES", clearCacheListener));
expect.verifySteps([]);
await toggleSearchBarMenu();
await toggleSaveFavorite();
await editFavoriteName("aaa");
await saveFavorite();
expect.verifySteps(["/web/dataset/call_kw/ir.filters/create_or_replace", "CLEAR-CACHES"]);
});
test("dynamic filters are saved dynamic", async () => {
onRpc("create_or_replace", ({ 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_or_replace"]);
});
test("save filters created via autocompletion works", async () => {
onRpc("create_or_replace", ({ 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_or_replace"]);
});
test("favorites have unique descriptions (the submenus of the favorite menu are correctly updated)", async () => {
mockService("notification", {
add(message, options) {
expect.step("notification");
expect(message).toBe("A filter with same name already exists.");
expect(options).toEqual({ type: "danger" });
},
});
onRpc("create_or_replace", ({ args, route }) => {
expect.step(route);
expect(args[0]).toEqual({
action_id: false,
context: { group_by: [] },
domain: `[]`,
is_default: false,
model_id: "foo",
name: "My favorite 2",
sort: `[]`,
embedded_action_id: false,
embedded_parent_res_id: false,
user_id: 7,
});
return 2; // fake serverSideId
});
await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["favorite"],
searchViewId: false,
irFilters: [
{
context: "{}",
domain: "[]",
id: 1,
is_default: false,
name: "My favorite",
sort: "[]",
user_id: [2, "Mitchell Admin"],
},
],
});
await toggleSearchBarMenu();
await toggleSaveFavorite();
// first try: should fail
await editFavoriteName("My favorite");
await saveFavorite();
expect.verifySteps(["notification"]);
// second try: should succeed
await editFavoriteName("My favorite 2");
await saveFavorite();
expect.verifySteps(["/web/dataset/call_kw/ir.filters/create_or_replace"]);
// third try: should fail
await editFavoriteName("My favorite 2");
await saveFavorite();
expect.verifySteps(["notification"]);
});
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_or_replace", () => 7); // fake serverSideId
await mountWithSearch(SearchBarMenu, {
resModel: "foo",
searchViewId: false,
});
await toggleSearchBarMenu();
await toggleSaveFavorite();
await saveFavorite();
expect.verifySteps(["notification"]);
});
test("add favorite with enter which already exists", 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" });
},
});
await mountWithSearch(SearchBarMenu, {
resModel: "foo",
searchViewId: false,
irFilters: [
{
context: "{}",
domain: "[]",
id: 1,
is_default: false,
name: "My favorite",
sort: "[]",
user_id: [2, "Mitchell Admin"],
},
],
});
await toggleSearchBarMenu();
await toggleSaveFavorite();
await editFavoriteName("My favorite");
await press("Enter");
expect.verifySteps(["notification"]);
});

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(`Add 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 "Add 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 "Add 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 "Add 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); // Add 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("Add 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 Add 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

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

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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,87 @@
import { expect, test } from "@odoo/hoot";
import { mockDate } from "@odoo/hoot-mock";
import {
getFacetTexts,
mountWithSearch,
removeFacet,
toggleMenuItem,
toggleMenuItemOption,
toggleSearchBarMenu,
} from "@web/../tests/web_test_helpers";
import { defineSearchBarModels } from "./models";
import { SearchBarMenu } from "@web/search/search_bar_menu/search_bar_menu";
import { queryAll, queryAllTexts } from "@odoo/hoot-dom";
import { SearchBar } from "@web/search/search_bar/search_bar";
defineSearchBarModels();
test("simple rendering", async () => {
mockDate("1997-01-09T12:00:00");
await mountWithSearch(SearchBarMenu, {
resModel: "foo",
searchMenuTypes: ["filter", "comparison"],
searchViewId: false,
});
expect(`.o_searchview_dropdown_toggler`).toHaveCount(1);
expect(`.dropdown.o_comparison_menu`).toHaveCount(0);
await toggleSearchBarMenu();
await toggleMenuItem("Birthday");
await toggleMenuItemOption("Birthday", "January");
expect(`.o_comparison_menu .fa.fa-adjust`).toHaveCount(1);
expect(`.o_comparison_menu .o_dropdown_title`).toHaveText(/^comparison$/i);
expect(`.o_comparison_menu .dropdown-item`).toHaveCount(2);
expect(`.o_comparison_menu .dropdown-item[role=menuitemcheckbox]`).toHaveCount(2);
expect(queryAllTexts`.o_comparison_menu .dropdown-item`).toEqual([
"Birthday: Previous Period",
"Birthday: Previous Year",
]);
expect(queryAll`.o_comparison_menu .dropdown-item`.map((e) => e.ariaChecked)).toEqual([
"false",
"false",
]);
});
test("activate a comparison works", async () => {
mockDate("1997-01-09T12:00:00");
await mountWithSearch(SearchBar, {
resModel: "foo",
searchMenuTypes: ["filter", "comparison"],
searchViewId: false,
});
await toggleSearchBarMenu();
await toggleMenuItem("Birthday");
await toggleMenuItemOption("Birthday", "January");
await toggleMenuItem("Birthday: Previous Period");
expect(getFacetTexts()).toEqual(["Birthday: January 1997", "Birthday: Previous Period"]);
await toggleMenuItem("Date");
await toggleMenuItemOption("Date", "December");
await toggleMenuItem("Date: Previous Year");
expect(getFacetTexts()).toEqual([
["Birthday: January 1997", "Date: December 1996"].join("\nor\n"),
"Date: Previous Year",
]);
await toggleMenuItemOption("Date", "1996");
expect(getFacetTexts()).toEqual(["Birthday: January 1997"]);
await toggleMenuItem("Birthday: Previous Year");
expect(`.o_comparison_menu .dropdown-item`).toHaveCount(2);
expect(`.o_comparison_menu .dropdown-item[role=menuitemcheckbox]`).toHaveCount(2);
expect(queryAllTexts`.o_comparison_menu .dropdown-item`).toEqual([
"Birthday: Previous Period",
"Birthday: Previous Year",
]);
expect(queryAll`.o_comparison_menu .dropdown-item`.map((e) => e.ariaChecked)).toEqual([
"false",
"true",
]);
expect(getFacetTexts()).toEqual(["Birthday: January 1997", "Birthday: Previous Year"]);
await removeFacet("Birthday: January 1997");
expect(getFacetTexts()).toEqual([]);
});

View file

@ -0,0 +1,338 @@
import { after, expect, test } from "@odoo/hoot";
import { queryFirst } from "@odoo/hoot-dom";
import { mockDate } from "@odoo/hoot-mock";
import { Component, onWillUpdateProps, xml } from "@odoo/owl";
import { editValue } from "@web/../tests/core/tree_editor/condition_tree_editor_test_helpers";
import {
contains,
deleteFavorite,
editFavoriteName,
getFacetTexts,
getService,
isItemSelected,
mountWithCleanup,
mountWithSearch,
onRpc,
patchWithCleanup,
saveFavorite,
serverState,
toggleMenuItem,
toggleSaveFavorite,
toggleSearchBarMenu,
} from "@web/../tests/web_test_helpers";
import { Foo, 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";
import { WebClient } from "@web/webclient/webclient";
const favoriteMenuRegistry = registry.category("favoriteMenu");
const viewsRegistry = registry.category("views");
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("delete an active favorite", async () => {
class ToyController extends Component {
static components = { SearchBar };
static template = xml`<div><SearchBar/></div>`;
static props = ["*"];
setup() {
expect(this.props.domain).toEqual([["foo", "=", "qsdf"]]);
onWillUpdateProps((nextProps) => {
expect.step("props updated");
expect(nextProps.domain).toEqual([]);
});
}
}
patchWithCleanup(serverState.view_info, {
toy: { multi_record: true, display_name: "Toy", icon: "fab fa-android" },
});
viewsRegistry.add("toy", {
type: "toy",
Controller: ToyController,
});
after(() => viewsRegistry.remove("toy"));
Foo._filters = [
{
context: "{}",
domain: "[['foo', '=', 'qsdf']]",
id: 7,
is_default: true,
name: "My favorite",
sort: "[]",
user_id: [2, "Mitchell Admin"],
},
];
onRpc("unlink", () => {
expect.step("deleteFavorite");
return true;
});
const webClient = await mountWithCleanup(WebClient);
const clearCacheListener = () => expect.step("CLEAR-CACHES");
webClient.env.bus.addEventListener("CLEAR-CACHES", clearCacheListener);
after(() => webClient.env.bus.removeEventListener("CLEAR-CACHES", clearCacheListener));
await getService("action").doAction({
name: "Action",
res_model: "foo",
type: "ir.actions.act_window",
views: [[false, "toy"]],
});
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 deleteFavorite("My favorite");
expect.verifySteps([]);
await contains(`div.o_dialog footer button`).click();
expect(getFacetTexts()).toEqual([]);
expect(".o_favorite_menu .o_menu_item").toHaveCount(1);
expect(".o_favorite_menu .o_add_favorite").toHaveCount(1);
expect.verifySteps(["deleteFavorite", "CLEAR-CACHES", "props updated"]);
});
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_id: [2, "Mitchell Admin"],
},
],
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, comparison and field "options"`, async () => {
mockDate("2019-07-31T13:43:00");
const searchBar = await mountWithSearch(SearchBar, {
resModel: "foo",
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="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(searchBar.env.searchModel.getFullComparison()).toBe(null);
expect(getFacetTexts()).toEqual([
"Foo\na",
"Date Field Filter: 2019",
"Date Field Groupby: Month",
]);
// activate a comparison
await toggleSearchBarMenu();
await toggleMenuItem("Date Field Filter: Previous Period");
expect(searchBar.env.searchModel.domain).toEqual([["foo", "ilike", "a"]]);
expect(searchBar.env.searchModel.groupBy).toEqual(["date_field:month"]);
expect(searchBar.env.searchModel.getFullComparison()).toEqual({
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
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(searchBar.env.searchModel.getFullComparison()).toEqual({
"favorite comparison content": "bla bla...",
});
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_id: [2, "Mitchell Admin"],
},
];
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(["Bar", "Foo contains abcde"]);
await toggleSearchBarMenu();
expect(`.o_group_by_menu .o_menu_item:not(.o_add_custom_group_menu)`).toHaveCount(0);
});
test("shared favorites are grouped under a dropdown if there are more than 10", async () => {
onRpc("create_or_replace", ({ args, route }) => {
expect.step(route);
const irFilter = args[0];
expect(irFilter.domain).toBe(`[]`);
return 10; // fake serverSideId
});
const irFilters = [];
for (let i = 1; i < 11; i++) {
irFilters.push({
context: "{}",
domain: "[('foo', '=', 'a')]",
id: i,
is_default: false,
name: "My favorite" + i,
sort: "[]",
});
}
await mountWithSearch(SearchBarMenu, {
resModel: "foo",
searchMenuTypes: ["favorite"],
searchViewId: false,
irFilters,
activateFavorite: false,
});
await toggleSearchBarMenu();
expect(".o_favorite_menu .o-dropdown-item").toHaveCount(10);
await toggleSaveFavorite();
await editFavoriteName("My favorite11");
await contains(".o-checkbox:eq(1)").click();
await saveFavorite();
expect.verifySteps(["/web/dataset/call_kw/ir.filters/create_or_replace"]);
expect(".o_favorite_menu .o-dropdown-item").toHaveCount(0);
expect(".o_favorite_menu .o_menu_item:contains(Shared filters)").toHaveCount(1);
await contains(".o_favorite_menu .o_menu_item:contains(Shared filters)").click();
expect(".o_favorite_menu .o-dropdown-item").toHaveCount(11);
});

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

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,459 @@
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { mockDate, mockTimeZone } from "@odoo/hoot-mock";
import { 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" });
patchTranslations();
});
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(`construct comparison domain based on date field and option "previous_period"`, () => {
mockDate("2020-01-01T12:00:00");
const referenceMoment = luxon.DateTime.local();
let domain = constructDateDomain(
referenceMoment,
dateSearchItem,
["month", "first_quarter", "year"],
"previous_period"
);
expect(domain).toEqual({
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",
});
domain = constructDateDomain(
referenceMoment,
dateSearchItem,
["second_quarter", "year", "year-1"],
"previous_period"
);
expect(domain).toEqual({
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",
});
domain = constructDateDomain(
referenceMoment,
dateSearchItem,
["year", "year-2", "month", "month-2"],
"previous_period"
);
expect(domain).toEqual({
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",
});
domain = constructDateDomain(
referenceMoment,
dateSearchItem,
["year", "year-1"],
"previous_period"
);
expect(domain).toEqual({
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",
});
domain = constructDateDomain(
referenceMoment,
dateSearchItem,
["second_quarter", "third_quarter", "year-1"],
"previous_period"
);
expect(domain).toEqual({
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",
});
});
test(`construct comparison domain based on datetime field and option "previous_year"`, () => {
mockDate("2020-06-01T13:00:00");
const referenceMoment = luxon.DateTime.local();
let domain = constructDateDomain(
referenceMoment,
dateTimeSearchItem,
["month", "first_quarter", "year"],
"previous_year"
);
expect(domain).toEqual({
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",
});
domain = constructDateDomain(
referenceMoment,
dateTimeSearchItem,
["second_quarter", "year", "year-1"],
"previous_year"
);
expect(domain).toEqual({
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",
});
domain = constructDateDomain(
referenceMoment,
dateTimeSearchItem,
["year", "year-2", "month", "month-2"],
"previous_year"
);
expect(domain).toEqual({
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",
});
});
test("Quarter option: custom translation", async () => {
mockDate("2020-06-01T13:00:00");
const referenceMoment = luxon.DateTime.local().setLocale("en");
patchTranslations({ 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({ 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

@ -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",
"Add Custom Filter",
"Name",
"Add 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);
});