Skip to content

Commit e7da760

Browse files
authored
Merge pull request #2577 from seleniumbase/better-pytest-collection-and-more
Improve pytest collection and more
2 parents e6a4477 + 015db5c commit e7da760

14 files changed

+163
-62
lines changed

examples/presenter/multi_uc.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@
77

88
@pytest.mark.parametrize("", [[]] * 3)
99
def test_multi_threaded(sb):
10-
sb.driver.uc_open_with_reconnect("https://top.gg/", 5)
10+
url = "https://gitlab.com/users/sign_in"
11+
sb.driver.uc_open_with_reconnect(url, 3)
1112
sb.set_window_rect(randint(0, 755), randint(38, 403), 700, 500)
1213
try:
13-
sb.assert_text("Discord Bots", "h1", timeout=2)
14-
sb.post_message("Selenium wasn't detected!", duration=4)
14+
sb.assert_text("Username", '[for="user_login"]', timeout=3)
15+
sb.post_message("SeleniumBase wasn't detected", duration=4)
1516
sb._print("\n Success! Website did not detect Selenium! ")
1617
except Exception:
17-
sb.driver.uc_open_with_reconnect("https://top.gg/", 5)
18+
sb.driver.uc_open_with_reconnect(url, 3)
1819
try:
19-
sb.assert_text("Discord Bots", "h1", timeout=2)
20-
sb.post_message("Selenium wasn't detected!", duration=4)
20+
sb.assert_text("Username", '[for="user_login"]', timeout=3)
21+
sb.post_message("SeleniumBase wasn't detected", duration=4)
2122
sb._print("\n Success! Website did not detect Selenium! ")
2223
except Exception:
2324
sb.fail('Selenium was detected! Try using: "pytest --uc"')

examples/presenter/uc_presentation.py

+7-10
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,16 @@ def test_presentation(self):
2828
self.begin_presentation(filename="uc_presentation.html")
2929

3030
self.get_new_driver(undetectable=True)
31+
url = "https://gitlab.com/users/sign_in"
3132
try:
32-
self.driver.uc_open_with_reconnect(
33-
"https://top.gg/", reconnect_time=4
34-
)
33+
self.driver.uc_open_with_reconnect(url, reconnect_time=3)
3534
try:
36-
self.assert_text("Discord Bots", "h1", timeout=3)
37-
self.post_message("Selenium wasn't detected!", duration=4)
35+
self.assert_text("Username", '[for="user_login"]', timeout=3)
36+
self.post_message("SeleniumBase wasn't detected", duration=4)
3837
except Exception:
39-
self.driver.uc_open_with_reconnect(
40-
"https://top.gg/", reconnect_time=5
41-
)
42-
self.assert_text("Discord Bots", "h1", timeout=2)
43-
self.post_message("Selenium wasn't detected!", duration=4)
38+
self.driver.uc_open_with_reconnect(url, reconnect_time=4)
39+
self.assert_text("Username", '[for="user_login"]', timeout=3)
40+
self.post_message("SeleniumBase wasn't detected", duration=4)
4441
finally:
4542
self.quit_extra_driver()
4643

examples/raw_call.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
import pytest
55
import subprocess
66

7-
pytest.main(["test_coffee_cart.py", "--chrome", "-v"])
8-
subprocess.call(["pytest", "test_mfa_login.py", "--chrome", "-v"])
7+
if __name__ == "__main__":
8+
pytest.main(["test_coffee_cart.py", "--chrome", "-v"])
9+
subprocess.call(["pytest", "test_mfa_login.py", "--chrome", "-v"])

