summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/tools/test_menus.js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/tools/test_menus.js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (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.js321
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);