medicentrev3-auth 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- medicentrev3_auth-1.0.0/PKG-INFO +46 -0
- medicentrev3_auth-1.0.0/README.md +26 -0
- medicentrev3_auth-1.0.0/medicentrev3_auth/__init__.py +18 -0
- medicentrev3_auth-1.0.0/medicentrev3_auth/auth.py +890 -0
- medicentrev3_auth-1.0.0/medicentrev3_auth.egg-info/PKG-INFO +46 -0
- medicentrev3_auth-1.0.0/medicentrev3_auth.egg-info/SOURCES.txt +10 -0
- medicentrev3_auth-1.0.0/medicentrev3_auth.egg-info/dependency_links.txt +1 -0
- medicentrev3_auth-1.0.0/medicentrev3_auth.egg-info/requires.txt +9 -0
- medicentrev3_auth-1.0.0/medicentrev3_auth.egg-info/top_level.txt +1 -0
- medicentrev3_auth-1.0.0/pyproject.toml +34 -0
- medicentrev3_auth-1.0.0/setup.cfg +4 -0
- medicentrev3_auth-1.0.0/setup.py +5 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: medicentrev3-auth
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Authentication and navigation module for MedicentreV3 automation
|
|
5
|
+
Author-email: Denis Ndiritu <denis@hanmak.co.ke>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: selenium>=4.0.0
|
|
13
|
+
Requires-Dist: python-dotenv>=0.19.0
|
|
14
|
+
Requires-Dist: webdriver-manager>=3.8.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
18
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
19
|
+
Requires-Dist: flake8>=5.0.0; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# MedicentreV3 Authentication Package
|
|
22
|
+
|
|
23
|
+
A reusable Python package for MedicentreV3 authentication and navigation.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Install from GitHub
|
|
29
|
+
pip install git+https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package.git
|
|
30
|
+
|
|
31
|
+
# For development
|
|
32
|
+
pip install -e git+https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package.git
|
|
33
|
+
|
|
34
|
+
## Updating the Package
|
|
35
|
+
|
|
36
|
+
When updates are available, users should upgrade using:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Upgrade to latest version
|
|
40
|
+
pip install --upgrade git+https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package.git
|
|
41
|
+
|
|
42
|
+
# Or reinstall (force latest)
|
|
43
|
+
pip install --force-reinstall git+https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package.git
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Important**: Always use `--upgrade` or `--force-reinstall` when updating from GitHub to ensure you get the latest changes.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# MedicentreV3 Authentication Package
|
|
2
|
+
|
|
3
|
+
A reusable Python package for MedicentreV3 authentication and navigation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install from GitHub
|
|
9
|
+
pip install git+https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package.git
|
|
10
|
+
|
|
11
|
+
# For development
|
|
12
|
+
pip install -e git+https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package.git
|
|
13
|
+
|
|
14
|
+
## Updating the Package
|
|
15
|
+
|
|
16
|
+
When updates are available, users should upgrade using:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Upgrade to latest version
|
|
20
|
+
pip install --upgrade git+https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package.git
|
|
21
|
+
|
|
22
|
+
# Or reinstall (force latest)
|
|
23
|
+
pip install --force-reinstall git+https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package.git
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Important**: Always use `--upgrade` or `--force-reinstall` when updating from GitHub to ensure you get the latest changes.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MedicentreV3 Authentication Package
|
|
3
|
+
|
|
4
|
+
Provides reusable login, navigation, and session handling
|
|
5
|
+
for MedicentreV3 IHMIS automation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .auth import (
|
|
9
|
+
MedicentreAuth,
|
|
10
|
+
create_auth_session,
|
|
11
|
+
quick_login,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"MedicentreAuth",
|
|
16
|
+
"create_auth_session",
|
|
17
|
+
"quick_login",
|
|
18
|
+
]
|
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MedicentreV3 Authentication & Navigation Module
|
|
3
|
+
===============================================
|
|
4
|
+
|
|
5
|
+
A reusable module for MedicentreV3 login and navigation that can be imported
|
|
6
|
+
into multiple automation scripts.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
1. Handles MedicentreV3 login flow (facility code, branch selection, credentials)
|
|
10
|
+
2. Provides flexible navigation to modules and panels
|
|
11
|
+
3. Manages WebDriver setup and teardown
|
|
12
|
+
4. Includes error handling and screenshot capabilities
|
|
13
|
+
5. Supports environment variables and configuration
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
from medicentre_auth import MedicentreAuth
|
|
17
|
+
|
|
18
|
+
# Create auth instance
|
|
19
|
+
auth = MedicentreAuth(headless=False)
|
|
20
|
+
|
|
21
|
+
# Setup and login
|
|
22
|
+
if auth.setup_and_login():
|
|
23
|
+
# Navigate to a module panel
|
|
24
|
+
if auth.navigate_to('procurement', 'suppliers'):
|
|
25
|
+
# Your module-specific automation here
|
|
26
|
+
pass
|
|
27
|
+
# Always logout when done
|
|
28
|
+
auth.logout()
|
|
29
|
+
|
|
30
|
+
Configuration:
|
|
31
|
+
Set environment variables in .env file:
|
|
32
|
+
MEDICENTRE_BASE_URL=https://medicentrev3.hanmak.co.ke
|
|
33
|
+
MEDICENTRE_ACCESS_CODE=your_access_code
|
|
34
|
+
MEDICENTRE_BRANCH=your_branch_name
|
|
35
|
+
MEDICENTRE_USERNAME=your_username
|
|
36
|
+
MEDICENTRE_PASSWORD=your_password
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
import os
|
|
40
|
+
import sys
|
|
41
|
+
import time
|
|
42
|
+
import logging
|
|
43
|
+
from datetime import datetime
|
|
44
|
+
from typing import Dict, Optional, List, Tuple, Union
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
# Selenium imports
|
|
48
|
+
from selenium import webdriver
|
|
49
|
+
from selenium.webdriver.common.by import By
|
|
50
|
+
from selenium.webdriver.support.ui import WebDriverWait, Select
|
|
51
|
+
from selenium.webdriver.support import expected_conditions as EC
|
|
52
|
+
from selenium.common.exceptions import (
|
|
53
|
+
TimeoutException,
|
|
54
|
+
NoSuchElementException,
|
|
55
|
+
ElementNotInteractableException
|
|
56
|
+
)
|
|
57
|
+
from selenium.webdriver.chrome.service import Service
|
|
58
|
+
from selenium.webdriver.chrome.options import Options
|
|
59
|
+
from selenium.webdriver.common.action_chains import ActionChains
|
|
60
|
+
from selenium.webdriver.common.keys import Keys
|
|
61
|
+
from webdriver_manager.chrome import ChromeDriverManager
|
|
62
|
+
|
|
63
|
+
# Environment configuration
|
|
64
|
+
from dotenv import load_dotenv
|
|
65
|
+
|
|
66
|
+
# Load environment variables
|
|
67
|
+
load_dotenv()
|
|
68
|
+
|
|
69
|
+
# Configure module logger
|
|
70
|
+
logger = logging.getLogger(__name__)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class MedicentreAuth:
|
|
74
|
+
"""Reusable authentication and navigation module for MedicentreV3."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, headless: bool = False, base_dir: str = "."):
|
|
77
|
+
"""
|
|
78
|
+
Initialize the authentication module.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
headless: Run browser in headless mode (default: False)
|
|
82
|
+
base_dir: Base directory for logs and screenshots (default: current directory)
|
|
83
|
+
"""
|
|
84
|
+
self.driver = None
|
|
85
|
+
self.wait = None
|
|
86
|
+
self.headless = headless
|
|
87
|
+
self.base_dir = base_dir
|
|
88
|
+
|
|
89
|
+
# Configuration from environment
|
|
90
|
+
self.base_url = os.getenv('MEDICENTRE_BASE_URL', 'https://medicentrev3.hanmak.co.ke')
|
|
91
|
+
self.accesscode = os.getenv('MEDICENTRE_ACCESS_CODE', '')
|
|
92
|
+
self.branch = os.getenv('MEDICENTRE_BRANCH', '')
|
|
93
|
+
self.username = os.getenv('MEDICENTRE_USERNAME', '')
|
|
94
|
+
self.password = os.getenv('MEDICENTRE_PASSWORD', '')
|
|
95
|
+
|
|
96
|
+
# Create directories if they don't exist
|
|
97
|
+
self._setup_directories()
|
|
98
|
+
|
|
99
|
+
# Common page elements
|
|
100
|
+
self.elements = {
|
|
101
|
+
# Login page elements
|
|
102
|
+
'access_code': (By.ID, 'hospCode'),
|
|
103
|
+
'proceed_button': (By.ID, 'btnProceed'),
|
|
104
|
+
'branch_select': (By.ID, 'CompanyBranchID'),
|
|
105
|
+
'login_username': (By.ID, 'userName'),
|
|
106
|
+
'login_password': (By.ID, 'userPassword'),
|
|
107
|
+
'login_button': (By.ID, 'btnLogin'),
|
|
108
|
+
|
|
109
|
+
# Navigation elements
|
|
110
|
+
'billing_module': (By.XPATH, "//a[normalize-space()='Billing']"),
|
|
111
|
+
'clinical_module': (By.XPATH, "//a[normalize-space()='Clinical']"),
|
|
112
|
+
'cds_tools_module': (By.XPATH, "//a[normalize-space()='CDS Tools']"),
|
|
113
|
+
'maternity_module': (By.XPATH, "//a[normalize-space()='Maternity']"),
|
|
114
|
+
'dialysis_module': (By.XPATH, "//a[normalize-space()='Dialysis']"),
|
|
115
|
+
'procurement_module': (By.XPATH, "//a[normalize-space()='Procurement']"),
|
|
116
|
+
'inventory_module': (By.XPATH, "//a[@href='#'][normalize-space()='Inventory']"),
|
|
117
|
+
'accounts_module': (By.XPATH, "//a[normalize-space()='Accounts']"),
|
|
118
|
+
'human_resource_module': (By.XPATH, "//a[normalize-space()='Human Resource']"),
|
|
119
|
+
'security_module': (By.XPATH, "//a[normalize-space()='Security']"),
|
|
120
|
+
'configuration_module': (By.XPATH, "//a[normalize-space()='Configuration']"),
|
|
121
|
+
'morgue_module': (By.XPATH, "//a[normalize-space()='Morgue']"),
|
|
122
|
+
'communication_module': (By.XPATH, "//a[normalize-space()='Communication']"),
|
|
123
|
+
|
|
124
|
+
# Module PANELS
|
|
125
|
+
# Procurement panels
|
|
126
|
+
'procurement.suppliers': (By.XPATH, "//a[normalize-space()='Suppliers']"),
|
|
127
|
+
'procurement.supplier_bills': (By.XPATH, "//a[normalize-space()='Supplier Bills(A/P Invoices)']"),
|
|
128
|
+
'procurement.purchase_requisition_note': (By.XPATH, "//a[normalize-space()='Purchase Requisition Note']"),
|
|
129
|
+
'procurement.purchase_orders': (By.XPATH, "//a[normalize-space()='Purchase Orders']"),
|
|
130
|
+
'procurement.goods_received_notes': (By.XPATH, "//a[normalize-space()='Goods Received Notes']"),
|
|
131
|
+
'procurement.ap_payment_vouchers': (By.XPATH, "//a[normalize-space()='A/P Payment Vouchers']"),
|
|
132
|
+
'procurement.supplier_prepayments': (By.XPATH, "//a[normalize-space()='Supplier Prepayments']"),
|
|
133
|
+
|
|
134
|
+
# Inventory panels
|
|
135
|
+
'inventory.inventory': (By.XPATH, "//a[@href='/Inventory/Inventory']"),
|
|
136
|
+
'inventory.unit_of_measure': (By.XPATH, "//a[normalize-space()='Unit Of Measure']"),
|
|
137
|
+
|
|
138
|
+
# Accounts panels
|
|
139
|
+
'accounts.ledger_accounts': (By.XPATH, "//a[normalize-space()='Ledger Accounts']"),
|
|
140
|
+
'accounts.payment_modes': (By.XPATH, "//a[normalize-space()='Payment Modes']"),
|
|
141
|
+
'accounts.banks': (By.XPATH, "//a[contains(text(),'Banks')]"),
|
|
142
|
+
'accounts.schemes': (By.XPATH, "//a[contains(text(),'Schemes')]"),
|
|
143
|
+
'accounts.taxes': (By.XPATH, "//a[normalize-space()='Taxes']"),
|
|
144
|
+
|
|
145
|
+
# Human Resource panels
|
|
146
|
+
'human_resource.consultants': (By.XPATH, "//a[normalize-space()='Consultants']"),
|
|
147
|
+
'human_resource.employees': (By.XPATH, "//a[normalize-space()='Employees']"),
|
|
148
|
+
'human_resource.tax_ranges': (By.XPATH, "//a[normalize-space()='PAYE Tax Ranges']"),
|
|
149
|
+
|
|
150
|
+
# Security panels
|
|
151
|
+
'security.user_roles': (By.XPATH, "//a[normalize-space()='User Roles']"),
|
|
152
|
+
'security.privileges': (By.XPATH, "//a[normalize-space()='Privileges']"),
|
|
153
|
+
'security.system_users': (By.XPATH, "//a[normalize-space()='System Users']"),
|
|
154
|
+
|
|
155
|
+
# Configuration panels
|
|
156
|
+
'configuration.services': (By.XPATH, "//a[normalize-space()='Services']"),
|
|
157
|
+
'configuration.rooms': (By.XPATH, "//a[normalize-space()='Rooms']"),
|
|
158
|
+
'configuration.ward_beds': (By.XPATH, "//a[normalize-space()='Ward Beds']"),
|
|
159
|
+
'configuration.departments': (By.XPATH, "//a[normalize-space()='Departments']"),
|
|
160
|
+
'configuration.storage_locations': (By.XPATH, "//a[normalize-space()='Storage Location']"),
|
|
161
|
+
'configuration.medical_clinics': (By.XPATH, "//a[normalize-space()='Medical Clinics']"),
|
|
162
|
+
|
|
163
|
+
# Dashboard verification
|
|
164
|
+
'dashboard_element': (By.XPATH, "//h5[normalize-space()='My Appointments']"),
|
|
165
|
+
|
|
166
|
+
# User menu/logout
|
|
167
|
+
'user_menu': (By.XPATH, f"//a[@title='User: {self.username}']"),
|
|
168
|
+
'logout_button': (By.XPATH, "//a[normalize-space()='Logout']"),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Module aliases for easier access (optional but useful)
|
|
172
|
+
self.module_aliases = {
|
|
173
|
+
# Short names for modules
|
|
174
|
+
'proc': 'procurement',
|
|
175
|
+
'inv': 'inventory',
|
|
176
|
+
'acc': 'accounts',
|
|
177
|
+
'hr': 'human_resource',
|
|
178
|
+
'sec': 'security',
|
|
179
|
+
'config': 'configuration',
|
|
180
|
+
|
|
181
|
+
# Panel shortcuts
|
|
182
|
+
'supp': 'suppliers',
|
|
183
|
+
'bills': 'supplier_bills',
|
|
184
|
+
'prn': 'purchase_requisition_note',
|
|
185
|
+
'po': 'purchase_orders',
|
|
186
|
+
'grn': 'goods_received_notes',
|
|
187
|
+
'ap': 'ap_payment_vouchers',
|
|
188
|
+
'prepay': 'supplier_prepayments',
|
|
189
|
+
'uom': 'unit_of_measure',
|
|
190
|
+
'ledger': 'ledger_accounts',
|
|
191
|
+
'paymodes': 'payment_modes',
|
|
192
|
+
'tax': 'taxes',
|
|
193
|
+
'consult': 'consultants',
|
|
194
|
+
'emp': 'employees',
|
|
195
|
+
'taxrange': 'tax_ranges',
|
|
196
|
+
'roles': 'user_roles',
|
|
197
|
+
'priv': 'privileges',
|
|
198
|
+
'sysusers': 'system_users',
|
|
199
|
+
'serv': 'services',
|
|
200
|
+
'room': 'rooms',
|
|
201
|
+
'beds': 'ward_beds',
|
|
202
|
+
'dept': 'departments',
|
|
203
|
+
'storage': 'storage_locations',
|
|
204
|
+
'clinics': 'medical_clinics',
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# Validate environment on initialization
|
|
208
|
+
self._validate_environment()
|
|
209
|
+
|
|
210
|
+
def _setup_directories(self):
|
|
211
|
+
"""Create necessary directories for logs and screenshots."""
|
|
212
|
+
directories = [
|
|
213
|
+
os.path.join(self.base_dir, 'logs'),
|
|
214
|
+
os.path.join(self.base_dir, 'screenshots'),
|
|
215
|
+
os.path.join(self.base_dir, 'auth_logs')
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
for directory in directories:
|
|
219
|
+
if not os.path.exists(directory):
|
|
220
|
+
os.makedirs(directory)
|
|
221
|
+
logger.info(f"Created directory: {directory}")
|
|
222
|
+
|
|
223
|
+
def _validate_environment(self):
|
|
224
|
+
"""Validate that all required environment variables are set."""
|
|
225
|
+
missing_vars = []
|
|
226
|
+
|
|
227
|
+
if not self.accesscode:
|
|
228
|
+
missing_vars.append('MEDICENTRE_ACCESS_CODE')
|
|
229
|
+
if not self.branch:
|
|
230
|
+
missing_vars.append('MEDICENTRE_BRANCH')
|
|
231
|
+
if not self.username:
|
|
232
|
+
missing_vars.append('MEDICENTRE_USERNAME')
|
|
233
|
+
if not self.password:
|
|
234
|
+
missing_vars.append('MEDICENTRE_PASSWORD')
|
|
235
|
+
|
|
236
|
+
if missing_vars:
|
|
237
|
+
error_msg = f"Missing required environment variables: {', '.join(missing_vars)}"
|
|
238
|
+
logger.error(error_msg)
|
|
239
|
+
raise ValueError(error_msg)
|
|
240
|
+
|
|
241
|
+
logger.info("Environment validation passed")
|
|
242
|
+
|
|
243
|
+
def setup_driver(self) -> bool:
|
|
244
|
+
"""
|
|
245
|
+
Setup Chrome WebDriver with appropriate options.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
bool: True if setup successful, False otherwise
|
|
249
|
+
"""
|
|
250
|
+
logger.info("Setting up Chrome WebDriver")
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
chrome_options = Options()
|
|
254
|
+
|
|
255
|
+
if self.headless:
|
|
256
|
+
chrome_options.add_argument('--headless=new')
|
|
257
|
+
|
|
258
|
+
chrome_options.add_argument('--no-sandbox')
|
|
259
|
+
chrome_options.add_argument('--disable-dev-shm-usage')
|
|
260
|
+
chrome_options.add_argument('--disable-gpu')
|
|
261
|
+
chrome_options.add_argument('--disable-notifications')
|
|
262
|
+
chrome_options.add_argument('--disable-popup-blocking')
|
|
263
|
+
chrome_options.add_argument('--start-maximized')
|
|
264
|
+
|
|
265
|
+
prefs = {
|
|
266
|
+
"credentials_enable_service": False,
|
|
267
|
+
"profile.password_manager_enabled": False,
|
|
268
|
+
"profile.password_manager_leak_detection": False
|
|
269
|
+
}
|
|
270
|
+
chrome_options.add_experimental_option("prefs", prefs)
|
|
271
|
+
|
|
272
|
+
# Add preferences for better stability
|
|
273
|
+
chrome_options.add_experimental_option('excludeSwitches', ['enable-logging'])
|
|
274
|
+
chrome_options.add_experimental_option('prefs', {
|
|
275
|
+
'credentials_enable_service': False,
|
|
276
|
+
'profile.password_manager_enabled': False,
|
|
277
|
+
'profile.default_content_setting_values.notifications': 2
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
# Use webdriver-manager to handle ChromeDriver
|
|
281
|
+
service = Service(ChromeDriverManager().install())
|
|
282
|
+
self.driver = webdriver.Chrome(service=service, options=chrome_options)
|
|
283
|
+
|
|
284
|
+
# Set window size
|
|
285
|
+
self.driver.maximize_window()
|
|
286
|
+
logger.info("Browser window maximized to full screen")
|
|
287
|
+
|
|
288
|
+
self.wait = WebDriverWait(self.driver, 30)
|
|
289
|
+
|
|
290
|
+
logger.info("WebDriver setup completed successfully")
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
except Exception as e:
|
|
294
|
+
logger.error(f"Failed to setup WebDriver: {e}")
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
def save_screenshot(self, screenshot_name: str, subdirectory: str = "auth") -> str:
|
|
298
|
+
"""
|
|
299
|
+
Save screenshot to screenshots folder.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
screenshot_name: Name of the screenshot file
|
|
303
|
+
subdirectory: Subdirectory within screenshots (default: "auth")
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
str: Path to the saved screenshot
|
|
307
|
+
"""
|
|
308
|
+
# Create subdirectory path
|
|
309
|
+
screenshot_dir = os.path.join(self.base_dir, 'screenshots', subdirectory)
|
|
310
|
+
if not os.path.exists(screenshot_dir):
|
|
311
|
+
os.makedirs(screenshot_dir)
|
|
312
|
+
|
|
313
|
+
# Create timestamped filename
|
|
314
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
315
|
+
filename = os.path.join(screenshot_dir, f"{screenshot_name}_{timestamp}.png")
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
self.driver.save_screenshot(filename)
|
|
319
|
+
logger.info(f"Saved screenshot: {filename}")
|
|
320
|
+
return filename
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logger.error(f"Failed to save screenshot: {e}")
|
|
323
|
+
return ""
|
|
324
|
+
|
|
325
|
+
def select_branch_on_login(self) -> bool:
|
|
326
|
+
"""
|
|
327
|
+
Select branch on login page.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
bool: True if branch selected or not required, False if failed
|
|
331
|
+
"""
|
|
332
|
+
logger.info(f"Selecting branch: {self.branch}")
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
# Wait for branch select element
|
|
336
|
+
branch_element = self.wait.until(
|
|
337
|
+
EC.presence_of_element_located(self.elements['branch_select'])
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Check if it's a select element
|
|
341
|
+
if branch_element.tag_name.lower() == 'select':
|
|
342
|
+
select_obj = Select(branch_element)
|
|
343
|
+
|
|
344
|
+
# Get all options
|
|
345
|
+
options = select_obj.options
|
|
346
|
+
available_branches = [opt.text.strip() for opt in options if opt.text.strip()]
|
|
347
|
+
logger.info(f"Available branches: {', '.join(available_branches)}")
|
|
348
|
+
|
|
349
|
+
# Try to select the branch
|
|
350
|
+
try:
|
|
351
|
+
select_obj.select_by_visible_text(self.branch)
|
|
352
|
+
logger.info(f"Selected branch: {self.branch}")
|
|
353
|
+
|
|
354
|
+
# Verify selection
|
|
355
|
+
selected_option = select_obj.first_selected_option.text.strip()
|
|
356
|
+
if selected_option == self.branch:
|
|
357
|
+
logger.info(f"Branch selection verified: {selected_option}")
|
|
358
|
+
return True
|
|
359
|
+
else:
|
|
360
|
+
logger.warning(f"Selected '{selected_option}' instead of '{self.branch}'")
|
|
361
|
+
return True # Still return True as something was selected
|
|
362
|
+
|
|
363
|
+
except Exception as select_error:
|
|
364
|
+
logger.warning(f"Exact branch match failed: {select_error}")
|
|
365
|
+
|
|
366
|
+
# Try case-insensitive match
|
|
367
|
+
for option in options:
|
|
368
|
+
if option.text and self.branch.lower() in option.text.lower():
|
|
369
|
+
select_obj.select_by_visible_text(option.text)
|
|
370
|
+
logger.info(f"Selected branch (partial match): {option.text}")
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
# Select first available branch as fallback
|
|
374
|
+
for option in options:
|
|
375
|
+
if option.text.strip():
|
|
376
|
+
select_obj.select_by_visible_text(option.text)
|
|
377
|
+
logger.warning(f"Selected first available branch: {option.text}")
|
|
378
|
+
return True
|
|
379
|
+
|
|
380
|
+
logger.error("No selectable branch options found")
|
|
381
|
+
return False
|
|
382
|
+
else:
|
|
383
|
+
logger.warning("Branch element is not a select dropdown")
|
|
384
|
+
# Try to click or send keys
|
|
385
|
+
branch_element.click()
|
|
386
|
+
branch_element.send_keys(self.branch)
|
|
387
|
+
logger.info(f"Set branch to: {self.branch}")
|
|
388
|
+
return True
|
|
389
|
+
|
|
390
|
+
except TimeoutException:
|
|
391
|
+
logger.warning("Branch select element not found - system may not require branch selection")
|
|
392
|
+
return True # Assume success if branch not required
|
|
393
|
+
|
|
394
|
+
except Exception as e:
|
|
395
|
+
logger.error(f"Error during branch selection: {e}")
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
def verify_login_success(self) -> bool:
|
|
399
|
+
"""
|
|
400
|
+
Verify if login was successful.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
bool: True if login successful, False otherwise
|
|
404
|
+
"""
|
|
405
|
+
logger.info("Verifying login success")
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
# Wait for dashboard or user menu
|
|
409
|
+
time.sleep(3)
|
|
410
|
+
|
|
411
|
+
# Check multiple indicators of successful login
|
|
412
|
+
success_indicators = [
|
|
413
|
+
(self.elements['dashboard_element'], "Dashboard found"),
|
|
414
|
+
(By.XPATH, "//h5[normalize-space()='My Appointments']"),
|
|
415
|
+
(By.XPATH, "//h5[normalize-space()='Todo List']"),
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
for element_locator, message in success_indicators:
|
|
419
|
+
try:
|
|
420
|
+
element = self.driver.find_element(*element_locator)
|
|
421
|
+
if element.is_displayed():
|
|
422
|
+
logger.info(f"Login successful: {message}")
|
|
423
|
+
return True
|
|
424
|
+
except:
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
# Check URL for dashboard or main page
|
|
428
|
+
current_url = self.driver.current_url.lower()
|
|
429
|
+
if 'https://medicentrev3.hanmak.co.ke' in current_url:
|
|
430
|
+
logger.info("Login successful - dashboard URL detected")
|
|
431
|
+
return True
|
|
432
|
+
|
|
433
|
+
# Check for absence of login form
|
|
434
|
+
try:
|
|
435
|
+
self.driver.find_element(*self.elements['login_username'])
|
|
436
|
+
logger.error("Login failed - still on login page")
|
|
437
|
+
return False
|
|
438
|
+
except:
|
|
439
|
+
logger.info("Login successful - login form no longer present")
|
|
440
|
+
return True
|
|
441
|
+
|
|
442
|
+
except Exception as e:
|
|
443
|
+
logger.error(f"Error verifying login: {e}")
|
|
444
|
+
return False
|
|
445
|
+
|
|
446
|
+
def login(self) -> bool:
|
|
447
|
+
"""
|
|
448
|
+
Login to MedicentreV3 system.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
bool: True if login successful, False otherwise
|
|
452
|
+
"""
|
|
453
|
+
logger.info(f"Attempting to login to {self.base_url}")
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
# Navigate to login page
|
|
457
|
+
self.driver.get(self.base_url)
|
|
458
|
+
logger.info(f"Navigated to {self.base_url}")
|
|
459
|
+
time.sleep(2)
|
|
460
|
+
|
|
461
|
+
# Take screenshot of login page
|
|
462
|
+
self.save_screenshot("login_page")
|
|
463
|
+
|
|
464
|
+
# Step 1: Enter facility code
|
|
465
|
+
accesscode_field = self.wait.until(
|
|
466
|
+
EC.presence_of_element_located(self.elements['access_code'])
|
|
467
|
+
)
|
|
468
|
+
accesscode_field.clear()
|
|
469
|
+
accesscode_field.send_keys(self.accesscode)
|
|
470
|
+
logger.info("Entered facility access code")
|
|
471
|
+
|
|
472
|
+
# Step 2: Click proceed button
|
|
473
|
+
proceed_button = self.driver.find_element(*self.elements['proceed_button'])
|
|
474
|
+
proceed_button.click()
|
|
475
|
+
logger.info("Clicked proceed button")
|
|
476
|
+
time.sleep(2)
|
|
477
|
+
|
|
478
|
+
# Step 3: Select branch (if required)
|
|
479
|
+
branch_selected = self.select_branch_on_login()
|
|
480
|
+
if not branch_selected:
|
|
481
|
+
logger.warning("Branch selection may have failed, continuing...")
|
|
482
|
+
|
|
483
|
+
# Step 4: Enter username
|
|
484
|
+
username_field = self.wait.until(
|
|
485
|
+
EC.presence_of_element_located(self.elements['login_username'])
|
|
486
|
+
)
|
|
487
|
+
username_field.clear()
|
|
488
|
+
username_field.send_keys(self.username)
|
|
489
|
+
logger.info("Entered username")
|
|
490
|
+
|
|
491
|
+
# Step 5: Enter password
|
|
492
|
+
password_field = self.driver.find_element(*self.elements['login_password'])
|
|
493
|
+
password_field.clear()
|
|
494
|
+
password_field.send_keys(self.password)
|
|
495
|
+
logger.info("Entered password")
|
|
496
|
+
|
|
497
|
+
# Step 6: Click login button
|
|
498
|
+
login_button = self.driver.find_element(*self.elements['login_button'])
|
|
499
|
+
login_button.click()
|
|
500
|
+
logger.info("Clicked login button")
|
|
501
|
+
|
|
502
|
+
# Step 7: Verify login success
|
|
503
|
+
login_success = self.verify_login_success()
|
|
504
|
+
|
|
505
|
+
if login_success:
|
|
506
|
+
self.save_screenshot("after_login")
|
|
507
|
+
|
|
508
|
+
return login_success
|
|
509
|
+
|
|
510
|
+
except Exception as e:
|
|
511
|
+
logger.error(f"Login failed with error: {e}")
|
|
512
|
+
self.save_screenshot("login_error")
|
|
513
|
+
return False
|
|
514
|
+
|
|
515
|
+
def logout(self) -> bool:
|
|
516
|
+
"""
|
|
517
|
+
Logout from MedicentreV3 system.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
bool: True if logout successful, False otherwise
|
|
521
|
+
"""
|
|
522
|
+
logger.info("Attempting to logout")
|
|
523
|
+
|
|
524
|
+
try:
|
|
525
|
+
# Try to find and click user menu
|
|
526
|
+
try:
|
|
527
|
+
user_menu = self.driver.find_element(*self.elements['user_menu'])
|
|
528
|
+
user_menu.click()
|
|
529
|
+
time.sleep(1)
|
|
530
|
+
except:
|
|
531
|
+
logger.warning("User menu not found, trying direct logout")
|
|
532
|
+
|
|
533
|
+
# Try to find and click logout button
|
|
534
|
+
try:
|
|
535
|
+
logout_button = self.driver.find_element(*self.elements['logout_button'])
|
|
536
|
+
logout_button.click()
|
|
537
|
+
logger.info("Clicked logout button")
|
|
538
|
+
time.sleep(2)
|
|
539
|
+
return True
|
|
540
|
+
except:
|
|
541
|
+
# If logout button not found, navigate to logout URL directly
|
|
542
|
+
logout_url = f"{self.base_url}/Account/LogOut"
|
|
543
|
+
self.driver.get(logout_url)
|
|
544
|
+
logger.info(f"Navigated to logout URL: {logout_url}")
|
|
545
|
+
time.sleep(2)
|
|
546
|
+
return True
|
|
547
|
+
|
|
548
|
+
except Exception as e:
|
|
549
|
+
logger.error(f"Logout failed with error: {e}")
|
|
550
|
+
return False
|
|
551
|
+
|
|
552
|
+
def navigate_to(self, module: str, panel: str = None) -> bool:
|
|
553
|
+
"""
|
|
554
|
+
Navigate to a module or module panel.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
module: Main module name (e.g., 'procurement', 'accounts', 'configuration')
|
|
558
|
+
panel: Optional panel name (e.g., 'suppliers', 'ledger_accounts', 'rooms')
|
|
559
|
+
If None, just clicks the main module
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
bool: True if navigation successful, False otherwise
|
|
563
|
+
"""
|
|
564
|
+
# Resolve aliases
|
|
565
|
+
module = self.module_aliases.get(module, module)
|
|
566
|
+
if panel:
|
|
567
|
+
panel = self.module_aliases.get(panel, panel)
|
|
568
|
+
|
|
569
|
+
logger.info(f"Navigating to {' -> '.join(filter(None, [module, panel]))}")
|
|
570
|
+
|
|
571
|
+
try:
|
|
572
|
+
# Step 1: Click the main module menu
|
|
573
|
+
menu_key = f"{module}_menu"
|
|
574
|
+
if menu_key not in self.elements:
|
|
575
|
+
# Try alternative naming
|
|
576
|
+
menu_key = f"{module}_module"
|
|
577
|
+
if menu_key not in self.elements:
|
|
578
|
+
logger.error(f"Module '{module}' not found. Available modules: {[k for k in self.elements.keys() if k.endswith('_menu')]}")
|
|
579
|
+
return False
|
|
580
|
+
|
|
581
|
+
menu_element = self.wait.until(
|
|
582
|
+
EC.element_to_be_clickable(self.elements[menu_key])
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# Click the menu
|
|
586
|
+
menu_element.click()
|
|
587
|
+
logger.info(f"Clicked {module} menu")
|
|
588
|
+
time.sleep(1) # Wait for dropdown to appear if applicable
|
|
589
|
+
|
|
590
|
+
# Step 2: If panel is specified, click it
|
|
591
|
+
if panel:
|
|
592
|
+
# First check if panel exists in elements with module.panel format
|
|
593
|
+
panel_key = f"{module}.{panel}"
|
|
594
|
+
if panel_key in self.elements:
|
|
595
|
+
panel_element = self.wait.until(
|
|
596
|
+
EC.element_to_be_clickable(self.elements[panel_key])
|
|
597
|
+
)
|
|
598
|
+
panel_element.click()
|
|
599
|
+
logger.info(f"Clicked {panel} panel")
|
|
600
|
+
else:
|
|
601
|
+
# Try alternative panel naming
|
|
602
|
+
panel_key = f"{panel}_panel"
|
|
603
|
+
if panel_key in self.elements:
|
|
604
|
+
panel_element = self.wait.until(
|
|
605
|
+
EC.element_to_be_clickable(self.elements[panel_key])
|
|
606
|
+
)
|
|
607
|
+
panel_element.click()
|
|
608
|
+
logger.info(f"Clicked {panel} panel")
|
|
609
|
+
else:
|
|
610
|
+
logger.error(f"Panel '{panel}' not found for module '{module}'")
|
|
611
|
+
return False
|
|
612
|
+
|
|
613
|
+
# Step 3: Wait for page to load
|
|
614
|
+
time.sleep(2)
|
|
615
|
+
|
|
616
|
+
# Step 4: Take screenshot
|
|
617
|
+
screenshot_name = f"{module}_{panel}" if panel else f"{module}"
|
|
618
|
+
self.save_screenshot(screenshot_name, "navigation")
|
|
619
|
+
|
|
620
|
+
# Step 5: Verify navigation (optional but recommended)
|
|
621
|
+
if self._verify_navigation(module, panel):
|
|
622
|
+
logger.info(f"Successfully navigated to {' -> '.join(filter(None, [module, panel]))}")
|
|
623
|
+
return True
|
|
624
|
+
else:
|
|
625
|
+
logger.warning(f"Navigation may not have completed as expected")
|
|
626
|
+
return True # Still return True as we likely reached some page
|
|
627
|
+
|
|
628
|
+
except Exception as e:
|
|
629
|
+
logger.error(f"Failed to navigate to {' -> '.join(filter(None, [module, panel]))}: {e}")
|
|
630
|
+
self.save_screenshot(f"nav_error_{module}_{panel if panel else ''}", "errors")
|
|
631
|
+
return False
|
|
632
|
+
|
|
633
|
+
def _verify_navigation(self, module: str, panel: str = None) -> bool:
|
|
634
|
+
"""
|
|
635
|
+
Verify that navigation was successful.
|
|
636
|
+
|
|
637
|
+
Args:
|
|
638
|
+
module: Module name
|
|
639
|
+
panel: Panel name (optional)
|
|
640
|
+
|
|
641
|
+
Returns:
|
|
642
|
+
bool: True if navigation appears successful
|
|
643
|
+
"""
|
|
644
|
+
try:
|
|
645
|
+
# Check URL for module/panel indicators
|
|
646
|
+
current_url = self.driver.current_url.lower()
|
|
647
|
+
|
|
648
|
+
# Check page title or headers
|
|
649
|
+
try:
|
|
650
|
+
page_title = self.driver.title
|
|
651
|
+
if page_title and page_title.strip():
|
|
652
|
+
logger.debug(f"Page title: {page_title}")
|
|
653
|
+
except:
|
|
654
|
+
pass
|
|
655
|
+
|
|
656
|
+
# Check for common page elements
|
|
657
|
+
try:
|
|
658
|
+
# Look for h1, h2, or h3 headers
|
|
659
|
+
headers = self.driver.find_elements(By.TAG_NAME, 'h1') + \
|
|
660
|
+
self.driver.find_elements(By.TAG_NAME, 'h2') + \
|
|
661
|
+
self.driver.find_elements(By.TAG_NAME, 'h3')
|
|
662
|
+
|
|
663
|
+
for header in headers[:3]: # Check first 3 headers
|
|
664
|
+
if header.text.strip():
|
|
665
|
+
logger.debug(f"Found header: {header.text.strip()}")
|
|
666
|
+
break
|
|
667
|
+
except:
|
|
668
|
+
pass
|
|
669
|
+
|
|
670
|
+
return True
|
|
671
|
+
|
|
672
|
+
except Exception as e:
|
|
673
|
+
logger.warning(f"Navigation verification failed: {e}")
|
|
674
|
+
return False
|
|
675
|
+
|
|
676
|
+
def navigate_to_url(self, url: str) -> bool:
|
|
677
|
+
"""
|
|
678
|
+
Navigate to a specific URL.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
url: URL to navigate to
|
|
682
|
+
|
|
683
|
+
Returns:
|
|
684
|
+
bool: True if navigation successful, False otherwise
|
|
685
|
+
"""
|
|
686
|
+
logger.info(f"Navigating to URL: {url}")
|
|
687
|
+
|
|
688
|
+
try:
|
|
689
|
+
self.driver.get(url)
|
|
690
|
+
time.sleep(2)
|
|
691
|
+
logger.info(f"Successfully navigated to {url}")
|
|
692
|
+
return True
|
|
693
|
+
except Exception as e:
|
|
694
|
+
logger.error(f"Failed to navigate to {url}: {e}")
|
|
695
|
+
return False
|
|
696
|
+
|
|
697
|
+
def get_current_url(self) -> str:
|
|
698
|
+
"""
|
|
699
|
+
Get the current URL.
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
str: Current URL
|
|
703
|
+
"""
|
|
704
|
+
return self.driver.current_url if self.driver else ""
|
|
705
|
+
|
|
706
|
+
def get_page_title(self) -> str:
|
|
707
|
+
"""
|
|
708
|
+
Get the current page title.
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
str: Page title
|
|
712
|
+
"""
|
|
713
|
+
return self.driver.title if self.driver else ""
|
|
714
|
+
|
|
715
|
+
def is_element_present(self, locator: Tuple[str, str], timeout: int = 5) -> bool:
|
|
716
|
+
"""
|
|
717
|
+
Check if an element is present on the page.
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
locator: Element locator tuple (By, value)
|
|
721
|
+
timeout: Time to wait for element (default: 5 seconds)
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
bool: True if element is present, False otherwise
|
|
725
|
+
"""
|
|
726
|
+
try:
|
|
727
|
+
WebDriverWait(self.driver, timeout).until(
|
|
728
|
+
EC.presence_of_element_located(locator)
|
|
729
|
+
)
|
|
730
|
+
return True
|
|
731
|
+
except:
|
|
732
|
+
return False
|
|
733
|
+
|
|
734
|
+
def wait_for_element(self, locator: Tuple[str, str], timeout: int = 30) -> bool:
|
|
735
|
+
"""
|
|
736
|
+
Wait for an element to be present and visible.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
locator: Element locator tuple (By, value)
|
|
740
|
+
timeout: Time to wait for element (default: 30 seconds)
|
|
741
|
+
|
|
742
|
+
Returns:
|
|
743
|
+
bool: True if element found and visible, False otherwise
|
|
744
|
+
"""
|
|
745
|
+
try:
|
|
746
|
+
element = WebDriverWait(self.driver, timeout).until(
|
|
747
|
+
EC.visibility_of_element_located(locator)
|
|
748
|
+
)
|
|
749
|
+
return element is not None
|
|
750
|
+
except Exception as e:
|
|
751
|
+
logger.warning(f"Element not found within {timeout} seconds: {e}")
|
|
752
|
+
return False
|
|
753
|
+
|
|
754
|
+
def click_element(self, locator: Tuple[str, str]) -> bool:
|
|
755
|
+
"""
|
|
756
|
+
Click on an element.
|
|
757
|
+
|
|
758
|
+
Args:
|
|
759
|
+
locator: Element locator tuple (By, value)
|
|
760
|
+
|
|
761
|
+
Returns:
|
|
762
|
+
bool: True if click successful, False otherwise
|
|
763
|
+
"""
|
|
764
|
+
try:
|
|
765
|
+
element = self.wait.until(
|
|
766
|
+
EC.element_to_be_clickable(locator)
|
|
767
|
+
)
|
|
768
|
+
element.click()
|
|
769
|
+
return True
|
|
770
|
+
except Exception as e:
|
|
771
|
+
logger.error(f"Failed to click element: {e}")
|
|
772
|
+
return False
|
|
773
|
+
|
|
774
|
+
def find_element(self, locator: Tuple[str, str], timeout: int = 10):
|
|
775
|
+
"""
|
|
776
|
+
Find an element on the page.
|
|
777
|
+
|
|
778
|
+
Args:
|
|
779
|
+
locator: Element locator tuple (By, value)
|
|
780
|
+
timeout: Time to wait for element (default: 10 seconds)
|
|
781
|
+
|
|
782
|
+
Returns:
|
|
783
|
+
WebElement if found, None otherwise
|
|
784
|
+
"""
|
|
785
|
+
try:
|
|
786
|
+
return WebDriverWait(self.driver, timeout).until(
|
|
787
|
+
EC.presence_of_element_located(locator)
|
|
788
|
+
)
|
|
789
|
+
except:
|
|
790
|
+
return None
|
|
791
|
+
|
|
792
|
+
def find_elements(self, locator: Tuple[str, str], timeout: int = 10):
|
|
793
|
+
"""
|
|
794
|
+
Find multiple elements on the page.
|
|
795
|
+
|
|
796
|
+
Args:
|
|
797
|
+
locator: Element locator tuple (By, value)
|
|
798
|
+
timeout: Time to wait for elements (default: 10 seconds)
|
|
799
|
+
|
|
800
|
+
Returns:
|
|
801
|
+
List of WebElements if found, empty list otherwise
|
|
802
|
+
"""
|
|
803
|
+
try:
|
|
804
|
+
WebDriverWait(self.driver, timeout).until(
|
|
805
|
+
EC.presence_of_element_located(locator)
|
|
806
|
+
)
|
|
807
|
+
return self.driver.find_elements(*locator)
|
|
808
|
+
except:
|
|
809
|
+
return []
|
|
810
|
+
|
|
811
|
+
def add_element(self, name: str, locator: Tuple[str, str]):
|
|
812
|
+
"""
|
|
813
|
+
Add a new element to the elements dictionary.
|
|
814
|
+
Useful for adding custom elements for specific scripts.
|
|
815
|
+
|
|
816
|
+
Args:
|
|
817
|
+
name: Element name/key
|
|
818
|
+
locator: Element locator tuple (By, value)
|
|
819
|
+
"""
|
|
820
|
+
self.elements[name] = locator
|
|
821
|
+
logger.info(f"Added element: {name}")
|
|
822
|
+
|
|
823
|
+
def setup_and_login(self) -> bool:
|
|
824
|
+
"""
|
|
825
|
+
Setup WebDriver and login in one call.
|
|
826
|
+
|
|
827
|
+
Returns:
|
|
828
|
+
bool: True if both setup and login successful, False otherwise
|
|
829
|
+
"""
|
|
830
|
+
if not self.setup_driver():
|
|
831
|
+
logger.error("Failed to setup WebDriver")
|
|
832
|
+
return False
|
|
833
|
+
|
|
834
|
+
if not self.login():
|
|
835
|
+
logger.error("Failed to login")
|
|
836
|
+
self.quit_driver()
|
|
837
|
+
return False
|
|
838
|
+
|
|
839
|
+
return True
|
|
840
|
+
|
|
841
|
+
def quit_driver(self):
|
|
842
|
+
"""Quit the WebDriver."""
|
|
843
|
+
if self.driver:
|
|
844
|
+
self.driver.quit()
|
|
845
|
+
logger.info("WebDriver quit")
|
|
846
|
+
|
|
847
|
+
def __enter__(self):
|
|
848
|
+
"""Context manager entry."""
|
|
849
|
+
return self
|
|
850
|
+
|
|
851
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
852
|
+
"""Context manager exit - automatically logout and quit driver."""
|
|
853
|
+
try:
|
|
854
|
+
self.logout()
|
|
855
|
+
except:
|
|
856
|
+
pass
|
|
857
|
+
finally:
|
|
858
|
+
self.quit_driver()
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
# Convenience functions for common operations
|
|
862
|
+
def create_auth_session(headless: bool = False, base_dir: str = ".") -> MedicentreAuth:
|
|
863
|
+
"""
|
|
864
|
+
Create a new MedicentreAuth session.
|
|
865
|
+
|
|
866
|
+
Args:
|
|
867
|
+
headless: Run browser in headless mode
|
|
868
|
+
base_dir: Base directory for logs and screenshots
|
|
869
|
+
|
|
870
|
+
Returns:
|
|
871
|
+
MedicentreAuth: Authentication session object
|
|
872
|
+
"""
|
|
873
|
+
return MedicentreAuth(headless=headless, base_dir=base_dir)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def quick_login(headless: bool = False, base_dir: str = ".") -> Optional[MedicentreAuth]:
|
|
877
|
+
"""
|
|
878
|
+
Quick login function that returns a logged-in session.
|
|
879
|
+
|
|
880
|
+
Args:
|
|
881
|
+
headless: Run browser in headless mode
|
|
882
|
+
base_dir: Base directory for logs and screenshots
|
|
883
|
+
|
|
884
|
+
Returns:
|
|
885
|
+
MedicentreAuth: Logged-in session or None if failed
|
|
886
|
+
"""
|
|
887
|
+
auth = MedicentreAuth(headless=headless, base_dir=base_dir)
|
|
888
|
+
if auth.setup_and_login():
|
|
889
|
+
return auth
|
|
890
|
+
return None
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: medicentrev3-auth
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Authentication and navigation module for MedicentreV3 automation
|
|
5
|
+
Author-email: Denis Ndiritu <denis@hanmak.co.ke>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: selenium>=4.0.0
|
|
13
|
+
Requires-Dist: python-dotenv>=0.19.0
|
|
14
|
+
Requires-Dist: webdriver-manager>=3.8.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
18
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
19
|
+
Requires-Dist: flake8>=5.0.0; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# MedicentreV3 Authentication Package
|
|
22
|
+
|
|
23
|
+
A reusable Python package for MedicentreV3 authentication and navigation.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Install from GitHub
|
|
29
|
+
pip install git+https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package.git
|
|
30
|
+
|
|
31
|
+
# For development
|
|
32
|
+
pip install -e git+https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package.git
|
|
33
|
+
|
|
34
|
+
## Updating the Package
|
|
35
|
+
|
|
36
|
+
When updates are available, users should upgrade using:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Upgrade to latest version
|
|
40
|
+
pip install --upgrade git+https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package.git
|
|
41
|
+
|
|
42
|
+
# Or reinstall (force latest)
|
|
43
|
+
pip install --force-reinstall git+https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package.git
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Important**: Always use `--upgrade` or `--force-reinstall` when updating from GitHub to ensure you get the latest changes.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
medicentrev3_auth/__init__.py
|
|
5
|
+
medicentrev3_auth/auth.py
|
|
6
|
+
medicentrev3_auth.egg-info/PKG-INFO
|
|
7
|
+
medicentrev3_auth.egg-info/SOURCES.txt
|
|
8
|
+
medicentrev3_auth.egg-info/dependency_links.txt
|
|
9
|
+
medicentrev3_auth.egg-info/requires.txt
|
|
10
|
+
medicentrev3_auth.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
medicentrev3_auth
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "medicentrev3-auth"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Authentication and navigation module for MedicentreV3 automation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Denis Ndiritu", email = "denis@hanmak.co.ke"}
|
|
12
|
+
]
|
|
13
|
+
license = {text = "MIT"}
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"selenium>=4.0.0",
|
|
21
|
+
"python-dotenv>=0.19.0",
|
|
22
|
+
"webdriver-manager>=3.8.0"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=7.0.0",
|
|
28
|
+
"pytest-cov>=4.0.0",
|
|
29
|
+
"black>=22.0.0",
|
|
30
|
+
"flake8>=5.0.0"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/Hanmak-Technologies-Automation-Project/medicentrev3-authentication-package"
|