Files

751 lines
24 KiB
Python

import json
import os
from datetime import datetime
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import NoAlertPresentException, TimeoutException, StaleElementReferenceException
from .locators import Locator
from .config_reader import (
get_login_captcha_wait_seconds,
get_login_post_submit_wait_seconds,
get_step_delay_seconds,
)
import time
import msvcrt
STEP_DELAY_SECONDS = get_step_delay_seconds()
LOGIN_CAPTCHA_WAIT_SECONDS = get_login_captcha_wait_seconds()
LOGIN_POST_SUBMIT_WAIT_SECONDS = get_login_post_submit_wait_seconds()
def pause_after_step(seconds=STEP_DELAY_SECONDS):
time.sleep(seconds)
def wait_for_manual_input(prompt="Press Enter in the terminal when you want to finish this run..."):
print(prompt)
try:
input()
except EOFError as exc:
raise RuntimeError(
"Manual input is required for this test. Run pytest with -s so Enter can be captured."
) from exc
def clear_pending_console_input():
while msvcrt.kbhit():
msvcrt.getwch()
def enter_pressed():
if not msvcrt.kbhit():
return False
pressed = False
while msvcrt.kbhit():
key = msvcrt.getwch()
if key in ("\r", "\n"):
pressed = True
return pressed
def check_captcha_error(driver) -> str:
"""Check if there's a CAPTCHA error message on the page."""
try:
# First handle any alerts
try:
alert = driver.switch_to.alert
alert_text = alert.text
print(f" DEBUG: Alert detected in check_captcha_error: {alert_text}")
if "logout" in alert_text.lower() or "session" in alert_text.lower() or "previous" in alert_text.lower():
alert.accept()
print(" DEBUG: Accepted session/logout alert")
time.sleep(2)
except:
pass
error_selectors = [
"//div[@id='error']//strong[normalize-space()='Invalid Captcha']",
"//div[@id='error']//span[contains(@class,'help-block')]//strong[contains(text(),'Invalid')]",
"//*[contains(@class,'error') and contains(text(),'Invalid Captcha')]",
]
for selector in error_selectors:
elements = driver.find_elements(By.XPATH, selector)
for el in elements:
if el.is_displayed() and el.text.strip():
return el.text.strip()
except Exception as e:
print(f" DEBUG: check_captcha_error exception: {e}")
return ""
def check_login_success(driver) -> bool:
"""Check if login was successful using multiple indicators."""
try:
# Handle any pending alerts first
try:
alert = driver.switch_to.alert
alert_text = alert.text
print(f" DEBUG: Alert detected: {alert_text}")
if "logout" in alert_text.lower() or "session" in alert_text.lower() or "previous" in alert_text.lower():
alert.accept()
print(" DEBUG: Accepted session/logout alert")
time.sleep(2)
except:
pass
# Wait a bit for page to load after submit
time.sleep(3)
# Print current URL and page title for debugging
print(f" DEBUG: Current URL: {driver.current_url}")
print(f" DEBUG: Page title: {driver.title}")
# Multiple dashboard indicators
dashboard_indicators = [
"//span[normalize-space()='TRADING MAINTENANCE']",
"//a[contains(@href, 'tradingMaintenance')]",
"//span[contains(text(), 'Dashboard')]",
"//h1[contains(text(), 'Dashboard')]",
"//div[contains(@class, 'dashboard')]",
"//a[contains(text(), 'Trading')]",
"//div[contains(@class, 'navbar')]//span[contains(text(), 'Trading')]",
]
wait = WebDriverWait(driver, 15)
for indicator in dashboard_indicators:
try:
wait.until(EC.visibility_of_element_located((By.XPATH, indicator)))
print(f" DEBUG: Found dashboard indicator: {indicator}")
return True
except:
continue
# URL-based check
if "home" in driver.current_url.lower() or "dashboard" in driver.current_url.lower():
print(" DEBUG: Login success detected via URL")
return True
return False
except Exception as e:
print(f" DEBUG: check_login_success failed: {e}")
return False
def login(
driver,
username,
password,
max_captcha_retries=5,
captcha_metrics=None,
max_login_duration_seconds=1200,
):
wait = WebDriverWait(driver, 20)
# --- AUTOMATED CAPTCHA SOLVING WITH RETRY ON FAILURE ---
from tests.captcha_solver import solve_and_fill_captcha, find_refresh_button, wait_for_new_captcha
login_deadline = time.monotonic() + max_login_duration_seconds
captcha_attempt = 0
while True:
captcha_attempt += 1
# Refreshing after a failed CAPTCHA clears the login form, so refill
# both credentials before every automatic or manual retry.
userid_field = wait.until(
EC.visibility_of_element_located((By.XPATH, Locator.userid))
)
userid_field.clear()
userid_field.send_keys(username)
password_field = wait.until(
EC.visibility_of_element_located((By.XPATH, Locator.password))
)
password_field.clear()
password_field.send_keys(password)
if captcha_metrics is not None:
captcha_metrics["tries"] = captcha_attempt
captcha_metrics["max_retries"] = max_captcha_retries
print(f"Solving CAPTCHA (attempt {captcha_attempt})...")
# Once automatic solving has run for the configured duration, ask for
# the CAPTCHA value in a browser prompt and continue the same login flow.
if time.monotonic() >= login_deadline:
print("Automatic CAPTCHA retries timed out; waiting for manual CAPTCHA entry.")
manual_value = driver.execute_script(
"return window.prompt('Automatic CAPTCHA solving timed out. Enter the CAPTCHA shown on the page:');"
)
if not manual_value:
raise RuntimeError("Manual CAPTCHA entry was cancelled")
captcha_input = wait.until(
EC.visibility_of_element_located((By.NAME, "captcha"))
)
captcha_input.clear()
captcha_input.send_keys(manual_value.strip())
wait.until(EC.element_to_be_clickable((By.XPATH, Locator.submitbtn))).click()
time.sleep(LOGIN_POST_SUBMIT_WAIT_SECONDS)
accept_browser_alert_if_present(driver)
handle_transient_popups(driver)
captcha_error = check_captcha_error(driver)
if captcha_error:
print(f" Manual CAPTCHA rejected: {captcha_error}")
continue
if check_login_success(driver):
print("Login successful after manual CAPTCHA entry")
if captcha_metrics is not None:
captcha_metrics["correct_attempt"] = captcha_attempt
captcha_metrics.setdefault("captcha_attempts", []).append({
"attempt": captcha_attempt,
"captcha_text": "manual",
"variant": manual_value.strip(),
"status": "manual",
"screenshot": "",
"error": "",
})
close_tac_expiry_modal_if_present(driver)
return True
continue
def attempt_callback(attempt_num, captcha_text, variant, status, screenshot_path="", error=""):
if captcha_metrics is not None:
captcha_metrics.setdefault("captcha_attempts", []).append({
"attempt": attempt_num,
"captcha_text": captcha_text,
"variant": variant,
"status": status,
"screenshot": screenshot_path,
"error": error
})
if not solve_and_fill_captcha(driver, max_retries=1, attempt_callback=attempt_callback):
print(" Automatic CAPTCHA solve failed; retrying until the login deadline.")
driver.refresh()
time.sleep(3)
continue
# solve_and_fill_captcha already clicks submit, so just wait and check result
time.sleep(LOGIN_POST_SUBMIT_WAIT_SECONDS)
accept_browser_alert_if_present(driver)
# Handle any popups that appear
handle_transient_popups(driver)
# Check for CAPTCHA error (invalid captcha message)
captcha_error = check_captcha_error(driver)
if captcha_error:
print(f" CAPTCHA validation failed: {captcha_error}")
# Reload the page to get a fresh CAPTCHA and keep retrying until
# the overall login deadline is reached.
print(f" Reloading page to get fresh CAPTCHA (attempt {captcha_attempt + 1})...")
driver.refresh()
time.sleep(3)
continue
# Check for successful login - use improved detection
if check_login_success(driver):
print("Login successful")
if captcha_metrics is not None:
captcha_metrics["correct_attempt"] = captcha_attempt
close_tac_expiry_modal_if_present(driver)
return True
# If we get here, something else is wrong
print(" Login not confirmed, retrying...")
driver.refresh()
time.sleep(3)
def accept_browser_alert_if_present(driver, timeout=5):
try:
alert = WebDriverWait(driver, timeout).until(EC.alert_is_present())
print(f"Browser alert accepted: {alert.text}")
alert.accept()
return True
except (TimeoutException, NoAlertPresentException):
return False
def close_tac_expiry_modal_if_present(driver, timeout=5):
try:
close_button = WebDriverWait(driver, timeout).until(
EC.element_to_be_clickable((By.XPATH, Locator.tacexpiryclosebtn))
)
except TimeoutException:
return False
try:
close_button.click()
except Exception:
driver.execute_script("arguments[0].click();", close_button)
print("Closed TAC Expiry modal")
return True
def handle_transient_popups(driver):
accepted_alert = accept_browser_alert_if_present(driver, timeout=2)
closed_modal = close_tac_expiry_modal_if_present(driver, timeout=2)
return accepted_alert or closed_modal
def backup_current_session_token(driver):
session_backup = {
"current_url": driver.current_url,
"cookies": driver.get_cookies(),
}
print(f"Backed up current browser session with {len(session_backup['cookies'])} cookies")
return session_backup
def save_session_backup(driver, test_context, name="session_backup"):
backup = backup_current_session_token(driver)
backup["created_at"] = datetime.now().isoformat(timespec="seconds")
path = os.path.join(test_context["folder"], f"{name}.json")
with open(path, "w", encoding="utf-8") as backup_file:
json.dump(backup, backup_file, indent=2)
test_context.setdefault("extra_folders", [])
print(f"Session backup saved: {path}")
return backup
def logout(driver):
wait = WebDriverWait(driver, 20)
handle_transient_popups(driver)
try:
user_menu = wait.until(
EC.element_to_be_clickable((By.XPATH, Locator.usermenutoggle))
)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", user_menu)
try:
user_menu.click()
except Exception:
driver.execute_script("arguments[0].click();", user_menu)
wait.until(
EC.visibility_of_element_located(
(
By.XPATH,
"//li[contains(@class,'dropdown') and contains(@class,'navbar-user') and contains(@class,'open')]"
)
)
)
logout_link = wait.until(
EC.element_to_be_clickable((By.XPATH, Locator.logoutlink))
)
try:
logout_link.click()
except Exception:
driver.execute_script("arguments[0].click();", logout_link)
accept_browser_alert_if_present(driver, timeout=3)
wait.until(
EC.visibility_of_element_located((By.XPATH, Locator.userid))
)
print("Logout successful")
return True
except TimeoutException:
print("Logout link was not found; browser will still close from fixture teardown")
return False
def select_ui_dropdown(driver, dropdown_label, value):
wait = WebDriverWait(driver, 20)
# 1️⃣ Open dropdown
dropdown = wait.until(
EC.element_to_be_clickable(
(By.XPATH, f"//span[@aria-label='{dropdown_label} activate']")
)
)
dropdown.click()
# 2️⃣ Find search box
search_box = wait.until(
EC.visibility_of_element_located((By.CSS_SELECTOR, "input.ui-select-search"))
)
# 3️⃣ Type product name
search_box.clear()
search_box.send_keys(value)
# 4️⃣ Wait for dropdown options to load
option_xpath = f"//div[contains(@class,'ui-select-choices-row')]//span[contains(text(),'{value}')]"
option = wait.until(
EC.element_to_be_clickable((By.XPATH, option_xpath))
)
# 5️⃣ Click option
option.click()
print(f"{value} selected successfully")
def select_labeled_ui_dropdown(driver, field_label, value):
wait = WebDriverWait(driver, 20)
dropdown_xpath = (
f"//label[normalize-space()='{field_label}']"
"/following::span[contains(@class,'ui-select-toggle')][1]"
)
dropdown = wait.until(
EC.element_to_be_clickable(
(
By.XPATH,
dropdown_xpath
)
)
)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", dropdown)
try:
dropdown.click()
except Exception:
driver.execute_script("arguments[0].click();", dropdown)
search_xpath = (
f"//label[normalize-space()='{field_label}']"
"/following::input[contains(@class,'ui-select-search')][1]"
)
search_box = wait.until(
EC.visibility_of_element_located(
(By.XPATH, search_xpath)
)
)
search_box.click()
search_box.send_keys(Keys.CONTROL, "a")
search_box.clear()
search_box.send_keys(value)
wait.until(
EC.presence_of_element_located(
(By.XPATH, "//div[contains(@class,'ui-select-choices-row')]")
)
)
def _visible_choice_rows():
rows = driver.find_elements(By.XPATH, "//div[contains(@class,'ui-select-choices-row')]")
return [row for row in rows if row.is_displayed()]
option = None
visible_rows = _visible_choice_rows()
for row in visible_rows:
if row.text.strip() == value:
option = row
break
if option is None:
available_options = [row.text.strip() for row in visible_rows if row.text.strip()]
raise AssertionError(
f"Exact option '{value}' was not available in {field_label}. "
f"Visible options: {available_options}"
)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", option)
try:
option.click()
except Exception:
driver.execute_script("arguments[0].click();", option)
print(f"{value} selected successfully from {field_label}")
def open_trading_maintenance_page(driver, menu_xpath):
wait = WebDriverWait(driver, 20)
def visible_elements(xpath):
return [element for element in driver.find_elements(By.XPATH, xpath) if element.is_displayed()]
submenu_visible = any(
item.is_enabled() for item in visible_elements(menu_xpath)
)
if not submenu_visible:
maintenance_items = visible_elements(Locator.tradingmaintenance)
if not maintenance_items:
wait.until(lambda current_driver: visible_elements(Locator.tradingmaintenance))
maintenance_items = visible_elements(Locator.tradingmaintenance)
maintenance = maintenance_items[0]
driver.execute_script(
"arguments[0].scrollIntoView({block: 'center'});", maintenance
)
try:
wait.until(lambda current_driver: maintenance.is_enabled())
maintenance.click()
except Exception:
driver.execute_script("arguments[0].click();", maintenance)
menu_item = wait.until(
lambda current_driver: next(
(item for item in visible_elements(menu_xpath) if item.is_enabled()),
False,
)
)
try:
menu_item.click()
except Exception:
driver.execute_script("arguments[0].click();", menu_item)
def click_visible_menu_item(driver, menu_xpath):
wait = WebDriverWait(driver, 20)
menu_items = driver.find_elements(By.XPATH, menu_xpath)
for menu_item in menu_items:
if menu_item.is_displayed():
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", menu_item)
try:
menu_item.click()
except Exception:
driver.execute_script("arguments[0].click();", menu_item)
return
menu_item = wait.until(
EC.element_to_be_clickable((By.XPATH, menu_xpath))
)
try:
menu_item.click()
except Exception:
driver.execute_script("arguments[0].click();", menu_item)
def type_into_field(driver, locator, value):
wait = WebDriverWait(driver, 20)
field = wait.until(
EC.visibility_of_element_located(locator)
)
field.clear()
field.send_keys(value)
def select_bootstrap_dropdown_by_id(driver, element_id, value, exact=False):
wait = WebDriverWait(driver, 20)
container = wait.until(
EC.presence_of_element_located((By.ID, element_id))
)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", container)
toggle_xpath = (
f"//div[@id='{element_id}']//span[contains(@class,'ui-select-toggle')]"
f"|//div[@id='{element_id}']//span[contains(@class,'btn-default')]"
f"|//div[@id='{element_id}']//span[contains(@class,'ui-select-placeholder')]"
f"|//div[@id='{element_id}']//input[contains(@class,'ui-select-search')]"
)
toggle = wait.until(
EC.presence_of_element_located((By.XPATH, toggle_xpath))
)
try:
wait.until(EC.element_to_be_clickable((By.XPATH, toggle_xpath)))
toggle.click()
except Exception:
driver.execute_script("arguments[0].click();", toggle)
try:
search_box = wait.until(
EC.visibility_of_element_located(
(By.XPATH, f"//div[@id='{element_id}']//input[contains(@class,'ui-select-search')]")
)
)
except TimeoutException:
search_box = wait.until(
EC.visibility_of_element_located(
(By.XPATH, "//input[contains(@class,'ui-select-search') and not(@aria-hidden='true')]")
)
)
search_box.click()
search_box.send_keys(Keys.CONTROL, "a")
search_box.clear()
search_box.send_keys(value)
option_xpath = (
f"//div[@id='{element_id}']//div[contains(@class,'ui-select-choices-row')]"
f"[normalize-space()='{value}']"
if exact else
"//div[contains(@class,'ui-select-choices-row')]"
f"//*[normalize-space()='{value}' or starts-with(normalize-space(),'{value}')]"
)
option = wait.until(
EC.element_to_be_clickable(
(
By.XPATH,
option_xpath
)
)
)
option.click()
print(f"{value} selected successfully from {element_id}")
def click_grid_button_for_row_text(driver, row_text, button_text):
wait = WebDriverWait(driver, 20)
button_xpath = (
"//div[contains(@class,'ui-grid-row')]"
f"[contains(.,'{row_text}')]"
f"//button[normalize-space()='{button_text}']"
)
last_error = None
for _ in range(3):
try:
button = wait.until(
EC.presence_of_element_located((By.XPATH, button_xpath))
)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", button)
wait.until(EC.element_to_be_clickable((By.XPATH, button_xpath)))
refreshed_button = driver.find_element(By.XPATH, button_xpath)
try:
refreshed_button.click()
except Exception:
driver.execute_script("arguments[0].click();", refreshed_button)
print(f"Clicked {button_text} for row containing {row_text}")
return
except StaleElementReferenceException as exc:
last_error = exc
time.sleep(1)
if last_error:
raise last_error
def click_first_grid_button(driver, button_text):
wait = WebDriverWait(driver, 20)
button_xpath = f"(//button[normalize-space()='{button_text}' and not(contains(@class,'ng-hide'))])[1]"
last_error = None
for _ in range(3):
try:
button = wait.until(
EC.presence_of_element_located((By.XPATH, button_xpath))
)
driver.execute_script("arguments[0].scrollIntoView({block: 'start'});", button)
refreshed_button = driver.find_element(By.XPATH, button_xpath)
try:
refreshed_button.click()
except Exception:
driver.execute_script("arguments[0].click();", refreshed_button)
print(f"Clicked first visible {button_text}")
return
except StaleElementReferenceException as exc:
last_error = exc
time.sleep(1)
if last_error:
raise last_error
def type_first_visible_input(driver, xpath, value):
wait = WebDriverWait(driver, 20)
def _find_visible_input(_driver):
elements = _driver.find_elements(By.XPATH, xpath)
for element in elements:
if element.is_displayed() and element.is_enabled():
return element
return False
element = wait.until(_find_visible_input)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)
element.click()
element.clear()
element.send_keys(value)
def toggle_bootstrap_dropdown_by_id(driver, element_id):
wait = WebDriverWait(driver, 20)
toggle_xpath = (
f"//div[@id='{element_id}']//span[contains(@class,'ui-select-toggle')]"
f"|//div[@id='{element_id}']//span[contains(@class,'btn-default')]"
f"|//div[@id='{element_id}']//span[contains(@class,'ui-select-placeholder')]"
f"|//div[@id='{element_id}']//input[contains(@class,'ui-select-search')]"
)
toggle = wait.until(
EC.presence_of_element_located((By.XPATH, toggle_xpath))
)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", toggle)
try:
wait.until(EC.element_to_be_clickable((By.XPATH, toggle_xpath)))
toggle.click()
except Exception:
driver.execute_script("arguments[0].click();", toggle)
def click_page_body(driver):
body = WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.TAG_NAME, "body"))
)
body.click()
def toggle_labeled_ui_dropdown(driver, field_label):
wait = WebDriverWait(driver, 20)
dropdown_xpath = (
f"//label[normalize-space()='{field_label}']"
"/following::span[contains(@class,'ui-select-toggle')][1]"
)
dropdown = wait.until(
EC.element_to_be_clickable((By.XPATH, dropdown_xpath))
)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", dropdown)
try:
dropdown.click()
except Exception:
driver.execute_script("arguments[0].click();", dropdown)