Author SHA1 Message Date
tejasshasane 58b4c502eb Merge pull request 'Development' (#11) from development into master
Reviewed-on: #11
2026-11-02 08:19:55 +01:00
tejasshasane 48d0d786b2 Add DAC and Intraday token setup support 2026-11-02 12:46:15 +05:30
tejasshasane d681002b0f Merge pull request 'Add manual and offset time modes' (#10) from try-captcha-automation into development
Reviewed-on: #10
2026-11-02 06:35:38 +01:00
tejasshasane eb8a831edb Add manual and offset time modes 2026-10-13 09:45:51 +05:30
tejasshasane 2454ca3180 Merge pull request 'Try captcha automation' (#9) from try-captcha-automation into development
Reviewed-on: #9
2026-10-13 06:01:34 +02:00
tejasshasane 771b755ca1 Remove unused UPC time session settings 2026-10-13 09:29:50 +05:30
tejasshasane d9218709f0 session token fix for captcha 2026-10-09 12:39:32 +05:30
tejasshasane 385a9e22cc Update requirements.txt 2026-10-08 17:57:58 +05:30
tejasshasane 24f2c1fe6a Add CAPTCHA automation beta flow 2026-10-08 17:36:30 +05:30
tejasshasane 631f64b84f Merge pull request 'Fix REC token creation and time session flow' (#8) from development into master
Reviewed-on: #8
2026-10-06 13:34:19 +02:00
tejasshasane c42cc83aa1 Fix REC token creation and time session flow 2026-10-06 17:01:26 +05:30
tejasshasane d6c1f7a3d3 Merge pull request 'Add shared configuration template' (#7) from development into master
Reviewed-on: #7
2026-10-06 10:10:36 +02:00
tejasshasane 106fc29c6a Add shared configuration template 2026-10-06 13:32:09 +05:30
tejasshasane 0312a542a2 Merge pull request 'Fix UPC token-only variant selection' (#6) from development into master
Reviewed-on: #6
2026-10-06 07:14:33 +02:00
tejasshasane 4218f8572d Fix UPC token-only variant selection 2026-10-06 10:36:02 +05:30
tejasshasane d89ebdf869 Merge pull request 'Archify' (#5) from development into master
Reviewed-on: #5
2026-09-29 14:09:30 +02:00
tejasshasane f0496b3d67 Archify 2026-09-29 17:38:15 +05:30
tejasshasane 5ab89ce005 Merge pull request 'Add configurable UPC and RA setup flows' (#4) from development into master
Reviewed-on: #4
2026-09-29 10:07:42 +02:00
tejasshasane ec38d5f65f Add configurable UPC and RA setup flows 2026-09-29 13:33:45 +05:30
tejasshasane a0473cde1d Merge pull request 'Fix setup report folders to use product and flow' (#3) from development into master
Reviewed-on: #3
2026-09-23 09:15:55 +02:00
tejasshasane 97158d2006 Fix setup report folders to use product and flow 2026-09-23 12:41:29 +05:30
tejasshasane 394d2749bf Add RTM session and configurable token generator flow 2026-09-23 10:16:28 +05:30
tejasshasane 9d16b20eb4 Add product-specific session and token setup 2026-09-23 09:27:55 +05:30
50 changed files with 51388 additions and 895 deletions
@@ -0,0 +1,395 @@
{
"schema_version": 1,
"diagram_type": "architecture",
"meta": {
"title": "PXIL Automation Runtime",
"output": ".archify/architecture-pxil-runtime-20260929-151855/pxil-runtime.html",
"quality_profile": "showcase",
"locale": "en",
"repository": {
"url": "https://git.tejasshasane.xyz/tejasshasane/pxil-selenium-automation.git",
"revision": "5ab89ce0059525b31558a1fa82251a2a6f610dd7",
"link_mode": "local-only"
}
},
"components": [
{
"id": "runner",
"type": "backend",
"label": "pytest runner",
"sublabel": "Selection + fixture lifecycle",
"pos": [
60,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/conftest.py",
"line": 37,
"end_line": 62
},
{
"path": "pytest.ini",
"line": 1,
"end_line": 14
}
]
},
{
"id": "flows",
"type": "backend",
"label": "Automation flows",
"sublabel": "Setup / bidding / matching",
"pos": [
360,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/regression/exchange/idas/bidding/test_idas_exchange_bidding_flow.py",
"line": 54,
"end_line": 90
},
{
"path": "tests/regression/exchange/idas/matching/test_idas_exchange_matching_flow.py",
"line": 76,
"end_line": 156
},
{
"path": "tests/setup/test_combined_setup.py",
"line": 13,
"end_line": 37
}
]
},
{
"id": "browser",
"type": "frontend",
"label": "Chrome + WebDriver",
"sublabel": "Selenium browser dependency",
"pos": [
680,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/browser.py",
"line": 1,
"end_line": 14
},
{
"path": "tests/regression/auth.py",
"line": 46,
"end_line": 86
}
],
"tag": "Manual CAPTCHA for Pratyay"
},
{
"id": "pratyay",
"type": "external",
"label": "Pratyay portal",
"sublabel": "Exchange / member UI",
"pos": [
970,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/regression/exchange/idas/bidding/test_idas_exchange_bidding_flow.py",
"line": 68,
"end_line": 90
},
{
"path": "tests/regression/exchange/idas/matching/test_idas_exchange_matching_flow.py",
"line": 47,
"end_line": 59
}
]
},
{
"id": "config",
"type": "database",
"label": "Configuration",
"sublabel": "JSON + role credentials",
"pos": [
360,
100
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/config_reader.py",
"line": 6,
"end_line": 32
},
{
"path": "tests/regression/config_reader.py",
"line": 5,
"end_line": 29
},
{
"path": "tests/regression/config_reader.py",
"line": 121,
"end_line": 150
}
]
},
{
"id": "inputs",
"type": "database",
"label": "Bid input files",
"sublabel": "Excel / JSON / upload folders",
"pos": [
360,
560
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/regression/exchange/idas/bidding/input_manifest.py",
"line": 4,
"end_line": 28
},
{
"path": "tests/regression/exchange/idas/bidding/json_reader.py",
"line": 5,
"end_line": 35
},
{
"path": "tests/regression/exchange/idas/bidding/test_idas_exchange_bidding_flow.py",
"line": 217,
"end_line": 249
}
]
},
{
"id": "reporting",
"type": "backend",
"label": "Evidence reporting",
"sublabel": "Step capture + pytest hooks",
"pos": [
60,
560
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/step_actions.py",
"line": 10,
"end_line": 40
},
{
"path": "tests/conftest.py",
"line": 104,
"end_line": 175
}
]
},
{
"id": "artifacts",
"type": "database",
"label": "Run artifacts",
"sublabel": "HTML / screenshots / logs",
"pos": [
60,
770
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/conftest.py",
"line": 37,
"end_line": 48
},
{
"path": "tests/conftest.py",
"line": 70,
"end_line": 97
},
{
"path": "tests/step_actions.py",
"line": 21,
"end_line": 35
}
]
},
{
"id": "mco",
"type": "external",
"label": "MCO portal",
"sublabel": "Independent login-only test",
"pos": [
970,
560
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/regression/exchange/idas/matching/test_idas_mco_login.py",
"line": 13,
"end_line": 36
}
],
"tag": "Only when enabled"
}
],
"boundaries": [
{
"kind": "region",
"label": "LOCAL AUTOMATION HOST | trusted code, inputs and credentials",
"wraps": [
"runner",
"flows",
"browser",
"config",
"inputs",
"reporting",
"artifacts"
],
"pad": 28
},
{
"kind": "security-group",
"label": "Browser process boundary",
"wraps": [
"browser"
],
"pad": 18
},
{
"kind": "region",
"label": "REMOTE PORTALS | separate authentication",
"wraps": [
"pratyay",
"mco"
],
"pad": 28
}
],
"connections": [
{
"id": "selected-tests",
"from": "runner",
"to": "flows",
"label": "run tests",
"variant": "emphasis"
},
{
"id": "ui-actions",
"from": "flows",
"to": "browser",
"label": "UI commands",
"variant": "emphasis"
},
{
"id": "portal-session",
"from": "browser",
"to": "pratyay",
"label": "login + UI",
"variant": "emphasis"
},
{
"id": "read-settings",
"from": "flows",
"to": "config",
"label": "read settings"
},
{
"id": "read-bids",
"from": "flows",
"to": "inputs",
"label": "read bids"
},
{
"id": "report-hooks",
"from": "runner",
"to": "reporting",
"label": "test hooks"
},
{
"id": "write-evidence",
"from": "reporting",
"to": "artifacts",
"label": "write evidence"
},
{
"id": "optional-mco",
"from": "browser",
"to": "mco",
"label": "if enabled: login",
"variant": "dashed"
}
],
"cards": [
{
"dot": "cyan",
"title": "Primary path",
"items": [
"pytest runs selected tests; flow code controls Selenium and Chrome, which interacts with the remote Pratyay UI.",
"Pratyay login fills credentials and pauses for manual CAPTCHA entry before submission (tests/regression/auth.py:46-122).",
"Arrows describe control and I/O; results return to pytest. No direct backend API or database integration is asserted."
]
},
{
"dot": "violet",
"title": "Flow selection and capabilities",
"items": [
"Root config.json selects setup products and session/token actions; config/runtime.json selects regression environment, role, product and flow.",
"Bidding supports Excel/JSON grids, files and folders. Optional parallel folder workers create their own browsers (bidding flow:551-587).",
"Matching requires the exchange role; rejected or unsupported bid-transfer status stops matching (matching flow:28-44, 123-135)."
]
},
{
"dot": "rose",
"title": "Trust and external dependencies",
"items": [
"Boundaries show host/process and remote ownership, not verified network isolation or firewall policy.",
"Credentials come from local JSON and are entered into remote login forms. Values and endpoint addresses are omitted.",
"Chrome/WebDriver is an external runtime dependency. Pratyay and MCO server internals and transport guarantees are outside the observed source.",
"MCO is an independent, enabled-only dashboard login test; no Pratyay-to-MCO backend connection is inferred."
]
},
{
"dot": "emerald",
"title": "Evidence and scope",
"items": [
"StepActions requests browser screenshots; pytest hooks attach evidence and configure pytest-html. Reports live under reports/.",
"Matching configures browser downloads separately; setup can save session backups. These details are kept off the main path.",
"Snapshot: clean repository at 5ab89ce0059525b31558a1fa82251a2a6f610dd7. Sources use local-only links.",
"Registered reports/order-book/trade-book markers are placeholders, not implemented runtime services (RUN_COMMANDS.md)."
]
}
]
}
@@ -0,0 +1,721 @@
{
"schemaVersion": 1,
"ok": true,
"command": "browser-check",
"evidenceKind": "automated-browser",
"status": "pass",
"visualReview": "not-requested",
"provenance": "current",
"deliveryReceiptId": "95e7d275-4f11-44f8-af68-086f2d8272bd",
"artifact": {
"path": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\pxil-runtime.html",
"sha256": "3db6aa51a98c38d8aebafa9dd0682f3ea5c0ffd1d554c2ac6f000c94b73a193a",
"bytes": 767845
},
"state": {
"detail": "read",
"motion": "still"
},
"chrome": {
"status": "available",
"executable": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
},
"diagnostics": [],
"containment": {
"status": "pass",
"policy": "fit-or-reader-declared-readable-vertical-scroll",
"viewports": [
{
"width": 1440,
"height": 900,
"theme": "light",
"innerWidth": 1440,
"innerHeight": 900,
"scrollWidth": 1440,
"scrollHeight": 920,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 12,
"headerPx": 52,
"diagramChromePx": 78,
"svgPx": 777,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1600,
"height": 1000,
"theme": "light",
"innerWidth": 1600,
"innerHeight": 1000,
"scrollWidth": 1600,
"scrollHeight": 1000,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1058,
"diagramWidth": 1028,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 780,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.697068403908795,
"minimumProjectedNonEdgeTextPx": 7.534201954397394,
"minimumProjectedEdgeTextPx": 6.697068403908795,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1920,
"height": 1080,
"theme": "light",
"innerWidth": 1920,
"innerHeight": 1080,
"scrollWidth": 1920,
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1163,
"diagramWidth": 1133,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 860,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 7.381107491856677,
"minimumProjectedNonEdgeTextPx": 8.303745928338762,
"minimumProjectedEdgeTextPx": 7.381107491856677,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 2048,
"height": 1320,
"theme": "light",
"innerWidth": 2048,
"innerHeight": 1320,
"scrollWidth": 2048,
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1476,
"diagramWidth": 1426,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 68,
"diagramChromePx": 94,
"svgPx": 1082,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 8,
"minimumProjectedNonEdgeTextPx": 9,
"minimumProjectedEdgeTextPx": 8,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
}
]
},
"themeStates": {
"status": "pass",
"viewports": [
{
"width": 1440,
"height": 900,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 1600,
"height": 1000,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 1920,
"height": 1080,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 2048,
"height": 1320,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 1440,
"height": 900,
"requestedTheme": "dark",
"resolvedTheme": "dark",
"ok": true
},
{
"width": 2048,
"height": 1320,
"requestedTheme": "dark",
"resolvedTheme": "dark",
"ok": true
}
]
},
"readability": {
"status": "pass",
"minimumProjectedNodeTextPx": 6,
"viewports": [
{
"width": 1440,
"height": 900,
"theme": "light",
"innerWidth": 1440,
"innerHeight": 900,
"scrollWidth": 1440,
"scrollHeight": 920,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 12,
"headerPx": 52,
"diagramChromePx": 78,
"svgPx": 777,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1600,
"height": 1000,
"theme": "light",
"innerWidth": 1600,
"innerHeight": 1000,
"scrollWidth": 1600,
"scrollHeight": 1000,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1058,
"diagramWidth": 1028,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 780,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.697068403908795,
"minimumProjectedNonEdgeTextPx": 7.534201954397394,
"minimumProjectedEdgeTextPx": 6.697068403908795,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1920,
"height": 1080,
"theme": "light",
"innerWidth": 1920,
"innerHeight": 1080,
"scrollWidth": 1920,
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1163,
"diagramWidth": 1133,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 860,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 7.381107491856677,
"minimumProjectedNonEdgeTextPx": 8.303745928338762,
"minimumProjectedEdgeTextPx": 7.381107491856677,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 2048,
"height": 1320,
"theme": "light",
"innerWidth": 2048,
"innerHeight": 1320,
"scrollWidth": 2048,
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1476,
"diagramWidth": 1426,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 68,
"diagramChromePx": 94,
"svgPx": 1082,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 8,
"minimumProjectedNonEdgeTextPx": 9,
"minimumProjectedEdgeTextPx": 8,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
}
]
},
"viewerChrome": {
"status": "pass",
"viewports": [
{
"width": 1440,
"height": 900,
"theme": "light",
"innerWidth": 1440,
"innerHeight": 900,
"scrollWidth": 1440,
"scrollHeight": 920,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 12,
"headerPx": 52,
"diagramChromePx": 78,
"svgPx": 777,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1600,
"height": 1000,
"theme": "light",
"innerWidth": 1600,
"innerHeight": 1000,
"scrollWidth": 1600,
"scrollHeight": 1000,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1058,
"diagramWidth": 1028,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 780,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.697068403908795,
"minimumProjectedNonEdgeTextPx": 7.534201954397394,
"minimumProjectedEdgeTextPx": 6.697068403908795,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1920,
"height": 1080,
"theme": "light",
"innerWidth": 1920,
"innerHeight": 1080,
"scrollWidth": 1920,
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1163,
"diagramWidth": 1133,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 860,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 7.381107491856677,
"minimumProjectedNonEdgeTextPx": 8.303745928338762,
"minimumProjectedEdgeTextPx": 7.381107491856677,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 2048,
"height": 1320,
"theme": "light",
"innerWidth": 2048,
"innerHeight": 1320,
"scrollWidth": 2048,
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1476,
"diagramWidth": 1426,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 68,
"diagramChromePx": 94,
"svgPx": 1082,
"cardsPx": 0,
"viewBoxHeight": 932
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 8,
"minimumProjectedNonEdgeTextPx": 9,
"minimumProjectedEdgeTextPx": 8,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
}
]
},
"captures": {
"status": "not-requested",
"screenshots": [],
"contactSheet": null
},
"sidecars": {
"receipt": "pxil-runtime.browser-check.json",
"files": []
}
}
@@ -0,0 +1,17 @@
{
"schemaVersion": 1,
"receiptId": "95e7d275-4f11-44f8-af68-086f2d8272bd",
"status": "current",
"command": "deliver",
"type": "architecture",
"input": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\candidate.json",
"output": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\pxil-runtime.html",
"specification": {
"sha256": "5255f87576eeb3afd6713d933790d481cf9e8034f24453367adc240aa958ccbc",
"bytes": 20698
},
"artifact": {
"sha256": "3db6aa51a98c38d8aebafa9dd0682f3ea5c0ffd1d554c2ac6f000c94b73a193a",
"bytes": 767845
}
}
@@ -0,0 +1,47 @@
{
"schemaVersion": 1,
"ok": true,
"command": "finalize",
"status": "pass",
"type": "architecture",
"quality": "showcase",
"specification": {
"path": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\candidate.json",
"sha256": "5255f87576eeb3afd6713d933790d481cf9e8034f24453367adc240aa958ccbc",
"bytes": 20698
},
"artifact": {
"path": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\pxil-runtime.html",
"sha256": "3db6aa51a98c38d8aebafa9dd0682f3ea5c0ffd1d554c2ac6f000c94b73a193a",
"bytes": 767845
},
"gates": {
"validate": "pass",
"deliver": "pass",
"check": "pass",
"browser-check": "pass"
},
"diagnostics": [],
"diagnosticSummary": {
"total": 0,
"shown": 0,
"truncated": false
},
"evidence": {
"receipt": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\pxil-runtime.finalize.json",
"summaryReceipt": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\pxil-runtime.finalize-summary.json",
"browserCheckReceipt": "D:\\Selenium Test\\.archify\\architecture-pxil-runtime-20260929-151855\\pxil-runtime.browser-check.json"
},
"update": {
"status": "current",
"installedVersion": "3.0.1",
"availableVersion": "3.0.1",
"releaseNotes": null,
"checkedAt": "2026-09-29T09:50:05.829Z",
"source": "cache",
"noticeRequired": false,
"noticeText": null
},
"visualReview": "not-requested",
"durationMs": 10625
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,461 @@
{
"schema_version": 1,
"diagram_type": "architecture",
"meta": {
"title": "PXIL Runtime: Sessions, Tokens and Regression",
"output": ".archify/architecture-pxil-setup-runtime-20260929-155341/pxil-setup-runtime.html",
"quality_profile": "showcase",
"locale": "en",
"repository": {
"url": "https://git.tejasshasane.xyz/tejasshasane/pxil-selenium-automation.git",
"revision": "5ab89ce0059525b31558a1fa82251a2a6f610dd7",
"link_mode": "local-only"
}
},
"components": [
{
"id": "runner",
"type": "backend",
"label": "pytest runner",
"sublabel": "Selection + fixture lifecycle",
"pos": [
60,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/conftest.py",
"line": 37,
"end_line": 62
},
{
"path": "pytest.ini",
"line": 1,
"end_line": 14
}
]
},
{
"id": "flows",
"type": "backend",
"label": "Automation flows",
"sublabel": "Setup / bidding / matching",
"pos": [
360,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/regression/exchange/idas/bidding/test_idas_exchange_bidding_flow.py",
"line": 54,
"end_line": 90
},
{
"path": "tests/regression/exchange/idas/matching/test_idas_exchange_matching_flow.py",
"line": 76,
"end_line": 156
},
{
"path": "tests/setup/test_combined_setup.py",
"line": 13,
"end_line": 37
}
]
},
{
"id": "browser",
"type": "frontend",
"label": "Chrome + WebDriver",
"sublabel": "Selenium browser dependency",
"pos": [
680,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/browser.py",
"line": 1,
"end_line": 14
},
{
"path": "tests/regression/auth.py",
"line": 46,
"end_line": 86
}
],
"tag": "Manual CAPTCHA for Pratyay"
},
{
"id": "pratyay",
"type": "external",
"label": "Pratyay portal",
"sublabel": "Exchange / member UI",
"pos": [
970,
300
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/regression/exchange/idas/bidding/test_idas_exchange_bidding_flow.py",
"line": 68,
"end_line": 90
},
{
"path": "tests/regression/exchange/idas/matching/test_idas_exchange_matching_flow.py",
"line": 47,
"end_line": 59
}
]
},
{
"id": "config",
"type": "database",
"label": "Config + bid inputs",
"sublabel": "JSON / Excel / upload folders",
"pos": [
360,
100
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/config_reader.py",
"line": 6,
"end_line": 32
},
{
"path": "tests/regression/config_reader.py",
"line": 5,
"end_line": 29
},
{
"path": "tests/regression/exchange/idas/bidding/test_idas_exchange_bidding_flow.py",
"line": 217,
"end_line": 249
}
]
},
{
"id": "reporting",
"type": "backend",
"label": "Evidence reporting",
"sublabel": "Step capture + pytest hooks",
"pos": [
60,
560
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/step_actions.py",
"line": 10,
"end_line": 40
},
{
"path": "tests/conftest.py",
"line": 104,
"end_line": 175
}
]
},
{
"id": "artifacts",
"type": "database",
"label": "Run artifacts",
"sublabel": "HTML / screenshots / logs",
"pos": [
60,
770
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/conftest.py",
"line": 37,
"end_line": 48
},
{
"path": "tests/conftest.py",
"line": 70,
"end_line": 97
},
{
"path": "tests/step_actions.py",
"line": 21,
"end_line": 35
}
]
},
{
"id": "mco",
"type": "external",
"label": "MCO portal",
"sublabel": "Independent login-only test",
"pos": [
970,
560
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/regression/exchange/idas/matching/test_idas_mco_login.py",
"line": 13,
"end_line": 36
}
],
"tag": "Only when enabled"
},
{
"id": "sessions",
"type": "backend",
"label": "Trading session setup",
"sublabel": "ME instance + times + Save",
"tag": "When run_session = true",
"pos": [
360,
560
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/setup/sessions/test_session.py",
"line": 24,
"end_line": 64
},
{
"path": "tests/setup/test_combined_setup.py",
"line": 21,
"end_line": 37
}
]
},
{
"id": "tokens",
"type": "backend",
"label": "Trading token setup",
"sublabel": "Per product code + Save",
"tag": "When run_token = true",
"pos": [
360,
800
],
"size": [
190,
74
],
"sources": [
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 150,
"end_line": 180
},
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 212,
"end_line": 326
},
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 328,
"end_line": 342
}
]
}
],
"boundaries": [
{
"kind": "region",
"label": "LOCAL AUTOMATION HOST | trusted code, inputs and credentials",
"wraps": [
"runner",
"flows",
"browser",
"config",
"reporting",
"artifacts",
"sessions",
"tokens"
],
"pad": 28
},
{
"kind": "security-group",
"label": "Browser process boundary",
"wraps": [
"browser"
],
"pad": 18
},
{
"kind": "region",
"label": "REMOTE PORTALS | separate authentication",
"wraps": [
"pratyay",
"mco"
],
"pad": 28
}
],
"connections": [
{
"id": "selected-tests",
"from": "runner",
"to": "flows",
"label": "run tests",
"variant": "emphasis"
},
{
"id": "ui-actions",
"from": "flows",
"to": "browser",
"label": "UI commands",
"variant": "emphasis"
},
{
"id": "portal-session",
"from": "browser",
"to": "pratyay",
"label": "login + UI",
"variant": "emphasis"
},
{
"id": "read-settings",
"from": "flows",
"to": "config",
"label": "read files"
},
{
"id": "report-hooks",
"from": "runner",
"to": "reporting",
"label": "test hooks"
},
{
"id": "write-evidence",
"from": "reporting",
"to": "artifacts",
"label": "write evidence"
},
{
"id": "optional-mco",
"from": "browser",
"to": "mco",
"label": "if enabled: login",
"variant": "dashed"
},
{
"id": "session-enabled",
"from": "flows",
"to": "sessions",
"label": "run_session",
"variant": "default"
},
{
"id": "combined-order",
"from": "sessions",
"to": "tokens",
"label": "both enabled: next",
"variant": "dashed"
},
{
"id": "token-only",
"from": "flows",
"to": "tokens",
"label": "token only",
"variant": "default",
"fromSide": "bottom",
"toSide": "right"
}
],
"cards": [
{
"dot": "amber",
"title": "Session and token execution",
"items": [
"After setup login and manual CAPTCHA: session-only runs create_session; token-only runs run_token_setup; combined runs create_session then run_token_setup for each product code.",
"Both setup modules use the shared Selenium driver and StepActions through Automation flows. The dashed session-to-token arrow denotes controller ordering, not a direct module call (tests/setup/test_combined_setup.py:21-37).",
"Session creation opens Trading Session, selects the ME instance, fills start/end times, then clicks Save (tests/setup/sessions/test_session.py:24-40).",
"These are trading sessions and trading tokens, distinct from browser authentication/session backups."
]
},
{
"dot": "orange",
"title": "Product-specific token behavior",
"items": [
"IDAS/default branch: create token, set delivery dates and expiry, Save, edit ORDER_NOR / ORDER_EXCH / TRADE times, then Save and Activate (tests/setup/tokens/test_trading_token.py:150-180, 264-325).",
"RTM branch fills delivery and session times and saves the token; activation is not called (lines 212-225, 254-261).",
"UPC branch fills configured order/trade times, saves, then resets the generator (lines 228-258).",
"config.json selects enabled products and run_session/run_token flags. Disabled products and both-false products are omitted; UPC variants have additional routing rules (tests/config_reader.py:36-83)."
]
},
{
"dot": "cyan",
"title": "Primary path",
"items": [
"pytest runs selected tests; flow code controls Selenium and Chrome, which interacts with the remote Pratyay UI.",
"Pratyay login fills credentials and pauses for manual CAPTCHA entry before submission (tests/regression/auth.py:46-122).",
"Arrows describe control and I/O; results return to pytest. No direct backend API or database integration is asserted."
]
},
{
"dot": "violet",
"title": "Flow selection and capabilities",
"items": [
"Root config.json selects setup products and session/token actions; config/runtime.json selects regression environment, role, product and flow.",
"Bidding supports Excel/JSON grids, files and folders. Optional parallel folder workers create their own browsers (bidding flow:551-587).",
"Matching requires the exchange role; rejected or unsupported bid-transfer status stops matching (matching flow:28-44, 123-135)."
]
},
{
"dot": "rose",
"title": "Trust and external dependencies",
"items": [
"Boundaries show host/process and remote ownership, not verified network isolation or firewall policy.",
"Credentials come from local JSON and are entered into remote login forms. Values and endpoint addresses are omitted.",
"Chrome/WebDriver is an external runtime dependency. Pratyay and MCO server internals and transport guarantees are outside the observed source.",
"MCO is an independent, enabled-only dashboard login test; no Pratyay-to-MCO backend connection is inferred."
]
},
{
"dot": "emerald",
"title": "Evidence and scope",
"items": [
"StepActions requests browser screenshots; pytest hooks attach evidence and configure pytest-html. Reports live under reports/.",
"Matching configures browser downloads separately; setup can save session backups. These details are kept off the main path.",
"Source snapshot: 5ab89ce0059525b31558a1fa82251a2a6f610dd7; tracked code unchanged. Only generated .archify/ artifacts are untracked. Sources use local-only links.",
"Registered reports/order-book/trade-book markers are placeholders, not implemented runtime services (RUN_COMMANDS.md)."
]
}
]
}
@@ -0,0 +1,721 @@
{
"schemaVersion": 1,
"ok": true,
"command": "browser-check",
"evidenceKind": "automated-browser",
"status": "pass",
"visualReview": "not-requested",
"provenance": "current",
"deliveryReceiptId": "4a5fda98-da80-480c-a108-3987d32a0fb2",
"artifact": {
"path": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\pxil-setup-runtime.html",
"sha256": "7f1a2d9fcc2ed49fc6a2c2c9c541bd0620ab0ca1cf04b5e388dae27d01e5b3ec",
"bytes": 773779
},
"state": {
"detail": "read",
"motion": "still"
},
"chrome": {
"status": "available",
"executable": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
},
"diagnostics": [],
"containment": {
"status": "pass",
"policy": "fit-or-reader-declared-readable-vertical-scroll",
"viewports": [
{
"width": 1440,
"height": 900,
"theme": "light",
"innerWidth": 1440,
"innerHeight": 900,
"scrollWidth": 1440,
"scrollHeight": 945,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 12,
"headerPx": 52,
"diagramChromePx": 78,
"svgPx": 802,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1600,
"height": 1000,
"theme": "light",
"innerWidth": 1600,
"innerHeight": 1000,
"scrollWidth": 1600,
"scrollHeight": 1011,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 802,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1920,
"height": 1080,
"theme": "light",
"innerWidth": 1920,
"innerHeight": 1080,
"scrollWidth": 1920,
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1128,
"diagramWidth": 1098,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 860,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 7.153094462540716,
"minimumProjectedNonEdgeTextPx": 8.047231270358306,
"minimumProjectedEdgeTextPx": 7.153094462540716,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 2048,
"height": 1320,
"theme": "light",
"innerWidth": 2048,
"innerHeight": 1320,
"scrollWidth": 2048,
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1431,
"diagramWidth": 1381,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 68,
"diagramChromePx": 94,
"svgPx": 1082,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 8,
"minimumProjectedNonEdgeTextPx": 9,
"minimumProjectedEdgeTextPx": 8,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
}
]
},
"themeStates": {
"status": "pass",
"viewports": [
{
"width": 1440,
"height": 900,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 1600,
"height": 1000,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 1920,
"height": 1080,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 2048,
"height": 1320,
"requestedTheme": "light",
"resolvedTheme": "light",
"ok": true
},
{
"width": 1440,
"height": 900,
"requestedTheme": "dark",
"resolvedTheme": "dark",
"ok": true
},
{
"width": 2048,
"height": 1320,
"requestedTheme": "dark",
"resolvedTheme": "dark",
"ok": true
}
]
},
"readability": {
"status": "pass",
"minimumProjectedNodeTextPx": 6,
"viewports": [
{
"width": 1440,
"height": 900,
"theme": "light",
"innerWidth": 1440,
"innerHeight": 900,
"scrollWidth": 1440,
"scrollHeight": 945,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 12,
"headerPx": 52,
"diagramChromePx": 78,
"svgPx": 802,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1600,
"height": 1000,
"theme": "light",
"innerWidth": 1600,
"innerHeight": 1000,
"scrollWidth": 1600,
"scrollHeight": 1011,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 802,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1920,
"height": 1080,
"theme": "light",
"innerWidth": 1920,
"innerHeight": 1080,
"scrollWidth": 1920,
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1128,
"diagramWidth": 1098,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 860,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 7.153094462540716,
"minimumProjectedNonEdgeTextPx": 8.047231270358306,
"minimumProjectedEdgeTextPx": 7.153094462540716,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 2048,
"height": 1320,
"theme": "light",
"innerWidth": 2048,
"innerHeight": 1320,
"scrollWidth": 2048,
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1431,
"diagramWidth": 1381,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 68,
"diagramChromePx": 94,
"svgPx": 1082,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 8,
"minimumProjectedNonEdgeTextPx": 9,
"minimumProjectedEdgeTextPx": 8,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
}
]
},
"viewerChrome": {
"status": "pass",
"viewports": [
{
"width": 1440,
"height": 900,
"theme": "light",
"innerWidth": 1440,
"innerHeight": 900,
"scrollWidth": 1440,
"scrollHeight": 945,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 12,
"headerPx": 52,
"diagramChromePx": 78,
"svgPx": 802,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1600,
"height": 1000,
"theme": "light",
"innerWidth": 1600,
"innerHeight": 1000,
"scrollWidth": 1600,
"scrollHeight": 1011,
"overflowX": false,
"overflowY": true,
"verticalScrollAccepted": true,
"overflowDisposition": "readable-vertical-scroll",
"readerLayout": "adaptive",
"readerOverflow": "authored",
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1054,
"diagramWidth": 1024,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 802,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 6.671009771986971,
"minimumProjectedNonEdgeTextPx": 7.504885993485343,
"minimumProjectedEdgeTextPx": 6.671009771986971,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 1920,
"height": 1080,
"theme": "light",
"innerWidth": 1920,
"innerHeight": 1080,
"scrollWidth": 1920,
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1128,
"diagramWidth": 1098,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 60,
"diagramChromePx": 84,
"svgPx": 860,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 7.153094462540716,
"minimumProjectedNonEdgeTextPx": 8.047231270358306,
"minimumProjectedEdgeTextPx": 7.153094462540716,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
},
{
"width": 2048,
"height": 1320,
"theme": "light",
"innerWidth": 2048,
"innerHeight": 1320,
"scrollWidth": 2048,
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
"verticalScrollAccepted": false,
"overflowDisposition": "contained",
"readerLayout": "adaptive",
"readerOverflow": null,
"readerFit": "intrinsic-height",
"ok": true,
"readerWidth": 1431,
"diagramWidth": 1381,
"pageComposition": {
"bodyPaddingPx": 64,
"headerPx": 68,
"diagramChromePx": 94,
"svgPx": 1082,
"cardsPx": 0,
"viewBoxHeight": 962
},
"viewBoxWidth": 1228,
"minimumProjectedNodeTextPx": 8,
"minimumProjectedNonEdgeTextPx": 9,
"minimumProjectedEdgeTextPx": 8,
"minimumProjectedNodeText": "run tests",
"minimumProjectedNodeTextDetail": "edge",
"minimumProjectedNodeTextOwner": {
"kind": "edge",
"id": "selected-tests",
"from": "runner",
"to": "flows"
},
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
"dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
"viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
}
]
},
"captures": {
"status": "not-requested",
"screenshots": [],
"contactSheet": null
},
"sidecars": {
"receipt": "pxil-setup-runtime.browser-check.json",
"files": []
}
}
@@ -0,0 +1,17 @@
{
"schemaVersion": 1,
"receiptId": "4a5fda98-da80-480c-a108-3987d32a0fb2",
"status": "current",
"command": "deliver",
"type": "architecture",
"input": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\candidate.json",
"output": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\pxil-setup-runtime.html",
"specification": {
"sha256": "1419cbca3fed61e28342a8b9767aa43fa85a7b18ff380e93ebedccfa96f9549b",
"bytes": 25091
},
"artifact": {
"sha256": "7f1a2d9fcc2ed49fc6a2c2c9c541bd0620ab0ca1cf04b5e388dae27d01e5b3ec",
"bytes": 773779
}
}
@@ -0,0 +1,85 @@
{
"schemaVersion": 1,
"ok": true,
"command": "finalize",
"status": "pass",
"type": "architecture",
"quality": "showcase",
"specification": {
"path": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\candidate.json",
"sha256": "1419cbca3fed61e28342a8b9767aa43fa85a7b18ff380e93ebedccfa96f9549b",
"bytes": 25091
},
"artifact": {
"path": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\pxil-setup-runtime.html",
"sha256": "7f1a2d9fcc2ed49fc6a2c2c9c541bd0620ab0ca1cf04b5e388dae27d01e5b3ec",
"bytes": 773779
},
"gates": {
"validate": "pass",
"deliver": "pass",
"check": "pass",
"browser-check": "pass"
},
"diagnostics": [],
"diagnosticSummary": {
"total": 0,
"shown": 0,
"truncated": false
},
"evidence": {
"receipt": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\pxil-setup-runtime.finalize.json",
"summaryReceipt": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\pxil-setup-runtime.finalize-summary.json",
"browserCheckReceipt": "D:\\Selenium Test\\.archify\\architecture-pxil-setup-runtime-20260929-155341\\pxil-setup-runtime.browser-check.json"
},
"update": {
"status": "current",
"installedVersion": "3.0.1",
"availableVersion": "3.0.1",
"releaseNotes": null,
"checkedAt": "2026-09-29T09:50:05.829Z",
"source": "cache",
"noticeRequired": false,
"noticeText": null
},
"visualReview": "not-requested",
"durationMs": 10108,
"visualReviewRecommendation": {
"action": "inspect-route-readability",
"signals": {
"routesOverSuggestedBends": 1
},
"reason": "Automated gates passed, but crossings or detours still need perceptual review before claiming visual quality.",
"affectedRoutes": {
"crossings": [],
"detours": [
{
"relationship": {
"id": "token-only",
"from": "flows",
"to": "tokens",
"label": "token only",
"collectionIndex": 9,
"artifactIndex": 10
},
"bends": 3,
"stretch": 1.087,
"directCorridorBlockers": [
{
"id": "sessions",
"label": "Trading session setup",
"box": [
360,
560,
190,
74
]
}
]
}
],
"truncated": false
},
"repair": "Trace these relationships at the desktop viewport. For Architecture, use references/architecture-layout-repair.md: reflow a blocked main path or tangled connected scene, and repair an isolated defect locally only when the surrounding composition is accepted. Preserve all semantic content and user-fixed geometry. Rerun finalize once after the edit."
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,430 @@
{
"schema_version": 2,
"diagram_type": "workflow",
"meta": {
"title": "PXIL Session + Trading Token Setup",
"subtitle": "Configuration-driven pytest/Selenium workflow",
"output": ".archify/workflow-pxil-session-token-20260929-160100/pxil-session-token.html",
"quality_profile": "showcase",
"animation": "none",
"repository": {
"url": "https://git.tejasshasane.xyz/tejasshasane/pxil-selenium-automation.git",
"revision": "5ab89ce0059525b31558a1fa82251a2a6f610dd7",
"link_mode": "local-only"
}
},
"lanes": [
{
"id": "config",
"label": "Configuration \u0026 selection"
},
{
"id": "pytest",
"label": "Pytest orchestration"
},
{
"id": "browser",
"label": "Browser automation"
},
{
"id": "portal",
"label": "Pratyay portal"
}
],
"phases": [
{
"id": "resolve",
"label": "Resolve run",
"fromCol": 0,
"toCol": 1
},
{
"id": "login",
"label": "Authenticate",
"fromCol": 2,
"toCol": 2,
"variant": "security"
},
{
"id": "setup",
"label": "Create session + tokens",
"fromCol": 3,
"toCol": 4,
"variant": "emphasis"
},
{
"id": "evidence",
"label": "Capture evidence",
"fromCol": 5,
"toCol": 5,
"variant": "dashed"
}
],
"groups": [
{
"id": "config_group",
"label": "Product and mode resolution",
"lane": "config",
"fromCol": 0,
"toCol": 1,
"variant": "dashed"
},
{
"id": "auth_group",
"label": "One browser login",
"lane": "browser",
"fromCol": 2,
"toCol": 2,
"variant": "security"
},
{
"id": "session_group",
"label": "Session creation gate",
"lane": "portal",
"fromCol": 3,
"toCol": 3,
"variant": "emphasis"
},
{
"id": "token_group",
"label": "Token creation loop",
"lane": "portal",
"fromCol": 4,
"toCol": 5,
"variant": "emphasis"
},
{
"id": "evidence_group",
"label": "Step artifacts",
"lane": "config",
"fromCol": 5,
"toCol": 5,
"variant": "dashed"
}
],
"mainPath": [
"pytest_start",
"load_config",
"parametrize",
"driver_login",
"token_dispatch",
"token_save",
"save_report"
],
"nodes": [
{
"id": "pytest_start",
"lane": "pytest",
"col": 0,
"type": "backend",
"label": "Pytest setup test",
"sublabel": "test_combined_setup.py",
"width": 150,
"sources": [
{
"path": "tests/setup/test_combined_setup.py",
"line": 11,
"end_line": 14
}
]
},
{
"id": "load_config",
"lane": "config",
"col": 1,
"type": "database",
"label": "Load config.json",
"sublabel": "products + setup flags",
"width": 150,
"sources": [
{
"path": "tests/config_reader.py",
"line": 6,
"end_line": 33
}
]
},
{
"id": "parametrize",
"lane": "config",
"col": 2,
"type": "backend",
"label": "get_setup_configs",
"sublabel": "combined / session / token",
"tag": "skip disabled",
"width": 150,
"sources": [
{
"path": "tests/config_reader.py",
"line": 36,
"end_line": 83
}
]
},
{
"id": "driver_login",
"lane": "browser",
"col": 2,
"type": "security",
"label": "Open + login",
"sublabel": "Chrome, credentials, CAPTCHA",
"tag": "manual CAPTCHA",
"width": 150,
"sources": [
{
"path": "tests/setup/test_combined_setup.py",
"line": 21,
"end_line": 29
},
{
"path": "tests/helpers.py",
"line": 60,
"end_line": 91
}
]
},
{
"id": "session_gate",
"lane": "pytest",
"col": 3,
"type": "security",
"label": "run_session?",
"sublabel": "product flag gate",
"width": 150,
"sources": [
{
"path": "tests/setup/test_combined_setup.py",
"line": 31,
"end_line": 32
}
]
},
{
"id": "session_create",
"lane": "portal",
"col": 3,
"type": "backend",
"label": "create_session",
"sublabel": "ME instance + dates -\u003e Save",
"width": 150,
"sources": [
{
"path": "tests/setup/sessions/test_session.py",
"line": 24,
"end_line": 40
}
]
},
{
"id": "token_loop",
"lane": "pytest",
"col": 4,
"type": "backend",
"label": "Product-code loop",
"sublabel": "one token per configured code",
"width": 150,
"sources": [
{
"path": "tests/setup/test_combined_setup.py",
"line": 34,
"end_line": 37
},
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 74,
"end_line": 83
}
]
},
{
"id": "token_dispatch",
"lane": "portal",
"col": 4,
"type": "backend",
"label": "run_token_setup",
"sublabel": "IDAS / RTM / UPC branch",
"width": 150,
"sources": [
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 254,
"end_line": 262
}
]
},
{
"id": "token_save",
"lane": "portal",
"col": 5,
"type": "backend",
"label": "Create + save token",
"sublabel": "dates, expiry, time sessions",
"width": 150,
"sources": [
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 264,
"end_line": 325
}
]
},
{
"id": "save_report",
"lane": "config",
"col": 5,
"type": "database",
"label": "Evidence + teardown",
"sublabel": "screenshots, backup, logout",
"width": 150,
"sources": [
{
"path": "tests/step_actions.py",
"line": 21,
"end_line": 40
},
{
"path": "tests/helpers.py",
"line": 135,
"end_line": 159
},
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 330,
"end_line": 342
}
]
},
{
"id": "session_skip",
"lane": "config",
"col": 3,
"type": "messagebus",
"label": "Skip session",
"sublabel": "run_session = false",
"width": 150,
"sources": [
{
"path": "tests/setup/test_combined_setup.py",
"line": 31,
"end_line": 32
}
]
},
{
"id": "pratyay_ui",
"lane": "portal",
"col": 2,
"type": "external",
"label": "Pratyay UI",
"sublabel": "Trading maintenance pages",
"width": 150,
"sources": [
{
"path": "tests/setup/sessions/test_session.py",
"line": 28,
"end_line": 40
},
{
"path": "tests/setup/tokens/test_trading_token.py",
"line": 274,
"end_line": 325
}
]
}
],
"edges": [
{
"id": "start-config",
"from": "pytest_start",
"to": "load_config",
"label": "collect + resolve",
"variant": "emphasis"
},
{
"id": "config-param",
"from": "load_config",
"to": "parametrize",
"label": "product settings"
},
{
"id": "param-login",
"from": "parametrize",
"to": "driver_login",
"label": "selected config",
"variant": "emphasis"
},
{
"id": "login-portal",
"from": "driver_login",
"to": "pratyay_ui",
"label": "authenticate",
"variant": "security"
},
{
"id": "skip-token-loop",
"from": "session_skip",
"to": "token_loop",
"label": "token-only / combined decision",
"variant": "dashed"
},
{
"id": "dispatch-save",
"from": "token_dispatch",
"to": "token_save",
"label": "IDAS default",
"variant": "emphasis"
},
{
"id": "save-evidence",
"from": "token_save",
"to": "save_report",
"label": "step captures",
"variant": "dashed"
},
{
"id": "login-dispatch",
"from": "driver_login",
"to": "token_dispatch",
"label": "run token flow",
"variant": "emphasis"
}
],
"cards": [
{
"dot": "cyan",
"title": "How configuration is called",
"items": [
"pytest parametrization calls get_setup_configs(\"combined\") when test_session_and_token_setup is collected.",
"get_product_config merges product overrides into setup.run_session/run_token and resolves token settings; disabled products are filtered out.",
"Each selected config carries url, login, product.me_instance, token settings and product_name into the test."
]
},
{
"dot": "amber",
"title": "Combined run semantics",
"items": [
"The test opens the portal and logs in once per browser fixture. It saves a browser cookie/session backup after login.",
"When run_session is true, create_session selects the configured ME instance and saves the calculated start/end window.",
"The same driver then loops product codes and calls run_token_setup for each one."
]
},
{
"dot": "violet",
"title": "Token branches",
"items": [
"IDAS/default creates the token, fills delivery/expiry values, then edits ORDER_NOR, ORDER_EXCH and TRADE time sessions before Save and Activate.",
"RTM selects market/product/generation mode, fills delivery and session times, then saves; activation is not part of this branch.",
"UPC fills configured order/trade times, saves, then resets the generator."
]
},
{
"dot": "rose",
"title": "Authentication and evidence",
"items": [
"login() sends credentials, waits for manual CAPTCHA completion, submits, waits for the dashboard marker and closes transient notices.",
"StepActions captures screenshots after interactions. conftest.py attaches them to pytest-html and creates report folders.",
"The diagram shows trading sessions/tokens; login session backups are browser cookies, not the trading-session records created in the portal."
]
}
]
}
@@ -0,0 +1,17 @@
{
"schemaVersion": 1,
"receiptId": "194f6ca1-621e-45b4-ab5d-d915aa4711fd",
"status": "current",
"command": "deliver",
"type": "workflow",
"input": "D:\\Selenium Test\\.archify\\workflow-pxil-session-token-20260929-160100\\candidate.json",
"output": "D:\\Selenium Test\\.archify\\workflow-pxil-session-token-20260929-160100\\pxil-session-token.html",
"specification": {
"sha256": "c349c44844d853c1dc013cd9f1e9380b0f1548224ef389a1960b97af956310b1",
"bytes": 19583
},
"artifact": {
"sha256": "d0c3abadeefa664a3d46893e7dc43042339bf42471f76967fe467600f0388882",
"bytes": 771409
}
}
@@ -0,0 +1,89 @@
{
"schemaVersion": 1,
"ok": true,
"command": "finalize",
"status": "pass",
"type": "workflow",
"quality": "showcase",
"specification": {
"path": "D:\\Selenium Test\\.archify\\workflow-pxil-session-token-20260929-160100\\candidate.json",
"sha256": "c349c44844d853c1dc013cd9f1e9380b0f1548224ef389a1960b97af956310b1",
"bytes": 19583
},
"artifact": {
"path": "D:\\Selenium Test\\.archify\\workflow-pxil-session-token-20260929-160100\\pxil-session-token.html",
"sha256": "d0c3abadeefa664a3d46893e7dc43042339bf42471f76967fe467600f0388882",
"bytes": 771409
},
"gates": {
"validate": "pass",
"deliver": "pass",
"check": "pass",
"browser-check": "pass"
},
"diagnostics": [],
"diagnosticSummary": {
"total": 0,
"shown": 0,
"truncated": false
},
"evidence": {
"receipt": "D:\\Selenium Test\\.archify\\workflow-pxil-session-token-20260929-160100\\pxil-session-token.finalize.json",
"summaryReceipt": "D:\\Selenium Test\\.archify\\workflow-pxil-session-token-20260929-160100\\pxil-session-token.finalize-summary.json",
"browserCheckReceipt": "D:\\Selenium Test\\.archify\\workflow-pxil-session-token-20260929-160100\\pxil-session-token.browser-check.json"
},
"update": {
"status": "current",
"installedVersion": "3.0.1",
"availableVersion": "3.0.1",
"releaseNotes": null,
"checkedAt": "2026-09-29T09:50:05.829Z",
"source": "cache",
"noticeRequired": false,
"noticeText": null
},
"visualReview": "not-requested",
"durationMs": 12243,
"visualReviewRecommendation": {
"action": "inspect-route-readability",
"signals": {
"routesOverSuggestedBends": 2
},
"reason": "Automated gates passed, but crossings or detours still need perceptual review before claiming visual quality.",
"affectedRoutes": {
"crossings": [],
"detours": [
{
"relationship": {
"id": "login-dispatch",
"from": "driver_login",
"to": "token_dispatch",
"label": "run token flow",
"collectionIndex": 2,
"artifactIndex": 3
},
"bends": 3,
"stretch": 1.164
},
{
"relationship": {
"id": "param-login",
"from": "parametrize",
"to": "driver_login",
"label": "selected config",
"collectionIndex": 4,
"artifactIndex": 5
},
"bends": 3,
"stretch": 1.109
}
],
"truncated": false
},
"hints": [
"driver_login → token_dispatch needs 3 bends: move node driver_login or token_dispatch so they share a row or column with matching centers, or sit diagonally with a clear corner.",
"parametrize → driver_login needs 3 bends: move node parametrize or driver_login so they share a row or column with matching centers, or sit diagonally with a clear corner."
],
"repair": "Trace these relationships at the desktop viewport. For Architecture, use references/architecture-layout-repair.md: reflow a blocked main path or tangled connected scene, and repair an isolated defect locally only when the surrounding composition is accepted. Preserve all semantic content and user-fixed geometry. Rerun finalize once after the edit."
}
}
File diff suppressed because one or more lines are too long
+6 -3
View File
@@ -1,9 +1,6 @@
# Virtual environments # Virtual environments
venv/ venv/
.venv/ .venv/
.venv-playwright/
reports/playwright/
test-results/
# Python cache # Python cache
__pycache__/ __pycache__/
@@ -12,6 +9,9 @@ __pycache__/
# Pytest cache # Pytest cache
.pytest_cache/ .pytest_cache/
# Machine-specific test configuration
config.json
# Generated reports (keep directory structure) # Generated reports (keep directory structure)
reports/screenshots/* reports/screenshots/*
!reports/screenshots/.gitkeep !reports/screenshots/.gitkeep
@@ -21,3 +21,6 @@ reports/logs/*
reports/html/* reports/html/*
!reports/html/.gitkeep !reports/html/.gitkeep
# CAPTCHA stress-test screenshots and CSV reports
reports/captcha_login_stress/
-82
View File
@@ -1,82 +0,0 @@
# Playwright migration review
## Preserved Selenium baseline
- Reviewed on 2026-09-08; working tree was clean before this document was added.
- Current branch: `development`.
- Backup branch: `backup/selenium-working-2026-09-08`, created locally and pushed to `origin`.
- Baseline commit: `4749efe45f47587e6d6aec228a4818576cb666d9`.
- HEAD, the local backup, and its remote-tracking reference were verified to match.
- The backup preserves tracked source, configuration, and committed input files. Ignored virtual environments and generated reports are not included.
- No browser tests were executed during this review; working behavior is the user's reported baseline.
To return to the baseline, first commit or stash any migration changes, then use:
```powershell
git switch backup/selenium-working-2026-09-08
```
Keep the backup branch unchanged. Start implementation on a separate branch such as `migration/playwright-python`, created from `development`.
## Recommended stack
Keep Python, pytest, pytest-html, existing markers, and JSON/Excel inputs. Use Playwright's synchronous Python API with the official pytest-playwright plugin. This retains the existing business logic and test runner while changing the browser layer. A TypeScript rewrite would also require porting the data readers, configuration, and reporting, without a demonstrated need here.
Start with headed Chromium because exchange and trading-member logins currently require manual CAPTCHA input. Select a compatible dependency set, verify it in a separate virtual environment, and pin the validated versions. Install Chromium through Playwright as well as installing the Python packages.
References: [installation](https://playwright.dev/python/docs/intro), [pytest fixtures and options](https://playwright.dev/python/docs/test-runners).
## Repository-specific changes
| Existing area | Recommended migration |
| --- | --- |
| `tests/browser.py`, `tests/conftest.py` | Replace driver creation with plugin-managed browser/context/page fixtures for migrated tests. Keep Selenium's driver fixture while both suites coexist. Start with one isolated context per test. |
| `tests/step_actions.py`, `tests/reporting.py` | Preserve step names and report metadata; use `page.screenshot(path=...)`. Adapt the failure hook, which currently only looks for `driver`. Add failure traces and include their paths in reports. |
| `tests/helpers.py`, `tests/regression/auth.py` | Port setup, exchange, trading-member, and MCO login separately. Wait for a successful authenticated page. Replace fixed CAPTCHA sleep with a bounded wait for manual entry and then verify login. Playwright does not remove the CAPTCHA requirement. |
| `tests/locators.py`, `trading_locators.py` | Keep stable IDs initially. Convert Selenium By tuples into Playwright locators. Scope broad XPath matches to the active form, modal, or grid to avoid ambiguous matches. |
| `tests/regression/ui_helpers.py` | Rewrite Bootstrap and Select2 interactions around the visible dropdown and exact option. Select2 is a custom widget; native `select_option()` is appropriate only for actual select elements. Preserve code-to-label matching deliberately. |
| `bidding/grid_actions.py` | Port the existing Handsontable integration through `page.evaluate()` if needed. Separate waiting for the instance from mutating it. Validate the complete matrix and submitted outcome; the present helper only checks the first displayed value after populating. |
| `bidding/upload_actions.py`, `folder_upload_actions.py`, `block_bulk_upload_actions.py` | Use `set_input_files()` for file inputs; use directory input support where the actual element supports directory selection. Preserve progress, per-file status, logs, and failure handling. Register expected confirmation-dialog handling before the triggering action. |
| `matching/actions.py` | Replace CDP download configuration and directory polling with an `expect_download()` block around the export action, then save the download with the existing source-specific naming convention. Preserve business-status polling and timeouts. |
| `matching/bid_transfer_actions.py` | Preserve request/status correlation and explicit rejection handling. Browser action readiness is not proof of successful bid transfer. |
| `tests/regression/config_reader.py`, `tests/config_reader.py`, input manifests, JSON/Excel readers | Reuse browser-independent logic. Keep configuration schema and defaults stable during initial migration. |
| `tests/file_generation.py` | Retain payload-generation logic and audit its separate dependency requirements when preparing the new environment. |
Playwright auto-waits for action readiness and offers retrying assertions. Replace UI sleeps with visible/enabled/value/status assertions, but retain intentional business polling intervals. Avoid routinely replacing JavaScript click fallbacks with forced clicks; inspect overlays and locator scope first.
References: [auto-waiting](https://playwright.dev/python/docs/actionability), [inputs and uploads](https://playwright.dev/python/docs/input), [downloads](https://playwright.dev/python/docs/downloads).
## Parallel upload design
`test_idas_exchange_bidding_flow.py` currently creates separate Chrome drivers in ThreadPoolExecutor workers and synchronizes each batch with a Barrier. It also assigns accounts per TM and staggers logins.
First establish sequential Playwright upload parity. Do not share a Playwright instance, browser, context, or page across those threads: the Python API is not thread-safe. If retaining threads temporarily, each worker must create and close its own Playwright instance and browser.
For independent scenarios, consider pytest-xdist processes with separate accounts and data. For coordinated uploads that must begin together, use a dedicated async orchestration flow with separate contexts and an async barrier/event mechanism; ordinary test parallelism does not preserve the existing start barrier. Keep the rest of the suite synchronous. Preserve barrier cancellation when a worker fails.
Before concurrency, replace the current scan-and-increment report run numbering with collision-resistant run/worker identifiers. Preserve account-to-TM assignment and validate server-side session restrictions; browser-context isolation alone does not prevent account conflicts.
Reference: [Python library threading guidance](https://playwright.dev/python/docs/library).
## Suggested implementation sequence
1. Create the migration branch and an isolated Playwright environment. Add a separate `tests/playwright/` tree with scoped fixtures and explicit run commands so migration tests are not accidentally executed alongside the old suite. Keep shared data utilities browser-independent.
2. Port `test_idas_mco_login.py` as the small pilot: credentials, navigation, dashboard assertion, screenshot reporting, and failure trace. Then port exchange and trading-member login with their manual CAPTCHA steps.
3. Port session creation, token activation, and combined setup. Preserve configuration-controlled skips and verify the created session/token state rather than relying on screenshots alone. Root session/token test files are skip-only compatibility stubs; migrate the implementations under `tests/setup/`.
4. Port one single-bid flow, including Select2 filters, market-specific input matrix, submission, and observable success. Then cover each configured market and both login roles.
5. Port individual file upload, batch upload, folder upload, and block bulk upload sequentially. Compare file counts, progress completion, statuses, and generated logs against the Selenium behavior.
6. Port transfer and matching. Preserve rejection-stop behavior, provisional matching checks, flow generation, and exported files for Green, Conventional, and HPDAM. Validate downloaded content as well as file presence.
7. Add parallel upload support using the design above. Verify distinct sessions, coordinated start if required, failure cleanup, and distinct artifacts.
8. Revisit fake-activity/manual-run utilities, update `RUN_COMMANDS.md`, and remove Selenium dependencies only after required flows pass. Order-book, trade-book, and reports directories contain scaffolding; define new coverage separately from migration parity.
During coexistence, run each suite using explicit paths. Preserve the existing combined-setup behavior; do not assume pytest test ordering will create prerequisites. Avoid automatic retries for state-changing bid/session/matching actions unless duplicate submissions are handled.
## Completion criteria
- Validate both implementations against controlled equivalent test data; running each against the same mutable state back-to-back can itself change the result.
- Confirm session/token state, submitted bid data, upload success and failure outcomes, transfer rejection behavior, matching status, and downloaded content.
- Confirm screenshots, logs, failure traces, manual CAPTCHA flow, and browser cleanup.
- Require repeatable passing runs for every required configured flow before making Playwright the default.
- Keep generated authentication state and traces out of Git; these can contain login/session data. Preserve the frozen backup without editing its historical configuration.
This document is a migration proposal. No Selenium implementation or dependency files have been changed.
+75 -6
View File
@@ -11,6 +11,80 @@ cd "C:\Selenium Test"
Edit `config.json` before running. Edit `config.json` before running.
Common URL, login credentials, login waits, and step delay stay at the top level.
`setup.run_session` and `setup.run_token` are defaults for every enabled product.
`setup.session` provides common session timing defaults.
`products` contains separate `IDAS`, `RTM`, `UPC`, `RA`, and `REC` sections.
Each product has its own `me_instance`, optional `session` timing overrides, and
`token` settings (codes, delivery offset, expiry, and time sessions).
RTM currently runs token creation only (`run_session: false`, `run_token: true`).
If enabled again, session creation uses ME instance
`52` (`52:RTM`). It uses today's date plus a 7-minute start offset and today's
`20:30` end time, matching the current IDAS session end-time default. Change
`products.RTM.session.start_offset_minutes` and `products.RTM.session.end_time`
to configure RTM independently. The shared login, navigation, and Save steps apply
to every product. RTM token automation opens Token Generator,
selects `NATIONAL`, product code `RTM`, and generation mode `RTM`, then clicks
Add New Token. Product Type is populated automatically. It fills delivery dates
with today's `00:00` and `23:59`, session start with current time plus 5 minutes,
and session end with `20:00`, then clicks Save Token. All four timings are
configured under `products.RTM.token` as shown below. `delivery_offset_days: 0`
means today. RTM activation is not part of this stage.
Run RTM token creation through Save Token:
```powershell
pytest tests\setup -k RTM -s
```
Existing IDAS settings are preserved. The remaining product templates are disabled until
you fill in their actual ME instance/token codes and set `enabled` to `true`.
Additional products can be added using the same structure.
To override the defaults for one product, add flags directly inside that product:
```json
"RTM": {
"enabled": true,
"run_session": false,
"run_token": true,
"me_instance": "52",
"session": {
"start_offset_minutes": 7,
"end_time": "20:30"
},
"token": {
"market_code": "NATIONAL",
"product_type": "RTM",
"product_codes": ["RTM"],
"generation_mode": "RTM",
"delivery_offset_days": 0,
"delivery_from_time": "00:00",
"delivery_to_time": "23:59",
"session_start_offset_minutes": 5,
"session_end_time": "20:00"
}
}
```
Remove a product's `run_session` or `run_token` key to inherit that common flag
again. Explicit `false` overrides a common `true`; explicit `true` overrides a
common `false`. Both false skips that product; `enabled: false` always skips it.
Session timing overrides inherit any fields omitted from `setup.session`.
Token timing settings belong to each product independently.
Use `pytest tests\setup -s` to run all enabled products. Each product runs in its
own browser: both flags true runs session then the product's token flow in one login; otherwise
only its enabled action runs. Test/report names include the product name.
Screenshot and log folders use the collected product and action, such as
`IDAS_SESSION_TOKEN_001`, `RTM_TOKEN_001`, or `RTM_SESSION_001`.
The shared HTML report for `tests\setup` uses `PXIL_SETUP_###.html`.
Existing report folders retain their original names.
The session and token subfolder commands below select only products configured
for session-only and token-only respectively; use the full setup command for a
mixture of modes.
Session and token together in one login: Session and token together in one login:
```json ```json
@@ -69,7 +143,7 @@ Run only token setup:
pytest tests\setup\tokens -s --html=reports/html/token_run.html --self-contained-html 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`. Use this when `run_token` is `true`. It will create one token or multiple tokens based on `products.<product>.token.product_codes`.
Run only session setup: Run only session setup:
@@ -384,8 +458,3 @@ pytest tests\regression -m reports -s --html=reports/html/reports_run.html --sel
pytest tests\regression -m order_book -s --html=reports/html/order_book_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 pytest tests\regression -m trade_book -s --html=reports/html/trade_book_run.html --self-contained-html
``` ```
# Playwright session/token setup
See [Playwright setup commands](playwright_tests/README.md) for login, session creation,
token creation/activation, and combined setup on utilseastern. Use `-c playwright.ini`
to select the new suite; commands below continue to describe the Selenium suite.
+249
View File
@@ -0,0 +1,249 @@
{
"url": "https://utilseastern.pxil.in/Pxil/",
"stagingurl": "https://utilseastern.pxil.in/Pxil/",
"uaturl": "https://utilsuat.pxil.in/Pxil/",
"login": {
"username": "Tejas",
"password": "Pxil@123"
},
"login_captcha_wait_seconds": 10,
"login_captcha_max_retries": 10,
"login_post_submit_wait_seconds": 10,
"step_delay_seconds": 1,
"setup": {
"run_session": true,
"run_token": false,
"session": {
"time_mode": "offset",
"start_offset_minutes": 5,
"start_time": "14:30",
"end_time": "20:30"
}
},
"products": {
"IDAS": {
"enabled": true,
"run_session": true,
"run_token": false,
"me_instance": "55",
"session": {},
"token": {
"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": 10,
"end_time": "19:30"
},
"ORDER_EXCH": {
"start_time": "19:30",
"end_time": "19:45"
},
"TRADE": {
"start_time": "19:45",
"end_time": "19:50"
}
}
}
},
"RTM": {
"enabled": false,
"run_session": false,
"run_token": true,
"me_instance": "52",
"session": {
"time_mode": "offset",
"start_offset_minutes": 7,
"start_time": "14:30",
"end_time": "20:30"
},
"token": {
"market_code": "NATIONAL",
"product_type": "RTM",
"product_codes": [
"RTM"
],
"generation_mode": "RTM",
"delivery_offset_days": 0,
"delivery_from_time": "00:00",
"delivery_to_time": "23:59",
"time_mode": "offset",
"session_start_offset_minutes": 5,
"session_start_time": "14:30",
"session_end_time": "20:00"
}
},
"UPC": {
"enabled": true,
"run_session": true,
"run_token": true,
"session_variants": {
"DAILY": {
"enabled": true,
"run_session": false,
"run_token": true,
"me_instance": "58",
"token": {
"generation_mode": "AnyDayUPC",
"create_product_codes": [
"DailyC_E"
],
"available_product_codes": [
"DailyC_D",
"DailyGR_D",
"DailyC_RTC",
"DailyC_E",
"DailyC_M"
]
}
},
"WEEKLY": {
"enabled": true,
"run_session": false,
"run_token": true,
"me_instance": "59",
"token": {
"generation_mode": "WeeklyUPC",
"create_product_codes": [
"WeeklyC_E"
],
"available_product_codes": [
"WeeklyC_D",
"WeeklyC_N",
"WeeklyGR_D",
"WeeklyC_RTC"
]
}
},
"MONTHLY": {
"enabled": true,
"run_session": false,
"run_token": true,
"me_instance": "60",
"token": {
"generation_mode": "MonthlyUPC",
"create_product_codes": [
"MonthlyC_N"
],
"available_product_codes": [
"MonthlyC_D",
"MonthlyGR_D",
"MonthlyC_RTC"
]
}
}
},
"session": {},
"token": {
"market_code": "NATIONAL",
"product_type": "UPC",
"product_codes": [],
"delivery_offset_days": 1,
"expiry_time": "20:30",
"upc_times": {
"time_mode": "manual",
"order_normal_start_time": "13:00",
"order_normal_start_offset_minutes": 10,
"order_normal_end_time": "17:00",
"trade_session_start_time": "17:00",
"trade_session_end_time": "22:00"
}
}
},
"RA": {
"enabled": false,
"run_session": true,
"run_token": false,
"me_instance": "64",
"session": {},
"token": {
"market_code": "NATIONAL",
"product_type": "RA",
"product_codes": [],
"delivery_offset_days": 1,
"expiry_time": "20:30",
"time_sessions": {}
}
},
"REC": {
"enabled": false,
"run_session": true,
"run_token": true,
"me_instance": "25",
"session": {},
"token": {
"market_code": "NATIONAL",
"product_type": "REC",
"product_codes": [
"REC"
],
"delivery_offset_days": 1,
"expiry_time": "20:30",
"type_prefix": "REC",
"time_sessions": {
"ORDER_NOR": {
"time_mode": "offset",
"start_offset_minutes": 10,
"start_time": "14:30",
"end_time": "19:30"
},
"ORDER_EXCH": {
"start_time": "19:30",
"end_time": "19:45"
},
"TRADE": {
"start_time": "19:45",
"end_time": "19:50"
}
}
}
},
"INTRADAY": {
"enabled": true,
"run_session": true,
"run_token": true,
"me_instance": "53",
"session": {},
"token": {
"market_code": "NATIONAL",
"product_type": "INTRADAY",
"product_codes": [
"I_DayStatic_C"
],
"generation_mode": "Intraday24by7",
"delivery_start_offset_minutes": 5,
"trading_start_time": "00:20",
"trading_start_days_before_delivery": 1,
"duration_before_delivery_end_time": 150,
"delivery_to_time": "23:59"
}
},
"DAC": {
"enabled": false,
"run_session": true,
"run_token": true,
"me_instance": "57",
"session": {
"time_mode": "offset",
"start_offset_minutes": 5,
"end_time": "23:00"
},
"token": {
"market_code": "NATIONAL",
"product_type": "DAC",
"product_codes": ["DACStatic"],
"generation_mode": "DAC",
"delivery_start_offset_minutes": 5,
"delivery_to_time": "23:59",
"session_start_time": "13:00",
"session_end_time": "23:00",
"delivery_time_period": 15
}
}
}
}
-46
View File
@@ -1,46 +0,0 @@
{
"url": "https://utilseastern.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": "IDAS3",
"available_codes":",IDASTEST01,IDASTEST02,IDASTEST03,IDASTEST04,IDASTEST05,IDASTEST06,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": "22:30",
"time_sessions": {
"ORDER_NOR": {
"start_offset_minutes": 5,
"end_time": "21:30"
},
"ORDER_EXCH": {
"start_time": "21:30",
"end_time": "22:45"
},
"TRADE": {
"start_time": "22:45",
"end_time": "22:50"
}
}
}
}
+22
View File
@@ -28,3 +28,25 @@ In `runtime.json`:
- `IDAS`, `RTM`, and future top-level product sections: product-wise defaults, available codes, supported flows, bidding config, matching config, and future product-specific controls. - `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`. Regression config now lives entirely inside `runtime.json`.
## UPC session setup
Session creation uses the root `config.json`, independently of regression settings.
Set `products.UPC.enabled` to `true` to include UPC. Under
`products.UPC.session_variants`, enable all three variants or only the ones required:
| Variant | ME instance ID | UI name |
| --- | --- | --- |
| DAILY | 58 | UPC ANYDAY |
| WEEKLY | 59 | UPC WEEKLY |
| MONTHLY | 60 | UPC MONTHLY |
Each variant has an `enabled` flag. UPC remains disabled by default; all three
variants are preselected for when it is enabled. Set `UPC.run_session` and
`UPC.run_token` to `true` when both session and token creation are required. Each selected variant uses the
existing session creation flow and inherits `setup.session.start_offset_minutes`
and `setup.session.end_time`. Optional `UPC.session` settings override those
defaults, and a variant's `session` settings can override them individually.
Run session-only setup with `python -m pytest tests/setup/sessions/test_session.py`.
Selected UPC cases appear as `UPC_DAILY`, `UPC_WEEKLY`, and `UPC_MONTHLY`.
-7
View File
@@ -1,7 +0,0 @@
[pytest]
testpaths = playwright_tests
pythonpath = .
addopts = -p playwright_tests.options -s --tb=short --headed --browser chromium --tracing retain-on-failure --screenshot only-on-failure --output reports/playwright/artifacts
markers =
session: session creation
trading_token: token creation and activation
-156
View File
@@ -1,156 +0,0 @@
from datetime import datetime
import pytest
from playwright_tests.planning import build_plan
from playwright_tests.setup_page import SetupPage
@pytest.fixture
def config():
return {
"setup": {"run_session": True, "run_token": True},
"trading_token_idas": {"product_codes": "IDAS,IDASTEST01", "market_code": "NATIONAL"},
}
def test_combined_plan_dates_and_products(config):
plan = build_plan(config, now=datetime(2026, 9, 8, 10, 0))
assert plan["session"] == ("08/09/2026 10:07", "08/09/2026 20:30")
assert [t["product_code"] for t in plan["tokens"]] == ["IDAS", "IDASTEST01"]
assert plan["tokens"][0]["delivery_date"] == "09/09/2026"
assert plan["tokens"][0]["rows"][0] == ("ORDER_NOR", "10:10", "18:30")
def test_login_ignores_expired_mutation_settings(config):
assert build_plan(config, "login", now=datetime(2026, 9, 8, 23)) == {"session": None, "tokens": []}
def test_token_override_does_not_modify_config(config):
plan = build_plan(config, "token", "IDASTEST02", datetime(2026, 9, 8, 10))
assert plan["session"] is None
assert [t["product_code"] for t in plan["tokens"]] == ["IDASTEST02"]
assert config["trading_token_idas"]["product_codes"] == "IDAS,IDASTEST01"
@pytest.mark.parametrize("flow, message", [("session", "Session end_time"), ("token", "expiry_time")])
def test_reject_expired_schedule(config, flow, message):
with pytest.raises(ValueError, match=message):
build_plan(config, flow, now=datetime(2026, 9, 8, 23))
def test_reject_inverted_time_session(config):
config["trading_token_idas"]["time_sessions"] = {"ORDER_NOR": {"start_time": "19:00", "end_time": "18:30"}}
with pytest.raises(ValueError, match="ORDER_NOR"):
build_plan(config, "token", now=datetime(2026, 9, 8, 10))
def test_save_waits_for_delayed_confirmation(page, tmp_path):
page.set_content('''<button onclick="setTimeout(() => {
document.querySelector('#result').innerHTML = '<div class=alert-success>Saved successfully</div>';
}, 150)">Save</button><div id=result></div>''')
app = SetupPage(page, tmp_path)
app.save("//button", "saved", timeout=2)
assert len(app.screenshots) == 1
def test_save_rejects_error_dialog(page, tmp_path):
page.set_content('''<button onclick="alert('Invalid session')">Save</button>''')
app = SetupPage(page, tmp_path)
with pytest.raises(AssertionError, match="Invalid session"):
app.save("//button", "saved", timeout=2)
assert not app.screenshots
def test_save_accepts_success_dialog(page, tmp_path):
page.set_content('''<button onclick="alert('Token activated successfully')">Save</button>''')
app = SetupPage(page, tmp_path)
app.save("//button", "activated", timeout=2)
assert app.screenshots
def test_save_does_not_pass_on_click_alone(page, tmp_path):
page.set_content("<button>Save</button>")
with pytest.raises(AssertionError, match="no explicit success"):
SetupPage(page, tmp_path).save("//button", "saved", timeout=0.2)
def test_grid_button_targets_exact_time_session(page, tmp_path):
page.set_content('''
<div class=ui-grid-row><span>ORDER_NOR</span><button onclick="window.selected='normal'">Details</button></div>
<div class=ui-grid-row><span>ORDER_EXCH</span><button onclick="window.selected='exchange'">Details</button></div>
''')
SetupPage(page, tmp_path).grid_button("Details", "ORDER_EXCH")
assert page.evaluate("window.selected") == "exchange"
def test_login_clicks_automatically_after_complete_captcha(page, tmp_path):
page.route("https://setup.test/", lambda route: route.fulfill(body='''
<input placeholder="User ID"><input placeholder="Password" type=password>
<input name=captcha ng-model="login.captcha">
<button value=Login onclick="window.clickedValue=document.querySelector('[name=captcha]').value;
document.querySelector('#dashboard').hidden=false;">Sign in</button>
<span id=dashboard hidden>TRADING MAINTENANCE</span>
<script>
setTimeout(() => document.querySelector('[name=captcha]').value='12345', 100);
setTimeout(() => document.querySelector('[name=captcha]').value='123456', 300);
</script>
''', content_type="text/html"))
app = SetupPage(page, tmp_path)
app.login({"url": "https://setup.test/", "login": {"username": "sample", "password": "sample"}}, 2000)
assert page.evaluate("window.clickedValue") == "123456"
def test_token_details_button_can_be_outside_grid(page, tmp_path):
page.set_content('''<button onclick="window.opened=true">Token Details</button>''')
SetupPage(page, tmp_path).grid_button("Token Details")
assert page.evaluate("window.opened") is True
@pytest.mark.parametrize("configured,displayed", [("ORDER_NOR", "ORDER_NORMAL"), ("ORDER_EXCH", "ORDER_EXCHANGE")])
def test_time_session_aliases_match_live_labels(page, tmp_path, configured, displayed):
page.set_content(f'''<div class=ui-grid-row><span>{displayed}_OTHER</span>
<button onclick="window.selected='wrong'">Details</button></div>
<div class=ui-grid-row><span>{displayed}</span>
<button onclick="window.selected='correct'">Details</button></div>''')
SetupPage(page, tmp_path).grid_button("Details", configured)
assert page.evaluate("window.selected") == "correct"
def test_dropdown_preserves_already_selected_value(page, tmp_path):
page.set_content('<button id=market onclick="window.clicked=true">NATIONAL</button>')
SetupPage(page, tmp_path).choose(page.locator("#market"), "NATIONAL")
assert page.evaluate("window.clicked === undefined")
@pytest.mark.parametrize("message,passes", [("1|Token updated successfully", True), ("0|Invalid session", False)])
def test_token_save_requires_own_response_despite_old_banner(page, tmp_path, message, passes):
from tests.locators import Locator
page.route("https://setup.test/", lambda route: route.fulfill(content_type="text/html", body='''
<div class=alert-success>Previous token saved successfully</div>
<button data-ng-click="updateSession()" onclick="fetch('/tokenCtrl/updateTimeSessionMap', {method:'POST'})">Save</button>
'''))
page.route("**/tokenCtrl/updateTimeSessionMap", lambda route: route.fulfill(json={"message": message}))
page.goto("https://setup.test/")
app = SetupPage(page, tmp_path)
if passes:
app.save(Locator.savetimesessionbtn, "saved", timeout=2)
assert app.screenshots
else:
with pytest.raises(AssertionError, match="Invalid session"):
app.save(Locator.savetimesessionbtn, "saved", timeout=2)
def test_activation_rejects_null_message_even_with_zero_status(page, tmp_path):
from tests.locators import Locator
page.route("https://setup.test/", lambda route: route.fulfill(content_type="text/html", body='''
<div class=ui-grid-row><div role=gridcell>TOKEN123</div><div role=gridcell>ABC</div><div role=gridcell id=active>I</div></div>
<button data-ng-click="saveTokenDetails('T')" onclick="fetch('/tokenCtrl/updateTokenMstr', {method:'POST'}).then(() => document.querySelector('#active').textContent='T')">Save &amp; Activate</button>
'''))
page.route("**/tokenCtrl/updateTokenMstr", lambda route: route.fulfill(json={"message": None, "status": 0}))
page.goto("https://setup.test/")
app = SetupPage(page, tmp_path)
app.token_id = "TOKEN123"
with pytest.raises(AssertionError, match="Activation not confirmed for token TOKEN123"):
app.save(Locator.savetokenandactivatebtn, "activated", timeout=2)
assert not app.screenshots
-107
View File
@@ -1,107 +0,0 @@
# Session and token setup with Playwright
This suite uses `config.json` (not `config/runtime.json`) and the existing setup locators.
The setup URL is now `https://utilseastern.pxil.in/Pxil/`. This shared configuration
also changes the URL used by Selenium setup on the migration branch.
## Install
The separate `.venv-playwright` environment and Chromium are installed on this machine.
For a fresh checkout, from the repository root in PowerShell:
```powershell
python -m venv .venv-playwright
.\.venv-playwright\Scripts\python.exe -m pip install -r requirements-playwright.txt
.\.venv-playwright\Scripts\python.exe -m playwright install chromium
```
## Run
All commands below open Chromium. Credentials are filled automatically. Enter the
six-character CAPTCHA in the browser; Playwright then clicks Sign in automatically.
If CAPTCHA length changes, set `playwright.captcha_length` in `config.json`.
The default CAPTCHA timeout is 180 seconds; override with `--captcha-timeout 300`.
Login and logout only, without creating records:
```powershell
.\.venv-playwright\Scripts\python.exe -u -m pytest -c playwright.ini --setup-flow login
```
Create a session only:
```powershell
.\.venv-playwright\Scripts\python.exe -u -m pytest -c playwright.ini --setup-flow session
```
Create a token, edit ORDER_NOR / ORDER_EXCH / TRADE, and activate it for one product:
```powershell
.\.venv-playwright\Scripts\python.exe -u -m pytest -c playwright.ini --setup-flow token --token-products IDAS
```
Create a session and then a token within the same login:
```powershell
.\.venv-playwright\Scripts\python.exe -u -m pytest -c playwright.ini --setup-flow both --token-products IDAS
```
Follow the existing `setup.run_session` / `setup.run_token` flags and configured product list:
```powershell
.\.venv-playwright\Scripts\python.exe -u -m pytest -c playwright.ini
```
Explicit `--setup-flow` overrides the setup flags. `--token-products` overrides the
product list for that run without editing the configuration. Without that override,
the current configuration selects seven products. A standalone token run assumes a
valid trading session already exists. Combined setup runs session creation first.
If a token was saved before a later step failed, resume that exact token instead of
creating it again (replace the product and token ID with the saved record):
```powershell
.\.venv-playwright\Scripts\python.exe -u -m pytest -c playwright.ini --setup-flow token --token-products IDAS3 --resume-token-id 080926083528OXVH
```
Resume requires exactly one product and looks up the exact token ID in Time Session
View. It skips Add New Token/save Token, edits the configured time sessions, and
activates the existing token. The configuration names `ORDER_NOR` and `ORDER_EXCH`
also match the application's `ORDER_NORMAL` and `ORDER_EXCHANGE` labels.
## Schedule and outcome checks
Before browser startup, the suite validates the session window, token expiry, and
each time-session start/end pair. Update the same fields used by Selenium in
`config.json` when the day's schedule has expired. It does not silently move the
schedule or replace existing records.
Token saves and time-session updates require a successful response from their
specific server endpoint. Activation requires the application's `1|` success message
and the Save & Activate button to disappear. A null message is a failure, even if
the response status is zero. Session creation uses success notifications/alerts.
Visible errors and missing confirmations fail the test. A missing
confirmation must be investigated using the trace before rerunning, because the
server may already have saved the record. There are no automatic mutation retries.
Grid selection requires a unique matching button. It fails instead of silently
selecting the first token if the application lists multiple records.
## Reports and local checks
Each live run writes a unique folder under `reports/playwright/`, with an HTML report,
embedded step screenshots, and failure artifacts/traces. These files are Git-ignored.
The original Selenium suite does not collect these tests through its default config.
Run local schedule and browser-helper checks without contacting PXIL:
```powershell
.\.venv-playwright\Scripts\python.exe -m pytest -c playwright.ini playwright_checks -o addopts= -p no:cacheprovider --browser chromium -q
```
Validated on 2026-09-08: live utilseastern login, automatic Sign in after manual
CAPTCHA, dashboard detection, logout, new token creation, and all three time-session
updates. Activation of token `080926084458LQSS` returned a null message from
`updateTokenMstr`; successful activation is not confirmed. Session creation still
needs live validation. Selenium backup remains at
`backup/selenium-working-2026-09-08`.
View File
-74
View File
@@ -1,74 +0,0 @@
from datetime import datetime
from pathlib import Path
from uuid import uuid4
import base64
import pytest
from tests.config_reader import load_config
from playwright_tests.planning import build_plan
from playwright_tests.setup_page import SetupPage
def pytest_configure(config):
run = datetime.now().strftime("%Y%m%d_%H%M%S") + "_" + uuid4().hex[:8]
folder = Path(config.rootpath) / "reports" / "playwright" / run
folder.mkdir(parents=True, exist_ok=True)
config._setup_report_folder = folder
config.option.htmlpath = str(folder / "report.html")
config.option.self_contained_html = True
config.option.output = str(folder / "artifacts")
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
return {**browser_context_args, "viewport": {"width": 1600, "height": 1000}}
@pytest.fixture
def setup_config():
return load_config()
@pytest.fixture
def setup_plan(request, setup_config):
plan = build_plan(setup_config, request.config.getoption("--setup-flow"),
request.config.getoption("--token-products"))
resume_id = request.config.getoption("--resume-token-id")
if resume_id and (request.config.getoption("--setup-flow") != "token" or len(plan["tokens"]) != 1):
raise pytest.UsageError("--resume-token-id requires --setup-flow token and exactly one product.")
plan["resume_token_id"] = resume_id
return plan
@pytest.fixture
def setup_app(request, setup_plan, setup_config, page):
# setup_plan validates all dates before login or any save operation.
if not request.config.getoption("headed"):
raise pytest.UsageError("Setup login requires manual CAPTCHA: run with --headed.")
page.set_default_timeout(30_000)
folder = request.config._setup_report_folder / request.node.name
folder.mkdir(parents=True, exist_ok=True)
app = SetupPage(page, folder)
request.node._setup_app = app
app.login(setup_config, request.config.getoption("--captcha-timeout") * 1000)
return app
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
app = getattr(item, "_setup_app", None)
if app and report.when in ("setup", "call"):
if report.failed and not app.page.is_closed():
try:
app.capture("failure_state")
except Exception:
pass # Keep the original failure if the browser has disconnected.
if report.when == "call" or report.failed:
from pytest_html import extras
report.extras = list(getattr(report, "extras", [])) + [
extras.png(base64.b64encode(path.read_bytes()).decode("ascii"), name=name)
for name, path in app.screenshots
]
-6
View File
@@ -1,6 +0,0 @@
def pytest_addoption(parser):
group = parser.getgroup("session-token")
group.addoption("--setup-flow", choices=["configured", "login", "session", "token", "both"], default="configured")
group.addoption("--token-products", default=None, help="Comma-separated override; config.json is not modified")
group.addoption("--resume-token-id", default=None, help="Edit and activate this existing token; requires --setup-flow token and one product")
group.addoption("--captcha-timeout", type=int, default=180, help="Seconds allowed for manual CAPTCHA entry before automatic Sign in")
-52
View File
@@ -1,52 +0,0 @@
"""Build and validate the complete setup schedule before changing the application."""
from datetime import datetime, timedelta
from tests.config_reader import parse_product_codes
def build_plan(config, flow="configured", product_codes=None, now=None):
now = now or datetime.now()
setup = config.get("setup", {})
session = flow in ("session", "both") or (
flow == "configured" and setup.get("run_session", True)
)
token = flow in ("token", "both") or (
flow == "configured" and setup.get("run_token", True)
)
result = {"session": None, "tokens": []}
if session:
settings = setup.get("session", {})
start = now + timedelta(minutes=settings.get("start_offset_minutes", 7))
end = datetime.combine(now.date(), datetime.strptime(settings.get("end_time", "20:30"), "%H:%M").time())
if end <= start:
raise ValueError("Session end_time must be later than its calculated start time. Update config.json.")
result["session"] = (start.strftime("%d/%m/%Y %H:%M"), end.strftime("%d/%m/%Y %H:%M"))
if token:
settings = config.get("trading_token_idas")
if not settings:
raise ValueError("Configure trading_token_idas in config.json.")
codes = parse_product_codes(product_codes if product_codes is not None else settings.get("product_codes") or settings.get("product_code"))
if not codes or len(set(codes)) != len(codes):
raise ValueError("Specify nonempty, unique token product codes.")
expiry = datetime.combine(now.date(), datetime.strptime(settings.get("expiry_time", "20:30"), "%H:%M").time())
if expiry <= now:
raise ValueError("Token expiry_time is already past. Update config.json before creating tokens.")
delivery = (now + timedelta(days=settings.get("delivery_offset_days", 1))).strftime("%d/%m/%Y")
rows = []
defaults = [("ORDER_NOR", (now + timedelta(minutes=10)).strftime("%H:%M"), "18:30"),
("ORDER_EXCH", "18:30", "19:00"), ("TRADE", "19:00", "20:00")]
for name, start, end in defaults:
row = settings.get("time_sessions", {}).get(name, {})
if row.get("start_time") is not None:
start = row["start_time"]
elif row.get("start_offset_minutes") is not None:
start = (now + timedelta(minutes=row["start_offset_minutes"])).strftime("%H:%M")
end = row.get("end_time", end)
if datetime.strptime(end, "%H:%M") <= datetime.strptime(start, "%H:%M"):
raise ValueError(f"{name} end_time must be later than start_time. Update config.json.")
rows.append((name, start, end))
result["tokens"] = [dict(settings, product_code=code, delivery_date=delivery,
expiry=expiry.strftime("%d/%m/%Y %H:%M"),
session_date=now.strftime("%d/%m/%Y"), rows=rows)
for code in codes]
return result
-209
View File
@@ -1,209 +0,0 @@
"""Playwright implementation of the existing Angular setup UI."""
import re
import time
from urllib.parse import urlparse
from playwright.sync_api import expect
from tests.locators import Locator
class SetupPage:
def __init__(self, page, folder):
self.page = page
self.folder = folder
self.screenshots = []
self.dialogs = []
self.token_id = None
page.on("dialog", self._dialog)
def _dialog(self, dialog):
self.dialogs.append(dialog.message)
dialog.accept()
def visible(self, xpath):
return self.page.locator("xpath=" + xpath).filter(visible=True)
def capture(self, name):
path = self.folder / f"{len(self.screenshots) + 1:02d}_{name}.png"
self.page.screenshot(path=str(path))
self.screenshots.append((name, path))
print(f"STEP {len(self.screenshots)}: {name}")
def click(self, xpath, name):
self.visible(xpath).click()
self.capture(name)
def fill(self, xpath, value):
field = self.visible(xpath)
field.fill(value)
field.press("Tab")
expect(field).to_have_value(value)
def close_notice(self):
# Only the known TAC-expiry notice; never dismiss arbitrary validation dialogs.
notice = self.page.locator(".modal:visible").filter(has_text=re.compile(r"TAC.*expir", re.I | re.S))
if notice.count() == 1:
notice.locator("button.close").click()
def login(self, config, timeout):
self.page.goto(config["url"])
self.visible(Locator.userid).fill(config["login"]["username"])
self.visible(Locator.password).fill(config["login"]["password"])
captcha_length = int(config.get("playwright", {}).get("captcha_length", 6))
if captcha_length < 1:
raise ValueError("playwright.captcha_length must be positive.")
print(f"Credentials filled. Enter the {captcha_length}-character CAPTCHA in the browser; "
f"Sign in will be clicked automatically. Waiting up to {timeout // 1000} seconds...", flush=True)
captcha = self.page.locator('input[name="captcha"][ng-model="login.captcha"]')
expect(captcha).to_have_value(re.compile(r"^\S{" + str(captcha_length) + r"}$"), timeout=timeout)
self.visible(Locator.submitbtn).click()
expect(self.visible(Locator.dashboard_marker)).to_be_visible(timeout=30_000)
self.close_notice()
self.capture("login_success")
def menu(self, xpath):
self.close_notice()
if not self.visible(xpath).count():
self.visible(Locator.tradingmaintenance).click()
self.visible(xpath).click()
def choose(self, toggle, value):
expect(toggle).to_be_visible()
if toggle.inner_text().strip() == value:
return
toggle.click()
search = self.page.locator("input.ui-select-search:visible")
expect(search).to_have_count(1)
search.fill(value)
if toggle.is_visible() and toggle.inner_text().strip() == value:
return
option = self.page.locator(".ui-select-choices-row:visible").filter(
has_text=re.compile(r"^\s*" + re.escape(value) + r"\s*$")
)
option.click()
expect(self.page.locator(".ui-select-choices-row:visible")).to_have_count(0)
def grid_button(self, label, row=None):
scope = self.page
if row is not None:
aliases = {"ORDER_NOR": ("ORDER_NOR", "ORDER_NORMAL"),
"ORDER_EXCH": ("ORDER_EXCH", "ORDER_EXCHANGE")}
labels = aliases.get(row, (row,))
pattern = re.compile(r"^\s*(?:" + "|".join(re.escape(label) for label in labels) + r")\s*$")
scope = self.page.locator(".ui-grid-row:visible").filter(has=self.page.get_by_text(pattern))
button = scope.get_by_role("button", name=label, exact=True)
# Fail on multiple matches instead of silently editing a different token.
expect(button).to_have_count(1, timeout=30_000)
button.click()
def save(self, xpath, name, timeout=30):
"""Require an explicit success message; a click alone is not a pass."""
button = self.visible(xpath)
if xpath in (Locator.savetokenbtn, Locator.savetimesessionbtn, Locator.savetokenandactivatebtn):
endpoint = {Locator.savetokenbtn: "saveTokenDetails",
Locator.savetimesessionbtn: "updateTimeSessionMap",
Locator.savetokenandactivatebtn: "updateTokenMstr"}[xpath]
with self.page.expect_response(
lambda response: response.request.method == "POST"
and urlparse(response.url).path.endswith("/tokenCtrl/" + endpoint),
timeout=timeout * 1000,
) as pending:
button.click()
response = pending.value
assert response.ok, f"{name}: server returned HTTP {response.status}"
result = response.json()
message = result.get("message") or ""
if xpath == Locator.savetokenandactivatebtn:
assert message.startswith("1|"), (
f"Activation not confirmed for token {self.token_id}: updateTokenMstr "
f"returned {result}. The application requires a '1|' success message. "
"The token and time sessions are already saved; inspect activation before rerunning."
)
# The application hides this button after a successful update; it
# does not refresh the Token Master grid at this point.
expect(button).to_have_count(0, timeout=30_000)
else:
assert message.startswith("1|"), f"{name}: {message or 'Missing success response'}"
if xpath == Locator.savetokenbtn:
match = re.search(r"token Id\s*:\s*([^|\s]+)", message, re.I)
assert match, "Save response did not identify the generated token"
self.token_id = match.group(1)
self.capture(name)
return
previous_dialogs = len(self.dialogs)
success = self.page.locator(".alert-success:visible, .toast-success:visible, [role='alert']:visible").filter(
has_text=re.compile(r"success|saved|activated|updated", re.I)
)
# An earlier notification must disappear before the next save can be verified.
expect(success).to_have_count(0, timeout=30_000)
button.click()
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
messages = self.dialogs[previous_dialogs:]
errors = self.page.locator(".alert-danger:visible, .toast-error:visible")
if errors.count():
raise AssertionError(f"{name}: {'; '.join(errors.all_text_contents())}")
if any(re.search(r"fail|error|invalid|unable|already exists", text, re.I) for text in messages):
raise AssertionError(f"{name}: {'; '.join(messages)}")
if success.count() or any(re.search(r"success|saved|activated|updated", text, re.I) for text in messages):
self.capture(name)
return
# Poll business confirmation while servicing Playwright dialog events.
self.page.wait_for_timeout(100)
raise AssertionError(f"{name}: no explicit success confirmation within {timeout}s. "
"Inspect the trace for the application's response/notification selector.")
def create_session(self, config, window):
self.menu(Locator.tradingsession)
self.click(Locator.addsessionbtn, "add_session")
self.choose(self.page.locator('[aria-label="meInstanceId activate"]'), config["product"]["me_instance"])
self.fill(Locator.inputstartdate, window[0])
self.fill(Locator.inputenddate, window[1])
self.capture("session_values")
self.save(Locator.sessionsavebtn, "session_saved")
def create_token(self, token):
code = token["product_code"]
self.menu(Locator.tokengeneration)
self.choose(self.page.locator(f"#{Locator.tokenmarketcode} .ui-select-toggle"), token["market_code"])
self.choose(self.visible("//label[normalize-space()='PRODUCT CODE']/following::span[contains(@class,'ui-select-toggle')][1]"), code)
self.click(Locator.addnewtokenbtn, "add_token")
self.fill(Locator.tokendeliverystartdate, token["delivery_date"])
self.fill(Locator.tokendeliveryenddate, token["delivery_date"])
self.fill(Locator.tokenexpirydate, token["expiry"])
self.save(Locator.savetokenbtn, "token_saved")
self.visible(Locator.tokentimesessionbtn).click()
self.grid_button("Time Session Details")
self.edit_and_activate_token(token)
print(f"Created and activated token for {code}")
def resume_token(self, token, token_id):
self.token_id = token_id
self.menu("//a[@href='#/tradingMaintenance/timeSessionView']")
for element_id, value in (("mmMarketCode", token["market_code"]),
("mmProductType", token["product_type"]),
("mmProductCode", token["product_code"])):
self.choose(self.page.locator(f"#{element_id} .ui-select-toggle"), value)
self.visible("//button[@ng-click='getTokenDetailsList()' or @data-ng-click='getTokenDetailsList()']").click()
self.grid_button("Time Session Details", token_id)
self.capture("resume_saved_token")
self.edit_and_activate_token(token)
print(f"Updated and activated existing token {token_id}")
def edit_and_activate_token(self, token):
for name, start, end in token["rows"]:
self.grid_button("Details", name)
self.visible(Locator.edittimesessionbtn).click()
self.fill(Locator.timesessionstartdate, token["session_date"])
self.fill(Locator.timesessionenddate, token["session_date"])
self.fill(Locator.timesessionstarttime, start)
self.fill(Locator.timesessionendtime, end)
self.save(Locator.savetimesessionbtn, f"{name.lower()}_saved")
self.grid_button("Token Details")
self.save(Locator.savetokenandactivatebtn, "token_activated")
def logout(self):
self.visible(Locator.usermenutoggle).click()
self.visible(Locator.logoutlink).click()
expect(self.visible(Locator.userid)).to_be_visible()
self.capture("logout")
-10
View File
@@ -1,10 +0,0 @@
def test_session_token_setup(setup_app, setup_config, setup_plan):
"""Run selected setup steps in order, within a single authenticated session."""
if setup_plan["session"]:
setup_app.create_session(setup_config, setup_plan["session"])
for token in setup_plan["tokens"]:
if setup_plan["resume_token_id"]:
setup_app.resume_token(token, setup_plan["resume_token_id"])
else:
setup_app.create_token(token)
setup_app.logout()
+6 -1
View File
@@ -1,5 +1,4 @@
[pytest] [pytest]
norecursedirs = .* venv .venv-playwright playwright_tests playwright_checks
markers = markers =
session: session creation session: session creation
trading_token: trading token creation trading_token: trading token creation
@@ -13,3 +12,9 @@ markers =
reports: report download coverage reports: report download coverage
order_book: order book coverage order_book: order book coverage
trade_book: trade book coverage trade_book: trade book coverage
captcha: CAPTCHA login stress test
filterwarnings =
ignore::DeprecationWarning:easyocr.*
ignore::DeprecationWarning:torch.*
ignore::UserWarning:torch.*
-4
View File
@@ -1,4 +0,0 @@
pytest==8.4.1
pytest-html==4.2.0
playwright==1.62.0
pytest-playwright==0.9.0
+10 -2
View File
@@ -1,6 +1,14 @@
# Core test runner # Core test runner
pytest==8.4.1 pytest==9.1.1
pytest-html==4.2.0 pytest-html==4.2.0
# Browser automation # Browser automation
selenium==4.34.2 selenium==4.50.0
# CAPTCHA OCR and image processing
easyocr==1.7.2
opencv-python==5.0.0.93
Pillow==12.3.0
numpy==2.5.3
torch==2.14.1
torchvision==0.29.1
+10 -1
View File
@@ -1,4 +1,7 @@
import os
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.chrome.service import Service
def create_chrome_driver(): def create_chrome_driver():
@@ -10,5 +13,11 @@ def create_chrome_driver():
options.add_argument("--disable-features=CalculateNativeWinOcclusion") options.add_argument("--disable-features=CalculateNativeWinOcclusion")
options.add_argument("--disable-hang-monitor") options.add_argument("--disable-hang-monitor")
options.add_argument("--disable-popup-blocking") options.add_argument("--disable-popup-blocking")
options.add_argument("--disable-background-networking")
options.add_argument("--disable-component-update")
options.add_argument("--disable-sync")
options.add_argument("--disable-default-apps")
options.add_experimental_option("excludeSwitches", ["enable-logging"])
return webdriver.Chrome(options=options) service = Service(log_output=os.devnull)
return webdriver.Chrome(service=service, options=options)
+353
View File
@@ -0,0 +1,353 @@
"""
CAPTCHA Solver for PXIL Login using EasyOCR.
Extracts base64 from input's CSS background-image, runs EasyOCR OCR, fills the input.
"""
import re
import base64
import time
from typing import Optional
from PIL import Image
import cv2
import numpy as np
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import StaleElementReferenceException, TimeoutException
# Initialize EasyOCR once (module level)
# First run downloads models, then works offline
try:
import easyocr
_EASYOCR_READER = easyocr.Reader(['en'], gpu=False)
except ImportError:
_EASYOCR_READER = None
# Character confusion correction map for common OCR errors
CONFUSION_MAP = {
'0': 'O', 'O': '0', 'D': '0',
'1': 'I', 'I': '1', 'L': '1', 'l': '1',
'5': 'S', 'S': '5',
'8': 'B', 'B': '8',
'2': 'Z', 'Z': '2',
'6': 'G', 'G': '6',
'9': 'g', 'g': '9',
'Q': 'O',
'U': 'V', 'V': 'U',
'n': 'h', 'h': 'n',
'r': 'f', 'f': 'r',
'c': 'e', 'e': 'c',
'o': '0', '0': 'o',
's': '5', '5': 's',
'z': '2', '2': 'z',
'i': '1', '1': 'i',
'l': '1', '1': 'l',
}
def find_visible_captcha_input(driver: WebDriver) -> WebElement:
"""Find the visible captcha input element (name='captcha')."""
wait = WebDriverWait(driver, 15)
inputs = wait.until(EC.presence_of_all_elements_located((By.NAME, "captcha")))
for inp in inputs:
if inp.is_displayed():
return inp
raise RuntimeError("No visible captcha input found")
def extract_captcha_base64(captcha_input: WebElement) -> bytes:
"""Extract base64 image data from input's style attribute. Returns raw JPEG bytes."""
style = captcha_input.get_attribute("style") or ""
match = re.search(r'background:\s*url\(["\']?data:image/[^;]+;base64,([^"\')]+)["\']?\)', style)
if not match:
raise RuntimeError(f"No base64 background-image found in style: {style[:200]}")
b64_data = match.group(1)
if not b64_data:
raise RuntimeError("Base64 data is empty (captcha not loaded yet)")
b64_data += "=" * ((4 - len(b64_data) % 4) % 4) # Fix padding
return base64.b64decode(b64_data)
def wait_for_captcha_load(driver: WebDriver, captcha_input: WebElement, timeout: int = 15) -> bool:
"""Wait for captcha base64 to be populated with actual image data."""
end_time = time.time() + timeout
while time.time() < end_time:
try:
style = captcha_input.get_attribute("style") or ""
match = re.search(r'background:\s*url\(["\']?data:image/[^;]+;base64,([^"\')]+)["\']?\)', style)
if match and match.group(1):
return True
except StaleElementReferenceException:
captcha_input = find_visible_captcha_input(driver)
time.sleep(0.3)
return False
def preprocess_captcha_image(image_bytes: bytes) -> Image.Image:
"""Preprocess image for better OCR accuracy - gentle preprocessing."""
nparr = np.frombuffer(image_bytes, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) # Load as color (BGR)
if img is None:
raise RuntimeError("Failed to decode image bytes")
# Convert BGR to RGB
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# Simple threshold - keep it simple
_, gray = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY)
# Light denoise
gray = cv2.fastNlMeansDenoising(gray, h=3)
# Very light morphological close - only connect broken parts
kernel = np.ones((1, 1), np.uint8)
gray = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel)
# Resize 6x for better recognition (larger for 6-char captcha)
gray = cv2.resize(gray, None, fx=6, fy=6, interpolation=cv2.INTER_CUBIC)
# Convert back to RGB (3-channel) for EasyOCR
rgb = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB)
return Image.fromarray(rgb)
def apply_confusion_corrections(text: str) -> str:
"""Apply character confusion corrections based on common OCR errors."""
corrected = []
for ch in text:
corrected.append(CONFUSION_MAP.get(ch, ch))
return ''.join(corrected)
def ocr_captcha(image: Image.Image) -> str:
"""Run OCR on preprocessed captcha image using EasyOCR."""
if _EASYOCR_READER is None:
raise RuntimeError("EasyOCR not installed. Run: pip install easyocr")
img_array = np.array(image)
# EasyOCR with alphanumeric allowlist for 6-char captcha
results = _EASYOCR_READER.readtext(
img_array,
detail=0,
allowlist='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
width_ths=0.5,
paragraph=False
)
# Combine all detected text
result = ''.join(results)
# Clean result - keep only alphanumeric, max 6 chars
cleaned = ''.join(filter(str.isalnum, result))[:6]
print(f" DEBUG OCR: result='{result}', cleaned='{cleaned}', len={len(cleaned)}")
return cleaned
def find_refresh_button(driver: WebDriver, captcha_input: WebElement) -> Optional[WebElement]:
"""Find captcha refresh button near the captcha input."""
parent = captcha_input.find_element(By.XPATH, "..")
for sibling in parent.find_elements(By.XPATH, "*"):
onclick = (sibling.get_attribute("onclick") or "").lower()
class_attr = (sibling.get_attribute("class") or "").lower()
id_attr = (sibling.get_attribute("id") or "").lower()
if any(kw in onclick for kw in ["refresh", "captcha", "reload", "newcaptcha"]):
return sibling
if any(kw in class_attr for kw in ["refresh", "captcha-refresh", "reload"]):
return sibling
if any(kw in id_attr for kw in ["refresh", "captcha", "reload"]):
return sibling
# Fallback: search globally for refresh-like elements
try:
candidates = driver.find_elements(By.XPATH, "//*[self::a or self::button or self::img or self::span][contains(translate(@onclick, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'refresh') or contains(translate(@class, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'refresh') or contains(translate(@title, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'refresh')]")
for cand in candidates:
if cand.is_displayed():
return cand
except:
pass
return None
def wait_for_new_captcha(driver: WebDriver, captcha_input: WebElement, timeout: int = 10) -> bool:
"""Wait for captcha background-image to change (new challenge loaded)."""
old_style = captcha_input.get_attribute("style") or ""
end_time = time.time() + timeout
while time.time() < end_time:
try:
new_style = captcha_input.get_attribute("style") or ""
if new_style != old_style and "data:image" in new_style:
return True
except StaleElementReferenceException:
captcha_input = find_visible_captcha_input(driver)
old_style = captcha_input.get_attribute("style") or ""
time.sleep(0.3)
return False
def solve_and_fill_captcha(driver: WebDriver, max_retries: int = 5, attempt_callback=None) -> bool:
"""
Main entry point: solve captcha and fill input.
Returns True if filled successfully, False if max retries exceeded.
Each retry gets a fresh captcha via refresh button.
"""
for attempt in range(1, max_retries + 1):
captcha_text = ""
try:
captcha_input = find_visible_captcha_input(driver)
# Wait for captcha base64 to be populated (async load)
if not wait_for_captcha_load(driver, captcha_input):
print(f" [Attempt {attempt}] Captcha failed to load in time, retrying...")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
if attempt_callback:
attempt_callback(attempt, "", "", "not_loaded")
continue
image_bytes = extract_captcha_base64(captcha_input)
pil_image = preprocess_captcha_image(image_bytes)
captcha_text = ocr_captcha(pil_image)
if not captcha_text or len(captcha_text) < 6:
print(f" [Attempt {attempt}] OCR weak result: '{captcha_text}' (len={len(captcha_text)}), retrying...")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
if attempt_callback:
attempt_callback(attempt, captcha_text, "", "ocr_failed")
continue
# Apply confusion corrections to get best variant
corrected = apply_confusion_corrections(captcha_text)
# Generate case variants for case-sensitive captcha (mixed case + numbers)
variants = [
captcha_text, # as-is (from OCR)
corrected, # with confusion corrections
captcha_text.upper(), # all uppercase
captcha_text.capitalize(), # first letter uppercase
captcha_text.title(), # each word capitalized
captcha_text.swapcase(), # swap case
]
# Add variations with first char variations
if len(captcha_text) >= 1:
variants.append(captcha_text[0].upper() + captcha_text[1:])
variants.append(captcha_text[0].lower() + captcha_text[1:])
if len(captcha_text) >= 2:
variants.append(captcha_text[:2].upper() + captcha_text[2:])
variants.append(captcha_text[:2].lower() + captcha_text[2:])
if len(captcha_text) >= 3:
variants.append(captcha_text[:3].upper() + captcha_text[3:])
variants.append(captcha_text[:3].lower() + captcha_text[3:])
# Remove duplicates while preserving order, keep only 6-char variants
seen = set()
unique_variants = []
for v in variants:
if v not in seen and len(v) == 6:
seen.add(v)
unique_variants.append(v)
variants = unique_variants
if attempt_callback:
screenshot_path = attempt_callback(attempt, captcha_text, "", "before_entry") or ""
success = False
# Try ALL variants before giving up on this attempt
for variant in variants:
if len(variant) == 6:
print(f" [Attempt {attempt}] Trying variant: '{variant}'")
captcha_input.clear()
captcha_input.send_keys(variant)
success = True
if attempt_callback:
attempt_callback(attempt, captcha_text, variant, "filled")
break
if not success:
print(f" [Attempt {attempt}] No valid 6-char variant found for '{captcha_text}', retrying...")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
continue
# Click submit
submit_btn = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, "//button[@value='Login']"))
)
submit_btn.click()
# Wait a bit for response
time.sleep(2)
# Check for invalid captcha error using the exact xpath
try:
# First handle any alerts that might be open
try:
alert = driver.switch_to.alert
alert_text = alert.text
print(f" DEBUG: Alert detected after submit: {alert_text}")
if "logout" in alert_text.lower() or "session" in alert_text.lower() or "previous" in alert_text.lower():
alert.accept()
print(" DEBUG: Accepted session/logout alert")
time.sleep(2)
except:
pass
invalid_captcha_elem = WebDriverWait(driver, 3).until(
EC.visibility_of_element_located((
By.XPATH,
"//div[@id='error']//strong[contains(normalize-space(), 'Invalid Captcha')]",
))
)
print(f" [Attempt {attempt}] Invalid captcha detected: {invalid_captcha_elem.text}")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
continue
except TimeoutException:
# No invalid captcha error - likely success
print(f" [Attempt {attempt}] No invalid captcha error detected, assuming success")
return True
except Exception as e:
# Handle other exceptions like NoSuchElementException
print(f" [Attempt {attempt}] Error checking invalid captcha: {type(e).__name__}: {e}")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
continue
except Exception as e:
print(f" [Attempt {attempt}] Error: {e}")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
return False
+78
View File
@@ -1,4 +1,5 @@
import json import json
from copy import deepcopy
from pathlib import Path from pathlib import Path
@@ -11,6 +12,83 @@ def load_config():
return json.load(f) return json.load(f)
def get_product_config(product_name, config=None):
"""Resolve a product's setup flags and session defaults without mutating input."""
config = deepcopy(load_config() if config is None else config)
if "products" not in config:
if product_name != "IDAS":
raise ValueError(f"Unknown setup product: {product_name}")
config["token"] = config.get("trading_token_idas", {})
else:
product = config["products"][product_name]
setup = config.setdefault("setup", {})
for flag in ("run_session", "run_token"):
setup[flag] = product.get(flag, setup.get(flag, True))
if not isinstance(setup[flag], bool):
raise ValueError(f"products.{product_name}.{flag} must be true or false")
setup["session"] = {**setup.get("session", {}), **product.get("session", {})}
config["product"] = {"me_instance": product.get("me_instance")}
config["token"] = deepcopy(product.get("token", {}))
config["product_name"] = product_name
return config
def get_setup_configs(mode, config=None):
"""Partition products so a full setup run never creates a product twice."""
if mode not in ("combined", "session", "token"):
raise ValueError(f"Unknown setup mode: {mode}")
config = load_config() if config is None else config
products = config.get("products", {"IDAS": {}})
selected = []
for name, product in products.items():
if not product.get("enabled", True):
continue
variants = product.get("session_variants")
if variants is not None:
resolved = get_product_config(name, config)
# Expand variants when either session or token execution is
# enabled. Token-only UPC runs must still reach their variants.
setup = resolved.get("setup", {})
if not setup.get("run_session", True) and not setup.get("run_token", True):
continue
variant_config = deepcopy(config)
variant_config["products"] = {}
for variant_name, variant in variants.items():
settings = deepcopy(product)
settings.pop("session_variants")
settings.update(variant)
settings["session"] = {**product.get("session", {}), **variant.get("session", {})}
settings["token"] = {**product.get("token", {}), **variant.get("token", {})}
variant_config["products"][f"{name}_{variant_name}"] = settings
selected.extend(get_setup_configs(mode, variant_config))
continue
resolved = get_product_config(name, config)
setup = resolved.get("setup", {})
session = setup.get("run_session", True)
token = setup.get("run_token", True)
matches = {
# The full setup run owns every enabled product that has work to do.
# Products without tokens (for example RA) still belong in this
# shared browser flow; the combined test skips only their token step.
"combined": session or token,
"session": session and not token,
"token": token and not session,
}
if not matches[mode]:
continue
if session and not resolved.get("product", {}).get("me_instance"):
raise ValueError(f"products.{name}.me_instance is required for session creation")
if token:
settings = resolved["token"]
if not settings.get("market_code") or not (
parse_product_codes(settings.get("create_product_codes", settings.get("product_codes")))
or parse_product_codes(settings.get("product_code"))
):
raise ValueError(f"products.{name}.token requires market_code and product_code(s)")
selected.append(resolved)
return selected
def get_step_delay_seconds(default=5): def get_step_delay_seconds(default=5):
config = load_config() config = load_config()
+57 -10
View File
@@ -5,15 +5,15 @@ import json
from tests.browser import create_chrome_driver from tests.browser import create_chrome_driver
def get_run_id(config): def get_run_id(config, prefix=None):
run_id = getattr(config, "_pxil_run_id", None) run_prefix = prefix or _get_run_prefix(config)
if run_id: run_ids = getattr(config, "_pxil_run_ids", {})
return run_id if run_prefix in run_ids:
return run_ids[run_prefix]
base_folder = os.path.join("reports", "screenshots") base_folder = os.path.join("reports", "screenshots")
os.makedirs(base_folder, exist_ok=True) os.makedirs(base_folder, exist_ok=True)
run_prefix = _get_run_prefix(config)
existing = [ existing = [
name for name in os.listdir(base_folder) name for name in os.listdir(base_folder)
@@ -28,10 +28,11 @@ def get_run_id(config):
for name in existing for name in existing
if name.split("_")[-1].isdigit() if name.split("_")[-1].isdigit()
] ]
next_number = max(numbers) + 1 next_number = max(numbers, default=0) + 1
run_id = f"{run_prefix}_{next_number:03d}" run_id = f"{run_prefix}_{next_number:03d}"
config._pxil_run_id = run_id run_ids[run_prefix] = run_id
config._pxil_run_ids = run_ids
return run_id return run_id
@@ -46,10 +47,34 @@ def pytest_configure(config):
config.option.self_contained_html = True config.option.self_contained_html = True
def pytest_collection_modifyitems(config, items):
"""Keep the full setup run limited to the shared combined flow."""
invocation = " ".join(
str(arg).replace("\\", "/") for arg in config.invocation_params.args
).lower()
full_setup_run = (
"tests/setup" in invocation
and "tests/setup/sessions" not in invocation
and "tests/setup/tokens" not in invocation
and "test_session.py" not in invocation
and "test_trading_token.py" not in invocation
)
if not full_setup_run:
return
deselected = [
item for item in items
if "/tests/setup/sessions/" in f"/{item.nodeid.replace('\\', '/')}"
or "/tests/setup/tokens/" in f"/{item.nodeid.replace('\\', '/')}"
]
items[:] = [item for item in items if item not in deselected]
config.hook.pytest_deselected(items=deselected)
# ----------------------------- # -----------------------------
# WebDriver Fixture # WebDriver Fixture
# ----------------------------- # -----------------------------
@pytest.fixture @pytest.fixture(scope="module")
def driver(): def driver():
driver = create_chrome_driver() driver = create_chrome_driver()
@@ -66,7 +91,7 @@ def driver():
@pytest.fixture @pytest.fixture
def test_context(request): def test_context(request):
test_id = get_run_id(request.config) test_id = get_run_id(request.config, _setup_item_prefix(request.node))
test_name = request.node.name test_name = request.node.name
folder = os.path.abspath( folder = os.path.abspath(
@@ -201,9 +226,28 @@ def _build_steps_html(steps):
) )
def _setup_item_prefix(item):
"""Use the collected product and flow, including when -k selects a subset."""
nodeid = item.nodeid.replace("\\", "/")
if "tests/setup/" not in nodeid:
return None
callspec = getattr(item, "callspec", None)
product_config = callspec.params.get("config") if callspec else None
if not isinstance(product_config, dict):
return None
product = product_config["product_name"].upper()
if "test_combined_setup.py" in nodeid:
return f"{product}_SESSION_TOKEN"
if "/sessions/" in nodeid:
return f"{product}_SESSION"
if "/tokens/" in nodeid:
return f"{product}_TOKEN"
return f"{product}_SETUP"
def _get_run_prefix(config): def _get_run_prefix(config):
invocation_args = " ".join(str(arg) for arg in config.invocation_params.args).lower() invocation_args = " ".join(str(arg) for arg in config.invocation_params.args).replace("\\", "/").lower()
if "tests/setup/sessions" in invocation_args or "test_session.py" in invocation_args: if "tests/setup/sessions" in invocation_args or "test_session.py" in invocation_args:
return "PXIL_SESSION" return "PXIL_SESSION"
@@ -211,6 +255,9 @@ def _get_run_prefix(config):
if "tests/setup/tokens" in invocation_args or "test_trading_token.py" in invocation_args: if "tests/setup/tokens" in invocation_args or "test_trading_token.py" in invocation_args:
return "PXIL_TOKEN" return "PXIL_TOKEN"
if "tests/setup" in invocation_args or "test_combined_setup.py" in invocation_args:
return "PXIL_SETUP"
runtime_path = os.path.abspath(os.path.join("config", "runtime.json")) runtime_path = os.path.abspath(os.path.join("config", "runtime.json"))
if os.path.exists(runtime_path): if os.path.exists(runtime_path):
try: try:
+230 -40
View File
@@ -57,43 +57,216 @@ def enter_pressed():
return pressed return pressed
def login(driver, username, password): def check_captcha_error(driver) -> str:
"""Check if there's a CAPTCHA error message on the page."""
try:
# First handle any alerts
try:
alert = driver.switch_to.alert
alert_text = alert.text
print(f" DEBUG: Alert detected in check_captcha_error: {alert_text}")
if "logout" in alert_text.lower() or "session" in alert_text.lower() or "previous" in alert_text.lower():
alert.accept()
print(" DEBUG: Accepted session/logout alert")
time.sleep(2)
except:
pass
error_selectors = [
"//div[@id='error']//strong[normalize-space()='Invalid Captcha']",
"//div[@id='error']//span[contains(@class,'help-block')]//strong[contains(text(),'Invalid')]",
"//*[contains(@class,'error') and contains(text(),'Invalid Captcha')]",
]
for selector in error_selectors:
elements = driver.find_elements(By.XPATH, selector)
for el in elements:
if el.is_displayed() and el.text.strip():
return el.text.strip()
except Exception as e:
print(f" DEBUG: check_captcha_error exception: {e}")
return ""
def check_login_success(driver) -> bool:
"""Check if login was successful using multiple indicators."""
try:
# Handle any pending alerts first
try:
alert = driver.switch_to.alert
alert_text = alert.text
print(f" DEBUG: Alert detected: {alert_text}")
if "logout" in alert_text.lower() or "session" in alert_text.lower() or "previous" in alert_text.lower():
alert.accept()
print(" DEBUG: Accepted session/logout alert")
time.sleep(2)
except:
pass
# Wait a bit for page to load after submit
time.sleep(3)
# Print current URL and page title for debugging
print(f" DEBUG: Current URL: {driver.current_url}")
print(f" DEBUG: Page title: {driver.title}")
# Multiple dashboard indicators
dashboard_indicators = [
"//span[normalize-space()='TRADING MAINTENANCE']",
"//a[contains(@href, 'tradingMaintenance')]",
"//span[contains(text(), 'Dashboard')]",
"//h1[contains(text(), 'Dashboard')]",
"//div[contains(@class, 'dashboard')]",
"//a[contains(text(), 'Trading')]",
"//div[contains(@class, 'navbar')]//span[contains(text(), 'Trading')]",
]
wait = WebDriverWait(driver, 15)
for indicator in dashboard_indicators:
try:
wait.until(EC.visibility_of_element_located((By.XPATH, indicator)))
print(f" DEBUG: Found dashboard indicator: {indicator}")
return True
except:
continue
# URL-based check
if "home" in driver.current_url.lower() or "dashboard" in driver.current_url.lower():
print(" DEBUG: Login success detected via URL")
return True
return False
except Exception as e:
print(f" DEBUG: check_login_success failed: {e}")
return False
def login(
driver,
username,
password,
max_captcha_retries=5,
captcha_metrics=None,
max_login_duration_seconds=1200,
):
wait = WebDriverWait(driver, 20) wait = WebDriverWait(driver, 20)
# username # --- AUTOMATED CAPTCHA SOLVING WITH RETRY ON FAILURE ---
userid_field = wait.until( from tests.captcha_solver import solve_and_fill_captcha, find_refresh_button, wait_for_new_captcha
EC.visibility_of_element_located((By.XPATH, Locator.userid))
) login_deadline = time.monotonic() + max_login_duration_seconds
userid_field.send_keys(username) captcha_attempt = 0
while True:
captcha_attempt += 1
# password # Refreshing after a failed CAPTCHA clears the login form, so refill
password_field = driver.find_element(By.XPATH, Locator.password) # both credentials before every automatic or manual retry.
password_field.send_keys(password) userid_field = wait.until(
EC.visibility_of_element_located((By.XPATH, Locator.userid))
)
userid_field.clear()
userid_field.send_keys(username)
print(f"Solve captcha within {LOGIN_CAPTCHA_WAIT_SECONDS} seconds...") password_field = wait.until(
EC.visibility_of_element_located((By.XPATH, Locator.password))
)
password_field.clear()
password_field.send_keys(password)
# wait for manual captcha solve if captcha_metrics is not None:
time.sleep(LOGIN_CAPTCHA_WAIT_SECONDS) captcha_metrics["tries"] = captcha_attempt
captcha_metrics["max_retries"] = max_captcha_retries
print(f"Solving CAPTCHA (attempt {captcha_attempt})...")
# click submit # Once automatic solving has run for the configured duration, ask for
submit_btn = wait.until( # the CAPTCHA value in a browser prompt and continue the same login flow.
EC.element_to_be_clickable((By.XPATH, Locator.submitbtn)) if time.monotonic() >= login_deadline:
) print("Automatic CAPTCHA retries timed out; waiting for manual CAPTCHA entry.")
submit_btn.click() manual_value = driver.execute_script(
"return window.prompt('Automatic CAPTCHA solving timed out. Enter the CAPTCHA shown on the page:');"
)
if not manual_value:
raise RuntimeError("Manual CAPTCHA entry was cancelled")
time.sleep(LOGIN_POST_SUBMIT_WAIT_SECONDS) captcha_input = wait.until(
EC.visibility_of_element_located((By.NAME, "captcha"))
)
captcha_input.clear()
captcha_input.send_keys(manual_value.strip())
wait.until(EC.element_to_be_clickable((By.XPATH, Locator.submitbtn))).click()
time.sleep(LOGIN_POST_SUBMIT_WAIT_SECONDS)
accept_browser_alert_if_present(driver)
handle_transient_popups(driver)
accept_browser_alert_if_present(driver) captcha_error = check_captcha_error(driver)
if captcha_error:
print(f" Manual CAPTCHA rejected: {captcha_error}")
continue
# wait for dashboard if check_login_success(driver):
wait.until( print("Login successful after manual CAPTCHA entry")
EC.visibility_of_element_located((By.XPATH, Locator.dashboard_marker)) if captcha_metrics is not None:
) captcha_metrics["correct_attempt"] = captcha_attempt
captcha_metrics.setdefault("captcha_attempts", []).append({
close_tac_expiry_modal_if_present(driver) "attempt": captcha_attempt,
"captcha_text": "manual",
print("Login successful") "variant": manual_value.strip(),
"status": "manual",
"screenshot": "",
"error": "",
})
close_tac_expiry_modal_if_present(driver)
return True
continue
def attempt_callback(attempt_num, captcha_text, variant, status, screenshot_path="", error=""):
if captcha_metrics is not None:
captcha_metrics.setdefault("captcha_attempts", []).append({
"attempt": attempt_num,
"captcha_text": captcha_text,
"variant": variant,
"status": status,
"screenshot": screenshot_path,
"error": error
})
if not solve_and_fill_captcha(driver, max_retries=1, attempt_callback=attempt_callback):
print(" Automatic CAPTCHA solve failed; retrying until the login deadline.")
driver.refresh()
time.sleep(3)
continue
# solve_and_fill_captcha already clicks submit, so just wait and check result
time.sleep(LOGIN_POST_SUBMIT_WAIT_SECONDS)
accept_browser_alert_if_present(driver)
# Handle any popups that appear
handle_transient_popups(driver)
# Check for CAPTCHA error (invalid captcha message)
captcha_error = check_captcha_error(driver)
if captcha_error:
print(f" CAPTCHA validation failed: {captcha_error}")
# Reload the page to get a fresh CAPTCHA and keep retrying until
# the overall login deadline is reached.
print(f" Reloading page to get fresh CAPTCHA (attempt {captcha_attempt + 1})...")
driver.refresh()
time.sleep(3)
continue
# Check for successful login - use improved detection
if check_login_success(driver):
print("Login successful")
if captcha_metrics is not None:
captcha_metrics["correct_attempt"] = captcha_attempt
close_tac_expiry_modal_if_present(driver)
return True
# If we get here, something else is wrong
print(" Login not confirmed, retrying...")
driver.refresh()
time.sleep(3)
def accept_browser_alert_if_present(driver, timeout=5): def accept_browser_alert_if_present(driver, timeout=5):
@@ -309,23 +482,34 @@ def open_trading_maintenance_page(driver, menu_xpath):
wait = WebDriverWait(driver, 20) wait = WebDriverWait(driver, 20)
trading_maintenance = wait.until( def visible_elements(xpath):
EC.presence_of_element_located((By.XPATH, Locator.tradingmaintenance)) return [element for element in driver.find_elements(By.XPATH, xpath) if element.is_displayed()]
)
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", trading_maintenance)
submenu = driver.find_elements(By.XPATH, menu_xpath) submenu_visible = any(
submenu_visible = any(item.is_displayed() for item in submenu) item.is_enabled() for item in visible_elements(menu_xpath)
)
if not submenu_visible: if not submenu_visible:
maintenance_items = visible_elements(Locator.tradingmaintenance)
if not maintenance_items:
wait.until(lambda current_driver: visible_elements(Locator.tradingmaintenance))
maintenance_items = visible_elements(Locator.tradingmaintenance)
maintenance = maintenance_items[0]
driver.execute_script(
"arguments[0].scrollIntoView({block: 'center'});", maintenance
)
try: try:
wait.until(EC.element_to_be_clickable((By.XPATH, Locator.tradingmaintenance))) wait.until(lambda current_driver: maintenance.is_enabled())
trading_maintenance.click() maintenance.click()
except Exception: except Exception:
driver.execute_script("arguments[0].click();", trading_maintenance) driver.execute_script("arguments[0].click();", maintenance)
menu_item = wait.until( menu_item = wait.until(
EC.element_to_be_clickable((By.XPATH, menu_xpath)) lambda current_driver: next(
(item for item in visible_elements(menu_xpath) if item.is_enabled()),
False,
)
) )
try: try:
menu_item.click() menu_item.click()
@@ -367,7 +551,7 @@ def type_into_field(driver, locator, value):
field.send_keys(value) field.send_keys(value)
def select_bootstrap_dropdown_by_id(driver, element_id, value): def select_bootstrap_dropdown_by_id(driver, element_id, value, exact=False):
wait = WebDriverWait(driver, 20) wait = WebDriverWait(driver, 20)
@@ -411,12 +595,18 @@ def select_bootstrap_dropdown_by_id(driver, element_id, value):
search_box.clear() search_box.clear()
search_box.send_keys(value) search_box.send_keys(value)
option_xpath = (
f"//div[@id='{element_id}']//div[contains(@class,'ui-select-choices-row')]"
f"[normalize-space()='{value}']"
if exact else
"//div[contains(@class,'ui-select-choices-row')]"
f"//*[normalize-space()='{value}' or starts-with(normalize-space(),'{value}')]"
)
option = wait.until( option = wait.until(
EC.element_to_be_clickable( EC.element_to_be_clickable(
( (
By.XPATH, By.XPATH,
"//div[contains(@class,'ui-select-choices-row')]" option_xpath
f"//*[normalize-space()='{value}' or starts-with(normalize-space(),'{value}')]"
) )
) )
) )
+33 -2
View File
@@ -4,16 +4,27 @@ class Locator(object):
userid="//input[@placeholder='User ID']" userid="//input[@placeholder='User ID']"
password="//input[@placeholder='Password']" password="//input[@placeholder='Password']"
submitbtn="//button[@value='Login']" submitbtn="//button[@value='Login']"
dashboard_marker="//span[normalize-space()='TRADING MAINTENANCE']" dashboard_marker="//*[contains(normalize-space(), 'TRADING MAINTENANCE')]"
tacexpiryclosebtn="//div[contains(@class,'modal') and contains(@style,'display: block')]//button[contains(@class,'close')]" 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']" 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']" 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']" # Click the Angular menu anchor, not the nested label span. The anchor
# owns openSubMenu(item), which is required to render its child links.
tradingmaintenance="//a[.//span[normalize-space()='TRADING MAINTENANCE']]"
tradingsession="//a[normalize-space()='Trading Session Master']" tradingsession="//a[normalize-space()='Trading Session Master']"
tokengeneration="//a[@href='#/tradingMaintenance/generateToken' or normalize-space()='Token Generation']" tokengeneration="//a[@href='#/tradingMaintenance/generateToken' or normalize-space()='Token Generation']"
tokengenerator = "//a[@href='#/tradingMaintenance/tokenGenerator']"
generatormarketcode = "marketCode"
generatorproductcode = "productCode"
generatorgenerationmode = "tokenMstr.genMode"
rtmdeliveryfromdate = "//div[@ng-show='showgeneration4']//input[@placeholder='Delivery From Date']"
rtmdeliverytodate = "//div[@ng-show='showgeneration4']//input[@placeholder='Delivery To Date']"
rtmsessionstarttime = "//div[@ng-show='showgeneration4']//input[@placeholder='Session Start Time']"
rtmsessionendtime = "//div[@ng-show='showgeneration4']//input[@placeholder='Session End Time']"
tokenandtimesessionview="//a[@href='#/tradingMaintenance/tokenTimeSessionMap' or normalize-space()='Token and Time Session View']" tokenandtimesessionview="//a[@href='#/tradingMaintenance/tokenTimeSessionMap' or normalize-space()='Token and Time Session View']"
addsessionbtn="//button[contains(text(),'Add')]" addsessionbtn="//button[contains(text(),'Add')]"
addnewtokenbtn="//button[@data-ng-click='addToken()' and normalize-space()='Add New Token']" addnewtokenbtn="//button[@data-ng-click='addToken()' and normalize-space()='Add New Token']"
tokengeneratorresetbtn = "//button[@data-ng-click='refresh()' and normalize-space()='Reset']"
setmeinstance="//span[@aria-label='meInstanceId activate']" setmeinstance="//span[@aria-label='meInstanceId activate']"
#inputmeid="meInstanceId activate" #inputmeid="meInstanceId activate"
#"//li[@id='ui-select-choices-178']" #"//li[@id='ui-select-choices-178']"
@@ -27,6 +38,26 @@ class Locator(object):
tokendeliverystartdate = "//input[@placeholder='DELIVERY_START_DATE']" tokendeliverystartdate = "//input[@placeholder='DELIVERY_START_DATE']"
tokendeliveryenddate = "//input[@placeholder='DELIVERY_END_DATE']" tokendeliveryenddate = "//input[@placeholder='DELIVERY_END_DATE']"
tokenexpirydate = "//input[@placeholder='Token Expiry Date']" tokenexpirydate = "//input[@placeholder='Token Expiry Date']"
tokentype = "//input[@name='TYPE' or @placeholder='TYPE']"
intradaydeliveryfromdate = "//input[@placeholder='Delivery From Date']"
intradaydeliverytodate = "//input[@placeholder='Delivery To Date']"
intradaytradingstarttime = "//input[@placeholder='Trading Start Time']"
intradaydaysbeforedelivery = "//input[@placeholder='Trading Start days before delivery']"
intradaydurationbeforeend = "//input[@placeholder='Duration before delivery end time']"
intradaysuccessmessage = "//*[contains(normalize-space(text()), 'Tokens generated successfully')]"
dactradingfromdate = "//input[@placeholder='Trading From Date']"
dactradingtodate = "//input[@placeholder='Trading To Date']"
dacsessionstarttime = "//input[@placeholder='Session Start Time']"
dacsessionendtime = "//input[@placeholder='Session End Time']"
tokentradedate = "//input[@placeholder='Trade Date']"
upcordernormalstart = "//input[@name='ses3StartTime']"
upcordernormalend = "//input[@name='ses3EndTime']"
upctradesessionstart = "//input[@name='sesStartTime']"
upctradesessionend = "//input[@name='sesEndTime']"
upcorderstart = "//input[@placeholder='Order Normal Start Time']"
upcorderend = "//input[@placeholder='Order Normal End Time']"
upctradestart = "//input[@placeholder='Trade Start Time' or @placeholder='Trade Session Start Time']"
upctradeend = "//input[@placeholder='Trade End Time' or @placeholder='Trade Session End Time']"
savetokenbtn = "//button[@data-ng-click='saveToken()' and normalize-space()='save Token']" savetokenbtn = "//button[@data-ng-click='saveToken()' and normalize-space()='save Token']"
tokentimesessionbtn = "//button[@data-ng-click='redirectToTimeSession()' and normalize-space()='Time Session']" tokentimesessionbtn = "//button[@data-ng-click='redirectToTimeSession()' and normalize-space()='Time Session']"
viewproducttype = "pmProductType" viewproducttype = "pmProductType"
+22 -16
View File
@@ -5,34 +5,35 @@ from selenium.webdriver.support.ui import WebDriverWait
import pytest import pytest
from tests.step_actions import StepActions from tests.step_actions import StepActions
from tests.helpers import login, select_ui_dropdown from tests.helpers import login, select_ui_dropdown, open_trading_maintenance_page
from tests.config_reader import is_combined_setup_enabled, is_setup_session_enabled, load_config from tests.config_reader import get_setup_configs
from tests.locators import Locator 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): def get_session_window(config):
session_config = config.get("setup", {}).get("session", {}) session_config = config.get("setup", {}).get("session", {})
start_offset_minutes = session_config.get("start_offset_minutes", 7) time_mode = session_config.get("time_mode", "offset").lower()
if time_mode == "manual":
start_time = session_config.get("start_time")
if not start_time:
raise ValueError("Session time_mode 'manual' requires start_time")
elif time_mode == "offset":
start_offset_minutes = session_config.get("start_offset_minutes", 7)
start_time = (datetime.now() + timedelta(minutes=start_offset_minutes)).strftime("%H:%M")
else:
raise ValueError("Session time_mode must be 'manual' or 'offset'")
end_time = session_config.get("end_time", "20:30") 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") today = datetime.now().strftime("%d/%m/%Y")
return start_datetime.strftime("%d/%m/%Y %H:%M"), f"{today} {end_time}" return f"{today} {start_time}", f"{today} {end_time}"
def create_session(driver, step, config): def create_session(driver, step, config):
step.click((By.XPATH, Locator.tradingmaintenance), "trading_maintenance") open_trading_maintenance_page(driver, Locator.tradingsession)
step.capture("trading_session")
step.click((By.XPATH, Locator.tradingsession), "trading_session")
step.click((By.XPATH, Locator.addsessionbtn), "add_session") step.click((By.XPATH, Locator.addsessionbtn), "add_session")
@@ -50,9 +51,14 @@ def create_session(driver, step, config):
@pytest.mark.session @pytest.mark.session
def test_session(driver, test_context): @pytest.mark.parametrize("config", get_setup_configs("session"), ids=lambda c: c["product_name"])
def test_session(driver, test_context, config, request):
config = load_config() # The full setup command runs session-only products (such as RA) through
# test_combined_setup.py so they share the same browser/login context.
invocation = " ".join(str(arg).replace("\\", "/") for arg in request.config.invocation_params.args)
if "tests/setup" in invocation and "tests/setup/sessions" not in invocation and "test_session.py" not in invocation:
pytest.skip("Handled by the shared full setup flow")
wait = WebDriverWait(driver, 20) wait = WebDriverWait(driver, 20)
+20 -27
View File
@@ -1,46 +1,39 @@
import pytest import pytest
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from tests.config_reader import is_combined_setup_enabled, load_config from tests.config_reader import get_setup_configs
from tests.helpers import login, logout, save_session_backup from tests.helpers import login, logout, save_session_backup
from tests.setup.sessions.test_session import create_session 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.setup.tokens.test_trading_token import run_token_setup, get_product_codes
from tests.step_actions import StepActions 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.session
@pytest.mark.trading_token @pytest.mark.trading_token
def test_session_and_token_setup(driver, test_context): @pytest.mark.parametrize("config", get_setup_configs("combined"), ids=lambda c: c["product_name"])
def test_session_and_token_setup(driver, test_context, config):
config = load_config() token = config["token"]
token = config.get("trading_token_idas")
assert token, "Add trading_token_idas in config.json before running token setup."
wait = WebDriverWait(driver, 20) wait = WebDriverWait(driver, 20)
step = StepActions(driver, wait, test_context) step = StepActions(driver, wait, test_context)
driver.get(config["url"]) if not getattr(driver, "_pxil_logged_in", False):
step.capture("open_application") driver.get(config["url"])
step.capture("open_application")
login( if not getattr(driver, "_pxil_logged_in", False):
driver, login(driver, config["login"]["username"], config["login"]["password"])
config["login"]["username"], driver._pxil_logged_in = True
config["login"]["password"] step.capture("login")
) save_session_backup(driver, test_context, "login_session_backup")
step.capture("login")
save_session_backup(driver, test_context, "login_session_backup")
create_session(driver, step, config) if config.get("setup", {}).get("run_session", True):
create_session(driver, step, config)
for product_code in get_product_codes(token): if config.get("setup", {}).get("run_token", True):
product_token = dict(token) for product_code in get_product_codes(token):
product_token["product_code"] = product_code product_token = dict(token)
create_and_activate_token(driver, step, product_token) product_token["product_code"] = product_code
run_token_setup(driver, step, product_token)
logout(driver)
+279 -31
View File
@@ -3,8 +3,9 @@ from datetime import datetime, timedelta
import pytest import pytest
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from tests.config_reader import is_combined_setup_enabled, is_setup_token_enabled, load_config, parse_product_codes from tests.config_reader import get_setup_configs, parse_product_codes
from tests.helpers import ( from tests.helpers import (
click_visible_menu_item, click_visible_menu_item,
click_first_grid_button, click_first_grid_button,
@@ -23,20 +24,10 @@ from tests.locators import Locator
from tests.step_actions import StepActions from tests.step_actions import StepActions
pytestmark = pytest.mark.skipif( def get_token_context(driver, test_context, config):
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) wait = WebDriverWait(driver, 20)
step = StepActions(driver, wait, test_context) step = StepActions(driver, wait, test_context)
token = config.get("trading_token_idas") token = config["token"]
assert token, "Add trading_token_idas in config.json before running this test."
return config, step, token return config, step, token
@@ -65,15 +56,28 @@ def get_delivery_date_and_expiry(token):
return delivery_date, token_expiry return delivery_date, token_expiry
def get_intraday_delivery_window(token, now=None):
"""Return an Intraday delivery window that cannot start in the past."""
now = datetime.now() if now is None else now
start = now + timedelta(minutes=token.get("delivery_start_offset_minutes", 5))
start_text = start.strftime("%d/%m/%Y %H:%M")
end_text = f"{start.strftime('%d/%m/%Y')} {token.get('delivery_to_time', '23:59')}"
return start_text, end_text
def get_time_session_times(token, row_text, default_start_time, default_end_time): def get_time_session_times(token, row_text, default_start_time, default_end_time):
time_session_config = token.get("time_sessions", {}).get(row_text, {}) time_session_config = token.get("time_sessions", {}).get(row_text, {})
time_mode = time_session_config.get("time_mode")
start_time = time_session_config.get("start_time") start_time = time_session_config.get("start_time")
if start_time is None: if time_mode == "offset":
start_offset_minutes = time_session_config.get("start_offset_minutes") start_offset_minutes = time_session_config.get("start_offset_minutes")
if start_offset_minutes is not None: if start_offset_minutes is None:
start_time = (datetime.now() + timedelta(minutes=start_offset_minutes)).strftime("%H:%M") raise ValueError(f"{row_text} offset mode requires start_offset_minutes")
start_time = (datetime.now() + timedelta(minutes=start_offset_minutes)).strftime("%H:%M")
elif time_mode == "manual" and not start_time:
raise ValueError(f"{row_text} manual mode requires start_time")
return ( return (
start_time or default_start_time, start_time or default_start_time,
@@ -81,14 +85,37 @@ def get_time_session_times(token, row_text, default_start_time, default_end_time
) )
def get_upc_order_normal_start_time(timings, now=None):
"""Resolve UPC order-normal start time from manual or offset mode."""
mode = timings.get("time_mode", "manual").lower()
if mode == "offset":
offset = timings.get("order_normal_start_offset_minutes")
if offset is None:
raise ValueError(
"UPC time_mode 'offset' requires "
"order_normal_start_offset_minutes"
)
now = datetime.now() if now is None else now
return (now + timedelta(minutes=offset)).strftime("%H:%M")
if mode != "manual":
raise ValueError("UPC time_mode must be 'manual' or 'offset'")
start_time = timings.get("order_normal_start_time")
if not start_time:
raise ValueError(
"UPC time_mode 'manual' requires order_normal_start_time"
)
return start_time
def get_product_codes(token): def get_product_codes(token):
product_codes = parse_product_codes(token.get("product_codes")) product_codes = parse_product_codes(token.get("create_product_codes", token.get("product_codes")))
if product_codes: if product_codes:
return product_codes return product_codes
product_code = token.get("product_code") product_code = token.get("product_code")
assert product_code, "Add product_code or product_codes in config.json trading_token_idas." assert product_code, "Add product_code or product_codes in config.json products.<product>.token."
return parse_product_codes(product_code) return parse_product_codes(product_code)
@@ -143,11 +170,11 @@ def complete_time_session_edits(driver, step, token, next_day):
edit_time_session_row( edit_time_session_row(
driver, driver,
step, step,
"ORDER_NOR", "ORDER_NORMAL",
order_nor_start_time, order_nor_start_time,
order_nor_end_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, "ORDER_EXCHANGE", order_exch_start_time, order_exch_end_time)
edit_time_session_row(driver, step, "TRADE", trade_start_time, trade_end_time) edit_time_session_row(driver, step, "TRADE", trade_start_time, trade_end_time)
click_first_grid_button(driver, "Token Details") click_first_grid_button(driver, "Token Details")
@@ -160,6 +187,7 @@ def complete_time_session_edits(driver, step, token, next_day):
def complete_time_session_edits_from_saved_token(driver, step, token): def complete_time_session_edits_from_saved_token(driver, step, token):
step.click((By.XPATH, Locator.tokentimesessionbtn), "open_time_session") step.click((By.XPATH, Locator.tokentimesessionbtn), "open_time_session")
WebDriverWait(driver, 20).until(EC.url_contains("timeSessionView"))
click_first_grid_button(driver, "Time Session Details") click_first_grid_button(driver, "Time Session Details")
step.capture("open_time_session_details") step.capture("open_time_session_details")
step.capture("before_edit_order_normal") step.capture("before_edit_order_normal")
@@ -176,11 +204,11 @@ def complete_time_session_edits_from_saved_token(driver, step, token):
edit_time_session_row( edit_time_session_row(
driver, driver,
step, step,
"ORDER_NOR", "ORDER_NORMAL",
order_nor_start_time, order_nor_start_time,
order_nor_end_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, "ORDER_EXCHANGE", order_exch_start_time, order_exch_end_time)
edit_time_session_row(driver, step, "TRADE", trade_start_time, trade_end_time) edit_time_session_row(driver, step, "TRADE", trade_start_time, trade_end_time)
click_first_grid_button(driver, "Token Details") click_first_grid_button(driver, "Token Details")
@@ -190,6 +218,172 @@ def complete_time_session_edits_from_saved_token(driver, step, token):
pause_after_step() pause_after_step()
def open_rtm_new_token(driver, step, token):
"""Prepare the RTM generator through Add New Token."""
open_trading_maintenance_page(driver, Locator.tokengenerator)
handle_transient_popups(driver)
step.capture("open_token_generator_rtm")
for element_id, value, name in (
(Locator.generatormarketcode, token["market_code"], "market_code"),
(Locator.generatorproductcode, token["product_code"], "product_code"),
(Locator.generatorgenerationmode, token["generation_mode"], "generation_mode"),
):
select_bootstrap_dropdown_by_id(driver, element_id, value, exact=True)
step.capture(f"select_{name}_rtm")
# Product Type is populated by the application after selecting Product Code.
step.click((By.XPATH, Locator.addnewtokenbtn), "click_add_new_token_rtm")
def get_rtm_token_times(token, now=None):
now = datetime.now() if now is None else now
delivery_date = (now + timedelta(days=token.get("delivery_offset_days", 0))).strftime("%d/%m/%Y")
time_mode = token.get("time_mode", "offset").lower()
if time_mode == "manual":
session_start_time = token.get("session_start_time")
if not session_start_time:
raise ValueError("RTM time_mode 'manual' requires session_start_time")
elif time_mode == "offset":
session_start_time = (now + timedelta(minutes=token.get("session_start_offset_minutes", 5))).strftime("%H:%M")
else:
raise ValueError("RTM time_mode must be 'manual' or 'offset'")
return (
f"{delivery_date} {token.get('delivery_from_time', '00:00')}",
f"{delivery_date} {token.get('delivery_to_time', '23:59')}",
session_start_time,
token.get("session_end_time", "20:00"),
)
def create_rtm_token(driver, step, token):
open_rtm_new_token(driver, step, token)
values = get_rtm_token_times(token)
fields = (
(Locator.rtmdeliveryfromdate, "delivery_from_date"),
(Locator.rtmdeliverytodate, "delivery_to_date"),
(Locator.rtmsessionstarttime, "session_start_time"),
(Locator.rtmsessionendtime, "session_end_time"),
)
for (locator, name), value in zip(fields, values):
type_first_visible_input(driver, locator, value)
step.capture(f"enter_{name}_rtm")
step.click((By.XPATH, Locator.savetokenbtn), "save_token_rtm")
handle_transient_popups(driver)
def create_intraday_token(driver, step, token):
"""Create an Intraday token through the Token Generator page."""
open_trading_maintenance_page(driver, Locator.tokengenerator)
handle_transient_popups(driver)
step.capture("open_token_generator_intraday")
for element_id, value, name in (
(Locator.generatormarketcode, token["market_code"], "market_code"),
(Locator.generatorproductcode, token["product_code"], "product_code"),
(Locator.generatorgenerationmode, token["generation_mode"], "generation_mode"),
):
select_bootstrap_dropdown_by_id(driver, element_id, value, exact=True)
step.capture(f"select_{name}_intraday")
step.click((By.XPATH, Locator.addnewtokenbtn), "click_add_new_token_intraday")
delivery_start, delivery_end = get_intraday_delivery_window(token)
values = (
(Locator.intradaydeliveryfromdate, delivery_start, "delivery_from_date"),
(Locator.intradaydeliverytodate, delivery_end, "delivery_to_date"),
(Locator.intradaytradingstarttime, token.get("trading_start_time", "00:20"), "trading_start_time"),
(Locator.intradaydaysbeforedelivery, str(token.get("trading_start_days_before_delivery", 1)), "days_before_delivery"),
)
for locator, value, name in values:
type_first_visible_input(driver, locator, value)
step.capture(f"enter_{name}_intraday")
step.click((By.XPATH, Locator.savetokenbtn), "save_token_intraday")
WebDriverWait(driver, 30).until(
EC.visibility_of_element_located((By.XPATH, Locator.intradaysuccessmessage))
)
step.capture("intraday_token_generated_successfully")
handle_transient_popups(driver)
def create_dac_token(driver, step, token):
"""Create a DAC token through the Token Generator page."""
open_trading_maintenance_page(driver, Locator.tokengenerator)
handle_transient_popups(driver)
step.capture("open_token_generator_dac")
for element_id, value, name in (
(Locator.generatormarketcode, token["market_code"], "market_code"),
(Locator.generatorproductcode, token["product_code"], "product_code"),
(Locator.generatorgenerationmode, token["generation_mode"], "generation_mode"),
):
select_bootstrap_dropdown_by_id(driver, element_id, value, exact=True)
step.capture(f"select_{name}_dac")
step.click((By.XPATH, Locator.addnewtokenbtn), "click_add_new_token_dac")
trading_start, trading_end = get_intraday_delivery_window(token)
values = (
(Locator.dactradingfromdate, trading_start, "trading_from_date"),
(Locator.dactradingtodate, trading_end, "trading_to_date"),
(Locator.dacsessionstarttime, token.get("session_start_time", "13:00"), "session_start_time"),
(Locator.dacsessionendtime, token.get("session_end_time", "23:00"), "session_end_time"),
)
for locator, value, name in values:
type_first_visible_input(driver, locator, value)
step.capture(f"enter_{name}_dac")
step.click((By.XPATH, Locator.savetokenbtn), "save_token_dac")
WebDriverWait(driver, 30).until(
EC.visibility_of_element_located((By.XPATH, Locator.intradaysuccessmessage))
)
step.capture("dac_token_generated_successfully")
handle_transient_popups(driver)
def create_upc_token(driver, step, token):
open_trading_maintenance_page(driver, Locator.tokengenerator)
handle_transient_popups(driver)
step.capture("open_token_generator_upc")
fields = [(Locator.generatormarketcode, token["market_code"], "market_code"), (Locator.generatorproductcode, token["product_code"], "product_code")]
if driver.find_elements(By.ID, Locator.generatorgenerationmode):
fields.append((Locator.generatorgenerationmode, token["generation_mode"], "generation_mode"))
for element_id, value, name in fields:
select_bootstrap_dropdown_by_id(driver, element_id, value, exact=True)
step.capture(f"select_{name}_upc")
step.click((By.XPATH, Locator.addnewtokenbtn), "click_add_new_token_upc")
timings = token.get("upc_times", {})
for name, locator, value in (
("order_normal_start", Locator.upcorderstart, get_upc_order_normal_start_time(timings)),
("order_normal_end", Locator.upcorderend, timings.get("order_normal_end_time")),
("trade_start", Locator.upctradestart, timings.get("trade_session_start_time")),
("trade_end", Locator.upctradeend, timings.get("trade_session_end_time")),
):
if value:
type_first_visible_input(driver, locator, value)
step.capture(f"enter_{name}_upc")
step.click((By.XPATH, Locator.savetokenbtn), "save_token_upc")
handle_transient_popups(driver)
step.click((By.XPATH, Locator.tokengeneratorresetbtn), "reset_token_generator_upc")
def run_token_setup(driver, step, token):
if token.get("product_type", "").upper() == "DAC":
create_dac_token(driver, step, token)
return
if token.get("product_type", "").upper() == "INTRADAY":
create_intraday_token(driver, step, token)
return
if token.get("product_type", "").upper() == "UPC":
create_upc_token(driver, step, token)
return
if token.get("product_type", "").upper() == "RTM":
create_rtm_token(driver, step, token)
else:
create_and_activate_token(driver, step, token)
def create_and_activate_token(driver, step, token): def create_and_activate_token(driver, step, token):
product_code = token["product_code"] product_code = token["product_code"]
@@ -209,31 +403,85 @@ def create_and_activate_token(driver, step, token):
select_labeled_ui_dropdown(driver, "PRODUCT CODE", product_code) select_labeled_ui_dropdown(driver, "PRODUCT CODE", product_code)
step.capture(f"select_product_code_{product_code.lower()}") step.capture(f"select_product_code_{product_code.lower()}")
generation_mode = token.get("generation_mode")
if generation_mode:
mode_elements = driver.find_elements(By.ID, Locator.generatorgenerationmode)
if mode_elements:
select_bootstrap_dropdown_by_id(driver, Locator.generatorgenerationmode, generation_mode, exact=True)
step.capture(f"select_generation_mode_{product_code.lower()}")
step.click((By.XPATH, Locator.addnewtokenbtn), f"click_add_new_token_{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) is_intraday = token.get("product_type", "").upper() == "INTRADAY"
if is_intraday:
delivery_start, delivery_end = get_intraday_delivery_window(token)
else:
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()}") if token.get("product_type", "").upper() == "REC":
step.type((By.XPATH, Locator.tokendeliveryenddate), next_day, f"enter_delivery_end_date_{product_code.lower()}") token_type_prefix = token.get("type_prefix", "REC")
step.type((By.XPATH, Locator.tokenexpirydate), token_expiry, f"enter_token_expiry_date_{product_code.lower()}") token_type = f"{token_type_prefix}-{datetime.now().strftime('%d%m%Y')}"
step.type((By.XPATH, Locator.tokentype), token_type, f"enter_type_{product_code.lower()}")
if token.get("product_type", "").upper() == "UPC":
today = datetime.now().strftime("%d/%m/%Y")
if token.get("generation_mode") == "AnyDayUPC":
step.type((By.XPATH, Locator.tokentradedate), today, f"enter_trade_date_{product_code.lower()}")
timings = token.get("upc_times", {})
normal_start = get_upc_order_normal_start_time(timings)
values = {
Locator.upcordernormalstart: normal_start,
Locator.upcordernormalend: timings.get("order_normal_end_time"),
Locator.upctradesessionstart: timings.get("trade_session_start_time"),
Locator.upctradesessionend: timings.get("trade_session_end_time"),
}
for locator, value in values.items():
if value is None:
continue
type_first_visible_input(driver, locator, value)
if is_intraday:
step.type((By.XPATH, Locator.tokendeliverystartdate), delivery_start, f"enter_delivery_start_date_{product_code.lower()}")
step.type((By.XPATH, Locator.tokendeliveryenddate), delivery_end, f"enter_delivery_end_date_{product_code.lower()}")
elif (
token.get("generation_mode") not in ("WeeklyUPC", "MonthlyUPC")
and token.get("product_type", "").upper() != "REC"
):
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()}")
if not is_intraday and token.get("generation_mode") not in ("WeeklyUPC", "MonthlyUPC"):
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()}") step.click((By.XPATH, Locator.savetokenbtn), f"save_token_{product_code.lower()}")
handle_transient_popups(driver) handle_transient_popups(driver)
complete_time_session_edits_from_saved_token(driver, step, token) if token.get("product_type", "").upper() == "UPC":
step.click((By.XPATH, Locator.tokengeneratorresetbtn), f"reset_token_generator_{product_code.lower()}")
return
if not is_intraday:
complete_time_session_edits_from_saved_token(driver, step, token)
@pytest.mark.trading_token @pytest.mark.trading_token
def test_trading_token_creation_idas(driver, test_context): @pytest.mark.parametrize("config", get_setup_configs("token"), ids=lambda c: c["product_name"])
def test_trading_token_creation(driver, test_context, config, request):
config, step, token = get_token_context(driver, test_context) # The full setup command runs token-enabled products through the combined
# flow so session and token creation use one browser/login context.
invocation = " ".join(str(arg).replace("\\", "/") for arg in request.config.invocation_params.args)
if "tests/setup" in invocation and "tests/setup/tokens" not in invocation and "test_trading_token.py" not in invocation:
pytest.skip("Handled by the shared full setup flow")
config, step, token = get_token_context(driver, test_context, config)
open_app_and_login(driver, step, config) open_app_and_login(driver, step, config)
save_session_backup(driver, test_context, "login_session_backup") save_session_backup(driver, test_context, "login_session_backup")
for product_code in get_product_codes(token): for product_code in get_product_codes(token):
product_token = dict(token) product_token = dict(token)
product_token["product_code"] = product_code product_token["product_code"] = product_code
create_and_activate_token(driver, step, product_token) run_token_setup(driver, step, product_token)
logout(driver) logout(driver)
@@ -241,7 +489,7 @@ def test_trading_token_creation_idas(driver, test_context):
# @pytest.mark.trading_token # @pytest.mark.trading_token
# def test_trading_token_time_session_edit_idas(driver, test_context): # def test_trading_token_time_session_edit_idas(driver, test_context):
# #
# config, step, token = get_token_context(driver, test_context) # config, step, token = get_token_context(driver, test_context, config)
# open_app_and_login(driver, step, config) # open_app_and_login(driver, step, config)
# #
# next_day, _ = get_delivery_date_and_expiry(token) # next_day, _ = get_delivery_date_and_expiry(token)
+124
View File
@@ -0,0 +1,124 @@
import csv
import os
import time
from datetime import datetime
import pytest
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from tests.browser import create_chrome_driver
from tests.config_reader import load_config
from tests.helpers import login, logout
from tests.locators import Locator
LOGIN_ATTEMPTS = 50
MAX_CAPTCHA_RETRIES = 7
@pytest.mark.captcha
def test_captcha_login_stress():
"""Run bounded EasyOCR login attempts and retain auditable CAPTCHA evidence."""
config = load_config()
output_dir = os.path.abspath(os.path.join("reports", "captcha_login_stress"))
os.makedirs(output_dir, exist_ok=True)
started = datetime.now().strftime("%Y%m%d_%H%M%S")
csv_path = os.path.join(output_dir, f"captcha_login_{started}.csv")
rows = []
passed = 0
failed = 0
def record(row):
rows.append(row)
for login_number in range(1, LOGIN_ATTEMPTS + 1):
login_started = datetime.now().isoformat(timespec="seconds")
captcha_rows = []
driver = create_chrome_driver()
try:
driver.get(config["url"])
login_status = "FAILED"
logout_status = "NOT_ATTEMPTED"
error_text = ""
captcha_metrics = {"captcha_attempts": []}
try:
login(
driver,
config["login"]["username"],
config["login"]["password"],
max_captcha_retries=MAX_CAPTCHA_RETRIES,
captcha_metrics=captcha_metrics,
max_login_duration_seconds=1200,
)
login_status = "PASSED"
passed += 1
except Exception as exc:
failed += 1
error_text = f"{type(exc).__name__}: {exc}"
try:
driver.save_screenshot(os.path.join(output_dir, f"login_{login_number:03d}_failure.png"))
except Exception:
pass
finally:
# Explicitly log out after a successful authenticated flow,
# then close the browser session.
if login_status == "PASSED":
try:
logout_status = "PASSED" if logout(driver) else "FAILED"
except Exception as exc:
logout_status = "FAILED"
print(f"Logout error: {type(exc).__name__}: {exc}")
try:
driver.quit()
except Exception:
pass
captcha_rows = captcha_metrics.get("captcha_attempts", []) or [{
"attempt": 0, "captcha_text": "", "variant": "",
"status": "no_captcha_record", "screenshot": "", "error": error_text,
}]
for item in captcha_rows:
record({
"login_attempt": login_number,
"started_at": login_started,
"login_status": login_status,
"logout_status": logout_status,
"captcha_attempt": item["attempt"],
"attempted_captcha": item["variant"],
"ocr_captcha": item["captcha_text"],
"screenshot_path": item["screenshot"],
"captcha_status": item["status"],
"captcha_entered_count": sum(
1 for captcha_row in captcha_rows if captcha_row["variant"]
),
"error": item["error"] or error_text,
"attempts_to_result": len(captcha_rows),
"passed_count": passed,
"failed_count": failed,
})
finally:
# Clean up the driver after all attempts
try:
driver.quit()
except Exception:
pass
fieldnames = [
"login_attempt", "started_at", "login_status", "captcha_attempt",
"logout_status",
"attempted_captcha", "ocr_captcha", "screenshot_path", "captcha_status",
"captcha_entered_count", "error", "attempts_to_result", "passed_count",
"failed_count",
]
with open(csv_path, "w", newline="", encoding="utf-8") as report:
writer = csv.DictWriter(report, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
print(f"CAPTCHA stress report: {csv_path}")
print(f"Passed: {passed}; Failed: {failed}; Total: {LOGIN_ATTEMPTS}")
+3 -3
View File
@@ -5,7 +5,7 @@ import pytest
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from .config_reader import load_config from .config_reader import get_product_config
from .helpers import ( from .helpers import (
clear_pending_console_input, clear_pending_console_input,
click_page_body, click_page_body,
@@ -23,7 +23,7 @@ from .step_actions import StepActions
def _bounce_token_generation(driver, step, config): def _bounce_token_generation(driver, step, config):
token = config["trading_token_idas"] token = config["token"]
interactions = random.randint(2, 4) interactions = random.randint(2, 4)
open_trading_maintenance_page(driver, Locator.tokengeneration) open_trading_maintenance_page(driver, Locator.tokengeneration)
@@ -67,7 +67,7 @@ def _bounce_trading_session(driver, step, config):
@pytest.mark.fake_activity @pytest.mark.fake_activity
def test_random_fake_activity_no_save(driver, test_context): def test_random_fake_activity_no_save(driver, test_context):
config = load_config() config = get_product_config("IDAS")
wait = WebDriverWait(driver, 20) wait = WebDriverWait(driver, 20)
step = StepActions(driver, wait, test_context) step = StepActions(driver, wait, test_context)
+33
View File
@@ -0,0 +1,33 @@
from types import SimpleNamespace
import pytest
from tests.conftest import _get_run_prefix, _setup_item_prefix, get_run_id
@pytest.mark.parametrize("path,product,expected", [
("tests/setup/test_combined_setup.py", "IDAS", "IDAS_SESSION_TOKEN"),
("tests/setup/test_combined_setup.py", "RTM", "RTM_SESSION_TOKEN"),
("tests/setup/tokens/test_trading_token.py", "RTM", "RTM_TOKEN"),
("tests/setup/sessions/test_session.py", "RTM", "RTM_SESSION"),
])
def test_setup_artifacts_use_collected_product_and_flow(path, product, expected):
item = SimpleNamespace(nodeid=f"{path}::test[{product}]", callspec=SimpleNamespace(
params={"config": {"product_name": product}},
))
assert _setup_item_prefix(item) == expected
@pytest.mark.parametrize("path", ["tests/setup", "tests\\setup", "tests/setup/test_combined_setup.py"])
def test_setup_report_does_not_use_regression_config(path):
config = SimpleNamespace(invocation_params=SimpleNamespace(args=(path, "-k", "RTM")))
assert _get_run_prefix(config) == "PXIL_SETUP"
def test_mixed_run_ids_are_cached_separately(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
(tmp_path / "reports/screenshots/RTM_TOKEN_004").mkdir(parents=True)
config = SimpleNamespace()
assert get_run_id(config, "RTM_TOKEN") == "RTM_TOKEN_005"
assert get_run_id(config, "IDAS_SESSION_TOKEN") == "IDAS_SESSION_TOKEN_001"
assert get_run_id(config, "RTM_TOKEN") == "RTM_TOKEN_005"
+115
View File
@@ -0,0 +1,115 @@
from copy import deepcopy
import pytest
from tests.config_reader import get_product_config, get_setup_configs
@pytest.mark.parametrize("common_session", [False, True])
@pytest.mark.parametrize("common_token", [False, True])
@pytest.mark.parametrize("session_override", [None, False, True])
@pytest.mark.parametrize("token_override", [None, False, True])
def test_product_flags_inherit_or_override(common_session, common_token, session_override, token_override):
product = {"me_instance": "55", "token": {"market_code": "NATIONAL", "product_codes": "TEST"}}
if session_override is not None:
product["run_session"] = session_override
if token_override is not None:
product["run_token"] = token_override
config = {"setup": {"run_session": common_session, "run_token": common_token}, "products": {"RTM": product}}
session = common_session if session_override is None else session_override
token = common_token if token_override is None else token_override
expected = "combined" if session and token else "session" if session else "token" if token else None
for mode in ("combined", "session", "token"):
assert len(get_setup_configs(mode, config)) == int(mode == expected)
def test_mixed_products_defaults_and_no_mutation():
config = {
"setup": {"run_session": True, "run_token": False, "session": {"start_offset_minutes": 7, "end_time": "20:30"}},
"products": {
"IDAS": {"me_instance": "55", "session": {"end_time": "21:00"}},
"RTM": {"run_session": False, "run_token": True, "token": {"market_code": "NATIONAL", "product_codes": ["RTM1"]}},
"REC": {"enabled": False},
"RA": {"run_session": False},
},
}
original = deepcopy(config)
assert [c["product_name"] for c in get_setup_configs("session", config)] == ["IDAS"]
assert [c["product_name"] for c in get_setup_configs("token", config)] == ["RTM"]
assert get_setup_configs("combined", config) == []
resolved = get_product_config("IDAS", config)
assert resolved["setup"]["session"] == {"start_offset_minutes": 7, "end_time": "21:00"}
assert config == original
def test_missing_required_settings_fail_before_browser_start():
with pytest.raises(ValueError, match="me_instance"):
get_setup_configs("session", {"setup": {"run_token": False}, "products": {"REC": {}}})
with pytest.raises(ValueError, match="market_code"):
get_setup_configs("token", {"setup": {"run_session": False}, "products": {"REC": {}}})
def test_legacy_config_remains_supported():
config = {"product": {"me_instance": "55"}, "trading_token_idas": {"market_code": "NATIONAL", "product_code": "IDAS4"}}
assert get_setup_configs("combined", config)[0]["token"] == config["trading_token_idas"]
@pytest.mark.parametrize("selected", [[], ["DAILY"], ["WEEKLY"], ["MONTHLY"], ["DAILY", "WEEKLY", "MONTHLY"]])
def test_upc_session_selection_and_shared_timing(selected):
config = {
"setup": {"session": {"start_offset_minutes": 7, "end_time": "20:30"}},
"products": {"UPC": {
"run_session": True,
"run_token": False,
"session_variants": {
name: {"enabled": name in selected, "me_instance": instance}
for name, instance in (("DAILY", "58"), ("WEEKLY", "59"), ("MONTHLY", "60"))
},
}},
}
original = deepcopy(config)
sessions = get_setup_configs("session", config)
assert [c["product_name"] for c in sessions] == [f"UPC_{name}" for name in selected]
ids = {"DAILY": "58", "WEEKLY": "59", "MONTHLY": "60"}
assert [c["product"]["me_instance"] for c in sessions] == [ids[name] for name in selected]
assert all(c["setup"]["session"] == config["setup"]["session"] for c in sessions)
assert get_setup_configs("combined", config) == []
assert get_setup_configs("token", config) == []
assert config == original
config["products"]["UPC"]["enabled"] = False
assert get_setup_configs("session", config) == []
def test_upc_token_only_expands_all_enabled_variants():
config = {
"setup": {"run_session": True, "run_token": True},
"products": {
"UPC": {
"enabled": True,
"run_session": False,
"run_token": True,
"token": {
"market_code": "NATIONAL",
"product_type": "UPC",
"product_codes": [],
},
"session_variants": {
name: {
"enabled": True,
"run_session": False,
"run_token": True,
"token": {"create_product_codes": [f"{name}_TOKEN"]},
}
for name in ("DAILY", "WEEKLY", "MONTHLY")
},
}
},
}
configs = get_setup_configs("token", config)
assert [item["product_name"] for item in configs] == [
"UPC_DAILY",
"UPC_WEEKLY",
"UPC_MONTHLY",
]
+71
View File
@@ -0,0 +1,71 @@
from unittest.mock import Mock
from datetime import datetime
from tests.locators import Locator
from tests.setup.tokens import test_trading_token as actions
def test_rtm_saves_generator_token_without_idas_activation(monkeypatch):
events = []
monkeypatch.setattr(actions, "open_trading_maintenance_page", lambda driver, menu: events.append(menu))
monkeypatch.setattr(actions, "handle_transient_popups", lambda driver: None)
monkeypatch.setattr(
actions, "select_bootstrap_dropdown_by_id",
lambda driver, field, value, exact=False: events.append((field, value, exact)),
)
idas_flow = Mock()
monkeypatch.setattr(actions, "create_and_activate_token", idas_flow)
step = Mock()
step.click.side_effect = lambda locator, name: events.append(locator[1])
monkeypatch.setattr(actions, "get_rtm_token_times", lambda token: (
"23/09/2026 00:00", "23/09/2026 23:59", "10:15", "20:00",
))
monkeypatch.setattr(actions, "type_first_visible_input", lambda driver, locator, value: events.append((locator, value)))
actions.run_token_setup(Mock(), step, {
"product_type": "RTM", "market_code": "NATIONAL",
"product_code": "RTM", "generation_mode": "RTM",
})
assert events == [
Locator.tokengenerator,
("marketCode", "NATIONAL", True),
("productCode", "RTM", True),
("tokenMstr.genMode", "RTM", True),
Locator.addnewtokenbtn,
(Locator.rtmdeliveryfromdate, "23/09/2026 00:00"),
(Locator.rtmdeliverytodate, "23/09/2026 23:59"),
(Locator.rtmsessionstarttime, "10:15"),
(Locator.rtmsessionendtime, "20:00"),
Locator.savetokenbtn,
]
step.type.assert_not_called()
idas_flow.assert_not_called()
def test_idas_keeps_existing_creation_and_activation_flow(monkeypatch):
idas_flow = Mock()
rtm_flow = Mock()
monkeypatch.setattr(actions, "create_and_activate_token", idas_flow)
monkeypatch.setattr(actions, "open_rtm_new_token", rtm_flow)
driver, step = Mock(), Mock()
token = {"product_type": "IDAS", "product_code": "IDAS4"}
actions.run_token_setup(driver, step, token)
idas_flow.assert_called_once_with(driver, step, token)
rtm_flow.assert_not_called()
def test_rtm_token_timing_defaults_and_overrides():
now = datetime(2026, 9, 23, 10, 58)
assert actions.get_rtm_token_times({}, now) == (
"23/09/2026 00:00", "23/09/2026 23:59", "11:03", "20:00",
)
assert actions.get_rtm_token_times({
"delivery_offset_days": 1,
"delivery_from_time": "01:30", "delivery_to_time": "22:00",
"session_start_offset_minutes": 12, "session_end_time": "21:15",
}, now) == (
"24/09/2026 01:30", "24/09/2026 22:00", "11:10", "21:15",
)