""" Student Navigation Page Object Model Handles student navigation header including profile dropdown and logout. Scope: student_nav """ import time from selenium.webdriver.common.by import By from pages.base_page import BasePage from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from config.config import SHORT_WAIT, BASE_URL class StudentNavPage(BasePage): """Page Object for Student Navigation Header""" # Navigation links DASHBOARD_LINK = (By.CSS_SELECTOR, "[data-testid='student_nav__dashboard_link']") ASSESSMENTS_LINK = (By.CSS_SELECTOR, "[data-testid='student_nav__assessments_link']") # Profile dropdown button (user icon with dropdown) - IMPLEMENTED ✅ PROFILE_BUTTON = (By.CSS_SELECTOR, "[data-testid='student_nav__profile_button']") # Profile dropdown menu items - IMPLEMENTED ✅ PROFILE_DROPDOWN = (By.CSS_SELECTOR, "[data-testid='student_nav__profile_dropdown']") EDIT_PROFILE_BUTTON = (By.CSS_SELECTOR, "[data-testid='student_nav__edit_profile_button']") RESET_PASSWORD_BUTTON = (By.CSS_SELECTOR, "[data-testid='student_nav__reset_password_button']") SIGN_OUT_BUTTON = (By.CSS_SELECTOR, "[data-testid='student_nav__sign_out_button']") def __init__(self, driver): """Initialize Student Navigation Page""" super().__init__(driver) def click_dashboard(self): """Click Dashboard navigation link""" self.click_element(self.DASHBOARD_LINK) self.wait.wait_for_url_contains("/dashboard") def click_assessments(self): """Click Assessments navigation link""" self.click_element(self.ASSESSMENTS_LINK) self.wait.wait_for_url_contains("/assessments") def click_profile_button(self): """ Click profile button to open dropdown IMPORTANT: Ensures no modals are blocking (password reset or profile incomplete). Uses robust modal detection with fallback mechanisms. """ # First, ensure no modals are blocking the button from pages.mandatory_reset_page import MandatoryResetPage from pages.profile_incomplete_page import ProfileIncompletePage from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from config.config import SHORT_WAIT, MEDIUM_WAIT reset_page = MandatoryResetPage(self.driver) profile_incomplete = ProfileIncompletePage(self.driver) # Handle password reset modal if present if reset_page.is_modal_present(): print("⚠️ Password reset modal blocking - handling automatically...") from utils.password_tracker import password_tracker from config.config import TEST_PASSWORD, TEST_NEW_PASSWORD current_password = password_tracker.get_password("", TEST_PASSWORD) reset_page.reset_password( current_password=current_password, new_password=TEST_NEW_PASSWORD, confirm_password=TEST_NEW_PASSWORD, student_cpid="" ) # Wait for modal to fully disappear self._wait_for_modal_overlays_to_disappear() # Handle profile incomplete modal if present - click "Complete Profile Now" to dismiss if profile_incomplete.is_modal_present(): print("⚠️ Profile incomplete modal blocking - clicking 'Complete Profile Now'...") profile_incomplete.click_complete() # Wait for navigation and modal to disappear self._wait_for_modal_overlays_to_disappear() # Final check: Wait for any remaining modal overlays to disappear self._wait_for_modal_overlays_to_disappear() # Verify no modals are blocking before clicking max_retries = 3 for attempt in range(max_retries): if not reset_page.is_modal_present() and not profile_incomplete.is_modal_present(): break if attempt < max_retries - 1: print(f"⚠️ Modals still present, waiting... (attempt {attempt + 1}/{max_retries})") time.sleep(1) self._wait_for_modal_overlays_to_disappear() else: raise Exception("Modals are still blocking profile button after multiple attempts") # Use data-testid locator (implemented ✅) profile_btn = self.wait.wait_for_element_clickable(self.PROFILE_BUTTON) self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", profile_btn) time.sleep(0.2) # Brief wait for scroll # Try clicking with retry mechanism try: profile_btn.click() except Exception as e: # If click is intercepted, wait a bit more and try JavaScript click if "click intercepted" in str(e).lower(): print("⚠️ Click intercepted, waiting for overlays and retrying with JavaScript...") self._wait_for_modal_overlays_to_disappear() time.sleep(0.5) self.driver.execute_script("arguments[0].click();", profile_btn) else: raise # Wait for dropdown to appear using data-testid (implemented ✅) time.sleep(0.3) # Brief wait for React state to update try: self.wait.wait_for_element_visible(self.SIGN_OUT_BUTTON) except: # Check if we're already logged out current_url = self.driver.current_url from config.config import BASE_URL if "/login" in current_url or current_url.rstrip("/") == BASE_URL.rstrip("/"): return # Already logged out raise Exception(f"Profile dropdown did not open. Current URL: {current_url}") def _wait_for_modal_overlays_to_disappear(self): """ Wait for all modal overlays to disappear. Checks for common modal overlay patterns that might block clicks. """ from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from config.config import SHORT_WAIT # Check for common modal overlay selectors (high z-index overlays) modal_overlay_selectors = [ "div.fixed.inset-0.bg-black\\/60.z-\\[99999\\]", # Password reset modal "div.fixed.inset-0.bg-black\\/50.z-\\[9999\\]", # Profile incomplete modal "div[class*='fixed'][class*='inset-0'][class*='bg-black'][class*='z-[99999]']", # Variant 1 "div[class*='fixed'][class*='inset-0'][class*='bg-black'][class*='z-[9999]']", # Variant 2 "div.fixed.inset-0[class*='z-[99999]']", # Simplified high z-index "div.fixed.inset-0[class*='z-[9999]']", # Simplified medium-high z-index ] for selector in modal_overlay_selectors: try: WebDriverWait(self.driver, SHORT_WAIT).until( EC.invisibility_of_element_located((By.CSS_SELECTOR, selector)) ) except: # Overlay might not exist or already gone, continue pass def click_sign_out(self): """ Click Sign Out button in profile dropdown IMPORTANT: Profile dropdown must be open first (call click_profile_button). Ensures no modals are blocking before clicking. """ # Ensure no modals are blocking before clicking sign out from pages.mandatory_reset_page import MandatoryResetPage from pages.profile_incomplete_page import ProfileIncompletePage reset_page = MandatoryResetPage(self.driver) profile_incomplete = ProfileIncompletePage(self.driver) # Wait for any modals to disappear max_retries = 3 for attempt in range(max_retries): if not reset_page.is_modal_present() and not profile_incomplete.is_modal_present(): break if attempt < max_retries - 1: print(f"⚠️ Modals detected before sign out, waiting... (attempt {attempt + 1}/{max_retries})") self._wait_for_modal_overlays_to_disappear() time.sleep(1) else: raise Exception("Modals are still blocking sign out button after multiple attempts") # Final wait for modal overlays self._wait_for_modal_overlays_to_disappear() # Use data-testid locator (implemented ✅) signout_btn = self.wait.wait_for_element_clickable(self.SIGN_OUT_BUTTON) self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", signout_btn) time.sleep(0.2) # Try clicking with retry mechanism try: signout_btn.click() except Exception as e: # If click is intercepted, wait for overlays and try JavaScript click if "click intercepted" in str(e).lower(): print("⚠️ Sign out click intercepted, waiting for overlays and retrying with JavaScript...") self._wait_for_modal_overlays_to_disappear() time.sleep(0.5) self.driver.execute_script("arguments[0].click();", signout_btn) else: raise # Wait for redirect to login page self.wait.wait_for_url_contains("/") # Redirects to login (root) def logout(self): """ Complete logout flow: Open profile dropdown and click Sign Out This is the main method to use for logout. """ self.click_profile_button() self.click_sign_out()