import { beforeEach, destroy, expect, test } from "@odoo/hoot"; import { queryAll, queryAllAttributes, queryAllTexts, resize } from "@odoo/hoot-dom"; import { advanceTime, animationFrame, runAllTimers } from "@odoo/hoot-mock"; import { clearRegistry, contains, defineMenus, defineParams, getService, makeMockEnv, mountWithCleanup, patchWithCleanup, } from "@web/../tests/web_test_helpers"; import { Component, onRendered, xml } from "@odoo/owl"; import { registry } from "@web/core/registry"; import { NavBar } from "@web/webclient/navbar/navbar"; const systrayRegistry = registry.category("systray"); // Debounce time for Adaptation (`debouncedAdapt`) on resize event in navbar const waitNavbarAdaptation = () => advanceTime(500); class MySystrayItem extends Component { static props = ["*"]; static template = xml`
  • my item
  • `; } beforeEach(async () => { systrayRegistry.add("addon.myitem", { Component: MySystrayItem }); defineMenus([ { id: "root", children: [{ id: 1, children: [], name: "App0", appID: 1 }], name: "root", appID: "root", }, ]); return () => { clearRegistry(systrayRegistry); }; }); test.tags("desktop"); test("can be rendered", async () => { await mountWithCleanup(NavBar); expect(".o_navbar_apps_menu button.dropdown-toggle").toHaveCount(1, { message: "1 apps menu toggler present", }); }); test.tags("desktop"); test("dropdown menu can be toggled", async () => { await mountWithCleanup(NavBar); await contains(".o_navbar_apps_menu button.dropdown-toggle").click(); expect(".dropdown-menu").toHaveCount(1); await contains(".o_navbar_apps_menu button.dropdown-toggle").click(); expect(".dropdown-menu").toHaveCount(0); }); test.tags("desktop"); test("href attribute on apps menu items", async () => { defineMenus([ { id: "root", children: [{ id: 1, children: [], name: "My app", appID: 1, actionID: 339 }], name: "root", appID: "root", }, ]); await mountWithCleanup(NavBar); await contains(".o_navbar_apps_menu button.dropdown-toggle").click(); expect(".o-dropdown--menu .dropdown-item").toHaveAttribute("href", "/odoo/action-339"); }); test.tags("desktop"); test("href attribute with path on apps menu items", async () => { defineMenus([ { id: "root", children: [ { id: 1, children: [], name: "My app", appID: 1, actionID: 339, actionPath: "my-path", }, ], name: "root", appID: "root", }, ]); await mountWithCleanup(NavBar); await contains(".o_navbar_apps_menu button.dropdown-toggle").click(); expect(".o-dropdown--menu .dropdown-item").toHaveAttribute("href", "/odoo/my-path"); }); test.tags("desktop"); test("many sublevels in app menu items", async () => { defineMenus([ { id: "root", children: [ { id: 1, children: [ { id: 2, children: [ { id: 3, children: [ { id: 4, children: [ { id: 5, children: [ { id: 6, children: [ { id: 7, children: [ { id: 8, children: [ { id: 9, children: [], name: "My submenu 7", appID: 1, }, ], name: "My submenu 6", appID: 1, }, ], name: "My submenu 5", appID: 1, }, ], name: "My submenu 4", appID: 1, }, ], name: "My submenu 3", appID: 1, }, ], name: "My submenu 2", appID: 1, }, ], name: "My submenu 1", appID: 1, }, ], name: "My menu", appID: 1, }, ], name: "My app", appID: 1, }, ], name: "root", appID: "root", }, ]); await makeMockEnv(); getService("menu").setCurrentMenu(1); await mountWithCleanup(NavBar); await contains(".o_menu_sections .o-dropdown").click(); expect( queryAll(".o-dropdown--menu > *").map((el) => ({ text: el.innerText, paddingLeft: el.style.paddingLeft, tagName: el.tagName, })) ).toEqual([ { 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" }, ]); }); test.tags("desktop"); test("data-menu-xmlid attribute on AppsMenu items", async () => { // Replace all default menus and setting new one defineParams({ menus: [ { id: 1, children: [ { id: 3, children: [], name: "Menu without children", appID: 1, xmlid: "menu_3", }, { id: 4, children: [ { id: 5, children: [], name: "Sub menu", appID: 1, xmlid: "menu_5", }, ], name: "Menu with children", appID: 1, xmlid: "menu_4", }, ], name: "App0 with xmlid", appID: 1, xmlid: "wowl", }, { id: 2, children: [], name: "App1 without xmlid", appID: 2 }, ], }); await mountWithCleanup(NavBar); // check apps await contains(".o_navbar_apps_menu button.dropdown-toggle").click(); expect(queryAllAttributes(".o-dropdown--menu a", "data-menu-xmlid")).toEqual(["wowl", null], { message: "menu items should have the correct data-menu-xmlid attribute (only the first is set)", }); // check menus getService("menu").setCurrentMenu(1); await animationFrame(); expect(".o_menu_sections .dropdown-item[data-menu-xmlid=menu_3]").toHaveCount(1); // check sub menus toggler expect(".o_menu_sections button.dropdown-toggle[data-menu-xmlid=menu_4]").toHaveCount(1); // check sub menus await contains(".o_menu_sections .dropdown-toggle").click(); expect(".o-dropdown--menu .dropdown-item[data-menu-xmlid=menu_5]").toHaveCount(1); }); test.tags("desktop"); test("navbar can display current active app", async () => { await mountWithCleanup(NavBar); // Open apps menu await contains(".o_navbar_apps_menu button.dropdown-toggle").click(); expect(".o-dropdown--menu .dropdown-item:not(.focus)").toHaveCount(1, { message: "should not show the current active app as the menus service has not loaded an app yet", }); // Activate an app getService("menu").setCurrentMenu(1); await animationFrame(); expect(".o-dropdown--menu .dropdown-item.focus").toHaveCount(1, { message: "should show the current active app", }); }); test("navbar can display systray items", async () => { await mountWithCleanup(NavBar); expect("li.my-item").toHaveCount(1); }); test("navbar can display systray items ordered based on their sequence", async () => { class MyItem1 extends Component { static props = ["*"]; static template = xml`
  • my item 1
  • `; } class MyItem2 extends Component { static props = ["*"]; static template = xml`
  • my item 2
  • `; } class MyItem3 extends Component { static props = ["*"]; static template = xml`
  • my item 3
  • `; } class MyItem4 extends Component { static props = ["*"]; static template = xml`
  • my item 4
  • `; } // Remove systray added by beforeEach systrayRegistry.remove("addon.myitem"); 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 }); await mountWithCleanup(NavBar); expect(".o_menu_systray:eq(0) li").toHaveCount(4, { message: "four systray items should be displayed", }); expect(queryAllTexts(".o_menu_systray:eq(0) li")).toEqual([ "my item 3", "my item 4", "my item 2", "my item 1", ]); }); test("navbar updates after adding a systray item", async () => { class MyItem1 extends Component { static props = ["*"]; static template = xml`
  • my item 1
  • `; } // Remove systray added by beforeEach systrayRegistry.remove("addon.myitem"); systrayRegistry.add("addon.myitem1", { Component: MyItem1 }); patchWithCleanup(NavBar.prototype, { setup() { onRendered(() => { if (!systrayRegistry.contains("addon.myitem2")) { class MyItem2 extends Component { static props = ["*"]; static template = xml`
  • my item 2
  • `; } systrayRegistry.add("addon.myitem2", { Component: MyItem2 }); } }); super.setup(); }, }); await mountWithCleanup(NavBar); expect(".o_menu_systray:eq(0) li").toHaveCount(2, { message: "2 systray items should be displayed", }); }); test.tags("desktop"); test("can adapt with 'more' menu sections behavior", async () => { class MyNavbar extends NavBar { async adapt() { await super.adapt(); const sectionsCount = this.currentAppSections.length; const hiddenSectionsCount = this.currentAppSectionsExtra.length; expect.step(`adapt -> hide ${hiddenSectionsCount}/${sectionsCount} sections`); } } defineMenus([ { id: "root", children: [ { id: 1, children: [ { id: 10, children: [], name: "Section 10", appID: 1 }, { id: 11, children: [], name: "Section 11", appID: 1 }, { id: 12, children: [ { id: 120, children: [], name: "Section 120", appID: 1 }, { id: 121, children: [], name: "Section 121", appID: 1 }, { id: 122, children: [], name: "Section 122", appID: 1 }, ], name: "Section 12", appID: 1, }, ], name: "App0", appID: 1, }, ], name: "root", appID: "root", }, ]); // Force the parent width, to make this test independent of screen size await resize({ width: 1080 }); // TODO: this test case doesn't make sense since it relies on small widths // with `env.isSmall` still returning `false`. const env = await makeMockEnv(); Object.defineProperty(env, "isSmall", { get: () => false }); // Set menu and mount getService("menu").setCurrentMenu(1); await mountWithCleanup(MyNavbar); expect(".o_menu_sections > *:not(.o_menu_sections_more):visible").toHaveCount(3, { message: "should have 3 menu sections displayed (that are not the 'more' menu)", }); expect(".o_menu_sections_more").toHaveCount(0); // Force minimal width await resize({ width: 0 }); await waitNavbarAdaptation(); expect(".o_menu_sections").not.toBeVisible({ message: "no menu section should be displayed", }); // Reset to full width await resize({ width: 1366 }); await waitNavbarAdaptation(); expect(".o_menu_sections > *:not(.o_menu_sections_more):not(.d-none)").toHaveCount(3, { message: "should have 3 menu sections displayed (that are not the 'more' menu)", }); expect(".o_menu_sections_more").toHaveCount(0, { message: "the 'more' menu should not exist" }); expect.verifySteps([ "adapt -> hide 0/3 sections", "adapt -> hide 3/3 sections", "adapt -> hide 0/3 sections", ]); }); test.tags("desktop"); test("'more' menu sections adaptations do not trigger render in some cases", async () => { 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); } } defineMenus([ { id: "root", children: [ { id: 1, children: [ { id: 11, children: [], name: "Section with a very long name 1", appID: 1, }, { id: 12, children: [], name: "Section with a very long name 2", appID: 1, }, { id: 13, children: [], name: "Section with a very long name 3", appID: 1, }, ], name: "App1", appID: 1, }, ], name: "root", appID: "root", }, ]); // Force the parent width, to make this test independent of screen size await resize({ width: 600 }); // TODO: this test case doesn't make sense since it relies on small widths // with `env.isSmall` still returning `false`. const env = await makeMockEnv(); Object.defineProperty(env, "isSmall", { get: () => false }); const navbar = await mountWithCleanup(MyNavbar); expect(navbar.currentAppSections).toHaveLength(0, { message: "0 app sub menus" }); expect(".o_navbar").toHaveRect({ width: 600 }); expect(adaptCount).toBe(1); expect(adaptRenderCount).toBe(0, { message: "during adapt, render not triggered as the navbar has no app sub menus", }); await resize({ width: 0 }); await waitNavbarAdaptation(); expect(".o_navbar").toHaveRect({ width: 0 }); expect(adaptCount).toBe(2); expect(adaptRenderCount).toBe(0, { message: "during adapt, render not triggered as the navbar has no app sub menus", }); // Set menu getService("menu").setCurrentMenu(1); await animationFrame(); expect(navbar.currentAppSections).toHaveLength(3, { message: "3 app sub menus" }); expect(navbar.currentAppSectionsExtra).toHaveLength(3, { message: "all app sub menus are inside the more menu", }); expect(adaptCount).toBe(3); expect(adaptRenderCount).toBe(1, { message: "during adapt, render triggered as the navbar does not have enough space for app sub menus", }); // Force small width await resize({ width: 240 }); await waitNavbarAdaptation(); expect(navbar.currentAppSectionsExtra).toHaveLength(3, { message: "all app sub menus are inside the more menu", }); expect(adaptCount).toBe(4); expect(adaptRenderCount).toBe(1, { message: "during adapt, render not triggered as the more menu dropdown is STILL the same", }); // Reset to full width await resize({ width: 1366 }); await waitNavbarAdaptation(); expect(navbar.currentAppSections).toHaveLength(3, { message: "still 3 app sub menus" }); expect(navbar.currentAppSectionsExtra).toHaveLength(0, { message: "all app sub menus are NO MORE inside the more menu", }); expect(adaptCount).toBe(5); expect(adaptRenderCount).toBe(2, { message: "during adapt, render triggered as the more menu dropdown is NO MORE the same", }); }); test.tags("desktop"); test("'more' menu sections properly updated on app change", async () => { defineMenus([ { id: "root", children: [ // First App { id: 1, children: [ { id: 10, children: [], name: "Section 10", appID: 1 }, { id: 11, children: [], name: "Section 11", appID: 1 }, { id: 12, children: [ { id: 120, children: [], name: "Section 120", appID: 1 }, { id: 121, children: [], name: "Section 121", appID: 1 }, { id: 122, children: [], name: "Section 122", appID: 1 }, ], name: "Section 12", appID: 1, }, ], name: "App1", appID: 1, }, // Second App { id: 2, children: [ { id: 20, children: [], name: "Section 20", appID: 2 }, { id: 21, children: [], name: "Section 21", appID: 2 }, { id: 22, children: [ { id: 220, children: [], name: "Section 220", appID: 2 }, { id: 221, children: [], name: "Section 221", appID: 2 }, { id: 222, children: [], name: "Section 222", appID: 2 }, ], name: "Section 22", appID: 2, }, ], name: "App2", appID: 2, }, ], name: "root", appID: "root", }, ]); // Force the parent width, to make this test independent of screen size await resize({ width: 1080 }); // TODO: this test case doesn't make sense since it relies on small widths // with `env.isSmall` still returning `false`. const env = await makeMockEnv(); Object.defineProperty(env, "isSmall", { get: () => false }); // Set menu and mount getService("menu").setCurrentMenu(1); await mountWithCleanup(NavBar); // Force minimal width await resize({ width: 0 }); await waitNavbarAdaptation(); expect(".o_menu_sections > *:not(.d-none)").toHaveCount(1, { message: "only one menu section should be displayed", }); expect(".o_menu_sections_more:not(.d-none)").toHaveCount(1, { message: "the displayed menu section should be the 'more' menu", }); // Open the more menu await contains(".o_menu_sections_more .dropdown-toggle").click(); expect(queryAllTexts(".dropdown-menu > *")).toEqual( ["Section 10", "Section 11", "Section 12", "Section 120", "Section 121", "Section 122"], { message: "'more' menu should contain App1 sections" } ); // Close the more menu await contains(".o_menu_sections_more .dropdown-toggle").click(); // Set App2 menu getService("menu").setCurrentMenu(2); await animationFrame(); // Open the more menu await contains(".o_menu_sections_more .dropdown-toggle").click(); expect(queryAllTexts(".dropdown-menu > *")).toEqual( ["Section 20", "Section 21", "Section 22", "Section 220", "Section 221", "Section 222"], { message: "'more' menu should contain App2 sections" } ); }); test("Do not execute adapt when navbar is destroyed", async () => { expect.assertions(3); class MyNavbar extends NavBar { async adapt() { expect.step("adapt NavBar"); return super.adapt(); } } await makeMockEnv(); // Set menu and mount getService("menu").setCurrentMenu(1); const navbar = await mountWithCleanup(MyNavbar); expect.verifySteps(["adapt NavBar"]); await resize(); await runAllTimers(); expect.verifySteps(["adapt NavBar"]); await resize(); destroy(navbar); await runAllTimers(); expect.verifySteps([]); });