Initial commit - Selenium automation framework

This commit is contained in:
2026-09-08 09:24:02 +05:30
commit e84b7e9950
76 changed files with 7484 additions and 0 deletions
@@ -0,0 +1 @@
@@ -0,0 +1,400 @@
import json
from pathlib import Path
import time
from datetime import datetime
from selenium.common.exceptions import NoAlertPresentException, TimeoutException
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from tests.regression.ui_helpers import select_select2_value_by_container_id
def prepare_block_bulk_upload_page(driver, config, entry, upload_method):
product = config["product"]
bidding_config = config["exchange_bidding"]
block_config = bidding_config.get("block_bid", {})
_click_tab(driver, upload_method)
_click_block_bid_tab(driver)
_select_product_type(driver, product["product_type"])
_select_first_available_select2_value(
driver,
block_config.get("product_container_id", "s2id_ddlproduct3"),
[
block_config.get("product_code"),
product["product_code"],
],
)
_select_first_available_select2_value(
driver,
block_config.get("segment_container_id", "s2id_ddlSegment3"),
[product["trading_market"]],
)
if config["selected_role"] != "trading_member":
_select_first_available_select2_value(
driver,
block_config.get("tm_container_id", "s2id_ddlTMID3"),
[entry["tm_id"]],
)
def upload_block_bulk_file(
driver,
file_path,
success_timeout_seconds=1800,
poll_seconds=2,
progress_log_interval_seconds=60,
progress_log_label="block_file_upload",
):
upload_path = Path(file_path)
if not upload_path.exists() or not upload_path.is_file():
raise FileNotFoundError(f"Block bid upload file not found: {upload_path}")
file_input = _find_block_upload_input(driver)
_prepare_file_input(driver, file_input)
selected_at = datetime.now()
selected_monotonic = time.monotonic()
file_input.send_keys(str(upload_path.resolve()))
_accept_browser_upload_confirmation_if_present(driver)
success = wait_for_block_upload_success(
driver,
timeout_seconds=success_timeout_seconds,
poll_seconds=poll_seconds,
progress_log_interval_seconds=progress_log_interval_seconds,
progress_log_label=progress_log_label,
)
return _build_upload_timing(selected_at, selected_monotonic, success, [upload_path])
def upload_block_bulk_folder(
driver,
folder_path,
success_timeout_seconds=1800,
poll_seconds=2,
progress_log_interval_seconds=60,
progress_log_label="block_folder_upload",
):
upload_path = Path(folder_path)
if not upload_path.exists() or not upload_path.is_dir():
raise FileNotFoundError(f"Block bid upload folder not found: {upload_path}")
upload_files = [path for path in upload_path.rglob("*") if path.is_file()]
file_input = _find_block_upload_input(driver)
_prepare_file_input(driver, file_input)
selected_at = datetime.now()
selected_monotonic = time.monotonic()
file_input.send_keys(str(upload_path.resolve()))
_accept_browser_upload_confirmation_if_present(driver)
success = wait_for_block_upload_success(
driver,
timeout_seconds=success_timeout_seconds,
poll_seconds=poll_seconds,
progress_log_interval_seconds=progress_log_interval_seconds,
progress_log_label=progress_log_label,
)
return _build_upload_timing(selected_at, selected_monotonic, success, upload_files)
def wait_for_block_upload_success(
driver,
timeout_seconds=1800,
poll_seconds=2,
progress_log_interval_seconds=60,
progress_log_label="block_upload",
):
end_time = time.monotonic() + timeout_seconds
next_progress_log = time.monotonic() + progress_log_interval_seconds
last_message = ""
while time.monotonic() < end_time:
message = _current_upload_message(driver)
if message:
last_message = message
if _is_success_message(message):
return {
"message": message,
"displayed_at": datetime.now().isoformat(timespec="seconds"),
}
if progress_log_interval_seconds and time.monotonic() >= next_progress_log:
print(
f"Block upload progress [{progress_log_label}]: "
f"waiting for success message. Last message: {last_message or 'none'}"
)
next_progress_log = time.monotonic() + progress_log_interval_seconds
time.sleep(poll_seconds)
raise TimeoutException(
"Block upload success message did not appear within the timeout. "
f"Last visible message: {last_message or 'No status message found'}"
)
def save_block_bulk_upload_log(test_context, market, entry, upload_method, timing):
folder = Path(test_context.get("log_folder", test_context["folder"]))
folder.mkdir(parents=True, exist_ok=True)
side_name = entry.get("side", "")
log_name_parts = ["block_bulk_upload", upload_method, market]
if side_name:
log_name_parts.append(side_name)
log_name_parts.append(entry["tm_id"])
path = folder / f"{'_'.join(log_name_parts)}.json"
payload = {
"market": market,
"side": side_name,
"tm_id": entry["tm_id"],
"folder_name": entry.get("folder_name", ""),
"folder_path": entry.get("folder_path", ""),
"file_name": entry.get("file_name", ""),
"file_path": entry.get("upload_path", ""),
"upload_method": upload_method,
"timing": timing,
}
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
return str(path)
def _click_tab(driver, upload_method):
tab_href = "#Folder" if upload_method == "folder_upload" else "#File"
_click_by_xpath(
driver,
f"//a[@href='{tab_href}' or normalize-space()='{tab_href.lstrip('#')}']"
)
def _click_block_bid_tab(driver):
if _block_bid_fields_are_visible(driver):
return
_click_visible_by_xpath(
driver,
"//a[contains(normalize-space(),'Block Bid')]"
"|//span[contains(normalize-space(),'Block Bid')]/ancestor::a[1]"
"|//*[self::button][contains(normalize-space(),'Block Bid')]"
)
WebDriverWait(driver, 20).until(lambda _driver: _block_bid_fields_are_visible(_driver))
def _select_product_type(driver, product_type):
wait = WebDriverWait(driver, 20)
candidates = driver.find_elements(
By.XPATH,
"//select[contains(@id,'ProductType') or contains(@name,'ProductType')]"
"|//select[option[normalize-space()='IDAS']]"
)
for candidate in candidates:
if not candidate.is_displayed():
continue
try:
from selenium.webdriver.support.select import Select
Select(candidate).select_by_visible_text(product_type)
return
except Exception:
continue
wait.until(lambda _driver: candidates or True)
def _select_first_available_select2_value(driver, container_id, values):
last_error = None
for value in values:
if not value:
continue
try:
select_select2_value_by_container_id(
driver,
container_id,
value,
exact=True
)
return value
except TimeoutException as exc:
last_error = exc
if last_error:
raise last_error
raise ValueError(f"No values configured for select2 container '{container_id}'.")
def _find_block_upload_input(driver):
wait = WebDriverWait(driver, 30)
return wait.until(
lambda _driver: _first_displayed_or_existing(
_driver.find_elements(
By.XPATH,
"//input[@type='file' and ("
"@id='filepicker3' or contains(@id,'filepicker') or "
"contains(@id,'file') or contains(@name,'file') or @webkitdirectory"
")]"
)
) or False,
"No block bid upload input was found."
)
def _first_displayed_or_existing(elements):
for element in elements:
if element.is_displayed() and element.is_enabled():
return element
if elements:
return elements[0]
return None
def _prepare_file_input(driver, file_input):
driver.execute_script(
"""
arguments[0].style.display = 'block';
arguments[0].style.visibility = 'visible';
arguments[0].style.opacity = 1;
arguments[0].style.height = '20px';
arguments[0].style.width = '120px';
arguments[0].removeAttribute('disabled');
arguments[0].value = '';
""",
file_input
)
def _click_by_xpath(driver, xpath):
wait = WebDriverWait(driver, 20)
element = wait.until(EC.element_to_be_clickable((By.XPATH, xpath)))
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)
try:
element.click()
except Exception:
driver.execute_script("arguments[0].click();", element)
def _click_visible_by_xpath(driver, xpath):
wait = WebDriverWait(driver, 20)
element = wait.until(lambda _driver: _first_clickable(_driver, xpath) or False)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)
try:
element.click()
except Exception:
driver.execute_script("arguments[0].click();", element)
def _first_clickable(driver, xpath):
for element in driver.find_elements(By.XPATH, xpath):
try:
if element.is_displayed() and element.is_enabled():
return element
except Exception:
continue
return None
def _block_bid_fields_are_visible(driver):
product_fields = driver.find_elements(By.ID, "s2id_ddlproduct3")
segment_fields = driver.find_elements(By.ID, "s2id_ddlSegment3")
upload_inputs = driver.find_elements(By.ID, "filepicker3")
return any(_is_visible(element) for element in product_fields) and (
any(_is_visible(element) for element in segment_fields)
or any(_is_visible(element) for element in upload_inputs)
)
def _is_visible(element):
try:
return element.is_displayed()
except Exception:
return False
def _accept_browser_upload_confirmation_if_present(driver):
try:
alert = driver.switch_to.alert
alert.accept()
return
except NoAlertPresentException:
pass
try:
ActionChains(driver).send_keys("\ue007").perform()
except Exception:
pass
def _current_upload_message(driver):
candidates = []
for locator in (
(By.ID, "successMsg"),
(By.ID, "divSuccess"),
(By.ID, "divInfo"),
(By.ID, "divError"),
):
candidates.extend(driver.find_elements(*locator))
candidates.extend(
driver.find_elements(
By.XPATH,
"//*[contains(@class,'alert') and not(contains(@style,'display: none'))]"
)
)
for candidate in candidates:
try:
if candidate.is_displayed():
text = candidate.text.strip()
if text:
return text
except Exception:
continue
return ""
def _is_success_message(message):
normalized_message = " ".join(str(message or "").split()).lower()
return (
"folder_uploaded_successfully" in normalized_message
or "folder uploaded successfully" in normalized_message
or "file_uploaded_successfully" in normalized_message
or "file uploaded successfully" in normalized_message
)
def _build_upload_timing(selected_at, selected_monotonic, success, upload_files):
success_monotonic = time.monotonic()
return {
"files_selected_at": selected_at.isoformat(timespec="seconds"),
"success_displayed_at": success["displayed_at"],
"duration_seconds": round(success_monotonic - selected_monotonic, 3),
"success_message": success["message"],
"file_count": len(upload_files),
"files": [str(path) for path in upload_files],
}
@@ -0,0 +1,167 @@
from pathlib import Path
import re
import zipfile
from xml.etree import ElementTree as ET
MAIN_NS = {"a": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
REL_NS = {"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"}
PACKAGE_REL_NS = {"pr": "http://schemas.openxmlformats.org/package/2006/relationships"}
def load_single_bid_market_matrix(excel_path, trading_market, sheet_name="Sheet1", excel_layout=None):
excel_file = Path(excel_path)
if not excel_file.exists():
raise FileNotFoundError(f"Excel input file not found: {excel_file}")
layout = excel_layout or {}
with zipfile.ZipFile(excel_file) as workbook_zip:
shared_strings = _load_shared_strings(workbook_zip)
sheet_xml_path = _resolve_sheet_xml_path(workbook_zip, sheet_name)
sheet_root = ET.fromstring(workbook_zip.read(sheet_xml_path))
cell_map = _load_cell_map(sheet_root, shared_strings)
start_column = layout.get("column_start", "B")
market_price_rows = layout.get("market_price_rows", {})
common_data_start_row = int(layout.get("common_data_start_row", 6))
price_row = int(market_price_rows.get(trading_market, 2))
_, start_col = _split_cell_reference(f"{start_column}1")
max_row, max_col = _max_used_extent(cell_map, price_row, start_col)
matrix = []
for row_index in [price_row, *range(common_data_start_row, max_row + 1)]:
row_values = []
for col_index in range(start_col, max_col + 1):
row_values.append(cell_map.get((row_index, col_index), ""))
matrix.append(row_values)
matrix = _trim_trailing_empty_rows_and_columns(matrix)
if not matrix or not matrix[0]:
raise ValueError(f"No bidding data found in {excel_file} for market {trading_market}.")
tsv = "\n".join("\t".join(value for value in row) for row in matrix)
return {
"matrix": matrix,
"tsv": tsv,
"row_count": len(matrix),
"column_count": len(matrix[0]),
"first_value": matrix[0][0],
}
def _load_shared_strings(workbook_zip):
try:
root = ET.fromstring(workbook_zip.read("xl/sharedStrings.xml"))
except KeyError:
return []
values = []
for item in root.findall("a:si", MAIN_NS):
text_parts = [node.text or "" for node in item.findall(".//a:t", MAIN_NS)]
values.append("".join(text_parts))
return values
def _resolve_sheet_xml_path(workbook_zip, sheet_name):
workbook_root = ET.fromstring(workbook_zip.read("xl/workbook.xml"))
relationships_root = ET.fromstring(workbook_zip.read("xl/_rels/workbook.xml.rels"))
relationship_map = {
rel.attrib["Id"]: rel.attrib["Target"]
for rel in relationships_root.findall("pr:Relationship", PACKAGE_REL_NS)
}
for sheet in workbook_root.findall("a:sheets/a:sheet", {**MAIN_NS, **REL_NS}):
if sheet.attrib.get("name") != sheet_name:
continue
relationship_id = sheet.attrib.get(f"{{{REL_NS['r']}}}id")
target = relationship_map[relationship_id]
target = target.lstrip("/")
if not target.startswith("xl/"):
target = f"xl/{target}"
return target
raise KeyError(f"Sheet '{sheet_name}' not found in workbook.")
def _load_cell_map(sheet_root, shared_strings):
cell_map = {}
for row in sheet_root.findall(".//a:sheetData/a:row", MAIN_NS):
for cell in row.findall("a:c", MAIN_NS):
reference = cell.attrib.get("r", "")
row_index, col_index = _split_cell_reference(reference)
cell_map[(row_index, col_index)] = _cell_value(cell, shared_strings)
return cell_map
def _cell_value(cell, shared_strings):
cell_type = cell.attrib.get("t")
if cell_type == "inlineStr":
text_nodes = cell.findall(".//a:t", MAIN_NS)
return "".join(node.text or "" for node in text_nodes)
value_node = cell.find("a:v", MAIN_NS)
if value_node is None or value_node.text is None:
return ""
raw_value = value_node.text
if cell_type == "s":
return shared_strings[int(raw_value)]
return raw_value
def _split_cell_reference(reference):
match = re.match(r"([A-Z]+)(\d+)", reference)
if not match:
raise ValueError(f"Unsupported cell reference: {reference}")
column_letters, row_number = match.groups()
return int(row_number), _column_letters_to_number(column_letters)
def _column_letters_to_number(column_letters):
result = 0
for character in column_letters:
result = result * 26 + (ord(character) - ord("A") + 1)
return result
def _max_used_extent(cell_map, start_row, start_col):
used_rows = [row for row, col in cell_map if row >= start_row and col >= start_col and cell_map[(row, col)] != ""]
used_cols = [col for row, col in cell_map if row >= start_row and col >= start_col and cell_map[(row, col)] != ""]
if not used_rows or not used_cols:
raise ValueError("No populated cells were found in the requested data area.")
return max(used_rows), max(used_cols)
def _trim_trailing_empty_rows_and_columns(matrix):
while matrix and all(value == "" for value in matrix[-1]):
matrix.pop()
if not matrix:
return matrix
while matrix and matrix[0]:
if any(row[-1] != "" for row in matrix):
break
for row in matrix:
row.pop()
return matrix
@@ -0,0 +1,72 @@
import re
from pathlib import Path
TM_ID_PATTERN = re.compile(r"([CS]\d{4})", re.IGNORECASE)
def get_batch_upload_entries(config):
bidding_config = config["exchange_bidding"]
upload_root = Path(bidding_config["upload_input_root"])
manifest_path = upload_root / "upload_manifest.json"
if manifest_path.exists():
return _load_manifest_entries(manifest_path)
return _load_entries_from_filenames(upload_root)
def _load_manifest_entries(manifest_path):
import json
with manifest_path.open("r", encoding="utf-8") as file:
payload = json.load(file)
entries = []
for item in payload.get("entries", []):
entries.append(
{
"tm_id": item["tm_id"],
"tac_id": item["tac_id"],
"upload_path": str((manifest_path.parent / item["file_name"]).resolve()),
"file_name": item["file_name"],
}
)
return entries
def _load_entries_from_filenames(upload_root):
entries = []
for path in sorted(upload_root.iterdir()):
if not path.is_file() or path.name.startswith("."):
continue
tac_id = path.stem.upper()
tm_id = _derive_tm_id_from_tac(tac_id)
entries.append(
{
"tm_id": tm_id,
"tac_id": tac_id,
"upload_path": str(path.resolve()),
"file_name": path.name,
}
)
if not entries:
raise FileNotFoundError(f"No upload files were found in {upload_root}")
return entries
def _derive_tm_id_from_tac(tac_id):
match = TM_ID_PATTERN.search(tac_id)
if not match:
raise ValueError(
f"Could not derive TM ID from TAC/file name '{tac_id}'. "
"Use a manifest file for custom mapping."
)
return match.group(1).upper()
@@ -0,0 +1,622 @@
import json
import re
import time
from datetime import datetime
from pathlib import Path
from selenium.common.exceptions import NoAlertPresentException, TimeoutException, WebDriverException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
TM_ID_PATTERN = re.compile(r"([CS]\d{4})", re.IGNORECASE)
SCREEN_FOLDER_NAMES = {
"single_bid": "Single",
"block_bid": "Block",
"single_and_block_bid": "Single",
}
BUYSELL_SIDE = "BUYSELL"
BUYSELL_COMPONENT_SIDES = ("Buy", "Sell")
def get_folder_upload_entries(config):
product = config["product"]
bidding_config = config["exchange_bidding"]
upload_root = Path(
bidding_config.get("folder_upload_root")
or bidding_config.get("upload_input_root", "")
)
base_paths = _resolve_folder_upload_base_paths(upload_root, product, bidding_config)
entries = []
for base_path, side_name in base_paths:
for folder_path in sorted(path for path in base_path.iterdir() if path.is_dir()):
if folder_path.name.startswith("."):
continue
entries.append(
{
"folder_path": str(folder_path.resolve()),
"folder_name": folder_path.name,
"side": side_name,
"tm_id": _derive_tm_id_from_folder_name(folder_path.name),
}
)
if not entries:
searched_paths = [str(base_path) for base_path, _side_name in base_paths]
raise FileNotFoundError(f"No TM folders were found in {searched_paths}")
return entries
def upload_folder(driver, folder_path):
folder = Path(folder_path)
if not folder.exists() or not folder.is_dir():
raise FileNotFoundError(f"Upload folder not found: {folder}")
wait = WebDriverWait(driver, 30)
folder_input = wait.until(
lambda _driver: _folder_file_input(_driver) or False,
"No folder upload input was found. Expected an input[type='file'][webkitdirectory]."
)
driver.execute_script(
"""
arguments[0].style.display = 'block';
arguments[0].style.visibility = 'visible';
arguments[0].style.opacity = 1;
arguments[0].style.height = '1px';
arguments[0].style.width = '1px';
arguments[0].removeAttribute('disabled');
arguments[0].value = '';
""",
folder_input
)
folder_input.send_keys(str(folder.resolve()))
_accept_upload_confirmation_if_present(driver)
wait.until(lambda _driver: _folder_file_rows(_driver) or False)
def submit_all_and_wait_for_progress(
driver,
timeout_seconds=300,
poll_seconds=2,
keep_alive_interval_seconds=30,
progress_log_interval_seconds=60,
progress_log_label="folder_upload",
continue_when_last_file_complete=False,
):
wait = WebDriverWait(driver, 30)
submit_all_button = wait.until(
EC.element_to_be_clickable(
(
By.XPATH,
"//table[@id='tblFileList']//button[normalize-space()='Submit All']"
"|//button[normalize-space()='Submit All' or contains(normalize-space(),'Submit All')]"
)
)
)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", submit_all_button)
_click(driver, submit_all_button)
submit_all_clicked_at = datetime.now()
submit_all_monotonic = time.monotonic()
progress_rows = _wait_for_all_progress_100(
driver,
timeout_seconds,
poll_seconds,
keep_alive_interval_seconds,
progress_log_interval_seconds,
progress_log_label,
continue_when_last_file_complete,
)
all_progress_100_at = datetime.now()
incomplete_rows = [
row for row in progress_rows
if row.get("progress", 0) < 100
]
return {
"submit_all_clicked_at": submit_all_clicked_at.isoformat(timespec="seconds"),
"all_progress_100_at": all_progress_100_at.isoformat(timespec="seconds"),
"completion_condition": _completion_condition(progress_rows),
"duration_seconds": round(time.monotonic() - submit_all_monotonic, 3),
"file_count": len(progress_rows),
"incomplete_count": len(incomplete_rows),
"incomplete_files": incomplete_rows,
"files": progress_rows,
}
def fetch_folder_upload_statuses(driver, timeout_seconds=30):
results = []
rows = _folder_file_rows(driver)
for index, row in enumerate(rows, start=1):
file_name = _row_cell_text(row, 1)
remarks = _row_cell_text(row, 2)
progress_text = _row_cell_text(row, 4)
progress = _row_progress_value(row, progress_text)
if progress < 100:
results.append(
{
"index": index,
"file_name": file_name,
"status": "upload_incomplete",
"raw_status": remarks,
"remarks": remarks,
"progress": progress,
"progress_text": progress_text,
"fetch_attempted": False,
}
)
print(
f"Skipping fetch for incomplete upload row: "
f"file={file_name}, progress={progress}%, remarks={remarks}"
)
continue
previous_status = _current_status_text(driver)
fetch_button = _row_fetch_button(row)
if fetch_button is None:
results.append(
{
"index": index,
"file_name": file_name,
"status": "missing_fetch_button",
"raw_status": "",
"remarks": remarks,
"progress": progress,
"progress_text": progress_text,
"fetch_attempted": False,
}
)
continue
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", fetch_button)
_click(driver, fetch_button)
try:
status_text = _wait_for_refreshed_status(driver, previous_status, timeout_seconds)
except TimeoutException:
status_text = _current_status_text(driver)
results.append(
{
"index": index,
"file_name": file_name,
"status": "fetch_timeout",
"raw_status": status_text,
"previous_status": previous_status,
"remarks": remarks,
"progress": progress,
"progress_text": progress_text,
"fetch_attempted": True,
}
)
print(
f"Fetch status timeout for uploaded row: "
f"file={file_name}, progress={progress}%, "
f"previous_status={previous_status}, current_status={status_text}"
)
continue
lowered_status = status_text.lower()
if "reject" in lowered_status:
status = "rejected"
elif "open" in lowered_status or "accept" in lowered_status or "submitted" in lowered_status:
status = "open"
else:
status = "unknown"
results.append(
{
"index": index,
"file_name": file_name,
"status": status,
"raw_status": status_text,
"remarks": remarks,
"progress": progress,
"progress_text": progress_text,
"fetch_attempted": True,
}
)
return results
def save_folder_upload_log(test_context, market, entry, timing, fetch_results):
folder = Path(test_context.get("log_folder", test_context["folder"]))
folder.mkdir(parents=True, exist_ok=True)
side_name = entry.get("side", "")
log_name_parts = ["folder_upload", market]
if side_name:
log_name_parts.append(side_name)
log_name_parts.append(entry["tm_id"])
path = folder / f"{'_'.join(log_name_parts)}.json"
rejected_files = [item for item in fetch_results if item["status"] == "rejected"]
unresolved_files = [
item for item in fetch_results
if item["status"] in {"upload_incomplete", "fetch_timeout", "missing_fetch_button", "unknown"}
]
payload = {
"market": market,
"side": side_name,
"tm_id": entry["tm_id"],
"folder_name": entry["folder_name"],
"folder_path": entry["folder_path"],
"timing": timing,
"fetch_results": fetch_results,
"rejected_files": rejected_files,
"unresolved_files": unresolved_files,
}
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
return str(path)
def _resolve_folder_upload_base_paths(upload_root, product, bidding_config):
if not upload_root:
raise ValueError("Set exchange_bidding.folder_upload_root or upload_input_root in config/runtime.json.")
base_path = upload_root.resolve()
if not base_path.exists():
raise FileNotFoundError(f"Folder upload root does not exist: {base_path}")
market_path = base_path / product["trading_market"]
if not market_path.exists():
available_markets = [
path.name for path in base_path.iterdir()
if path.is_dir() and not path.name.startswith(".")
]
raise FileNotFoundError(
f"Folder upload market path does not exist: {market_path}. "
f"Available market folders under {base_path}: {available_markets}. "
"Set active_selection.trading_market to an available market, "
"or create folders for every market when using ALL."
)
base_path = market_path
screen_folder = SCREEN_FOLDER_NAMES.get(product.get("bidding_screen", "single_bid"))
if screen_folder:
screen_path = base_path / screen_folder
if not screen_path.exists():
raise FileNotFoundError(f"Folder upload screen path does not exist: {screen_path}")
base_path = screen_path
sides = bidding_config.get("folder_upload_sides") or [bidding_config.get("folder_upload_side", "Buy")]
if isinstance(sides, str):
sides = [sides]
expanded_sides = _expand_folder_upload_sides(base_path, sides)
existing_side_paths = [
(base_path / side, side)
for side in expanded_sides
if (base_path / side).exists()
]
if existing_side_paths:
return existing_side_paths
raise FileNotFoundError(
f"Folder upload side path does not exist under {base_path}. "
f"Configured folder_upload_sides: {sides}. Expanded sides: {expanded_sides}"
)
def _expand_folder_upload_sides(base_path, sides):
expanded_sides = []
for side in sides:
side_name = str(side).strip()
if not side_name:
continue
if side_name.upper() != BUYSELL_SIDE:
expanded_sides.append(side_name)
continue
if (base_path / BUYSELL_SIDE).exists():
expanded_sides.append(BUYSELL_SIDE)
continue
expanded_sides.extend(BUYSELL_COMPONENT_SIDES)
return list(dict.fromkeys(expanded_sides))
def _derive_tm_id_from_folder_name(folder_name):
match = TM_ID_PATTERN.search(folder_name)
if not match:
raise ValueError(f"Could not derive TM ID from folder name '{folder_name}'.")
return match.group(1).upper()
def _folder_file_input(driver):
inputs = driver.find_elements(
By.XPATH,
"//input[@type='file' and (@webkitdirectory or @id='fileInput' or @name='fileInput')]"
)
if inputs:
return inputs[0]
return None
def _accept_upload_confirmation_if_present(driver):
try:
alert = driver.switch_to.alert
alert.accept()
except NoAlertPresentException:
pass
def _folder_file_rows(driver):
rows = driver.find_elements(By.XPATH, "//table[@id='tblFileList']//tbody/tr[td]")
return [row for row in rows if row.is_displayed()]
def _wait_for_all_progress_100(
driver,
timeout_seconds,
poll_seconds,
keep_alive_interval_seconds,
progress_log_interval_seconds,
progress_log_label,
continue_when_last_file_complete,
):
end_time = time.monotonic() + timeout_seconds
next_keep_alive = time.monotonic() + keep_alive_interval_seconds
next_progress_log = time.monotonic() + progress_log_interval_seconds
last_rows = []
while time.monotonic() < end_time:
try:
rows = _read_progress_rows(driver)
except WebDriverException as exc:
raise RuntimeError(
"Browser session disconnected while waiting for folder upload progress. "
"This usually means Chrome was closed, crashed, or Windows/Chrome "
"suspended the renderer during the long upload. Re-run with the browser "
"windows kept open and the PC sleep/lock settings disabled for the upload window."
) from exc
if rows:
last_rows = rows
if all(row["progress"] >= 100 for row in rows):
_print_progress_summary(progress_log_label, rows)
return rows
if continue_when_last_file_complete and _last_file_is_complete(rows):
_print_progress_summary(progress_log_label, rows)
_print_incomplete_rows(progress_log_label, rows)
return rows
if keep_alive_interval_seconds and time.monotonic() >= next_keep_alive:
_keep_session_active(driver)
next_keep_alive = time.monotonic() + keep_alive_interval_seconds
if progress_log_interval_seconds and time.monotonic() >= next_progress_log:
_print_progress_summary(progress_log_label, last_rows)
next_progress_log = time.monotonic() + progress_log_interval_seconds
time.sleep(poll_seconds)
raise TimeoutException(
"Folder upload progress did not reach 100% for all files. "
f"Last progress rows: {last_rows}"
)
def _print_progress_summary(label, rows):
if not rows:
print(f"Folder upload progress [{label}]: waiting for progress rows...")
return
completed = sum(1 for row in rows if row["progress"] >= 100)
total = len(rows)
max_progress = max(row["progress"] for row in rows)
min_progress = min(row["progress"] for row in rows)
print(
f"Folder upload progress [{label}]: "
f"{completed}/{total} files at 100%, "
f"min={min_progress}%, max={max_progress}%"
)
def _print_incomplete_rows(label, rows):
incomplete_rows = [
row for row in rows
if row.get("progress", 0) < 100
]
if not incomplete_rows:
return
print(
f"Folder upload progress [{label}]: continuing because last file is 100%. "
"Incomplete/error rows: "
+ "; ".join(
f"{row.get('file_name', '')} progress={row.get('progress', 0)}% "
f"remarks={row.get('remarks', '')}"
for row in incomplete_rows
)
)
def _last_file_is_complete(rows):
return bool(rows) and rows[-1].get("progress", 0) >= 100
def _completion_condition(rows):
if all(row.get("progress", 0) >= 100 for row in rows):
return "all_files_100"
if _last_file_is_complete(rows):
return "last_file_100_with_incomplete_rows"
return "incomplete"
def _read_progress_rows(driver):
rows = []
for row in _folder_file_rows(driver):
file_name = _row_cell_text(row, 1)
remarks = _row_cell_text(row, 2)
progress_text = _row_cell_text(row, 4)
progress = _row_progress_value(row, progress_text)
rows.append(
{
"file_name": file_name,
"remarks": remarks,
"progress": progress,
"progress_text": progress_text,
}
)
return rows
def _keep_session_active(driver):
driver.execute_script(
"""
const scrollY = window.scrollY || document.documentElement.scrollTop || 0;
window.scrollBy(0, 1);
window.scrollTo(0, scrollY);
document.dispatchEvent(new MouseEvent('mousemove', {
bubbles: true,
clientX: 1,
clientY: 1
}));
document.dispatchEvent(new KeyboardEvent('keydown', {
bubbles: true,
key: 'Shift'
}));
"""
)
def _row_progress_value(row, fallback_text):
progress_bars = row.find_elements(By.XPATH, ".//*[contains(@class,'progress-bar')]")
for progress_bar in progress_bars:
aria_value = progress_bar.get_attribute("aria-valuenow")
progress = _parse_progress(aria_value)
if progress > 0:
return progress
style = progress_bar.get_attribute("style") or ""
progress = _parse_progress(style)
if progress > 0:
return progress
text = progress_bar.text.strip()
progress = _parse_progress(text)
if progress > 0:
return progress
return _parse_progress(fallback_text)
def _row_cell_text(row, zero_based_index):
cells = row.find_elements(By.XPATH, "./td")
if len(cells) <= zero_based_index:
return ""
return cells[zero_based_index].text.strip()
def _parse_progress(progress_text):
progress_text = str(progress_text or "")
match = re.search(r"(\d+)\s*%", progress_text)
if match:
return int(match.group(1))
match = re.search(r"\b(\d{1,3})\b", progress_text)
if match:
return int(match.group(1))
if "100" in progress_text:
return 100
return 0
def _row_fetch_button(row):
buttons = row.find_elements(By.XPATH, ".//*[self::button or self::a][normalize-space()='Fetch']")
for button in buttons:
if button.is_displayed() and button.is_enabled():
return button
return None
def _current_status_text(driver):
by_id = driver.find_elements(By.ID, "lblStatus")
for candidate in by_id:
text = candidate.text.strip()
if text:
return text
candidates = driver.find_elements(
By.XPATH,
"//*[contains(normalize-space(),'Status :') or contains(normalize-space(),'Status:')]"
)
for candidate in candidates:
text = candidate.text.strip()
if text:
return text
return ""
def _wait_for_refreshed_status(driver, previous_status, timeout_seconds):
wait = WebDriverWait(driver, timeout_seconds)
return wait.until(
lambda _driver: _refreshed_status_text(_driver, previous_status) or False
)
def _refreshed_status_text(driver, previous_status):
current_status = _current_status_text(driver).strip()
if not current_status:
return ""
normalized_current = " ".join(current_status.split()).lower()
normalized_previous = " ".join((previous_status or "").split()).lower()
terminal_tokens = ("reject", "submitted", "open", "accept")
if any(token in normalized_current for token in terminal_tokens):
if normalized_current != normalized_previous:
return current_status
if "order id" in normalized_current:
return current_status
return ""
def _click(driver, element):
try:
element.click()
except Exception:
driver.execute_script("arguments[0].click();", element)
@@ -0,0 +1,97 @@
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
def click_fetch_button(driver):
wait = WebDriverWait(driver, 20)
button = wait.until(
EC.element_to_be_clickable(
(
By.XPATH,
"//*[self::button or self::a or self::input]"
"[normalize-space()='Fetch' or contains(normalize-space(),'Fetch') or @value='Fetch']"
"|//*[contains(normalize-space(),'Fetch')]/ancestor::*[self::button or self::a][1]"
)
)
)
try:
button.click()
except Exception:
driver.execute_script("arguments[0].click();", button)
def paste_matrix_into_bidding_grid(driver, tsv_text, expected_first_value):
matrix = [row.split("\t") for row in tsv_text.splitlines()]
wait = WebDriverWait(driver, 30)
wait.until(
lambda _driver: _populate_handsontable(_driver, matrix) is not False
)
wait.until(
lambda _driver: _top_left_grid_value(_driver).strip() == str(expected_first_value).strip()
)
def click_submit_button(driver):
wait = WebDriverWait(driver, 20)
button = wait.until(
EC.element_to_be_clickable(
(By.XPATH, "//button[normalize-space()='Submit']")
)
)
try:
button.click()
except Exception:
driver.execute_script("arguments[0].click();", button)
def _top_left_grid_value(driver):
cell = driver.find_element(
By.XPATH,
"(//table[contains(@class,'htCore')]//tbody/tr[1]/td[@role='gridcell'])[1]"
)
return cell.text
def _populate_handsontable(driver, matrix):
return driver.execute_script(
"""
const matrix = arguments[0];
const instances = (window.Handsontable && window.Handsontable.instances) || [];
for (const instance of instances) {
if (!instance || !instance.rootElement) {
continue;
}
const root = instance.rootElement;
if (!root.isConnected || root.offsetParent === null) {
continue;
}
const hasGridTable = !!root.querySelector("table.htCore tbody td[role='gridcell']");
const isInsideSelect2 = !!root.closest(".select2-drop, .select2-container, .select2-results");
const isInsidePopup = !!root.closest(".modal, .popover, .tooltip");
if (!hasGridTable || isInsideSelect2 || isInsidePopup) {
continue;
}
try {
instance.selectCell(0, 0);
instance.populateFromArray(0, 0, matrix, null, null, "paste");
return instance.getDataAtCell(0, 0);
} catch (error) {
// Try the next visible instance.
}
}
return false;
""",
matrix,
)
@@ -0,0 +1,74 @@
from pathlib import Path
def get_enabled_bidding_entries(config):
bidding_config = config["exchange_bidding"]
input_root = Path(bidding_config["input_root"])
json_input_root = Path(bidding_config.get("json_input_root", ""))
upload_input_root = Path(bidding_config.get("upload_input_root", ""))
excel_layout = bidding_config.get("excel_layout", {})
entries = bidding_config.get("entries", [])
resolved_entries = []
for entry in entries:
if not entry.get("enabled", True):
continue
resolved_entries.append(
_resolve_entry_paths(
**entry,
input_root=input_root,
json_input_root=json_input_root,
upload_input_root=upload_input_root,
excel_layout=excel_layout
)
)
return resolved_entries
def get_active_bidding_entry(config):
bidding_config = config["exchange_bidding"]
active_entry_key = bidding_config.get("active_entry_key")
for entry in get_enabled_bidding_entries(config):
if entry["key"] == active_entry_key:
return entry
raise KeyError(
f"Active bidding entry '{active_entry_key}' was not found among enabled entries."
)
def _resolve_entry_paths(**kwargs):
entry = dict(kwargs)
input_root = entry.pop("input_root")
json_input_root = entry.pop("json_input_root")
upload_input_root = entry.pop("upload_input_root")
excel_layout = entry.pop("excel_layout")
excel_path = (input_root / entry["excel_file"]).resolve()
json_path = None
if entry.get("json_file"):
json_path = (json_input_root / entry["json_file"]).resolve()
upload_path = None
upload_file_name = entry.get("upload_file") or entry.get("excel_file")
if upload_file_name:
preferred_upload_path = (upload_input_root / upload_file_name).resolve()
upload_path = preferred_upload_path if preferred_upload_path.exists() else excel_path
return {
**entry,
"excel_path": str(excel_path),
"json_path": str(json_path) if json_path else None,
"upload_path": str(upload_path) if upload_path else None,
"excel_layout": {
**excel_layout,
**entry.get("excel_layout", {})
}
}
@@ -0,0 +1,80 @@
import json
from pathlib import Path
def load_single_bid_json_matrix(json_path):
data_file = Path(json_path)
if not data_file.exists():
raise FileNotFoundError(f"JSON input file not found: {data_file}")
with data_file.open("r", encoding="utf-8") as file:
payload = json.load(file)
if "matrix" in payload:
matrix = [[_to_text(value) for value in row] for row in payload["matrix"]]
elif "slot_range" in payload:
matrix = _build_range_matrix(payload)
else:
price_row = [_to_text(value) for value in payload["price_row"]]
slot_rows = [
[_to_text(value) for value in slot["orders"]]
for slot in payload.get("slots", [])
]
matrix = [price_row, *slot_rows]
if not matrix or not matrix[0]:
raise ValueError(f"No bidding data found in {data_file}.")
tsv = "\n".join("\t".join(row) for row in matrix)
return {
"matrix": matrix,
"tsv": tsv,
"row_count": len(matrix),
"column_count": len(matrix[0]),
"first_value": matrix[0][0],
}
def _build_range_matrix(payload):
price_row = [_to_text(value) for value in payload["price_row"]]
slot_range = payload["slot_range"]
from_slot = int(slot_range["from_slot"])
to_slot = int(slot_range["to_slot"])
orders = payload.get("orders", [])
if from_slot < 1 or to_slot < from_slot:
raise ValueError(f"Invalid slot_range: from_slot={from_slot}, to_slot={to_slot}")
expected_row_count = to_slot - from_slot + 1
if len(orders) != expected_row_count:
raise ValueError(
"Order row count does not match slot range: "
f"expected {expected_row_count}, found {len(orders)}"
)
column_count = len(price_row)
blank_row = [""] * column_count
slot_rows = []
for _ in range(from_slot - 1):
slot_rows.append(blank_row.copy())
for order_row in orders:
normalized_row = [_to_text(value) for value in order_row]
if len(normalized_row) != column_count:
raise ValueError(
"Each order row must have the same number of values as price_row: "
f"expected {column_count}, found {len(normalized_row)}"
)
slot_rows.append(normalized_row)
return [price_row, *slot_rows]
def _to_text(value):
if value is None:
return ""
return str(value)
@@ -0,0 +1,231 @@
import time
from urllib.parse import urlparse
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from tests.regression.ui_helpers import (
click_select2_option_by_text,
open_bootstrap_nav_submenu,
select_select2_value_by_container_id,
wait_for_select2_option_by_text,
)
TRADING_MARKET_LABELS = {
"GDAM": "GDAM Order Entry",
"DAM": "DAM Order Entry",
"HPDAM": "HPDAM Order Entry",
}
TRADING_MARKET_HREFS = {
"GDAM": "../IDAS/GDAMOrderEntry.aspx",
"DAM": "../IDAS/DAMOrderEntry.aspx",
"HPDAM": "../IDAS/HPDAMOrderEntry.aspx",
}
BIDDING_SCREEN_LABELS = {
"single_bid": "Single Bid",
"block_bid": "Block Bid",
}
BLOCK_BULK_UPLOAD_HREF = "../FrontOffice/BulkOrderUploadIDAS.aspx"
def open_idas_market_page(driver, trading_market):
open_bootstrap_nav_submenu(
driver,
"Trading",
"IDAS",
TRADING_MARKET_HREFS[trading_market]
)
wait = WebDriverWait(driver, 20)
wait.until(
EC.any_of(
EC.url_contains("OrderEntry.aspx"),
EC.visibility_of_element_located((By.XPATH, "//a[normalize-space()='Single Bid']")),
EC.visibility_of_element_located((By.ID, "s2id_ddlproduct"))
)
)
def open_idas_block_bulk_upload_page(driver):
open_bootstrap_nav_submenu(
driver,
"Trading",
"IDAS",
"BulkOrderUploadIDAS.aspx"
)
wait = WebDriverWait(driver, 20)
wait.until(
EC.any_of(
EC.url_contains("BulkOrderUploadIDAS.aspx"),
EC.visibility_of_element_located((By.XPATH, "//*[contains(normalize-space(),'IDAS Bulk Order Upload')]")),
EC.visibility_of_element_located((By.ID, "validateOrder"))
)
)
def open_idas_market_page_direct(driver, trading_market):
current_url = urlparse(driver.current_url)
base_url = f"{current_url.scheme}://{current_url.netloc}"
order_entry_path = TRADING_MARKET_HREFS[trading_market].replace("../", "/")
driver.get(f"{base_url}{order_entry_path}")
wait = WebDriverWait(driver, 20)
wait.until(
EC.any_of(
EC.url_contains("OrderEntry.aspx"),
EC.visibility_of_element_located((By.XPATH, "//a[normalize-space()='Single Bid']")),
EC.visibility_of_element_located((By.ID, "s2id_ddlproduct"))
)
)
def open_bidding_screen(driver, bidding_screen):
if bidding_screen == "single_bid":
return
tab_label = BIDDING_SCREEN_LABELS[bidding_screen]
wait = WebDriverWait(driver, 20)
active_or_visible_tab = wait.until(
EC.visibility_of_element_located(
(
By.XPATH,
f"//*[self::a or self::button or self::span][normalize-space()='{tab_label}']"
)
)
)
tab_classes = (active_or_visible_tab.get_attribute("class") or "").lower()
if "active" in tab_classes:
return
tab = wait.until(
EC.element_to_be_clickable(
(
By.XPATH,
f"//*[self::a or self::button or self::span][normalize-space()='{tab_label}']"
)
)
)
try:
tab.click()
except Exception:
driver.execute_script("arguments[0].click();", tab)
def select_bidding_filters(
driver,
product_code,
tm_id,
tac_id,
product_to_tm_delay_seconds=0,
tm_to_tac_delay_seconds=0,
tm_timeout_seconds=30,
tm_attempts=3,
tac_timeout_seconds=15,
tac_attempts=2,
bidding_screen_ready_wait_seconds=10
):
if bidding_screen_ready_wait_seconds > 0:
time.sleep(bidding_screen_ready_wait_seconds)
wait_for_bidding_product_configuration(driver)
select_select2_value_by_container_id(driver, "s2id_ddlproduct", product_code, exact=True)
if product_to_tm_delay_seconds > 0:
time.sleep(product_to_tm_delay_seconds)
wait_for_select2_option_by_text(
driver,
"s2id_ddlTMID",
tm_id,
timeout_seconds=tm_timeout_seconds,
attempts=tm_attempts
)
click_select2_option_by_text(driver, "s2id_ddlTMID", tm_id)
if tm_to_tac_delay_seconds > 0:
time.sleep(tm_to_tac_delay_seconds)
wait_for_select2_option_by_text(
driver,
"s2id_ddlTACID",
tac_id,
timeout_seconds=tac_timeout_seconds,
attempts=tac_attempts
)
click_select2_option_by_text(driver, "s2id_ddlTACID", tac_id)
def select_bidding_filters_for_trading_member(
driver,
product_code,
tac_id,
tac_timeout_seconds=15,
tac_attempts=2,
bidding_screen_ready_wait_seconds=10
):
if bidding_screen_ready_wait_seconds > 0:
time.sleep(bidding_screen_ready_wait_seconds)
wait_for_bidding_product_configuration(driver)
select_select2_value_by_container_id(driver, "s2id_ddlproduct", product_code, exact=True)
wait_for_select2_option_by_text(
driver,
"s2id_ddlTACID",
tac_id,
timeout_seconds=tac_timeout_seconds,
attempts=tac_attempts
)
click_select2_option_by_text(driver, "s2id_ddlTACID", tac_id)
def select_bidding_product_and_tm(
driver,
product_code,
tm_id,
product_to_tm_delay_seconds=0,
tm_timeout_seconds=30,
tm_attempts=3,
bidding_screen_ready_wait_seconds=10
):
if bidding_screen_ready_wait_seconds > 0:
time.sleep(bidding_screen_ready_wait_seconds)
wait_for_bidding_product_configuration(driver)
select_select2_value_by_container_id(driver, "s2id_ddlproduct", product_code, exact=True)
if product_to_tm_delay_seconds > 0:
time.sleep(product_to_tm_delay_seconds)
wait_for_select2_option_by_text(
driver,
"s2id_ddlTMID",
tm_id,
timeout_seconds=tm_timeout_seconds,
attempts=tm_attempts
)
click_select2_option_by_text(driver, "s2id_ddlTMID", tm_id)
def wait_for_bidding_product_configuration(driver, timeout_seconds=60):
wait = WebDriverWait(driver, timeout_seconds)
wait.until(
lambda _driver: not any(
element.is_displayed()
for element in _driver.find_elements(
By.XPATH,
"//*[contains(translate(normalize-space(), "
"'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), "
"'begin product configuration')]"
)
)
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,191 @@
from pathlib import Path
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
def upload_bid_file(driver, upload_path):
file_path = Path(upload_path)
if not file_path.exists():
raise FileNotFoundError(f"Upload file not found: {file_path}")
wait = WebDriverWait(driver, 30)
existing_inputs = _file_inputs(driver)
file_input = _preferred_file_input(existing_inputs)
if file_input is None:
upload_button = wait.until(
EC.element_to_be_clickable(
(
By.XPATH,
"//*[self::button or self::a or self::input]"
"[normalize-space()='Upload' or contains(normalize-space(),'Upload') or @value='Upload']"
)
)
)
try:
upload_button.click()
except Exception:
driver.execute_script("arguments[0].click();", upload_button)
file_input = wait.until(
lambda _driver: _find_new_or_existing_file_input(_driver, existing_inputs)
)
if file_input is None:
raise RuntimeError(
"No HTML file input was found for the Upload action. "
"If this page only exposes the native Windows file picker, we will need a Windows GUI automation fallback."
)
driver.execute_script("arguments[0].style.display='block';", file_input)
file_input.send_keys(str(file_path.resolve()))
wait.until(
lambda _driver: _upload_acknowledged(_driver, file_path.name)
)
def submit_uploaded_bid(driver, post_upload_delay_seconds=0, submit_to_fetch_delay_seconds=0):
wait = WebDriverWait(driver, 30)
previous_status = _current_status_text(driver)
if post_upload_delay_seconds > 0:
time.sleep(post_upload_delay_seconds)
submit_button = wait.until(
EC.element_to_be_clickable((By.ID, "btnSubmitOrder"))
)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", submit_button)
try:
submit_button.click()
except Exception:
driver.execute_script("arguments[0].click();", submit_button)
if submit_to_fetch_delay_seconds > 0:
time.sleep(submit_to_fetch_delay_seconds)
fetch_button = wait.until(
EC.element_to_be_clickable((By.ID, "btnFetch"))
)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", fetch_button)
try:
fetch_button.click()
except Exception:
driver.execute_script("arguments[0].click();", fetch_button)
status_text = wait.until(
lambda _driver: _refreshed_status_text(_driver, previous_status) or False
)
lowered_status = status_text.lower()
if "reject" in lowered_status:
raise AssertionError(f"Uploaded order was rejected. Status: {status_text}")
if "submit" not in lowered_status and "open" not in lowered_status and "accept" not in lowered_status:
raise AssertionError(f"Unexpected uploaded order status after fetch: {status_text}")
return status_text
def _find_new_or_existing_file_input(driver, existing_inputs):
current_inputs = _file_inputs(driver)
for current in current_inputs:
if current not in existing_inputs:
return current
preferred = _preferred_file_input(current_inputs)
if preferred is not None:
return preferred
for current in current_inputs:
if current.is_displayed():
return current
if current_inputs:
return current_inputs[0]
return False
def _file_inputs(driver):
return driver.find_elements(By.XPATH, "//input[@type='file' and not(@webkitdirectory)]")
def _preferred_file_input(inputs):
if not inputs:
return None
for current in inputs:
element_id = (current.get_attribute("id") or "").lower()
element_name = (current.get_attribute("name") or "").lower()
outer_html = (current.get_attribute("outerHTML") or "").lower()
if any(token in text for token in ("upload", "file") for text in (element_id, element_name, outer_html)):
return current
for current in inputs:
if current.is_displayed():
return current
return inputs[0]
def _upload_acknowledged(driver, file_name):
page_text = driver.find_element(By.TAG_NAME, "body").text
if file_name in page_text:
return True
if "Upload Logs" in page_text or "View Upload Logs" in page_text:
return True
return False
def _current_status_text(driver):
by_id = driver.find_elements(By.ID, "lblStatus")
for candidate in by_id:
text = candidate.text.strip()
if text:
return text
candidates = driver.find_elements(
By.XPATH,
"//*[contains(normalize-space(),'Status :') or contains(normalize-space(),'Status:')]"
)
for candidate in candidates:
text = candidate.text.strip()
if text:
return text
return ""
def _refreshed_status_text(driver, previous_status):
current_status = _current_status_text(driver).strip()
if not current_status:
return ""
normalized_current = " ".join(current_status.split()).lower()
normalized_previous = " ".join((previous_status or "").split()).lower()
terminal_tokens = ("reject", "submitted", "open", "accept")
if any(token in normalized_current for token in terminal_tokens):
if normalized_current != normalized_previous:
return current_status
if "order id" in normalized_current:
return current_status
return ""