Odoo18-Base/addons/web/static/tests/webclient/navbar_tests.js
2025-03-10 10:52:11 +07:00

517 lines
21 KiB
JavaScript

/** @odoo-module **/
import { overlayService } from "@web/core/overlay/overlay_service";
import { browser } from "@web/core/browser/browser";
import { notificationService } from "@web/core/notifications/notification_service";
import { menuService } from "@web/webclient/menus/menu_service";
import { registry } from "@web/core/registry";
import { uiService } from "@web/core/ui/ui_service";
import { actionService } from "@web/webclient/actions/action_service";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { NavBar } from "@web/webclient/navbar/navbar";
import { clearRegistryWithCleanup, makeTestEnv } from "../helpers/mock_env";
import {
click,
destroy,
getFixture,
mount,
nextTick,
patchWithCleanup,
mockTimeout,
} from "../helpers/utils";
import { Component, xml, onRendered } from "@odoo/owl";
const systrayRegistry = registry.category("systray");
const serviceRegistry = registry.category("services");
class MySystrayItem extends Component {}
MySystrayItem.template = xml`<li class="my-item">my item</li>`;
let baseConfig;
let target;
QUnit.module("Navbar", {
async beforeEach() {
target = getFixture();
serviceRegistry.add("overlay", overlayService);
serviceRegistry.add("menu", menuService);
serviceRegistry.add("action", actionService);
serviceRegistry.add("notification", notificationService);
serviceRegistry.add("hotkey", hotkeyService);
serviceRegistry.add("ui", uiService);
systrayRegistry.add("addon.myitem", { Component: MySystrayItem });
patchWithCleanup(browser, {
setTimeout: (handler, delay, ...args) => handler(...args),
clearTimeout: () => {},
});
const menus = {
root: { id: "root", children: [1], name: "root", appID: "root" },
1: { id: 1, children: [], name: "App0", appID: 1 },
};
const serverData = { menus };
baseConfig = { serverData };
},
});
QUnit.test("can be rendered", async (assert) => {
const env = await makeTestEnv(baseConfig);
await mount(NavBar, target, { env });
assert.containsOnce(
target,
".o_navbar_apps_menu button.dropdown-toggle",
"1 apps menu toggler present"
);
});
QUnit.test("dropdown menu can be toggled", async (assert) => {
const env = await makeTestEnv(baseConfig);
await mount(NavBar, target, { env });
const dropdown = target.querySelector(".o_navbar_apps_menu");
await click(dropdown, "button.dropdown-toggle");
assert.containsOnce(dropdown, ".dropdown-menu");
await click(dropdown, "button.dropdown-toggle");
assert.containsNone(dropdown, ".dropdown-menu");
});
QUnit.test("href attribute on apps menu items", async (assert) => {
baseConfig.serverData.menus = {
root: { id: "root", children: [1], name: "root", appID: "root" },
1: { id: 1, children: [2], name: "My app", appID: 1, actionID: 339 },
};
const env = await makeTestEnv(baseConfig);
await mount(NavBar, target, { env });
const appsMenu = target.querySelector(".o_navbar_apps_menu");
await click(appsMenu, "button.dropdown-toggle");
const dropdownItem = target.querySelector(".o_navbar_apps_menu .dropdown-item");
assert.strictEqual(dropdownItem.getAttribute("href"), "#menu_id=1&action=339");
});
QUnit.test("many sublevels in app menu items", async (assert) => {
baseConfig.serverData.menus = {
root: { id: "root", children: [1], name: "root", appID: "root" },
1: { id: 1, children: [2], name: "My app", appID: 1 },
2: { id: 2, children: [3], name: "My menu", appID: 1 },
3: { id: 3, children: [4], name: "My submenu 1", appID: 1 },
4: { id: 4, children: [5], name: "My submenu 2", appID: 1 },
5: { id: 5, children: [6], name: "My submenu 3", appID: 1 },
6: { id: 6, children: [7], name: "My submenu 4", appID: 1 },
7: { id: 7, children: [8], name: "My submenu 5", appID: 1 },
8: { id: 8, children: [9], name: "My submenu 6", appID: 1 },
9: { id: 9, children: [], name: "My submenu 7", appID: 1 },
};
const env = await makeTestEnv(baseConfig);
env.services.menu.setCurrentMenu(1);
await mount(NavBar, target, { env });
const firstSectionMenu = target.querySelector(".o_menu_sections .dropdown");
await click(firstSectionMenu, "button.dropdown-toggle");
const menuChildren = [...firstSectionMenu.querySelectorAll(".dropdown-menu > *")];
assert.deepEqual(
menuChildren.map((el) => ({
text: el.textContent,
paddingLeft: el.style.paddingLeft,
tagName: el.tagName,
})),
[
{ text: "My submenu 1", paddingLeft: "20px", tagName: "DIV" },
{ text: "My submenu 2", paddingLeft: "32px", tagName: "DIV" },
{ text: "My submenu 3", paddingLeft: "44px", tagName: "DIV" },
{ text: "My submenu 4", paddingLeft: "56px", tagName: "DIV" },
{ text: "My submenu 5", paddingLeft: "68px", tagName: "DIV" },
{ text: "My submenu 6", paddingLeft: "80px", tagName: "DIV" },
{ text: "My submenu 7", paddingLeft: "92px", tagName: "A" },
]
);
});
QUnit.test("data-menu-xmlid attribute on AppsMenu items", async (assert) => {
baseConfig.serverData.menus = {
root: { id: "root", children: [1, 2], name: "root", appID: "root" },
1: { id: 1, children: [3, 4], name: "App0 with xmlid", appID: 1, xmlid: "wowl" },
2: { id: 2, children: [], name: "App1 without xmlid", appID: 2 },
3: { id: 3, children: [], name: "Menu without children", appID: 1, xmlid: "menu_3" },
4: { id: 4, children: [5], name: "Menu with children", appID: 1, xmlid: "menu_4" },
5: { id: 5, children: [], name: "Sub menu", appID: 1, xmlid: "menu_5" },
};
const env = await makeTestEnv(baseConfig);
await mount(NavBar, target, { env });
// check apps
const appsMenu = target.querySelector(".o_navbar_apps_menu");
await click(appsMenu, "button.dropdown-toggle");
const menuItems = appsMenu.querySelectorAll("a");
assert.strictEqual(
menuItems[0].dataset.menuXmlid,
"wowl",
"first menu item should have the correct data-menu-xmlid attribute set"
);
assert.strictEqual(
menuItems[1].dataset.menuXmlid,
undefined,
"second menu item should not have any data-menu-xmlid attribute set"
);
// check menus
env.services.menu.setCurrentMenu(1);
await nextTick();
assert.containsOnce(target, ".o_menu_sections .dropdown-item[data-menu-xmlid=menu_3]");
// check sub menus toggler
assert.containsOnce(target, ".o_menu_sections button.dropdown-toggle[data-menu-xmlid=menu_4]");
// check sub menus
await click(target.querySelector(".o_menu_sections .dropdown-toggle"));
assert.containsOnce(target, ".o_menu_sections .dropdown-item[data-menu-xmlid=menu_5]");
});
QUnit.test("navbar can display current active app", async (assert) => {
const env = await makeTestEnv(baseConfig);
await mount(NavBar, target, { env });
const dropdown = target.querySelector(".o_navbar_apps_menu");
// Open apps menu
await click(dropdown, "button.dropdown-toggle");
assert.containsOnce(
dropdown,
".dropdown-menu .dropdown-item:not(.focus)",
"should not show the current active app as the menus service has not loaded an app yet"
);
// Activate an app
env.services.menu.setCurrentMenu(1);
await nextTick();
assert.containsOnce(
dropdown,
".dropdown-menu .dropdown-item.focus",
"should show the current active app"
);
});
QUnit.test("navbar can display systray items", async (assert) => {
const env = await makeTestEnv(baseConfig);
await mount(NavBar, target, { env });
assert.containsOnce(target, "li.my-item");
});
QUnit.test("navbar can display systray items ordered based on their sequence", async (assert) => {
class MyItem1 extends Component {}
MyItem1.template = xml`<li class="my-item-1">my item 1</li>`;
class MyItem2 extends Component {}
MyItem2.template = xml`<li class="my-item-2">my item 2</li>`;
class MyItem3 extends Component {}
MyItem3.template = xml`<li class="my-item-3">my item 3</li>`;
class MyItem4 extends Component {}
MyItem4.template = xml`<li class="my-item-4">my item 4</li>`;
clearRegistryWithCleanup(systrayRegistry);
systrayRegistry.add("addon.myitem2", { Component: MyItem2 });
systrayRegistry.add("addon.myitem1", { Component: MyItem1 }, { sequence: 0 });
systrayRegistry.add("addon.myitem3", { Component: MyItem3 }, { sequence: 100 });
systrayRegistry.add("addon.myitem4", { Component: MyItem4 });
const env = await makeTestEnv(baseConfig);
await mount(NavBar, target, { env });
const menuSystray = target.getElementsByClassName("o_menu_systray")[0];
assert.containsN(menuSystray, "li", 4, "four systray items should be displayed");
assert.strictEqual(menuSystray.innerText, "my item 3\nmy item 4\nmy item 2\nmy item 1");
});
QUnit.test("navbar updates after adding a systray item", async (assert) => {
class MyItem1 extends Component {}
MyItem1.template = xml`<li class="my-item-1">my item 1</li>`;
clearRegistryWithCleanup(systrayRegistry);
systrayRegistry.add("addon.myitem1", { Component: MyItem1 });
const env = await makeTestEnv(baseConfig);
patchWithCleanup(NavBar.prototype, {
setup() {
onRendered(() => {
if (!systrayRegistry.contains("addon.myitem2")) {
class MyItem2 extends Component {}
MyItem2.template = xml`<li class="my-item-2">my item 2</li>`;
systrayRegistry.add("addon.myitem2", { Component: MyItem2 });
}
});
super.setup();
},
});
await mount(NavBar, target, { env });
const menuSystray = target.getElementsByClassName("o_menu_systray")[0];
assert.containsN(menuSystray, "li", 2, "2 systray items should be displayed");
});
QUnit.test("can adapt with 'more' menu sections behavior", async (assert) => {
class MyNavbar extends NavBar {
async adapt() {
await super.adapt();
const sectionsCount = this.currentAppSections.length;
const hiddenSectionsCount = this.currentAppSectionsExtra.length;
assert.step(`adapt -> hide ${hiddenSectionsCount}/${sectionsCount} sections`);
}
}
const newMenus = {
root: { id: "root", children: [1, 2], name: "root", appID: "root" },
1: { id: 1, children: [10, 11, 12], name: "App0", appID: 1 },
10: { id: 10, children: [], name: "Section 10", appID: 1 },
11: { id: 11, children: [], name: "Section 11", appID: 1 },
12: { id: 12, children: [120, 121, 122], name: "Section 12", appID: 1 },
120: { id: 120, children: [], name: "Section 120", appID: 1 },
121: { id: 121, children: [], name: "Section 121", appID: 1 },
122: { id: 122, children: [], name: "Section 122", appID: 1 },
};
baseConfig.serverData.menus = newMenus;
const env = await makeTestEnv(baseConfig);
// Force the parent width, to make this test independent of screen size
target.style.width = "1080px";
// Set menu and mount
env.services.menu.setCurrentMenu(1);
await mount(MyNavbar, target, { env });
assert.containsN(
target,
".o_menu_sections > *:not(.o_menu_sections_more):not(.d-none)",
3,
"should have 3 menu sections displayed (that are not the 'more' menu)"
);
assert.containsNone(target, ".o_menu_sections_more", "the 'more' menu should not exist");
// Force minimal width and dispatch window resize event
target.style.width = "0%";
window.dispatchEvent(new Event("resize"));
await nextTick();
assert.containsOnce(
target,
".o_menu_sections > *:not(.d-none)",
"only one menu section should be displayed"
);
assert.containsOnce(
target,
".o_menu_sections_more:not(.d-none)",
"the displayed menu section should be the 'more' menu"
);
// Open the more menu
await click(target, ".o_menu_sections_more .dropdown-toggle");
assert.deepEqual(
[...target.querySelectorAll(".dropdown-menu > *")].map((el) => el.textContent),
["Section 10", "Section 11", "Section 12", "Section 120", "Section 121", "Section 122"],
"'more' menu should contain all hidden sections in correct order"
);
// Reset to full width and dispatch window resize event
target.style.width = "100%";
window.dispatchEvent(new Event("resize"));
await nextTick();
assert.containsN(
target,
".o_menu_sections > *:not(.o_menu_sections_more):not(.d-none)",
3,
"should have 3 menu sections displayed (that are not the 'more' menu)"
);
assert.containsNone(target, ".o_menu_sections_more", "the 'more' menu should not exist");
// Check the navbar adaptation calls
assert.verifySteps([
"adapt -> hide 0/3 sections",
"adapt -> hide 3/3 sections",
"adapt -> hide 0/3 sections",
]);
});
QUnit.test(
"'more' menu sections adaptations do not trigger render in some cases",
async (assert) => {
let adaptRunning = false;
let adaptCount = 0;
let adaptRenderCount = 0;
class MyNavbar extends NavBar {
async adapt() {
adaptRunning = true;
adaptCount++;
await super.adapt();
adaptRunning = false;
}
async render() {
if (adaptRunning) {
adaptRenderCount++;
}
await super.render(...arguments);
}
}
const newMenus = {
root: { id: "root", children: [1], name: "root", appID: "root" },
1: { id: 1, children: [11, 12, 13], name: "App1", appID: 1 },
11: { id: 11, children: [], name: "Section 1", appID: 1 },
12: { id: 12, children: [], name: "Section 2", appID: 1 },
13: { id: 13, children: [], name: "Section 3", appID: 1 },
};
baseConfig.serverData.menus = newMenus;
// Force the parent width, to make this test independent of screen size
target.style.width = "600px";
const env = await makeTestEnv(baseConfig);
const navbar = await mount(MyNavbar, target, { env });
assert.strictEqual(navbar.currentAppSections.length, 0, "0 app sub menus");
assert.strictEqual(target.querySelector(".o_navbar").offsetWidth, 600);
assert.strictEqual(adaptCount, 1);
assert.strictEqual(
adaptRenderCount,
0,
"during adapt, render not triggered as the navbar has no app sub menus"
);
// Force minimal width and dispatch window resize event
target.querySelector(".o_navbar").style.width = "0%";
window.dispatchEvent(new Event("resize"));
await nextTick();
assert.strictEqual(target.querySelector(".o_navbar").offsetWidth, 0);
assert.strictEqual(adaptCount, 2);
assert.strictEqual(
adaptRenderCount,
0,
"during adapt, render not triggered as the navbar has no app sub menus"
);
// Set menu
env.services.menu.setCurrentMenu(1);
await nextTick();
assert.strictEqual(navbar.currentAppSections.length, 3, "3 app sub menus");
assert.strictEqual(
navbar.currentAppSectionsExtra.length,
3,
"all app sub menus are inside the more menu"
);
assert.strictEqual(adaptCount, 3);
assert.strictEqual(
adaptRenderCount,
1,
"during adapt, render triggered as the navbar does not have enough space for app sub menus"
);
// Force 40% width and dispatch window resize event
target.querySelector(".o_navbar").style.width = "40%";
window.dispatchEvent(new Event("resize"));
await nextTick();
assert.strictEqual(
navbar.currentAppSectionsExtra.length,
3,
"all app sub menus are STILL inside the more menu"
);
assert.strictEqual(adaptCount, 4);
assert.strictEqual(
adaptRenderCount,
1,
"during adapt, render not triggered as the more menu dropdown is STILL the same"
);
// Reset to full width and dispatch window resize event
target.querySelector(".o_navbar").style.width = "100%";
window.dispatchEvent(new Event("resize"));
await nextTick();
assert.strictEqual(navbar.currentAppSections.length, 3, "still 3 app sub menus");
assert.strictEqual(
navbar.currentAppSectionsExtra.length,
0,
"all app sub menus are NO MORE inside the more menu"
);
assert.strictEqual(adaptCount, 5);
assert.strictEqual(
adaptRenderCount,
2,
"during adapt, render triggered as the more menu dropdown is NO MORE the same"
);
}
);
QUnit.test("'more' menu sections properly updated on app change", async (assert) => {
const newMenus = {
root: { id: "root", children: [1, 2], name: "root", appID: "root" },
// First App
1: { id: 1, children: [10, 11, 12], name: "App1", appID: 1 },
10: { id: 10, children: [], name: "Section 10", appID: 1 },
11: { id: 11, children: [], name: "Section 11", appID: 1 },
12: { id: 12, children: [120, 121, 122], name: "Section 12", appID: 1 },
120: { id: 120, children: [], name: "Section 120", appID: 1 },
121: { id: 121, children: [], name: "Section 121", appID: 1 },
122: { id: 122, children: [], name: "Section 122", appID: 1 },
// Second App
2: { id: 2, children: [20, 21, 22], name: "App2", appID: 2 },
20: { id: 20, children: [], name: "Section 20", appID: 2 },
21: { id: 21, children: [], name: "Section 21", appID: 2 },
22: { id: 22, children: [220, 221, 222], name: "Section 22", appID: 2 },
220: { id: 220, children: [], name: "Section 220", appID: 2 },
221: { id: 221, children: [], name: "Section 221", appID: 2 },
222: { id: 222, children: [], name: "Section 222", appID: 2 },
};
baseConfig.serverData.menus = newMenus;
const env = await makeTestEnv(baseConfig);
// Force the parent width, to make this test independent of screen size
target.style.width = "1080px";
// Set App1 menu and mount
env.services.menu.setCurrentMenu(1);
await mount(NavBar, target, { env });
// Force minimal width and dispatch window resize event
target.style.width = "0%";
window.dispatchEvent(new Event("resize"));
await nextTick();
assert.containsOnce(
target,
".o_menu_sections > *:not(.d-none)",
"only one menu section should be displayed"
);
assert.containsOnce(
target,
".o_menu_sections_more:not(.d-none)",
"the displayed menu section should be the 'more' menu"
);
// Open the more menu
await click(target, ".o_menu_sections_more .dropdown-toggle");
assert.deepEqual(
[...target.querySelectorAll(".dropdown-menu > *")].map((el) => el.textContent),
["Section 10", "Section 11", "Section 12", "Section 120", "Section 121", "Section 122"],
"'more' menu should contain App1 sections"
);
// Close the more menu
await click(target, ".o_menu_sections_more .dropdown-toggle");
// Set App2 menu
env.services.menu.setCurrentMenu(2);
await nextTick();
// Open the more menu
await click(target, ".o_menu_sections_more .dropdown-toggle");
assert.deepEqual(
[...target.querySelectorAll(".dropdown-menu > *")].map((el) => el.textContent),
["Section 20", "Section 21", "Section 22", "Section 220", "Section 221", "Section 222"],
"'more' menu should contain App2 sections"
);
});
QUnit.test("Do not execute adapt when navbar is destroyed", async (assert) => {
assert.expect(5);
const { execRegisteredTimeouts } = mockTimeout();
class MyNavbar extends NavBar {
async adapt() {
assert.step("adapt NavBar");
return super.adapt();
}
}
const env = await makeTestEnv(baseConfig);
// Set menu and mount
env.services.menu.setCurrentMenu(1);
const navbar = await mount(MyNavbar, target, { env });
assert.verifySteps(["adapt NavBar"]);
window.dispatchEvent(new Event("resize"));
execRegisteredTimeouts();
assert.verifySteps(["adapt NavBar"]);
window.dispatchEvent(new Event("resize"));
destroy(navbar);
execRegisteredTimeouts();
assert.verifySteps([]);
});