499 lines
22 KiB
Python
499 lines
22 KiB
Python
"""
|
|
Mandatory Password Reset Page Object Model
|
|
|
|
Handles first-login password reset modal.
|
|
Scope: mandatory_reset
|
|
"""
|
|
from selenium.webdriver.common.by import By
|
|
from pages.base_page import BasePage
|
|
|
|
|
|
class MandatoryResetPage(BasePage):
|
|
"""Page Object for Mandatory Password Reset Modal"""
|
|
|
|
# Locators using data-testid (scope: mandatory_reset)
|
|
MODAL = (By.CSS_SELECTOR, "[data-testid='mandatory_reset__modal']")
|
|
CONTINUE_BUTTON = (By.CSS_SELECTOR, "[data-testid='mandatory_reset__continue_button']")
|
|
FORM = (By.CSS_SELECTOR, "[data-testid='mandatory_reset__form']")
|
|
CURRENT_PASSWORD_INPUT = (By.CSS_SELECTOR, "[data-testid='mandatory_reset__current_password_input']")
|
|
NEW_PASSWORD_INPUT = (By.CSS_SELECTOR, "[data-testid='mandatory_reset__new_password_input']")
|
|
CONFIRM_PASSWORD_INPUT = (By.CSS_SELECTOR, "[data-testid='mandatory_reset__confirm_password_input']")
|
|
CURRENT_PASSWORD_ERROR = (By.CSS_SELECTOR, "[data-testid='mandatory_reset__current_password_error']")
|
|
NEW_PASSWORD_ERROR = (By.CSS_SELECTOR, "[data-testid='mandatory_reset__new_password_error']")
|
|
CONFIRM_PASSWORD_ERROR = (By.CSS_SELECTOR, "[data-testid='mandatory_reset__confirm_password_error']")
|
|
BACK_BUTTON = (By.CSS_SELECTOR, "[data-testid='mandatory_reset__back_button']")
|
|
SUBMIT_BUTTON = (By.CSS_SELECTOR, "[data-testid='mandatory_reset__submit_button']")
|
|
|
|
def __init__(self, driver):
|
|
"""Initialize Mandatory Reset Page"""
|
|
super().__init__(driver)
|
|
|
|
def is_modal_present(self):
|
|
"""
|
|
Check if mandatory reset modal is present
|
|
|
|
Uses multiple detection strategies for 100% reliability:
|
|
1. data-testid attribute (if present)
|
|
2. CSS selector for modal overlay
|
|
3. Text content detection ("Welcome to Cognitive Prism", "Reset Password")
|
|
4. DOM structure detection (modal container with specific classes)
|
|
|
|
Returns:
|
|
bool: True if modal is visible
|
|
"""
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
import time
|
|
|
|
# Strategy 1: Check by data-testid (primary method) - FAST detection
|
|
# Use quick check (200ms) for speed
|
|
try:
|
|
from utils.smart_wait_optimizer import SmartWaitOptimizer
|
|
if SmartWaitOptimizer.quick_check_modal_present(self.driver, self.MODAL):
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
# Strategy 2: Check for modal overlay by CSS pattern
|
|
# Modal overlay: fixed inset-0 bg-black/60 z-[99999]
|
|
overlay_selectors = [
|
|
"div.fixed.inset-0.bg-black\\/60.z-\\[99999\\]", # Exact match
|
|
"div[class*='fixed'][class*='inset-0'][class*='bg-black'][class*='z-[99999]']", # Partial match
|
|
"div.fixed.inset-0[class*='z-[99999]']", # Simplified
|
|
"div.fixed.inset-0.bg-black", # More generic
|
|
]
|
|
|
|
for selector in overlay_selectors:
|
|
try:
|
|
overlay = WebDriverWait(self.driver, 2).until(
|
|
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
|
|
)
|
|
if overlay.is_displayed():
|
|
# Verify it's the password reset modal by checking for form elements or text
|
|
try:
|
|
# Check if form or continue button is present
|
|
form_present = self.is_element_present(self.FORM, timeout=1) or \
|
|
self.is_element_present(self.CONTINUE_BUTTON, timeout=1)
|
|
if form_present:
|
|
return True
|
|
except:
|
|
pass
|
|
except:
|
|
continue
|
|
|
|
# Strategy 3: Check page source for password reset modal text
|
|
# This is the MOST RELIABLE fallback - if text is present, modal is there
|
|
try:
|
|
page_text = self.driver.page_source.lower()
|
|
password_reset_indicators = [
|
|
"welcome to cognitive prism",
|
|
"reset your password",
|
|
"password reset required",
|
|
"mandatory password reset",
|
|
"secure your account",
|
|
]
|
|
|
|
for indicator in password_reset_indicators:
|
|
if indicator in page_text:
|
|
# Double-check: Make sure we're not on login page (which might have similar text)
|
|
current_url = self.driver.current_url.lower()
|
|
if "/login" not in current_url:
|
|
# We're on dashboard/student page and password reset text is present
|
|
# This means modal is definitely present
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
# Strategy 4: Check for modal container structure
|
|
# Look for modal container with specific structure
|
|
try:
|
|
# Look for modal container with "Welcome" text
|
|
modal_containers = self.driver.find_elements(By.XPATH,
|
|
"//div[contains(@class, 'fixed') and contains(@class, 'inset-0')]"
|
|
"//*[contains(text(), 'Welcome to Cognitive Prism') or contains(text(), 'Password Reset Required')]"
|
|
)
|
|
if modal_containers:
|
|
for container in modal_containers:
|
|
if container.is_displayed():
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
# Strategy 5: Check for Continue button (step 1) or Form (step 2) directly
|
|
try:
|
|
# Check for Continue button (step 1)
|
|
continue_btn = self.driver.find_elements(By.XPATH,
|
|
"//button[contains(text(), 'Continue') or contains(text(), 'Reset Password')]"
|
|
)
|
|
for btn in continue_btn:
|
|
if btn.is_displayed():
|
|
# Check if it's inside a modal overlay
|
|
parent = btn.find_element(By.XPATH, "./ancestor::div[contains(@class, 'fixed')]")
|
|
if parent and parent.is_displayed():
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
# Check for form inputs (step 2)
|
|
form_inputs = self.driver.find_elements(By.XPATH,
|
|
"//input[@name='currentPassword' or @name='newPassword' or @name='confirmPassword']"
|
|
)
|
|
for inp in form_inputs:
|
|
if inp.is_displayed():
|
|
# Check if it's inside a modal overlay
|
|
parent = inp.find_element(By.XPATH, "./ancestor::div[contains(@class, 'fixed')]")
|
|
if parent and parent.is_displayed():
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
return False
|
|
|
|
def click_continue(self):
|
|
"""Click Continue button to show password reset form"""
|
|
# Wait for button to be clickable and visible
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
from selenium.webdriver.common.by import By
|
|
from config.config import SHORT_WAIT, MEDIUM_WAIT
|
|
import time
|
|
|
|
# Strategy 1: Try data-testid locator first (if present)
|
|
button = None
|
|
try:
|
|
button = WebDriverWait(self.driver, 2).until(
|
|
EC.element_to_be_clickable(self.CONTINUE_BUTTON)
|
|
)
|
|
except:
|
|
# Strategy 2: Find button with span containing "Continue" (VERIFIED WORKING by DOM Inspector)
|
|
# DOM Inspector confirmed: //button[.//span[contains(text(), 'Continue')]] works
|
|
try:
|
|
button = WebDriverWait(self.driver, MEDIUM_WAIT).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
"//button[.//span[contains(text(), 'Continue')]]"
|
|
))
|
|
)
|
|
except:
|
|
# Strategy 3: Find button containing "Reset Password" (VERIFIED WORKING by DOM Inspector)
|
|
try:
|
|
button = WebDriverWait(self.driver, MEDIUM_WAIT).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
"//button[contains(., 'Reset Password')]"
|
|
))
|
|
)
|
|
except:
|
|
# Strategy 4: Find button inside modal overlay (VERIFIED WORKING by DOM Inspector)
|
|
try:
|
|
button = WebDriverWait(self.driver, MEDIUM_WAIT).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
"//div[contains(@class, 'fixed')]//button[contains(., 'Continue')]"
|
|
))
|
|
)
|
|
except:
|
|
# Strategy 5: Find span first, then get parent button
|
|
try:
|
|
span = WebDriverWait(self.driver, MEDIUM_WAIT).until(
|
|
EC.presence_of_element_located((By.XPATH,
|
|
"//span[contains(text(), 'Continue to Reset Password')]"
|
|
))
|
|
)
|
|
# Get parent button using JavaScript (more reliable for React components)
|
|
button = self.driver.execute_script("return arguments[0].closest('button');", span)
|
|
if not button or not button.is_displayed():
|
|
raise Exception("Button not visible")
|
|
except Exception as e:
|
|
raise Exception(f"❌ Continue button not found after all verified strategies. "
|
|
f"Last error: {e}. "
|
|
f"Use scripts/debug_dom_inspector.py to inspect DOM structure.")
|
|
|
|
if not button:
|
|
raise Exception("❌ Continue button not found after all detection strategies")
|
|
|
|
# Scroll button into view if needed
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", button)
|
|
time.sleep(0.3)
|
|
|
|
# Click using JavaScript (more reliable for modals)
|
|
try:
|
|
self.driver.execute_script("arguments[0].click();", button)
|
|
except:
|
|
# Fallback to regular click
|
|
button.click()
|
|
|
|
# Wait for form to appear (form appears immediately after click)
|
|
# Check for form by data-testid or by input fields
|
|
try:
|
|
WebDriverWait(self.driver, SHORT_WAIT).until(
|
|
EC.visibility_of_element_located(self.FORM)
|
|
)
|
|
except:
|
|
# Fallback: Check for password input fields
|
|
try:
|
|
WebDriverWait(self.driver, SHORT_WAIT).until(
|
|
EC.visibility_of_element_located(self.CURRENT_PASSWORD_INPUT)
|
|
)
|
|
except:
|
|
# Final fallback: Check for any password input
|
|
try:
|
|
WebDriverWait(self.driver, SHORT_WAIT).until(
|
|
EC.presence_of_element_located((By.XPATH, "//input[@name='currentPassword' or @name='newPassword']"))
|
|
)
|
|
except Exception as e:
|
|
raise Exception(f"❌ Password reset form did not appear after clicking Continue. Error: {e}")
|
|
|
|
def enter_current_password(self, password):
|
|
"""Enter current password"""
|
|
self.send_keys(self.CURRENT_PASSWORD_INPUT, password)
|
|
|
|
def enter_new_password(self, password):
|
|
"""Enter new password"""
|
|
self.send_keys(self.NEW_PASSWORD_INPUT, password)
|
|
|
|
def enter_confirm_password(self, password):
|
|
"""Enter confirm password"""
|
|
self.send_keys(self.CONFIRM_PASSWORD_INPUT, password)
|
|
|
|
def reset_password(self, current_password, new_password, confirm_password=None, student_cpid=None):
|
|
"""
|
|
Complete password reset flow with WORLD-CLASS robust error handling
|
|
|
|
This method ensures 100% reliability by:
|
|
1. Verifying modal presence
|
|
2. Handling 2-step flow (Continue button → Form)
|
|
3. Filling all fields with proper waits
|
|
4. Submitting and waiting for API call completion
|
|
5. Verifying success (toast message or modal closure)
|
|
6. Checking for errors and handling them
|
|
7. Updating password tracker on success
|
|
|
|
Args:
|
|
current_password: Current password (default password)
|
|
new_password: New password to set
|
|
confirm_password: Confirm password (defaults to new_password)
|
|
student_cpid: Student CPID (optional, for password tracking)
|
|
|
|
Raises:
|
|
Exception: If password reset fails for any reason
|
|
"""
|
|
import time
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
from selenium.webdriver.common.by import By
|
|
from config.config import SHORT_WAIT, MEDIUM_WAIT
|
|
LONG_WAIT = MEDIUM_WAIT * 2 # 20 seconds for API calls
|
|
|
|
if confirm_password is None:
|
|
confirm_password = new_password
|
|
|
|
print(f"🔐 Starting password reset flow for {student_cpid or 'student'}")
|
|
|
|
# STEP 1: Verify modal is present
|
|
if not self.is_modal_present():
|
|
raise Exception("❌ Password reset modal is not present - cannot proceed")
|
|
print("✅ Password reset modal detected")
|
|
|
|
# STEP 2: Handle 2-step flow (Continue button → Form)
|
|
try:
|
|
# Check if form is already visible (step 2)
|
|
WebDriverWait(self.driver, 2).until(
|
|
EC.visibility_of_element_located(self.FORM)
|
|
)
|
|
print("✅ Password reset form already visible (step 2)")
|
|
except:
|
|
# Form not visible, need to click continue button (step 1)
|
|
print("📋 Clicking Continue button to show password reset form...")
|
|
try:
|
|
self.click_continue()
|
|
# Wait for form to appear
|
|
WebDriverWait(self.driver, SHORT_WAIT).until(
|
|
EC.visibility_of_element_located(self.FORM)
|
|
)
|
|
print("✅ Password reset form is now visible")
|
|
except Exception as e:
|
|
raise Exception(f"❌ Failed to show password reset form: {e}")
|
|
|
|
# STEP 3: Clear any existing errors before filling
|
|
time.sleep(0.3) # Brief wait for form to stabilize
|
|
|
|
# STEP 4: Fill form fields with proper waits and verification
|
|
print("📝 Filling password reset form...")
|
|
|
|
# Current password
|
|
try:
|
|
current_input = self.wait.wait_for_element_visible(self.CURRENT_PASSWORD_INPUT)
|
|
current_input.clear()
|
|
time.sleep(0.2)
|
|
current_input.send_keys(current_password)
|
|
print(" ✅ Current password entered")
|
|
except Exception as e:
|
|
raise Exception(f"❌ Failed to enter current password: {e}")
|
|
|
|
# New password
|
|
try:
|
|
new_input = self.wait.wait_for_element_visible(self.NEW_PASSWORD_INPUT)
|
|
new_input.clear()
|
|
time.sleep(0.2)
|
|
new_input.send_keys(new_password)
|
|
print(" ✅ New password entered")
|
|
except Exception as e:
|
|
raise Exception(f"❌ Failed to enter new password: {e}")
|
|
|
|
# Confirm password
|
|
try:
|
|
confirm_input = self.wait.wait_for_element_visible(self.CONFIRM_PASSWORD_INPUT)
|
|
confirm_input.clear()
|
|
time.sleep(0.2)
|
|
confirm_input.send_keys(confirm_password)
|
|
print(" ✅ Confirm password entered")
|
|
except Exception as e:
|
|
raise Exception(f"❌ Failed to enter confirm password: {e}")
|
|
|
|
# STEP 5: Verify no validation errors before submit
|
|
time.sleep(0.3) # Wait for any client-side validation
|
|
if self.has_errors():
|
|
error_text = []
|
|
try:
|
|
if self.is_element_visible(self.CURRENT_PASSWORD_ERROR, timeout=1):
|
|
error_text.append(f"Current password: {self.find_element(self.CURRENT_PASSWORD_ERROR).text}")
|
|
except:
|
|
pass
|
|
try:
|
|
if self.is_element_visible(self.NEW_PASSWORD_ERROR, timeout=1):
|
|
error_text.append(f"New password: {self.find_element(self.NEW_PASSWORD_ERROR).text}")
|
|
except:
|
|
pass
|
|
try:
|
|
if self.is_element_visible(self.CONFIRM_PASSWORD_ERROR, timeout=1):
|
|
error_text.append(f"Confirm password: {self.find_element(self.CONFIRM_PASSWORD_ERROR).text}")
|
|
except:
|
|
pass
|
|
if error_text:
|
|
raise Exception(f"❌ Validation errors before submit: {'; '.join(error_text)}")
|
|
|
|
# STEP 6: Submit form
|
|
print("🚀 Submitting password reset form...")
|
|
try:
|
|
submit_button = self.wait.wait_for_element_clickable(self.SUBMIT_BUTTON)
|
|
# Scroll into view if needed
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", submit_button)
|
|
time.sleep(0.2)
|
|
submit_button.click()
|
|
print(" ✅ Submit button clicked")
|
|
except Exception as e:
|
|
raise Exception(f"❌ Failed to click submit button: {e}")
|
|
|
|
# STEP 7: Wait for API call to complete (check for loading state to finish)
|
|
print("⏳ Waiting for password reset API call to complete...")
|
|
time.sleep(1) # Initial wait for API call to start
|
|
|
|
# Wait for loading state to finish (submit button becomes enabled again or modal closes)
|
|
max_wait = LONG_WAIT
|
|
start_time = time.time()
|
|
api_completed = False
|
|
|
|
while (time.time() - start_time) < max_wait:
|
|
# Check if modal is closing (success indicator)
|
|
try:
|
|
if not self.is_element_present(self.MODAL, timeout=1):
|
|
api_completed = True
|
|
print(" ✅ Modal closing detected (success)")
|
|
break
|
|
except:
|
|
pass
|
|
|
|
# Check for success toast message
|
|
try:
|
|
# Look for success toast: "Password reset successfully!"
|
|
toast_selectors = [
|
|
"//div[@role='status' and contains(text(), 'Password reset successfully')]",
|
|
"//div[@role='status' and contains(text(), 'reset successfully')]",
|
|
"//div[contains(@class, 'toast') and contains(text(), 'successfully')]",
|
|
]
|
|
for selector in toast_selectors:
|
|
try:
|
|
toast = self.driver.find_element(By.XPATH, selector)
|
|
if toast.is_displayed():
|
|
api_completed = True
|
|
print(f" ✅ Success toast detected: {toast.text[:50]}...")
|
|
break
|
|
except:
|
|
continue
|
|
if api_completed:
|
|
break
|
|
except:
|
|
pass
|
|
|
|
# Check for errors after submit
|
|
if self.has_errors():
|
|
error_text = []
|
|
try:
|
|
if self.is_element_visible(self.CURRENT_PASSWORD_ERROR, timeout=1):
|
|
error_text.append(f"Current password: {self.find_element(self.CURRENT_PASSWORD_ERROR).text}")
|
|
except:
|
|
pass
|
|
try:
|
|
if self.is_element_visible(self.NEW_PASSWORD_ERROR, timeout=1):
|
|
error_text.append(f"New password: {self.find_element(self.NEW_PASSWORD_ERROR).text}")
|
|
except:
|
|
pass
|
|
if error_text:
|
|
error_msg = '; '.join(error_text)
|
|
raise Exception(f"❌ Password reset failed with errors: {error_msg}")
|
|
|
|
time.sleep(0.5) # Check every 0.5 seconds
|
|
|
|
if not api_completed:
|
|
raise Exception("❌ Password reset API call did not complete within timeout")
|
|
|
|
# STEP 8: Wait for modal to fully close
|
|
print("⏳ Waiting for modal to close...")
|
|
try:
|
|
WebDriverWait(self.driver, MEDIUM_WAIT).until(
|
|
EC.invisibility_of_element_located(self.MODAL)
|
|
)
|
|
print(" ✅ Modal closed successfully")
|
|
except:
|
|
# Check if modal is still present
|
|
if self.is_modal_present():
|
|
raise Exception("❌ Password reset modal did not close after successful reset")
|
|
print(" ✅ Modal closed (verified)")
|
|
|
|
# STEP 9: Update password tracker on success
|
|
if student_cpid:
|
|
try:
|
|
from utils.password_tracker import password_tracker
|
|
password_tracker.update_password(student_cpid, new_password)
|
|
print(f" ✅ Password tracker updated for {student_cpid}")
|
|
except Exception as e:
|
|
print(f" ⚠️ Warning: Failed to update password tracker: {e}")
|
|
|
|
# STEP 10: Final verification - ensure we're not on login page (should be on dashboard)
|
|
time.sleep(1) # Brief wait for navigation
|
|
current_url = self.driver.current_url
|
|
if "/login" in current_url:
|
|
raise Exception(f"❌ Unexpected redirect to login page after password reset. URL: {current_url}")
|
|
|
|
print("✅ Password reset completed successfully!")
|
|
return True
|
|
|
|
def click_back(self):
|
|
"""Click back button"""
|
|
self.click_element(self.BACK_BUTTON)
|
|
|
|
def has_errors(self):
|
|
"""
|
|
Check if there are any validation errors
|
|
|
|
Returns:
|
|
bool: True if any error is visible
|
|
"""
|
|
try:
|
|
return (self.is_element_visible(self.CURRENT_PASSWORD_ERROR, timeout=1) or
|
|
self.is_element_visible(self.NEW_PASSWORD_ERROR, timeout=1) or
|
|
self.is_element_visible(self.CONFIRM_PASSWORD_ERROR, timeout=1))
|
|
except:
|
|
return False
|
|
|