Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/boilerplates/samples/google_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def test_google_dot_com(self):
self.assert_title_contains("Google")
self.sleep(0.05)
self.save_screenshot_to_logs() # ("./latest_logs" folder)
self.type(HomePage.search_box, "GitHub")
self.press_keys(HomePage.search_box, "GitHub")
self.assert_element(HomePage.search_button)
self.assert_element(HomePage.feeling_lucky_button)
self.click(HomePage.search_button)
Expand Down
5 changes: 5 additions & 0 deletions examples/cdp_mode/raw_cdp_clearcote.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

sb = sb_cdp.Chrome()
sb.goto("https://www.clearcotelabs.com/audit")
sb.click('button[aria-label="Marker 1"]')
sb.click('button[aria-label="Marker 2"]')
sb.click('button[aria-label="Marker 3"]')
sb.press_keys("input", "audit")
sb.select_option_by_text("select", "Mouse")
sb.click('button:contains("Run the audit")')
sb.sleep(6)
sb.assert_element("div.text-emerald-700", timeout=8)
Expand Down
1 change: 1 addition & 0 deletions examples/cdp_mode/raw_cdp_etsy.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
sb.click('button[aria-label="Search"]')
sb.sleep(2)
sb.click_if_visible('button[aria-label="Ok"]')
sb.sleep(1)
soup = sb.get_beautiful_soup()
items = soup.select("div.v2-listing-card__info")
num = 0
Expand Down
9 changes: 6 additions & 3 deletions examples/cdp_mode/raw_chatgpt.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@
close_2 = 'button[data-testid="close-button"]'
sb.click_if_visible("%s, %s" % (close_1, close_2))
query = "Compare Playwright to SeleniumBase in under 178 words"
sb.press_keys("#prompt-textarea", query)
sb.click('button[data-testid="send-button"]')
sb.press_keys("#prompt-textarea, #mobile-composer-prompt", query)
sb.click('button[data-testid="send-button"], button[aria-label*="Send"]')
print('*** Input for ChatGPT: ***\n"%s"' % query)
sb.sleep(3)
with suppress(Exception):
# The "Stop" button disappears when ChatGPT is done typing a response
sb.wait_for_element_not_visible(
'button[data-testid="stop-button"]', timeout=20
)
chat = sb.find_element('[data-message-author-role="assistant"] .markdown')
chat = sb.find_element(
'[data-message-author-role="assistant"] .markdown, '
'[data-assistant-markdown]'
)
soup = sb.get_beautiful_soup(chat.get_html()).text.strip()
soup = soup.replace("\n\n\n", "\n\n")
print("*** Response from ChatGPT: ***\n%s" % soup)
Expand Down
4 changes: 2 additions & 2 deletions examples/cdp_mode/raw_easyjet.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
sb.sleep(1)
sb.click('input[name="from"]')
sb.sleep(1)
sb.type('input[name="from"]', "London Gatwick")
sb.press_keys('input[name="from"]', "London Gatwick")
sb.sleep(1)
sb.click('span[data-testid="airport-name"]')
sb.sleep(1)
sb.type('input[name="to"]', "Paris")
sb.press_keys('input[name="to"]', "Paris")
sb.sleep(1)
sb.click('span[data-testid="airport-name"]')
sb.sleep(1)
Expand Down
10 changes: 6 additions & 4 deletions examples/cdp_mode/raw_glassdoor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
sb.highlight('[data-test="sign-in-button"]')
sb.highlight('[data-test="company-search-autocomplete"]')
sb.press_keys("#employer-autocomplete", "NASA Goddard\n")
sb.sleep(0.5)
sb.click('button[data-role-variant="primary"] span:contains("Search")')
sb.sleep(2)
sb.sleep(1)
sb.click_if_visible(
'[data-role-variant="primary"] span:contains("Search")'
)
sb.sleep(1)
sb.click_if_visible('[aria-label*="NASA"] img')
sb.sleep(2)
sb.sleep(1)
print(sb.get_page_title())
sb.save_as_pdf_to_logs()
sb.save_page_source_to_logs()
Expand Down
7 changes: 2 additions & 5 deletions examples/cdp_mode/raw_priceline.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
"""An example of bypassing PerimeterX detection on Priceline.
(PyAutoGUI is installed at runtime if it's not installed.)"""
"""An example of bypassing PerimeterX detection on Priceline."""
from seleniumbase import SB

