diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/tools/test_menus.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/static/src/js/tools/test_menus.js')
| -rw-r--r-- | addons/web/static/src/js/tools/test_menus.js | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/addons/web/static/src/js/tools/test_menus.js b/addons/web/static/src/js/tools/test_menus.js new file mode 100644 index 00000000..9112b7f5 --- /dev/null +++ b/addons/web/static/src/js/tools/test_menus.js @@ -0,0 +1,321 @@ +(function (exports) { + /** + * The purpose of this test is to click on every installed App and then + * open each view. On each view, click on each filter. + */ + "use strict"; + var clientActionCount = 0; + var viewUpdateCount = 0; + var testedApps; + var testedMenus; + var blackListedMenus = ['base.menu_theme_store', 'base.menu_third_party', 'account.menu_action_account_bank_journal_form', 'pos_adyen.menu_pos_adyen_account']; + var appsMenusOnly = false; + let isEnterprise = odoo.session_info.server_version_info[5] === 'e'; + + function createWebClientHooks() { + var AbstractController = odoo.__DEBUG__.services['web.AbstractController']; + var DiscussWidget = odoo.__DEBUG__.services['mail/static/src/widgets/discuss/discuss.js']; + var WebClient = odoo.__DEBUG__.services["web.WebClient"]; + + WebClient.include({ + current_action_updated : function (action, controller) { + this._super(action, controller); + clientActionCount++; + }, + }); + + AbstractController.include({ + start: function(){ + this.$el.attr('data-view-type', this.viewType); + return this._super.apply(this, arguments); + }, + update: function(params, options) { + return this._super(params, options).then(function (){ + viewUpdateCount++; + }); + }, + }); + + if (DiscussWidget) { + DiscussWidget.include({ + /** + * Overriding a method that is called every time the discuss + * component is updated. + */ + _updateControlPanel: async function () { + await this._super(...arguments); + viewUpdateCount++; + }, + }); + } + } + + function clickEverywhere(xmlId, light){ + appsMenusOnly = light; + setTimeout(_clickEverywhere, 1000, xmlId); + } + + // Main function that starts orchestration of tests + async function _clickEverywhere(xmlId){ + console.log("Starting ClickEverywhere test"); + var startTime = performance.now(); + createWebClientHooks(); + testedApps = []; + testedMenus = []; + // finding applications menus + let appMenuItems; + if (isEnterprise) { + console.log("Odoo flavor: Enterprise"); + appMenuItems = document.querySelectorAll(xmlId ? + `a.o_app.o_menuitem[data-menu-xmlid="${xmlId}"]` : + 'a.o_app.o_menuitem' + ); + } else { + console.log("Odoo flavor: Community"); + appMenuItems = document.querySelectorAll(xmlId ? + `a.o_app[data-menu-xmlid="${xmlId}"]` : + 'a.o_app' + ); + } + console.log("Found", appMenuItems.length, "apps to test"); + try { + for (const app of appMenuItems) { + await testApp(app); + } + console.log("Test took", (performance.now() - startTime) / 1000, "seconds"); + console.log("Successfully tested", testedApps.length, " apps"); + console.log("Successfully tested", testedMenus.length - testedApps.length, "menus"); + console.log("test successful"); + } catch (err) { + console.log("Test took", (performance.now() - startTime) / 1000, "seconds"); + console.error(err || "test failed"); + } + } + + + /** + * Test an "App" menu item by orchestrating the following actions: + * 1 - clicking on its menuItem + * 2 - clicking on each view + * 3 - clicking on each menu + * 3.1 - clicking on each view + * @param {DomElement} element: the App menu item + * @returns {Promise} + */ + async function testApp(element) { + console.log("Testing app menu:", element.dataset.menuXmlid); + if (testedApps.indexOf(element.dataset.menuXmlid) >= 0) return; // Another infinite loop protection + testedApps.push(element.dataset.menuXmlid); + if (isEnterprise) { + await ensureHomeMenu(); + } + await testMenuItem(element); + if (appsMenusOnly === true) return; + const subMenuItems = document.querySelectorAll('.o_menu_entry_lvl_1, .o_menu_entry_lvl_2, .o_menu_entry_lvl_3, .o_menu_entry_lvl_4'); + for (const subMenuItem of subMenuItems) { + await testMenuItem(subMenuItem); + } + if (isEnterprise) { + await ensureHomeMenu(); + } + } + + + /** + * Test a menu item by: + * 1 - clikcing on the menuItem + * 2 - Orchestrate the view switch + * + * @param {DomElement} element: the menu item + * @returns {Promise} + */ + async function testMenuItem(element){ + if (testedMenus.indexOf(element.dataset.menuXmlid) >= 0) return Promise.resolve(); // Avoid infinite loop + var menuDescription = element.innerText.trim() + " " + element.dataset.menuXmlid; + var menuTimeLimit = 10000; + console.log("Testing menu", menuDescription); + testedMenus.push(element.dataset.menuXmlid); + if (blackListedMenus.includes(element.dataset.menuXmlid)) return Promise.resolve(); // Skip black listed menus + if (element.innerText.trim() == 'Settings') menuTimeLimit = 20000; + var startActionCount = clientActionCount; + await triggerClick(element, `menu item "${element.innerText.trim()}"`); + var isModal = false; + return waitForCondition(function () { + // sometimes, the app is just a modal that needs to be closed + var $modal = $('.modal[role="dialog"][open="open"]'); + if ($modal.length > 0) { + const closeButton = document.querySelector('header > button.close'); + if (closeButton) { + closeButton.focus(); + triggerClick(closeButton, "modal close button"); + } else { $modal.modal('hide'); } + isModal = true; + return true; + } + return startActionCount !== clientActionCount; + }, menuTimeLimit).then(function() { + if (!isModal) { + return testFilters(); + } + }).then(function () { + if (!isModal) { + return testViews(); + } + }).catch(function (err) { + console.error("Error while testing", menuDescription); + return Promise.reject(err); + }); + }; + + + /** + * Orchestrate the test of views + * This function finds the buttons that permit to switch views and orchestrate + * the click on each of them + * @returns {Promise} + */ + async function testViews() { + if (appsMenusOnly === true) { + return; + } + const switchButtons = document.querySelectorAll('nav.o_cp_switch_buttons > button.o_switch_view:not(.active):not(.o_map)'); + for (const switchButton of switchButtons) { + // Only way to get the viewType from the switchButton + const viewType = [...switchButton.classList] + .find(cls => cls !== 'o_switch_view' && cls.startsWith('o_')) + .slice(2); + console.log("Testing view switch:", viewType); + // timeout to avoid click debounce + setTimeout(function () { + const target = document.querySelector(`nav.o_cp_switch_buttons > button.o_switch_view.o_${viewType}`); + if (target) { + triggerClick(target, `${viewType} view switcher`); + } + }, 250); + await waitForCondition(() => document.querySelector('.o_action_manager > .o_action.o_view_controller').dataset.viewType === viewType); + await testFilters(); + } + } + + /** + * Test filters + * Click on each filter in the control pannel + */ + async function testFilters() { + if (appsMenusOnly === true) { + return; + } + const filterMenuButton = document.querySelector('.o_control_panel .o_filter_menu > button'); + if (!filterMenuButton) { + return; + } + // Open the filter menu dropdown + await triggerClick(filterMenuButton, `toggling menu "${filterMenuButton.innerText.trim()}"`); + + const filterMenuItems = document.querySelectorAll('.o_control_panel .o_filter_menu > ul > li.o_menu_item'); + console.log("Testing", filterMenuItems.length, "filters"); + + for (const filter of filterMenuItems) { + const currentViewCount = viewUpdateCount; + const filterLink = filter.querySelector('a'); + await triggerClick(filterLink, `filter "${filter.innerText.trim()}"`); + if (filterLink.classList.contains('o_menu_item_parent')) { + // If a fitler has options, it will simply unfold and show all options. + // We then click on the first one. + const firstOption = filter.querySelector('.o_menu_item_options > li.o_item_option > a'); + console.log(); + await triggerClick(firstOption, `filter option "${firstOption.innerText.trim()}"`); + } + await waitForCondition(() => currentViewCount !== viewUpdateCount); + } + } + + // utility functions + /** + * Wait a certain amount of time for a condition to occur + * @param {function} stopCondition a function that returns a boolean + * @returns {Promise} that is rejected if the timeout is exceeded + */ + function waitForCondition(stopCondition, tl=10000) { + var prom = new Promise(function (resolve, reject) { + var interval = 250; + var timeLimit = tl; + + function checkCondition() { + if (stopCondition()) { + resolve(); + } else { + timeLimit -= interval; + if (timeLimit > 0) { + // recursive call until the resolve or the timeout + setTimeout(checkCondition, interval); + } else { + console.error('Timeout, the clicked element took more than', tl/1000,'seconds to load'); + reject(); + } + } + } + setTimeout(checkCondition, interval); + }); + return prom; + } + + + /** + * Chain deferred actions. + * + * @param {jQueryElement} $elements a list of jquery elements to be passed as arg to the function + * @param {Promise} promise the promise on which other promises will be chained + * @param {function} f the function to be deferred + * @returns {Promise} the chained promise + */ + function chainDeferred($elements, promise, f) { + _.each($elements, function(el) { + promise = promise.then(function () { + return f(el); + }); + }); + return promise; + } + + /** + * Make sure the home menu is open + */ + async function ensureHomeMenu() { + const menuToggle = document.querySelector('nav.o_main_navbar > a.o_menu_toggle.fa-th'); + if (menuToggle) { + await triggerClick(menuToggle, 'home menu toggle button'); + await waitForCondition(() => document.querySelector('.o_home_menu')); + } + } + + const MOUSE_EVENTS = [ + 'mouseover', + 'mouseenter', + 'mousedown', + 'mouseup', + 'click', + ]; + + /** + * Simulate all of the mouse events triggered during a click action. + * @param {EventTarget} target the element on which to perform the click + * @param {string} elDescription description of the item + * @returns {Promise} resolved after next animation frame + */ + async function triggerClick(target, elDescription) { + if (target) { + console.log("Clicking on", elDescription); + } else { + throw new Error(`No element "${elDescription}" found.`); + } + MOUSE_EVENTS.forEach(type => { + const event = new MouseEvent(type, { bubbles: true, cancelable: true, view: window }); + target.dispatchEvent(event); + }); + await new Promise(setTimeout); + await new Promise(r => requestAnimationFrame(r)); + } + + exports.clickEverywhere = clickEverywhere; +})(window); |
