diff --git a/modules/core/navigation/navigation.js b/modules/core/navigation/navigation.js index 57e608cf7..c9cca9ce8 100644 --- a/modules/core/navigation/navigation.js +++ b/modules/core/navigation/navigation.js @@ -7,6 +7,10 @@ function trackLocationSearchChanges() { } window.addEventListener('popstate', function(event) { + // Signal navigation start for Selenium detection + document.body.setAttribute('data-navigation-state', 'loading'); + window.dispatchEvent(new CustomEvent('navigation-started', { detail: { url: window.location.href } })); + Hm_Ajax.abort_all_requests(); if (event.state) { @@ -27,12 +31,18 @@ window.addEventListener('popstate', function(event) { unMountSubscribers[previousLocationSearch]?.(); trackLocationSearchChanges(); + + // Signal navigation completion for Selenium detection + document.body.setAttribute('data-navigation-state', 'complete'); + window.dispatchEvent(new CustomEvent('navigation-completed', { detail: { url: window.location.href } })); }); window.addEventListener('load', function() { if (!hm_is_logged()) { return; } + // Initialize navigation state for Selenium detection + document.body.setAttribute('data-navigation-state', 'complete'); const unMountCallback = renderPage(window.location.href); history.replaceState({ main: $('#cypht-main').prop('outerHTML'), scripts: extractCustomScripts($(document)) }, ""); @@ -61,6 +71,8 @@ $(document).on('click', '.cypht-layout a', function(event) { const targetParams = new URLSearchParams(href.split('?')[1]); if (currentPage !== targetParams.toString()) { Hm_Ajax.abort_all_requests(); + // Signal navigation start immediately when clicking + document.body.setAttribute('data-navigation-state', 'loading'); navigate(autoAppendParamsForNavigation(href)); } } @@ -94,6 +106,10 @@ function autoAppendParamsForNavigation(href) } async function navigate(url, loaderMessage) { + // Signal navigation start for Selenium detection + document.body.setAttribute('data-navigation-state', 'loading'); + window.dispatchEvent(new CustomEvent('navigation-started', { detail: { url } })); + showRoutingToast(loaderMessage); Hm_Ajax.abort_all_requests(); @@ -168,7 +184,16 @@ async function navigate(url, loaderMessage) { unMountSubscribers[previousLocationSearch]?.(); trackLocationSearchChanges(); + + // Signal navigation completion for Selenium detection + document.body.setAttribute('data-navigation-state', 'complete'); + window.dispatchEvent(new CustomEvent('navigation-completed', { detail: { url } })); + } catch (error) { + // Signal navigation error for Selenium detection + document.body.setAttribute('data-navigation-state', 'error'); + window.dispatchEvent(new CustomEvent('navigation-failed', { detail: { url, error: error.message } })); + Hm_Notices.show(error.message, 'danger'); console.log(error); } finally { diff --git a/tests/selenium/base.py b/tests/selenium/base.py index 3af3b80d0..6da86b581 100644 --- a/tests/selenium/base.py +++ b/tests/selenium/base.py @@ -170,50 +170,76 @@ def wait_on_sys_message(self, timeout=30): def wait_for_navigation_to_complete(self, timeout=30): print(" - waiting for the navigation to complete...") - # Wait for the main content to be updated and any loading indicators to disappear - try: - # Wait for any loading indicators to disappear - WebDriverWait(self.driver, 5).until_not( - lambda driver: len(driver.find_elements(By.ID, "loading_indicator")) > 0 - ) - except: - # Loading icon might not be present, continue - pass + import time - # Wait for the main content area to be stable try: - WebDriverWait(self.driver, timeout).until( - lambda driver: driver.execute_script(""" - return new Promise((resolve) => { - let lastContent = ''; - let stableCount = 0; - const checkStability = () => { - const mainContent = document.querySelector('main')?.innerHTML || ''; - if (mainContent === lastContent) { - stableCount++; - if (stableCount >= 3) { - resolve(true); - return; - } - } else { - stableCount = 0; - lastContent = mainContent; - } - setTimeout(checkStability, 100); - }; - checkStability(); - }); - """) - ) - except: - # Fallback: just wait for the main element to be present - print(" - fallback: waiting for main element") - WebDriverWait(self.driver, timeout).until( - exp_cond.presence_of_element_located((By.TAG_NAME, "main")) - ) - # Additional wait for any dynamic content - import time - time.sleep(1) + # First, check if we have navigation state attributes (new method) + try: + # Wait for navigation to start (state changes to 'loading') + WebDriverWait(self.driver, 5).until( + lambda driver: driver.execute_script( + 'return document.body.getAttribute("data-navigation-state") === "loading"' + ) + ) + print(" - navigation start detected") + + # Then wait for navigation to complete (state changes to 'complete') + WebDriverWait(self.driver, timeout).until( + lambda driver: driver.execute_script( + 'return document.body.getAttribute("data-navigation-state") === "complete"' + ) + ) + print(" - navigation completion detected via state attribute") + + # Small delay to ensure DOM is settled + time.sleep(0.5) + return + + except Exception as state_error: + print(f" - navigation state monitoring failed: {state_error}, trying fallback methods") + + # Fallback 1: Try to detect fetch requests + try: + get_current_navigations_request_entries_length = lambda: self.driver.execute_script( + 'return window.performance.getEntriesByType("resource").filter((r) => r.initiatorType === "fetch").length' + ) + navigation_length = get_current_navigations_request_entries_length() + + WebDriverWait(self.driver, min(timeout, 10)).until( + lambda driver: get_current_navigations_request_entries_length() > navigation_length + ) + print(" - navigation detected via fetch requests") + + time.sleep(0.5) + return + + except Exception as fetch_error: + print(f" - fetch monitoring failed: {fetch_error}, trying final fallback") + + # Fallback 2: Check for loading indicators and main element + try: + WebDriverWait(self.driver, 5).until_not( + lambda driver: len(driver.find_elements(By.ID, "loading_indicator")) > 0 + ) + print(" - loading indicator disappeared") + except: + pass + + try: + WebDriverWait(self.driver, min(timeout, 15)).until( + exp_cond.presence_of_element_located((By.TAG_NAME, "main")) + ) + print(" - main element present") + + time.sleep(1) + + except Exception as main_error: + print(f" - all navigation waiting methods failed: {state_error}, {fetch_error}, {main_error}") + time.sleep(2) + + except Exception as e: + print(f" - unexpected error in navigation waiting: {e}") + time.sleep(2) def wait_for_settings_to_expand(self): print(" - waiting for the settings section to expand...") diff --git a/tests/selenium/login.py b/tests/selenium/login.py index ef796626f..875255941 100644 --- a/tests/selenium/login.py +++ b/tests/selenium/login.py @@ -51,11 +51,13 @@ def good_login(self): def good_logout(self): self.logout() - self.wait() + # self.wait() self.safari_workaround() - self.wait_on_class('sys_messages') - sys_messages = self.by_class('sys_messages') - assert sys_messages is not None + self.wait(By.CLASS_NAME, 'login_form', 60) + # debugging line + print('debugging line') + print(self.by_class('login_form')) + assert self.by_class('login_form') != None if __name__ == '__main__': diff --git a/tests/selenium/pages.py b/tests/selenium/pages.py index 15c06040b..8c2bfe389 100644 --- a/tests/selenium/pages.py +++ b/tests/selenium/pages.py @@ -351,4 +351,4 @@ def profiles(self): if __name__ == '__main__': print("PAGES TEST") - test_runner(PageTests) + test_runner(PageTests) \ No newline at end of file diff --git a/tests/selenium/servers.py b/tests/selenium/servers.py index 42ccb3b36..c1be67582 100644 --- a/tests/selenium/servers.py +++ b/tests/selenium/servers.py @@ -31,8 +31,39 @@ def load_servers_page(self): def server_stmp_and_imap_add(self): self.toggle_server_section('server_config') self.wait_on_class('imap-jmap-smtp-btn') - self.by_id('add_new_server_button').click() - # self.wait_on_class('srv_setup_stepper_profile_name') + + # Wait for the add button to be clickable (combines presence and clickability) + add_button = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((By.ID, "add_new_server_button")) + ) + + # Scroll the button into view before clicking + self.driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});", add_button) + sleep(0.5) + + try: + add_button.click() + print("Normal click succeeded") + except Exception as e: + print(f"Normal click failed: {e}. Trying JavaScript click...") + self.driver.execute_script("arguments[0].click();", add_button) + + # Wait for the form to appear after clicking + try: + WebDriverWait(self.driver, 10).until( + EC.any_of( + EC.presence_of_element_located((By.ID, 'srv_setup_stepper_profile_name')), + EC.presence_of_element_located((By.NAME, 'srv_setup_stepper_profile_name')) + ) + ) + print("Server setup form appeared") + except Exception as e: + print(f"Server setup form did not appear: {e}") + # Debug: print current page state + print("Current URL:", self.driver.current_url) + print("Page title:", self.driver.title) + raise e + name = self.by_id('srv_setup_stepper_profile_name') name.send_keys('Test') email = self.by_name('srv_setup_stepper_email')