commit e84b7e9950d636546161e08665b15f264619cb8b Author: Tejas Shasane Date: Tue Sep 8 09:24:02 2026 +0530 Initial commit - Selenium automation framework diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..245807d --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/RUN_COMMANDS.md b/RUN_COMMANDS.md new file mode 100644 index 0000000..d14feb3 --- /dev/null +++ b/RUN_COMMANDS.md @@ -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//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 +``` diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config.json b/config.json new file mode 100644 index 0000000..9811e74 --- /dev/null +++ b/config.json @@ -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" + } + } + } +} diff --git a/config/README.md b/config/README.md new file mode 100644 index 0000000..de4ec04 --- /dev/null +++ b/config/README.md @@ -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..username` and `users.trading_members..password`: trading-member login. +- `users.trading_members..member_id`: trading member / ME instance value used by flows. +- `users.trading_members..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`. diff --git a/config/runtime.json b/config/runtime.json new file mode 100644 index 0000000..c1e8e21 --- /dev/null +++ b/config/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" + ] + } +} diff --git a/data/idas/README.md b/data/idas/README.md new file mode 100644 index 0000000..d947d5a --- /dev/null +++ b/data/idas/README.md @@ -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. diff --git a/data/idas/bidding/README.md b/data/idas/bidding/README.md new file mode 100644 index 0000000..5c483d3 --- /dev/null +++ b/data/idas/bidding/README.md @@ -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`. diff --git a/data/idas/bidding/block_bid/.gitkeep b/data/idas/bidding/block_bid/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/data/idas/bidding/block_bid/.gitkeep @@ -0,0 +1 @@ + diff --git a/data/idas/bidding/json/.gitkeep b/data/idas/bidding/json/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/data/idas/bidding/json/.gitkeep @@ -0,0 +1 @@ + diff --git a/data/idas/bidding/single_bid/.gitkeep b/data/idas/bidding/single_bid/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/data/idas/bidding/single_bid/.gitkeep @@ -0,0 +1 @@ + diff --git a/data/idas/bidding/single_bid/GUJC10010001 - Copy.xlsx b/data/idas/bidding/single_bid/GUJC10010001 - Copy.xlsx new file mode 100644 index 0000000..42e9418 Binary files /dev/null and b/data/idas/bidding/single_bid/GUJC10010001 - Copy.xlsx differ diff --git a/data/idas/bidding/single_bid/GUJC10010001.xlsx b/data/idas/bidding/single_bid/GUJC10010001.xlsx new file mode 100644 index 0000000..42e9418 Binary files /dev/null and b/data/idas/bidding/single_bid/GUJC10010001.xlsx differ diff --git a/data/idas/bidding/uploads/.gitkeep b/data/idas/bidding/uploads/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/data/idas/bidding/uploads/.gitkeep @@ -0,0 +1 @@ + diff --git a/data/idas/bidding/uploads/CHHC10050003.xlsx b/data/idas/bidding/uploads/CHHC10050003.xlsx new file mode 100644 index 0000000..896a62d Binary files /dev/null and b/data/idas/bidding/uploads/CHHC10050003.xlsx differ diff --git a/data/idas/bidding/uploads/GUJC10030013.xlsx b/data/idas/bidding/uploads/GUJC10030013.xlsx new file mode 100644 index 0000000..635aef7 Binary files /dev/null and b/data/idas/bidding/uploads/GUJC10030013.xlsx differ diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..e984976 --- /dev/null +++ b/pytest.ini @@ -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 diff --git a/reports/html/.gitkeep b/reports/html/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/reports/html/.gitkeep @@ -0,0 +1 @@ + diff --git a/reports/logs/.gitkeep b/reports/logs/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/reports/logs/.gitkeep @@ -0,0 +1 @@ + diff --git a/reports/screenshots/.gitkeep b/reports/screenshots/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/reports/screenshots/.gitkeep @@ -0,0 +1 @@ + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3dde252 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +# Core test runner +pytest==8.4.1 +pytest-html==4.2.0 + +# Browser automation +selenium==4.34.2 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/browser.py b/tests/browser.py new file mode 100644 index 0000000..f692ec3 --- /dev/null +++ b/tests/browser.py @@ -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) diff --git a/tests/config_reader.py b/tests/config_reader.py new file mode 100644 index 0000000..b542358 --- /dev/null +++ b/tests/config_reader.py @@ -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()] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..64d18c5 --- /dev/null +++ b/tests/conftest.py @@ -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( + "" + f"{step['index']:02d}" + f"{step['status'].capitalize()}" + f"{step['name']}" + f"Open Screenshot" + "" + ) + + return ( + "
" + "

Step Details

" + "" + "" + f"{''.join(rows)}" + "
#StatusStepScreenshot
" + "
" + ) + + +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" diff --git a/tests/file_generation.py b/tests/file_generation.py new file mode 100644 index 0000000..0d955d8 --- /dev/null +++ b/tests/file_generation.py @@ -0,0 +1,393 @@ +""" +generate_bid_json.py +==================== +Bulk JSON generator for bid submission payloads. + +FOLDER STRUCTURE (input_root): + input_root/ + / e.g. GDAM | DAM | HPDAM + / e.g. Single | Block + / e.g. Buy | Sell + / e.g. C1001 → becomes userId AND tmId + .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 : "