with SB(uc=True, test=True, locale="en", guest=True, pls="none") as sb:
sb.activate_cdp_mode()
sb.goto("https://www.priceline.com")
sb.sleep(3)
input_selector = "div.location-input input"
sb.gui_hover_element(input_selector)
sb.mouse_click(input_selector)
sb.click(input_selector)
location = "Portland, OR"
selection = "Oregon, United States" # (Dropdown option)
sb.gui_hover_element(input_selector)
sb.press_keys(input_selector, location)
sb.sleep(0.5)
sb.click(selection)
Expand Down
4 changes: 2 additions & 2 deletions examples/cdp_mode/raw_southwest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
sb.activate_cdp_mode()
sb.goto("https://www.southwest.com/air/booking/")
sb.sleep(2.8)
origin = "BOS"
destination = "MDW"
origin = "DEN"
destination = "LAX"
sb.click_if_visible("button#onetrust-accept-btn-handler")
sb.sleep(0.5)
sb.gui_click_element("input#originationAirportCode")
Expand Down
3 changes: 1 addition & 2 deletions examples/raw_google.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
sb.activate_cdp_mode()
sb.goto("https://google.com/ncr")
sb.click_if_visible('button:contains("Accept all")')
sb.type('[name="q"]', "SeleniumBase GitHub page")
sb.click('[value="Google Search"]')
sb.press_keys('[name="q"]', "SeleniumBase GitHub page\n")
sb.sleep(4) # The "AI Overview" sometimes loads
print(sb.get_page_title())
sb.save_as_pdf_to_logs()
Expand Down
2 changes: 1 addition & 1 deletion mkdocs_build/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

regex>=2026.7.19
pymdown-extensions>=10.21.3
pipdeptree>=4.0.0
pipdeptree>=4.1.0
python-dateutil>=2.8.2
click>=8.4.2
Markdown==3.10.2
Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ exceptiongroup>=1.3.1
websockets~=15.0.1;python_version<"3.10"
websockets>=16.1.1;python_version>="3.10"
filelock~=3.19.1;python_version<"3.10"
filelock>=3.31.1;python_version>="3.10"
filelock>=3.32.0;python_version>="3.10"
fasteners>=0.20
mycdp>=1.4.0
pynose>=1.5.5
platformdirs~=4.4.0;python_version<"3.10"
platformdirs>=4.10.1;python_version>="3.10"
platformdirs>=4.11.0;python_version>="3.10"
typing-extensions>=4.16.0
sbvirtualdisplay>=1.4.0
MarkupSafe>=3.0.3
Expand Down Expand Up @@ -67,7 +67,7 @@ pytest-xdist==3.8.0
parameterized==0.9.0
behave==1.2.6
soupsieve~=2.8.4;python_version<"3.10"
soupsieve~=2.9.0;python_version>="3.10"
soupsieve~=2.9.1;python_version>="3.10"
beautifulsoup4~=4.15.0
pyotp~=2.10.0
python-xlib==0.33;platform_system=="Linux"
Expand Down
2 changes: 1 addition & 1 deletion seleniumbase/__version__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# seleniumbase package
__version__ = "4.51.4"
__version__ = "4.51.5"
61 changes: 22 additions & 39 deletions seleniumbase/core/sb_cdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,10 @@ def click_nth_element(self, selector, number, scroll=True):
element = elements[number]
if scroll:
element.scroll_into_view()
element.click()
try:
element.mouse_click() # Simulated click (NO PyAutoGUI)
except Exception:
element.click() # Standard CDP click (Can be detected)