examples/raw_multi_drivers.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import sys
2+
import threading
3+
from concurrent.futures import ThreadPoolExecutor
4+
from random import randint, seed
5+
from seleniumbase import Driver
6+
sys.argv.append("-n") # Tell SeleniumBase to do thread-locking as needed
7+
8+
9+
def launch_driver(url):
10+
seed(len(threading.enumerate())) # Random seed for browser placement
11+
driver = Driver()
12+
try:
13+
driver.set_window_rect(randint(4, 720), randint(8, 410), 700, 500)
14+
driver.get(url=url)
15+
if driver.is_element_visible("h1"):
16+
driver.highlight("h1", loops=9)
17+
else:
18+
driver.sleep(2.2)
19+
finally:
20+
driver.quit()
21+
22+
23+
if __name__ == "__main__":
24+
urls = ['https://seleniumbase.io/demo_page' for i in range(4)]
25+
with ThreadPoolExecutor(max_workers=len(urls)) as executor:
26+
for url in urls:
27+
executor.submit(launch_driver, url)

examples/raw_turnstile.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ def click_turnstile_and_verify(sb):
2121
open_the_turnstile_page(sb)
2222
click_turnstile_and_verify(sb)
2323
sb.set_messenger_theme(location="top_left")
24-
sb.post_message("Selenium wasn't detected!", duration=3)
24+
sb.post_message("SeleniumBase wasn't detected", duration=3)

examples/raw_uc_mode.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
from seleniumbase import SB
33

44
with SB(uc=True, test=True) as sb:
5-
sb.driver.uc_open_with_reconnect("https://top.gg/", 5)
6-
if not sb.is_text_visible("Discord Bots", "h1"):
7-
sb.driver.uc_open_with_reconnect("https://top.gg/", 5)
8-
sb.assert_text("Discord Bots", "h1", timeout=3)
9-
sb.highlight("h1", loops=3)
10-
sb.set_messenger_theme(location="top_center")
11-
sb.post_message("Selenium wasn't detected!", duration=3)
5+
url = "https://gitlab.com/users/sign_in"
6+
sb.driver.uc_open_with_reconnect(url, 3)
7+
if not sb.is_text_visible("Username", '[for="user_login"]'):
8+
sb.driver.uc_open_with_reconnect(url, 4)
9+
sb.assert_text("Username", '[for="user_login"]', timeout=3)
10+
sb.highlight('label[for="user_login"]', loops=3)
11+
sb.post_message("SeleniumBase wasn't detected", duration=4)

examples/verify_undetected.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77

88
class UndetectedTest(BaseCase):
99
def test_browser_is_undetected(self):
10+
url = "https://gitlab.com/users/sign_in"
1011
if not self.undetectable:
1112
self.get_new_driver(undetectable=True)
12-
self.driver.uc_open_with_reconnect("https://top.gg/", 5)
13-
if not self.is_text_visible("Discord Bots", "h1"):
13+
self.driver.uc_open_with_reconnect(url, 3)
14+
if not self.is_text_visible("Username", '[for="user_login"]'):
1415
self.get_new_driver(undetectable=True)
15-
self.driver.uc_open_with_reconnect("https://top.gg/", 5)
16-
self.assert_text("Discord Bots", "h1", timeout=3)
17-
self.set_messenger_theme(location="top_center")
18-
self.post_message("Selenium wasn't detected!", duration=2.8)
16+
self.driver.uc_open_with_reconnect(url, 4)
17+
self.assert_text("Username", '[for="user_login"]', timeout=3)
18+
self.post_message("SeleniumBase wasn't detected", duration=4)
1919
self._print("\n Success! Website did not detect Selenium! ")

help_docs/uc_mode.md

+50-14
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from seleniumbase import Driver
2626

2727
driver = Driver(uc=True)
28-
driver.uc_open_with_reconnect("https://top.gg/", 6)
28+
driver.uc_open_with_reconnect("https://gitlab.com/users/sign_in", 3)
2929
driver.quit()
3030
```
3131

@@ -35,7 +35,7 @@ driver.quit()
3535
from seleniumbase import SB
3636

3737
with SB(uc=True) as sb:
38-
sb.driver.uc_open_with_reconnect("https://top.gg/", 6)
38+
sb.driver.uc_open_with_reconnect("https://gitlab.com/users/sign_in", 3)
3939
```
4040

