Initial commit - Selenium automation framework
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
# Virtual environments
|
||||
venv/
|
||||
.venv/
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Pytest cache
|
||||
.pytest_cache/
|
||||
|
||||
# Generated reports (keep directory structure)
|
||||
reports/screenshots/*
|
||||
!reports/screenshots/.gitkeep
|
||||
|
||||
reports/logs/*
|
||||
!reports/logs/.gitkeep
|
||||
|
||||
reports/html/*
|
||||
!reports/html/.gitkeep
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"python.testing.pytestArgs": [
|
||||
"tests"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
}
|
||||
+386
@@ -0,0 +1,386 @@
|
||||
# PXIL Selenium Run Commands
|
||||
|
||||
Use these commands from the project folder:
|
||||
|
||||
```powershell
|
||||
cd "C:\Selenium Test"
|
||||
.\venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
## Config Controls
|
||||
|
||||
Edit `config.json` before running.
|
||||
|
||||
Session and token together in one login:
|
||||
|
||||
```json
|
||||
"setup": {
|
||||
"run_session": true,
|
||||
"run_token": true
|
||||
}
|
||||
```
|
||||
|
||||
Token only:
|
||||
|
||||
```json
|
||||
"setup": {
|
||||
"run_session": false,
|
||||
"run_token": true
|
||||
}
|
||||
```
|
||||
|
||||
Session only:
|
||||
|
||||
```json
|
||||
"setup": {
|
||||
"run_session": true,
|
||||
"run_token": false
|
||||
}
|
||||
```
|
||||
|
||||
Multiple token product codes:
|
||||
|
||||
```json
|
||||
"product_codes": "IDAS,IDAS1,IDAS2,IDAS3,IDAS4"
|
||||
```
|
||||
|
||||
Single token product code:
|
||||
|
||||
```json
|
||||
"product_codes": "IDAS4"
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
Run setup based on `config.json`:
|
||||
|
||||
```powershell
|
||||
pytest tests\setup -s --html=reports/html/setup_run.html --self-contained-html
|
||||
```
|
||||
|
||||
Use this for:
|
||||
- Session and token together when both flags are `true`.
|
||||
- Token only when `run_session` is `false` and `run_token` is `true`.
|
||||
- Session only when `run_session` is `true` and `run_token` is `false`.
|
||||
|
||||
Run only token setup:
|
||||
|
||||
```powershell
|
||||
pytest tests\setup\tokens -s --html=reports/html/token_run.html --self-contained-html
|
||||
```
|
||||
|
||||
Use this when `run_token` is `true`. It will create one token or multiple tokens based on `trading_token_idas.product_codes`.
|
||||
|
||||
Run only session setup:
|
||||
|
||||
```powershell
|
||||
pytest tests\setup\sessions -s --html=reports/html/session_run.html --self-contained-html
|
||||
```
|
||||
|
||||
Use this when `run_session` is `true`.
|
||||
|
||||
Run combined session plus token setup explicitly:
|
||||
|
||||
```powershell
|
||||
pytest tests\setup\test_combined_setup.py -s --html=reports/html/combined_setup_run.html --self-contained-html
|
||||
```
|
||||
|
||||
Use this when both `run_session` and `run_token` are `true`. It logs in once, creates the session, then creates and activates all configured tokens in the same browser session.
|
||||
|
||||
Collect tests without opening browser:
|
||||
|
||||
```powershell
|
||||
pytest tests\setup --collect-only -q
|
||||
```
|
||||
|
||||
Use this to confirm pytest can see the setup tests.
|
||||
|
||||
## Regression Config Controls
|
||||
|
||||
Regression flows use `config/runtime.json`, not `config.json`.
|
||||
|
||||
Common fields to edit:
|
||||
|
||||
```json
|
||||
"execution": {
|
||||
"login_role": "trading_member",
|
||||
"trading_member_key": "tm_55",
|
||||
"product_family": "IDAS",
|
||||
"flow_scope": "bidding"
|
||||
}
|
||||
```
|
||||
|
||||
Trading-member login uses `environment.login_urls.trading_member`; exchange login
|
||||
uses `environment.login_urls.exchange`.
|
||||
|
||||
Product and market selection:
|
||||
|
||||
```json
|
||||
"active_selection": {
|
||||
"product_code": "IDAS1",
|
||||
"trading_market": "DAM",
|
||||
"bidding_screen": "single_bid"
|
||||
}
|
||||
```
|
||||
|
||||
Use `"trading_market": "ALL"` to run IDAS bidding for `GDAM`, `DAM`, and `HPDAM` in one test run. Use `"trading_markets": ["GDAM", "DAM"]` to run only those segments in that order.
|
||||
Use `"bidding_screen": "single_and_block_bid"` to run single-bid folder upload first and then block-bid folder upload in the same logged-in browser session.
|
||||
|
||||
Bidding entry method:
|
||||
|
||||
```json
|
||||
"entry_method": {
|
||||
"bid_entry_method": "file_upload",
|
||||
"file_upload_mode": "batch",
|
||||
"auto_submit": true
|
||||
}
|
||||
```
|
||||
|
||||
Available `bid_entry_method` values:
|
||||
- `excel_grid`: read Excel and paste values into the bidding grid.
|
||||
- `json_grid`: read JSON and paste values into the bidding grid.
|
||||
- `file_upload`: upload one Excel file or upload multiple files in batch mode.
|
||||
- `folder_upload`: upload each TM folder under the configured folder-upload root, click Submit All, wait for all files to reach 100%, then fetch each file status.
|
||||
|
||||
Available `file_upload_mode` values:
|
||||
- `single`: upload only the configured active entry file.
|
||||
- `batch`: upload all enabled upload files.
|
||||
|
||||
## Regression Commands
|
||||
|
||||
Run all active regression tests based on `config/runtime.json`:
|
||||
|
||||
```powershell
|
||||
pytest tests\regression -m regression -s --html=reports/html/regression_run.html --self-contained-html
|
||||
```
|
||||
|
||||
Use this when `execution.flow_scope` is `e2e`, `bidding`, or `matching`. Current active regression tests are exchange IDAS bidding and exchange IDAS matching.
|
||||
|
||||
Run IDAS exchange bidding:
|
||||
|
||||
```powershell
|
||||
pytest tests\regression\exchange\idas\bidding -m bidding -s --html=reports/html/bidding_run.html --self-contained-html
|
||||
```
|
||||
|
||||
Set this in `config/runtime.json` before running:
|
||||
|
||||
```json
|
||||
"execution": {
|
||||
"login_role": "exchange",
|
||||
"product_family": "IDAS",
|
||||
"flow_scope": "bidding"
|
||||
}
|
||||
```
|
||||
|
||||
Run Excel grid bidding:
|
||||
|
||||
```powershell
|
||||
pytest tests\regression\exchange\idas\bidding -m bidding -s --html=reports/html/bidding_excel_grid_run.html --self-contained-html
|
||||
```
|
||||
|
||||
Set this in `config/runtime.json`:
|
||||
|
||||
```json
|
||||
"entry_method": {
|
||||
"bid_entry_method": "excel_grid",
|
||||
"auto_submit": true
|
||||
}
|
||||
```
|
||||
|
||||
Run JSON grid bidding:
|
||||
|
||||
```powershell
|
||||
pytest tests\regression\exchange\idas\bidding -m bidding -s --html=reports/html/bidding_json_grid_run.html --self-contained-html
|
||||
```
|
||||
|
||||
Set this in `config/runtime.json`:
|
||||
|
||||
```json
|
||||
"entry_method": {
|
||||
"bid_entry_method": "json_grid",
|
||||
"auto_submit": true
|
||||
}
|
||||
```
|
||||
|
||||
Run single file upload bidding:
|
||||
|
||||
```powershell
|
||||
pytest tests\regression\exchange\idas\bidding -m bidding -s --html=reports/html/bidding_single_upload_run.html --self-contained-html
|
||||
```
|
||||
|
||||
Set this in `config/runtime.json`:
|
||||
|
||||
```json
|
||||
"entry_method": {
|
||||
"bid_entry_method": "file_upload",
|
||||
"file_upload_mode": "single",
|
||||
"auto_submit": true
|
||||
}
|
||||
```
|
||||
|
||||
Run batch file upload bidding:
|
||||
|
||||
```powershell
|
||||
pytest tests\regression\exchange\idas\bidding -m bidding -s --html=reports/html/bidding_batch_upload_run.html --self-contained-html
|
||||
```
|
||||
|
||||
Set this in `config/runtime.json`:
|
||||
|
||||
```json
|
||||
"entry_method": {
|
||||
"bid_entry_method": "file_upload",
|
||||
"file_upload_mode": "batch",
|
||||
"auto_submit": true
|
||||
}
|
||||
```
|
||||
|
||||
Batch upload uses enabled entries from `config/runtime.json > IDAS > bidding > entries`, and upload files from:
|
||||
|
||||
```text
|
||||
data/idas/bidding/uploads
|
||||
```
|
||||
|
||||
Run folder upload bidding:
|
||||
|
||||
```powershell
|
||||
pytest tests\regression\exchange\idas\bidding -m bidding -s --html=reports/html/bidding_folder_upload_run.html --self-contained-html
|
||||
```
|
||||
|
||||
Set this in `config/runtime.json`:
|
||||
|
||||
```json
|
||||
"users": {
|
||||
"exchange_parallel": [
|
||||
{
|
||||
"label": "Parallel Exchange 1",
|
||||
"username": "exchange_user_1",
|
||||
"password": "exchange_password_1"
|
||||
},
|
||||
{
|
||||
"label": "Parallel Exchange 2",
|
||||
"username": "exchange_user_2",
|
||||
"password": "exchange_password_2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"entry_method": {
|
||||
"bid_entry_method": "folder_upload",
|
||||
"folder_upload_execution_mode": "sequential",
|
||||
"auto_submit": true
|
||||
},
|
||||
"data_paths": {
|
||||
"folder_upload_root": "data/idas/bidding/folder_uploads",
|
||||
"folder_upload_sides": ["BUYSELL"]
|
||||
},
|
||||
"timing": {
|
||||
"folder_upload_max_parallel_sessions": 2,
|
||||
"folder_upload_parallel_login_stagger_seconds": 20,
|
||||
"folder_upload_parallel_captcha_wait_seconds": 60
|
||||
}
|
||||
```
|
||||
|
||||
Folder upload uses a separate root from normal single-file uploads. Keep folders like:
|
||||
|
||||
```text
|
||||
data/idas/bidding/folder_uploads/DAM/Single/Buy/C1036
|
||||
data/idas/bidding/folder_uploads/DAM/Single/Sell/C1036
|
||||
```
|
||||
|
||||
If `trading_market` is `ALL`, the folder root can contain market folders like `GDAM`, `DAM`, and `HPDAM`. The runner then looks under `Market/Single/Buy` or `Market/Single/Sell` and uses each TM folder name, such as `C1036`, to select the TM ID.
|
||||
|
||||
After opening each bidding screen, the runner waits for `bidding_screen_ready_wait_seconds` from `IDAS > bidding > timing`. The default is `10`.
|
||||
|
||||
Set `folder_upload_execution_mode` to `parallel` to run multiple TM folders at the same time. Parallel mode opens one browser per active TM folder up to `folder_upload_max_parallel_sessions`; each browser has its own manual CAPTCHA wait from `folder_upload_parallel_captcha_wait_seconds`, and is staggered by `folder_upload_parallel_login_stagger_seconds`.
|
||||
|
||||
For trading-member login, configure each folder owner under `users.trading_members`
|
||||
with `member_id` or `folder_upload_tm_ids` matching the folder names, for example
|
||||
`C1024`. The runner uses that mapping to choose the browser login for each folder
|
||||
and skips TM dropdown selection because the application locks the TM from login.
|
||||
For exchange login, configure at least as many `users.exchange_parallel` entries
|
||||
as `folder_upload_max_parallel_sessions`.
|
||||
|
||||
Folder-upload timing/status JSON logs are written under:
|
||||
|
||||
```text
|
||||
reports/logs/<RUN_ID>/test_idas_exchange_bidding_flow
|
||||
```
|
||||
|
||||
Run IDAS exchange matching:
|
||||
|
||||
```powershell
|
||||
pytest tests\regression\exchange\idas\matching -m matching -s --html=reports/html/matching_run.html --self-contained-html
|
||||
```
|
||||
|
||||
Set this in `config/runtime.json` before running:
|
||||
|
||||
```json
|
||||
"execution": {
|
||||
"login_role": "exchange",
|
||||
"product_family": "IDAS",
|
||||
"flow_scope": "matching"
|
||||
}
|
||||
```
|
||||
|
||||
Matching settings live here:
|
||||
|
||||
```json
|
||||
"matching": {
|
||||
"matching_product_code": "IDAS(I)",
|
||||
"matching_type": "Provisional Match",
|
||||
"source_types": [
|
||||
"Green",
|
||||
"Conventional",
|
||||
"HPDAM"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Run full IDAS e2e regression:
|
||||
|
||||
```powershell
|
||||
pytest tests\regression -m regression -s --html=reports/html/idas_e2e_run.html --self-contained-html
|
||||
```
|
||||
|
||||
Set this in `config/runtime.json`:
|
||||
|
||||
```json
|
||||
"execution": {
|
||||
"login_role": "exchange",
|
||||
"product_family": "IDAS",
|
||||
"flow_scope": "e2e"
|
||||
}
|
||||
```
|
||||
|
||||
This runs all active regression tests whose flow supports `e2e`. At present, that includes bidding and matching.
|
||||
|
||||
Run fake activity smoke test:
|
||||
|
||||
```powershell
|
||||
pytest tests\test_fake_activity.py -m fake_activity -s --html=reports/html/fake_activity_run.html --self-contained-html
|
||||
```
|
||||
|
||||
Use this for random menu/dropdown activity without saving data.
|
||||
|
||||
Collect regression tests without opening browser:
|
||||
|
||||
```powershell
|
||||
pytest tests\regression --collect-only -q
|
||||
```
|
||||
|
||||
Use this to confirm which regression tests exist.
|
||||
|
||||
## Placeholder Markers
|
||||
|
||||
These markers are registered in `pytest.ini`, but no runnable test files are currently present for them in this repo:
|
||||
|
||||
- `reports`
|
||||
- `order_book`
|
||||
- `trade_book`
|
||||
|
||||
When test files are added under `tests\regression`, use the same command shape:
|
||||
|
||||
```powershell
|
||||
pytest tests\regression -m reports -s --html=reports/html/reports_run.html --self-contained-html
|
||||
pytest tests\regression -m order_book -s --html=reports/html/order_book_run.html --self-contained-html
|
||||
pytest tests\regression -m trade_book -s --html=reports/html/trade_book_run.html --self-contained-html
|
||||
```
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"url": "https://utilsuat.pxil.in/Pxil/",
|
||||
"stagingurl": "https://utilseastern.pxil.in/Pxil/",
|
||||
"uaturl": "https://utilsuat.pxil.in/Pxil/",
|
||||
"login": {
|
||||
"username": "Tejas",
|
||||
"password": "Pxil@123"
|
||||
},
|
||||
"product": {
|
||||
"me_instance": "55"
|
||||
},
|
||||
"login_captcha_wait_seconds": 10,
|
||||
"login_post_submit_wait_seconds": 10,
|
||||
"step_delay_seconds": 1,
|
||||
"setup": {
|
||||
"run_session": false,
|
||||
"run_token": true,
|
||||
"session": {
|
||||
"start_offset_minutes": 7,
|
||||
"end_time": "20:30"
|
||||
}
|
||||
},
|
||||
"trading_token_idas": {
|
||||
"market_code": "NATIONAL",
|
||||
"product_type": "IDAS",
|
||||
"product_code": "IDAS4",
|
||||
"product_codes": "IDAS,IDASTEST01,IDASTEST02,IDASTEST03,IDASTEST04,IDASTEST05,IDASTEST06",
|
||||
"available_codes":",IDASTEST06,IDASTEST07,IDASTEST08,IDASTEST09,IDASTEST10,IDASTEST11,IDASTEST12,IDASTEST13,IDASTEST14,IDASTEST15,IDASTEST16,IDASTEST17,IDASTEST18,IDASTEST19,IDASTEST20,IDASTEST21,IDASTEST22,IDASTEST23,IDASTEST24,IDASTEST25,IDASTEST26,IDASTEST27,IDASTEST28,IDASTEST29,IDASTEST30",
|
||||
"delivery_offset_days": 1,
|
||||
"expiry_time": "20:30",
|
||||
"time_sessions": {
|
||||
"ORDER_NOR": {
|
||||
"start_offset_minutes": 5,
|
||||
"end_time": "19:30"
|
||||
},
|
||||
"ORDER_EXCH": {
|
||||
"start_time": "19:30",
|
||||
"end_time": "19:45"
|
||||
},
|
||||
"TRADE": {
|
||||
"start_time": "19:45",
|
||||
"end_time": "19:50"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# Config Layout
|
||||
|
||||
This folder controls environment, login, user, product, and flow selection for regression automation runs.
|
||||
|
||||
## Files
|
||||
|
||||
- `runtime.json`: the single regression config file. It contains environment settings, active execution settings, users, and product-wise sections such as `IDAS` and `RTM`.
|
||||
|
||||
## Main controls
|
||||
|
||||
In `runtime.json`:
|
||||
|
||||
- `environment.active_environment`: selects `UAT` or `Eastern` for the entire run.
|
||||
- `environment.portal_environments`: keeps the Pratyay and MCO URLs paired for each environment. Pratyay uses the role-specific exchange/trading-member URL; MCO uses its own portal URL.
|
||||
- `environment.base_url` and `environment.login_urls`: legacy fallbacks used only when no active portal environment profile is configured.
|
||||
- `users.mco`: credentials for the MCO portal. These are independent of `users.exchange`, which remains the Pratyay exchange login.
|
||||
- `IDAS.matching.portals.mco.stage`: currently `login_only`; the MCO test signs in and verifies `/Dashboard/Index` independently from the Pratyay matching flow.
|
||||
- `IDAS.active_selection.product_code`: the shared product used for bidding, Pratyay bid transfer, and initiate matching. UI suffixes such as `(I)` are resolved automatically.
|
||||
- `execution.login_role`: `exchange` or `trading_member`.
|
||||
- `execution.trading_member_key`: which trading member entry from `runtime.json > users.trading_members` should be used.
|
||||
- `execution.product_family`: product section to load from `runtime.json`, for example `IDAS` or `RTM`.
|
||||
- `execution.product_code`: selected product code, for example `IDAS1`, `IDAS2`, or `IDAS3`.
|
||||
- `execution.flow_scope`: `e2e`, `bidding`, `matching`, `reports`, `order_book`, or `trade_book`.
|
||||
- `users.exchange.username` and `users.exchange.password`: exchange login.
|
||||
- `users.trading_members.<key>.username` and `users.trading_members.<key>.password`: trading-member login.
|
||||
- `users.trading_members.<key>.member_id`: trading member / ME instance value used by flows.
|
||||
- `users.trading_members.<key>.folder_upload_tm_ids`: TM folder names this trading-member login is allowed to upload. Parallel folder upload uses this mapping to choose the right login for each TM folder.
|
||||
- `IDAS`, `RTM`, and future top-level product sections: product-wise defaults, available codes, supported flows, bidding config, matching config, and future product-specific controls.
|
||||
|
||||
Regression config now lives entirely inside `runtime.json`.
|
||||
@@ -0,0 +1,439 @@
|
||||
{
|
||||
"description": "Single regression config. Use execution to choose which product/flow to run, then edit the matching product section below for product-specific settings.",
|
||||
"environment": {
|
||||
"description": "Application URL selection. active_environment keeps the Pratyay and MCO portals on the same deployment.",
|
||||
"active_environment": "UAT",
|
||||
"active_environment_options": [
|
||||
"UAT",
|
||||
"Eastern"
|
||||
],
|
||||
"portal_environments": {
|
||||
"UAT": {
|
||||
"pratyay": {
|
||||
"exchange": "https://tradinguat.pxil.in/Public/Loginadmin.aspx",
|
||||
"trading_member": "https://tradinguat.pxil.in/Public/Login.aspx"
|
||||
},
|
||||
"mco": "https://membersuat.pxil.in"
|
||||
},
|
||||
"Eastern": {
|
||||
"pratyay": {
|
||||
"exchange": "https://tradingeastern.pxil.in/Public/Loginadmin.aspx",
|
||||
"trading_member": "https://tradingeastern.pxil.in/Public/Login.aspx"
|
||||
},
|
||||
"mco": "https://memberseastern.pxil.in"
|
||||
}
|
||||
},
|
||||
"base_url": "https://tradinguat.pxil.in/Public/Login.aspx",
|
||||
"login_urls": {
|
||||
"exchange": "https://tradinguat.pxil.in/Public/Loginadmin.aspx",
|
||||
"trading_member": "https://tradinguat.pxil.in/Public/Login.aspx"
|
||||
},
|
||||
"available_base_urls": {
|
||||
"UAT": "https://tradinguat.pxil.in/Public/Login.aspx",
|
||||
"Staging": "https://tradingeastern.pxil.in/Public/Loginadmin.aspx",
|
||||
"Pre Prod": ""
|
||||
}
|
||||
},
|
||||
"execution": {
|
||||
"description": "Top-level run controls used by the regression suite.",
|
||||
"login_role": "trading_member",
|
||||
"login_role_options": [
|
||||
"exchange",
|
||||
"trading_member"
|
||||
],
|
||||
"trading_member_key": "tm_55",
|
||||
"product_family": "IDAS",
|
||||
"product_family_options": [
|
||||
"IDAS",
|
||||
"RTM"
|
||||
],
|
||||
"flow_scope": "bidding",
|
||||
"flow_scope_options": [
|
||||
"e2e",
|
||||
"bidding",
|
||||
"matching",
|
||||
"reports",
|
||||
"order_book",
|
||||
"trade_book"
|
||||
],
|
||||
"product_code_override": "",
|
||||
"product_code_override_description": "Optional. Leave blank to use the active_selection.product_code inside the chosen product section."
|
||||
},
|
||||
"timing": {
|
||||
"description": "General wait controls used across regression flows.",
|
||||
"login_captcha_wait_seconds": 10,
|
||||
"login_post_submit_wait_seconds": 10,
|
||||
"step_delay_seconds": 5
|
||||
},
|
||||
"users": {
|
||||
"description": "Regression login credentials used when execution.login_role changes.",
|
||||
"exchange": {
|
||||
"username": "Tejas",
|
||||
"password": "Pxil@123"
|
||||
},
|
||||
"mco": {
|
||||
"description": "MCO uses credentials independent of the Pratyay exchange login.",
|
||||
"username": "superadmin",
|
||||
"password": "SuperAdmin@123"
|
||||
},
|
||||
"exchange_parallel": [
|
||||
{
|
||||
"label": "Parallel Exchange 1",
|
||||
"username": "Kshipra",
|
||||
"password": "Pxil@123"
|
||||
},
|
||||
{
|
||||
"label": "Parallel Exchange 2",
|
||||
"username": "Mahimaimary",
|
||||
"password": "Pxil@123"
|
||||
}
|
||||
],
|
||||
"trading_members": {
|
||||
"tm_55": {
|
||||
"member_id": "C1024",
|
||||
"label": "Default Trading Member",
|
||||
"username": "TRDC10240001",
|
||||
"password": "Pxil@123",
|
||||
"folder_upload_tm_ids": [
|
||||
"C1024"
|
||||
]
|
||||
},
|
||||
"tm_c1026": {
|
||||
"member_id": "C1026",
|
||||
"label": "Folder Upload TM C1026",
|
||||
"username": "TRDC10260001",
|
||||
"password": "Pxil@123",
|
||||
"folder_upload_tm_ids": [
|
||||
"C1026"
|
||||
]
|
||||
},
|
||||
"tm_c1015": {
|
||||
"member_id": "C1015",
|
||||
"label": "Folder Upload TM C1015",
|
||||
"username": "TRDC10150001",
|
||||
"password": "Pxil@123",
|
||||
"folder_upload_tm_ids": [
|
||||
"C1015"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"IDAS": {
|
||||
"description": "IDAS product-wise regression config.",
|
||||
"active_selection": {
|
||||
"description": "This block controls what the bidding and matching pages select for IDAS.",
|
||||
"product_code": "IDASTEST05",
|
||||
"product_code_options": [
|
||||
"IDAS",
|
||||
"IDAS1",
|
||||
"IDAS2",
|
||||
"IDAS3",
|
||||
"IDAS4"
|
||||
],
|
||||
"trading_market": "DAM",
|
||||
"trading_markets": [
|
||||
"GDAM",
|
||||
"DAM"
|
||||
],
|
||||
"trading_market_options": [
|
||||
"ALL",
|
||||
"GDAM",
|
||||
"DAM",
|
||||
"HPDAM"
|
||||
],
|
||||
"bidding_screen": "single_and_block_bid",
|
||||
"bidding_screen_options": [
|
||||
"single_bid",
|
||||
"block_bid",
|
||||
"single_and_block_bid"
|
||||
]
|
||||
},
|
||||
"defaults": {
|
||||
"description": "Fallbacks used if active_selection is incomplete.",
|
||||
"market_code": "NATIONAL",
|
||||
"product_type": "IDAS",
|
||||
"product_code": "IDAS",
|
||||
"trading_market": "DAM",
|
||||
"bidding_screen": "single_bid"
|
||||
},
|
||||
"flows": {
|
||||
"description": "Supported IDAS regression flow groups.",
|
||||
"e2e": {
|
||||
"description": "Complete regression flow across all enabled modules."
|
||||
},
|
||||
"bidding": {
|
||||
"description": "Run only bidding flows."
|
||||
},
|
||||
"matching": {
|
||||
"description": "Run only matching flows."
|
||||
},
|
||||
"reports": {
|
||||
"description": "Run only report download flows."
|
||||
},
|
||||
"order_book": {
|
||||
"description": "Run only order book flows."
|
||||
},
|
||||
"trade_book": {
|
||||
"description": "Run only trade book flows."
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"description": "Product-level setup controls.",
|
||||
"session": {
|
||||
"enabled": true
|
||||
},
|
||||
"token": {
|
||||
"enabled": true,
|
||||
"market_code": "NATIONAL",
|
||||
"product_type": "IDAS"
|
||||
}
|
||||
},
|
||||
"bidding": {
|
||||
"description": "All IDAS bidding-page related inputs and waits.",
|
||||
"entry_method": {
|
||||
"bid_entry_method": "folder_upload",
|
||||
"available_bid_entry_methods": [
|
||||
"excel_grid",
|
||||
"json_grid",
|
||||
"file_upload",
|
||||
"folder_upload"
|
||||
],
|
||||
"file_upload_mode": "batch",
|
||||
"folder_upload_execution_mode": "sequential",
|
||||
"folder_upload_execution_mode_options": [
|
||||
"sequential",
|
||||
"parallel"
|
||||
],
|
||||
"auto_submit": true,
|
||||
"block_bid": {
|
||||
"block_upload_method": "folder_upload",
|
||||
"block_upload_method_options": [
|
||||
"file_upload",
|
||||
"folder_upload"
|
||||
],
|
||||
"product_code": "IDASTEST05",
|
||||
"product_container_id": "s2id_ddlproduct3",
|
||||
"segment_container_id": "s2id_ddlSegment3",
|
||||
"tm_container_id": "s2id_ddlTMID3",
|
||||
"block_upload_success_timeout_seconds": 5400,
|
||||
"block_upload_success_poll_seconds": 2,
|
||||
"block_upload_progress_log_interval_seconds": 60
|
||||
}
|
||||
},
|
||||
"data_paths": {
|
||||
"input_root": "data/idas/bidding/single_bid",
|
||||
"json_input_root": "data/idas/bidding/json",
|
||||
"upload_input_root": "data/idas/bidding/uploads",
|
||||
"folder_upload_root": "data/idas/bidding/folder_uploads",
|
||||
"folder_upload_sides": [
|
||||
"BUYSELL"
|
||||
]
|
||||
},
|
||||
"selection": {
|
||||
"active_entry_key": "gujc10010001"
|
||||
},
|
||||
"timing": {
|
||||
"upload_to_submit_delay_seconds": 2,
|
||||
"submit_to_fetch_delay_seconds": 2,
|
||||
"folder_upload_progress_timeout_seconds": 5400,
|
||||
"folder_upload_progress_poll_seconds": 2,
|
||||
"folder_upload_progress_log_interval_seconds": 60,
|
||||
"folder_upload_continue_when_last_file_complete": true,
|
||||
"folder_upload_keep_alive_interval_seconds": 30,
|
||||
"folder_upload_max_parallel_sessions": 2,
|
||||
"folder_upload_parallel_login_stagger_seconds": 20,
|
||||
"folder_upload_parallel_captcha_wait_seconds": 60,
|
||||
"folder_upload_fetch_timeout_seconds": 30,
|
||||
"bidding_screen_ready_wait_seconds": 15,
|
||||
"product_to_tm_delay_seconds": 15,
|
||||
"tm_to_tac_delay_seconds": 3,
|
||||
"tm_timeout_seconds": 30,
|
||||
"tm_attempts": 3,
|
||||
"tac_timeout_seconds": 15,
|
||||
"tac_attempts": 2
|
||||
},
|
||||
"excel_layout": {
|
||||
"column_start": "B",
|
||||
"market_price_rows": {
|
||||
"GDAM": 2,
|
||||
"DAM": 3,
|
||||
"HPDAM": 4
|
||||
},
|
||||
"common_data_start_row": 6
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"key": "gujc10010001",
|
||||
"tm_id": "C1001",
|
||||
"tac_id": "GUJC10010001",
|
||||
"excel_file": "GUJC10010001.xlsx",
|
||||
"json_file": "GUJC10010001.json",
|
||||
"upload_file": "GUJC10010001.xlsx",
|
||||
"sheet_name": "Sheet1",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "chhc10110001",
|
||||
"tm_id": "C1011",
|
||||
"tac_id": "CHHC10110001",
|
||||
"excel_file": "CHHC10110001.xlsx",
|
||||
"json_file": "CHHC10110001.json",
|
||||
"upload_file": "CHHC10110001.xlsx",
|
||||
"sheet_name": "Sheet1",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"key": "apts10240001",
|
||||
"tm_id": "S1024",
|
||||
"tac_id": "APTS10240001",
|
||||
"excel_file": "APTS10240001.xlsx",
|
||||
"json_file": "APTS10240001.json",
|
||||
"upload_file": "APTS10240001.xlsx",
|
||||
"sheet_name": "Sheet1",
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"matching": {
|
||||
"description": "IDAS order matching split between Pratyay preparation and the separate MCO market-coupling portal.",
|
||||
"portals": {
|
||||
"pratyay": {
|
||||
"enabled": true,
|
||||
"description": "Existing exchange-side provisional matching and flow-request preparation."
|
||||
},
|
||||
"mco": {
|
||||
"enabled": true,
|
||||
"stage": "login_only",
|
||||
"description": "MCO login and dashboard verification are active. Matching steps will be added as their pages are captured."
|
||||
}
|
||||
},
|
||||
"matching_product_code": "",
|
||||
"matching_product_code_description": "Legacy fallback only. Bid transfer and initiate matching use IDAS.active_selection.product_code.",
|
||||
"matching_type": "Provisional Match",
|
||||
"source_types": [
|
||||
"Green",
|
||||
"Conventional",
|
||||
"HPDAM"
|
||||
],
|
||||
"bid_transfer_markets": [
|
||||
"GDAM",
|
||||
"DAM",
|
||||
"HPDAM"
|
||||
],
|
||||
"accepted_bid_transfer_statuses": [
|
||||
"ACCEPTED",
|
||||
"PARTIALLY_ACCEPTED",
|
||||
"NO_BIDS"
|
||||
],
|
||||
"initiation_mode": "provisional_match",
|
||||
"timing": {
|
||||
"bid_transfer_timeout_seconds": 30,
|
||||
"check_status_interval_seconds": 2,
|
||||
"check_status_timeout_seconds": 120,
|
||||
"flow_request_success_timeout_seconds": 30,
|
||||
"download_timeout_seconds": 60
|
||||
},
|
||||
"downloads": {
|
||||
"download_folder_name": "matching_files"
|
||||
}
|
||||
},
|
||||
"available_product_codes": [
|
||||
"IDAS",
|
||||
"IDAS1",
|
||||
"IDAS2",
|
||||
"IDAS3",
|
||||
"IDAS4"
|
||||
],
|
||||
"available_trading_markets": [
|
||||
"GDAM",
|
||||
"DAM",
|
||||
"HPDAM"
|
||||
],
|
||||
"available_bidding_screens": [
|
||||
"single_bid",
|
||||
"block_bid",
|
||||
"single_and_block_bid"
|
||||
]
|
||||
},
|
||||
"RTM": {
|
||||
"description": "Placeholder section for RTM product-wise regression config.",
|
||||
"active_selection": {
|
||||
"description": "Fill this section when RTM regression flows are added.",
|
||||
"product_code": "RTM",
|
||||
"product_code_options": [
|
||||
"RTM"
|
||||
],
|
||||
"trading_market": "",
|
||||
"trading_market_options": [],
|
||||
"bidding_screen": "single_bid",
|
||||
"bidding_screen_options": [
|
||||
"single_bid",
|
||||
"block_bid"
|
||||
]
|
||||
},
|
||||
"defaults": {
|
||||
"description": "Fallbacks used if active_selection is incomplete.",
|
||||
"market_code": "NATIONAL",
|
||||
"product_type": "RTM",
|
||||
"product_code": "RTM",
|
||||
"trading_market": "",
|
||||
"bidding_screen": "single_bid"
|
||||
},
|
||||
"flows": {
|
||||
"description": "Supported RTM regression flow groups.",
|
||||
"e2e": {
|
||||
"description": "Complete regression flow across all enabled modules."
|
||||
},
|
||||
"bidding": {
|
||||
"description": "Run only bidding flows."
|
||||
},
|
||||
"matching": {
|
||||
"description": "Run only matching flows."
|
||||
},
|
||||
"reports": {
|
||||
"description": "Run only report download flows."
|
||||
},
|
||||
"order_book": {
|
||||
"description": "Run only order book flows."
|
||||
},
|
||||
"trade_book": {
|
||||
"description": "Run only trade book flows."
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"description": "Product-level setup controls.",
|
||||
"session": {
|
||||
"enabled": false
|
||||
},
|
||||
"token": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"bidding": {
|
||||
"description": "RTM bidding config placeholder.",
|
||||
"entry_method": {},
|
||||
"data_paths": {},
|
||||
"selection": {},
|
||||
"timing": {},
|
||||
"excel_layout": {},
|
||||
"entries": []
|
||||
},
|
||||
"matching": {
|
||||
"description": "RTM matching config placeholder.",
|
||||
"matching_product_code": "",
|
||||
"matching_type": "",
|
||||
"source_types": [],
|
||||
"initiation_mode": "",
|
||||
"timing": {},
|
||||
"downloads": {}
|
||||
},
|
||||
"available_product_codes": [
|
||||
"RTM"
|
||||
],
|
||||
"available_trading_markets": [],
|
||||
"available_bidding_screens": [
|
||||
"single_bid",
|
||||
"block_bid"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# IDAS Input Data
|
||||
|
||||
Keep bidding input Excel files here instead of embedding bid values in config.
|
||||
|
||||
## Suggested structure
|
||||
|
||||
- `data/idas/bidding/single_bid/`
|
||||
- `data/idas/bidding/block_bid/`
|
||||
|
||||
## Naming
|
||||
|
||||
Name each file by TAC ID when possible:
|
||||
|
||||
- `GUJC10010001.xlsx`
|
||||
- `CHHC10110001.xlsx`
|
||||
- `APTS10240001.xlsx`
|
||||
|
||||
## Config link
|
||||
|
||||
The run manifest lives in:
|
||||
|
||||
- `config/products/idas.json`
|
||||
|
||||
Each manifest entry points to:
|
||||
|
||||
- `tm_id`
|
||||
- `tac_id`
|
||||
- `excel_file`
|
||||
- optional `sheet_name`
|
||||
|
||||
This lets the automation run one TAC after another without hardcoding bid data into config.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Bidding Input Files
|
||||
|
||||
Place Excel input files for IDAS bidding here.
|
||||
|
||||
- `single_bid/` for normal single-bid templates
|
||||
- `block_bid/` for future block-bid templates
|
||||
- `json/` for direct structured bid payloads
|
||||
- `uploads/` for files sent through the application's `Upload` flow
|
||||
- `folder_uploads/` for folder-upload scenarios grouped by market, screen, side, and TM ID
|
||||
|
||||
For folder uploads, `folder_upload_sides` can be set to `Buy`, `Sell`, or `BUYSELL`.
|
||||
`BUYSELL` uses a `BUYSELL` side folder when it exists; otherwise it runs the `Buy`
|
||||
and `Sell` side folders for the selected market and screen.
|
||||
|
||||
Large folder uploads can take longer than normal grid entry. The regression config
|
||||
uses `folder_upload_progress_timeout_seconds` for the maximum upload wait and
|
||||
`folder_upload_keep_alive_interval_seconds` for periodic browser activity during
|
||||
that wait.
|
||||
If `folder_upload_continue_when_last_file_complete` is true, the runner can
|
||||
continue when the last uploaded file reaches 100%, even if earlier rows remain
|
||||
below 100% because of errors such as bid-data validation. Incomplete rows are
|
||||
logged with file name, progress, and remarks.
|
||||
|
||||
Folder uploads run sequentially by default. Set `folder_upload_execution_mode` to
|
||||
`parallel` to run multiple TM folders at the same time, using
|
||||
`folder_upload_max_parallel_sessions` to control the number of browsers and
|
||||
`folder_upload_parallel_login_stagger_seconds` to stagger manual CAPTCHA entry.
|
||||
Use `folder_upload_parallel_captcha_wait_seconds` to control how long each
|
||||
parallel browser waits before submitting login after you type the CAPTCHA.
|
||||
Parallel mode uses one login per browser slot. With exchange login it uses
|
||||
`users.exchange_parallel`. With trading-member login it uses the entries under
|
||||
`users.trading_members`, and each member must list the TM folders it can upload
|
||||
with `folder_upload_tm_ids` or a matching `member_id`. Trading-member runs skip
|
||||
the TM dropdown because the application locks the TM to the logged-in user.
|
||||
|
||||
Config can now choose one of these bid-entry methods:
|
||||
|
||||
- `excel_grid`
|
||||
- `json_grid`
|
||||
- `file_upload`
|
||||
- `folder_upload`
|
||||
|
||||
For block-bid bulk upload, set `active_selection.bidding_screen` to `block_bid`
|
||||
and choose `IDAS > bidding > entry_method > block_bid > block_upload_method` as
|
||||
`file_upload` or `folder_upload`. Folder upload uses the same grouped directory
|
||||
shape, with `Block` instead of `Single`, for example:
|
||||
|
||||
```text
|
||||
data/idas/bidding/folder_uploads/DAM/Block/BUYSELL/C1032
|
||||
```
|
||||
|
||||
Set `active_selection.bidding_screen` to `single_and_block_bid` to run both
|
||||
folder-upload screens in one browser login. The runner processes `single_bid`
|
||||
first, then navigates directly to block bulk upload for `block_bid`.
|
||||
|
||||
After the block folder is selected, the runner accepts the browser upload
|
||||
confirmation when WebDriver exposes it, then waits until the page success message
|
||||
changes to `FOLDER_UPLOADED_SUCCESSFULLY`. Timing is logged from file selection
|
||||
through the success message. For large load-test folders, tune
|
||||
`block_upload_success_timeout_seconds`, `block_upload_success_poll_seconds`, and
|
||||
`block_upload_progress_log_interval_seconds` in `runtime.json`.
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+14
@@ -0,0 +1,14 @@
|
||||
[pytest]
|
||||
markers =
|
||||
session: session creation
|
||||
trading_token: trading token creation
|
||||
fake_activity: random menu and dropdown activity without saving
|
||||
regression: product-wise end-to-end regression flow
|
||||
exchange: flow executed with exchange login
|
||||
trading_member: flow executed with trading member login
|
||||
bidding: bidding flow coverage
|
||||
matching: matching flow coverage
|
||||
mco: MCO market-coupling portal tests
|
||||
reports: report download coverage
|
||||
order_book: order book coverage
|
||||
trade_book: trade book coverage
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Core test runner
|
||||
pytest==8.4.1
|
||||
pytest-html==4.2.0
|
||||
|
||||
# Browser automation
|
||||
selenium==4.34.2
|
||||
@@ -0,0 +1,14 @@
|
||||
from selenium import webdriver
|
||||
|
||||
|
||||
def create_chrome_driver():
|
||||
|
||||
options = webdriver.ChromeOptions()
|
||||
options.add_argument("--disable-background-timer-throttling")
|
||||
options.add_argument("--disable-backgrounding-occluded-windows")
|
||||
options.add_argument("--disable-renderer-backgrounding")
|
||||
options.add_argument("--disable-features=CalculateNativeWinOcclusion")
|
||||
options.add_argument("--disable-hang-monitor")
|
||||
options.add_argument("--disable-popup-blocking")
|
||||
|
||||
return webdriver.Chrome(options=options)
|
||||
@@ -0,0 +1,62 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
CONFIG_PATH = Path(__file__).resolve().parent.parent / "config.json"
|
||||
|
||||
|
||||
def load_config():
|
||||
|
||||
with CONFIG_PATH.open("r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def get_step_delay_seconds(default=5):
|
||||
|
||||
config = load_config()
|
||||
return config.get("step_delay_seconds", default)
|
||||
|
||||
|
||||
def get_login_captcha_wait_seconds(default=20):
|
||||
|
||||
config = load_config()
|
||||
return config.get("login_captcha_wait_seconds", default)
|
||||
|
||||
|
||||
def get_login_post_submit_wait_seconds(default=10):
|
||||
|
||||
config = load_config()
|
||||
return config.get("login_post_submit_wait_seconds", default)
|
||||
|
||||
|
||||
def is_setup_session_enabled(default=True):
|
||||
|
||||
config = load_config()
|
||||
return config.get("setup", {}).get("run_session", default)
|
||||
|
||||
|
||||
def is_setup_token_enabled(default=True):
|
||||
|
||||
config = load_config()
|
||||
return config.get("setup", {}).get("run_token", default)
|
||||
|
||||
|
||||
def is_combined_setup_enabled():
|
||||
|
||||
config = load_config()
|
||||
setup = config.get("setup", {})
|
||||
return setup.get("run_session", True) and setup.get("run_token", True)
|
||||
|
||||
|
||||
def parse_product_codes(value):
|
||||
|
||||
if value is None:
|
||||
return []
|
||||
|
||||
if isinstance(value, str):
|
||||
return [code.strip() for code in value.split(",") if code.strip()]
|
||||
|
||||
if isinstance(value, list):
|
||||
return [str(code).strip() for code in value if str(code).strip()]
|
||||
|
||||
return [str(value).strip()]
|
||||
@@ -0,0 +1,242 @@
|
||||
import pytest
|
||||
import os
|
||||
import json
|
||||
|
||||
from tests.browser import create_chrome_driver
|
||||
|
||||
|
||||
def get_run_id(config):
|
||||
|
||||
run_id = getattr(config, "_pxil_run_id", None)
|
||||
if run_id:
|
||||
return run_id
|
||||
|
||||
base_folder = os.path.join("reports", "screenshots")
|
||||
os.makedirs(base_folder, exist_ok=True)
|
||||
run_prefix = _get_run_prefix(config)
|
||||
|
||||
existing = [
|
||||
name for name in os.listdir(base_folder)
|
||||
if name.startswith(f"{run_prefix}_")
|
||||
]
|
||||
|
||||
if not existing:
|
||||
next_number = 1
|
||||
else:
|
||||
numbers = [
|
||||
int(name.split("_")[-1])
|
||||
for name in existing
|
||||
if name.split("_")[-1].isdigit()
|
||||
]
|
||||
next_number = max(numbers) + 1
|
||||
|
||||
run_id = f"{run_prefix}_{next_number:03d}"
|
||||
config._pxil_run_id = run_id
|
||||
return run_id
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
|
||||
run_id = get_run_id(config)
|
||||
|
||||
if config.pluginmanager.hasplugin("html"):
|
||||
reports_dir = os.path.abspath(os.path.join("reports", "html"))
|
||||
os.makedirs(reports_dir, exist_ok=True)
|
||||
config.option.htmlpath = os.path.join(reports_dir, f"{run_id}.html")
|
||||
config.option.self_contained_html = True
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# WebDriver Fixture
|
||||
# -----------------------------
|
||||
@pytest.fixture
|
||||
def driver():
|
||||
|
||||
driver = create_chrome_driver()
|
||||
driver.maximize_window()
|
||||
|
||||
yield driver
|
||||
|
||||
driver.quit()
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Generate Incremental Test ID
|
||||
# -----------------------------
|
||||
@pytest.fixture
|
||||
def test_context(request):
|
||||
|
||||
test_id = get_run_id(request.config)
|
||||
test_name = request.node.name
|
||||
|
||||
folder = os.path.abspath(
|
||||
os.path.join("reports", "screenshots", test_id, test_name)
|
||||
)
|
||||
log_folder = os.path.abspath(
|
||||
os.path.join("reports", "logs", test_id, test_name)
|
||||
)
|
||||
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
os.makedirs(log_folder, exist_ok=True)
|
||||
|
||||
print(f"\nTEST ID: {test_id}")
|
||||
|
||||
return {
|
||||
"test_id": test_id,
|
||||
"folder": folder,
|
||||
"log_folder": log_folder,
|
||||
"steps": [],
|
||||
"extra_folders": [],
|
||||
}
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Attach Screenshot Folder Link
|
||||
# -----------------------------
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
|
||||
outcome = yield
|
||||
report = outcome.get_result()
|
||||
|
||||
if report.when == "call":
|
||||
|
||||
context = item.funcargs.get("test_context", None)
|
||||
|
||||
if context:
|
||||
|
||||
folder = context["folder"]
|
||||
steps = context.get("steps", [])
|
||||
|
||||
if report.failed:
|
||||
driver = item.funcargs.get("driver", None)
|
||||
failure_index = len(steps) + 1
|
||||
failure_name = "failure_state"
|
||||
failure_filename = f"{failure_index:02d}_{failure_name}.png"
|
||||
failure_path = os.path.join(folder, failure_filename)
|
||||
|
||||
if driver:
|
||||
try:
|
||||
driver.save_screenshot(failure_path)
|
||||
steps.append(
|
||||
{
|
||||
"index": failure_index,
|
||||
"name": failure_name,
|
||||
"path": failure_path,
|
||||
"status": "failed",
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if item.config.pluginmanager.hasplugin("html"):
|
||||
|
||||
from pytest_html import extras
|
||||
|
||||
extras_list = getattr(report, "extras", [])
|
||||
|
||||
for step in steps:
|
||||
extras_list.append(
|
||||
extras.url(
|
||||
_as_file_url(step["path"]),
|
||||
name=f"Step {step['index']:02d} [{step['status']}] {step['name']}"
|
||||
)
|
||||
)
|
||||
|
||||
if steps:
|
||||
extras_list.append(
|
||||
extras.html(_build_steps_html(steps))
|
||||
)
|
||||
|
||||
extras_list.append(
|
||||
extras.url(_as_file_url(folder), name="Screenshots Folder")
|
||||
)
|
||||
|
||||
log_folder = context.get("log_folder")
|
||||
if log_folder:
|
||||
extras_list.append(
|
||||
extras.url(_as_file_url(log_folder), name="Logs Folder")
|
||||
)
|
||||
|
||||
for extra_folder in context.get("extra_folders", []):
|
||||
extras_list.append(
|
||||
extras.url(
|
||||
_as_file_url(extra_folder["path"]),
|
||||
name=extra_folder["name"]
|
||||
)
|
||||
)
|
||||
|
||||
report.extras = extras_list
|
||||
|
||||
|
||||
def _as_file_url(path):
|
||||
|
||||
absolute_path = os.path.abspath(path).replace("\\", "/")
|
||||
return f"file:///{absolute_path}"
|
||||
|
||||
|
||||
def _build_steps_html(steps):
|
||||
|
||||
rows = []
|
||||
for step in steps:
|
||||
step_url = _as_file_url(step["path"])
|
||||
rows.append(
|
||||
"<tr>"
|
||||
f"<td>{step['index']:02d}</td>"
|
||||
f"<td>{step['status'].capitalize()}</td>"
|
||||
f"<td>{step['name']}</td>"
|
||||
f"<td><a href=\"{step_url}\">Open Screenshot</a></td>"
|
||||
"</tr>"
|
||||
)
|
||||
|
||||
return (
|
||||
"<div>"
|
||||
"<h4>Step Details</h4>"
|
||||
"<table border='1' cellpadding='4' cellspacing='0' "
|
||||
"style='border-collapse: collapse; margin-top: 8px;'>"
|
||||
"<thead><tr><th>#</th><th>Status</th><th>Step</th><th>Screenshot</th></tr></thead>"
|
||||
f"<tbody>{''.join(rows)}</tbody>"
|
||||
"</table>"
|
||||
"</div>"
|
||||
)
|
||||
|
||||
|
||||
def _get_run_prefix(config):
|
||||
|
||||
invocation_args = " ".join(str(arg) for arg in config.invocation_params.args).lower()
|
||||
|
||||
if "tests/setup/sessions" in invocation_args or "test_session.py" in invocation_args:
|
||||
return "PXIL_SESSION"
|
||||
|
||||
if "tests/setup/tokens" in invocation_args or "test_trading_token.py" in invocation_args:
|
||||
return "PXIL_TOKEN"
|
||||
|
||||
runtime_path = os.path.abspath(os.path.join("config", "runtime.json"))
|
||||
if os.path.exists(runtime_path):
|
||||
try:
|
||||
with open(runtime_path, "r", encoding="utf-8") as runtime_file:
|
||||
runtime_config = json.load(runtime_file)
|
||||
|
||||
execution = runtime_config.get("execution", {})
|
||||
product_family = execution.get("product_family", "regression").upper()
|
||||
flow_scope = execution.get("flow_scope", "run").upper()
|
||||
return f"{product_family}_{flow_scope}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if "matching" in invocation_args:
|
||||
return "REGRESSION_MATCHING"
|
||||
|
||||
if "bidding" in invocation_args:
|
||||
return "REGRESSION_BIDDING"
|
||||
|
||||
if "reports" in invocation_args:
|
||||
return "REGRESSION_REPORTS"
|
||||
|
||||
if "order_book" in invocation_args:
|
||||
return "REGRESSION_ORDER_BOOK"
|
||||
|
||||
if "trade_book" in invocation_args:
|
||||
return "REGRESSION_TRADE_BOOK"
|
||||
|
||||
return "PXIL_RUN"
|
||||
@@ -0,0 +1,393 @@
|
||||
"""
|
||||
generate_bid_json.py
|
||||
====================
|
||||
Bulk JSON generator for bid submission payloads.
|
||||
|
||||
FOLDER STRUCTURE (input_root):
|
||||
input_root/
|
||||
<segment>/ e.g. GDAM | DAM | HPDAM
|
||||
<order_type>/ e.g. Single | Block
|
||||
<order_side>/ e.g. Buy | Sell
|
||||
<TM_folder>/ e.g. C1001 → becomes userId AND tmId
|
||||
<tacId>.xlsx ← one file per TAC / bid sheet
|
||||
|
||||
SEGMENT → JSON "segment" MAPPING:
|
||||
GDAM → "GREEN"
|
||||
DAM → "CONVENTIONAL"
|
||||
HPDAM → "HPDAM"
|
||||
|
||||
SEGMENT → JSON order "source" MAPPING:
|
||||
GDAM → "SL"
|
||||
DAM → "CO"
|
||||
HPDAM → "HP"
|
||||
|
||||
ORDER SIDE → JSON "orderSide" MAPPING:
|
||||
Buy → "B"
|
||||
Sell → "S"
|
||||
|
||||
ORDER TYPE → JSON "orderType" MAPPING:
|
||||
Single → "N"
|
||||
Block → "B"
|
||||
|
||||
EXCEL LAYOUT (every .xlsx):
|
||||
Row 1 : "Select Source" | "Order 1" | "Order 2" ... "Order N"
|
||||
Row 2 : "<label>" | <price1> | <price2> ... <priceN>
|
||||
Row 3+: "HH:MM-HH:MM [slot]" | <qty1> | <qty2> ...
|
||||
(rows where ALL qty cells are blank are skipped)
|
||||
|
||||
OUTPUT:
|
||||
output_root/
|
||||
<segment>/
|
||||
<order_type>/
|
||||
<order_side>/
|
||||
<TM_folder>/
|
||||
<tacId>_bid.json
|
||||
|
||||
CONFIGURABLE via CLI (or edit DEFAULTS below):
|
||||
--input_root path to input folder tree
|
||||
--output_root path where JSON tree is written
|
||||
--product_code e.g. "IDAS(01)"
|
||||
--password password applied to every payload
|
||||
--ocf_opted Y or N
|
||||
--ocf_premium_disc integer
|
||||
--ocf_qty integer
|
||||
|
||||
FIXED / AUTO-DERIVED:
|
||||
transcode = "3040" (always)
|
||||
requestType = "N" (always)
|
||||
productType = "I" (always)
|
||||
userId = TM folder name
|
||||
tmId = TM folder name
|
||||
segment = derived from segment folder name (see mapping above)
|
||||
orderSide = derived from order_side folder name
|
||||
orderType = derived from order_type folder name
|
||||
deliveryDate = tomorrow's date (auto-calculated at run time)
|
||||
|
||||
USAGE:
|
||||
python generate_bid_json.py \\
|
||||
--input_root ./input \\
|
||||
--output_root ./output \\
|
||||
--product_code "IDAS(01)" \\
|
||||
--password "your_password"
|
||||
|
||||
Or just edit DEFAULTS below and run:
|
||||
python generate_bid_json.py
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from openpyxl import load_workbook
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# DEFAULTS – edit here or override via CLI
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
DEFAULTS = {
|
||||
"input_root": "D:/Load testing input json/Input scenario upload files",
|
||||
"output_root": "D:/Load testing input json/UAT_Team_10%_Load_File",
|
||||
"product_code": "IDASTEST25",
|
||||
"password": "CHANGE_ME",
|
||||
"ocf_opted": "N",
|
||||
"ocf_premium_disc": 0,
|
||||
"ocf_qty": 0,
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# FIXED CONSTANTS
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
TRANSCODE = "3040"
|
||||
REQUEST_TYPE = "N"
|
||||
PRODUCT_TYPE = "I"
|
||||
|
||||
# Segment folder name → JSON segment value
|
||||
SEGMENT_MAP = {
|
||||
"GDAM": "GREEN",
|
||||
"DAM": "Conventional",
|
||||
"HPDAM": "HPDAM",
|
||||
}
|
||||
|
||||
# Order-type folder name → JSON orderType value
|
||||
ORDER_TYPE_MAP = {
|
||||
"Single": "N",
|
||||
"Block": "B",
|
||||
}
|
||||
|
||||
# Order-side folder name → JSON orderSide value
|
||||
ORDER_SIDE_MAP = {
|
||||
"Buy": "B",
|
||||
"Sell": "S",
|
||||
}
|
||||
|
||||
# Segment folder name → JSON order "source" value
|
||||
SOURCE_MAP = {
|
||||
"GDAM": "SL",
|
||||
"DAM": "CO",
|
||||
"HPDAM": "HP",
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# HELPERS
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def tomorrow_str() -> str:
|
||||
"""Return tomorrow's date as 'DD-Mon-YYYY', e.g. '18-Jun-2026'."""
|
||||
d = date.today() + timedelta(days=1)
|
||||
# %-d is Linux-only; %#d is Windows-only — use manual strip for cross-platform
|
||||
return d.strftime("%d-%b-%Y").lstrip("0")
|
||||
|
||||
|
||||
def parse_excel(xlsx_path: Path, order_type_val: str, order_side_val: str, source_val: str, seg_folder: str, cfg: dict) -> list[dict]:
|
||||
"""
|
||||
Parse one bid Excel file and return a list of order dicts.
|
||||
tacId is the filename stem (e.g. 'MCOC100500002').
|
||||
"""
|
||||
wb = load_workbook(str(xlsx_path), read_only=True, data_only=True)
|
||||
ws = wb.active
|
||||
rows = list(ws.iter_rows(values_only=True))
|
||||
|
||||
if len(rows) < 3:
|
||||
raise ValueError(f"File has fewer than 3 rows: {xlsx_path}")
|
||||
|
||||
# Row 2 (index 1): price row
|
||||
price_row = rows[1]
|
||||
prices = []
|
||||
for cell in price_row[1:]:
|
||||
if cell is None or cell == "":
|
||||
break
|
||||
prices.append(int(round(float(cell))))
|
||||
|
||||
if not prices:
|
||||
raise ValueError(f"No price columns found in row 2 of: {xlsx_path}")
|
||||
|
||||
tac_id = xlsx_path.stem
|
||||
orders = []
|
||||
|
||||
for row in rows[2:]:
|
||||
label = row[0]
|
||||
if not label:
|
||||
continue
|
||||
match = re.search(r'\[(\d+)\]', str(label))
|
||||
if not match:
|
||||
continue
|
||||
slot_num = int(match.group(1))
|
||||
|
||||
qtys = []
|
||||
for i in range(len(prices)):
|
||||
raw = row[i + 1] if (i + 1) < len(row) else None
|
||||
qtys.append(round(abs(float(raw)), 4) if (raw is not None and raw != "") else None)
|
||||
|
||||
# Skip slots where every qty cell is blank
|
||||
if all(q is None for q in qtys):
|
||||
continue
|
||||
|
||||
pq_data = [
|
||||
{"price": price, "qty": qty}
|
||||
for price, qty in zip(prices, qtys)
|
||||
if qty is not None
|
||||
]
|
||||
|
||||
order = {
|
||||
"tacId": tac_id,
|
||||
"fromSlot": slot_num,
|
||||
"toSlot": slot_num,
|
||||
"orderType": order_type_val,
|
||||
"orderSide": order_side_val,
|
||||
"source": source_val,
|
||||
"sourceType": "",
|
||||
}
|
||||
# OCF fields are excluded for HPDAM segment
|
||||
if seg_folder != "HPDAM":
|
||||
order["ocfOpted"] = cfg["ocf_opted"]
|
||||
order["ocfPremiumDisc"] = cfg["ocf_premium_disc"]
|
||||
order["ocfQty"] = cfg["ocf_qty"]
|
||||
order["pqData"] = pq_data
|
||||
orders.append(order)
|
||||
|
||||
return orders
|
||||
|
||||
|
||||
def build_payload(tm_id: str, segment_val: str, orders: list[dict], cfg: dict) -> dict:
|
||||
return {
|
||||
"transcode": TRANSCODE,
|
||||
"userId": tm_id,
|
||||
"password": cfg["password"],
|
||||
"tmId": tm_id,
|
||||
"requestType": REQUEST_TYPE,
|
||||
"productCode": cfg["product_code"],
|
||||
"productType": PRODUCT_TYPE,
|
||||
"segment": segment_val,
|
||||
"deliveryDate": tomorrow_str(),
|
||||
"orders": orders,
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# MAIN WALK
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def process_all(cfg: dict):
|
||||
input_root = Path(cfg["input_root"])
|
||||
output_root = Path(cfg["output_root"])
|
||||
|
||||
if not input_root.exists():
|
||||
print(f"[ERROR] input_root not found: {input_root}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
total_files = 0
|
||||
total_errors = 0
|
||||
|
||||
print(f"Delivery date (tomorrow): {tomorrow_str()}")
|
||||
print(f"Input : {input_root.resolve()}")
|
||||
print(f"Output : {output_root.resolve()}")
|
||||
print("-" * 72)
|
||||
|
||||
# ── Level 1: segment folder (GDAM / DAM / HPDAM) ─────────────────────────
|
||||
for seg_dir in sorted(input_root.iterdir()):
|
||||
if not seg_dir.is_dir() or seg_dir.name.startswith("."):
|
||||
continue
|
||||
|
||||
seg_folder = seg_dir.name
|
||||
segment_val = SEGMENT_MAP.get(seg_folder)
|
||||
if segment_val is None:
|
||||
print(f"[WARN] Unknown segment folder '{seg_folder}' — skipping.")
|
||||
continue
|
||||
source_val = SOURCE_MAP[seg_folder] # guaranteed present since SEGMENT_MAP check passed
|
||||
|
||||
# ── Level 2: order type folder (Single / Block) ───────────────────────
|
||||
for ot_dir in sorted(seg_dir.iterdir()):
|
||||
if not ot_dir.is_dir():
|
||||
continue
|
||||
|
||||
ot_folder = ot_dir.name
|
||||
if ot_folder == "Block":
|
||||
print(f" [SKIP] Block order-type folder ignored: {ot_dir.relative_to(input_root)}")
|
||||
continue
|
||||
order_type_val = ORDER_TYPE_MAP.get(ot_folder)
|
||||
if order_type_val is None:
|
||||
print(f"[WARN] Unknown order-type folder '{ot_folder}' — skipping.")
|
||||
continue
|
||||
|
||||
# ── Level 3: order side folder (Buy / Sell) ───────────────────────
|
||||
for os_dir in sorted(ot_dir.iterdir()):
|
||||
if not os_dir.is_dir():
|
||||
continue
|
||||
|
||||
os_folder = os_dir.name
|
||||
order_side_val = ORDER_SIDE_MAP.get(os_folder)
|
||||
if order_side_val is None:
|
||||
print(f"[WARN] Unknown order-side folder '{os_folder}' — skipping.")
|
||||
continue
|
||||
# Fix 2: source is blank for Buy orders, segment-mapped only for Sell
|
||||
effective_source = "" if os_folder == "Buy" else source_val
|
||||
|
||||
# ── Level 4: TM folder (e.g. C1001) ──────────────────────────
|
||||
for tm_dir in sorted(os_dir.iterdir()):
|
||||
if not tm_dir.is_dir():
|
||||
continue
|
||||
tm_id = tm_dir.name # → userId AND tmId
|
||||
|
||||
# ── Level 5: xlsx files ───────────────────────────────────
|
||||
for xlsx_file in sorted(tm_dir.glob("*.xlsx")):
|
||||
total_files += 1
|
||||
rel_path = xlsx_file.relative_to(input_root)
|
||||
print(f" {rel_path}", end=" ... ")
|
||||
|
||||
try:
|
||||
orders = parse_excel(
|
||||
xlsx_file,
|
||||
order_type_val=order_type_val,
|
||||
order_side_val=order_side_val,
|
||||
source_val=effective_source,
|
||||
seg_folder=seg_folder,
|
||||
cfg=cfg,
|
||||
)
|
||||
|
||||
payload = build_payload(
|
||||
tm_id=tm_id,
|
||||
segment_val=segment_val,
|
||||
orders=orders,
|
||||
cfg=cfg,
|
||||
)
|
||||
|
||||
# Output lands directly in output_root/<segment>/
|
||||
out_dir = output_root / seg_folder
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_file = out_dir / f"{xlsx_file.stem}_bid.json"
|
||||
|
||||
with open(out_file, "w") as f:
|
||||
json.dump(payload, f, indent=4)
|
||||
|
||||
print(f"OK → {out_file.relative_to(output_root)} ({len(orders)} orders)")
|
||||
|
||||
except Exception as exc:
|
||||
print(f"FAILED — {exc}", file=sys.stderr)
|
||||
total_errors += 1
|
||||
|
||||
print("-" * 72)
|
||||
print(f"Done. {total_files} file(s) processed, {total_errors} error(s).")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# CLI
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Bulk-generate bid JSON files from Excel bid sheets.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Folder structure expected under --input_root:
|
||||
<segment>/ GDAM | DAM | HPDAM
|
||||
<order_type>/ Single | Block
|
||||
<order_side>/ Buy | Sell
|
||||
<TM>/ e.g. C1001 (becomes userId + tmId)
|
||||
*.xlsx bid data files
|
||||
""",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input_root",
|
||||
default=DEFAULTS["input_root"],
|
||||
help="Root folder containing segment/order_type/order_side/TM/file.xlsx tree (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output_root",
|
||||
default=DEFAULTS["output_root"],
|
||||
help="Root folder where JSON files are written (same sub-tree) (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--product_code",
|
||||
default=DEFAULTS["product_code"],
|
||||
help='Product code string, e.g. "IDAS(01)" (default: %(default)s)',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--password",
|
||||
default=DEFAULTS["password"],
|
||||
help="Password applied to every payload (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ocf_opted",
|
||||
default=DEFAULTS["ocf_opted"],
|
||||
help="OCF opted flag Y/N (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ocf_premium_disc",
|
||||
type=int,
|
||||
default=DEFAULTS["ocf_premium_disc"],
|
||||
help="OCF premium discount integer (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ocf_qty",
|
||||
type=int,
|
||||
default=DEFAULTS["ocf_qty"],
|
||||
help="OCF quantity integer (default: %(default)s)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
process_all(vars(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,560 @@
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.common.exceptions import NoAlertPresentException, TimeoutException, StaleElementReferenceException
|
||||
from .locators import Locator
|
||||
from .config_reader import (
|
||||
get_login_captcha_wait_seconds,
|
||||
get_login_post_submit_wait_seconds,
|
||||
get_step_delay_seconds,
|
||||
)
|
||||
import time
|
||||
import msvcrt
|
||||
|
||||
STEP_DELAY_SECONDS = get_step_delay_seconds()
|
||||
LOGIN_CAPTCHA_WAIT_SECONDS = get_login_captcha_wait_seconds()
|
||||
LOGIN_POST_SUBMIT_WAIT_SECONDS = get_login_post_submit_wait_seconds()
|
||||
|
||||
|
||||
def pause_after_step(seconds=STEP_DELAY_SECONDS):
|
||||
|
||||
time.sleep(seconds)
|
||||
|
||||
|
||||
def wait_for_manual_input(prompt="Press Enter in the terminal when you want to finish this run..."):
|
||||
|
||||
print(prompt)
|
||||
try:
|
||||
input()
|
||||
except EOFError as exc:
|
||||
raise RuntimeError(
|
||||
"Manual input is required for this test. Run pytest with -s so Enter can be captured."
|
||||
) from exc
|
||||
|
||||
|
||||
def clear_pending_console_input():
|
||||
|
||||
while msvcrt.kbhit():
|
||||
msvcrt.getwch()
|
||||
|
||||
|
||||
def enter_pressed():
|
||||
|
||||
if not msvcrt.kbhit():
|
||||
return False
|
||||
|
||||
pressed = False
|
||||
while msvcrt.kbhit():
|
||||
key = msvcrt.getwch()
|
||||
if key in ("\r", "\n"):
|
||||
pressed = True
|
||||
|
||||
return pressed
|
||||
|
||||
|
||||
def login(driver, username, password):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
# username
|
||||
userid_field = wait.until(
|
||||
EC.visibility_of_element_located((By.XPATH, Locator.userid))
|
||||
)
|
||||
userid_field.send_keys(username)
|
||||
|
||||
# password
|
||||
password_field = driver.find_element(By.XPATH, Locator.password)
|
||||
password_field.send_keys(password)
|
||||
|
||||
print(f"Solve captcha within {LOGIN_CAPTCHA_WAIT_SECONDS} seconds...")
|
||||
|
||||
# wait for manual captcha solve
|
||||
time.sleep(LOGIN_CAPTCHA_WAIT_SECONDS)
|
||||
|
||||
# click submit
|
||||
submit_btn = wait.until(
|
||||
EC.element_to_be_clickable((By.XPATH, Locator.submitbtn))
|
||||
)
|
||||
submit_btn.click()
|
||||
|
||||
time.sleep(LOGIN_POST_SUBMIT_WAIT_SECONDS)
|
||||
|
||||
accept_browser_alert_if_present(driver)
|
||||
|
||||
# wait for dashboard
|
||||
wait.until(
|
||||
EC.visibility_of_element_located((By.XPATH, Locator.dashboard_marker))
|
||||
)
|
||||
|
||||
close_tac_expiry_modal_if_present(driver)
|
||||
|
||||
print("Login successful")
|
||||
|
||||
|
||||
def accept_browser_alert_if_present(driver, timeout=5):
|
||||
|
||||
try:
|
||||
alert = WebDriverWait(driver, timeout).until(EC.alert_is_present())
|
||||
print(f"Browser alert accepted: {alert.text}")
|
||||
alert.accept()
|
||||
return True
|
||||
except (TimeoutException, NoAlertPresentException):
|
||||
return False
|
||||
|
||||
|
||||
def close_tac_expiry_modal_if_present(driver, timeout=5):
|
||||
|
||||
try:
|
||||
close_button = WebDriverWait(driver, timeout).until(
|
||||
EC.element_to_be_clickable((By.XPATH, Locator.tacexpiryclosebtn))
|
||||
)
|
||||
except TimeoutException:
|
||||
return False
|
||||
|
||||
try:
|
||||
close_button.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", close_button)
|
||||
|
||||
print("Closed TAC Expiry modal")
|
||||
return True
|
||||
|
||||
|
||||
def handle_transient_popups(driver):
|
||||
|
||||
accepted_alert = accept_browser_alert_if_present(driver, timeout=2)
|
||||
closed_modal = close_tac_expiry_modal_if_present(driver, timeout=2)
|
||||
return accepted_alert or closed_modal
|
||||
|
||||
|
||||
def backup_current_session_token(driver):
|
||||
|
||||
session_backup = {
|
||||
"current_url": driver.current_url,
|
||||
"cookies": driver.get_cookies(),
|
||||
}
|
||||
print(f"Backed up current browser session with {len(session_backup['cookies'])} cookies")
|
||||
return session_backup
|
||||
|
||||
|
||||
def save_session_backup(driver, test_context, name="session_backup"):
|
||||
|
||||
backup = backup_current_session_token(driver)
|
||||
backup["created_at"] = datetime.now().isoformat(timespec="seconds")
|
||||
|
||||
path = os.path.join(test_context["folder"], f"{name}.json")
|
||||
with open(path, "w", encoding="utf-8") as backup_file:
|
||||
json.dump(backup, backup_file, indent=2)
|
||||
|
||||
test_context.setdefault("extra_folders", [])
|
||||
print(f"Session backup saved: {path}")
|
||||
return backup
|
||||
|
||||
|
||||
def logout(driver):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
handle_transient_popups(driver)
|
||||
|
||||
try:
|
||||
user_menu = wait.until(
|
||||
EC.element_to_be_clickable((By.XPATH, Locator.usermenutoggle))
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", user_menu)
|
||||
try:
|
||||
user_menu.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", user_menu)
|
||||
|
||||
wait.until(
|
||||
EC.visibility_of_element_located(
|
||||
(
|
||||
By.XPATH,
|
||||
"//li[contains(@class,'dropdown') and contains(@class,'navbar-user') and contains(@class,'open')]"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
logout_link = wait.until(
|
||||
EC.element_to_be_clickable((By.XPATH, Locator.logoutlink))
|
||||
)
|
||||
try:
|
||||
logout_link.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", logout_link)
|
||||
|
||||
accept_browser_alert_if_present(driver, timeout=3)
|
||||
wait.until(
|
||||
EC.visibility_of_element_located((By.XPATH, Locator.userid))
|
||||
)
|
||||
print("Logout successful")
|
||||
return True
|
||||
except TimeoutException:
|
||||
print("Logout link was not found; browser will still close from fixture teardown")
|
||||
return False
|
||||
|
||||
def select_ui_dropdown(driver, dropdown_label, value):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
# 1️⃣ Open dropdown
|
||||
dropdown = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.XPATH, f"//span[@aria-label='{dropdown_label} activate']")
|
||||
)
|
||||
)
|
||||
dropdown.click()
|
||||
|
||||
# 2️⃣ Find search box
|
||||
search_box = wait.until(
|
||||
EC.visibility_of_element_located((By.CSS_SELECTOR, "input.ui-select-search"))
|
||||
)
|
||||
|
||||
# 3️⃣ Type product name
|
||||
search_box.clear()
|
||||
search_box.send_keys(value)
|
||||
|
||||
# 4️⃣ Wait for dropdown options to load
|
||||
option_xpath = f"//div[contains(@class,'ui-select-choices-row')]//span[contains(text(),'{value}')]"
|
||||
|
||||
option = wait.until(
|
||||
EC.element_to_be_clickable((By.XPATH, option_xpath))
|
||||
)
|
||||
|
||||
# 5️⃣ Click option
|
||||
option.click()
|
||||
|
||||
print(f"{value} selected successfully")
|
||||
|
||||
|
||||
def select_labeled_ui_dropdown(driver, field_label, value):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
dropdown_xpath = (
|
||||
f"//label[normalize-space()='{field_label}']"
|
||||
"/following::span[contains(@class,'ui-select-toggle')][1]"
|
||||
)
|
||||
|
||||
dropdown = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(
|
||||
By.XPATH,
|
||||
dropdown_xpath
|
||||
)
|
||||
)
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", dropdown)
|
||||
|
||||
try:
|
||||
dropdown.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", dropdown)
|
||||
|
||||
search_xpath = (
|
||||
f"//label[normalize-space()='{field_label}']"
|
||||
"/following::input[contains(@class,'ui-select-search')][1]"
|
||||
)
|
||||
|
||||
search_box = wait.until(
|
||||
EC.visibility_of_element_located(
|
||||
(By.XPATH, search_xpath)
|
||||
)
|
||||
)
|
||||
search_box.click()
|
||||
search_box.send_keys(Keys.CONTROL, "a")
|
||||
search_box.clear()
|
||||
search_box.send_keys(value)
|
||||
|
||||
wait.until(
|
||||
EC.presence_of_element_located(
|
||||
(By.XPATH, "//div[contains(@class,'ui-select-choices-row')]")
|
||||
)
|
||||
)
|
||||
|
||||
def _visible_choice_rows():
|
||||
rows = driver.find_elements(By.XPATH, "//div[contains(@class,'ui-select-choices-row')]")
|
||||
return [row for row in rows if row.is_displayed()]
|
||||
|
||||
option = None
|
||||
visible_rows = _visible_choice_rows()
|
||||
|
||||
for row in visible_rows:
|
||||
if row.text.strip() == value:
|
||||
option = row
|
||||
break
|
||||
|
||||
if option is None:
|
||||
available_options = [row.text.strip() for row in visible_rows if row.text.strip()]
|
||||
raise AssertionError(
|
||||
f"Exact option '{value}' was not available in {field_label}. "
|
||||
f"Visible options: {available_options}"
|
||||
)
|
||||
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", option)
|
||||
try:
|
||||
option.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", option)
|
||||
|
||||
print(f"{value} selected successfully from {field_label}")
|
||||
|
||||
|
||||
def open_trading_maintenance_page(driver, menu_xpath):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
trading_maintenance = wait.until(
|
||||
EC.presence_of_element_located((By.XPATH, Locator.tradingmaintenance))
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", trading_maintenance)
|
||||
|
||||
submenu = driver.find_elements(By.XPATH, menu_xpath)
|
||||
submenu_visible = any(item.is_displayed() for item in submenu)
|
||||
|
||||
if not submenu_visible:
|
||||
try:
|
||||
wait.until(EC.element_to_be_clickable((By.XPATH, Locator.tradingmaintenance)))
|
||||
trading_maintenance.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", trading_maintenance)
|
||||
|
||||
menu_item = wait.until(
|
||||
EC.element_to_be_clickable((By.XPATH, menu_xpath))
|
||||
)
|
||||
try:
|
||||
menu_item.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", menu_item)
|
||||
|
||||
|
||||
def click_visible_menu_item(driver, menu_xpath):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
menu_items = driver.find_elements(By.XPATH, menu_xpath)
|
||||
|
||||
for menu_item in menu_items:
|
||||
if menu_item.is_displayed():
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", menu_item)
|
||||
try:
|
||||
menu_item.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", menu_item)
|
||||
return
|
||||
|
||||
menu_item = wait.until(
|
||||
EC.element_to_be_clickable((By.XPATH, menu_xpath))
|
||||
)
|
||||
try:
|
||||
menu_item.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", menu_item)
|
||||
|
||||
|
||||
def type_into_field(driver, locator, value):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
field = wait.until(
|
||||
EC.visibility_of_element_located(locator)
|
||||
)
|
||||
field.clear()
|
||||
field.send_keys(value)
|
||||
|
||||
|
||||
def select_bootstrap_dropdown_by_id(driver, element_id, value):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
container = wait.until(
|
||||
EC.presence_of_element_located((By.ID, element_id))
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", container)
|
||||
|
||||
toggle_xpath = (
|
||||
f"//div[@id='{element_id}']//span[contains(@class,'ui-select-toggle')]"
|
||||
f"|//div[@id='{element_id}']//span[contains(@class,'btn-default')]"
|
||||
f"|//div[@id='{element_id}']//span[contains(@class,'ui-select-placeholder')]"
|
||||
f"|//div[@id='{element_id}']//input[contains(@class,'ui-select-search')]"
|
||||
)
|
||||
|
||||
toggle = wait.until(
|
||||
EC.presence_of_element_located((By.XPATH, toggle_xpath))
|
||||
)
|
||||
|
||||
try:
|
||||
wait.until(EC.element_to_be_clickable((By.XPATH, toggle_xpath)))
|
||||
toggle.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", toggle)
|
||||
|
||||
try:
|
||||
search_box = wait.until(
|
||||
EC.visibility_of_element_located(
|
||||
(By.XPATH, f"//div[@id='{element_id}']//input[contains(@class,'ui-select-search')]")
|
||||
)
|
||||
)
|
||||
except TimeoutException:
|
||||
search_box = wait.until(
|
||||
EC.visibility_of_element_located(
|
||||
(By.XPATH, "//input[contains(@class,'ui-select-search') and not(@aria-hidden='true')]")
|
||||
)
|
||||
)
|
||||
|
||||
search_box.click()
|
||||
search_box.send_keys(Keys.CONTROL, "a")
|
||||
search_box.clear()
|
||||
search_box.send_keys(value)
|
||||
|
||||
option = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(
|
||||
By.XPATH,
|
||||
"//div[contains(@class,'ui-select-choices-row')]"
|
||||
f"//*[normalize-space()='{value}' or starts-with(normalize-space(),'{value}')]"
|
||||
)
|
||||
)
|
||||
)
|
||||
option.click()
|
||||
|
||||
print(f"{value} selected successfully from {element_id}")
|
||||
|
||||
|
||||
def click_grid_button_for_row_text(driver, row_text, button_text):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
button_xpath = (
|
||||
"//div[contains(@class,'ui-grid-row')]"
|
||||
f"[contains(.,'{row_text}')]"
|
||||
f"//button[normalize-space()='{button_text}']"
|
||||
)
|
||||
|
||||
last_error = None
|
||||
|
||||
for _ in range(3):
|
||||
try:
|
||||
button = wait.until(
|
||||
EC.presence_of_element_located((By.XPATH, button_xpath))
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", button)
|
||||
wait.until(EC.element_to_be_clickable((By.XPATH, button_xpath)))
|
||||
|
||||
refreshed_button = driver.find_element(By.XPATH, button_xpath)
|
||||
|
||||
try:
|
||||
refreshed_button.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", refreshed_button)
|
||||
|
||||
print(f"Clicked {button_text} for row containing {row_text}")
|
||||
return
|
||||
except StaleElementReferenceException as exc:
|
||||
last_error = exc
|
||||
time.sleep(1)
|
||||
|
||||
if last_error:
|
||||
raise last_error
|
||||
|
||||
|
||||
def click_first_grid_button(driver, button_text):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
button_xpath = f"(//button[normalize-space()='{button_text}' and not(contains(@class,'ng-hide'))])[1]"
|
||||
last_error = None
|
||||
|
||||
for _ in range(3):
|
||||
try:
|
||||
button = wait.until(
|
||||
EC.presence_of_element_located((By.XPATH, button_xpath))
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'start'});", button)
|
||||
refreshed_button = driver.find_element(By.XPATH, button_xpath)
|
||||
|
||||
try:
|
||||
refreshed_button.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", refreshed_button)
|
||||
|
||||
print(f"Clicked first visible {button_text}")
|
||||
return
|
||||
except StaleElementReferenceException as exc:
|
||||
last_error = exc
|
||||
time.sleep(1)
|
||||
|
||||
if last_error:
|
||||
raise last_error
|
||||
|
||||
|
||||
def type_first_visible_input(driver, xpath, value):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
def _find_visible_input(_driver):
|
||||
elements = _driver.find_elements(By.XPATH, xpath)
|
||||
for element in elements:
|
||||
if element.is_displayed() and element.is_enabled():
|
||||
return element
|
||||
return False
|
||||
|
||||
element = wait.until(_find_visible_input)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)
|
||||
element.click()
|
||||
element.clear()
|
||||
element.send_keys(value)
|
||||
|
||||
|
||||
def toggle_bootstrap_dropdown_by_id(driver, element_id):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
toggle_xpath = (
|
||||
f"//div[@id='{element_id}']//span[contains(@class,'ui-select-toggle')]"
|
||||
f"|//div[@id='{element_id}']//span[contains(@class,'btn-default')]"
|
||||
f"|//div[@id='{element_id}']//span[contains(@class,'ui-select-placeholder')]"
|
||||
f"|//div[@id='{element_id}']//input[contains(@class,'ui-select-search')]"
|
||||
)
|
||||
|
||||
toggle = wait.until(
|
||||
EC.presence_of_element_located((By.XPATH, toggle_xpath))
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", toggle)
|
||||
|
||||
try:
|
||||
wait.until(EC.element_to_be_clickable((By.XPATH, toggle_xpath)))
|
||||
toggle.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", toggle)
|
||||
|
||||
|
||||
def click_page_body(driver):
|
||||
|
||||
body = WebDriverWait(driver, 20).until(
|
||||
EC.presence_of_element_located((By.TAG_NAME, "body"))
|
||||
)
|
||||
body.click()
|
||||
|
||||
|
||||
def toggle_labeled_ui_dropdown(driver, field_label):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
dropdown_xpath = (
|
||||
f"//label[normalize-space()='{field_label}']"
|
||||
"/following::span[contains(@class,'ui-select-toggle')][1]"
|
||||
)
|
||||
|
||||
dropdown = wait.until(
|
||||
EC.element_to_be_clickable((By.XPATH, dropdown_xpath))
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", dropdown)
|
||||
|
||||
try:
|
||||
dropdown.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", dropdown)
|
||||
@@ -0,0 +1,49 @@
|
||||
class Locator(object):
|
||||
|
||||
|
||||
userid="//input[@placeholder='User ID']"
|
||||
password="//input[@placeholder='Password']"
|
||||
submitbtn="//button[@value='Login']"
|
||||
dashboard_marker="//span[normalize-space()='TRADING MAINTENANCE']"
|
||||
tacexpiryclosebtn="//div[contains(@class,'modal') and contains(@style,'display: block')]//button[contains(@class,'close')]"
|
||||
usermenutoggle="//li[contains(@class,'dropdown') and contains(@class,'navbar-user')]//a[contains(@class,'dropdown-toggle') and @data-toggle='dropdown']"
|
||||
logoutlink="//li[contains(@class,'dropdown') and contains(@class,'navbar-user')]//ul[contains(@class,'dropdown-menu')]//a[@ng-click='logout()' and normalize-space()='Log Out']"
|
||||
tradingmaintenance="//span[normalize-space()='TRADING MAINTENANCE']"
|
||||
tradingsession="//a[normalize-space()='Trading Session Master']"
|
||||
tokengeneration="//a[@href='#/tradingMaintenance/generateToken' or normalize-space()='Token Generation']"
|
||||
tokenandtimesessionview="//a[@href='#/tradingMaintenance/tokenTimeSessionMap' or normalize-space()='Token and Time Session View']"
|
||||
addsessionbtn="//button[contains(text(),'Add')]"
|
||||
addnewtokenbtn="//button[@data-ng-click='addToken()' and normalize-space()='Add New Token']"
|
||||
setmeinstance="//span[@aria-label='meInstanceId activate']"
|
||||
#inputmeid="meInstanceId activate"
|
||||
#"//li[@id='ui-select-choices-178']"
|
||||
#"//i[@class='caret pull-right']"
|
||||
#idasme="//li[text()='IDAS']"
|
||||
inputstartdate="//input[@placeholder='Start Date']"
|
||||
inputenddate="//input[@placeholder='End Date']"
|
||||
sessionsavebtn = "//button[@data-ng-click='saveTrade()']"
|
||||
tokenmarketcode = "mmMarketCode"
|
||||
tokenproductcode = "mmPrdCode"
|
||||
tokendeliverystartdate = "//input[@placeholder='DELIVERY_START_DATE']"
|
||||
tokendeliveryenddate = "//input[@placeholder='DELIVERY_END_DATE']"
|
||||
tokenexpirydate = "//input[@placeholder='Token Expiry Date']"
|
||||
savetokenbtn = "//button[@data-ng-click='saveToken()' and normalize-space()='save Token']"
|
||||
tokentimesessionbtn = "//button[@data-ng-click='redirectToTimeSession()' and normalize-space()='Time Session']"
|
||||
viewproducttype = "pmProductType"
|
||||
viewproductcode = "pmProductCode"
|
||||
viewdeliveryfromdate = "//input[@placeholder='Delivery From Date']"
|
||||
viewdeliverytodate = "//input[@placeholder='Delivery To Date']"
|
||||
viewsearchbtn = "//button[@data-ng-click='getSearchList()' and normalize-space()='Search']"
|
||||
savetokenandactivatebtn = (
|
||||
"//button[@data-ng-click=\"saveTokenDetails('T')\" "
|
||||
"and contains(normalize-space(),'Save') "
|
||||
"and contains(normalize-space(),'Activate')]"
|
||||
)
|
||||
edittimesessionbtn = "//button[@data-ng-click='editSession()' and normalize-space()='Edit']"
|
||||
savetimesessionbtn = "//button[@data-ng-click='updateSession()' and normalize-space()='Save']"
|
||||
timesessionstartdate = "//input[@name='tsmTokenStartDateTime' or @placeholder='TokenStartDateTime']"
|
||||
timesessionenddate = "//input[@name='TokenEndDateTime']"
|
||||
timesessionstarttime = "//input[@name='startTime']"
|
||||
timesessionendtime = "//input[@name='endTime']"
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Regression Suite Structure
|
||||
|
||||
This suite is split first by login role and then by product and business flow.
|
||||
|
||||
## Current layout
|
||||
|
||||
- `tests/setup/sessions/`: session creation and other session-level setup flows.
|
||||
- `tests/setup/tokens/`: token generation and token/time-session setup flows.
|
||||
- `tests/regression/exchange/`: end-to-end regression scenarios executed with exchange login.
|
||||
- `tests/regression/trading_member/`: end-to-end regression scenarios executed with trading member login.
|
||||
|
||||
## Product-wise regression pattern
|
||||
|
||||
Create product folders under both login roles as needed:
|
||||
|
||||
- `tests/regression/exchange/<product>/bidding/`
|
||||
- `tests/regression/exchange/<product>/matching/`
|
||||
- `tests/regression/exchange/<product>/reports/`
|
||||
- `tests/regression/exchange/<product>/order_book/`
|
||||
- `tests/regression/exchange/<product>/trade_book/`
|
||||
- `tests/regression/trading_member/<product>/bidding/`
|
||||
- `tests/regression/trading_member/<product>/matching/`
|
||||
- `tests/regression/trading_member/<product>/reports/`
|
||||
- `tests/regression/trading_member/<product>/order_book/`
|
||||
- `tests/regression/trading_member/<product>/trade_book/`
|
||||
|
||||
## Suggested naming
|
||||
|
||||
- Buy-side bidding flow: `test_buy_flow.py`
|
||||
- Sell-side bidding flow: `test_sell_flow.py`
|
||||
- Matching flow: `test_matching_flow.py`
|
||||
- Report download flow: `test_report_downloads.py`
|
||||
- Order book flow: `test_order_book.py`
|
||||
- Trade book flow: `test_trade_book.py`
|
||||
|
||||
For now, `idas` is scaffolded under both roles as the first product. Additional products can be added by repeating the same folder pattern.
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
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
|
||||
|
||||
|
||||
class TradingLoginLocator:
|
||||
|
||||
username = "txtUserID"
|
||||
password = "txtPwd"
|
||||
captcha_input = (
|
||||
"//input["
|
||||
"contains(@placeholder,'Capt') "
|
||||
"or contains(@placeholder,'capt') "
|
||||
"or contains(@id,'Captcha') "
|
||||
"or contains(@name,'Captcha')"
|
||||
"]"
|
||||
)
|
||||
login_button = "loginButtonLink"
|
||||
|
||||
|
||||
class TradingMemberLoginLocator:
|
||||
|
||||
username = "uname"
|
||||
username_continue_button = "submit_user"
|
||||
password = "password"
|
||||
captcha_input = "InputCaptcha"
|
||||
password_continue_button = "submit_password"
|
||||
|
||||
|
||||
class McoLoginLocator:
|
||||
|
||||
form = "loginform"
|
||||
username = "#loginform input:not([type='hidden']):not([type='password'])"
|
||||
password = "#loginform input[type='password']"
|
||||
sign_in_button = "#loginform button[type='submit']"
|
||||
|
||||
|
||||
def login_to_trading_application(driver, username, password, timing, login_role="exchange"):
|
||||
|
||||
if login_role == "trading_member":
|
||||
return login_to_trading_member_application(driver, username, password, timing)
|
||||
|
||||
return login_to_exchange_application(driver, username, password, timing)
|
||||
|
||||
|
||||
def login_to_exchange_application(driver, username, password, timing):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
username_field = wait.until(
|
||||
EC.visibility_of_element_located((By.ID, TradingLoginLocator.username))
|
||||
)
|
||||
username_field.clear()
|
||||
username_field.send_keys(username)
|
||||
|
||||
password_field = wait.until(
|
||||
EC.visibility_of_element_located((By.ID, TradingLoginLocator.password))
|
||||
)
|
||||
password_field.clear()
|
||||
password_field.send_keys(password)
|
||||
|
||||
wait.until(
|
||||
EC.visibility_of_element_located((By.XPATH, TradingLoginLocator.captcha_input))
|
||||
)
|
||||
|
||||
captcha_wait_seconds = timing.get("login_captcha_wait_seconds", 10)
|
||||
print(f"Enter trading captcha manually within {captcha_wait_seconds} seconds...")
|
||||
time.sleep(captcha_wait_seconds)
|
||||
|
||||
login_button = wait.until(
|
||||
EC.element_to_be_clickable((By.ID, TradingLoginLocator.login_button))
|
||||
)
|
||||
login_button.click()
|
||||
|
||||
post_submit_wait_seconds = timing.get("login_post_submit_wait_seconds", 10)
|
||||
time.sleep(post_submit_wait_seconds)
|
||||
|
||||
|
||||
def login_to_trading_member_application(driver, username, password, timing):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
username_field = wait.until(
|
||||
EC.visibility_of_element_located((By.ID, TradingMemberLoginLocator.username))
|
||||
)
|
||||
username_field.clear()
|
||||
username_field.send_keys(username)
|
||||
|
||||
username_continue_button = wait.until(
|
||||
EC.element_to_be_clickable((By.ID, TradingMemberLoginLocator.username_continue_button))
|
||||
)
|
||||
username_continue_button.click()
|
||||
|
||||
password_field = wait.until(
|
||||
EC.visibility_of_element_located((By.ID, TradingMemberLoginLocator.password))
|
||||
)
|
||||
password_field.clear()
|
||||
password_field.send_keys(password)
|
||||
|
||||
wait.until(
|
||||
EC.visibility_of_element_located((By.ID, TradingMemberLoginLocator.captcha_input))
|
||||
)
|
||||
|
||||
captcha_wait_seconds = timing.get("login_captcha_wait_seconds", 10)
|
||||
print(f"Enter trading member captcha manually within {captcha_wait_seconds} seconds...")
|
||||
time.sleep(captcha_wait_seconds)
|
||||
|
||||
password_continue_button = wait.until(
|
||||
EC.element_to_be_clickable((By.ID, TradingMemberLoginLocator.password_continue_button))
|
||||
)
|
||||
password_continue_button.click()
|
||||
|
||||
post_submit_wait_seconds = timing.get("login_post_submit_wait_seconds", 10)
|
||||
time.sleep(post_submit_wait_seconds)
|
||||
|
||||
|
||||
def login_to_mco_application(driver, username, password, timing):
|
||||
|
||||
if not username or not password:
|
||||
raise ValueError("Configure users.mco.username and users.mco.password in config/runtime.json.")
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
wait.until(EC.presence_of_element_located((By.ID, McoLoginLocator.form)))
|
||||
|
||||
username_field = wait.until(
|
||||
EC.visibility_of_element_located((By.CSS_SELECTOR, McoLoginLocator.username))
|
||||
)
|
||||
username_field.clear()
|
||||
username_field.send_keys(username)
|
||||
|
||||
password_field = wait.until(
|
||||
EC.visibility_of_element_located((By.CSS_SELECTOR, McoLoginLocator.password))
|
||||
)
|
||||
password_field.clear()
|
||||
password_field.send_keys(password)
|
||||
|
||||
sign_in_button = wait.until(
|
||||
EC.element_to_be_clickable((By.CSS_SELECTOR, McoLoginLocator.sign_in_button))
|
||||
)
|
||||
sign_in_button.click()
|
||||
|
||||
wait.until(EC.url_contains("/Dashboard/"))
|
||||
wait.until(
|
||||
EC.visibility_of_element_located(
|
||||
(By.XPATH, "//*[self::a or self::h1 or self::h2 or self::div][normalize-space()='Dashboard']")
|
||||
)
|
||||
)
|
||||
|
||||
post_submit_wait_seconds = timing.get("login_post_submit_wait_seconds", 10)
|
||||
time.sleep(post_submit_wait_seconds)
|
||||
@@ -0,0 +1,272 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
REGRESSION_CONFIG_DIR = PROJECT_ROOT / "config"
|
||||
RUNTIME_CONFIG_PATH = REGRESSION_CONFIG_DIR / "runtime.json"
|
||||
|
||||
|
||||
def _load_json(path):
|
||||
|
||||
with path.open("r", encoding="utf-8") as file:
|
||||
return json.load(file)
|
||||
|
||||
|
||||
def load_runtime_config():
|
||||
|
||||
return _load_json(RUNTIME_CONFIG_PATH)
|
||||
|
||||
|
||||
def load_users_config():
|
||||
|
||||
runtime_config = load_runtime_config()
|
||||
users_config = runtime_config.get("users")
|
||||
|
||||
if not users_config:
|
||||
raise KeyError(f"Users config not found in {RUNTIME_CONFIG_PATH}.")
|
||||
|
||||
return users_config
|
||||
|
||||
|
||||
def load_product_config(product_family=None):
|
||||
|
||||
runtime_config = load_runtime_config()
|
||||
selected_product = product_family or runtime_config["execution"]["product_family"]
|
||||
product_config = runtime_config.get(selected_product)
|
||||
|
||||
if not product_config:
|
||||
product_config = runtime_config.get(selected_product.upper())
|
||||
|
||||
if not product_config:
|
||||
product_config = runtime_config.get(selected_product.lower())
|
||||
|
||||
if not product_config:
|
||||
raise KeyError(
|
||||
f"Product config not found for '{selected_product}' in {RUNTIME_CONFIG_PATH}."
|
||||
)
|
||||
|
||||
return product_config
|
||||
|
||||
|
||||
def _resolve_login(users_config, execution_config):
|
||||
|
||||
login_role = execution_config["login_role"]
|
||||
trading_member_key = execution_config.get("trading_member_key")
|
||||
|
||||
if login_role == "exchange":
|
||||
return users_config["exchange"]
|
||||
|
||||
if login_role == "trading_member":
|
||||
trading_member = users_config["trading_members"].get(trading_member_key)
|
||||
if not trading_member:
|
||||
raise KeyError(
|
||||
f"Trading member '{trading_member_key}' is not defined in {RUNTIME_CONFIG_PATH}"
|
||||
)
|
||||
return {
|
||||
"username": trading_member["username"],
|
||||
"password": trading_member["password"],
|
||||
"label": trading_member.get("label", trading_member_key),
|
||||
"member_id": trading_member.get("member_id", ""),
|
||||
"folder_upload_tm_ids": trading_member.get("folder_upload_tm_ids", []),
|
||||
}
|
||||
|
||||
raise ValueError(
|
||||
f"Unsupported login_role '{login_role}'. Use 'exchange' or 'trading_member'."
|
||||
)
|
||||
|
||||
|
||||
def _resolve_exchange_parallel_logins(users_config):
|
||||
|
||||
return [
|
||||
{
|
||||
"username": exchange_user["username"],
|
||||
"password": exchange_user["password"],
|
||||
"label": exchange_user.get("label", exchange_user["username"]),
|
||||
}
|
||||
for exchange_user in users_config.get("exchange_parallel", [])
|
||||
if exchange_user.get("username") and exchange_user.get("password")
|
||||
]
|
||||
|
||||
|
||||
def _resolve_trading_member_parallel_logins(users_config):
|
||||
|
||||
return [
|
||||
{
|
||||
"username": trading_member["username"],
|
||||
"password": trading_member["password"],
|
||||
"label": trading_member.get("label", trading_member_key),
|
||||
"key": trading_member_key,
|
||||
"member_id": trading_member.get("member_id", ""),
|
||||
"folder_upload_tm_ids": trading_member.get("folder_upload_tm_ids", []),
|
||||
}
|
||||
for trading_member_key, trading_member in users_config.get("trading_members", {}).items()
|
||||
if trading_member.get("username") and trading_member.get("password")
|
||||
]
|
||||
|
||||
|
||||
def _resolve_trading_member(users_config, execution_config):
|
||||
|
||||
trading_member_key = execution_config.get("trading_member_key")
|
||||
trading_member = users_config["trading_members"].get(trading_member_key)
|
||||
|
||||
if not trading_member:
|
||||
raise KeyError(
|
||||
f"Trading member '{trading_member_key}' is not defined in {RUNTIME_CONFIG_PATH}"
|
||||
)
|
||||
|
||||
return trading_member
|
||||
|
||||
|
||||
def _resolve_portal_urls(environment_config, execution_config):
|
||||
|
||||
active_environment = environment_config.get("active_environment")
|
||||
profiles = environment_config.get("portal_environments", {})
|
||||
profile = profiles.get(active_environment, {})
|
||||
pratyay_urls = profile.get("pratyay", {})
|
||||
login_role = execution_config["login_role"]
|
||||
|
||||
if profile:
|
||||
if not pratyay_urls.get(login_role):
|
||||
raise KeyError(
|
||||
f"Pratyay URL for role '{login_role}' is not configured in "
|
||||
f"environment.portal_environments.{active_environment}."
|
||||
)
|
||||
if not profile.get("mco"):
|
||||
raise KeyError(
|
||||
f"MCO URL is not configured in "
|
||||
f"environment.portal_environments.{active_environment}."
|
||||
)
|
||||
return {
|
||||
"environment": active_environment,
|
||||
"pratyay": pratyay_urls[login_role],
|
||||
"mco": profile["mco"],
|
||||
}
|
||||
|
||||
return {
|
||||
"environment": active_environment or "legacy",
|
||||
"pratyay": _resolve_application_url(environment_config, execution_config),
|
||||
"mco": "",
|
||||
}
|
||||
|
||||
|
||||
def _resolve_application_url(environment_config, execution_config):
|
||||
|
||||
login_role = execution_config["login_role"]
|
||||
role_urls = environment_config.get("login_urls", {})
|
||||
|
||||
if login_role in role_urls and role_urls[login_role]:
|
||||
return role_urls[login_role]
|
||||
|
||||
return environment_config["base_url"]
|
||||
|
||||
|
||||
def load_regression_config():
|
||||
|
||||
runtime_config = load_runtime_config()
|
||||
users_config = load_users_config()
|
||||
execution_config = runtime_config["execution"]
|
||||
product_config = load_product_config(execution_config["product_family"])
|
||||
active_selection = product_config.get("active_selection", {})
|
||||
defaults = product_config.get("defaults", {})
|
||||
|
||||
selected_product_code = execution_config.get(
|
||||
"product_code",
|
||||
active_selection.get("product_code", defaults["product_code"])
|
||||
)
|
||||
selected_trading_market = active_selection.get(
|
||||
"trading_market",
|
||||
defaults["trading_market"]
|
||||
)
|
||||
selected_trading_markets = active_selection.get("trading_markets", [])
|
||||
selected_bidding_screen = active_selection.get(
|
||||
"bidding_screen",
|
||||
defaults["bidding_screen"]
|
||||
)
|
||||
|
||||
trading_member = _resolve_trading_member(users_config, execution_config)
|
||||
login = _resolve_login(users_config, execution_config)
|
||||
portal_urls = _resolve_portal_urls(runtime_config["environment"], execution_config)
|
||||
|
||||
return {
|
||||
"url": portal_urls["pratyay"],
|
||||
"portal_urls": portal_urls,
|
||||
"login": login,
|
||||
"mco_login": users_config.get("mco", {}),
|
||||
"exchange_parallel_logins": _resolve_exchange_parallel_logins(users_config),
|
||||
"trading_member_parallel_logins": _resolve_trading_member_parallel_logins(users_config),
|
||||
"product": {
|
||||
"family": execution_config["product_family"],
|
||||
"product_type": defaults["product_type"],
|
||||
"product_code": selected_product_code,
|
||||
"trading_market": selected_trading_market,
|
||||
"trading_markets": selected_trading_markets,
|
||||
"bidding_screen": selected_bidding_screen
|
||||
},
|
||||
"execution": execution_config,
|
||||
"selected_role": execution_config["login_role"],
|
||||
"selected_flow": execution_config["flow_scope"],
|
||||
"selected_trading_member": trading_member,
|
||||
"timing": runtime_config["timing"],
|
||||
"available_product_codes": product_config.get("available_product_codes", []),
|
||||
"available_trading_markets": product_config.get("available_trading_markets", []),
|
||||
"available_bidding_screens": product_config.get("available_bidding_screens", []),
|
||||
"flows": product_config.get("flows", {}),
|
||||
"setup": product_config.get("setup", {}),
|
||||
"exchange_bidding": _normalize_bidding_config(product_config.get("bidding", {})),
|
||||
"exchange_matching": _normalize_matching_config(product_config.get("matching", {}))
|
||||
}
|
||||
|
||||
|
||||
def should_run_flow(flow_name):
|
||||
|
||||
selected_flow = load_regression_config()["selected_flow"]
|
||||
return selected_flow == "e2e" or selected_flow == flow_name
|
||||
|
||||
|
||||
def _normalize_bidding_config(raw_bidding_config):
|
||||
|
||||
if not raw_bidding_config:
|
||||
return {}
|
||||
|
||||
entry_method = raw_bidding_config.get("entry_method", {})
|
||||
data_paths = raw_bidding_config.get("data_paths", {})
|
||||
selection = raw_bidding_config.get("selection", {})
|
||||
timing = raw_bidding_config.get("timing", {})
|
||||
|
||||
return {
|
||||
**entry_method,
|
||||
**data_paths,
|
||||
**selection,
|
||||
**timing,
|
||||
"block_bid": entry_method.get("block_bid", raw_bidding_config.get("block_bid", {})),
|
||||
"excel_layout": raw_bidding_config.get("excel_layout", {}),
|
||||
"entries": raw_bidding_config.get("entries", [])
|
||||
}
|
||||
|
||||
|
||||
def _normalize_matching_config(raw_matching_config):
|
||||
|
||||
if not raw_matching_config:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"matching_product_code": raw_matching_config.get("matching_product_code", ""),
|
||||
"matching_type": raw_matching_config.get("matching_type", ""),
|
||||
"source_types": raw_matching_config.get("source_types", []),
|
||||
"bid_transfer_markets": raw_matching_config.get(
|
||||
"bid_transfer_markets",
|
||||
["GDAM", "DAM", "HPDAM"]
|
||||
),
|
||||
"accepted_bid_transfer_statuses": raw_matching_config.get(
|
||||
"accepted_bid_transfer_statuses",
|
||||
["ACCEPTED", "PARTIALLY_ACCEPTED", "NO_BIDS"]
|
||||
),
|
||||
"initiation_mode": raw_matching_config.get("initiation_mode", ""),
|
||||
"portals": raw_matching_config.get("portals", {}),
|
||||
"download_folder_name": raw_matching_config.get("downloads", {}).get(
|
||||
"download_folder_name",
|
||||
"matching_files"
|
||||
),
|
||||
**raw_matching_config.get("timing", {})
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.common.exceptions import StaleElementReferenceException, TimeoutException
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
|
||||
def _menu_text_xpath(tag_name, label):
|
||||
|
||||
return (
|
||||
f"//{tag_name}[contains(normalize-space(),\"{label}\")]"
|
||||
f"|//{tag_name}[contains(.,\"{label}\")]"
|
||||
)
|
||||
|
||||
|
||||
def open_bootstrap_nav_submenu(driver, top_menu_label, submenu_label, final_href_part):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
top_menu = wait.until(
|
||||
EC.element_to_be_clickable(
|
||||
(
|
||||
By.XPATH,
|
||||
"//ul[contains(@class,'nav') and contains(@class,'navbar-nav')]"
|
||||
f"//a[contains(@class,'dropdown-toggle') and contains(normalize-space(),'{top_menu_label}')]"
|
||||
)
|
||||
)
|
||||
)
|
||||
try:
|
||||
top_menu.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", top_menu)
|
||||
|
||||
top_menu_container = wait.until(
|
||||
lambda _driver: _find_open_top_menu_container(_driver, top_menu_label)
|
||||
)
|
||||
|
||||
submenu = wait.until(
|
||||
lambda _driver: _find_clickable_submenu(_driver, top_menu_container, submenu_label)
|
||||
)
|
||||
try:
|
||||
submenu.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", submenu)
|
||||
|
||||
final_option = wait.until(
|
||||
lambda _driver: _find_clickable_final_option(_driver, top_menu_container, final_href_part)
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", final_option)
|
||||
try:
|
||||
final_option.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", final_option)
|
||||
|
||||
|
||||
def _find_open_top_menu_container(driver, top_menu_label):
|
||||
|
||||
containers = driver.find_elements(
|
||||
By.XPATH,
|
||||
"//ul[contains(@class,'nav') and contains(@class,'navbar-nav')]/li"
|
||||
)
|
||||
|
||||
normalized_label = top_menu_label.strip().lower()
|
||||
for container in containers:
|
||||
classes = (container.get_attribute("class") or "").lower()
|
||||
text = container.text.strip().lower()
|
||||
if normalized_label in text and "open" in classes:
|
||||
return container
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _find_clickable_submenu(driver, top_menu_container, submenu_label):
|
||||
|
||||
normalized_label = submenu_label.strip().lower()
|
||||
submenu_links = top_menu_container.find_elements(
|
||||
By.XPATH,
|
||||
".//ul[contains(@class,'dropdown-menu')]//a"
|
||||
)
|
||||
|
||||
for link in submenu_links:
|
||||
text = link.text.strip().lower()
|
||||
if normalized_label in text and link.is_displayed() and link.is_enabled():
|
||||
return link
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _find_clickable_final_option(driver, top_menu_container, final_href_part):
|
||||
|
||||
final_links = top_menu_container.find_elements(
|
||||
By.XPATH,
|
||||
f".//ul[contains(@class,'dropdown-menu')]//a[contains(@href,'{final_href_part}')]"
|
||||
)
|
||||
|
||||
for link in final_links:
|
||||
if link.is_displayed() and link.is_enabled():
|
||||
return link
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def select_select2_value_by_container_id(driver, container_id, value, exact=False):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
container = wait.until(
|
||||
EC.element_to_be_clickable((By.ID, container_id))
|
||||
)
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", container)
|
||||
try:
|
||||
container.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", container)
|
||||
|
||||
try:
|
||||
search_input = wait.until(
|
||||
EC.visibility_of_element_located(
|
||||
(By.XPATH, "//div[contains(@class,'select2-drop-active')]//input[contains(@class,'select2-input')]")
|
||||
)
|
||||
)
|
||||
search_input.click()
|
||||
search_input.send_keys(Keys.CONTROL, "a")
|
||||
search_input.send_keys(value)
|
||||
except TimeoutException:
|
||||
pass
|
||||
|
||||
try:
|
||||
option = wait.until(
|
||||
lambda _driver: _find_select2_option(_driver, value, exact=exact)
|
||||
)
|
||||
except TimeoutException as exc:
|
||||
available_options = _get_visible_select2_option_texts(driver)
|
||||
raise TimeoutException(
|
||||
f"Option '{value}' did not load for {container_id}. "
|
||||
f"Visible options: {available_options}"
|
||||
) from exc
|
||||
|
||||
try:
|
||||
option.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", option)
|
||||
|
||||
|
||||
def _find_select2_option(driver, value, exact=False):
|
||||
|
||||
options = driver.find_elements(
|
||||
By.XPATH,
|
||||
"//div[contains(@class,'select2-drop-active')]//div[contains(@class,'select2-result-label')]"
|
||||
)
|
||||
|
||||
normalized_value = value.strip().lower()
|
||||
visible_options = []
|
||||
for option in options:
|
||||
try:
|
||||
if option.is_displayed():
|
||||
visible_options.append(option)
|
||||
except StaleElementReferenceException:
|
||||
return False
|
||||
|
||||
for option in visible_options:
|
||||
text = _safe_element_text(option)
|
||||
if text is None:
|
||||
return False
|
||||
if text == normalized_value:
|
||||
return option
|
||||
|
||||
for option in visible_options:
|
||||
text = _safe_element_text(option)
|
||||
if text is None:
|
||||
return False
|
||||
option_code = text.split("(", 1)[0].strip()
|
||||
if option_code == normalized_value:
|
||||
return option
|
||||
|
||||
if exact:
|
||||
return False
|
||||
|
||||
for option in visible_options:
|
||||
text = _safe_element_text(option)
|
||||
if text is None:
|
||||
return False
|
||||
if text.startswith(normalized_value):
|
||||
return option
|
||||
|
||||
for option in visible_options:
|
||||
text = _safe_element_text(option)
|
||||
if text is None:
|
||||
return False
|
||||
if normalized_value in text:
|
||||
return option
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _safe_element_text(element):
|
||||
|
||||
try:
|
||||
return element.text.strip().lower()
|
||||
except StaleElementReferenceException:
|
||||
return None
|
||||
|
||||
|
||||
def click_select2_option_by_text(driver, container_id, value):
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
container = wait.until(EC.element_to_be_clickable((By.ID, container_id)))
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", container)
|
||||
try:
|
||||
container.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", container)
|
||||
|
||||
option = wait.until(lambda _driver: _find_select2_option(_driver, value))
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'nearest'});", option)
|
||||
try:
|
||||
option.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", option)
|
||||
|
||||
|
||||
def wait_for_select2_option_by_text(driver, container_id, value, timeout_seconds=30, attempts=3):
|
||||
|
||||
wait = WebDriverWait(driver, timeout_seconds)
|
||||
last_error = None
|
||||
|
||||
for _ in range(attempts):
|
||||
container = wait.until(EC.element_to_be_clickable((By.ID, container_id)))
|
||||
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", container)
|
||||
try:
|
||||
container.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", container)
|
||||
|
||||
try:
|
||||
option = wait.until(lambda _driver: _find_select2_option(_driver, value))
|
||||
try:
|
||||
container.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", container)
|
||||
return option
|
||||
except TimeoutException as exc:
|
||||
last_error = exc
|
||||
try:
|
||||
container.click()
|
||||
except Exception:
|
||||
driver.execute_script("arguments[0].click();", container)
|
||||
|
||||
if last_error:
|
||||
available_options = _get_visible_select2_option_texts(driver)
|
||||
raise TimeoutException(
|
||||
f"Option '{value}' did not load for {container_id}. "
|
||||
f"Visible options: {available_options}"
|
||||
) from last_error
|
||||
|
||||
raise TimeoutException(f"Option '{value}' did not load for {container_id}")
|
||||
|
||||
|
||||
def _get_visible_select2_option_texts(driver):
|
||||
|
||||
options = driver.find_elements(
|
||||
By.XPATH,
|
||||
"//div[contains(@class,'select2-drop-active')]//div[contains(@class,'select2-result-label')]"
|
||||
)
|
||||
return [option.text.strip() for option in options if option.is_displayed()]
|
||||
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
def log_step(driver, request, step_name):
|
||||
|
||||
screenshots_dir = "reports/screenshots"
|
||||
os.makedirs(screenshots_dir, exist_ok=True)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"{step_name}_{timestamp}.png"
|
||||
|
||||
filepath = os.path.join(screenshots_dir, filename)
|
||||
|
||||
driver.save_screenshot(filepath)
|
||||
|
||||
if hasattr(request.node, "extra"):
|
||||
import pytest_html
|
||||
request.node.extra.append(pytest_html.extras.image(filepath))
|
||||
|
||||
print(f"STEP: {step_name}")
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
import pytest
|
||||
|
||||
from tests.step_actions import StepActions
|
||||
from tests.helpers import login, select_ui_dropdown
|
||||
from tests.config_reader import is_combined_setup_enabled, is_setup_session_enabled, load_config
|
||||
from tests.locators import Locator
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
is_combined_setup_enabled() or not is_setup_session_enabled(),
|
||||
reason="Standalone session setup is disabled or runs inside combined setup",
|
||||
)
|
||||
|
||||
|
||||
def get_session_window(config):
|
||||
|
||||
session_config = config.get("setup", {}).get("session", {})
|
||||
start_offset_minutes = session_config.get("start_offset_minutes", 7)
|
||||
end_time = session_config.get("end_time", "20:30")
|
||||
|
||||
start_datetime = datetime.now() + timedelta(minutes=start_offset_minutes)
|
||||
today = datetime.now().strftime("%d/%m/%Y")
|
||||
|
||||
return start_datetime.strftime("%d/%m/%Y %H:%M"), f"{today} {end_time}"
|
||||
|
||||
|
||||
def create_session(driver, step, config):
|
||||
|
||||
step.click((By.XPATH, Locator.tradingmaintenance), "trading_maintenance")
|
||||
|
||||
step.click((By.XPATH, Locator.tradingsession), "trading_session")
|
||||
|
||||
step.click((By.XPATH, Locator.addsessionbtn), "add_session")
|
||||
|
||||
select_ui_dropdown(driver, "meInstanceId", config["product"]["me_instance"])
|
||||
|
||||
step.capture("select_product")
|
||||
|
||||
start_date_time, end_date_time = get_session_window(config)
|
||||
|
||||
step.type((By.XPATH, Locator.inputstartdate), start_date_time, "start_date")
|
||||
|
||||
step.type((By.XPATH, Locator.inputenddate), end_date_time, "end_date")
|
||||
|
||||
step.click((By.XPATH, Locator.sessionsavebtn), "save_session")
|
||||
|
||||
|
||||
@pytest.mark.session
|
||||
def test_session(driver, test_context):
|
||||
|
||||
config = load_config()
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
|
||||
step = StepActions(driver, wait, test_context)
|
||||
|
||||
driver.get(config["url"])
|
||||
step.capture("open_application")
|
||||
|
||||
login(
|
||||
driver,
|
||||
config["login"]["username"],
|
||||
config["login"]["password"]
|
||||
)
|
||||
|
||||
step.capture("login")
|
||||
|
||||
create_session(driver, step, config)
|
||||
@@ -0,0 +1,46 @@
|
||||
import pytest
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
from tests.config_reader import is_combined_setup_enabled, load_config
|
||||
from tests.helpers import login, logout, save_session_backup
|
||||
from tests.setup.sessions.test_session import create_session
|
||||
from tests.setup.tokens.test_trading_token import create_and_activate_token, get_product_codes
|
||||
from tests.step_actions import StepActions
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not is_combined_setup_enabled(),
|
||||
reason="Combined setup requires setup.run_session and setup.run_token to both be true",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.session
|
||||
@pytest.mark.trading_token
|
||||
def test_session_and_token_setup(driver, test_context):
|
||||
|
||||
config = load_config()
|
||||
token = config.get("trading_token_idas")
|
||||
assert token, "Add trading_token_idas in config.json before running token setup."
|
||||
|
||||
wait = WebDriverWait(driver, 20)
|
||||
step = StepActions(driver, wait, test_context)
|
||||
|
||||
driver.get(config["url"])
|
||||
step.capture("open_application")
|
||||
|
||||
login(
|
||||
driver,
|
||||
config["login"]["username"],
|
||||
config["login"]["password"]
|
||||
)
|
||||
step.capture("login")
|
||||
save_session_backup(driver, test_context, "login_session_backup")
|
||||
|
||||
create_session(driver, step, config)
|
||||
|
||||
for product_code in get_product_codes(token):
|
||||
product_token = dict(token)
|
||||
product_token["product_code"] = product_code
|
||||
create_and_activate_token(driver, step, product_token)
|
||||
|
||||
logout(driver)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytest
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
from tests.config_reader import is_combined_setup_enabled, is_setup_token_enabled, load_config, parse_product_codes
|
||||
from tests.helpers import (
|
||||
click_visible_menu_item,
|
||||
click_first_grid_button,
|
||||
click_grid_button_for_row_text,
|
||||
handle_transient_popups,
|
||||
login,
|
||||
logout,
|
||||
open_trading_maintenance_page,
|
||||
pause_after_step,
|
||||
save_session_backup,
|
||||
select_bootstrap_dropdown_by_id,
|
||||
select_labeled_ui_dropdown,
|
||||
type_first_visible_input,
|
||||
)
|
||||
from tests.locators import Locator
|
||||
from tests.step_actions import StepActions
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
is_combined_setup_enabled() or not is_setup_token_enabled(),
|
||||
reason="Standalone token setup is disabled or runs inside combined setup",
|
||||
)
|
||||
|
||||
|
||||
def get_token_context(driver, test_context):
|
||||
|
||||
config = load_config()
|
||||
wait = WebDriverWait(driver, 20)
|
||||
step = StepActions(driver, wait, test_context)
|
||||
token = config.get("trading_token_idas")
|
||||
|
||||
assert token, "Add trading_token_idas in config.json before running this test."
|
||||
|
||||
return config, step, token
|
||||
|
||||
|
||||
def open_app_and_login(driver, step, config):
|
||||
|
||||
driver.get(config["url"])
|
||||
step.capture("open_application")
|
||||
|
||||
login(
|
||||
driver,
|
||||
config["login"]["username"],
|
||||
config["login"]["password"]
|
||||
)
|
||||
step.capture("login")
|
||||
|
||||
|
||||
def get_delivery_date_and_expiry(token):
|
||||
|
||||
delivery_offset_days = token.get("delivery_offset_days", 1)
|
||||
expiry_time = token.get("expiry_time", "20:30")
|
||||
|
||||
delivery_date = (datetime.now() + timedelta(days=delivery_offset_days)).strftime("%d/%m/%Y")
|
||||
token_expiry = f"{datetime.now().strftime('%d/%m/%Y')} {expiry_time}"
|
||||
|
||||
return delivery_date, token_expiry
|
||||
|
||||
|
||||
def get_time_session_times(token, row_text, default_start_time, default_end_time):
|
||||
|
||||
time_session_config = token.get("time_sessions", {}).get(row_text, {})
|
||||
start_time = time_session_config.get("start_time")
|
||||
|
||||
if start_time is None:
|
||||
start_offset_minutes = time_session_config.get("start_offset_minutes")
|
||||
if start_offset_minutes is not None:
|
||||
start_time = (datetime.now() + timedelta(minutes=start_offset_minutes)).strftime("%H:%M")
|
||||
|
||||
return (
|
||||
start_time or default_start_time,
|
||||
time_session_config.get("end_time", default_end_time)
|
||||
)
|
||||
|
||||
|
||||
def get_product_codes(token):
|
||||
|
||||
product_codes = parse_product_codes(token.get("product_codes"))
|
||||
if product_codes:
|
||||
return product_codes
|
||||
|
||||
product_code = token.get("product_code")
|
||||
assert product_code, "Add product_code or product_codes in config.json trading_token_idas."
|
||||
return parse_product_codes(product_code)
|
||||
|
||||
|
||||
def edit_time_session_row(driver, step, row_text, start_time, end_time):
|
||||
|
||||
today = datetime.now().strftime("%d/%m/%Y")
|
||||
|
||||
click_grid_button_for_row_text(driver, row_text, "Details")
|
||||
step.capture(f"open_{row_text.lower()}_details")
|
||||
|
||||
step.click((By.XPATH, Locator.edittimesessionbtn), f"edit_{row_text.lower()}_time_session")
|
||||
|
||||
step.type((By.XPATH, Locator.timesessionstartdate), today, f"enter_{row_text.lower()}_start_date")
|
||||
step.type((By.XPATH, Locator.timesessionenddate), today, f"enter_{row_text.lower()}_end_date")
|
||||
type_first_visible_input(driver, Locator.timesessionstarttime, start_time)
|
||||
step.capture(f"enter_{row_text.lower()}_start_time")
|
||||
type_first_visible_input(driver, Locator.timesessionendtime, end_time)
|
||||
step.capture(f"enter_{row_text.lower()}_end_time")
|
||||
|
||||
step.click((By.XPATH, Locator.savetimesessionbtn), f"save_{row_text.lower()}_time_session")
|
||||
handle_transient_popups(driver)
|
||||
|
||||
|
||||
def complete_time_session_edits(driver, step, token, next_day):
|
||||
|
||||
open_trading_maintenance_page(driver, Locator.tokenandtimesessionview)
|
||||
step.capture("open_token_and_time_session_view")
|
||||
|
||||
select_bootstrap_dropdown_by_id(driver, Locator.viewproducttype, token["product_type"])
|
||||
step.capture("select_view_product_type")
|
||||
|
||||
select_bootstrap_dropdown_by_id(driver, Locator.viewproductcode, token["product_code"])
|
||||
step.capture("select_view_product_code")
|
||||
|
||||
step.type((By.XPATH, Locator.viewdeliveryfromdate), next_day, "enter_view_delivery_from_date")
|
||||
step.type((By.XPATH, Locator.viewdeliverytodate), next_day, "enter_view_delivery_to_date")
|
||||
|
||||
step.click((By.XPATH, Locator.viewsearchbtn), "search_token_and_time_session")
|
||||
click_grid_button_for_row_text(driver, "12-Mar-2026", "Time Session Details")
|
||||
step.capture("open_time_session_details")
|
||||
step.capture("before_edit_order_normal")
|
||||
|
||||
order_nor_start_time, order_nor_end_time = get_time_session_times(
|
||||
token,
|
||||
"ORDER_NOR",
|
||||
(datetime.now() + timedelta(minutes=10)).strftime("%H:%M"),
|
||||
"18:30"
|
||||
)
|
||||
order_exch_start_time, order_exch_end_time = get_time_session_times(token, "ORDER_EXCH", "18:30", "19:00")
|
||||
trade_start_time, trade_end_time = get_time_session_times(token, "TRADE", "19:00", "20:00")
|
||||
|
||||
edit_time_session_row(
|
||||
driver,
|
||||
step,
|
||||
"ORDER_NOR",
|
||||
order_nor_start_time,
|
||||
order_nor_end_time
|
||||
)
|
||||
edit_time_session_row(driver, step, "ORDER_EXCH", order_exch_start_time, order_exch_end_time)
|
||||
edit_time_session_row(driver, step, "TRADE", trade_start_time, trade_end_time)
|
||||
|
||||
click_first_grid_button(driver, "Token Details")
|
||||
step.capture("open_token_details")
|
||||
step.click((By.XPATH, Locator.savetokenandactivatebtn), "save_and_activate_token")
|
||||
handle_transient_popups(driver)
|
||||
pause_after_step()
|
||||
|
||||
|
||||
def complete_time_session_edits_from_saved_token(driver, step, token):
|
||||
|
||||
step.click((By.XPATH, Locator.tokentimesessionbtn), "open_time_session")
|
||||
click_first_grid_button(driver, "Time Session Details")
|
||||
step.capture("open_time_session_details")
|
||||
step.capture("before_edit_order_normal")
|
||||
|
||||
order_nor_start_time, order_nor_end_time = get_time_session_times(
|
||||
token,
|
||||
"ORDER_NOR",
|
||||
(datetime.now() + timedelta(minutes=10)).strftime("%H:%M"),
|
||||
"18:30"
|
||||
)
|
||||
order_exch_start_time, order_exch_end_time = get_time_session_times(token, "ORDER_EXCH", "18:30", "19:00")
|
||||
trade_start_time, trade_end_time = get_time_session_times(token, "TRADE", "19:00", "20:00")
|
||||
|
||||
edit_time_session_row(
|
||||
driver,
|
||||
step,
|
||||
"ORDER_NOR",
|
||||
order_nor_start_time,
|
||||
order_nor_end_time
|
||||
)
|
||||
edit_time_session_row(driver, step, "ORDER_EXCH", order_exch_start_time, order_exch_end_time)
|
||||
edit_time_session_row(driver, step, "TRADE", trade_start_time, trade_end_time)
|
||||
|
||||
click_first_grid_button(driver, "Token Details")
|
||||
step.capture("open_token_details")
|
||||
step.click((By.XPATH, Locator.savetokenandactivatebtn), "save_and_activate_token")
|
||||
handle_transient_popups(driver)
|
||||
pause_after_step()
|
||||
|
||||
|
||||
def create_and_activate_token(driver, step, token):
|
||||
|
||||
product_code = token["product_code"]
|
||||
|
||||
token_menu_items = driver.find_elements(By.XPATH, Locator.tokengeneration)
|
||||
if any(item.is_displayed() for item in token_menu_items):
|
||||
click_visible_menu_item(driver, Locator.tokengeneration)
|
||||
else:
|
||||
open_trading_maintenance_page(driver, Locator.tokengeneration)
|
||||
|
||||
handle_transient_popups(driver)
|
||||
step.capture(f"open_token_generation_{product_code.lower()}")
|
||||
|
||||
select_bootstrap_dropdown_by_id(driver, Locator.tokenmarketcode, token["market_code"])
|
||||
step.capture(f"select_market_code_{product_code.lower()}")
|
||||
|
||||
select_labeled_ui_dropdown(driver, "PRODUCT CODE", product_code)
|
||||
step.capture(f"select_product_code_{product_code.lower()}")
|
||||
|
||||
step.click((By.XPATH, Locator.addnewtokenbtn), f"click_add_new_token_{product_code.lower()}")
|
||||
|
||||
next_day, token_expiry = get_delivery_date_and_expiry(token)
|
||||
|
||||
step.type((By.XPATH, Locator.tokendeliverystartdate), next_day, f"enter_delivery_start_date_{product_code.lower()}")
|
||||
step.type((By.XPATH, Locator.tokendeliveryenddate), next_day, f"enter_delivery_end_date_{product_code.lower()}")
|
||||
step.type((By.XPATH, Locator.tokenexpirydate), token_expiry, f"enter_token_expiry_date_{product_code.lower()}")
|
||||
|
||||
step.click((By.XPATH, Locator.savetokenbtn), f"save_token_{product_code.lower()}")
|
||||
handle_transient_popups(driver)
|
||||
|
||||
complete_time_session_edits_from_saved_token(driver, step, token)
|
||||
|
||||
|
||||
@pytest.mark.trading_token
|
||||
def test_trading_token_creation_idas(driver, test_context):
|
||||
|
||||
config, step, token = get_token_context(driver, test_context)
|
||||
open_app_and_login(driver, step, config)
|
||||
save_session_backup(driver, test_context, "login_session_backup")
|
||||
|
||||
for product_code in get_product_codes(token):
|
||||
product_token = dict(token)
|
||||
product_token["product_code"] = product_code
|
||||
create_and_activate_token(driver, step, product_token)
|
||||
|
||||
logout(driver)
|
||||
|
||||
|
||||
# @pytest.mark.trading_token
|
||||
# def test_trading_token_time_session_edit_idas(driver, test_context):
|
||||
#
|
||||
# config, step, token = get_token_context(driver, test_context)
|
||||
# open_app_and_login(driver, step, config)
|
||||
#
|
||||
# next_day, _ = get_delivery_date_and_expiry(token)
|
||||
#
|
||||
# complete_time_session_edits(driver, step, token, next_day)
|
||||
@@ -0,0 +1,68 @@
|
||||
import os
|
||||
from time import sleep
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.common.exceptions import ElementClickInterceptedException
|
||||
from .config_reader import get_step_delay_seconds
|
||||
|
||||
STEP_DELAY_SECONDS = get_step_delay_seconds()
|
||||
|
||||
|
||||
class StepActions:
|
||||
|
||||
def __init__(self, driver, wait, test_context):
|
||||
|
||||
self.driver = driver
|
||||
self.wait = wait
|
||||
self.folder = test_context["folder"]
|
||||
self.test_context = test_context
|
||||
self.step = 1
|
||||
self.test_context.setdefault("steps", [])
|
||||
|
||||
def capture(self, name):
|
||||
|
||||
filename = f"{self.step:02d}_{name}.png"
|
||||
|
||||
path = os.path.join(self.folder, filename)
|
||||
|
||||
self.driver.save_screenshot(path)
|
||||
self.test_context["steps"].append(
|
||||
{
|
||||
"index": self.step,
|
||||
"name": name,
|
||||
"path": path,
|
||||
"status": "passed",
|
||||
}
|
||||
)
|
||||
|
||||
print(f"STEP {self.step}: {name}")
|
||||
|
||||
self.step += 1
|
||||
sleep(STEP_DELAY_SECONDS)
|
||||
|
||||
|
||||
def click(self, locator, name):
|
||||
|
||||
element = self.wait.until(
|
||||
EC.element_to_be_clickable(locator)
|
||||
)
|
||||
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)
|
||||
|
||||
try:
|
||||
element.click()
|
||||
except ElementClickInterceptedException:
|
||||
self.driver.execute_script("arguments[0].click();", element)
|
||||
|
||||
self.capture(name)
|
||||
|
||||
|
||||
def type(self, locator, value, name):
|
||||
|
||||
element = self.wait.until(
|
||||
EC.visibility_of_element_located(locator)
|
||||
)
|
||||
|
||||
element.clear()
|
||||
element.send_keys(value)
|
||||
|
||||
self.capture(name)
|
||||
@@ -0,0 +1,95 @@
|
||||
import random
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
from .config_reader import load_config
|
||||
from .helpers import (
|
||||
clear_pending_console_input,
|
||||
click_page_body,
|
||||
enter_pressed,
|
||||
login,
|
||||
open_trading_maintenance_page,
|
||||
select_bootstrap_dropdown_by_id,
|
||||
select_ui_dropdown,
|
||||
toggle_bootstrap_dropdown_by_id,
|
||||
toggle_labeled_ui_dropdown,
|
||||
)
|
||||
from .locators import Locator
|
||||
from .step_actions import StepActions
|
||||
|
||||
|
||||
def _bounce_token_generation(driver, step, config):
|
||||
|
||||
token = config["trading_token_idas"]
|
||||
interactions = random.randint(2, 4)
|
||||
|
||||
open_trading_maintenance_page(driver, Locator.tokengeneration)
|
||||
step.capture("open_token_generation_fake")
|
||||
|
||||
for index in range(interactions):
|
||||
if random.choice([True, False]):
|
||||
toggle_bootstrap_dropdown_by_id(driver, Locator.tokenmarketcode)
|
||||
step.capture(f"toggle_market_dropdown_{index + 1}")
|
||||
click_page_body(driver)
|
||||
else:
|
||||
select_bootstrap_dropdown_by_id(driver, Locator.tokenmarketcode, token["market_code"])
|
||||
step.capture(f"select_market_code_{index + 1}")
|
||||
|
||||
toggle_labeled_ui_dropdown(driver, "PRODUCT CODE")
|
||||
step.capture(f"toggle_product_dropdown_{index + 1}")
|
||||
click_page_body(driver)
|
||||
|
||||
time.sleep(random.uniform(0.5, 1.5))
|
||||
|
||||
|
||||
def _bounce_trading_session(driver, step, config):
|
||||
|
||||
open_trading_maintenance_page(driver, Locator.tradingsession)
|
||||
step.capture("open_trading_session_fake")
|
||||
|
||||
step.click((By.XPATH, Locator.addsessionbtn), "open_add_session_fake")
|
||||
|
||||
interactions = random.randint(2, 4)
|
||||
for index in range(interactions):
|
||||
if random.choice([True, False]):
|
||||
select_ui_dropdown(driver, "meInstanceId", config["product"]["me_instance"])
|
||||
step.capture(f"select_me_instance_{index + 1}")
|
||||
else:
|
||||
click_page_body(driver)
|
||||
step.capture(f"idle_on_session_form_{index + 1}")
|
||||
|
||||
time.sleep(random.uniform(0.5, 1.5))
|
||||
|
||||
|
||||
@pytest.mark.fake_activity
|
||||
def test_random_fake_activity_no_save(driver, test_context):
|
||||
|
||||
config = load_config()
|
||||
wait = WebDriverWait(driver, 20)
|
||||
step = StepActions(driver, wait, test_context)
|
||||
|
||||
driver.get(config["url"])
|
||||
step.capture("open_application")
|
||||
|
||||
login(
|
||||
driver,
|
||||
config["login"]["username"],
|
||||
config["login"]["password"]
|
||||
)
|
||||
step.capture("login")
|
||||
|
||||
actions = [_bounce_token_generation, _bounce_trading_session]
|
||||
random.shuffle(actions)
|
||||
|
||||
clear_pending_console_input()
|
||||
print("Fake activity started. Press Enter in the terminal any time to stop.")
|
||||
|
||||
while True:
|
||||
random.choice(actions)(driver, step, config)
|
||||
if enter_pressed():
|
||||
break
|
||||
|
||||
step.capture("manual_stop_point")
|
||||
@@ -0,0 +1,7 @@
|
||||
import pytest
|
||||
|
||||
|
||||
pytest.skip(
|
||||
"Session test moved to tests/setup/sessions/test_session.py",
|
||||
allow_module_level=True,
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
import pytest
|
||||
|
||||
|
||||
pytest.skip(
|
||||
"Trading token test moved to tests/setup/tokens/test_trading_token.py",
|
||||
allow_module_level=True,
|
||||
)
|
||||
Reference in New Issue
Block a user