autoflex 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.
- autoflex-1.0.0/PKG-INFO +26 -0
- autoflex-1.0.0/README.md +1 -0
- autoflex-1.0.0/autoflex/__init__.py +0 -0
- autoflex-1.0.0/autoflex/core/__init__.py +0 -0
- autoflex-1.0.0/autoflex/core/actions.py +446 -0
- autoflex-1.0.0/autoflex/core/config_loader.py +28 -0
- autoflex-1.0.0/autoflex/core/exceptions.py +83 -0
- autoflex-1.0.0/autoflex/core/logger.py +31 -0
- autoflex-1.0.0/autoflex/core/web_manager.py +115 -0
- autoflex-1.0.0/autoflex/tests/__init__.py +0 -0
- autoflex-1.0.0/autoflex/tests/test_browser.py +50 -0
- autoflex-1.0.0/autoflex.egg-info/PKG-INFO +26 -0
- autoflex-1.0.0/autoflex.egg-info/SOURCES.txt +17 -0
- autoflex-1.0.0/autoflex.egg-info/dependency_links.txt +1 -0
- autoflex-1.0.0/autoflex.egg-info/requires.txt +3 -0
- autoflex-1.0.0/autoflex.egg-info/top_level.txt +1 -0
- autoflex-1.0.0/pyproject.toml +3 -0
- autoflex-1.0.0/setup.cfg +4 -0
- autoflex-1.0.0/setup.py +24 -0
autoflex-1.0.0/PKG-INFO
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: autoflex
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: Web and System Automation Testing Framework
|
5
|
+
Home-page: https://github.com/LZYEIL/AutoFlex
|
6
|
+
Author: Zhiyuan Li
|
7
|
+
Author-email: zhiyuanjeremy@gmail.com
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Requires-Python: >=3.8
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
Requires-Dist: selenium>=4.24.0
|
14
|
+
Requires-Dist: pyautogui>=0.9.54
|
15
|
+
Requires-Dist: pynput>=1.8.1
|
16
|
+
Dynamic: author
|
17
|
+
Dynamic: author-email
|
18
|
+
Dynamic: classifier
|
19
|
+
Dynamic: description
|
20
|
+
Dynamic: description-content-type
|
21
|
+
Dynamic: home-page
|
22
|
+
Dynamic: requires-dist
|
23
|
+
Dynamic: requires-python
|
24
|
+
Dynamic: summary
|
25
|
+
|
26
|
+
# This is AutoFlex README Template
|
autoflex-1.0.0/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# This is AutoFlex README Template
|
File without changes
|
File without changes
|
@@ -0,0 +1,446 @@
|
|
1
|
+
from .selenium_actions import (
|
2
|
+
element_actions as selenium_element,
|
3
|
+
dropdown_actions as selenium_dropdown,
|
4
|
+
page_actions as selenium_page,
|
5
|
+
mouse_actions as selenium_mouse,
|
6
|
+
wait_actions as selenium_wait,
|
7
|
+
script_actions as selenium_script,
|
8
|
+
screenshot_actions as selenium_screenshot
|
9
|
+
)
|
10
|
+
from .pyautogui_actions import (
|
11
|
+
mouse_actions as pyauto_mouse,
|
12
|
+
keyboard_actions as pyauto_keyboard,
|
13
|
+
screen_actions as pyauto_screen,
|
14
|
+
dialog_actions as pyauto_dialog
|
15
|
+
)
|
16
|
+
from .pynput_actions import (
|
17
|
+
keyboard_controller as pynput_keyboard,
|
18
|
+
keyboard_listener as pynput_kb_listener,
|
19
|
+
mouse_listener as pynput_mouse_listener
|
20
|
+
)
|
21
|
+
from .logger import logger
|
22
|
+
from .exceptions import OperationTimeoutError
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
|
27
|
+
class Actions:
|
28
|
+
|
29
|
+
|
30
|
+
# Here are the functions from the Selenium:
|
31
|
+
@staticmethod
|
32
|
+
def find_element(locator, timeout=10):
|
33
|
+
element = selenium_element.ElementActions.find_element(locator, timeout)
|
34
|
+
return element
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
|
39
|
+
# Click: Supports switching to PyAutoGUI
|
40
|
+
@staticmethod
|
41
|
+
def click(locator=None, x=None, y=None, timeout=10):
|
42
|
+
if locator:
|
43
|
+
try:
|
44
|
+
selenium_element.ElementActions.click(locator, timeout)
|
45
|
+
logger.info(f"Click via Selenium: {locator}")
|
46
|
+
return
|
47
|
+
except Exception as e:
|
48
|
+
logger.warning(f"Selenium click failed: {e}, fallback to PyAutoGUI.")
|
49
|
+
if x is not None and y is not None:
|
50
|
+
try:
|
51
|
+
pyauto_mouse.MouseActions.click_at(x, y)
|
52
|
+
logger.info(f"Click at ({x},{y}) via PyAutoGUI")
|
53
|
+
return
|
54
|
+
except Exception as e:
|
55
|
+
logger.error(f"PyAutoGUI click failed: {e}")
|
56
|
+
raise OperationTimeoutError("Click failed.")
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
|
61
|
+
@staticmethod
|
62
|
+
def input_text(locator, text, timeout=10):
|
63
|
+
try:
|
64
|
+
selenium_element.ElementActions.input_text(locator, text, timeout)
|
65
|
+
logger.info(f"Input text via Selenium on {locator}: {text}")
|
66
|
+
except Exception as e:
|
67
|
+
logger.error(f"Input text on {locator} failed: {e}")
|
68
|
+
raise OperationTimeoutError("Input text failed.")
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
@staticmethod
|
74
|
+
def get_text(locator, timeout=10):
|
75
|
+
try:
|
76
|
+
text = selenium_element.ElementActions.get_text(locator, timeout)
|
77
|
+
logger.info(f"Get text via Selenium from {locator}: {text}")
|
78
|
+
return text
|
79
|
+
except Exception as e:
|
80
|
+
logger.error(f"Get text from {locator} failed: {e}")
|
81
|
+
raise OperationTimeoutError("Get text failed.")
|
82
|
+
|
83
|
+
|
84
|
+
|
85
|
+
|
86
|
+
@staticmethod
|
87
|
+
def get_attribute(locator, attr, timeout=10):
|
88
|
+
try:
|
89
|
+
value = selenium_element.ElementActions.get_attribute(locator, attr, timeout)
|
90
|
+
logger.info(f"Get attribute '{attr}' from {locator}: {value}")
|
91
|
+
return value
|
92
|
+
except Exception as e:
|
93
|
+
logger.error(f"Get attribute {attr} from {locator} failed: {e}")
|
94
|
+
raise OperationTimeoutError("Get attribute failed.")
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
@staticmethod
|
100
|
+
def is_visible(locator, timeout=10):
|
101
|
+
visible = selenium_element.ElementActions.is_visible(locator, timeout)
|
102
|
+
return visible
|
103
|
+
|
104
|
+
|
105
|
+
|
106
|
+
|
107
|
+
@staticmethod
|
108
|
+
def move_to_element(locator, timeout=10):
|
109
|
+
try:
|
110
|
+
selenium_mouse.MouseActions.move_to_element(locator, timeout)
|
111
|
+
logger.info(f"Moved to element via Selenium: {locator}")
|
112
|
+
except Exception as e:
|
113
|
+
logger.error(f"Move to {locator} failed: {e}")
|
114
|
+
raise OperationTimeoutError("Move to element failed.")
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
|
119
|
+
# double_click supports switching to PyAutoGUI
|
120
|
+
@staticmethod
|
121
|
+
def double_click(locator=None, x=None, y=None, timeout=10):
|
122
|
+
if locator:
|
123
|
+
try:
|
124
|
+
selenium_mouse.MouseActions.double_click(locator, timeout)
|
125
|
+
logger.info(f"Double click via Selenium: {locator}")
|
126
|
+
return
|
127
|
+
except Exception as e:
|
128
|
+
logger.warning(f"Selenium double click failed: {e}, fallback.")
|
129
|
+
if x is not None and y is not None:
|
130
|
+
try:
|
131
|
+
pyauto_mouse.MouseActions.double_click_at(x, y)
|
132
|
+
logger.info(f"Double click at ({x},{y}) via PyAutoGUI")
|
133
|
+
return
|
134
|
+
except Exception as e:
|
135
|
+
logger.error(f"PyAutoGUI double click failed: {e}")
|
136
|
+
raise OperationTimeoutError("Double click failed.")
|
137
|
+
|
138
|
+
|
139
|
+
|
140
|
+
|
141
|
+
# right_click supports switching to PyAutoGUI
|
142
|
+
@staticmethod
|
143
|
+
def right_click(locator=None, x=None, y=None, timeout=10):
|
144
|
+
if locator:
|
145
|
+
try:
|
146
|
+
selenium_mouse.MouseActions.right_click(locator, timeout)
|
147
|
+
logger.info(f"Right click via Selenium: {locator}")
|
148
|
+
return
|
149
|
+
except Exception as e:
|
150
|
+
logger.warning(f"Selenium right click failed: {e}, fallback to PyAutoGUI.")
|
151
|
+
if x is not None and y is not None:
|
152
|
+
try:
|
153
|
+
pyauto_mouse.MouseActions.right_click_at(x, y)
|
154
|
+
logger.info(f"Right click at ({x},{y}) via PyAutoGUI")
|
155
|
+
return
|
156
|
+
except Exception as e:
|
157
|
+
logger.error(f"PyAutoGUI right click failed: {e}")
|
158
|
+
raise OperationTimeoutError("Right click action failed.")
|
159
|
+
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
|
164
|
+
@staticmethod
|
165
|
+
def drag_and_drop(source_locator, target_locator, timeout=10):
|
166
|
+
try:
|
167
|
+
selenium_mouse.MouseActions.drag_and_drop(source_locator, target_locator, timeout)
|
168
|
+
except Exception as e:
|
169
|
+
logger.error(f"Drag and drop from {source_locator} to {target_locator} failed: {e}")
|
170
|
+
raise OperationTimeoutError("Drag and drop failed.")
|
171
|
+
|
172
|
+
|
173
|
+
|
174
|
+
@staticmethod
|
175
|
+
def select_by_visible_text(locator, text, timeout=10):
|
176
|
+
try:
|
177
|
+
selenium_dropdown.DropdownActions.select_by_visible_text(locator, text, timeout)
|
178
|
+
except Exception as e:
|
179
|
+
logger.error(f"Select by text in {locator} failed: {e}")
|
180
|
+
raise OperationTimeoutError("Select by visible text failed.")
|
181
|
+
|
182
|
+
|
183
|
+
|
184
|
+
|
185
|
+
@staticmethod
|
186
|
+
def select_by_value(locator, value, timeout=10):
|
187
|
+
try:
|
188
|
+
selenium_dropdown.DropdownActions.select_by_value(locator, value, timeout)
|
189
|
+
except Exception as e:
|
190
|
+
logger.error(f"Select by value in {locator} failed: {e}")
|
191
|
+
raise OperationTimeoutError("Select by value failed.")
|
192
|
+
|
193
|
+
|
194
|
+
|
195
|
+
|
196
|
+
@staticmethod
|
197
|
+
def select_by_index(locator, index, timeout=10):
|
198
|
+
try:
|
199
|
+
selenium_dropdown.DropdownActions.select_by_index(locator, index, timeout)
|
200
|
+
except Exception as e:
|
201
|
+
logger.error(f"Select by index in {locator} failed: {e}")
|
202
|
+
raise OperationTimeoutError("Select by index failed.")
|
203
|
+
|
204
|
+
|
205
|
+
|
206
|
+
|
207
|
+
@staticmethod
|
208
|
+
def switch_to_window(window_name):
|
209
|
+
selenium_page.PageActions.switch_to_window(window_name)
|
210
|
+
|
211
|
+
|
212
|
+
|
213
|
+
|
214
|
+
@staticmethod
|
215
|
+
def switch_to_frame(frame_reference):
|
216
|
+
selenium_page.PageActions.switch_to_frame(frame_reference)
|
217
|
+
|
218
|
+
|
219
|
+
|
220
|
+
|
221
|
+
@staticmethod
|
222
|
+
def switch_to_default_content():
|
223
|
+
try:
|
224
|
+
selenium_page.PageActions.switch_to_default_content()
|
225
|
+
except Exception as e:
|
226
|
+
logger.error(f"Switch to default content failed: {e}")
|
227
|
+
raise OperationTimeoutError("Switch to default content failed.")
|
228
|
+
|
229
|
+
|
230
|
+
|
231
|
+
|
232
|
+
@staticmethod
|
233
|
+
def accept_alert():
|
234
|
+
selenium_page.PageActions.accept_alert()
|
235
|
+
|
236
|
+
|
237
|
+
|
238
|
+
|
239
|
+
@staticmethod
|
240
|
+
def dismiss_alert():
|
241
|
+
selenium_page.PageActions.dismiss_alert()
|
242
|
+
|
243
|
+
|
244
|
+
|
245
|
+
|
246
|
+
# take_screenshot supports switching to PyAutoGUI
|
247
|
+
@staticmethod
|
248
|
+
def take_screenshot(file_path):
|
249
|
+
try:
|
250
|
+
selenium_screenshot.ScreenshotActions.take_screenshot(file_path)
|
251
|
+
logger.info(f"Screenshot via Selenium saved to {file_path}")
|
252
|
+
return
|
253
|
+
except Exception as e:
|
254
|
+
logger.warning(f"Selenium screenshot failed: {e}, fallback to PyAutoGUI.")
|
255
|
+
try:
|
256
|
+
pyauto_screen.ScreenActions.take_screenshot(file_path)
|
257
|
+
logger.info(f"Screenshot via PyAutoGUI saved to {file_path}")
|
258
|
+
except Exception as e:
|
259
|
+
logger.error(f"PyAutoGUI screenshot failed: {e}")
|
260
|
+
raise OperationTimeoutError("Screenshot action failed.")
|
261
|
+
|
262
|
+
|
263
|
+
|
264
|
+
|
265
|
+
@staticmethod
|
266
|
+
def execute_script(script):
|
267
|
+
result = selenium_script.ScriptActions.execute_script(script)
|
268
|
+
return result
|
269
|
+
|
270
|
+
|
271
|
+
|
272
|
+
|
273
|
+
@staticmethod
|
274
|
+
def execute_async_script(script):
|
275
|
+
result = selenium_script.ScriptActions.execute_async_script(script)
|
276
|
+
return result
|
277
|
+
|
278
|
+
|
279
|
+
|
280
|
+
|
281
|
+
@staticmethod
|
282
|
+
def wait_for_element_visible(locator, timeout=10):
|
283
|
+
try:
|
284
|
+
selenium_wait.WaitActions.wait_for_element_visible(locator, timeout)
|
285
|
+
logger.info(f"Element {locator} is visible.")
|
286
|
+
except Exception as e:
|
287
|
+
logger.error(f"Wait for element {locator} visible failed: {e}")
|
288
|
+
raise OperationTimeoutError("Wait for element visible failed.")
|
289
|
+
|
290
|
+
|
291
|
+
|
292
|
+
|
293
|
+
@staticmethod
|
294
|
+
def wait_for_element_clickable(locator, timeout=10):
|
295
|
+
try:
|
296
|
+
selenium_wait.WaitActions.wait_for_element_clickable(locator, timeout)
|
297
|
+
logger.info(f"Element {locator} is clickable.")
|
298
|
+
except Exception as e:
|
299
|
+
logger.error(f"Wait for element {locator} clickable failed: {e}")
|
300
|
+
raise OperationTimeoutError("Wait for element clickable failed.")
|
301
|
+
|
302
|
+
|
303
|
+
|
304
|
+
|
305
|
+
# Below are the functions from the PyAutogGUI:
|
306
|
+
# Dialog Actions:
|
307
|
+
@staticmethod
|
308
|
+
def show_alert(message):
|
309
|
+
pyauto_dialog.DialogActions.show_alert(message)
|
310
|
+
|
311
|
+
|
312
|
+
|
313
|
+
|
314
|
+
|
315
|
+
@staticmethod
|
316
|
+
def show_confirm(message):
|
317
|
+
result = pyauto_dialog.DialogActions.show_confirm(message)
|
318
|
+
return result
|
319
|
+
|
320
|
+
|
321
|
+
|
322
|
+
|
323
|
+
@staticmethod
|
324
|
+
def show_prompt(message):
|
325
|
+
result = pyauto_dialog.DialogActions.show_prompt(message)
|
326
|
+
return result
|
327
|
+
|
328
|
+
|
329
|
+
# Keyboard Actions:
|
330
|
+
@staticmethod
|
331
|
+
def press_key(key):
|
332
|
+
pyauto_keyboard.KeyboardActions.press_key(key)
|
333
|
+
|
334
|
+
|
335
|
+
|
336
|
+
@staticmethod
|
337
|
+
def press_hotkey(*keys):
|
338
|
+
pyauto_keyboard.KeyboardActions.press_hotkey(*keys)
|
339
|
+
|
340
|
+
|
341
|
+
|
342
|
+
@staticmethod
|
343
|
+
def hold_and_release(keys, hold_time=0.5):
|
344
|
+
pyauto_keyboard.KeyboardActions.hold_and_release(keys, hold_time)
|
345
|
+
|
346
|
+
|
347
|
+
|
348
|
+
@staticmethod
|
349
|
+
def type_text(text, interval=0.05):
|
350
|
+
pyauto_keyboard.KeyboardActions.type_text(text, interval)
|
351
|
+
|
352
|
+
|
353
|
+
# Mouse Actions:
|
354
|
+
@staticmethod
|
355
|
+
def move_to(x, y, duration=0.5):
|
356
|
+
pyauto_mouse.MouseActions.move_to(x, y, duration)
|
357
|
+
|
358
|
+
|
359
|
+
|
360
|
+
@staticmethod
|
361
|
+
def drag_to(x, y, duration=0.5):
|
362
|
+
pyauto_mouse.MouseActions.drag_to(x, y, duration)
|
363
|
+
|
364
|
+
|
365
|
+
|
366
|
+
@staticmethod
|
367
|
+
def scroll(amount):
|
368
|
+
pyauto_mouse.MouseActions.scroll(amount)
|
369
|
+
|
370
|
+
|
371
|
+
|
372
|
+
@staticmethod
|
373
|
+
def get_mouse_position():
|
374
|
+
pos = pyauto_mouse.MouseActions.get_position()
|
375
|
+
return pos
|
376
|
+
|
377
|
+
|
378
|
+
|
379
|
+
# Screen Actions:
|
380
|
+
@staticmethod
|
381
|
+
def get_pixel_color(x, y):
|
382
|
+
color = pyauto_screen.ScreenActions.get_pixel_color(x, y)
|
383
|
+
return color
|
384
|
+
|
385
|
+
|
386
|
+
|
387
|
+
@staticmethod
|
388
|
+
def pixel_matches_color(x, y, expected_color):
|
389
|
+
match = pyauto_screen.ScreenActions.pixel_matches_color(x, y, expected_color)
|
390
|
+
return match
|
391
|
+
|
392
|
+
|
393
|
+
|
394
|
+
|
395
|
+
@staticmethod
|
396
|
+
def locate_image_on_screen(image_path, confidence=0.9):
|
397
|
+
pos = pyauto_screen.ScreenActions.locate_image_on_screen(image_path, confidence)
|
398
|
+
return pos
|
399
|
+
|
400
|
+
|
401
|
+
|
402
|
+
# Below are the functions from the PynPut:
|
403
|
+
@staticmethod
|
404
|
+
def press_key_pynput(key):
|
405
|
+
pynput_keyboard.KeyboardController.press_key(key)
|
406
|
+
|
407
|
+
|
408
|
+
|
409
|
+
|
410
|
+
@staticmethod
|
411
|
+
def release_key_pynput(key):
|
412
|
+
pynput_keyboard.KeyboardController.release_key(key)
|
413
|
+
|
414
|
+
|
415
|
+
|
416
|
+
|
417
|
+
@staticmethod
|
418
|
+
def press_and_hold_pynput(keys, hold_time=0.5):
|
419
|
+
pynput_keyboard.KeyboardController.press_and_hold(keys, hold_time)
|
420
|
+
|
421
|
+
|
422
|
+
|
423
|
+
|
424
|
+
@staticmethod
|
425
|
+
def start_keyboard_listener(on_press_func):
|
426
|
+
try:
|
427
|
+
pynput_kb_listener.KeyboardListener.start_listener(on_press_func)
|
428
|
+
logger.info(f"Pynput keyboard listener started.")
|
429
|
+
except Exception as e:
|
430
|
+
logger.error(f"Pynput keyboard listener failed: {e}")
|
431
|
+
raise OperationTimeoutError("Pynput keyboard listener failed.")
|
432
|
+
|
433
|
+
|
434
|
+
|
435
|
+
|
436
|
+
@staticmethod
|
437
|
+
def start_mouse_listener(on_click_func):
|
438
|
+
try:
|
439
|
+
pynput_mouse_listener.MouseListener.start_listener(on_click_func)
|
440
|
+
logger.info(f"Pynput mouse listener started.")
|
441
|
+
except Exception as e:
|
442
|
+
logger.error(f"Pynput mouse listener failed: {e}")
|
443
|
+
raise OperationTimeoutError("Pynput mouse listener failed.")
|
444
|
+
|
445
|
+
|
446
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import yaml
|
2
|
+
import os
|
3
|
+
from .logger import logger
|
4
|
+
from .exceptions import ConfigLoadError
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
def load_config(config_path=None):
|
9
|
+
"""
|
10
|
+
Load YAML configurations, return a dict
|
11
|
+
"""
|
12
|
+
default_path = os.path.join(os.path.dirname(__file__), "../config/config.yaml")
|
13
|
+
final_path = config_path if config_path else default_path
|
14
|
+
try:
|
15
|
+
# Check if the path exists:
|
16
|
+
if not os.path.exists(final_path):
|
17
|
+
raise FileNotFoundError(f"No Configuration File Found: {final_path}")
|
18
|
+
|
19
|
+
# Open and analyze YAML file:
|
20
|
+
with open(final_path, "r", encoding="utf-8") as f:
|
21
|
+
config = yaml.safe_load(f)
|
22
|
+
|
23
|
+
logger.info(f"Successfully Loaded The Configuration File: {final_path}")
|
24
|
+
return config
|
25
|
+
|
26
|
+
except Exception as e:
|
27
|
+
logger.error(f"Failed To Load The Configuration File: {str(e)}")
|
28
|
+
raise ConfigLoadError("Failed To Load Configuration File.", e)
|
@@ -0,0 +1,83 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class BrowserLaunchError(Exception):
|
4
|
+
"""
|
5
|
+
Custom exception for browser launch failures.
|
6
|
+
|
7
|
+
Attributes:
|
8
|
+
message (str): Human-readable error message.
|
9
|
+
original_exception (Exception, optional): Original exception instance.
|
10
|
+
"""
|
11
|
+
|
12
|
+
def __init__(self, message, original_exception=None):
|
13
|
+
super().__init__(message)
|
14
|
+
self.original_exception = original_exception
|
15
|
+
|
16
|
+
|
17
|
+
def __str__(self):
|
18
|
+
if self.original_exception:
|
19
|
+
return f"{self.args[0]} (Caused by: {repr(self.original_exception)})"
|
20
|
+
return self.args[0]
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
class ConfigLoadError(Exception):
|
25
|
+
"""
|
26
|
+
Custom exception for configuration loading failures.
|
27
|
+
|
28
|
+
Attributes:
|
29
|
+
message (str): Human-readable error message.
|
30
|
+
original_exception (Exception, optional): Original exception instance.
|
31
|
+
"""
|
32
|
+
|
33
|
+
def __init__(self, message, original_exception=None):
|
34
|
+
super().__init__(message)
|
35
|
+
self.original_exception = original_exception
|
36
|
+
|
37
|
+
def __str__(self):
|
38
|
+
if self.original_exception:
|
39
|
+
return f"{self.args[0]} (Caused by: {repr(self.original_exception)})"
|
40
|
+
return self.args[0]
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
class ElementNotFoundError(Exception):
|
47
|
+
"""
|
48
|
+
Raised when a web element cannot be found.
|
49
|
+
|
50
|
+
Attributes:
|
51
|
+
message (str): Human-readable error message.
|
52
|
+
original_exception (Exception, optional): Original exception instance.
|
53
|
+
"""
|
54
|
+
|
55
|
+
def __init__(self, message="Element Not Found", original_exception=None):
|
56
|
+
super().__init__(message)
|
57
|
+
self.original_exception = original_exception
|
58
|
+
|
59
|
+
def __str__(self):
|
60
|
+
if self.original_exception:
|
61
|
+
return f"{self.args[0]} (Caused by: {repr(self.original_exception)})"
|
62
|
+
return self.args[0]
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
class OperationTimeoutError(Exception):
|
68
|
+
"""
|
69
|
+
Raised when a wait or operation times out.
|
70
|
+
|
71
|
+
Attributes:
|
72
|
+
message (str): Human-readable error message.
|
73
|
+
original_exception (Exception, optional): Original exception instance.
|
74
|
+
"""
|
75
|
+
|
76
|
+
def __init__(self, message="Operation Timed Out", original_exception=None):
|
77
|
+
super().__init__(message)
|
78
|
+
self.original_exception = original_exception
|
79
|
+
|
80
|
+
def __str__(self):
|
81
|
+
if self.original_exception:
|
82
|
+
return f"{self.args[0]} (Caused by: {repr(self.original_exception)})"
|
83
|
+
return self.args[0]
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
|
4
|
+
|
5
|
+
# The path for the logger file:
|
6
|
+
log_dir = "logs"
|
7
|
+
os.makedirs(log_dir, exist_ok=True)
|
8
|
+
log_file = os.path.join(log_dir, "autoflex.log")
|
9
|
+
|
10
|
+
# Create a logger from logging:
|
11
|
+
logger = logging.getLogger("AutoFlexLogger")
|
12
|
+
logger.setLevel(logging.DEBUG) # All levels (DEBUG -> INFO -> WARNING -> ERROR -> CRITICAL)
|
13
|
+
|
14
|
+
# Log format:
|
15
|
+
formatter = logging.Formatter(
|
16
|
+
"%(asctime)s - %(levelname)s - %(filename)s - %(funcName)s - %(lineno)d - %(message)s"
|
17
|
+
)
|
18
|
+
|
19
|
+
# File logger handler:
|
20
|
+
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
21
|
+
file_handler.setLevel(logging.DEBUG)
|
22
|
+
file_handler.setFormatter(formatter)
|
23
|
+
|
24
|
+
# Console logger handler:
|
25
|
+
console_handler = logging.StreamHandler()
|
26
|
+
console_handler.setLevel(logging.INFO)
|
27
|
+
console_handler.setFormatter(formatter)
|
28
|
+
|
29
|
+
# Add both handlers to the logger instance:
|
30
|
+
logger.addHandler(file_handler)
|
31
|
+
logger.addHandler(console_handler)
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# Imports
|
2
|
+
from selenium import webdriver
|
3
|
+
from selenium.webdriver.chrome.service import Service as ChromeService
|
4
|
+
from selenium.webdriver.edge.service import Service as EdgeService
|
5
|
+
from selenium.webdriver.firefox.service import Service as FirefoxService
|
6
|
+
|
7
|
+
from selenium.webdriver.chrome.options import Options as ChromeOptions
|
8
|
+
from selenium.webdriver.edge.options import Options as EdgeOptions
|
9
|
+
from selenium.webdriver.firefox.options import Options as FirefoxOptions
|
10
|
+
|
11
|
+
from .logger import logger
|
12
|
+
from .exceptions import BrowserLaunchError
|
13
|
+
from .config_loader import load_config
|
14
|
+
|
15
|
+
|
16
|
+
# The Browser Manager class:
|
17
|
+
class Browser:
|
18
|
+
_driver = None # Class variable (private)
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def start(cls, browser_type="chrome", remote_url=None, config_path=None): # The config_path here is for user's customizable path
|
24
|
+
try:
|
25
|
+
logger.info(f"Starting The Browser: {browser_type}")
|
26
|
+
|
27
|
+
config = load_config(config_path)
|
28
|
+
options = None # Initialize the options and service, will update later
|
29
|
+
service = None
|
30
|
+
|
31
|
+
# Chrome logic:
|
32
|
+
if browser_type.lower() == "chrome":
|
33
|
+
options = ChromeOptions()
|
34
|
+
driver_path = config.get("chrome")
|
35
|
+
if not driver_path:
|
36
|
+
raise FileNotFoundError("Chrome Driver Path Not Specified In config.yaml")
|
37
|
+
service = ChromeService(executable_path=driver_path)
|
38
|
+
|
39
|
+
for arg in config.get("chrome_args", []):
|
40
|
+
options.add_argument(arg)
|
41
|
+
|
42
|
+
# Edge logic:
|
43
|
+
elif browser_type.lower() == "edge":
|
44
|
+
options = EdgeOptions()
|
45
|
+
driver_path = config.get("edge")
|
46
|
+
if not driver_path:
|
47
|
+
raise FileNotFoundError("Edge Driver Path Not Specified In config.yaml")
|
48
|
+
service = EdgeService(executable_path=driver_path)
|
49
|
+
|
50
|
+
for arg in config.get("edge_args", []):
|
51
|
+
options.add_argument(arg)
|
52
|
+
|
53
|
+
# Firefox logic:
|
54
|
+
elif browser_type.lower() == "firefox":
|
55
|
+
options = FirefoxOptions()
|
56
|
+
driver_path = config.get("firefox")
|
57
|
+
if not driver_path:
|
58
|
+
raise FileNotFoundError("Firefox Driver Path Not Specified In config.yaml")
|
59
|
+
service = FirefoxService(executable_path=driver_path)
|
60
|
+
|
61
|
+
|
62
|
+
for key, value in config.get("firefox_prefs", {}).items():
|
63
|
+
options.set_preference(key, value)
|
64
|
+
|
65
|
+
else:
|
66
|
+
raise ValueError(f"Not Supported Browser Type: {browser_type}")
|
67
|
+
|
68
|
+
# Logic of dealing with remote url:
|
69
|
+
if remote_url:
|
70
|
+
logger.info(f"Utilizing Remote WebDriver: {remote_url}")
|
71
|
+
cls._driver = webdriver.Remote(command_executor=remote_url, options=options)
|
72
|
+
else:
|
73
|
+
if browser_type.lower() == "chrome":
|
74
|
+
cls._driver = webdriver.Chrome(service=service, options=options)
|
75
|
+
elif browser_type.lower() == "edge":
|
76
|
+
cls._driver = webdriver.Edge(service=service, options=options)
|
77
|
+
elif browser_type.lower() == "firefox":
|
78
|
+
cls._driver = webdriver.Firefox(service=service, options=options)
|
79
|
+
|
80
|
+
cls._driver.maximize_window()
|
81
|
+
|
82
|
+
logger.info(f"{browser_type} Successfully Running")
|
83
|
+
return cls._driver
|
84
|
+
|
85
|
+
except Exception as e:
|
86
|
+
logger.error(f"Error Occured When Trying To Start The Browser: {str(e)}")
|
87
|
+
raise BrowserLaunchError(f"Failed To Start The Browser: {str(e)}")
|
88
|
+
|
89
|
+
|
90
|
+
|
91
|
+
|
92
|
+
'''
|
93
|
+
This method acts as a getter,
|
94
|
+
it returns the current driver object
|
95
|
+
'''
|
96
|
+
@classmethod
|
97
|
+
def get_driver(cls):
|
98
|
+
if cls._driver is None:
|
99
|
+
raise BrowserLaunchError("Not Starting The Browser Yet, Please call Browser.start()")
|
100
|
+
return cls._driver
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
|
105
|
+
@classmethod
|
106
|
+
def quit(cls):
|
107
|
+
try:
|
108
|
+
if cls._driver:
|
109
|
+
logger.info("Closing the browser")
|
110
|
+
cls._driver.quit()
|
111
|
+
cls._driver = None
|
112
|
+
except Exception as e:
|
113
|
+
logger.error(f"Failed to close the browser: {str(e)}")
|
114
|
+
|
115
|
+
|
File without changes
|
@@ -0,0 +1,50 @@
|
|
1
|
+
from autoflex.core.web_manager import Browser
|
2
|
+
from autoflex.core.actions import Actions
|
3
|
+
from autoflex.core.exceptions import BrowserLaunchError
|
4
|
+
import time
|
5
|
+
|
6
|
+
|
7
|
+
def on_key_press(key):
|
8
|
+
print(f"[监听] 键盘按下: {key}")
|
9
|
+
if str(key) == "'q'":
|
10
|
+
print("监听到 Q,停止监听")
|
11
|
+
return False
|
12
|
+
|
13
|
+
|
14
|
+
def on_mouse_click(x, y, button, pressed):
|
15
|
+
if pressed:
|
16
|
+
print(f"[监听] 鼠标点击: ({x},{y}) {button}")
|
17
|
+
|
18
|
+
|
19
|
+
try:
|
20
|
+
# 启动浏览器
|
21
|
+
driver = Browser.start("edge")
|
22
|
+
Actions.take_screenshot("startup.png")
|
23
|
+
|
24
|
+
# 打开百度并执行点击+输入+截图
|
25
|
+
driver.get("https://www.baidu.com")
|
26
|
+
Actions.take_screenshot("baidu_home.png")
|
27
|
+
Actions.click(locator=("css selector", "#kw"))
|
28
|
+
Actions.type_text("AutoFlex 调度器测试")
|
29
|
+
Actions.click(locator=("css selector", "#su"))
|
30
|
+
|
31
|
+
# 鼠标移动+截图
|
32
|
+
Actions.move_to(500, 200)
|
33
|
+
Actions.take_screenshot("after_move.png")
|
34
|
+
|
35
|
+
# 执行 Pynput 键盘监听
|
36
|
+
print("按 Q 键停止键盘监听:")
|
37
|
+
Actions.start_keyboard_listener(on_key_press)
|
38
|
+
|
39
|
+
# 执行 Pynput 鼠标监听
|
40
|
+
print("鼠标点击事件监听中,单击停止监听:")
|
41
|
+
Actions.start_mouse_listener(on_mouse_click)
|
42
|
+
|
43
|
+
input("测试完成,按 Enter 退出")
|
44
|
+
Browser.quit()
|
45
|
+
|
46
|
+
except BrowserLaunchError as e:
|
47
|
+
print(f"Failed to launch browser: {e}")
|
48
|
+
except Exception as e:
|
49
|
+
print(f"执行异常: {e}")
|
50
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: autoflex
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: Web and System Automation Testing Framework
|
5
|
+
Home-page: https://github.com/LZYEIL/AutoFlex
|
6
|
+
Author: Zhiyuan Li
|
7
|
+
Author-email: zhiyuanjeremy@gmail.com
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Requires-Python: >=3.8
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
Requires-Dist: selenium>=4.24.0
|
14
|
+
Requires-Dist: pyautogui>=0.9.54
|
15
|
+
Requires-Dist: pynput>=1.8.1
|
16
|
+
Dynamic: author
|
17
|
+
Dynamic: author-email
|
18
|
+
Dynamic: classifier
|
19
|
+
Dynamic: description
|
20
|
+
Dynamic: description-content-type
|
21
|
+
Dynamic: home-page
|
22
|
+
Dynamic: requires-dist
|
23
|
+
Dynamic: requires-python
|
24
|
+
Dynamic: summary
|
25
|
+
|
26
|
+
# This is AutoFlex README Template
|
@@ -0,0 +1,17 @@
|
|
1
|
+
README.md
|
2
|
+
pyproject.toml
|
3
|
+
setup.py
|
4
|
+
autoflex/__init__.py
|
5
|
+
autoflex.egg-info/PKG-INFO
|
6
|
+
autoflex.egg-info/SOURCES.txt
|
7
|
+
autoflex.egg-info/dependency_links.txt
|
8
|
+
autoflex.egg-info/requires.txt
|
9
|
+
autoflex.egg-info/top_level.txt
|
10
|
+
autoflex/core/__init__.py
|
11
|
+
autoflex/core/actions.py
|
12
|
+
autoflex/core/config_loader.py
|
13
|
+
autoflex/core/exceptions.py
|
14
|
+
autoflex/core/logger.py
|
15
|
+
autoflex/core/web_manager.py
|
16
|
+
autoflex/tests/__init__.py
|
17
|
+
autoflex/tests/test_browser.py
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
autoflex
|
autoflex-1.0.0/setup.cfg
ADDED
autoflex-1.0.0/setup.py
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
from setuptools import setup, find_packages
|
2
|
+
|
3
|
+
setup(
|
4
|
+
name='autoflex',
|
5
|
+
version='1.0.0',
|
6
|
+
description='Web and System Automation Testing Framework',
|
7
|
+
long_description=open('README.md', encoding='utf-8').read(),
|
8
|
+
long_description_content_type='text/markdown',
|
9
|
+
author='Zhiyuan Li',
|
10
|
+
author_email='zhiyuanjeremy@gmail.com',
|
11
|
+
url='https://github.com/LZYEIL/AutoFlex',
|
12
|
+
packages=find_packages(),
|
13
|
+
install_requires=[
|
14
|
+
'selenium>=4.24.0',
|
15
|
+
'pyautogui>=0.9.54',
|
16
|
+
'pynput>=1.8.1',
|
17
|
+
],
|
18
|
+
classifiers=[
|
19
|
+
'Programming Language :: Python :: 3',
|
20
|
+
'License :: OSI Approved :: MIT License',
|
21
|
+
'Operating System :: OS Independent',
|
22
|
+
],
|
23
|
+
python_requires='>=3.8',
|
24
|
+
)
|