4141
👤 Here's a longer example, which includes a retry if the CAPTCHA isn't bypassed on the first attempt:
@@ -44,13 +44,13 @@ with SB(uc=True) as sb:
4444
from seleniumbase import SB
4545

4646
with SB(uc=True, test=True) as sb:
47-
sb.driver.uc_open_with_reconnect("https://top.gg/", 5)
48-
if not sb.is_text_visible("Discord Bots", "h1"):
49-
sb.driver.uc_open_with_reconnect("https://top.gg/", 5)
50-
sb.assert_text("Discord Bots", "h1", timeout=3)
51-
sb.highlight("h1", loops=3)
52-
sb.set_messenger_theme(location="top_center")
53-
sb.post_message("Selenium wasn't detected!", duration=3)
47+
url = "https://gitlab.com/users/sign_in"
48+
sb.driver.uc_open_with_reconnect(url, 3)
49+
if not sb.is_text_visible("Username", '[for="user_login"]'):
50+
sb.driver.uc_open_with_reconnect(url, 4)
51+
sb.assert_text("Username", '[for="user_login"]', timeout=3)
52+
sb.highlight('label[for="user_login"]', loops=3)
53+
sb.post_message("SeleniumBase wasn't detected", duration=4)
5454
```
5555

5656
👤 Here's an example where clicking the checkbox is required, even for humans: (Commonly seen with forms that are CAPTCHA-protected.)
@@ -76,7 +76,7 @@ with SB(uc=True, test=True) as sb:
7676
open_the_turnstile_page(sb)
7777
click_turnstile_and_verify(sb)
7878
sb.set_messenger_theme(location="top_left")
79-
sb.post_message("Selenium wasn't detected!", duration=3)
79+
sb.post_message("SeleniumBase wasn't detected", duration=3)
8080
```
8181

8282
### 👤 Here are some examples that use UC Mode:
@@ -117,8 +117,9 @@ driver.default_get(url) # Faster, but Selenium can be detected
117117
👤 Here are some examples of using those special UC Mode methods: (Use `self.driver` for `BaseCase` formats. Use `sb.driver` for `SB()` formats):
118118

119119
```python
120-
driver.uc_open_with_reconnect("https://top.gg/", reconnect_time=5)
121-
driver.uc_open_with_reconnect("https://top.gg/", 5)
120+
url = "https://gitlab.com/users/sign_in"
121+
driver.uc_open_with_reconnect(url, reconnect_time=3)
122+
driver.uc_open_with_reconnect(url, 3)
122123

123124
driver.reconnect(5)
124125
driver.reconnect(timeout=5)
@@ -127,8 +128,9 @@ driver.reconnect(timeout=5)
127128
👤 You can also set the `reconnect_time` / `timeout` to `"breakpoint"` as a valid option. This allows the user to perform manual actions (until typing `c` and pressing ENTER to continue from the breakpoint):
128129

129130
```python
130-
driver.uc_open_with_reconnect("https://top.gg/", reconnect_time="breakpoint")
131-
driver.uc_open_with_reconnect("https://top.gg/", "breakpoint")
131+
url = "https://gitlab.com/users/sign_in"
132+
driver.uc_open_with_reconnect(url, reconnect_time="breakpoint")
133+
driver.uc_open_with_reconnect(url, "breakpoint")
132134

