seleniumbase 4.41.3__py3-none-any.whl → 4.45.10__py3-none-any.whl
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.
- sbase/steps.py +9 -0
- seleniumbase/__version__.py +1 -1
- seleniumbase/behave/behave_helper.py +2 -0
- seleniumbase/behave/behave_sb.py +21 -8
- seleniumbase/common/decorators.py +3 -1
- seleniumbase/console_scripts/run.py +1 -0
- seleniumbase/console_scripts/sb_caseplans.py +3 -4
- seleniumbase/console_scripts/sb_install.py +142 -11
- seleniumbase/console_scripts/sb_mkchart.py +1 -2
- seleniumbase/console_scripts/sb_mkdir.py +99 -29
- seleniumbase/console_scripts/sb_mkfile.py +1 -2
- seleniumbase/console_scripts/sb_mkpres.py +1 -2
- seleniumbase/console_scripts/sb_mkrec.py +26 -2
- seleniumbase/console_scripts/sb_objectify.py +4 -5
- seleniumbase/console_scripts/sb_print.py +1 -1
- seleniumbase/console_scripts/sb_recorder.py +40 -3
- seleniumbase/core/browser_launcher.py +474 -151
- seleniumbase/core/detect_b_ver.py +258 -16
- seleniumbase/core/log_helper.py +15 -21
- seleniumbase/core/mysql.py +1 -1
- seleniumbase/core/recorder_helper.py +3 -0
- seleniumbase/core/report_helper.py +9 -12
- seleniumbase/core/sb_cdp.py +734 -215
- seleniumbase/core/sb_driver.py +46 -5
- seleniumbase/core/session_helper.py +2 -4
- seleniumbase/core/tour_helper.py +1 -2
- seleniumbase/drivers/atlas_drivers/__init__.py +0 -0
- seleniumbase/drivers/brave_drivers/__init__.py +0 -0
- seleniumbase/drivers/chromium_drivers/__init__.py +0 -0
- seleniumbase/drivers/comet_drivers/__init__.py +0 -0
- seleniumbase/drivers/opera_drivers/__init__.py +0 -0
- seleniumbase/fixtures/base_case.py +448 -251
- seleniumbase/fixtures/constants.py +36 -9
- seleniumbase/fixtures/js_utils.py +77 -18
- seleniumbase/fixtures/page_actions.py +41 -13
- seleniumbase/fixtures/page_utils.py +19 -12
- seleniumbase/fixtures/shared_utils.py +64 -6
- seleniumbase/masterqa/master_qa.py +16 -2
- seleniumbase/plugins/base_plugin.py +8 -0
- seleniumbase/plugins/basic_test_info.py +2 -3
- seleniumbase/plugins/driver_manager.py +131 -5
- seleniumbase/plugins/page_source.py +2 -3
- seleniumbase/plugins/pytest_plugin.py +244 -79
- seleniumbase/plugins/sb_manager.py +143 -20
- seleniumbase/plugins/selenium_plugin.py +144 -12
- seleniumbase/translate/translator.py +2 -3
- seleniumbase/undetected/__init__.py +17 -13
- seleniumbase/undetected/cdp.py +1 -12
- seleniumbase/undetected/cdp_driver/browser.py +330 -129
- seleniumbase/undetected/cdp_driver/cdp_util.py +328 -61
- seleniumbase/undetected/cdp_driver/config.py +110 -14
- seleniumbase/undetected/cdp_driver/connection.py +18 -48
- seleniumbase/undetected/cdp_driver/element.py +105 -33
- seleniumbase/undetected/cdp_driver/tab.py +414 -39
- seleniumbase/utilities/selenium_grid/download_selenium_server.py +1 -1
- seleniumbase/utilities/selenium_grid/grid_hub.py +1 -2
- seleniumbase/utilities/selenium_grid/grid_node.py +2 -3
- seleniumbase/utilities/selenium_ide/convert_ide.py +2 -3
- {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/METADATA +193 -166
- {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/RECORD +64 -59
- {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/licenses/LICENSE +1 -1
- {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/WHEEL +0 -0
- {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/entry_points.txt +0 -0
- {seleniumbase-4.41.3.dist-info → seleniumbase-4.45.10.dist-info}/top_level.txt +0 -0
|
@@ -264,16 +264,13 @@ class Reveal:
|
|
|
264
264
|
|
|
265
265
|
|
|
266
266
|
class HighCharts:
|
|
267
|
+
LIB = "https://cdn.jsdelivr.net/npm/highcharts"
|
|
267
268
|
VER = "10.3.3"
|
|
268
|
-
HC_CSS = "
|
|
269
|
-
HC_JS = "
|
|
270
|
-
EXPORTING_JS = "
|
|
271
|
-
EXPORT_DATA_JS = (
|
|
272
|
-
|
|
273
|
-
)
|
|
274
|
-
ACCESSIBILITY_JS = (
|
|
275
|
-
"https://code.highcharts.com/%s/modules/accessibility.js" % VER
|
|
276
|
-
)
|
|
269
|
+
HC_CSS = "%s@%s/css/highcharts.css" % (LIB, VER)
|
|
270
|
+
HC_JS = "%s@%s/highcharts.js" % (LIB, VER)
|
|
271
|
+
EXPORTING_JS = "%s@%s/modules/exporting.js" % (LIB, VER)
|
|
272
|
+
EXPORT_DATA_JS = "%s@%s/modules/export-data.js" % (LIB, VER)
|
|
273
|
+
ACCESSIBILITY_JS = "%s@%s/modules/accessibility.js" % (LIB, VER)
|
|
277
274
|
|
|
278
275
|
|
|
279
276
|
class BootstrapTour:
|
|
@@ -387,6 +384,20 @@ class ValidBrowsers:
|
|
|
387
384
|
"ie",
|
|
388
385
|
"safari",
|
|
389
386
|
"remote",
|
|
387
|
+
"opera",
|
|
388
|
+
"brave",
|
|
389
|
+
"comet",
|
|
390
|
+
"atlas",
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class ChromiumSubs:
|
|
395
|
+
# Chromium browsers that still use chromedriver
|
|
396
|
+
chromium_subs = [
|
|
397
|
+
"opera",
|
|
398
|
+
"brave",
|
|
399
|
+
"comet",
|
|
400
|
+
"atlas",
|
|
390
401
|
]
|
|
391
402
|
|
|
392
403
|
|
|
@@ -406,7 +417,14 @@ class ValidBinaries:
|
|
|
406
417
|
"brave",
|
|
407
418
|
"opera",
|
|
408
419
|
"opera-stable",
|
|
420
|
+
"comet",
|
|
421
|
+
"comet-browser",
|
|
422
|
+
"comet-stable",
|
|
423
|
+
"atlas",
|
|
424
|
+
"atlas-browser",
|
|
425
|
+
"atlas-stable",
|
|
409
426
|
"chrome.exe", # WSL (Windows Subsystem for Linux)
|
|
427
|
+
"chromium.exe", # WSL (Windows Subsystem for Linux)
|
|
410
428
|
]
|
|
411
429
|
valid_edge_binaries_on_linux = [
|
|
412
430
|
"microsoft-edge",
|
|
@@ -423,7 +441,14 @@ class ValidBinaries:
|
|
|
423
441
|
"Google Chrome Beta",
|
|
424
442
|
"Google Chrome Dev",
|
|
425
443
|
"Brave Browser",
|
|
444
|
+
"Brave",
|
|
445
|
+
"Opera Browser",
|
|
426
446
|
"Opera",
|
|
447
|
+
"Comet Browser",
|
|
448
|
+
"Comet",
|
|
449
|
+
"ChatGPT Atlas",
|
|
450
|
+
"Atlas Browser",
|
|
451
|
+
"Atlas",
|
|
427
452
|
]
|
|
428
453
|
valid_edge_binaries_on_macos = [
|
|
429
454
|
"Microsoft Edge",
|
|
@@ -434,6 +459,8 @@ class ValidBinaries:
|
|
|
434
459
|
"chrome-headless-shell.exe",
|
|
435
460
|
"brave.exe",
|
|
436
461
|
"opera.exe",
|
|
462
|
+
"comet.exe",
|
|
463
|
+
"atlas.exe",
|
|
437
464
|
]
|
|
438
465
|
valid_edge_binaries_on_windows = [
|
|
439
466
|
"msedge.exe",
|
|
@@ -29,7 +29,9 @@ def wait_for_ready_state_complete(driver, timeout=settings.LARGE_TIMEOUT):
|
|
|
29
29
|
If the timeout is exceeded, the test will still continue
|
|
30
30
|
because readyState == "interactive" may be good enough.
|
|
31
31
|
(Previously, tests would fail immediately if exceeding the timeout.)"""
|
|
32
|
-
if hasattr(
|
|
32
|
+
if hasattr(driver, "_swap_driver"):
|
|
33
|
+
return
|
|
34
|
+
if getattr(settings, "SKIP_JS_WAITS", None):
|
|
33
35
|
return
|
|
34
36
|
start_ms = time.time() * 1000.0
|
|
35
37
|
stop_ms = start_ms + (timeout * 1000.0)
|
|
@@ -54,18 +56,22 @@ def wait_for_ready_state_complete(driver, timeout=settings.LARGE_TIMEOUT):
|
|
|
54
56
|
|
|
55
57
|
|
|
56
58
|
def execute_async_script(driver, script, timeout=settings.LARGE_TIMEOUT):
|
|
57
|
-
driver
|
|
58
|
-
|
|
59
|
+
if hasattr(driver, "set_script_timeout"):
|
|
60
|
+
driver.set_script_timeout(timeout)
|
|
61
|
+
if hasattr(driver, "execute_async_script"):
|
|
62
|
+
return driver.execute_async_script(script)
|
|
63
|
+
else:
|
|
64
|
+
return None
|
|
59
65
|
|
|
60
66
|
|
|
61
67
|
def wait_for_angularjs(driver, timeout=settings.LARGE_TIMEOUT, **kwargs):
|
|
62
|
-
if
|
|
68
|
+
if getattr(settings, "SKIP_JS_WAITS", None):
|
|
63
69
|
return
|
|
64
70
|
with suppress(Exception):
|
|
65
71
|
# This closes pop-up alerts
|
|
66
72
|
execute_script(driver, "")
|
|
67
73
|
if (
|
|
68
|
-
(
|
|
74
|
+
getattr(driver, "_is_using_uc", None)
|
|
69
75
|
or not settings.WAIT_FOR_ANGULARJS
|
|
70
76
|
):
|
|
71
77
|
wait_for_ready_state_complete(driver)
|
|
@@ -205,6 +211,19 @@ def activate_jquery(driver):
|
|
|
205
211
|
try:
|
|
206
212
|
execute_script(driver, "jQuery('html');")
|
|
207
213
|
return
|
|
214
|
+
except TypeError as e:
|
|
215
|
+
if (
|
|
216
|
+
(
|
|
217
|
+
shared_utils.is_cdp_swap_needed(driver)
|
|
218
|
+
or hasattr(driver, "_swap_driver")
|
|
219
|
+
)
|
|
220
|
+
and "cannot unpack non-iterable" in str(e)
|
|
221
|
+
):
|
|
222
|
+
pass
|
|
223
|
+
else:
|
|
224
|
+
if x == 18:
|
|
225
|
+
add_js_link(driver, jquery_js)
|
|
226
|
+
time.sleep(0.1)
|
|
208
227
|
except Exception:
|
|
209
228
|
if x == 18:
|
|
210
229
|
add_js_link(driver, jquery_js)
|
|
@@ -276,6 +295,18 @@ def safe_execute_script(driver, script):
|
|
|
276
295
|
This method will load jQuery if it wasn't already loaded."""
|
|
277
296
|
try:
|
|
278
297
|
execute_script(driver, script)
|
|
298
|
+
except TypeError as e:
|
|
299
|
+
if (
|
|
300
|
+
(
|
|
301
|
+
shared_utils.is_cdp_swap_needed(driver)
|
|
302
|
+
or hasattr(driver, "_swap_driver")
|
|
303
|
+
)
|
|
304
|
+
and "cannot unpack non-iterable" in str(e)
|
|
305
|
+
):
|
|
306
|
+
pass
|
|
307
|
+
else:
|
|
308
|
+
activate_jquery(driver) # It's a good thing we can define it here
|
|
309
|
+
execute_script(driver, script)
|
|
279
310
|
except Exception:
|
|
280
311
|
# The likely reason this fails is because: "jQuery is not defined"
|
|
281
312
|
activate_jquery(driver) # It's a good thing we can define it here
|
|
@@ -868,7 +899,7 @@ def set_messenger_theme(
|
|
|
868
899
|
theme = "future"
|
|
869
900
|
if location == "default":
|
|
870
901
|
location = "bottom_right"
|
|
871
|
-
if
|
|
902
|
+
if getattr(sb_config, "mobile_emulator", None):
|
|
872
903
|
location = "top_center"
|
|
873
904
|
if max_messages == "default":
|
|
874
905
|
max_messages = "8"
|
|
@@ -908,15 +939,16 @@ def set_messenger_theme(
|
|
|
908
939
|
% (max_messages, messenger_location, theme)
|
|
909
940
|
)
|
|
910
941
|
try:
|
|
942
|
+
time.sleep(0.015)
|
|
911
943
|
execute_script(driver, msg_style)
|
|
912
944
|
except Exception:
|
|
913
|
-
time.sleep(0.
|
|
945
|
+
time.sleep(0.035)
|
|
914
946
|
activate_messenger(driver)
|
|
915
|
-
time.sleep(0.
|
|
947
|
+
time.sleep(0.175)
|
|
916
948
|
with suppress(Exception):
|
|
917
949
|
execute_script(driver, msg_style)
|
|
918
|
-
time.sleep(0.
|
|
919
|
-
time.sleep(0.
|
|
950
|
+
time.sleep(0.035)
|
|
951
|
+
time.sleep(0.055)
|
|
920
952
|
|
|
921
953
|
|
|
922
954
|
def post_message(driver, message, msg_dur=None, style="info"):
|
|
@@ -937,7 +969,10 @@ def post_message(driver, message, msg_dur=None, style="info"):
|
|
|
937
969
|
execute_script(driver, messenger_script)
|
|
938
970
|
except TypeError as e:
|
|
939
971
|
if (
|
|
940
|
-
|
|
972
|
+
(
|
|
973
|
+
shared_utils.is_cdp_swap_needed(driver)
|
|
974
|
+
or hasattr(driver, "_swap_driver")
|
|
975
|
+
)
|
|
941
976
|
and "cannot unpack non-iterable" in str(e)
|
|
942
977
|
):
|
|
943
978
|
pass
|
|
@@ -968,7 +1003,7 @@ def post_messenger_success_message(driver, message, msg_dur=None):
|
|
|
968
1003
|
with suppress(Exception):
|
|
969
1004
|
theme = "future"
|
|
970
1005
|
location = "bottom_right"
|
|
971
|
-
if
|
|
1006
|
+
if getattr(sb_config, "mobile_emulator", None):
|
|
972
1007
|
location = "top_right"
|
|
973
1008
|
set_messenger_theme(driver, theme=theme, location=location)
|
|
974
1009
|
post_message(driver, message, msg_dur, style="success")
|
|
@@ -1260,10 +1295,16 @@ def scroll_to_element(driver, element):
|
|
|
1260
1295
|
return False
|
|
1261
1296
|
try:
|
|
1262
1297
|
element_location_x = element.location["x"]
|
|
1298
|
+
except Exception:
|
|
1299
|
+
element_location_x = 0
|
|
1300
|
+
try:
|
|
1263
1301
|
element_width = element.size["width"]
|
|
1302
|
+
except Exception:
|
|
1303
|
+
element_width = 0
|
|
1304
|
+
try:
|
|
1264
1305
|
screen_width = driver.get_window_size()["width"]
|
|
1265
1306
|
except Exception:
|
|
1266
|
-
|
|
1307
|
+
screen_width = execute_script(driver, "return window.innerWidth;")
|
|
1267
1308
|
element_location_y = element_location_y - constants.Scroll.Y_OFFSET
|
|
1268
1309
|
if element_location_y < 0:
|
|
1269
1310
|
element_location_y = 0
|
|
@@ -1295,18 +1336,36 @@ def slow_scroll_to_element(driver, element, *args, **kwargs):
|
|
|
1295
1336
|
element_location_y = None
|
|
1296
1337
|
try:
|
|
1297
1338
|
if shared_utils.is_cdp_swap_needed(driver):
|
|
1298
|
-
element.get_position().y
|
|
1339
|
+
element_location_y = element.get_position().y
|
|
1299
1340
|
else:
|
|
1300
1341
|
element_location_y = element.location["y"]
|
|
1301
1342
|
except Exception:
|
|
1302
|
-
|
|
1343
|
+
if shared_utils.is_cdp_swap_needed(driver):
|
|
1344
|
+
element.scroll_into_view()
|
|
1345
|
+
else:
|
|
1346
|
+
element.location_once_scrolled_into_view
|
|
1303
1347
|
return
|
|
1304
1348
|
try:
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1349
|
+
if shared_utils.is_cdp_swap_needed(driver):
|
|
1350
|
+
element_location_x = element.get_position().x
|
|
1351
|
+
else:
|
|
1352
|
+
element_location_x = element.location["x"]
|
|
1308
1353
|
except Exception:
|
|
1309
1354
|
element_location_x = 0
|
|
1355
|
+
try:
|
|
1356
|
+
if shared_utils.is_cdp_swap_needed(driver):
|
|
1357
|
+
element_width = element.get_position().width
|
|
1358
|
+
else:
|
|
1359
|
+
element_width = element.size["width"]
|
|
1360
|
+
except Exception:
|
|
1361
|
+
element_width = 0
|
|
1362
|
+
try:
|
|
1363
|
+
if shared_utils.is_cdp_swap_needed(driver):
|
|
1364
|
+
screen_width = driver.cdp.get_window_size()["width"]
|
|
1365
|
+
else:
|
|
1366
|
+
screen_width = driver.get_window_size()["width"]
|
|
1367
|
+
except Exception:
|
|
1368
|
+
screen_width = execute_script(driver, "return window.innerWidth;")
|
|
1310
1369
|
element_location_y = element_location_y - constants.Scroll.Y_OFFSET
|
|
1311
1370
|
if element_location_y < 0:
|
|
1312
1371
|
element_location_y = 0
|
|
@@ -17,11 +17,10 @@ By.XPATH # "xpath"
|
|
|
17
17
|
By.TAG_NAME # "tag name"
|
|
18
18
|
By.PARTIAL_LINK_TEXT # "partial link text"
|
|
19
19
|
"""
|
|
20
|
-
import codecs
|
|
21
|
-
import fasteners
|
|
22
20
|
import os
|
|
23
21
|
import time
|
|
24
22
|
from contextlib import suppress
|
|
23
|
+
from filelock import FileLock
|
|
25
24
|
from selenium.common.exceptions import ElementNotInteractableException
|
|
26
25
|
from selenium.common.exceptions import ElementNotVisibleException
|
|
27
26
|
from selenium.common.exceptions import NoAlertPresentException
|
|
@@ -193,6 +192,10 @@ def is_attribute_present(
|
|
|
193
192
|
@Returns
|
|
194
193
|
Boolean (is attribute present)
|
|
195
194
|
"""
|
|
195
|
+
if __is_cdp_swap_needed(driver):
|
|
196
|
+
return driver.cdp.is_attribute_present(
|
|
197
|
+
selector, attribute, value=value
|
|
198
|
+
)
|
|
196
199
|
_reconnect_if_disconnected(driver)
|
|
197
200
|
try:
|
|
198
201
|
element = driver.find_element(by=by, value=selector)
|
|
@@ -1108,6 +1111,14 @@ def wait_for_element_absent(
|
|
|
1108
1111
|
timeout - the time to wait for elements in seconds
|
|
1109
1112
|
original_selector - handle pre-converted ":contains(TEXT)" selector
|
|
1110
1113
|
"""
|
|
1114
|
+
if __is_cdp_swap_needed(driver):
|
|
1115
|
+
if page_utils.is_valid_by(by):
|
|
1116
|
+
original_selector = selector
|
|
1117
|
+
elif page_utils.is_valid_by(selector):
|
|
1118
|
+
original_selector = by
|
|
1119
|
+
selector, by = page_utils.recalculate_selector(original_selector, by)
|
|
1120
|
+
driver.cdp.wait_for_element_absent(selector)
|
|
1121
|
+
return True
|
|
1111
1122
|
_reconnect_if_disconnected(driver)
|
|
1112
1123
|
start_ms = time.time() * 1000.0
|
|
1113
1124
|
stop_ms = start_ms + (timeout * 1000.0)
|
|
@@ -1156,6 +1167,14 @@ def wait_for_element_not_visible(
|
|
|
1156
1167
|
timeout - the time to wait for the element in seconds
|
|
1157
1168
|
original_selector - handle pre-converted ":contains(TEXT)" selector
|
|
1158
1169
|
"""
|
|
1170
|
+
if __is_cdp_swap_needed(driver):
|
|
1171
|
+
if page_utils.is_valid_by(by):
|
|
1172
|
+
original_selector = selector
|
|
1173
|
+
elif page_utils.is_valid_by(selector):
|
|
1174
|
+
original_selector = by
|
|
1175
|
+
selector, by = page_utils.recalculate_selector(original_selector, by)
|
|
1176
|
+
driver.cdp.wait_for_element_not_visible(selector)
|
|
1177
|
+
return True
|
|
1159
1178
|
_reconnect_if_disconnected(driver)
|
|
1160
1179
|
start_ms = time.time() * 1000.0
|
|
1161
1180
|
stop_ms = start_ms + (timeout * 1000.0)
|
|
@@ -1508,10 +1527,10 @@ def save_page_source(driver, name, folder=None):
|
|
|
1508
1527
|
page_source = driver.cdp.get_page_source()
|
|
1509
1528
|
else:
|
|
1510
1529
|
page_source = driver.page_source
|
|
1511
|
-
html_file = codecs.open(html_file_path, "w+", "utf-8")
|
|
1512
1530
|
rendered_source = log_helper.get_html_source_with_base_href(
|
|
1513
1531
|
driver, page_source
|
|
1514
1532
|
)
|
|
1533
|
+
html_file = open(html_file_path, mode="w+", encoding="utf-8")
|
|
1515
1534
|
html_file.write(rendered_source)
|
|
1516
1535
|
html_file.close()
|
|
1517
1536
|
|
|
@@ -1627,14 +1646,8 @@ def switch_to_frame(
|
|
|
1627
1646
|
|
|
1628
1647
|
|
|
1629
1648
|
def __switch_to_window(driver, window_handle, uc_lock=True):
|
|
1630
|
-
if (
|
|
1631
|
-
|
|
1632
|
-
and driver._is_using_uc
|
|
1633
|
-
and uc_lock
|
|
1634
|
-
):
|
|
1635
|
-
gui_lock = fasteners.InterProcessLock(
|
|
1636
|
-
constants.MultiBrowser.PYAUTOGUILOCK
|
|
1637
|
-
)
|
|
1649
|
+
if getattr(driver, "_is_using_uc", None) and uc_lock:
|
|
1650
|
+
gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
|
|
1638
1651
|
with gui_lock:
|
|
1639
1652
|
driver.switch_to.window(window_handle)
|
|
1640
1653
|
else:
|
|
@@ -1717,8 +1730,7 @@ def switch_to_window(
|
|
|
1717
1730
|
|
|
1718
1731
|
def _reconnect_if_disconnected(driver):
|
|
1719
1732
|
if (
|
|
1720
|
-
|
|
1721
|
-
and driver._is_using_uc
|
|
1733
|
+
getattr(driver, "_is_using_uc", None)
|
|
1722
1734
|
and hasattr(driver, "is_connected")
|
|
1723
1735
|
and not driver.is_connected()
|
|
1724
1736
|
):
|
|
@@ -1739,6 +1751,22 @@ def open_url(driver, url):
|
|
|
1739
1751
|
if __is_cdp_swap_needed(driver):
|
|
1740
1752
|
driver.cdp.open(url)
|
|
1741
1753
|
return
|
|
1754
|
+
elif (
|
|
1755
|
+
getattr(driver, "_is_using_uc", None)
|
|
1756
|
+
# and getattr(driver, "_is_using_auth", None)
|
|
1757
|
+
and not getattr(driver, "_is_using_cdp", None)
|
|
1758
|
+
):
|
|
1759
|
+
# Auth in UC Mode requires CDP Mode
|
|
1760
|
+
# (and now we're always forcing it)
|
|
1761
|
+
driver.uc_activate_cdp_mode(url)
|
|
1762
|
+
return
|
|
1763
|
+
elif (
|
|
1764
|
+
getattr(driver, "_is_using_uc", None)
|
|
1765
|
+
and getattr(driver, "_is_using_cdp", None)
|
|
1766
|
+
):
|
|
1767
|
+
driver.disconnect()
|
|
1768
|
+
driver.cdp.open(url)
|
|
1769
|
+
return
|
|
1742
1770
|
url = str(url).strip() # Remove leading and trailing whitespace
|
|
1743
1771
|
if not page_utils.looks_like_a_page_url(url):
|
|
1744
1772
|
if page_utils.is_valid_url("https://" + url):
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
"""This module contains useful utility methods"""
|
|
2
|
-
import codecs
|
|
3
2
|
import fasteners
|
|
4
3
|
import os
|
|
5
4
|
import re
|
|
@@ -110,10 +109,10 @@ def looks_like_a_page_url(url):
|
|
|
110
109
|
navigate to the page if a URL is detected, but will instead call
|
|
111
110
|
self.get_element(URL_AS_A_SELECTOR) if the input is not a URL."""
|
|
112
111
|
return url.startswith((
|
|
113
|
-
"http:", "https:", "://", "about:", "blob:", "chrome:",
|
|
112
|
+
"http:", "https:", "://", "about:", "blob:", "chrome:", "opera:",
|
|
114
113
|
"data:", "edge:", "file:", "view-source:", "chrome-search:",
|
|
115
114
|
"chrome-extension:", "chrome-untrusted:", "isolated-app:",
|
|
116
|
-
"chrome-devtools:", "devtools:"
|
|
115
|
+
"chrome-devtools:", "devtools:", "brave:", "comet:", "atlas:"
|
|
117
116
|
))
|
|
118
117
|
|
|
119
118
|
|
|
@@ -294,18 +293,20 @@ def _print_unique_links_with_status_codes(page_url, soup):
|
|
|
294
293
|
print(link, " -> ", status_code)
|
|
295
294
|
|
|
296
295
|
|
|
297
|
-
def _download_file_to(
|
|
296
|
+
def _download_file_to(
|
|
297
|
+
file_url, destination_folder, new_file_name=None, headers=None
|
|
298
|
+
):
|
|
298
299
|
if new_file_name:
|
|
299
300
|
file_name = new_file_name
|
|
300
301
|
else:
|
|
301
302
|
file_name = file_url.split("/")[-1]
|
|
302
|
-
r = requests.get(file_url, timeout=5)
|
|
303
|
+
r = requests.get(file_url, headers=headers, timeout=5)
|
|
303
304
|
file_path = os.path.join(destination_folder, file_name)
|
|
304
305
|
download_file_lock = fasteners.InterProcessLock(
|
|
305
306
|
constants.MultiBrowser.DOWNLOAD_FILE_LOCK
|
|
306
307
|
)
|
|
307
308
|
with download_file_lock:
|
|
308
|
-
with open(file_path, "wb") as code:
|
|
309
|
+
with open(file_path, mode="wb") as code:
|
|
309
310
|
code.write(r.content)
|
|
310
311
|
|
|
311
312
|
|
|
@@ -314,8 +315,10 @@ def _save_data_as(data, destination_folder, file_name):
|
|
|
314
315
|
constants.MultiBrowser.FILE_IO_LOCK
|
|
315
316
|
)
|
|
316
317
|
with file_io_lock:
|
|
317
|
-
out_file =
|
|
318
|
-
os.path.join(destination_folder, file_name),
|
|
318
|
+
out_file = open(
|
|
319
|
+
os.path.join(destination_folder, file_name),
|
|
320
|
+
mode="w+",
|
|
321
|
+
encoding="utf-8",
|
|
319
322
|
)
|
|
320
323
|
out_file.writelines(data)
|
|
321
324
|
out_file.close()
|
|
@@ -328,12 +331,16 @@ def _append_data_to_file(data, destination_folder, file_name):
|
|
|
328
331
|
with file_io_lock:
|
|
329
332
|
existing_data = ""
|
|
330
333
|
if os.path.exists(os.path.join(destination_folder, file_name)):
|
|
331
|
-
with open(
|
|
334
|
+
with open(
|
|
335
|
+
os.path.join(destination_folder, file_name), mode="r"
|
|
336
|
+
) as f:
|
|
332
337
|
existing_data = f.read()
|
|
333
338
|
if not existing_data.split("\n")[-1] == "":
|
|
334
339
|
existing_data += "\n"
|
|
335
|
-
out_file =
|
|
336
|
-
os.path.join(destination_folder, file_name),
|
|
340
|
+
out_file = open(
|
|
341
|
+
os.path.join(destination_folder, file_name),
|
|
342
|
+
mode="w+",
|
|
343
|
+
encoding="utf-8",
|
|
337
344
|
)
|
|
338
345
|
out_file.writelines("%s%s" % (existing_data, data))
|
|
339
346
|
out_file.close()
|
|
@@ -346,7 +353,7 @@ def _get_file_data(folder, file_name):
|
|
|
346
353
|
with file_io_lock:
|
|
347
354
|
if not os.path.exists(os.path.join(folder, file_name)):
|
|
348
355
|
raise Exception("File not found!")
|
|
349
|
-
with open(os.path.join(folder, file_name), "r") as f:
|
|
356
|
+
with open(os.path.join(folder, file_name), mode="r") as f:
|
|
350
357
|
data = f.read()
|
|
351
358
|
return data
|
|
352
359
|
|
|
@@ -7,6 +7,7 @@ import sys
|
|
|
7
7
|
import time
|
|
8
8
|
from contextlib import suppress
|
|
9
9
|
from seleniumbase import config as sb_config
|
|
10
|
+
from seleniumbase.config import settings
|
|
10
11
|
from seleniumbase.fixtures import constants
|
|
11
12
|
|
|
12
13
|
|
|
@@ -17,20 +18,79 @@ def pip_install(package, version=None):
|
|
|
17
18
|
pip_install_lock = fasteners.InterProcessLock(
|
|
18
19
|
constants.PipInstall.LOCKFILE
|
|
19
20
|
)
|
|
21
|
+
upgrade_to_latest = False
|
|
22
|
+
if (
|
|
23
|
+
version
|
|
24
|
+
and ("U" in str(version).upper() or "L" in str(version).upper())
|
|
25
|
+
):
|
|
26
|
+
# Upgrade to Latest when specified with "U" or "L"
|
|
27
|
+
upgrade_to_latest = True
|
|
20
28
|
with pip_install_lock:
|
|
21
29
|
if not version:
|
|
22
30
|
subprocess.check_call(
|
|
23
31
|
[sys.executable, "-m", "pip", "install", package]
|
|
24
32
|
)
|
|
25
|
-
|
|
33
|
+
elif not upgrade_to_latest:
|
|
26
34
|
package_and_version = package + "==" + str(version)
|
|
27
35
|
subprocess.check_call(
|
|
28
36
|
[sys.executable, "-m", "pip", "install", package_and_version]
|
|
29
37
|
)
|
|
38
|
+
else:
|
|
39
|
+
subprocess.check_call(
|
|
40
|
+
[sys.executable, "-m", "pip", "install", "-U", package]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def make_version_list(version_str):
|
|
45
|
+
return [int(i) for i in version_str.split(".") if i.isdigit()]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def make_version_tuple(version_str):
|
|
49
|
+
return tuple(make_version_list(version_str))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_mfa_code(totp_key=None):
|
|
53
|
+
"""Returns a time-based one-time password based on the
|
|
54
|
+
Google Authenticator algorithm for multi-factor authentication.
|
|
55
|
+
If the "totp_key" is not specified, this method defaults
|
|
56
|
+
to using the one provided in [seleniumbase/config/settings.py].
|
|
57
|
+
Google Authenticator codes expire & change at 30-sec intervals.
|
|
58
|
+
If the fetched password expires in the next 1.2 seconds, waits
|
|
59
|
+
for a new one before returning it (may take up to 1.2 seconds).
|
|
60
|
+
See https://pyotp.readthedocs.io/en/latest/ for details."""
|
|
61
|
+
import pyotp
|
|
62
|
+
|
|
63
|
+
if not totp_key:
|
|
64
|
+
totp_key = settings.TOTP_KEY
|
|
65
|
+
epoch_interval = time.time() / 30.0
|
|
66
|
+
cycle_lifespan = float(epoch_interval) - int(epoch_interval)
|
|
67
|
+
if float(cycle_lifespan) > 0.96:
|
|
68
|
+
# Password expires in the next 1.2 seconds. Wait for a new one.
|
|
69
|
+
for i in range(30):
|
|
70
|
+
time.sleep(0.04)
|
|
71
|
+
epoch_interval = time.time() / 30.0
|
|
72
|
+
cycle_lifespan = float(epoch_interval) - int(epoch_interval)
|
|
73
|
+
if not float(cycle_lifespan) > 0.96:
|
|
74
|
+
# The new password cycle has begun
|
|
75
|
+
break
|
|
76
|
+
totp = pyotp.TOTP(totp_key)
|
|
77
|
+
return str(totp.now())
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_arm_linux():
|
|
81
|
+
"""Returns True if machine is ARM Linux.
|
|
82
|
+
This will be useful once Google adds
|
|
83
|
+
support for ARM Linux ChromeDriver.
|
|
84
|
+
(Raspberry Pi uses ARM architecture.)"""
|
|
85
|
+
return (
|
|
86
|
+
platform.system() == "Linux"
|
|
87
|
+
and platform.machine() == "aarch64"
|
|
88
|
+
)
|
|
30
89
|
|
|
31
90
|
|
|
32
91
|
def is_arm_mac():
|
|
33
|
-
"""
|
|
92
|
+
"""Returns True if machine is ARM Mac.
|
|
93
|
+
(Eg. M1 / M2 Macs use ARM processors)"""
|
|
34
94
|
return (
|
|
35
95
|
"darwin" in sys.platform
|
|
36
96
|
and (
|
|
@@ -87,8 +147,7 @@ def fix_url_as_needed(url):
|
|
|
87
147
|
|
|
88
148
|
def reconnect_if_disconnected(driver):
|
|
89
149
|
if (
|
|
90
|
-
|
|
91
|
-
and driver._is_using_uc
|
|
150
|
+
getattr(driver, "_is_using_uc", None)
|
|
92
151
|
and hasattr(driver, "is_connected")
|
|
93
152
|
and not driver.is_connected()
|
|
94
153
|
):
|
|
@@ -238,8 +297,7 @@ def __time_limit_exceeded(message):
|
|
|
238
297
|
|
|
239
298
|
def check_if_time_limit_exceeded():
|
|
240
299
|
if (
|
|
241
|
-
|
|
242
|
-
and sb_config.time_limit
|
|
300
|
+
getattr(sb_config, "time_limit", None)
|
|
243
301
|
and not sb_config.recorder_mode
|
|
244
302
|
):
|
|
245
303
|
time_limit = sb_config.time_limit
|
|
@@ -11,6 +11,11 @@ from seleniumbase.config import settings
|
|
|
11
11
|
from seleniumbase.fixtures import js_utils
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
python3_11_or_newer = False
|
|
15
|
+
if sys.version_info >= (3, 11):
|
|
16
|
+
python3_11_or_newer = True
|
|
17
|
+
|
|
18
|
+
|
|
14
19
|
class MasterQA(BaseCase):
|
|
15
20
|
def setUp(self):
|
|
16
21
|
self.check_count = 0
|
|
@@ -309,8 +314,17 @@ class MasterQA(BaseCase):
|
|
|
309
314
|
if hasattr(sys, "last_traceback") and sys.last_traceback is not None:
|
|
310
315
|
has_exception = True
|
|
311
316
|
elif hasattr(self, "_outcome"):
|
|
312
|
-
if hasattr(self._outcome, "errors")
|
|
313
|
-
|
|
317
|
+
if hasattr(self._outcome, "errors"):
|
|
318
|
+
if python3_11_or_newer:
|
|
319
|
+
if (
|
|
320
|
+
self._outcome.errors
|
|
321
|
+
and self._outcome.errors[-1]
|
|
322
|
+
and self._outcome.errors[-1][1]
|
|
323
|
+
):
|
|
324
|
+
has_exception = True
|
|
325
|
+
else:
|
|
326
|
+
if self._outcome.errors:
|
|
327
|
+
has_exception = True
|
|
314
328
|
else:
|
|
315
329
|
has_exception = sys.exc_info()[1] is not None
|
|
316
330
|
return has_exception
|
|
@@ -240,14 +240,22 @@ class Base(Plugin):
|
|
|
240
240
|
test.test.test_id = test.id()
|
|
241
241
|
test.test.is_nosetest = True
|
|
242
242
|
test.test.environment = self.options.environment
|
|
243
|
+
sb_config.environment = self.options.environment
|
|
243
244
|
test.test.env = self.options.environment # Add a shortened version
|
|
244
245
|
test.test.account = self.options.account
|
|
246
|
+
sb_config.account = self.options.account
|
|
245
247
|
test.test.data = self.options.data
|
|
248
|
+
sb_config.data = self.options.data
|
|
246
249
|
test.test.var1 = self.options.var1
|
|
250
|
+
sb_config.var1 = self.options.var1
|
|
247
251
|
test.test.var2 = self.options.var2
|
|
252
|
+
sb_config.var2 = self.options.var2
|
|
248
253
|
test.test.var3 = self.options.var3
|
|
254
|
+
sb_config.var3 = self.options.var3
|
|
249
255
|
test.test.variables = variables # Already verified is a dictionary
|
|
256
|
+
sb_config.variables = variables
|
|
250
257
|
test.test.settings_file = self.options.settings_file
|
|
258
|
+
sb_config.settings_file = self.options.settings_file
|
|
251
259
|
test.test._final_debug = self.options.final_debug
|
|
252
260
|
test.test.log_path = self.options.log_path
|
|
253
261
|
if self.options.archive_downloads:
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Test Info Plugin for SeleniumBase tests that run with pynose / nosetests"""
|
|
2
2
|
import os
|
|
3
|
-
import codecs
|
|
4
3
|
import time
|
|
5
4
|
import traceback
|
|
6
5
|
from nose.plugins import Plugin
|
|
@@ -26,7 +25,7 @@ class BasicTestInfo(Plugin):
|
|
|
26
25
|
if not os.path.exists(test_logpath):
|
|
27
26
|
os.makedirs(test_logpath)
|
|
28
27
|
file_name = "%s/%s" % (test_logpath, self.logfile_name)
|
|
29
|
-
basic_info_file =
|
|
28
|
+
basic_info_file = open(file_name, mode="w+", encoding="utf-8")
|
|
30
29
|
self.__log_test_error_data(basic_info_file, test, err, "Error")
|
|
31
30
|
basic_info_file.close()
|
|
32
31
|
|
|
@@ -35,7 +34,7 @@ class BasicTestInfo(Plugin):
|
|
|
35
34
|
if not os.path.exists(test_logpath):
|
|
36
35
|
os.makedirs(test_logpath)
|
|
37
36
|
file_name = "%s/%s" % (test_logpath, self.logfile_name)
|
|
38
|
-
basic_info_file =
|
|
37
|
+
basic_info_file = open(file_name, mode="w+", encoding="utf-8")
|
|
39
38
|
self.__log_test_error_data(basic_info_file, test, err, "Error")
|
|
40
39
|
basic_info_file.close()
|
|
41
40
|
|