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 @@ + + +

Stealthy Playwright 🎭

+ +🎭 Stealthy Playwright Mode is a special mode of SeleniumBase that launches Playwright from SeleniumBase CDP Mode in order to grant Playwright new stealth features, such as the ability to click CAPTCHA checkboxes successfully. Playwright uses 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). + +-------- + +SeleniumBasePlaywright 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 @@ + + +

SeleniumBase CDP Mode Methods (CDP Mode API Reference)

+ +Here's a list of SeleniumBase CDP Mode method definitions, which are defined in **[sb_cdp.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/core/sb_cdp.py)** + +### 🐙 CDP Mode API / Methods + +```python +sb.cdp.get(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) +sb.cdp.locator(selector, best_match=False, timeout=None) +sb.cdp.find_element_by_text(text, tag_name=None, timeout=None) +sb.cdp.find_all(selector, timeout=None) +sb.cdp.find_elements_by_text(text, tag_name=None) +sb.cdp.select(selector, timeout=None) +sb.cdp.select_all(selector, timeout=None) +sb.cdp.find_elements(selector, timeout=None) +sb.cdp.find_visible_elements(selector, timeout=None) +sb.cdp.click(selector, timeout=None) +sb.cdp.click_if_visible(selector) +sb.cdp.click_visible_elements(selector, limit=0) +sb.cdp.click_nth_element(selector, number) +sb.cdp.click_nth_visible_element(selector, number) +sb.cdp.click_with_offset(selector, x, y, center=False) +sb.cdp.click_link(link_text) +sb.cdp.go_back() +sb.cdp.go_forward() +sb.cdp.get_navigation_history() +sb.cdp.tile_windows(windows=None, max_columns=0) +sb.cdp.grant_permissions(permissions, origin=None) +sb.cdp.grant_all_permissions() +sb.cdp.reset_permissions() +sb.cdp.get_all_cookies(*args, **kwargs) +sb.cdp.set_all_cookies(*args, **kwargs) +sb.cdp.save_cookies(*args, **kwargs) +sb.cdp.load_cookies(*args, **kwargs) +sb.cdp.clear_cookies() +sb.cdp.sleep(seconds) +sb.cdp.bring_active_window_to_front() +sb.cdp.bring_to_front() +sb.cdp.get_active_element() +sb.cdp.get_active_element_css() +sb.cdp.click_active_element() +sb.cdp.mouse_click(selector, timeout=None) +sb.cdp.nested_click(parent_selector, selector) +sb.cdp.get_nested_element(parent_selector, selector) +sb.cdp.select_option_by_text(dropdown_selector, option) +sb.cdp.select_option_by_index(dropdown_selector, option) +sb.cdp.select_option_by_value(dropdown_selector, option) +sb.cdp.flash(selector, duration=1, color="44CC88", pause=0) +sb.cdp.highlight(selector) +sb.cdp.focus(selector) +sb.cdp.highlight_overlay(selector) +sb.cdp.get_parent(element) +sb.cdp.remove_element(selector) +sb.cdp.remove_from_dom(selector) +sb.cdp.remove_elements(selector) +sb.cdp.send_keys(selector, text, timeout=None) +sb.cdp.press_keys(selector, text, timeout=None) +sb.cdp.type(selector, text, timeout=None) +sb.cdp.set_value(selector, text, timeout=None) +sb.cdp.clear_input(selector, timeout=None) +sb.cdp.clear(selector, timeout=None) +sb.cdp.submit(selector) +sb.cdp.evaluate(expression) +sb.cdp.execute_script(expression) +sb.cdp.js_dumps(obj_name) +sb.cdp.maximize() +sb.cdp.minimize() +sb.cdp.medimize() +sb.cdp.set_window_rect(x, y, width, height) +sb.cdp.reset_window_size() +sb.cdp.open_new_window(url=None, switch_to=True) +sb.cdp.switch_to_window(window) +sb.cdp.switch_to_newest_window() +sb.cdp.open_new_tab(url=None, switch_to=True) +sb.cdp.switch_to_tab(tab) +sb.cdp.switch_to_newest_tab() +sb.cdp.close_active_tab() +sb.cdp.get_active_tab() +sb.cdp.get_tabs() +sb.cdp.get_window() +sb.cdp.get_text(selector) +sb.cdp.get_title() +sb.cdp.get_current_url() +sb.cdp.get_origin() +sb.cdp.get_html(include_shadow_dom=True) +sb.cdp.get_page_source(include_shadow_dom=True) +sb.cdp.get_user_agent() +sb.cdp.get_cookie_string() +sb.cdp.get_locale_code() +sb.cdp.get_local_storage_item(key) +sb.cdp.get_session_storage_item(key) +sb.cdp.get_screen_rect() +sb.cdp.get_window_rect() +sb.cdp.get_window_size() +sb.cdp.get_window_position() +sb.cdp.get_element_rect(selector, timeout=None) +sb.cdp.get_element_size(selector, timeout=None) +sb.cdp.get_element_position(selector, timeout=None) +sb.cdp.get_gui_element_rect(selector, timeout=None) +sb.cdp.get_gui_element_center(selector, timeout=None) +sb.cdp.get_document() +sb.cdp.get_flattened_document() +sb.cdp.get_element_attributes(selector) +sb.cdp.get_element_attribute(selector, attribute) +sb.cdp.get_attribute(selector, attribute) +sb.cdp.get_element_html(selector) +sb.cdp.get_mfa_code(totp_key=None) +sb.cdp.enter_mfa_code(selector, totp_key=None, timeout=None) +sb.cdp.activate_messenger() +sb.cdp.set_messenger_theme(theme="default", location="default") +sb.cdp.post_message(message, duration=None, pause=True, style="info") +sb.cdp.set_locale(locale) +sb.cdp.set_local_storage_item(key, value) +sb.cdp.set_session_storage_item(key, value) +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) +sb.cdp.gui_click_x_y(x, y, timeframe=0.25) +sb.cdp.gui_click_element(selector, timeframe=0.25) +sb.cdp.gui_click_with_offset(selector, x, y, timeframe=0.25, center=False) +sb.cdp.gui_click_captcha() +sb.cdp.gui_drag_drop_points(x1, y1, x2, y2, timeframe=0.35) +sb.cdp.gui_drag_and_drop(drag_selector, drop_selector, timeframe=0.35) +sb.cdp.gui_click_and_hold(selector, timeframe=0.35) +sb.cdp.gui_hover_x_y(x, y) +sb.cdp.gui_hover_element(selector) +sb.cdp.gui_hover_and_click(hover_selector, click_selector) +sb.cdp.internalize_links() +sb.cdp.is_checked(selector) +sb.cdp.is_selected(selector) +sb.cdp.check_if_unchecked(selector) +sb.cdp.select_if_unselected(selector) +sb.cdp.uncheck_if_checked(selector) +sb.cdp.unselect_if_selected(selector) +sb.cdp.is_element_present(selector) +sb.cdp.is_element_visible(selector) +sb.cdp.is_text_visible(text, selector="body") +sb.cdp.is_exact_text_visible(text, selector="body") +sb.cdp.wait_for_text(text, selector="body", timeout=None) +sb.cdp.wait_for_text_not_visible(text, selector="body", timeout=None) +sb.cdp.wait_for_element_visible(selector, timeout=None) +sb.cdp.wait_for_element(selector, timeout=None) +sb.cdp.wait_for_element_not_visible(selector, timeout=None) +sb.cdp.wait_for_element_absent(selector, timeout=None) +sb.cdp.wait_for_any_of_elements_visible(*args, **kwargs) +sb.cdp.wait_for_any_of_elements_present(*args, **kwargs) +sb.cdp.assert_any_of_elements_visible(*args, **kwargs) +sb.cdp.assert_any_of_elements_present(*args, **kwargs) +sb.cdp.assert_element(selector, timeout=None) +sb.cdp.assert_element_visible(selector, timeout=None) +sb.cdp.assert_element_present(selector, timeout=None) +sb.cdp.assert_element_absent(selector, timeout=None) +sb.cdp.assert_element_not_visible(selector, timeout=None) +sb.cdp.assert_element_attribute(selector, attribute, value=None) +sb.cdp.assert_title(title) +sb.cdp.assert_title_contains(substring) +sb.cdp.assert_url(url) +sb.cdp.assert_url_contains(substring) +sb.cdp.assert_text(text, selector="html", timeout=None) +sb.cdp.assert_exact_text(text, selector="html", timeout=None) +sb.cdp.assert_text_not_visible(text, selector="body", timeout=None) +sb.cdp.assert_true() +sb.cdp.assert_false() +sb.cdp.assert_equal(first, second) +sb.cdp.assert_not_equal(first, second) +sb.cdp.assert_in(first, second) +sb.cdp.assert_not_in(first, second) +sb.cdp.scroll_into_view(selector) +sb.cdp.scroll_to_y(y) +sb.cdp.scroll_by_y(y) +sb.cdp.scroll_to_top() +sb.cdp.scroll_to_bottom() +sb.cdp.scroll_up(amount=25) +sb.cdp.scroll_down(amount=25) +sb.cdp.save_page_source(name, folder=None) +sb.cdp.save_as_html(name, folder=None) +sb.cdp.save_screenshot(name, folder=None, selector=None) +sb.cdp.print_to_pdf(name, folder=None) +sb.cdp.save_as_pdf(name, folder=None) +``` + +â„šī¸ When available, calling `sb.METHOD()` redirects to `sb.cdp.METHOD()` because regular SB methods automatically call their CDP Mode counterparts to maintain stealth when CDP Mode is active. + +-------- + + + +### 🐙 Pure CDP Mode (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() +``` + +-------- + +SeleniumBase + +
SeleniumBase
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.)