133135
driver.reconnect(timeout="breakpoint")
134136
driver.reconnect("breakpoint")
@@ -150,3 +152,37 @@ with SB(uc=True) as sb:
150152
```
151153

152154
(If you remain undetected while loading the page and performing manual actions, then you know you can create a working script once you swap the breakpoint with a time, and add special methods like `uc_click` as needed.)
155+
156+
👤 <b>Multithreaded UC Mode:</b>
157+
158+
If you're using `pytest` for multithreaded UC Mode (which requires using one of the `pytest` [syntax formats](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/syntax_formats.md)), then all you have to do is set the number of threads when your script runs. (`-n NUM`) Eg:
159+
160+
```bash
161+
pytest --uc -n 4
162+
```
163+
164+
(Then `pytest-xdist` is automatically used to spin up and process the threads.)
165+
166+
If you don't want to use `pytest` for multithreading, then you'll need to do a little more work. That involves using a different multithreading library, (eg. `concurrent.futures`), and making sure that thread-locking is done correctly for processes that share resources. To handle that thread-locking, include `sys.argv.append("-n")` in your SeleniumBase file.
167+
168+
Here's a sample script that uses `concurrent.futures` for spinning up multiple processes:
169+
170+
```python
171+
import sys
172+
from concurrent.futures import ThreadPoolExecutor
173+
from seleniumbase import Driver
174+
sys.argv.append("-n") # Tell SeleniumBase to do thread-locking as needed
175+
176+
def launch_driver(url):
177+
driver = Driver(uc=True)
178+
try:
179+
driver.get(url=url)
180+
driver.sleep(2)
181+
finally:
182+
driver.quit()
183+
184+
urls = ['https://seleniumbase.io/demo_page' for i in range(3)]
185+
with ThreadPoolExecutor(max_workers=len(urls)) as executor:
186+
for url in urls:
187+
executor.submit(launch_driver, url)
188+
```

mkdocs_build/requirements.txt

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
# Minimum Python version: 3.8 (for generating docs only)
33

44
regex>=2023.12.25
5-
pymdown-extensions>=10.7
6-
pipdeptree>=2.15.1
5+
pymdown-extensions>=10.7.1
6+
pipdeptree>=2.16.1
77
python-dateutil>=2.8.2
88
Markdown==3.5.2
99
markdown2==2.4.13
@@ -20,7 +20,7 @@ lxml==5.1.0
2020
pyquery==2.0.0
2121
readtime==3.0.0
2222
mkdocs==1.5.3
23-
mkdocs-material==9.5.12
23+
mkdocs-material==9.5.13
2424
mkdocs-exclude-search==0.6.6
2525
mkdocs-simple-hooks==0.1.5
2626
mkdocs-material-extensions==1.3.1

seleniumbase/__version__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# seleniumbase package
2-
__version__ = "4.24.3"
2+
__version__ = "4.24.4"

seleniumbase/core/sb_driver.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def get_user_agent(self, *args, **kwargs):
147147
return js_utils.get_user_agent(self.driver, *args, **kwargs)
148148

149149
def highlight(self, *args, **kwargs):
150-
w_args = kwargs
150+
w_args = kwargs.copy()
151151
if "loops" in w_args:
152152
w_args.pop("loops")
153153
element = page_actions.wait_for_element(self.driver, *args, **w_args)

seleniumbase/plugins/driver_manager.py

+28
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,39 @@ def Driver(
127127
wire=None, # Shortcut / Duplicate of "use_wire".
128128
pls=None, # Shortcut / Duplicate of "page_load_strategy".
129129
):
130+
from seleniumbase import config as sb_config
130131
from seleniumbase.fixtures import constants
131132
from seleniumbase.fixtures import shared_utils
132133

133134
sys_argv = sys.argv
134135
arg_join = " ".join(sys_argv)
136+
existing_runner = False
137+
collect_only = ("--co" in sys_argv or "--collect-only" in sys_argv)
138+
all_scripts = (hasattr(sb_config, "all_scripts") and sb_config.all_scripts)
139+
if (
140+
(hasattr(sb_config, "is_behave") and sb_config.is_behave)
141+
or (hasattr(sb_config, "is_pytest") and sb_config.is_pytest)
142+
or (hasattr(sb_config, "is_nosetest") and sb_config.is_nosetest)
143+
):
144+
existing_runner = True
145+
if (
146+
existing_runner
147+
and not hasattr(sb_config, "_context_of_runner")
148+
):
149+
if hasattr(sb_config, "is_pytest") and sb_config.is_pytest:
150+
import pytest
151+
msg = "Skipping `Driver()` script. (Use `python`, not `pytest`)"
152+
if not collect_only and not all_scripts:
153+
print("\n *** %s" % msg)
154+
if collect_only or not all_scripts:
155+
pytest.skip(allow_module_level=True)
156+
elif hasattr(sb_config, "is_nosetest") and sb_config.is_nosetest:
157+
raise Exception(
158+
"\n A Driver() script was triggered by nosetest collection!"
159+
'\n (Prevent that by using: ``if __name__ == "__main__":``)'
160+
)
161+
elif existing_runner:
162+
sb_config._context_of_runner = True
135163
browser_changes = 0
136164
browser_set = None
137165
browser_text = None

seleniumbase/plugins/pytest_plugin.py

+11
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,16 @@ def pytest_addoption(parser):
759759
help="""This is used by the BaseCase class to tell apart
760760
pytest runs from nosetest runs. (Automatic)""",
761761
)
762+
parser.addoption(
763+
"--all-scripts",
764+
"--all_scripts",
765+
action="store_true",
766+
dest="all_scripts",
767+
default=False,
768+
help="""Use this to run `SB()`, `DriverContext()` and
769+
`Driver()` scripts that are discovered during
770+
the pytest collection phase.""",
771+
)
762772
parser.addoption(
763773
"--time_limit",
764774
"--time-limit",
@@ -1525,6 +1535,7 @@ def pytest_configure(config):
15251535
settings.ARCHIVE_EXISTING_DOWNLOADS = True
15261536
if config.getoption("skip_js_waits"):
15271537
settings.SKIP_JS_WAITS = True
1538+
sb_config.all_scripts = config.getoption("all_scripts")
15281539
sb_config._time_limit = config.getoption("time_limit")
15291540
sb_config.time_limit = config.getoption("time_limit")
15301541
sb_config.slow_mode = config.getoption("slow_mode")

seleniumbase/plugins/sb_manager.py

+11-11
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ def SB(
136136
arg_join = " ".join(sys_argv)
137137
archive_logs = False
138138
existing_runner = False
139+
collect_only = ("--co" in sys_argv or "--collect-only" in sys_argv)
140+
all_scripts = (hasattr(sb_config, "all_scripts") and sb_config.all_scripts)
139141
do_log_folder_setup = False # The first "test=True" run does it
140142
if (
141143
(hasattr(sb_config, "is_behave") and sb_config.is_behave)
@@ -146,21 +148,21 @@ def SB(
146148
test = False # Already using a test runner. Skip extra test steps.
147149
elif test is None and "--test" in sys_argv:
148150
test = True
149-
if (
150-
existing_runner
151-
and not hasattr(sb_config, "_context_of_runner")
152-
):
153-
sb_config._context_of_runner = True
151+
if existing_runner and not hasattr(sb_config, "_context_of_runner"):
154152
if hasattr(sb_config, "is_pytest") and sb_config.is_pytest:
155-
print(
156-
"\n SB Manager script was triggered by pytest collection!"
157-
'\n (Prevent that by using: `if __name__ == "__main__":`)'
158-
)
153+
import pytest
154+
msg = "Skipping `SB()` script. (Use `python`, not `pytest`)"
155+
if not collect_only and not all_scripts:
156+
print("\n *** %s" % msg)
157+
if collect_only or not all_scripts:
158+
pytest.skip(allow_module_level=True)
159159
elif hasattr(sb_config, "is_nosetest") and sb_config.is_nosetest:
160160
raise Exception(
161161
"\n SB Manager script was triggered by nosetest collection!"
162162
'\n (Prevent that by using: ``if __name__ == "__main__":``)'
163163
)
164+
elif existing_runner:
165+
sb_config._context_of_runner = True
164166
if (
165167
not existing_runner
166168
and not hasattr(sb_config, "_has_older_context")
@@ -959,8 +961,6 @@ def SB(
959961
sb_config = sb_config_backup
960962
if test:
961963
sb_config._has_older_context = True
962-
if existing_runner:
963-
sb_config._context_of_runner = True
964964
if test_name:
965965
result = "passed"
966966
if test and not test_passed:

0 commit comments

Comments
 (0)