218 lines
9.8 KiB
Python
218 lines
9.8 KiB
Python
"""
|
|
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()
|