Initial commit - Selenium automation framework
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# IDAS Exchange Regression
|
||||
|
||||
This area is for exchange-side IDAS regression flows.
|
||||
|
||||
## Current scaffold
|
||||
|
||||
- `bidding/test_idas_exchange_bidding_flow.py`: starting point for the exchange bidding flow.
|
||||
|
||||
## Config source
|
||||
|
||||
This regression suite reads from the new regression config layer:
|
||||
|
||||
- `config/runtime.json`
|
||||
- `config/runtime.json > users`
|
||||
- `config/runtime.json > IDAS`
|
||||
|
||||
## Current runtime expectations
|
||||
|
||||
- `execution.login_role` should be `exchange`
|
||||
- `execution.product_family` should be `IDAS`
|
||||
- `execution.flow_scope` can be `e2e` or `bidding`
|
||||
|
||||
## Planned expansion
|
||||
|
||||
As steps are finalized, we can add:
|
||||
|
||||
- exchange bidding buy flow
|
||||
- exchange bidding sell flow
|
||||
- matching verification
|
||||
- report downloads
|
||||
- order book filters
|
||||
- trade book filters
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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 ""
|
||||
@@ -0,0 +1 @@
|
||||
from tests.regression.auth import login_to_trading_application
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.support.select import Select
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
from tests.regression.ui_helpers import open_bootstrap_nav_submenu
|
||||
|
||||
|
||||
MATCHING_URL_FRAGMENT = "../Application/IDASInitiateMatching.aspx"
|
||||
GENERATE_FLOW_REQUEST_BUTTON_ID = "btnGenerateProvisionalFlowRequest"
|
||||
REVIEW_RESULT_BUTTON_ID = "btnReviewResult"
|
||||
REVIEW_RESULT_MODAL_ID = "divReviewResult"
|
||||
SUCCESS_CONTAINER_ID = "divSuccess"
|
||||
SUCCESS_MESSAGE_ID = "successMsg"
|
||||
|
||||
|
||||
def open_idas_matching_page(driver):
|
||||
|
||||
open_bootstrap_nav_submenu(
|
||||
driver,
|
||||
"Trade Processing",
|
||||
"IDAS",
|
||||
MATCHING_URL_FRAGMENT
|
||||
)
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
wait.until(EC.visibility_of_element_located((By.ID, "ddlproduct")))
|
||||
|
||||
|
||||
def configure_matching_request(driver, product_code, matching_type, source_type=None):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
if source_type:
|
||||
source_type_select = wait.until(
|
||||
EC.visibility_of_element_located(
|
||||
(
|
||||
By.XPATH,
|
||||
"//label[normalize-space()='SourceType']/following::select[1]"
|
||||
)
|
||||
)
|
||||
)
|
||||
Select(source_type_select).select_by_visible_text(source_type)
|
||||
|
||||
product_select = wait.until(
|
||||
EC.visibility_of_element_located((By.ID, "ddlproduct"))
|
||||
)
|
||||
_select_product_code(product_select, product_code)
|
||||
|
||||
matching_type_select = wait.until(
|
||||
EC.visibility_of_element_located((By.ID, "ddlMatchingTypeIM"))
|
||||
)
|
||||
Select(matching_type_select).select_by_visible_text(matching_type)
|
||||
|
||||
|
||||
def _select_product_code(select_element, product_code):
|
||||
|
||||
normalized_code = product_code.strip().lower()
|
||||
product_select = Select(select_element)
|
||||
for option in product_select.options:
|
||||
option_text = option.text.strip()
|
||||
option_code = option_text.split("(", 1)[0].strip().lower()
|
||||
if option_text.lower() == normalized_code or option_code == normalized_code:
|
||||
product_select.select_by_visible_text(option_text)
|
||||
return
|
||||
|
||||
available_options = [option.text.strip() for option in product_select.options]
|
||||
raise ValueError(
|
||||
f"Product code '{product_code}' is unavailable on the matching page. "
|
||||
f"Available options: {available_options}"
|
||||
)
|
||||
|
||||
|
||||
def start_provisional_match(driver):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
provisional_button = wait.until(
|
||||
EC.element_to_be_clickable((By.ID, "btnProvisionalMatch"))
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", provisional_button)
|
||||
try:
|
||||
provisional_button.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", provisional_button)
|
||||
|
||||
confirm_provisional_match(driver)
|
||||
|
||||
|
||||
def confirm_provisional_match(driver):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
modal = wait.until(
|
||||
EC.visibility_of_element_located((By.ID, "matchingConfirmation"))
|
||||
)
|
||||
|
||||
ok_button = wait.until(
|
||||
EC.element_to_be_clickable((By.ID, "btnProvisional"))
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", ok_button)
|
||||
try:
|
||||
ok_button.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", ok_button)
|
||||
|
||||
wait.until(EC.invisibility_of_element(modal))
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
def poll_provisional_match_status(driver, interval_seconds=2, timeout_seconds=120):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
end_time = time.monotonic() + timeout_seconds
|
||||
last_message = ""
|
||||
|
||||
while time.monotonic() < end_time:
|
||||
check_status_button = wait.until(
|
||||
EC.element_to_be_clickable((By.ID, "lnkCheckStatusProvisional"))
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", check_status_button)
|
||||
try:
|
||||
check_status_button.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", check_status_button)
|
||||
|
||||
message, state = _read_matching_status(driver)
|
||||
if message:
|
||||
last_message = message
|
||||
|
||||
if state == "success":
|
||||
return message
|
||||
|
||||
if state == "error":
|
||||
raise AssertionError(f"Matching failed: {message}")
|
||||
|
||||
time.sleep(interval_seconds)
|
||||
|
||||
raise AssertionError(
|
||||
"Matching status did not reach success within the timeout. "
|
||||
f"Last visible message: {last_message or 'No status message found'}"
|
||||
)
|
||||
|
||||
|
||||
def prepare_matching_download_folder(test_context, folder_name="matching_files"):
|
||||
|
||||
download_folder = Path(test_context["folder"]) / folder_name
|
||||
download_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
extra_folders = test_context.setdefault("extra_folders", [])
|
||||
if not any(item.get("path") == str(download_folder) for item in extra_folders):
|
||||
extra_folders.append(
|
||||
{
|
||||
"name": "Matching Files Folder",
|
||||
"path": str(download_folder),
|
||||
}
|
||||
)
|
||||
|
||||
return download_folder
|
||||
|
||||
|
||||
def configure_browser_downloads(driver, download_folder):
|
||||
|
||||
driver.execute_cdp_cmd(
|
||||
"Page.setDownloadBehavior",
|
||||
{
|
||||
"behavior": "allow",
|
||||
"downloadPath": str(download_folder),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def click_generate_flow_request(driver):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
generate_button = wait.until(
|
||||
EC.element_to_be_clickable((By.ID, GENERATE_FLOW_REQUEST_BUTTON_ID))
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", generate_button)
|
||||
try:
|
||||
generate_button.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", generate_button)
|
||||
|
||||
|
||||
def wait_for_flow_request_success(driver, timeout_seconds=30):
|
||||
|
||||
wait = WebDriverWait(driver, timeout_seconds)
|
||||
success_container = wait.until(
|
||||
EC.visibility_of_element_located((By.ID, SUCCESS_CONTAINER_ID))
|
||||
)
|
||||
|
||||
try:
|
||||
message = success_container.find_element(By.ID, SUCCESS_MESSAGE_ID).text.strip()
|
||||
except Exception:
|
||||
message = success_container.text.strip()
|
||||
|
||||
if not message:
|
||||
raise AssertionError("Flow request success banner appeared without a message.")
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def export_review_result_for_source(driver, source_type, download_folder, timeout_seconds=60):
|
||||
|
||||
existing_files = {path.name for path in Path(download_folder).iterdir()}
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
review_result_button = wait.until(
|
||||
EC.element_to_be_clickable((By.ID, REVIEW_RESULT_BUTTON_ID))
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", review_result_button)
|
||||
try:
|
||||
review_result_button.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", review_result_button)
|
||||
|
||||
modal = wait.until(
|
||||
EC.visibility_of_element_located((By.ID, REVIEW_RESULT_MODAL_ID))
|
||||
)
|
||||
|
||||
export_button = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(
|
||||
By.CSS_SELECTOR,
|
||||
f"#{REVIEW_RESULT_MODAL_ID} .pExcel.pButton"
|
||||
)
|
||||
)
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", export_button)
|
||||
try:
|
||||
export_button.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", export_button)
|
||||
|
||||
downloaded_file = wait_for_new_download(
|
||||
download_folder,
|
||||
existing_files=existing_files,
|
||||
timeout_seconds=timeout_seconds
|
||||
)
|
||||
renamed_file = rename_download_with_source_type(downloaded_file, source_type)
|
||||
|
||||
close_button = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(
|
||||
By.CSS_SELECTOR,
|
||||
f"#{REVIEW_RESULT_MODAL_ID} button.close"
|
||||
)
|
||||
)
|
||||
)
|
||||
try:
|
||||
close_button.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", close_button)
|
||||
|
||||
wait.until(EC.invisibility_of_element(modal))
|
||||
return renamed_file
|
||||
|
||||
|
||||
def wait_for_new_download(download_folder, existing_files=None, timeout_seconds=60):
|
||||
|
||||
download_path = Path(download_folder)
|
||||
existing_files = existing_files or set()
|
||||
end_time = time.monotonic() + timeout_seconds
|
||||
|
||||
while time.monotonic() < end_time:
|
||||
current_files = [path for path in download_path.iterdir() if path.is_file()]
|
||||
new_files = [path for path in current_files if path.name not in existing_files]
|
||||
partial_downloads = [
|
||||
path for path in current_files
|
||||
if path.suffix.lower() in {".crdownload", ".tmp", ".part"}
|
||||
]
|
||||
|
||||
completed_files = [
|
||||
path for path in new_files
|
||||
if path.suffix.lower() not in {".crdownload", ".tmp", ".part"}
|
||||
]
|
||||
if completed_files and not partial_downloads:
|
||||
completed_files.sort(key=lambda item: item.stat().st_mtime, reverse=True)
|
||||
return str(completed_files[0])
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
visible_names = sorted(path.name for path in download_path.iterdir() if path.is_file())
|
||||
raise AssertionError(
|
||||
"No completed matching download was detected within the timeout. "
|
||||
f"Folder: {download_folder}. Current files: {visible_names}"
|
||||
)
|
||||
|
||||
|
||||
def rename_download_with_source_type(downloaded_file, source_type):
|
||||
|
||||
file_path = Path(downloaded_file)
|
||||
normalized_source_type = source_type.strip().lower().replace(" ", "_")
|
||||
target_path = file_path.with_name(f"{normalized_source_type}_{file_path.name}")
|
||||
|
||||
if target_path.exists():
|
||||
stem = file_path.stem
|
||||
suffix = file_path.suffix
|
||||
counter = 2
|
||||
while True:
|
||||
candidate = file_path.with_name(
|
||||
f"{normalized_source_type}_{stem}_{counter}{suffix}"
|
||||
)
|
||||
if not candidate.exists():
|
||||
target_path = candidate
|
||||
break
|
||||
counter += 1
|
||||
|
||||
file_path.rename(target_path)
|
||||
return str(target_path)
|
||||
|
||||
|
||||
def _read_matching_status(driver):
|
||||
|
||||
success_divs = driver.find_elements(By.ID, "divSuccess")
|
||||
for current in success_divs:
|
||||
if current.is_displayed():
|
||||
text = current.text.strip()
|
||||
if text:
|
||||
return text, "success"
|
||||
|
||||
error_divs = driver.find_elements(By.ID, "divError")
|
||||
for current in error_divs:
|
||||
if current.is_displayed():
|
||||
text = current.text.strip()
|
||||
if text:
|
||||
return text, "error"
|
||||
|
||||
warning_divs = driver.find_elements(By.ID, "divMatchingStatus")
|
||||
for current in warning_divs:
|
||||
if current.is_displayed():
|
||||
text = current.text.strip()
|
||||
if text:
|
||||
return text, "in_progress"
|
||||
|
||||
warning_messages = driver.find_elements(By.ID, "warningMsg")
|
||||
for current in warning_messages:
|
||||
text = current.text.strip()
|
||||
if text:
|
||||
return text, "in_progress"
|
||||
|
||||
return "", "unknown"
|
||||
@@ -0,0 +1,207 @@
|
||||
import re
|
||||
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 (
|
||||
open_bootstrap_nav_submenu,
|
||||
select_select2_value_by_container_id,
|
||||
)
|
||||
|
||||
|
||||
BID_TRANSFER_URL_FRAGMENT = "../MCO/IDASBidTransfer.aspx"
|
||||
SUPPORTED_MARKETS = ("GDAM", "DAM", "HPDAM")
|
||||
|
||||
|
||||
def open_idas_bid_transfer_page(driver):
|
||||
|
||||
open_bootstrap_nav_submenu(
|
||||
driver,
|
||||
"Trade Processing",
|
||||
"IDAS",
|
||||
BID_TRANSFER_URL_FRAGMENT,
|
||||
)
|
||||
WebDriverWait(driver, 20).until(
|
||||
EC.visibility_of_element_located((By.ID, "s2id_ddlproduct"))
|
||||
)
|
||||
|
||||
|
||||
def select_bid_transfer_product(driver, product_code):
|
||||
|
||||
select_select2_value_by_container_id(
|
||||
driver,
|
||||
"s2id_ddlproduct",
|
||||
product_code,
|
||||
)
|
||||
WebDriverWait(driver, 20).until(
|
||||
lambda current_driver: product_code.lower()
|
||||
in current_driver.find_element(By.ID, "s2id_ddlproduct").text.lower()
|
||||
)
|
||||
|
||||
|
||||
def transfer_market_orders(driver, market, timeout_seconds=30):
|
||||
|
||||
market = _validate_market(market)
|
||||
wait = WebDriverWait(driver, timeout_seconds)
|
||||
row = _market_row(driver, market)
|
||||
old_request_id = row.find_elements(By.TAG_NAME, "td")[1].text.strip()
|
||||
old_status = _status_element(driver, market).text.strip()
|
||||
|
||||
transfer_button = wait.until(EC.element_to_be_clickable((By.ID, market)))
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", transfer_button)
|
||||
transfer_button.click()
|
||||
|
||||
wait.until(
|
||||
lambda current_driver: _transfer_response_received(
|
||||
current_driver,
|
||||
market,
|
||||
old_request_id,
|
||||
old_status,
|
||||
)
|
||||
)
|
||||
|
||||
error_message = _visible_message(driver, "divError")
|
||||
if error_message:
|
||||
raise AssertionError(f"{market} bid transfer failed: {error_message}")
|
||||
|
||||
success_message = _visible_message(driver, "successMsg")
|
||||
status = _read_status(driver, market)
|
||||
if not success_message:
|
||||
raise AssertionError(
|
||||
f"{market} bid transfer did not show a success message. Current status: {status['text']}"
|
||||
)
|
||||
_assert_not_failed(market, "transfer", status)
|
||||
return {"message": success_message, **status}
|
||||
|
||||
|
||||
def check_market_transfer_status(driver, market, timeout_seconds=30):
|
||||
|
||||
market = _validate_market(market)
|
||||
check_button = WebDriverWait(driver, 20).until(
|
||||
EC.element_to_be_clickable(
|
||||
(
|
||||
By.XPATH,
|
||||
f"//button[contains(@onclick,\"GetMCOstatus('{market}')\")]",
|
||||
)
|
||||
)
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", check_button)
|
||||
check_button.click()
|
||||
|
||||
modal = WebDriverWait(driver, timeout_seconds).until(_visible_status_modal)
|
||||
|
||||
error_message = _visible_message(driver, "divError")
|
||||
if error_message:
|
||||
raise AssertionError(f"{market} status check failed: {error_message}")
|
||||
|
||||
modal_text = modal.text.strip()
|
||||
status_match = re.search(
|
||||
r"(?:^|\n)\s*Status\s*:\s*([^\r\n]+)",
|
||||
modal_text,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if not status_match:
|
||||
raise AssertionError(
|
||||
f"Could not read {market} MCO status from the Send Order Details popup."
|
||||
)
|
||||
|
||||
status_text = status_match.group(1).strip()
|
||||
return {
|
||||
"text": status_text,
|
||||
"normalized": _normalize_popup_status(status_text),
|
||||
"modal": modal,
|
||||
}
|
||||
|
||||
|
||||
def close_market_transfer_status(driver, status_result):
|
||||
|
||||
modal = status_result["modal"]
|
||||
close_button = modal.find_element(
|
||||
By.XPATH,
|
||||
".//button[normalize-space()='Close' or @data-dismiss='modal']",
|
||||
)
|
||||
close_button.click()
|
||||
WebDriverWait(driver, 20).until(EC.invisibility_of_element(modal))
|
||||
|
||||
|
||||
def _validate_market(market):
|
||||
|
||||
normalized_market = market.strip().upper()
|
||||
if normalized_market not in SUPPORTED_MARKETS:
|
||||
raise ValueError(
|
||||
f"Unsupported IDAS bid-transfer market '{market}'. Use one of {SUPPORTED_MARKETS}."
|
||||
)
|
||||
return normalized_market
|
||||
|
||||
|
||||
def _market_row(driver, market):
|
||||
|
||||
return driver.find_element(
|
||||
By.XPATH,
|
||||
f"//div[@id='SendOrderId']//tr[td[1][normalize-space()='{market}']]",
|
||||
)
|
||||
|
||||
|
||||
def _status_element(driver, market):
|
||||
|
||||
return driver.find_element(By.ID, f"{market.lower()}sat")
|
||||
|
||||
|
||||
def _transfer_response_received(driver, market, old_request_id, old_status):
|
||||
|
||||
if _visible_message(driver, "divError"):
|
||||
return True
|
||||
row = _market_row(driver, market)
|
||||
request_id = row.find_elements(By.TAG_NAME, "td")[1].text.strip()
|
||||
status = _status_element(driver, market).text.strip()
|
||||
return bool(request_id and request_id != old_request_id) or status != old_status
|
||||
|
||||
|
||||
def _visible_message(driver, element_id):
|
||||
|
||||
elements = driver.find_elements(By.ID, element_id)
|
||||
if not elements or not elements[0].is_displayed():
|
||||
return ""
|
||||
return elements[0].text.strip()
|
||||
|
||||
|
||||
def _visible_status_modal(driver):
|
||||
|
||||
modals = driver.find_elements(
|
||||
By.XPATH,
|
||||
"//div[contains(@class,'modal')][.//*[contains(normalize-space(),'Send Order Details')]]",
|
||||
)
|
||||
for modal in modals:
|
||||
if modal.is_displayed():
|
||||
return modal
|
||||
return False
|
||||
|
||||
|
||||
def _normalize_popup_status(status):
|
||||
|
||||
return re.sub(r"[\s-]+", "_", status.strip()).upper()
|
||||
|
||||
|
||||
def _read_status(driver, market):
|
||||
|
||||
element = _status_element(driver, market)
|
||||
background_color = element.value_of_css_property("background-color")
|
||||
return {
|
||||
"text": element.text.strip(),
|
||||
"background_color": background_color,
|
||||
"is_green": _is_green(background_color),
|
||||
}
|
||||
|
||||
|
||||
def _is_green(css_color):
|
||||
|
||||
channels = [int(value) for value in re.findall(r"\d+", css_color)[:3]]
|
||||
return len(channels) == 3 and channels[1] > channels[0] and channels[1] > channels[2]
|
||||
|
||||
|
||||
def _assert_not_failed(market, action, status):
|
||||
|
||||
if "fail" in status["text"].lower():
|
||||
raise AssertionError(
|
||||
f"{market} {action} returned '{status['text']}'. Matching has been stopped."
|
||||
)
|
||||
@@ -0,0 +1,215 @@
|
||||
import pytest
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
from tests.regression.auth import login_to_trading_application
|
||||
from tests.regression.config_reader import load_regression_config, should_run_flow
|
||||
from tests.regression.exchange.idas.matching.actions import (
|
||||
click_generate_flow_request,
|
||||
configure_browser_downloads,
|
||||
configure_matching_request,
|
||||
export_review_result_for_source,
|
||||
open_idas_matching_page,
|
||||
poll_provisional_match_status,
|
||||
prepare_matching_download_folder,
|
||||
start_provisional_match,
|
||||
wait_for_flow_request_success,
|
||||
)
|
||||
from tests.regression.exchange.idas.matching.bid_transfer_actions import (
|
||||
SUPPORTED_MARKETS,
|
||||
check_market_transfer_status,
|
||||
close_market_transfer_status,
|
||||
open_idas_bid_transfer_page,
|
||||
select_bid_transfer_product,
|
||||
transfer_market_orders,
|
||||
)
|
||||
from tests.step_actions import StepActions
|
||||
|
||||
|
||||
def get_exchange_matching_context(driver, test_context):
|
||||
|
||||
config = load_regression_config()
|
||||
|
||||
assert config["selected_role"] == "exchange", (
|
||||
"IDAS matching is available only for exchange login. "
|
||||
"Set execution.login_role to 'exchange' in config/runtime.json."
|
||||
)
|
||||
|
||||
if not should_run_flow("matching"):
|
||||
pytest.skip(
|
||||
f"Skipping exchange matching flow because flow_scope is '{config['selected_flow']}'."
|
||||
)
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
step = StepActions(driver, wait, test_context)
|
||||
return config, step
|
||||
|
||||
|
||||
def open_app_and_login(driver, step, config):
|
||||
|
||||
driver.get(config["portal_urls"]["pratyay"])
|
||||
step.capture("open_pratyay_application")
|
||||
|
||||
login_to_trading_application(
|
||||
driver,
|
||||
config["login"]["username"],
|
||||
config["login"]["password"],
|
||||
config["timing"],
|
||||
login_role=config["selected_role"]
|
||||
)
|
||||
step.capture("pratyay_login_submitted")
|
||||
|
||||
|
||||
def _step_slug(source_type):
|
||||
|
||||
return source_type.strip().lower().replace(" ", "_")
|
||||
|
||||
|
||||
def _configured_transfer_markets(matching_config):
|
||||
|
||||
markets = matching_config.get("bid_transfer_markets") or list(SUPPORTED_MARKETS)
|
||||
normalized_markets = [market.strip().upper() for market in markets]
|
||||
if "ALL" in normalized_markets:
|
||||
return list(SUPPORTED_MARKETS)
|
||||
return list(dict.fromkeys(normalized_markets))
|
||||
|
||||
|
||||
@pytest.mark.regression
|
||||
@pytest.mark.exchange
|
||||
@pytest.mark.matching
|
||||
def test_idas_exchange_matching_flow(driver, test_context):
|
||||
|
||||
config, step = get_exchange_matching_context(driver, test_context)
|
||||
matching_config = config["exchange_matching"]
|
||||
|
||||
if not matching_config.get("portals", {}).get("pratyay", {}).get("enabled", True):
|
||||
pytest.skip("Pratyay matching preparation is disabled in config/runtime.json.")
|
||||
|
||||
open_app_and_login(driver, step, config)
|
||||
|
||||
product_config = config["product"]
|
||||
transfer_markets = _configured_transfer_markets(matching_config)
|
||||
transfer_timeout = matching_config.get("bid_transfer_timeout_seconds", 30)
|
||||
accepted_transfer_statuses = {
|
||||
status.strip().upper()
|
||||
for status in matching_config.get(
|
||||
"accepted_bid_transfer_statuses",
|
||||
["ACCEPTED", "PARTIALLY_ACCEPTED", "NO_BIDS"],
|
||||
)
|
||||
}
|
||||
|
||||
open_idas_bid_transfer_page(driver)
|
||||
step.capture("open_idas_bid_transfer")
|
||||
select_bid_transfer_product(driver, product_config["product_code"])
|
||||
step.capture("select_bid_transfer_product")
|
||||
|
||||
for index, market in enumerate(transfer_markets, start=1):
|
||||
transfer_result = transfer_market_orders(
|
||||
driver,
|
||||
market,
|
||||
timeout_seconds=transfer_timeout,
|
||||
)
|
||||
step.capture(f"bid_transfer_{index}_{market.lower()}")
|
||||
|
||||
status_result = check_market_transfer_status(
|
||||
driver,
|
||||
market,
|
||||
timeout_seconds=transfer_timeout,
|
||||
)
|
||||
popup_status = status_result["normalized"]
|
||||
step.capture(
|
||||
f"bid_transfer_status_{index}_{market.lower()}_{popup_status.lower()}"
|
||||
)
|
||||
|
||||
if popup_status == "REJECTED":
|
||||
step.test_context["steps"][-1]["status"] = "failed"
|
||||
raise AssertionError(
|
||||
f"{market} MCO bid-transfer status is REJECTED. "
|
||||
"Matching has been stopped before processing the next market."
|
||||
)
|
||||
if popup_status not in accepted_transfer_statuses:
|
||||
step.test_context["steps"][-1]["status"] = "failed"
|
||||
raise AssertionError(
|
||||
f"{market} returned unsupported MCO bid-transfer status "
|
||||
f"'{status_result['text']}'. Matching has been stopped. "
|
||||
f"Accepted statuses: {sorted(accepted_transfer_statuses)}"
|
||||
)
|
||||
|
||||
close_market_transfer_status(driver, status_result)
|
||||
print(
|
||||
f"IDAS bid transfer completed for market={market}, "
|
||||
f"transfer_message={transfer_result['message']}, "
|
||||
f"popup_status={status_result['text']}"
|
||||
)
|
||||
|
||||
open_idas_matching_page(driver)
|
||||
step.capture("open_idas_initiate_matching")
|
||||
|
||||
if matching_config.get("initiation_mode", "provisional_match") != "provisional_match":
|
||||
raise ValueError(
|
||||
"Only 'provisional_match' initiation_mode is supported right now."
|
||||
)
|
||||
|
||||
download_folder = prepare_matching_download_folder(
|
||||
test_context,
|
||||
folder_name=matching_config.get("download_folder_name", "matching_files")
|
||||
)
|
||||
configure_browser_downloads(driver, download_folder)
|
||||
|
||||
source_types = matching_config.get(
|
||||
"source_types",
|
||||
["Green", "Conventional", "HPDAM"]
|
||||
)
|
||||
exported_files = []
|
||||
|
||||
for index, source_type in enumerate(source_types, start=1):
|
||||
source_slug = _step_slug(source_type)
|
||||
|
||||
configure_matching_request(
|
||||
driver,
|
||||
product_config["product_code"],
|
||||
matching_config["matching_type"],
|
||||
source_type=source_type
|
||||
)
|
||||
step.capture(f"select_matching_filters_{index}_{source_slug}")
|
||||
|
||||
start_provisional_match(driver)
|
||||
step.capture(f"start_provisional_match_{index}_{source_slug}")
|
||||
|
||||
final_status = poll_provisional_match_status(
|
||||
driver,
|
||||
interval_seconds=matching_config.get("check_status_interval_seconds", 2),
|
||||
timeout_seconds=matching_config.get("check_status_timeout_seconds", 120)
|
||||
)
|
||||
step.capture(f"provisional_matching_completed_{index}_{source_slug}")
|
||||
|
||||
click_generate_flow_request(driver)
|
||||
success_message = wait_for_flow_request_success(
|
||||
driver,
|
||||
timeout_seconds=matching_config.get("flow_request_success_timeout_seconds", 30)
|
||||
)
|
||||
step.capture(f"generate_flow_request_{index}_{source_slug}")
|
||||
|
||||
exported_file = export_review_result_for_source(
|
||||
driver,
|
||||
source_type,
|
||||
download_folder,
|
||||
timeout_seconds=matching_config.get("download_timeout_seconds", 60)
|
||||
)
|
||||
exported_files.append(exported_file)
|
||||
step.capture(f"review_result_exported_{index}_{source_slug}")
|
||||
|
||||
print(
|
||||
f"Matching review export completed for source_type={source_type}, "
|
||||
f"status={final_status}, "
|
||||
f"flow_request_message={success_message}, "
|
||||
f"exported_file={exported_file}"
|
||||
)
|
||||
|
||||
print(
|
||||
"IDAS exchange matching completed for: "
|
||||
f"matching_product_code={product_config['product_code']}, "
|
||||
f"matching_type={matching_config['matching_type']}, "
|
||||
f"source_types={source_types}, "
|
||||
f"matching_files_folder={download_folder}, "
|
||||
f"exported_files={exported_files}"
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
from tests.regression.auth import login_to_mco_application
|
||||
from tests.regression.config_reader import load_regression_config, should_run_flow
|
||||
from tests.step_actions import StepActions
|
||||
|
||||
|
||||
@pytest.mark.regression
|
||||
@pytest.mark.exchange
|
||||
@pytest.mark.matching
|
||||
@pytest.mark.mco
|
||||
def test_idas_mco_login(driver, test_context):
|
||||
|
||||
config = load_regression_config()
|
||||
matching_config = config["exchange_matching"]
|
||||
mco_config = matching_config.get("portals", {}).get("mco", {})
|
||||
|
||||
if not should_run_flow("matching"):
|
||||
pytest.skip(
|
||||
f"Skipping MCO login because flow_scope is '{config['selected_flow']}'."
|
||||
)
|
||||
if not mco_config.get("enabled", False):
|
||||
pytest.skip("MCO matching portal is disabled in config/runtime.json.")
|
||||
|
||||
step = StepActions(driver, WebDriverWait(driver, 20), test_context)
|
||||
driver.get(config["portal_urls"]["mco"])
|
||||
step.capture("open_mco_application")
|
||||
|
||||
login_to_mco_application(
|
||||
driver,
|
||||
config["mco_login"].get("username"),
|
||||
config["mco_login"].get("password"),
|
||||
config["timing"],
|
||||
)
|
||||
step.capture("mco_dashboard_loaded")
|
||||
|
||||
print(
|
||||
"IDAS MCO login completed for "
|
||||
f"environment={config['portal_urls']['environment']}, "
|
||||
f"dashboard_url={driver.current_url}"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from tests.regression.auth import TradingLoginLocator
|
||||
Reference in New Issue
Block a user