Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37e0ddae3d |
@@ -1,395 +0,0 @@
|
|||||||
{
|
|
||||||
"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)."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,721 +0,0 @@
|
|||||||
{
|
|
||||||
"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": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"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
@@ -1,461 +0,0 @@
|
|||||||
{
|
|
||||||
"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)."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
-721
@@ -1,721 +0,0 @@
|
|||||||
{
|
|
||||||
"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": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-85
@@ -1,85 +0,0 @@
|
|||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-1041
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,430 +0,0 @@
|
|||||||
{
|
|
||||||
"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."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
-1345
File diff suppressed because it is too large
Load Diff
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-89
@@ -1,89 +0,0 @@
|
|||||||
{
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-1657
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+3
-6
@@ -1,6 +1,9 @@
|
|||||||
# Virtual environments
|
# Virtual environments
|
||||||
venv/
|
venv/
|
||||||
.venv/
|
.venv/
|
||||||
|
.venv-playwright/
|
||||||
|
reports/playwright/
|
||||||
|
test-results/
|
||||||
|
|
||||||
# Python cache
|
# Python cache
|
||||||
__pycache__/
|
__pycache__/
|
||||||
@@ -9,9 +12,6 @@ __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,6 +21,3 @@ reports/logs/*
|
|||||||
|
|
||||||
reports/html/*
|
reports/html/*
|
||||||
!reports/html/.gitkeep
|
!reports/html/.gitkeep
|
||||||
|
|
||||||
# CAPTCHA stress-test screenshots and CSV reports
|
|
||||||
reports/captcha_login_stress/
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# 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.
|
||||||
+6
-75
@@ -11,80 +11,6 @@ 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
|
||||||
@@ -143,7 +69,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 `products.<product>.token.product_codes`.
|
Use this when `run_token` is `true`. It will create one token or multiple tokens based on `trading_token_idas.product_codes`.
|
||||||
|
|
||||||
Run only session setup:
|
Run only session setup:
|
||||||
|
|
||||||
@@ -458,3 +384,8 @@ 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.
|
||||||
|
|||||||
@@ -1,195 +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"
|
|
||||||
},
|
|
||||||
"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": {
|
|
||||||
"start_offset_minutes": 5,
|
|
||||||
"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": {
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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": {
|
|
||||||
"order_normal_start_time": "13:00",
|
|
||||||
"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": {
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,25 +28,3 @@ 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`.
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[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
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
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 & 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
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# 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`.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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
|
||||||
|
]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
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")
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""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
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
"""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")
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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()
|
||||||
+1
-6
@@ -1,4 +1,5 @@
|
|||||||
[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
|
||||||
@@ -12,9 +13,3 @@ 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.*
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pytest==8.4.1
|
||||||
|
pytest-html==4.2.0
|
||||||
|
playwright==1.62.0
|
||||||
|
pytest-playwright==0.9.0
|
||||||
+2
-10
@@ -1,14 +1,6 @@
|
|||||||
# Core test runner
|
# Core test runner
|
||||||
pytest==9.1.1
|
pytest==8.4.1
|
||||||
pytest-html==4.2.0
|
pytest-html==4.2.0
|
||||||
|
|
||||||
# Browser automation
|
# Browser automation
|
||||||
selenium==4.50.0
|
selenium==4.34.2
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|||||||
+1
-10
@@ -1,7 +1,4 @@
|
|||||||
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():
|
||||||
@@ -13,11 +10,5 @@ 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"])
|
|
||||||
|
|
||||||
service = Service(log_output=os.devnull)
|
return webdriver.Chrome(options=options)
|
||||||
return webdriver.Chrome(service=service, options=options)
|
|
||||||
|
|||||||
@@ -1,353 +0,0 @@
|
|||||||
"""
|
|
||||||
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
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import json
|
import json
|
||||||
from copy import deepcopy
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
@@ -12,83 +11,6 @@ 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()
|
||||||
|
|||||||
+10
-57
@@ -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, prefix=None):
|
def get_run_id(config):
|
||||||
|
|
||||||
run_prefix = prefix or _get_run_prefix(config)
|
run_id = getattr(config, "_pxil_run_id", None)
|
||||||
run_ids = getattr(config, "_pxil_run_ids", {})
|
if run_id:
|
||||||
if run_prefix in run_ids:
|
return run_id
|
||||||
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,11 +28,10 @@ def get_run_id(config, prefix=None):
|
|||||||
for name in existing
|
for name in existing
|
||||||
if name.split("_")[-1].isdigit()
|
if name.split("_")[-1].isdigit()
|
||||||
]
|
]
|
||||||
next_number = max(numbers, default=0) + 1
|
next_number = max(numbers) + 1
|
||||||
|
|
||||||
run_id = f"{run_prefix}_{next_number:03d}"
|
run_id = f"{run_prefix}_{next_number:03d}"
|
||||||
run_ids[run_prefix] = run_id
|
config._pxil_run_id = run_id
|
||||||
config._pxil_run_ids = run_ids
|
|
||||||
return run_id
|
return run_id
|
||||||
|
|
||||||
|
|
||||||
@@ -47,34 +46,10 @@ 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(scope="module")
|
@pytest.fixture
|
||||||
def driver():
|
def driver():
|
||||||
|
|
||||||
driver = create_chrome_driver()
|
driver = create_chrome_driver()
|
||||||
@@ -91,7 +66,7 @@ def driver():
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_context(request):
|
def test_context(request):
|
||||||
|
|
||||||
test_id = get_run_id(request.config, _setup_item_prefix(request.node))
|
test_id = get_run_id(request.config)
|
||||||
test_name = request.node.name
|
test_name = request.node.name
|
||||||
|
|
||||||
folder = os.path.abspath(
|
folder = os.path.abspath(
|
||||||
@@ -226,28 +201,9 @@ 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).replace("\\", "/").lower()
|
invocation_args = " ".join(str(arg) for arg in config.invocation_params.args).lower()
|
||||||
|
|
||||||
if "tests/setup/sessions" in invocation_args or "test_session.py" in invocation_args:
|
if "tests/setup/sessions" in invocation_args or "test_session.py" in invocation_args:
|
||||||
return "PXIL_SESSION"
|
return "PXIL_SESSION"
|
||||||
@@ -255,9 +211,6 @@ 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:
|
||||||
|
|||||||
+40
-230
@@ -57,216 +57,43 @@ def enter_pressed():
|
|||||||
return pressed
|
return pressed
|
||||||
|
|
||||||
|
|
||||||
def check_captcha_error(driver) -> str:
|
def login(driver, username, password):
|
||||||
"""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)
|
||||||
|
|
||||||
# --- AUTOMATED CAPTCHA SOLVING WITH RETRY ON FAILURE ---
|
# username
|
||||||
from tests.captcha_solver import solve_and_fill_captcha, find_refresh_button, wait_for_new_captcha
|
userid_field = wait.until(
|
||||||
|
EC.visibility_of_element_located((By.XPATH, Locator.userid))
|
||||||
login_deadline = time.monotonic() + max_login_duration_seconds
|
)
|
||||||
captcha_attempt = 0
|
userid_field.send_keys(username)
|
||||||
while True:
|
|
||||||
captcha_attempt += 1
|
|
||||||
|
|
||||||
# Refreshing after a failed CAPTCHA clears the login form, so refill
|
# password
|
||||||
# both credentials before every automatic or manual retry.
|
password_field = driver.find_element(By.XPATH, Locator.password)
|
||||||
userid_field = wait.until(
|
password_field.send_keys(password)
|
||||||
EC.visibility_of_element_located((By.XPATH, Locator.userid))
|
|
||||||
)
|
|
||||||
userid_field.clear()
|
|
||||||
userid_field.send_keys(username)
|
|
||||||
|
|
||||||
password_field = wait.until(
|
print(f"Solve captcha within {LOGIN_CAPTCHA_WAIT_SECONDS} seconds...")
|
||||||
EC.visibility_of_element_located((By.XPATH, Locator.password))
|
|
||||||
)
|
|
||||||
password_field.clear()
|
|
||||||
password_field.send_keys(password)
|
|
||||||
|
|
||||||
if captcha_metrics is not None:
|
# wait for manual captcha solve
|
||||||
captcha_metrics["tries"] = captcha_attempt
|
time.sleep(LOGIN_CAPTCHA_WAIT_SECONDS)
|
||||||
captcha_metrics["max_retries"] = max_captcha_retries
|
|
||||||
print(f"Solving CAPTCHA (attempt {captcha_attempt})...")
|
|
||||||
|
|
||||||
# Once automatic solving has run for the configured duration, ask for
|
# click submit
|
||||||
# the CAPTCHA value in a browser prompt and continue the same login flow.
|
submit_btn = wait.until(
|
||||||
if time.monotonic() >= login_deadline:
|
EC.element_to_be_clickable((By.XPATH, Locator.submitbtn))
|
||||||
print("Automatic CAPTCHA retries timed out; waiting for manual CAPTCHA entry.")
|
)
|
||||||
manual_value = driver.execute_script(
|
submit_btn.click()
|
||||||
"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")
|
|
||||||
|
|
||||||
captcha_input = wait.until(
|
time.sleep(LOGIN_POST_SUBMIT_WAIT_SECONDS)
|
||||||
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)
|
|
||||||
|
|
||||||
captcha_error = check_captcha_error(driver)
|
accept_browser_alert_if_present(driver)
|
||||||
if captcha_error:
|
|
||||||
print(f" Manual CAPTCHA rejected: {captcha_error}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if check_login_success(driver):
|
# wait for dashboard
|
||||||
print("Login successful after manual CAPTCHA entry")
|
wait.until(
|
||||||
if captcha_metrics is not None:
|
EC.visibility_of_element_located((By.XPATH, Locator.dashboard_marker))
|
||||||
captcha_metrics["correct_attempt"] = captcha_attempt
|
)
|
||||||
captcha_metrics.setdefault("captcha_attempts", []).append({
|
|
||||||
"attempt": captcha_attempt,
|
close_tac_expiry_modal_if_present(driver)
|
||||||
"captcha_text": "manual",
|
|
||||||
"variant": manual_value.strip(),
|
print("Login successful")
|
||||||
"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):
|
||||||
@@ -482,34 +309,23 @@ def open_trading_maintenance_page(driver, menu_xpath):
|
|||||||
|
|
||||||
wait = WebDriverWait(driver, 20)
|
wait = WebDriverWait(driver, 20)
|
||||||
|
|
||||||
def visible_elements(xpath):
|
trading_maintenance = wait.until(
|
||||||
return [element for element in driver.find_elements(By.XPATH, xpath) if element.is_displayed()]
|
EC.presence_of_element_located((By.XPATH, Locator.tradingmaintenance))
|
||||||
|
|
||||||
submenu_visible = any(
|
|
||||||
item.is_enabled() for item in visible_elements(menu_xpath)
|
|
||||||
)
|
)
|
||||||
|
driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", trading_maintenance)
|
||||||
|
|
||||||
|
submenu = driver.find_elements(By.XPATH, menu_xpath)
|
||||||
|
submenu_visible = any(item.is_displayed() for item in submenu)
|
||||||
|
|
||||||
if not submenu_visible:
|
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(lambda current_driver: maintenance.is_enabled())
|
wait.until(EC.element_to_be_clickable((By.XPATH, Locator.tradingmaintenance)))
|
||||||
maintenance.click()
|
trading_maintenance.click()
|
||||||
except Exception:
|
except Exception:
|
||||||
driver.execute_script("arguments[0].click();", maintenance)
|
driver.execute_script("arguments[0].click();", trading_maintenance)
|
||||||
|
|
||||||
menu_item = wait.until(
|
menu_item = wait.until(
|
||||||
lambda current_driver: next(
|
EC.element_to_be_clickable((By.XPATH, menu_xpath))
|
||||||
(item for item in visible_elements(menu_xpath) if item.is_enabled()),
|
|
||||||
False,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
menu_item.click()
|
menu_item.click()
|
||||||
@@ -551,7 +367,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, exact=False):
|
def select_bootstrap_dropdown_by_id(driver, element_id, value):
|
||||||
|
|
||||||
wait = WebDriverWait(driver, 20)
|
wait = WebDriverWait(driver, 20)
|
||||||
|
|
||||||
@@ -595,18 +411,12 @@ def select_bootstrap_dropdown_by_id(driver, element_id, value, exact=False):
|
|||||||
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,
|
||||||
option_xpath
|
"//div[contains(@class,'ui-select-choices-row')]"
|
||||||
|
f"//*[normalize-space()='{value}' or starts-with(normalize-space(),'{value}')]"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-23
@@ -4,27 +4,16 @@ 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="//*[contains(normalize-space(), 'TRADING MAINTENANCE')]"
|
dashboard_marker="//span[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']"
|
||||||
# Click the Angular menu anchor, not the nested label span. The anchor
|
tradingmaintenance="//span[normalize-space()='TRADING MAINTENANCE']"
|
||||||
# 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']"
|
||||||
@@ -38,16 +27,6 @@ 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']"
|
|
||||||
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"
|
||||||
|
|||||||
@@ -5,11 +5,17 @@ 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, open_trading_maintenance_page
|
from tests.helpers import login, select_ui_dropdown
|
||||||
from tests.config_reader import get_setup_configs
|
from tests.config_reader import is_combined_setup_enabled, is_setup_session_enabled, load_config
|
||||||
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", {})
|
||||||
@@ -24,8 +30,9 @@ def get_session_window(config):
|
|||||||
|
|
||||||
def create_session(driver, step, config):
|
def create_session(driver, step, config):
|
||||||
|
|
||||||
open_trading_maintenance_page(driver, Locator.tradingsession)
|
step.click((By.XPATH, Locator.tradingmaintenance), "trading_maintenance")
|
||||||
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")
|
||||||
|
|
||||||
@@ -43,14 +50,9 @@ def create_session(driver, step, config):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.session
|
@pytest.mark.session
|
||||||
@pytest.mark.parametrize("config", get_setup_configs("session"), ids=lambda c: c["product_name"])
|
def test_session(driver, test_context):
|
||||||
def test_session(driver, test_context, config, request):
|
|
||||||
|
|
||||||
# The full setup command runs session-only products (such as RA) through
|
config = load_config()
|
||||||
# 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)
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,46 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
|
||||||
from tests.config_reader import get_setup_configs
|
from tests.config_reader import is_combined_setup_enabled, load_config
|
||||||
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 run_token_setup, get_product_codes
|
from tests.setup.tokens.test_trading_token import create_and_activate_token, 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
|
||||||
@pytest.mark.parametrize("config", get_setup_configs("combined"), ids=lambda c: c["product_name"])
|
def test_session_and_token_setup(driver, test_context):
|
||||||
def test_session_and_token_setup(driver, test_context, config):
|
|
||||||
|
|
||||||
token = config["token"]
|
config = load_config()
|
||||||
|
token = config.get("trading_token_idas")
|
||||||
|
assert token, "Add trading_token_idas in config.json before running token setup."
|
||||||
|
|
||||||
wait = WebDriverWait(driver, 20)
|
wait = WebDriverWait(driver, 20)
|
||||||
step = StepActions(driver, wait, test_context)
|
step = StepActions(driver, wait, test_context)
|
||||||
|
|
||||||
if not getattr(driver, "_pxil_logged_in", False):
|
driver.get(config["url"])
|
||||||
driver.get(config["url"])
|
step.capture("open_application")
|
||||||
step.capture("open_application")
|
|
||||||
|
|
||||||
if not getattr(driver, "_pxil_logged_in", False):
|
login(
|
||||||
login(driver, config["login"]["username"], config["login"]["password"])
|
driver,
|
||||||
driver._pxil_logged_in = True
|
config["login"]["username"],
|
||||||
step.capture("login")
|
config["login"]["password"]
|
||||||
save_session_backup(driver, test_context, "login_session_backup")
|
)
|
||||||
|
step.capture("login")
|
||||||
|
save_session_backup(driver, test_context, "login_session_backup")
|
||||||
|
|
||||||
if config.get("setup", {}).get("run_session", True):
|
create_session(driver, step, config)
|
||||||
create_session(driver, step, config)
|
|
||||||
|
|
||||||
if config.get("setup", {}).get("run_token", True):
|
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)
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ 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 get_setup_configs, parse_product_codes
|
from tests.config_reader import is_combined_setup_enabled, is_setup_token_enabled, load_config, 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,
|
||||||
@@ -24,10 +23,20 @@ from tests.locators import Locator
|
|||||||
from tests.step_actions import StepActions
|
from tests.step_actions import StepActions
|
||||||
|
|
||||||
|
|
||||||
def get_token_context(driver, test_context, config):
|
pytestmark = pytest.mark.skipif(
|
||||||
|
is_combined_setup_enabled() or not is_setup_token_enabled(),
|
||||||
|
reason="Standalone token setup is disabled or runs inside combined setup",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_token_context(driver, test_context):
|
||||||
|
|
||||||
|
config = load_config()
|
||||||
wait = WebDriverWait(driver, 20)
|
wait = WebDriverWait(driver, 20)
|
||||||
step = StepActions(driver, wait, test_context)
|
step = StepActions(driver, wait, test_context)
|
||||||
token = config["token"]
|
token = config.get("trading_token_idas")
|
||||||
|
|
||||||
|
assert token, "Add trading_token_idas in config.json before running this test."
|
||||||
|
|
||||||
return config, step, token
|
return config, step, token
|
||||||
|
|
||||||
@@ -74,12 +83,12 @@ def get_time_session_times(token, row_text, default_start_time, default_end_time
|
|||||||
|
|
||||||
def get_product_codes(token):
|
def get_product_codes(token):
|
||||||
|
|
||||||
product_codes = parse_product_codes(token.get("create_product_codes", token.get("product_codes")))
|
product_codes = parse_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 products.<product>.token."
|
assert product_code, "Add product_code or product_codes in config.json trading_token_idas."
|
||||||
return parse_product_codes(product_code)
|
return parse_product_codes(product_code)
|
||||||
|
|
||||||
|
|
||||||
@@ -134,11 +143,11 @@ def complete_time_session_edits(driver, step, token, next_day):
|
|||||||
edit_time_session_row(
|
edit_time_session_row(
|
||||||
driver,
|
driver,
|
||||||
step,
|
step,
|
||||||
"ORDER_NORMAL",
|
"ORDER_NOR",
|
||||||
order_nor_start_time,
|
order_nor_start_time,
|
||||||
order_nor_end_time
|
order_nor_end_time
|
||||||
)
|
)
|
||||||
edit_time_session_row(driver, step, "ORDER_EXCHANGE", order_exch_start_time, order_exch_end_time)
|
edit_time_session_row(driver, step, "ORDER_EXCH", order_exch_start_time, order_exch_end_time)
|
||||||
edit_time_session_row(driver, step, "TRADE", trade_start_time, trade_end_time)
|
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")
|
||||||
@@ -151,7 +160,6 @@ 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")
|
||||||
@@ -168,11 +176,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_NORMAL",
|
"ORDER_NOR",
|
||||||
order_nor_start_time,
|
order_nor_start_time,
|
||||||
order_nor_end_time
|
order_nor_end_time
|
||||||
)
|
)
|
||||||
edit_time_session_row(driver, step, "ORDER_EXCHANGE", order_exch_start_time, order_exch_end_time)
|
edit_time_session_row(driver, step, "ORDER_EXCH", order_exch_start_time, order_exch_end_time)
|
||||||
edit_time_session_row(driver, step, "TRADE", trade_start_time, trade_end_time)
|
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")
|
||||||
@@ -182,87 +190,6 @@ 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")
|
|
||||||
return (
|
|
||||||
f"{delivery_date} {token.get('delivery_from_time', '00:00')}",
|
|
||||||
f"{delivery_date} {token.get('delivery_to_time', '23:59')}",
|
|
||||||
(now + timedelta(minutes=token.get("session_start_offset_minutes", 5))).strftime("%H:%M"),
|
|
||||||
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_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, timings.get("order_normal_start_time")),
|
|
||||||
("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() == "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"]
|
||||||
@@ -282,79 +209,31 @@ 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)
|
next_day, token_expiry = get_delivery_date_and_expiry(token)
|
||||||
|
|
||||||
if token.get("product_type", "").upper() == "REC":
|
step.type((By.XPATH, Locator.tokendeliverystartdate), next_day, f"enter_delivery_start_date_{product_code.lower()}")
|
||||||
token_type_prefix = token.get("type_prefix", "REC")
|
step.type((By.XPATH, Locator.tokendeliveryenddate), next_day, f"enter_delivery_end_date_{product_code.lower()}")
|
||||||
token_type = f"{token_type_prefix}-{datetime.now().strftime('%d%m%Y')}"
|
step.type((By.XPATH, Locator.tokenexpirydate), token_expiry, f"enter_token_expiry_date_{product_code.lower()}")
|
||||||
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 = timings.get("order_normal_start_time")
|
|
||||||
if normal_start is None and timings.get("order_normal_start_offset_minutes") is not None:
|
|
||||||
normal_start = (datetime.now() + timedelta(minutes=timings["order_normal_start_offset_minutes"])).strftime("%H:%M")
|
|
||||||
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 (
|
|
||||||
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 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)
|
||||||
|
|
||||||
if token.get("product_type", "").upper() == "UPC":
|
|
||||||
step.click((By.XPATH, Locator.tokengeneratorresetbtn), f"reset_token_generator_{product_code.lower()}")
|
|
||||||
return
|
|
||||||
|
|
||||||
complete_time_session_edits_from_saved_token(driver, step, token)
|
complete_time_session_edits_from_saved_token(driver, step, token)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.trading_token
|
@pytest.mark.trading_token
|
||||||
@pytest.mark.parametrize("config", get_setup_configs("token"), ids=lambda c: c["product_name"])
|
def test_trading_token_creation_idas(driver, test_context):
|
||||||
def test_trading_token_creation(driver, test_context, config, request):
|
|
||||||
|
|
||||||
# The full setup command runs token-enabled products through the combined
|
config, step, token = get_token_context(driver, test_context)
|
||||||
# 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
|
||||||
run_token_setup(driver, step, product_token)
|
create_and_activate_token(driver, step, product_token)
|
||||||
|
|
||||||
logout(driver)
|
logout(driver)
|
||||||
|
|
||||||
@@ -362,7 +241,7 @@ def test_trading_token_creation(driver, test_context, config, request):
|
|||||||
# @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)
|
# config, step, token = get_token_context(driver, test_context)
|
||||||
# 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)
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
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}")
|
|
||||||
@@ -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 get_product_config
|
from .config_reader import load_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["token"]
|
token = config["trading_token_idas"]
|
||||||
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 = get_product_config("IDAS")
|
config = load_config()
|
||||||
wait = WebDriverWait(driver, 20)
|
wait = WebDriverWait(driver, 20)
|
||||||
step = StepActions(driver, wait, test_context)
|
step = StepActions(driver, wait, test_context)
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
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",
|
|
||||||
]
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
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",
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user