def click_nth_visible_element(self, selector, number, scroll=True):
"""Finds all matching page elements and clicks the nth visible one.
Expand All @@ -518,7 +521,10 @@ def click_nth_visible_element(self, selector, number, scroll=True):
element = elements[number]
if scroll:
element.scroll_into_view()
element.click()
try:
element.mouse_click() # Simulated click (NO PyAutoGUI)
except Exception:
element.click() # Standard CDP click (Can be detected)

def click_link(self, link_text):
self.find_elements_by_text(link_text, "a")[0].click()
Expand Down Expand Up @@ -875,41 +881,12 @@ def click(self, selector, timeout=None, scroll=True):
timeout = settings.SMALL_TIMEOUT
self.__slow_mode_pause_if_set()
element = self.find_element(selector, timeout=timeout)
tag_name = element.tag_name
if tag_name:
tag_name = tag_name.lower().strip()
if (
tag_name in [
"a",
"button",
"canvas",
"div",
"input",
"label",
"li",
"path",
"span",
"svg",
]
and "contains(" not in selector
):
if scroll:
element.scroll_into_view()
try:
element.mouse_click() # Simulated click (NOT PyAutoGUI)
except Exception:
element.click() # Standard CDP click
else:
if scroll:
if "contains(" in selector:
element.scroll_into_view()
else:
try:
element.scroll_into_view()
except Exception:
with suppress(Exception):
self.js_scroll_into_view(selector)
element.click() # Standard CDP click
if scroll:
element.scroll_into_view()
try:
element.mouse_click() # Simulated click (NO PyAutoGUI)
except Exception:
element.click() # Standard CDP click (Can be detected)
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.wait(0.2))

Expand Down Expand Up @@ -957,7 +934,10 @@ def click_visible_elements(self, selector, limit=0, scroll=True):
if (width != 0 or height != 0):
if scroll:
element.scroll_into_view()
element.click()
try:
element.mouse_click() # Simulated click (NO PyAutoGUI)
except Exception:
element.click() # Standard CDP click (Can be detected)
click_count += 1
time.sleep(0.044)
self.__slow_mode_pause_if_set()
Expand Down Expand Up @@ -998,7 +978,10 @@ def select_option_by_text(self, dropdown_selector, option):
options = element.query_selector_all("option")
for found_option in options:
if found_option.text.strip() == option.strip():
found_option.select_option()
try:
self.type(dropdown_selector, option)
except Exception:
found_option.select_option()
return
raise Exception(
"Unable to find text option {%s} in dropdown {%s}!"
Expand Down
105 changes: 99 additions & 6 deletions seleniumbase/undetected/cdp_driver/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,19 +786,112 @@ async def clear_input_async(self):
async def send_keys_async(self, text: str):
"""
Send text to an input field, or any other html element.
Hint: If you ever get stuck where using py:meth:`~click`
does not work, sending the keystroke \\n or \\r\\n
Hint: If you ever get stuck where using `~click()`
does not work, sending the keystroke \n or \r\n
or a spacebar works wonders!
:param text: text to send
:return: None
"""
if self.tag_name.lower() == "textarea" and text.endswith("\r\n"):
text = text[0:-1]
await self.apply("(elem) => elem.focus()")
[
# Map non-alphanumeric symbols to the correct CDP virtual key code.
# Prevents collisions with control keys. Eg. ord('.') = 46 = VK_DELETE.
SYMBOL_MAP = {
".": ("Period", 190),
",": ("Comma", 188),
"-": ("Minus", 189),
"=": ("Equal", 187),
"/": ("Slash", 191),
"\\": ("Backslash", 220),
";": ("Semicolon", 186),
"'": ("Quote", 222),
"`": ("Backquote", 192),
"[": ("BracketLeft", 219),
"]": ("BracketRight", 221),
"!": ("Digit1", 49),
"@": ("Digit2", 50),
"#": ("Digit3", 51),
"$": ("Digit4", 52),
"%": ("Digit5", 53),
"^": ("Digit6", 54),
"&": ("Digit7", 55),
"*": ("Digit8", 56),
"(": ("Digit9", 57),
")": ("Digit0", 48),
"_": ("Minus", 189),
"+": ("Equal", 187),
"{": ("BracketLeft", 219),
"}": ("BracketRight", 221),
"|": ("Backslash", 220),
":": ("Semicolon", 186),
'"': ("Quote", 222),
"<": ("Comma", 188),
">": ("Period", 190),
"?": ("Slash", 191),
"~": ("Backquote", 192),
}
for char in text:
# 1. Map character to its DOM key attributes
if char in ("\r", "\n"):
key = "Enter"
code = "Enter"
vk = 13
text_val = "\r"
elif char == "\t":
key = "Tab"
code = "Tab"
vk = 9
text_val = ""
elif char == " ":
key = " "
code = "Space"
vk = 32
text_val = " "
else:
key = char
text_val = char
if char.isalpha():
code = f"Key{char.upper()}"
vk = ord(char.upper())
elif char.isdigit():
code = f"Digit{char}"
vk = ord(char)
elif char in SYMBOL_MAP:
code, vk = SYMBOL_MAP[char]
else:
code = ""
vk = 0
# 2. Trigger keydown DOM event WITHOUT inserting text
await self._tab.send(
cdp.input_.dispatch_key_event("char", text=char)
cdp.input_.dispatch_key_event(
type_="rawKeyDown",
key=key,
code=code,
windows_virtual_key_code=vk,
)
)
# 3. Trigger keypress DOM event AND insert text
if text_val:
await self._tab.send(
cdp.input_.dispatch_key_event(
type_="char",
key=key,
code=code,
text=text_val,
unmodified_text=text_val,
windows_virtual_key_code=vk,
)
)
# 4. Trigger keyup DOM event
await self._tab.send(
cdp.input_.dispatch_key_event(
type_="keyUp",
key=key,
code=code,
windows_virtual_key_code=vk,
)
)
for char in list(text)
]

async def send_file_async(self, *file_paths: PathLike):
"""
Expand Down
Loading