Initial commit - Selenium automation framework
This commit is contained in:
@@ -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 ""
|
||||
Reference in New Issue
Block a user