diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md index dfd7ed0b5a3..601254a1630 100644 --- a/examples/cdp_mode/ReadMe.md +++ b/examples/cdp_mode/ReadMe.md @@ -370,10 +370,15 @@ with SB(uc=True, test=True, locale="en", pls="none") as sb: ```python sb.cdp.get(url, **kwargs) -sb.cdp.open(url, **kwargs) +sb.cdp.open(url, **kwargs) # Same as sb.cdp.get(url, **kwargs) sb.cdp.reload(ignore_cache=True, script_to_evaluate_on_load=None) sb.cdp.refresh(*args, **kwargs) sb.cdp.get_event_loop() +sb.cdp.get_rd_host() # Returns the remote-debugging host +sb.cdp.get_rd_port() # Returns the remote-debugging port +sb.cdp.get_rd_url() # Returns the remote-debugging URL +sb.cdp.get_endpoint_url() # Same as sb.cdp.get_rd_url() +sb.cdp.get_port() # Same as sb.cdp.get_rd_port() sb.cdp.add_handler(event, handler) sb.cdp.find_element(selector, best_match=False, timeout=None) sb.cdp.find(selector, best_match=False, timeout=None) @@ -487,6 +492,7 @@ sb.cdp.set_attributes(selector, attribute, value) sb.cdp.is_attribute_present(selector, attribute, value=None) sb.cdp.is_online() sb.cdp.solve_captcha() +sb.cdp.click_captcha() sb.cdp.gui_press_key(key) sb.cdp.gui_press_keys(keys) sb.cdp.gui_write(text) @@ -612,6 +618,69 @@ sb.driver.stop() -------- +### đ CDP Mode Async API / Methods + +```python +await get(url="about:blank") +await open(url="about:blank") +await find(text, best_match=False, timeout=10) # `text` can be a selector +await find_all(text, timeout=10) # `text` can be a selector +await select(selector, timeout=10) +await select_all(selector, timeout=10, include_frames=False) +await query_selector(selector) +await query_selector_all(selector) +await find_element_by_text(text, best_match=False) +await find_elements_by_text(text) +await reload(ignore_cache=True, script_to_evaluate_on_load=None) +await evaluate(expression) +await js_dumps(obj_name) +await back() +await forward() +await get_window() +await get_content() +await maximize() +await minimize() +await fullscreen() +await medimize() +await set_window_size(left=0, top=0, width=1280, height=1024) +await set_window_rect(left=0, top=0, width=1280, height=1024) +await activate() +await bring_to_front() +await set_window_state(left=0, top=0, width=1280, height=720, state="normal") +await get_navigation_history() +await open_external_inspector() # Open a separate browser for debugging +await close() +await scroll_down(amount=25) +await scroll_up(amount=25) +await wait_for(selector="", text="", timeout=10) +await download_file(url, filename=None) +await save_screenshot(filename="auto", format="png", full_page=False) +await print_to_pdf(filename="auto") +await set_download_path(path) +await get_all_linked_sources() +await get_all_urls(absolute=True) +await get_html() +await get_page_source() +await is_element_present(selector) +await is_element_visible(selector) +await get_element_rect(selector, timeout=5) # (relative to window) +await get_window_rect() +await get_gui_element_rect(selector, timeout=5) # (relative to screen) +await get_title() +await send_keys(selector, text, timeout=5) +await type(selector, text, timeout=5) +await click(selector, timeout=5) +await click_with_offset(selector, x, y, center=False, timeout=5) +await solve_captcha() +await click_captcha() # Same as solve_captcha() +await get_document() +await get_flattened_document() +await get_local_storage() +await set_local_storage(items) +``` + +-------- + ### đ CDP Mode WebElement API / Methods After finding an element in CDP Mode, you can access `WebElement` methods: diff --git a/examples/cdp_mode/playwright/ReadMe.md b/examples/cdp_mode/playwright/ReadMe.md new file mode 100644 index 00000000000..985008535a4 --- /dev/null +++ b/examples/cdp_mode/playwright/ReadMe.md @@ -0,0 +1,141 @@ + + +
connect_over_cdp() to attach itself onto an existing SeleniumBase session via the remote-debugging-port. From here, APIs of both frameworks can be used, giving you a hybrid approach that delivers the best experience of both worlds.
+
+--------
+
+### đ Getting started with Stealthy Playwright Mode:
+
+If `playwright` isn't already installed, then install it first:
+
+```zsh
+pip install playwright
+```
+
+Stealthy Playwright Mode comes in 3 formats:
+1. `sb_cdp` sync format
+2. `SB` nested sync format
+3. `cdp_driver` async format
+
+
+#### `sb_cdp` sync format (minimal boilerplate):
+
+```python
+from playwright.sync_api import sync_playwright
+from seleniumbase import sb_cdp
+
+sb = sb_cdp.Chrome()
+endpoint_url = sb.get_endpoint_url()
+
+with sync_playwright() as p:
+ browser = p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ page.goto("https://example.com")
+```
+
+#### `SB` nested sync format (minimal boilerplate):
+
+```python
+from playwright.sync_api import sync_playwright
+from seleniumbase import SB
+
+with SB(uc=True) as sb:
+ sb.activate_cdp_mode()
+ endpoint_url = sb.cdp.get_endpoint_url()
+
+ with sync_playwright() as p:
+ browser = p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ page.goto("https://example.com")
+```
+
+#### `cdp_driver` async format (minimal boilerplate):
+
+```python
+import asyncio
+from seleniumbase import cdp_driver
+from playwright.async_api import async_playwright
+
+async def main():
+ driver = await cdp_driver.start_async()
+ endpoint_url = driver.get_endpoint_url()
+
+ async with async_playwright() as p:
+ browser = await p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ await page.goto("https://example.com")
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ loop.run_until_complete(main())
+```
+
+### đ Stealthy Playwright Mode details:
+
+The `sb_cdp` and `cdp_driver` formats don't use WebDriver at all, meaning that `chromedriver` isn't needed. From these two formats, Stealthy Playwright Mode can call [CDP Mode methods](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/cdp_mode_methods.md) and Playwright methods.
+
+The `SB()` format requires WebDriver, therefore `chromedriver` will be downloaded (as `uc_driver`) if the driver isn't already present on the local machine. The `SB()` format has access to Selenium WebDriver methods via [the SeleniumBase API](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/method_summary.md). Using Stealthy Playwright Mode from `SB()` grants access to all the APIs: Selenium, SeleniumBase, [UC Mode](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/uc_mode.md), [CDP Mode](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/ReadMe.md), and Playwright.
+
+In the sync formats, `get_endpoint_url()` also applies `nest-asyncio` so that nested event loops are allowed. (Python doesn't allow nested event loops by default). Without this, you'd get the error: `"Cannot run the event loop while another loop is running"` when calling CDP Mode methods (such as `solve_captcha()`) from within the Playwright context manager. This `nest-asyncio` call is done behind-the-scenes so that users don't need to handle this on their own.
+
+### đ Stealthy Playwright Mode examples:
+
+Here's an example that queries Microsoft Copilot:
+
+```python
+from playwright.sync_api import sync_playwright
+from seleniumbase import sb_cdp
+
+sb = sb_cdp.Chrome()
+endpoint_url = sb.get_endpoint_url()
+
+with sync_playwright() as p:
+ browser = p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ page.goto("https://copilot.microsoft.com")
+ page.wait_for_selector("textarea#userInput")
+ sb.sleep(1)
+ query = "Playwright Python connect_over_cdp() sync example"
+ page.fill("textarea#userInput", query)
+ page.click('button[data-testid="submit-button"]')
+ sb.sleep(3)
+ sb.solve_captcha()
+ page.wait_for_selector('button[data-testid*="-thumbs-up"]')
+ sb.sleep(4)
+ page.click('button[data-testid*="scroll-to-bottom"]')
+ sb.sleep(3)
+ chat_results = '[data-testid="highlighted-chats"]'
+ result = page.locator(chat_results).inner_text()
+ print(result.replace("\n\n", " \n"))
+```
+
+Here's an example that solves the Bing CAPTCHA:
+
+```python
+from playwright.sync_api import sync_playwright
+from seleniumbase import sb_cdp
+
+sb = sb_cdp.Chrome(locale="en")
+endpoint_url = sb.get_endpoint_url()
+
+with sync_playwright() as p:
+ browser = p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ page.goto("https://www.bing.com/turing/captcha/challenge")
+ sb.sleep(3)
+ sb.solve_captcha()
+ sb.sleep(3)
+```
+
+For more examples, see [examples/cdp_mode/playwright](https://github.com/seleniumbase/SeleniumBase/tree/master/examples/cdp_mode/playwright).
+
+--------
+
+
diff --git a/examples/cdp_mode/playwright/__init__.py b/examples/cdp_mode/playwright/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/examples/cdp_mode/playwright/raw_basic_async.py b/examples/cdp_mode/playwright/raw_basic_async.py
new file mode 100644
index 00000000000..4419282a932
--- /dev/null
+++ b/examples/cdp_mode/playwright/raw_basic_async.py
@@ -0,0 +1,24 @@
+import asyncio
+from playwright.async_api import async_playwright
+from seleniumbase import cdp_driver
+
+
+async def main():
+ driver = await cdp_driver.start_async()
+ endpoint_url = driver.get_endpoint_url()
+
+ async with async_playwright() as p:
+ browser = await p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ await page.goto("https://seleniumbase.io/simple/login")
+ await page.fill("#username", "demo_user")
+ await page.fill("#password", "secret_pass")
+ await page.click("#log-in")
+ await page.wait_for_selector("h1")
+ await driver.sleep(1)
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ loop.run_until_complete(main())
diff --git a/examples/cdp_mode/playwright/raw_basic_nested.py b/examples/cdp_mode/playwright/raw_basic_nested.py
new file mode 100644
index 00000000000..cf0214a92be
--- /dev/null
+++ b/examples/cdp_mode/playwright/raw_basic_nested.py
@@ -0,0 +1,17 @@
+from playwright.sync_api import sync_playwright
+from seleniumbase import SB
+
+with SB(uc=True) as sb:
+ sb.activate_cdp_mode()
+ endpoint_url = sb.cdp.get_endpoint_url()
+
+ with sync_playwright() as p:
+ browser = p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ page.goto("https://seleniumbase.io/simple/login")
+ page.fill("#username", "demo_user")
+ page.fill("#password", "secret_pass")
+ page.click("#log-in")
+ page.wait_for_selector("h1")
+ sb.sleep(1)
diff --git a/examples/cdp_mode/playwright/raw_basic_sync.py b/examples/cdp_mode/playwright/raw_basic_sync.py
new file mode 100644
index 00000000000..5115051259d
--- /dev/null
+++ b/examples/cdp_mode/playwright/raw_basic_sync.py
@@ -0,0 +1,16 @@
+from playwright.sync_api import sync_playwright
+from seleniumbase import sb_cdp
+
+sb = sb_cdp.Chrome()
+endpoint_url = sb.get_endpoint_url()
+
+with sync_playwright() as p:
+ browser = p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ page.goto("https://seleniumbase.io/simple/login")
+ page.fill("#username", "demo_user")
+ page.fill("#password", "secret_pass")
+ page.click("#log-in")
+ page.wait_for_selector("h1")
+ sb.sleep(1)
diff --git a/examples/cdp_mode/playwright/raw_bing_cap_async.py b/examples/cdp_mode/playwright/raw_bing_cap_async.py
new file mode 100644
index 00000000000..7781aa96a2e
--- /dev/null
+++ b/examples/cdp_mode/playwright/raw_bing_cap_async.py
@@ -0,0 +1,22 @@
+import asyncio
+from playwright.async_api import async_playwright
+from seleniumbase import cdp_driver
+
+
+async def main():
+ driver = await cdp_driver.start_async(locale="en")
+ endpoint_url = driver.get_endpoint_url()
+
+ async with async_playwright() as p:
+ browser = await p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ await page.goto("https://www.bing.com/turing/captcha/challenge")
+ await driver.sleep(3)
+ await driver.solve_captcha()
+ await driver.sleep(3)
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ loop.run_until_complete(main())
diff --git a/examples/cdp_mode/playwright/raw_bing_cap_nested.py b/examples/cdp_mode/playwright/raw_bing_cap_nested.py
new file mode 100644
index 00000000000..3c9cb9f9d97
--- /dev/null
+++ b/examples/cdp_mode/playwright/raw_bing_cap_nested.py
@@ -0,0 +1,15 @@
+from playwright.sync_api import sync_playwright
+from seleniumbase import SB
+
+with SB(uc=True, locale="en") as sb:
+ sb.activate_cdp_mode()
+ endpoint_url = sb.cdp.get_endpoint_url()
+
+ with sync_playwright() as p:
+ browser = p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ page.goto("https://www.bing.com/turing/captcha/challenge")
+ sb.sleep(3)
+ sb.solve_captcha()
+ sb.sleep(3)
diff --git a/examples/cdp_mode/playwright/raw_bing_cap_sync.py b/examples/cdp_mode/playwright/raw_bing_cap_sync.py
new file mode 100644
index 00000000000..71242af8cca
--- /dev/null
+++ b/examples/cdp_mode/playwright/raw_bing_cap_sync.py
@@ -0,0 +1,14 @@
+from playwright.sync_api import sync_playwright
+from seleniumbase import sb_cdp
+
+sb = sb_cdp.Chrome(locale="en")
+endpoint_url = sb.get_endpoint_url()
+
+with sync_playwright() as p:
+ browser = p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ page.goto("https://www.bing.com/turing/captcha/challenge")
+ sb.sleep(3)
+ sb.solve_captcha()
+ sb.sleep(3)
diff --git a/examples/cdp_mode/playwright/raw_copilot_async.py b/examples/cdp_mode/playwright/raw_copilot_async.py
new file mode 100644
index 00000000000..2aa1cd29770
--- /dev/null
+++ b/examples/cdp_mode/playwright/raw_copilot_async.py
@@ -0,0 +1,33 @@
+import asyncio
+from playwright.async_api import async_playwright
+from seleniumbase import cdp_driver
+
+
+async def main():
+ driver = await cdp_driver.start_async()
+ endpoint_url = driver.get_endpoint_url()
+
+ async with async_playwright() as p:
+ browser = await p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ await page.goto("https://copilot.microsoft.com")
+ await page.wait_for_selector("textarea#userInput")
+ await driver.sleep(1)
+ query = "Playwright Python connect_over_cdp() sync example"
+ await page.fill("textarea#userInput", query)
+ await page.click('button[data-testid="submit-button"]')
+ await driver.sleep(3)
+ await driver.solve_captcha()
+ await page.wait_for_selector('button[data-testid*="-thumbs-up"]')
+ await driver.sleep(4)
+ await page.click('button[data-testid*="scroll-to-bottom"]')
+ await driver.sleep(3)
+ chat_results = '[data-testid="highlighted-chats"]'
+ result = await page.locator(chat_results).inner_text()
+ print(result.replace("\n\n", " \n"))
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ loop.run_until_complete(main())
diff --git a/examples/cdp_mode/playwright/raw_copilot_nested.py b/examples/cdp_mode/playwright/raw_copilot_nested.py
new file mode 100644
index 00000000000..8c9d021a535
--- /dev/null
+++ b/examples/cdp_mode/playwright/raw_copilot_nested.py
@@ -0,0 +1,26 @@
+from playwright.sync_api import sync_playwright
+from seleniumbase import SB
+
+with SB(uc=True) as sb:
+ sb.activate_cdp_mode()
+ endpoint_url = sb.cdp.get_endpoint_url()
+
+ with sync_playwright() as p:
+ browser = p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ page.goto("https://copilot.microsoft.com")
+ page.wait_for_selector("textarea#userInput")
+ sb.sleep(1)
+ query = "Playwright Python connect_over_cdp() sync example"
+ page.fill("textarea#userInput", query)
+ page.click('button[data-testid="submit-button"]')
+ sb.sleep(3)
+ sb.solve_captcha()
+ page.wait_for_selector('button[data-testid*="-thumbs-up"]')
+ sb.sleep(4)
+ page.click('button[data-testid*="scroll-to-bottom"]')
+ sb.sleep(3)
+ chat_results = '[data-testid="highlighted-chats"]'
+ result = page.locator(chat_results).inner_text()
+ print(result.replace("\n\n", " \n"))
diff --git a/examples/cdp_mode/playwright/raw_copilot_sync.py b/examples/cdp_mode/playwright/raw_copilot_sync.py
new file mode 100644
index 00000000000..f3af0d31f0d
--- /dev/null
+++ b/examples/cdp_mode/playwright/raw_copilot_sync.py
@@ -0,0 +1,25 @@
+from playwright.sync_api import sync_playwright
+from seleniumbase import sb_cdp
+
+sb = sb_cdp.Chrome()
+endpoint_url = sb.get_endpoint_url()
+
+with sync_playwright() as p:
+ browser = p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ page.goto("https://copilot.microsoft.com")
+ page.wait_for_selector("textarea#userInput")
+ sb.sleep(1)
+ query = "Playwright Python connect_over_cdp() sync example"
+ page.fill("textarea#userInput", query)
+ page.click('button[data-testid="submit-button"]')
+ sb.sleep(3)
+ sb.solve_captcha()
+ page.wait_for_selector('button[data-testid*="-thumbs-up"]')
+ sb.sleep(4)
+ page.click('button[data-testid*="scroll-to-bottom"]')
+ sb.sleep(3)
+ chat_results = '[data-testid="highlighted-chats"]'
+ result = page.locator(chat_results).inner_text()
+ print(result.replace("\n\n", " \n"))
diff --git a/examples/cdp_mode/playwright/raw_gitlab_async.py b/examples/cdp_mode/playwright/raw_gitlab_async.py
new file mode 100644
index 00000000000..32687079686
--- /dev/null
+++ b/examples/cdp_mode/playwright/raw_gitlab_async.py
@@ -0,0 +1,26 @@
+import asyncio
+from playwright.async_api import async_playwright
+from seleniumbase import cdp_driver
+
+
+async def main():
+ driver = await cdp_driver.start_async(locale="en", agent="headless")
+ endpoint_url = driver.get_endpoint_url()
+
+ async with async_playwright() as p:
+ browser = await p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ await page.goto("https://gitlab.com/users/sign_in")
+ await driver.sleep(3)
+ await driver.solve_captcha()
+ await driver.sleep(1)
+ await page.locator('label[for="user_login"]').click()
+ await page.wait_for_selector('[data-testid="sign-in-button"]')
+ await page.locator("#user_login").fill("Username")
+ await driver.sleep(2)
+
+
+if __name__ == "__main__":
+ loop = asyncio.new_event_loop()
+ loop.run_until_complete(main())
diff --git a/examples/cdp_mode/playwright/raw_gitlab_nested.py b/examples/cdp_mode/playwright/raw_gitlab_nested.py
new file mode 100644
index 00000000000..eafc7b29019
--- /dev/null
+++ b/examples/cdp_mode/playwright/raw_gitlab_nested.py
@@ -0,0 +1,19 @@
+from playwright.sync_api import sync_playwright
+from seleniumbase import SB
+
+with SB(uc=True, locale="en") as sb:
+ sb.activate_cdp_mode()
+ endpoint_url = sb.cdp.get_endpoint_url()
+
+ with sync_playwright() as p:
+ browser = p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ page.goto("https://gitlab.com/users/sign_in")
+ sb.sleep(3)
+ sb.solve_captcha()
+ sb.sleep(1)
+ page.locator('label[for="user_login"]').click()
+ page.wait_for_selector('[data-testid="sign-in-button"]')
+ page.locator("#user_login").fill("Username")
+ sb.sleep(2)
diff --git a/examples/cdp_mode/playwright/raw_gitlab_sync.py b/examples/cdp_mode/playwright/raw_gitlab_sync.py
new file mode 100644
index 00000000000..337c9984644
--- /dev/null
+++ b/examples/cdp_mode/playwright/raw_gitlab_sync.py
@@ -0,0 +1,18 @@
+from playwright.sync_api import sync_playwright
+from seleniumbase import sb_cdp
+
+sb = sb_cdp.Chrome(locale="en")
+endpoint_url = sb.get_endpoint_url()
+
+with sync_playwright() as p:
+ browser = p.chromium.connect_over_cdp(endpoint_url)
+ context = browser.contexts[0]
+ page = context.pages[0]
+ page.goto("https://gitlab.com/users/sign_in")
+ sb.sleep(3)
+ sb.solve_captcha()
+ sb.sleep(1)
+ page.locator('label[for="user_login"]').click()
+ page.wait_for_selector('[data-testid="sign-in-button"]')
+ page.locator("#user_login").fill("Username")
+ sb.sleep(2)
diff --git a/examples/cdp_mode/raw_basic_async.py b/examples/cdp_mode/raw_basic_async.py
index 8547cc2b365..af21373db96 100644
--- a/examples/cdp_mode/raw_basic_async.py
+++ b/examples/cdp_mode/raw_basic_async.py
@@ -7,14 +7,11 @@ async def main():
url = "seleniumbase.io/simple/login"
driver = await cdp_driver.start_async()
page = await driver.get(url, lang="en")
- print(await page.evaluate("document.title"))
- element = await page.select("#username")
- await element.send_keys_async("demo_user")
- element = await page.select("#password")
- await element.send_keys_async("secret_pass")
- element = await page.select("#log-in")
- await element.click_async()
- print(await page.evaluate("document.title"))
+ print(await page.get_title())
+ await page.type("#username", "demo_user")
+ await page.type("#password", "secret_pass")
+ await page.click("#log-in")
+ print(await page.get_title())
element = await page.select("h1")
assert element.text == "Welcome!"
top_nav = await page.select("div.topnav")
diff --git a/examples/cdp_mode/raw_mobile_async.py b/examples/cdp_mode/raw_mobile_async.py
index 66e5e204405..2c99ff7d3f4 100644
--- a/examples/cdp_mode/raw_mobile_async.py
+++ b/examples/cdp_mode/raw_mobile_async.py
@@ -8,7 +8,7 @@
async def main():
url = "https://gitlab.com/users/sign_in"
driver = await cdp_driver.start_async()
- await driver.main_tab.send(
+ await driver.page.send(
mycdp.emulation.set_device_metrics_override(
width=412, height=732, device_scale_factor=3, mobile=True
)
diff --git a/help_docs/cdp_mode_methods.md b/help_docs/cdp_mode_methods.md
new file mode 100644
index 00000000000..8fba1e994d6
--- /dev/null
+++ b/help_docs/cdp_mode_methods.md
@@ -0,0 +1,364 @@
+
+
+sb_cdp)
+
+Pure CDP Mode doesn't use WebDriver for anything. The browser is launched using CDP, and all browser actions are performed using CDP (or PyAutoGUI). Initialization:
+
+```python
+from seleniumbase import sb_cdp
+
+sb = sb_cdp.Chrome(url=None, **kwargs)
+```
+
+Pure CDP Mode includes all methods from regular CDP Mode, except that they're called directly from sb instead of sb.cdp. Eg: sb.gui_click_captcha(). To quit a CDP-launched browser, use `sb.driver.stop()`.
+
+Basic example from [SeleniumBase/examples/cdp_mode/raw_cdp_turnstile.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_cdp_turnstile.py):
+
+```python
+from seleniumbase import sb_cdp
+
+url = "https://seleniumbase.io/apps/turnstile"
+sb = sb_cdp.Chrome(url)
+sb.solve_captcha()
+sb.assert_element("img#captcha-success")
+sb.set_messenger_theme(location="top_left")
+sb.post_message("SeleniumBase wasn't detected", duration=3)
+sb.driver.stop()
+```
+
+Another example: ([SeleniumBase/examples/cdp_mode/raw_cdp_methods.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_cdp_methods.py))
+
+```python
+from seleniumbase import sb_cdp
+
+url = "https://seleniumbase.io/demo_page"
+sb = sb_cdp.Chrome(url)
+sb.press_keys("input", "Text")
+sb.highlight("button")
+sb.type("textarea", "Here are some words")
+sb.click("button")
+sb.set_value("input#mySlider", "100")
+sb.click_visible_elements("input.checkBoxClassB")
+sb.select_option_by_text("#mySelect", "Set to 75%")
+sb.gui_hover_and_click("#myDropdown", "#dropOption2")
+sb.gui_click_element("#checkBox1")
+sb.gui_drag_and_drop("img#logo", "div#drop2")
+sb.nested_click("iframe#myFrame3", ".fBox")
+sb.sleep(2)
+sb.driver.stop()
+```
+
+âšī¸ Even if you don't call `sb.driver.stop()`, the browser still quits after the script goes out-of-scope.
+
+--------
+
+### đ CDP Mode Async API / Methods
+
+```python
+await get(url="about:blank")
+await open(url="about:blank")
+await find(text, best_match=False, timeout=10) # `text` can be a selector
+await find_all(text, timeout=10) # `text` can be a selector
+await select(selector, timeout=10)
+await select_all(selector, timeout=10, include_frames=False)
+await query_selector(selector)
+await query_selector_all(selector)
+await find_element_by_text(text, best_match=False)
+await find_elements_by_text(text)
+await reload(ignore_cache=True, script_to_evaluate_on_load=None)
+await evaluate(expression)
+await js_dumps(obj_name)
+await back()
+await forward()
+await get_window()
+await get_content()
+await maximize()
+await minimize()
+await fullscreen()
+await medimize()
+await set_window_size(left=0, top=0, width=1280, height=1024)
+await set_window_rect(left=0, top=0, width=1280, height=1024)
+await activate()
+await bring_to_front()
+await set_window_state(left=0, top=0, width=1280, height=720, state="normal")
+await get_navigation_history()
+await open_external_inspector() # Open a separate browser for debugging
+await close()
+await scroll_down(amount=25)
+await scroll_up(amount=25)
+await wait_for(selector="", text="", timeout=10)
+await download_file(url, filename=None)
+await save_screenshot(filename="auto", format="png", full_page=False)
+await print_to_pdf(filename="auto")
+await set_download_path(path)
+await get_all_linked_sources()
+await get_all_urls(absolute=True)
+await get_html()
+await get_page_source()
+await is_element_present(selector)
+await is_element_visible(selector)
+await get_element_rect(selector, timeout=5) # (relative to window)
+await get_window_rect()
+await get_gui_element_rect(selector, timeout=5) # (relative to screen)
+await get_title()
+await send_keys(selector, text, timeout=5)
+await type(selector, text, timeout=5)
+await click(selector, timeout=5)
+await click_with_offset(selector, x, y, center=False, timeout=5)
+await solve_captcha()
+await click_captcha() # Same as solve_captcha()
+await get_document()
+await get_flattened_document()
+await get_local_storage()
+await set_local_storage(items)
+```
+
+--------
+
+### đ CDP Mode WebElement API / Methods
+
+After finding an element in CDP Mode, you can access `WebElement` methods:
+
+(Eg. After `element = sb.find_element(selector)`)
+
+```python
+element.clear_input()
+element.click()
+element.click_with_offset(x, y, center=False)
+element.flash(duration=0.5, color="EE4488")
+element.focus()
+element.gui_click(timeframe=0.25)
+element.highlight_overlay()
+element.mouse_click()
+element.mouse_drag(destination)
+element.mouse_move()
+element.press_keys(text)
+element.query_selector(selector)
+element.querySelector(selector)
+element.query_selector_all(selector)
+element.querySelectorAll(selector)
+element.remove_from_dom()
+element.save_screenshot(*args, **kwargs)
+element.save_to_dom()
+element.scroll_into_view()
+element.select_option()
+element.send_file(*file_paths)
+element.send_keys(text)
+element.set_text(value)
+element.type(text)
+element.get_position()
+element.get_html()
+element.get_js_attributes()
+element.get_attribute(attribute)
+element.get_parent()
+```
+
+--------
+
+
+
+
diff --git a/mkdocs.yml b/mkdocs.yml
index e31c2d8d832..402e1418eee 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -10,7 +10,7 @@ edit_uri: ""
site_dir: "site"
docs_dir: "mkdocs_build"
# Copyright
-copyright: Copyright © 2014 - 2024 Michael Mintz
+copyright: Copyright © 2014 - 2026 Michael Mintz
# Extensions
markdown_extensions:
- admonition
@@ -101,6 +101,7 @@ nav:
- đī¸ GUI / Commander: help_docs/commander.md
- đ´ Recorder Mode: help_docs/recorder_mode.md
- đ API Reference: help_docs/method_summary.md
+ - đ CDP Mode APIs: help_docs/cdp_mode_methods.md
- Python Setup / Install:
- đ Get Python, pip, & git: help_docs/install_python_pip_git.md
- âī¸ Virtualenv Instructions: help_docs/virtualenv_instructions.md
@@ -117,6 +118,7 @@ nav:
- Integrations:
- đ¤ UC Mode: help_docs/uc_mode.md
- đ CDP Mode: examples/cdp_mode/ReadMe.md
+ - đ Stealthy Playwright: examples/cdp_mode/playwright/ReadMe.md
- đ¤ GitHub CI: integrations/github/workflows/ReadMe.md
- đ MasterQA: seleniumbase/masterqa/ReadMe.md
- đī¸ Case Plans: help_docs/case_plans.md
diff --git a/mkdocs_build/prepare.py b/mkdocs_build/prepare.py
index 1e268808ae6..81d33b1d4a5 100644
--- a/mkdocs_build/prepare.py
+++ b/mkdocs_build/prepare.py
@@ -76,6 +76,7 @@ def main(*args, **kwargs):
scanned_dir_list.append("help_docs")
scanned_dir_list.append("examples")
scanned_dir_list.append("examples/cdp_mode")
+ scanned_dir_list.append("examples/cdp_mode/playwright")
scanned_dir_list.append("examples/master_qa")
scanned_dir_list.append("examples/presenter")
scanned_dir_list.append("examples/behave_bdd")
diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt
index c5c4406bf1b..51eb1b0a35a 100644
--- a/mkdocs_build/requirements.txt
+++ b/mkdocs_build/requirements.txt
@@ -2,7 +2,7 @@
# Minimum Python version: 3.10 (for generating docs only)
regex>=2025.11.3
-pymdown-extensions>=10.17.2
+pymdown-extensions>=10.18
pipdeptree>=2.30.0
python-dateutil>=2.8.2
Markdown==3.10
diff --git a/requirements.txt b/requirements.txt
index 3fda0cb765f..4c692cdfffc 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,29 +1,22 @@
-pip>=25.0.1;python_version<"3.9"
-pip>=25.3;python_version>="3.9"
+pip>=25.3
packaging>=25.0
setuptools~=70.2;python_version<"3.10"
setuptools>=80.9.0;python_version>="3.10"
wheel>=0.45.1
-attrs~=25.3.0;python_version<"3.9"
-attrs>=25.4.0;python_version>="3.9"
+attrs>=25.4.0
certifi>=2025.11.12
exceptiongroup>=1.3.1
-websockets~=13.1;python_version<"3.9"
-websockets>=15.0.1;python_version>="3.9"
-filelock~=3.16.1;python_version<"3.9"
-filelock~=3.19.1;python_version>="3.9" and python_version<"3.10"
+websockets>=15.0.1
+filelock~=3.19.1;python_version<"3.10"
filelock>=3.20.0;python_version>="3.10"
fasteners>=0.20
-mycdp>=1.3.1
+mycdp>=1.3.2
pynose>=1.5.5
-platformdirs~=4.3.6;python_version<"3.9"
-platformdirs~=4.4.0;python_version>="3.9" and python_version<"3.10"
-platformdirs>=4.5.0;python_version>="3.10"
-typing-extensions~=4.13.2;python_version<"3.9"
-typing-extensions>=4.15.0;python_version>="3.9"
+platformdirs~=4.4.0;python_version<"3.10"
+platformdirs>=4.5.1;python_version>="3.10"
+typing-extensions>=4.15.0
sbvirtualdisplay>=1.4.0
-MarkupSafe==2.1.5;python_version<"3.9"
-MarkupSafe>=3.0.3;python_version>="3.9"
+MarkupSafe>=3.0.3
Jinja2>=3.1.6
six>=1.17.0
parse>=1.20.2
@@ -38,48 +31,39 @@ idna>=3.11
chardet==5.2.0
charset-normalizer>=3.4.4,<4
urllib3>=1.26.20,<2;python_version<"3.10"
-urllib3>=1.26.20,<2.6.0;python_version>="3.10"
-requests==2.32.4;python_version<"3.9"
-requests~=2.32.5;python_version>="3.9"
+urllib3>=1.26.20,<3;python_version>="3.10"
+requests~=2.32.5
sniffio==1.3.1
h11==0.16.0
outcome==1.3.0.post0
-trio==0.27.0;python_version<"3.9"
-trio>=0.31.0,<1;python_version>="3.9" and python_version<"3.10"
+trio>=0.31.0,<1;python_version<"3.10"
trio>=0.32.0,<1;python_version>="3.10"
trio-websocket~=0.12.2
wsproto==1.2.0;python_version<"3.10"
wsproto~=1.3.2;python_version>="3.10"
-websocket-client~=1.8.0;python_version<"3.9"
-websocket-client~=1.9.0;python_version>="3.9"
-selenium==4.27.1;python_version<"3.9"
-selenium==4.32.0;python_version>="3.9" and python_version<"3.10"
-selenium==4.38.0;python_version>="3.10"
-cssselect==1.2.0;python_version<"3.9"
-cssselect==1.3.0;python_version>="3.9"
+websocket-client~=1.9.0
+selenium==4.32.0;python_version<"3.10"
+selenium==4.39.0;python_version>="3.10"
+cssselect==1.3.0
+nest-asyncio==1.6.0
sortedcontainers==2.4.0
execnet==2.1.1;python_version<"3.10"
execnet==2.1.2;python_version>="3.10"
iniconfig==2.1.0;python_version<"3.10"
iniconfig==2.3.0;python_version>="3.10"
-pluggy==1.5.0;python_version<"3.9"
-pluggy==1.6.0;python_version>="3.9"
-pytest==8.3.5;python_version<"3.9"
-pytest==8.4.2;python_version>="3.9" and python_version<"3.11"
-pytest==9.0.1;python_version>="3.11"
+pluggy==1.6.0
+pytest==8.4.2;python_version<"3.11"
+pytest==9.0.2;python_version>="3.11"
pytest-html==4.0.2
pytest-metadata==3.1.1
pytest-ordering==0.6
-pytest-rerunfailures==14.0;python_version<"3.9"
-pytest-rerunfailures==16.0.1;python_version>="3.9" and python_version<"3.10"
+pytest-rerunfailures==16.0.1;python_version<"3.10"
pytest-rerunfailures==16.1;python_version>="3.10"
-pytest-xdist==3.6.1;python_version<"3.9"
-pytest-xdist==3.8.0;python_version>="3.9"
+pytest-xdist==3.8.0
parameterized==0.9.0
behave==1.2.6
-soupsieve==2.7;python_version<"3.9"
-soupsieve~=2.8;python_version>="3.9"
-beautifulsoup4~=4.14.2
+soupsieve~=2.8
+beautifulsoup4~=4.14.3
pyotp==2.9.0
python-xlib==0.33;platform_system=="Linux"
PyAutoGUI>=0.9.54;platform_system=="Linux"
@@ -91,15 +75,10 @@ rich>=14.2.0,<15
# --- Testing Requirements --- #
# ("pip install -r requirements.txt" also installs this, but "pip install -e ." won't.)
-coverage>=7.6.1;python_version<"3.9"
-coverage>=7.10.7;python_version>="3.9" and python_version<"3.10"
+coverage>=7.10.7;python_version<"3.10"
coverage>=7.12.0;python_version>="3.10"
-pytest-cov>=5.0.0;python_version<"3.9"
-pytest-cov>=7.0.0;python_version>="3.9"
-flake8==5.0.4;python_version<"3.9"
-flake8==7.3.0;python_version>="3.9"
+pytest-cov>=7.0.0
+flake8==7.3.0
mccabe==0.7.0
-pyflakes==2.5.0;python_version<"3.9"
-pyflakes==3.4.0;python_version>="3.9"
-pycodestyle==2.9.1;python_version<"3.9"
-pycodestyle==2.14.0;python_version>="3.9"
+pyflakes==3.4.0
+pycodestyle==2.14.0
diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py
index d09453272e8..0a8dac81b9b 100755
--- a/seleniumbase/__version__.py
+++ b/seleniumbase/__version__.py
@@ -1,2 +1,2 @@
# seleniumbase package
-__version__ = "4.44.20"
+__version__ = "4.45.0"
diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py
index 08779bfcce6..bfcd9bd273e 100644
--- a/seleniumbase/core/browser_launcher.py
+++ b/seleniumbase/core/browser_launcher.py
@@ -758,6 +758,11 @@ def uc_open_with_cdp_mode(driver, url=None, **kwargs):
cdp.refresh = CDPM.refresh
cdp.add_handler = CDPM.add_handler
cdp.get_event_loop = CDPM.get_event_loop
+ cdp.get_rd_host = CDPM.get_rd_host
+ cdp.get_rd_port = CDPM.get_rd_port
+ cdp.get_rd_url = CDPM.get_rd_url
+ cdp.get_endpoint_url = CDPM.get_endpoint_url
+ cdp.get_port = CDPM.get_port
cdp.find_element = CDPM.find_element
cdp.find = CDPM.find_element
cdp.locator = CDPM.find_element
@@ -823,6 +828,7 @@ def uc_open_with_cdp_mode(driver, url=None, **kwargs):
cdp.is_attribute_present = CDPM.is_attribute_present
cdp.is_online = CDPM.is_online
cdp.solve_captcha = CDPM.solve_captcha
+ cdp.click_captcha = CDPM.click_captcha
cdp.gui_press_key = CDPM.gui_press_key
cdp.gui_press_keys = CDPM.gui_press_keys
cdp.gui_write = CDPM.gui_write
@@ -954,6 +960,7 @@ def uc_open_with_cdp_mode(driver, url=None, **kwargs):
cdp.loop = cdp.get_event_loop()
driver.cdp = cdp
driver.solve_captcha = CDPM.solve_captcha
+ driver.click_captcha = CDPM.click_captcha
driver.find_element_by_text = CDPM.find_element_by_text
driver._is_using_cdp = True
if (
diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py
index d030d1b9fa4..8e3e7ac03fb 100644
--- a/seleniumbase/core/sb_cdp.py
+++ b/seleniumbase/core/sb_cdp.py
@@ -164,6 +164,44 @@ def refresh(self, *args, **kwargs):
def get_event_loop(self):
return self.loop
+ def get_rd_host(self):
+ """Returns the remote-debugging host (likely 127.0.0.1)"""
+ driver = self.driver
+ if hasattr(driver, "cdp_base"):
+ driver = driver.cdp_base
+ return driver.config.host
+
+ def get_rd_port(self):
+ """Returns the remote-debugging port (commonly 9222)"""
+ driver = self.driver
+ if hasattr(driver, "cdp_base"):
+ driver = driver.cdp_base
+ return driver.config.port
+
+ def get_rd_url(self):
+ """Returns the remote-debugging URL, which is used for
+ allowing the Playwright integration to launch stealthy,
+ and also applies nest-asyncio for nested event loops so
+ that SeleniumBase methods can be called from Playwright
+ without encountering event loop error messages such as:
+ Cannot run the event loop while another loop is running."""
+ import nest_asyncio
+ nest_asyncio.apply()
+ driver = self.driver
+ if hasattr(driver, "cdp_base"):
+ driver = driver.cdp_base
+ host = driver.config.host
+ port = driver.config.port
+ return f"http://{host}:{port}"
+
+ def get_endpoint_url(self):
+ """Same as get_rd_url(), which returns the remote-debugging URL."""
+ return self.get_rd_url()
+
+ def get_port(self):
+ """Same as get_rd_port(), which returns the remote-debugging port."""
+ return self.get_rd_port()
+
def add_handler(self, event, handler):
self.page.add_handler(event, handler)
@@ -1922,8 +1960,8 @@ def _on_a_cf_turnstile_page(self, source=None):
return False
def _on_a_g_recaptcha_page(self, source=None):
- time.sleep(0.2)
- self.loop.run_until_complete(self.page.wait(0.2))
+ time.sleep(0.4)
+ self.loop.run_until_complete(self.page.wait())
source = self.get_page_source()
if (
(
@@ -1932,11 +1970,17 @@ def _on_a_g_recaptcha_page(self, source=None):
)
and self.is_element_visible('iframe[title="reCAPTCHA"]')
):
- self.loop.run_until_complete(self.page.wait(0.1))
+ try:
+ self.loop.run_until_complete(self.page.wait(0.1))
+ except Exception:
+ time.sleep(0.1)
return True
elif "com/recaptcha/api.js" in source:
time.sleep(1.6) # Still loading
- self.loop.run_until_complete(self.page.wait(0.1))
+ try:
+ self.loop.run_until_complete(self.page.wait(0.1))
+ except Exception:
+ time.sleep(0.1)
return True
return False
@@ -1975,7 +2019,12 @@ def __gui_click_recaptcha(self, use_cdp=False):
def solve_captcha(self):
self.__click_captcha(use_cdp=True)
+ def click_captcha(self):
+ """Same as solve_captcha()"""
+ self.__click_captcha(use_cdp=True)
+
def gui_click_captcha(self):
+ """Use PyAutoGUI to click the CAPTCHA"""
self.__click_captcha(use_cdp=False)
def __click_captcha(self, use_cdp=False):
diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py
index 4413fc06e50..425ab1b00d6 100644
--- a/seleniumbase/fixtures/base_case.py
+++ b/seleniumbase/fixtures/base_case.py
@@ -5035,6 +5035,8 @@ def activate_cdp_mode(self, url=None, **kwargs):
self.cdp = self.driver.cdp
if hasattr(self.cdp, "solve_captcha"):
self.solve_captcha = self.cdp.solve_captcha
+ if hasattr(self.cdp, "click_captcha"):
+ self.click_captcha = self.cdp.click_captcha
if hasattr(self.cdp, "find_element_by_text"):
self.find_element_by_text = self.cdp.find_element_by_text
if getattr(self.driver, "_is_using_auth", None):
@@ -7335,21 +7337,6 @@ def get_pdf_text(
with pip_find_lock:
with suppress(Exception):
shared_utils.make_writable(constants.PipInstall.FINDLOCK)
- if sys.version_info < (3, 9):
- # Fix bug in newer cryptography for Python 3.7 and 3.8:
- # "pyo3_runtime.PanicException: Python API call failed"
- try:
- import cryptography
- if cryptography.__version__ != "39.0.2":
- del cryptography # To get newer ver
- shared_utils.pip_install(
- "cryptography", version="39.0.2"
- )
- import cryptography
- except Exception:
- shared_utils.pip_install(
- "cryptography", version="39.0.2"
- )
try:
from pdfminer.high_level import extract_text
except Exception:
@@ -7641,7 +7628,11 @@ def download_file(self, file_url, destination_folder=None):
destination_folder = constants.Files.DOWNLOADS_FOLDER
if not os.path.exists(destination_folder):
os.makedirs(destination_folder)
- page_utils._download_file_to(file_url, destination_folder)
+ agent = self.get_user_agent()
+ headers = {"user-agent": agent}
+ page_utils._download_file_to(
+ file_url, destination_folder, headers=headers
+ )
if self.recorder_mode and self.__current_url_is_recordable():
if self.get_session_storage_item("pause_recorder") == "no":
time_stamp = self.execute_script("return Date.now();")
@@ -7665,8 +7656,10 @@ def save_file_as(self, file_url, new_file_name, destination_folder=None):
destination_folder = constants.Files.DOWNLOADS_FOLDER
if not os.path.exists(destination_folder):
os.makedirs(destination_folder)
+ agent = self.get_user_agent()
+ headers = {"user-agent": agent}
page_utils._download_file_to(
- file_url, destination_folder, new_file_name
+ file_url, destination_folder, new_file_name, headers=headers
)
def save_data_as(self, data, file_name, destination_folder=None):
diff --git a/seleniumbase/fixtures/js_utils.py b/seleniumbase/fixtures/js_utils.py
index 5a8915fd24a..67e9c872da2 100644
--- a/seleniumbase/fixtures/js_utils.py
+++ b/seleniumbase/fixtures/js_utils.py
@@ -211,6 +211,19 @@ def activate_jquery(driver):
try:
execute_script(driver, "jQuery('html');")
return
+ except TypeError as e:
+ if (
+ (
+ shared_utils.is_cdp_swap_needed(driver)
+ or hasattr(driver, "_swap_driver")
+ )
+ and "cannot unpack non-iterable" in str(e)
+ ):
+ pass
+ else:
+ if x == 18:
+ add_js_link(driver, jquery_js)
+ time.sleep(0.1)
except Exception:
if x == 18:
add_js_link(driver, jquery_js)
diff --git a/seleniumbase/fixtures/page_utils.py b/seleniumbase/fixtures/page_utils.py
index 913bcddc56f..a659b3f6e0c 100644
--- a/seleniumbase/fixtures/page_utils.py
+++ b/seleniumbase/fixtures/page_utils.py
@@ -293,12 +293,14 @@ def _print_unique_links_with_status_codes(page_url, soup):
print(link, " -> ", status_code)
-def _download_file_to(file_url, destination_folder, new_file_name=None):
+def _download_file_to(
+ file_url, destination_folder, new_file_name=None, headers=None
+):
if new_file_name:
file_name = new_file_name
else:
file_name = file_url.split("/")[-1]
- r = requests.get(file_url, timeout=5)
+ r = requests.get(file_url, headers=headers, timeout=5)
file_path = os.path.join(destination_folder, file_name)
download_file_lock = fasteners.InterProcessLock(
constants.MultiBrowser.DOWNLOAD_FILE_LOCK
diff --git a/seleniumbase/undetected/cdp_driver/browser.py b/seleniumbase/undetected/cdp_driver/browser.py
index c410b1ba7b8..364b0d56ffc 100644
--- a/seleniumbase/undetected/cdp_driver/browser.py
+++ b/seleniumbase/undetected/cdp_driver/browser.py
@@ -251,6 +251,23 @@ async def _handle_target_update(
)
self.targets.remove(current_tab)
+ def get_rd_host(self):
+ return self.config.host
+
+ def get_rd_port(self):
+ return self.config.port
+
+ def get_rd_url(self):
+ host = self.config.host
+ port = self.config.port
+ return f"http://{host}:{port}"
+
+ def get_endpoint_url(self):
+ return self.get_rd_url()
+
+ def get_port(self):
+ return self.get_rd_port()
+
async def set_auth(self, username, password, tab):
async def auth_challenge_handler(event: cdp.fetch.AuthRequired):
await tab.send(
diff --git a/seleniumbase/undetected/cdp_driver/cdp_util.py b/seleniumbase/undetected/cdp_driver/cdp_util.py
index 30953bf6bb1..6bb92657f4c 100644
--- a/seleniumbase/undetected/cdp_driver/cdp_util.py
+++ b/seleniumbase/undetected/cdp_driver/cdp_util.py
@@ -688,6 +688,8 @@ async def start(
sb_config._cdp_platform = platform_var
else:
sb_config._cdp_platform = None
+ driver.page = driver.main_tab
+ driver.solve_captcha = driver.page.solve_captcha
return driver
diff --git a/seleniumbase/undetected/cdp_driver/tab.py b/seleniumbase/undetected/cdp_driver/tab.py
index b24e7dcc48c..d99827229e6 100644
--- a/seleniumbase/undetected/cdp_driver/tab.py
+++ b/seleniumbase/undetected/cdp_driver/tab.py
@@ -4,9 +4,15 @@
import datetime
import logging
import pathlib
+import re
import urllib.parse
import warnings
+from contextlib import suppress
+from filelock import FileLock
from seleniumbase import config as sb_config
+from seleniumbase.fixtures import constants
+from seleniumbase.fixtures import js_utils
+from seleniumbase.fixtures import shared_utils
from typing import Dict, List, Union, Optional, Tuple
from . import browser as cdp_browser
from . import element
@@ -368,6 +374,9 @@ async def get(
url, new_tab, new_window, **kwargs
)
+ async def open(self, url="about:blank"):
+ return await self.get(url=url)
+
async def query_selector_all(
self,
selector: str,
@@ -1282,19 +1291,352 @@ async def get_all_urls(self, absolute=True) -> List[str]:
res.append(abs_url)
return res
- async def verify_cf(self):
- """(An attempt)"""
- checkbox = None
- checkbox_sibling = await self.wait_for(text="verify you are human")
- if checkbox_sibling:
- parent = checkbox_sibling.parent
- while parent:
- checkbox = await parent.query_selector("input[type=checkbox]")
- if checkbox:
- break
- parent = parent.parent
- await checkbox.mouse_move()
- await checkbox.mouse_click()
+ async def get_html(self):
+ element = await self.find("html", timeout=1)
+ return await element.get_html_async()
+
+ async def get_page_source(self):
+ return await self.get_html()
+
+ async def is_element_present(self, selector):
+ try:
+ await self.select(selector, timeout=0.01)
+ return True
+ except Exception:
+ return False
+
+ async def is_element_visible(self, selector):
+ try:
+ element = await self.select(selector, timeout=0.01)
+ except Exception:
+ return False
+ if not element:
+ return False
+ try:
+ position = await element.get_position_async()
+ return (position.width != 0 or position.height != 0)
+ except Exception:
+ return False
+
+ async def __on_a_cf_turnstile_page(self, source=None):
+ if not source or len(source) < 400:
+ await self.sleep(0.22)
+ source = await self.get_html()
+ if (
+ (
+ 'data-callback="onCaptchaSuccess"' in source
+ and 'title="reCAPTCHA"' not in source
+ and 'id="recaptcha-token"' not in source
+ )
+ or "/challenge-platform/h/b/" in source
+ or 'id="challenge-widget-' in source
+ or "challenges.cloudf" in source
+ or "cf-turnstile-" in source
+ ):
+ return True
+ return False
+
+ async def __on_a_g_recaptcha_page(self, source=None):
+ await self.sleep(0.4)
+ source = await self.get_html()
+ if (
+ (
+ 'id="recaptcha-token"' in source
+ or 'title="reCAPTCHA"' in source
+ )
+ and await self.is_element_present('iframe[title="reCAPTCHA"]')
+ ):
+ await self.sleep(0.1)
+ return True
+ elif "com/recaptcha/api.js" in source:
+ await self.sleep(1.6) # Still loading
+ return True
+ return False
+
+ async def __gui_click_recaptcha(self):
+ selector = None
+ if await self.is_element_present('iframe[title="reCAPTCHA"]'):
+ selector = 'iframe[title="reCAPTCHA"]'
+ else:
+ return
+ await self.sleep(0.5)
+ with suppress(Exception):
+ element_rect = await self.get_gui_element_rect(selector, timeout=1)
+ e_x = element_rect["x"]
+ e_y = element_rect["y"]
+ x_offset = 26
+ y_offset = 35
+ if await asyncio.to_thread(shared_utils.is_windows):
+ x_offset = 29
+ x = e_x + x_offset
+ y = e_y + y_offset
+ sb_config._saved_cf_x_y = (x, y) # For debugging later
+ await self.sleep(0.11)
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
+ with await asyncio.to_thread(gui_lock.acquire):
+ await self.bring_to_front()
+ await self.sleep(0.05)
+ await self.click_with_offset(
+ selector, x_offset, y_offset, timeout=1
+ )
+ await self.sleep(0.22)
+
+ async def get_element_rect(self, selector, timeout=5):
+ element = await self.select(selector, timeout=timeout)
+ coordinates = None
+ if ":contains(" in selector:
+ position = await element.get_position_async()
+ x = position.x
+ y = position.y
+ width = position.width
+ height = position.height
+ coordinates = {"x": x, "y": y, "width": width, "height": height}
+ else:
+ coordinates = await self.js_dumps(
+ """document.querySelector('%s').getBoundingClientRect()"""
+ % js_utils.escape_quotes_if_needed(re.escape(selector))
+ )
+ return coordinates
+
+ async def get_window_rect(self):
+ coordinates = {}
+ innerWidth = await self.evaluate("window.innerWidth")
+ innerHeight = await self.evaluate("window.innerHeight")
+ outerWidth = await self.evaluate("window.outerWidth")
+ outerHeight = await self.evaluate("window.outerHeight")
+ pageXOffset = await self.evaluate("window.pageXOffset")
+ pageYOffset = await self.evaluate("window.pageYOffset")
+ scrollX = await self.evaluate("window.scrollX")
+ scrollY = await self.evaluate("window.scrollY")
+ screenLeft = await self.evaluate("window.screenLeft")
+ screenTop = await self.evaluate("window.screenTop")
+ x = await self.evaluate("window.screenX")
+ y = await self.evaluate("window.screenY")
+ coordinates["innerWidth"] = innerWidth
+ coordinates["innerHeight"] = innerHeight
+ coordinates["outerWidth"] = outerWidth
+ coordinates["outerHeight"] = outerHeight
+ coordinates["width"] = outerWidth
+ coordinates["height"] = outerHeight
+ coordinates["pageXOffset"] = pageXOffset if pageXOffset else 0
+ coordinates["pageYOffset"] = pageYOffset if pageYOffset else 0
+ coordinates["scrollX"] = scrollX if scrollX else 0
+ coordinates["scrollY"] = scrollY if scrollY else 0
+ coordinates["screenLeft"] = screenLeft if screenLeft else 0
+ coordinates["screenTop"] = screenTop if screenTop else 0
+ coordinates["x"] = x if x else 0
+ coordinates["y"] = y if y else 0
+ return coordinates
+
+ async def get_gui_element_rect(self, selector, timeout=5):
+ """(Coordinates are relative to the screen. Not the window.)"""
+ element_rect = await self.get_element_rect(selector, timeout=timeout)
+ e_width = element_rect["width"]
+ e_height = element_rect["height"]
+ window_rect = await self.get_window_rect()
+ w_bottom_y = window_rect["y"] + window_rect["height"]
+ viewport_height = window_rect["innerHeight"]
+ x = window_rect["x"] + element_rect["x"]
+ y = w_bottom_y - viewport_height + element_rect["y"]
+ y_scroll_offset = window_rect["pageYOffset"]
+ if (
+ hasattr(sb_config, "_cdp_browser")
+ and sb_config._cdp_browser == "opera"
+ ):
+ # Handle special case where Opera side panel shifts coordinates
+ x_offset = window_rect["outerWidth"] - window_rect["innerWidth"]
+ if x_offset > 56:
+ x_offset = 56
+ elif x_offset < 22:
+ x_offset = 0
+ x = x + x_offset
+ y = y - y_scroll_offset
+ x = x + window_rect["scrollX"]
+ y = y + window_rect["scrollY"]
+ return ({"height": e_height, "width": e_width, "x": x, "y": y})
+
+ async def get_title(self):
+ return await self.evaluate("document.title")
+
+ async def send_keys(self, selector, text, timeout=5):
+ element = await self.find(selector, timeout=timeout)
+ await element.send_keys_async(text)
+
+ async def type(self, selector, text, timeout=5):
+ await self.send_keys(selector, text, timeout=timeout)
+
+ async def click(self, selector, timeout=5):
+ element = await self.find(selector, timeout=timeout)
+ await element.click_async()
+
+ async def click_with_offset(self, selector, x, y, center=False, timeout=5):
+ element = await self.find(selector, timeout=timeout)
+ await element.scroll_into_view_async()
+ await element.mouse_click_with_offset_async(x=x, y=y, center=center)
+
+ async def solve_captcha(self):
+ await self.sleep(0.11)
+ source = await self.get_html()
+ if await self.__on_a_cf_turnstile_page(source):
+ pass
+ elif await self.__on_a_g_recaptcha_page(source):
+ await self.__gui_click_recaptcha()
+ return
+ else:
+ return
+ selector = None
+ if await self.is_element_present('[class="cf-turnstile"]'):
+ selector = '[class="cf-turnstile"]'
+ elif await self.is_element_present("#challenge-form div > div"):
+ selector = "#challenge-form div > div"
+ elif await self.is_element_present('[style="display: grid;"] div div'):
+ selector = '[style="display: grid;"] div div'
+ elif await self.is_element_present("[class*=spacer] + div div"):
+ selector = '[class*=spacer] + div div'
+ elif await self.is_element_present(".spacer div:not([class])"):
+ selector = ".spacer div:not([class])"
+ elif await self.is_element_present('[data-testid*="challenge-"] div'):
+ selector = '[data-testid*="challenge-"] div'
+ elif await self.is_element_present(
+ "div#turnstile-widget div:not([class])"
+ ):
+ selector = "div#turnstile-widget div:not([class])"
+ elif await self.is_element_present("ngx-turnstile div:not([class])"):
+ selector = "ngx-turnstile div:not([class])"
+ elif await self.is_element_present(
+ 'form div:not([class]):has(input[name*="cf-turn"])'
+ ):
+ selector = 'form div:not([class]):has(input[name*="cf-turn"])'
+ elif await self.is_element_present("form div:not(:has(*))"):
+ selector = "form div:not(:has(*))"
+ elif await self.is_element_present(
+ "body > div#check > div:not([class])"
+ ):
+ selector = "body > div#check > div:not([class])"
+ elif await self.is_element_present(".cf-turnstile-wrapper"):
+ selector = ".cf-turnstile-wrapper"
+ elif await self.is_element_present(
+ '[id*="turnstile"] div:not([class])'
+ ):
+ selector = '[id*="turnstile"] div:not([class])'
+ elif await self.is_element_present(
+ '[class*="turnstile"] div:not([class])'
+ ):
+ selector = '[class*="turnstile"] div:not([class])'
+ elif await self.is_element_present(
+ '[data-callback="onCaptchaSuccess"]'
+ ):
+ selector = '[data-callback="onCaptchaSuccess"]'
+ elif await self.is_element_present(
+ "div:not([class]) > div:not([class])"
+ ):
+ selector = "div:not([class]) > div:not([class])"
+ else:
+ return
+ if not selector:
+ return
+ if (
+ await self.is_element_present("form")
+ and (
+ await self.is_element_present('form[class*="center"]')
+ or await self.is_element_present('form[class*="right"]')
+ or await self.is_element_present('form div[class*="center"]')
+ or await self.is_element_present('form div[class*="right"]')
+ )
+ ):
+ script = (
+ """var $elements = document.querySelectorAll(
+ 'form[class], form div[class]');
+ var index = 0, length = $elements.length;
+ for(; index < length; index++){
+ the_class = $elements[index].getAttribute('class');
+ new_class = the_class.replaceAll('center', 'left');
+ new_class = new_class.replaceAll('right', 'left');
+ $elements[index].setAttribute('class', new_class);}"""
+ )
+ with suppress(Exception):
+ await self.evaluate(script)
+ elif (
+ await self.is_element_present("form")
+ and (
+ await self.is_element_present('form div[style*="center"]')
+ or await self.is_element_present('form div[style*="right"]')
+ )
+ ):
+ script = (
+ """var $elements = document.querySelectorAll(
+ 'form[style], form div[style]');
+ var index = 0, length = $elements.length;
+ for(; index < length; index++){
+ the_style = $elements[index].getAttribute('style');
+ new_style = the_style.replaceAll('center', 'left');
+ new_style = new_style.replaceAll('right', 'left');
+ $elements[index].setAttribute('style', new_style);}"""
+ )
+ with suppress(Exception):
+ await self.evaluate(script)
+ elif (
+ await self.is_element_present(
+ 'form [id*="turnstile"] div:not([class])'
+ )
+ or await self.is_element_present(
+ 'form [class*="turnstile"] div:not([class])'
+ )
+ ):
+ script = (
+ """var $elements = document.querySelectorAll(
+ 'form [id*="turnstile"]');
+ var index = 0, length = $elements.length;
+ for(; index < length; index++){
+ $elements[index].setAttribute('align', 'left');}
+ var $elements = document.querySelectorAll(
+ 'form [class*="turnstile"]');
+ var index = 0, length = $elements.length;
+ for(; index < length; index++){
+ $elements[index].setAttribute('align', 'left');}"""
+ )
+ with suppress(Exception):
+ await self.evaluate(script)
+ elif (
+ await self.is_element_present(
+ '[style*="text-align: center;"] div:not([class])'
+ )
+ ):
+ script = (
+ """var $elements = document.querySelectorAll(
+ '[style*="text-align: center;"]');
+ var index = 0, length = $elements.length;
+ for(; index < length; index++){
+ the_style = $elements[index].getAttribute('style');
+ new_style = the_style.replaceAll('center', 'left');
+ $elements[index].setAttribute('style', new_style);}"""
+ )
+ with suppress(Exception):
+ await self.evaluate(script)
+ with suppress(Exception):
+ await self.sleep(0.05)
+ element_rect = await self.get_gui_element_rect(selector, timeout=1)
+ e_x = element_rect["x"]
+ e_y = element_rect["y"]
+ x_offset = 32
+ y_offset = 32
+ if await asyncio.to_thread(shared_utils.is_windows):
+ y_offset = 28
+ x = e_x + x_offset
+ y = e_y + y_offset
+ sb_config._saved_cf_x_y = (x, y) # For debugging later
+ await self.sleep(0.11)
+ gui_lock = FileLock(constants.MultiBrowser.PYAUTOGUILOCK)
+ with await asyncio.to_thread(gui_lock.acquire):
+ await self.bring_to_front()
+ await self.sleep(0.05)
+ await self.click_with_offset(
+ selector, x_offset, y_offset, timeout=1
+ )
+ await self.sleep(0.22)
+
+ async def click_captcha(self):
+ await self.solve_captcha()
async def get_document(self):
return await self.send(cdp.dom.get_document())
diff --git a/setup.py b/setup.py
index e54afa365fc..85cd8e79776 100755
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,5 @@
"""Setup steps for installing SeleniumBase dependencies and plugins.
-(Uses selenium 4.x and is compatible with Python 3.8+)"""
+(Uses selenium 4.x and is compatible with Python 3.9+)"""
from setuptools import setup, find_packages # noqa: F401
import os
import sys
@@ -121,7 +121,6 @@
"Operating System :: POSIX :: Linux",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
@@ -145,34 +144,27 @@
"Topic :: Software Development :: Testing :: Traffic Generation",
"Topic :: Utilities",
],
- python_requires=">=3.8",
+ python_requires=">=3.9",
install_requires=[
- 'pip>=25.0.1;python_version<"3.9"',
- 'pip>=25.3;python_version>="3.9"',
+ 'pip>=25.3',
'packaging>=25.0',
'setuptools~=70.2;python_version<"3.10"', # Newer ones had issues
'setuptools>=80.9.0;python_version>="3.10"',
'wheel>=0.45.1',
- 'attrs~=25.3.0;python_version<"3.9"',
- 'attrs>=25.4.0;python_version>="3.9"',
- "certifi>=2025.11.12",
- "exceptiongroup>=1.3.1",
- 'websockets~=13.1;python_version<"3.9"',
- 'websockets>=15.0.1;python_version>="3.9"',
- 'filelock~=3.16.1;python_version<"3.9"',
- 'filelock~=3.19.1;python_version>="3.9" and python_version<"3.10"',
+ 'attrs>=25.4.0',
+ 'certifi>=2025.11.12',
+ 'exceptiongroup>=1.3.1',
+ 'websockets>=15.0.1',
+ 'filelock~=3.19.1;python_version<"3.10"',
'filelock>=3.20.0;python_version>="3.10"',
'fasteners>=0.20',
- "mycdp>=1.3.1",
- "pynose>=1.5.5",
- 'platformdirs~=4.3.6;python_version<"3.9"',
- 'platformdirs~=4.4.0;python_version>="3.9" and python_version<"3.10"',
- 'platformdirs>=4.5.0;python_version>="3.10"',
- 'typing-extensions~=4.13.2;python_version<"3.9"',
- 'typing-extensions>=4.15.0;python_version>="3.9"',
- "sbvirtualdisplay>=1.4.0",
- 'MarkupSafe==2.1.5;python_version<"3.9"',
- 'MarkupSafe>=3.0.3;python_version>="3.9"',
+ 'mycdp>=1.3.2',
+ 'pynose>=1.5.5',
+ 'platformdirs~=4.4.0;python_version<"3.10"',
+ 'platformdirs>=4.5.1;python_version>="3.10"',
+ 'typing-extensions>=4.15.0',
+ 'sbvirtualdisplay>=1.4.0',
+ 'MarkupSafe>=3.0.3',
"Jinja2>=3.1.6",
"six>=1.17.0",
'parse>=1.20.2',
@@ -181,54 +173,45 @@
'pyyaml>=6.0.3',
'pygments>=2.19.2',
'pyreadline3>=3.5.4;platform_system=="Windows"',
- "tabcompleter>=1.4.0",
- "pdbp>=1.8.1",
- "idna>=3.11",
+ 'tabcompleter>=1.4.0',
+ 'pdbp>=1.8.1',
+ 'idna>=3.11',
'chardet==5.2.0',
'charset-normalizer>=3.4.4,<4',
'urllib3>=1.26.20,<2;python_version<"3.10"',
- 'urllib3>=1.26.20,<2.6.0;python_version>="3.10"',
- 'requests==2.32.4;python_version<"3.9"',
- 'requests~=2.32.5;python_version>="3.9"',
+ 'urllib3>=1.26.20,<3;python_version>="3.10"',
+ 'requests~=2.32.5',
'sniffio==1.3.1',
'h11==0.16.0',
'outcome==1.3.0.post0',
- 'trio==0.27.0;python_version<"3.9"',
- 'trio>=0.31.0,<1;python_version>="3.9" and python_version<"3.10"',
+ 'trio>=0.31.0,<1;python_version<"3.10"',
'trio>=0.32.0,<1;python_version>="3.10"',
'trio-websocket~=0.12.2',
'wsproto==1.2.0;python_version<"3.10"',
'wsproto~=1.3.2;python_version>="3.10"',
- 'websocket-client~=1.8.0;python_version<"3.9"',
- 'websocket-client~=1.9.0;python_version>="3.9"',
- 'selenium==4.27.1;python_version<"3.9"',
- 'selenium==4.32.0;python_version>="3.9" and python_version<"3.10"',
- 'selenium==4.38.0;python_version>="3.10"',
- 'cssselect==1.2.0;python_version<"3.9"',
- 'cssselect==1.3.0;python_version>="3.9"',
- "sortedcontainers==2.4.0",
+ 'websocket-client~=1.9.0',
+ 'selenium==4.32.0;python_version<"3.10"',
+ 'selenium==4.39.0;python_version>="3.10"',
+ 'cssselect==1.3.0',
+ 'nest-asyncio==1.6.0',
+ 'sortedcontainers==2.4.0',
'execnet==2.1.1;python_version<"3.10"',
'execnet==2.1.2;python_version>="3.10"',
'iniconfig==2.1.0;python_version<"3.10"',
'iniconfig==2.3.0;python_version>="3.10"',
- 'pluggy==1.5.0;python_version<"3.9"',
- 'pluggy==1.6.0;python_version>="3.9"',
- 'pytest==8.3.5;python_version<"3.9"',
- 'pytest==8.4.2;python_version>="3.9" and python_version<"3.11"',
- 'pytest==9.0.1;python_version>="3.11"',
- "pytest-html==4.0.2", # Newer ones had issues
+ 'pluggy==1.6.0',
+ 'pytest==8.4.2;python_version<"3.11"',
+ 'pytest==9.0.2;python_version>="3.11"',
+ 'pytest-html==4.0.2', # Newer ones had issues
'pytest-metadata==3.1.1',
- "pytest-ordering==0.6",
- 'pytest-rerunfailures==14.0;python_version<"3.9"',
- 'pytest-rerunfailures==16.0.1;python_version>="3.9" and python_version<"3.10"', # noqa
+ 'pytest-ordering==0.6',
+ 'pytest-rerunfailures==16.0.1;python_version<"3.10"',
'pytest-rerunfailures==16.1;python_version>="3.10"',
- 'pytest-xdist==3.6.1;python_version<"3.9"',
- 'pytest-xdist==3.8.0;python_version>="3.9"',
+ 'pytest-xdist==3.8.0',
'parameterized==0.9.0',
- "behave==1.2.6", # Newer ones had issues
- 'soupsieve==2.7;python_version<"3.9"',
- 'soupsieve~=2.8;python_version>="3.9"',
- "beautifulsoup4~=4.14.2",
+ 'behave==1.2.6', # Newer ones had issues
+ 'soupsieve~=2.8',
+ 'beautifulsoup4~=4.14.3',
'pyotp==2.9.0',
'python-xlib==0.33;platform_system=="Linux"',
'PyAutoGUI>=0.9.54;platform_system=="Linux"',
@@ -249,22 +232,17 @@
# pip install -e .[coverage]
# Usage: coverage run -m pytest; coverage html; coverage report
"coverage": [
- 'coverage>=7.6.1;python_version<"3.9"',
- 'coverage>=7.10.7;python_version>="3.9" and python_version<"3.10"',
+ 'coverage>=7.10.7;python_version<"3.10"',
'coverage>=7.12.0;python_version>="3.10"',
- 'pytest-cov>=5.0.0;python_version<"3.9"',
- 'pytest-cov>=7.0.0;python_version>="3.9"',
+ 'pytest-cov>=7.0.0',
],
# pip install -e .[flake8]
# Usage: flake8
"flake8": [
- 'flake8==5.0.4;python_version<"3.9"',
- 'flake8==7.3.0;python_version>="3.9"',
+ 'flake8==7.3.0',
"mccabe==0.7.0",
- 'pyflakes==2.5.0;python_version<"3.9"',
- 'pyflakes==3.4.0;python_version>="3.9"',
- 'pycodestyle==2.9.1;python_version<"3.9"',
- 'pycodestyle==2.14.0;python_version>="3.9"',
+ 'pyflakes==3.4.0',
+ 'pycodestyle==2.14.0',
],
# pip install -e .[ipdb]
# (Not needed for debugging anymore. SeleniumBase now includes "pdbp".)
@@ -275,26 +253,20 @@
# pip install -e .[mss]
# (An optional library for tile_windows() in CDP Mode.)
"mss": [
- 'mss==9.0.2;python_version<"3.9"',
- 'mss==10.0.0;python_version>="3.9"',
+ 'mss==10.1.0',
],
# pip install -e .[pdfminer]
# (An optional library for parsing PDF files.)
"pdfminer": [
- 'pdfminer.six==20250324;python_version<"3.9"',
- 'pdfminer.six==20251107;python_version>="3.9"',
- 'cryptography==39.0.2;python_version<"3.9"',
- 'cryptography==46.0.3;python_version>="3.9"',
- 'cffi==1.17.1;python_version<"3.9"',
- 'cffi==2.0.0;python_version>="3.9"',
- 'pycparser==2.22;python_version<"3.9"',
- 'pycparser==2.23;python_version>="3.9"',
+ 'pdfminer.six==20251107',
+ 'cryptography==46.0.3',
+ 'cffi==2.0.0',
+ 'pycparser==2.23',
],
# pip install -e .[pillow]
# (An optional library for image-processing.)
"pillow": [
- 'Pillow>=10.4.0;python_version<"3.9"',
- 'Pillow>=11.3.0;python_version>="3.9" and python_version<"3.10"',
+ 'Pillow>=11.3.0;python_version<"3.10"',
'Pillow>=12.0.0;python_version>="3.10"',
],
# pip install -e .[pip-system-certs]
@@ -309,9 +281,14 @@
"proxy": [
"proxy.py==2.4.3", # 2.4.4 did not have "Listening on ..."
],
+ # pip install -e .[playwright]
+ # (For the Playwright integration.)
+ "playwright": [
+ "playwright>=1.56.0",
+ ],
# pip install -e .[psutil]
"psutil": [
- "psutil==7.1.2",
+ "psutil>=7.1.3",
],
# pip install -e .[pyautogui]
# (Already a required dependency on Linux now.)