diff --git a/examples/behave_bdd/features/simple.feature b/examples/behave_bdd/features/simple.feature new file mode 100644 index 00000000000..287b2874f4d --- /dev/null +++ b/examples/behave_bdd/features/simple.feature @@ -0,0 +1,13 @@ +Feature: SeleniumBase scenarios for the Simple App + + Scenario: Verify Simple App (log in / sign out) + Given Open "seleniumbase.io/simple/login" + And Clear Session Storage + And Type "demo_user" into "#username" + And Type "secret_pass" into "#password" + And Click 'a:contains("Sign in")' + And Assert exact text "Welcome!" in "h1" + And Assert element "img#image1" + And Highlight "#image1" + And Click link "Sign out" + And Assert text "signed out" in "#top_message" diff --git a/examples/hack_the_planet.py b/examples/hack_the_planet.py index 4fd78ee7f19..68059f0a0b9 100644 --- a/examples/hack_the_planet.py +++ b/examples/hack_the_planet.py @@ -284,12 +284,12 @@ def test_all_your_base_are_belong_to_us(self): self.open("https://wordpress.com/") zoom_out = "h1.is-page-header{zoom: 0.8;-moz-transform: scale(0.8);}" self.add_css_style(zoom_out) - zoom_in = "div.lp-is-cta-blue{zoom: 1.4;-moz-transform: scale(1.4);}" + zoom_in = "a.wp-element-button{zoom: 1.4;-moz-transform: scale(1.4);}" self.add_css_style(zoom_in) self.set_text_content("h1.is-page-header", aybabtu) - self.set_text_content("main div.lp-is-cta-blue", "Use SeleniumBase!") + self.set_text_content("a.wp-element-button", "Use SeleniumBase!") self.highlight("h1.is-page-header", loops=6, scroll=False) - self.highlight("main div.lp-is-cta-blue", loops=4, scroll=False) + self.highlight("a.wp-element-button", loops=4, scroll=False) self.open("https://seleniumbase.com/") self.set_text_content("h1", aybabtu) diff --git a/examples/raw_browser_launcher.py b/examples/raw_browser_launcher.py index 8d0c193ce6c..9069b9912f4 100644 --- a/examples/raw_browser_launcher.py +++ b/examples/raw_browser_launcher.py @@ -3,7 +3,7 @@ driver = Driver(browser="chrome", headless=False) try: - driver.get("https://seleniumbase.io/apps/calculator") + driver.open("seleniumbase.io/apps/calculator") driver.click('[id="4"]') driver.click('[id="2"]') driver.assert_text("42", "#output") @@ -13,7 +13,7 @@ driver = Driver() try: - driver.get("https://seleniumbase.github.io/demo_page") + driver.open("seleniumbase.github.io/demo_page") driver.highlight("h2") driver.type("#myTextInput", "Automation") driver.click("#checkBox1") diff --git a/examples/raw_driver_context.py b/examples/raw_driver_context.py index 32609eac026..ba4ec661793 100644 --- a/examples/raw_driver_context.py +++ b/examples/raw_driver_context.py @@ -2,18 +2,18 @@ from seleniumbase import DriverContext with DriverContext() as driver: - driver.get("https://seleniumbase.github.io/") + driver.open("seleniumbase.github.io/") driver.highlight('img[alt="SeleniumBase"]', loops=6) with DriverContext(browser="chrome", incognito=True) as driver: - driver.get("https://seleniumbase.io/apps/calculator") + driver.open("seleniumbase.io/apps/calculator") driver.click('[id="4"]') driver.click('[id="2"]') driver.assert_text("42", "#output") driver.highlight("#output", loops=6) with DriverContext() as driver: - driver.get("https://seleniumbase.github.io/demo_page") + driver.open("seleniumbase.github.io/demo_page") driver.highlight("h2") driver.type("#myTextInput", "Automation") driver.click("#checkBox1") diff --git a/examples/raw_login_context.py b/examples/raw_login_context.py new file mode 100644 index 00000000000..96c4612794d --- /dev/null +++ b/examples/raw_login_context.py @@ -0,0 +1,12 @@ +from seleniumbase import DriverContext + +with DriverContext() as driver: + driver.open("seleniumbase.io/simple/login") + driver.type("#username", "demo_user") + driver.type("#password", "secret_pass") + driver.click('a:contains("Sign in")') + driver.assert_exact_text("Welcome!", "h1") + driver.assert_element("img#image1") + driver.highlight("#image1") + driver.click_link("Sign out") + driver.assert_text("signed out", "#top_message") diff --git a/examples/raw_login_driver.py b/examples/raw_login_driver.py new file mode 100644 index 00000000000..43d53e4a14a --- /dev/null +++ b/examples/raw_login_driver.py @@ -0,0 +1,15 @@ +from seleniumbase import Driver + +driver = Driver() +try: + driver.open("seleniumbase.io/simple/login") + driver.type("#username", "demo_user") + driver.type("#password", "secret_pass") + driver.click('a:contains("Sign in")') + driver.assert_exact_text("Welcome!", "h1") + driver.assert_element("img#image1") + driver.highlight("#image1") + driver.click_link("Sign out") + driver.assert_text("signed out", "#top_message") +finally: + driver.quit() diff --git a/examples/raw_login_sb.py b/examples/raw_login_sb.py new file mode 100644 index 00000000000..a184626d564 --- /dev/null +++ b/examples/raw_login_sb.py @@ -0,0 +1,12 @@ +from seleniumbase import SB + +with SB() as sb: + sb.open("seleniumbase.io/simple/login") + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.click('a:contains("Sign in")') + sb.assert_exact_text("Welcome!", "h1") + sb.assert_element("img#image1") + sb.highlight("#image1") + sb.click_link("Sign out") + sb.assert_text("signed out", "#top_message") diff --git a/examples/raw_uc_mode.py b/examples/raw_uc_mode.py index 3cbfcbe24e1..6829a1f4797 100644 --- a/examples/raw_uc_mode.py +++ b/examples/raw_uc_mode.py @@ -11,7 +11,7 @@ if not sb.is_text_visible("OH YEAH, you passed!", "h1"): if sb.is_element_visible('iframe[src*="challenge"]'): with sb.frame_switch('iframe[src*="challenge"]'): - sb.click("area") + sb.click("span.mark") sb.sleep(4) sb.activate_demo_mode() sb.assert_text("OH YEAH, you passed!", "h1", timeout=3) diff --git a/examples/sb_fixture_tests.py b/examples/sb_fixture_tests.py new file mode 100644 index 00000000000..d03924fdff5 --- /dev/null +++ b/examples/sb_fixture_tests.py @@ -0,0 +1,25 @@ +# "sb" pytest fixture test in a method with no class +def test_sb_fixture_with_no_class(sb): + sb.open("seleniumbase.io/simple/login") + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.click('a:contains("Sign in")') + sb.assert_exact_text("Welcome!", "h1") + sb.assert_element("img#image1") + sb.highlight("#image1") + sb.click_link("Sign out") + sb.assert_text("signed out", "#top_message") + + +# "sb" pytest fixture test in a method inside a class +class Test_SB_Fixture: + def test_sb_fixture_inside_class(self, sb): + sb.open("seleniumbase.io/simple/login") + sb.type("#username", "demo_user") + sb.type("#password", "secret_pass") + sb.click('a:contains("Sign in")') + sb.assert_exact_text("Welcome!", "h1") + sb.assert_element("img#image1") + sb.highlight("#image1") + sb.click_link("Sign out") + sb.assert_text("signed out", "#top_message") diff --git a/examples/test_override_sb_fixture.py b/examples/test_override_sb_fixture.py index 66d121ef34d..5c6424fc3ab 100644 --- a/examples/test_override_sb_fixture.py +++ b/examples/test_override_sb_fixture.py @@ -7,6 +7,7 @@ def sb(request): from selenium import webdriver from seleniumbase import BaseCase from seleniumbase import config as sb_config + from seleniumbase.core import session_helper class BaseClass(BaseCase): def get_new_driver(self, *args, **kwargs): @@ -31,6 +32,11 @@ def tearDown(self): super().tearDown() if request.cls: + if sb_config.reuse_class_session: + the_class = str(request.cls).split(".")[-1].split("'")[0] + if the_class != sb_config._sb_class: + session_helper.end_reused_class_session_as_needed() + sb_config._sb_class = the_class request.cls.sb = BaseClass("base_method") request.cls.sb.setUp() request.cls.sb._needs_tearDown = True diff --git a/examples/test_sb_fixture.py b/examples/test_sb_fixture.py index 9666a45d4d2..4e618b03f79 100644 --- a/examples/test_sb_fixture.py +++ b/examples/test_sb_fixture.py @@ -1,6 +1,6 @@ # "sb" pytest fixture test in a method with no class def test_sb_fixture_with_no_class(sb): - sb.open("https://seleniumbase.io/help_docs/install/") + sb.open("seleniumbase.io/help_docs/install/") sb.type('input[aria-label="Search"]', "GUI Commander") sb.click('mark:contains("Commander")') sb.assert_title_contains("GUI / Commander") @@ -9,7 +9,7 @@ def test_sb_fixture_with_no_class(sb): # "sb" pytest fixture test in a method inside a class class Test_SB_Fixture: def test_sb_fixture_inside_class(self, sb): - sb.open("https://seleniumbase.io/help_docs/install/") + sb.open("seleniumbase.io/help_docs/install/") sb.type('input[aria-label="Search"]', "GUI Commander") sb.click('mark:contains("Commander")') sb.assert_title_contains("GUI / Commander") diff --git a/examples/test_scrape_bing.py b/examples/test_scrape_bing.py index 8e93d7421b5..611a61b0150 100644 --- a/examples/test_scrape_bing.py +++ b/examples/test_scrape_bing.py @@ -4,7 +4,7 @@ class ScrapeBingTests(BaseCase): def test_scrape_bing(self): - self.open(r"https://www.bing.com/search?q=SeleniumBase%20GitHub") + self.open("www.bing.com/search?q=SeleniumBase+GitHub&qs=n&form=QBRE") self.wait_for_element("main h2 a") soup = self.get_beautiful_soup() titles = [item.text for item in soup.select("main h2 a")] diff --git a/examples/test_simple_login.py b/examples/test_simple_login.py new file mode 100644 index 00000000000..2673d21d074 --- /dev/null +++ b/examples/test_simple_login.py @@ -0,0 +1,15 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + +class TestSimpleLogin(BaseCase): + def test_simple_login(self): + self.open("seleniumbase.io/simple/login") + self.type("#username", "demo_user") + self.type("#password", "secret_pass") + self.click('a:contains("Sign in")') + self.assert_exact_text("Welcome!", "h1") + self.assert_element("img#image1") + self.highlight("#image1") + self.click_link("Sign out") + self.assert_text("signed out", "#top_message") diff --git a/examples/uc_cdp_events.py b/examples/uc_cdp_events.py index 7faafe10224..745cf7a07e3 100644 --- a/examples/uc_cdp_events.py +++ b/examples/uc_cdp_events.py @@ -34,7 +34,7 @@ def test_display_cdp_events(self): except Exception: if self.is_element_visible('iframe[src*="challenge"]'): with self.frame_switch('iframe[src*="challenge"]'): - self.click("area") + self.click("span.mark") else: self.fail_me() try: diff --git a/examples/verify_undetected.py b/examples/verify_undetected.py index d447e0a2a40..738da308d14 100644 --- a/examples/verify_undetected.py +++ b/examples/verify_undetected.py @@ -29,7 +29,7 @@ def test_browser_is_undetected(self): except Exception: if self.is_element_visible('iframe[src*="challenge"]'): with self.frame_switch('iframe[src*="challenge"]'): - self.click("area") + self.click("span.mark") else: self.fail_me() try: diff --git a/help_docs/method_summary.md b/help_docs/method_summary.md index e5e7bd563d2..7cc7a449eba 100644 --- a/help_docs/method_summary.md +++ b/help_docs/method_summary.md @@ -41,6 +41,8 @@ self.send_keys(selector, text, by="css selector", timeout=None) # Duplicates: # self.add_text(selector, text, by="css selector", timeout=None) +self.press_keys(selector, text, by="css selector", timeout=None) + self.submit(selector, by="css selector") self.clear(selector, by="css selector", timeout=None) @@ -467,8 +469,12 @@ self.get_downloads_folder() self.get_browser_downloads_folder() +self.get_downloaded_files(regex=None, browser=False) + self.get_path_of_downloaded_file(file, browser=False) +self.get_data_from_downloaded_file(file, timeout=None, browser=False) + self.is_downloaded_file_present(file, browser=False) self.is_downloaded_file_regex_present(regex, browser=False) @@ -480,6 +486,8 @@ self.assert_downloaded_file(file, timeout=None, browser=False) self.assert_downloaded_file_regex(regex, timeout=None, browser=False) +self.assert_data_in_downloaded_file(data, file, timeout=None, browser=False) + self.assert_true(expr, msg=None) self.assert_false(expr, msg=None) diff --git a/help_docs/syntax_formats.md b/help_docs/syntax_formats.md index 76062f0806a..f1f4cee7b04 100644 --- a/help_docs/syntax_formats.md +++ b/help_docs/syntax_formats.md @@ -302,6 +302,7 @@ def sb(request): from selenium import webdriver from seleniumbase import BaseCase from seleniumbase import config as sb_config + from seleniumbase.core import session_helper class BaseClass(BaseCase): def get_new_driver(self, *args, **kwargs): @@ -326,6 +327,11 @@ def sb(request): super().tearDown() if request.cls: + if sb_config.reuse_class_session: + the_class = str(request.cls).split(".")[-1].split("'")[0] + if the_class != sb_config._sb_class: + session_helper.end_reused_class_session_as_needed() + sb_config._sb_class = the_class request.cls.sb = BaseClass("base_method") request.cls.sb.setUp() request.cls.sb._needs_tearDown = True @@ -877,18 +883,18 @@ This pure Python format gives you a raw webdriver in from seleniumbase import DriverContext with DriverContext() as driver: - driver.get("https://seleniumbase.github.io/") + driver.open("seleniumbase.github.io/") driver.highlight('img[alt="SeleniumBase"]', loops=6) with DriverContext(browser="chrome", incognito=True) as driver: - driver.get("https://seleniumbase.io/apps/calculator") + driver.open("seleniumbase.io/apps/calculator") driver.click('[id="4"]') driver.click('[id="2"]') driver.assert_text("42", "#output") driver.highlight("#output", loops=6) with DriverContext() as driver: - driver.get("https://seleniumbase.github.io/demo_page") + driver.open("seleniumbase.github.io/demo_page") driver.highlight("h2") driver.type("#myTextInput", "Automation") driver.click("#checkBox1") @@ -908,7 +914,7 @@ from seleniumbase import Driver driver = Driver(browser="chrome", headless=False) try: - driver.get("https://seleniumbase.io/apps/calculator") + driver.open("seleniumbase.io/apps/calculator") driver.click('[id="4"]') driver.click('[id="2"]') driver.assert_text("42", "#output") @@ -918,7 +924,7 @@ finally: driver = Driver() try: - driver.get("https://seleniumbase.github.io/demo_page") + driver.open("seleniumbase.github.io/demo_page") driver.highlight("h2") driver.type("#myTextInput", "Automation") driver.click("#checkBox1") diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt index 1bed333e6aa..0d17b251399 100644 --- a/mkdocs_build/requirements.txt +++ b/mkdocs_build/requirements.txt @@ -20,7 +20,7 @@ paginate==0.5.6 pyquery==2.0.0 readtime==3.0.0 mkdocs==1.5.3 -mkdocs-material==9.3.2 +mkdocs-material==9.4.1 mkdocs-exclude-search==0.6.5 mkdocs-simple-hooks==0.1.5 mkdocs-material-extensions==1.2 diff --git a/sbase/steps.py b/sbase/steps.py index dc8a4f4ea59..8e650493a69 100644 --- a/sbase/steps.py +++ b/sbase/steps.py @@ -1129,6 +1129,40 @@ def jquery_type(context, text, selector): sb.jquery_type(selector, text) +@step("Press keys '{text}' in '{selector}'") +@step('Press keys "{text}" in "{selector}"') +@step("Press keys '{text}' in \"{selector}\"") +@step('Press keys "{text}" in \'{selector}\'') +@step("Press keys '{text}' into '{selector}'") +@step('Press keys "{text}" into "{selector}"') +@step("Press keys '{text}' into \"{selector}\"") +@step('Press keys "{text}" into \'{selector}\'') +@step("In '{selector}' press keys '{text}'") +@step('In "{selector}" press keys "{text}"') +@step("In '{selector}' press keys \"{text}\"") +@step('In "{selector}" press keys \'{text}\'') +@step("Into '{selector}' press keys '{text}'") +@step('Into "{selector}" press keys "{text}"') +@step("Into '{selector}' press keys \"{text}\"") +@step('Into "{selector}" press keys \'{text}\'') +@step("Find '{selector}' and press keys '{text}'") +@step('Find "{selector}" and press keys "{text}"') +@step("Find '{selector}' and press keys \"{text}\"") +@step('Find "{selector}" and press keys \'{text}\'') +@step("User presses keys '{text}' in '{selector}'") +@step('User presses keys "{text}" in "{selector}"') +@step("User presses keys '{text}' in \"{selector}\"") +@step('User presses keys "{text}" in \'{selector}\'') +@step("User presses keys '{text}' into '{selector}'") +@step('User presses keys "{text}" into "{selector}"') +@step("User presses keys '{text}' into \"{selector}\"") +@step('User presses keys "{text}" into \'{selector}\'') +def press_keys(context, text, selector): + sb = context.sb + text = normalize_text(text) + sb.press_keys(selector, text) + + @step("Find '{selector}' and set {attribute} to '{value}'") @step('Find "{selector}" and set {attribute} to "{value}"') @step("Find '{selector}' and set {attribute} to \"{value}\"") diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index aee7e3174f2..b32009e3ce6 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.18.8" +__version__ = "4.18.9" diff --git a/seleniumbase/behave/behave_helper.py b/seleniumbase/behave/behave_helper.py index 8b9a3ea8554..8e45cae2514 100644 --- a/seleniumbase/behave/behave_helper.py +++ b/seleniumbase/behave/behave_helper.py @@ -119,6 +119,24 @@ def generate_gherkin(srt_actions): sb_actions.append( "jQuery type '%s' in '%s'" % (text, action[1]) ) + elif action[0] == "pkeys": + text = action[2].replace("\n", "\\n") + if '"' not in text and '"' not in action[1]: + sb_actions.append( + 'Press keys "%s" in "%s"' % (text, action[1]) + ) + elif '"' in text and '"' not in action[1]: + sb_actions.append( + 'Press keys \'%s\' in "%s"' % (text, action[1]) + ) + elif '"' not in text and '"' in action[1]: + sb_actions.append( + 'Press keys "%s" in \'%s\'' % (text, action[1]) + ) + elif '"' in text and '"' in action[1]: + sb_actions.append( + "Press keys '%s' in '%s'" % (text, action[1]) + ) elif action[0] == "hover": if '"' not in action[1]: sb_actions.append('Hover "%s"' % action[1]) diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 6bc7e4ddc50..6196a709da0 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -108,14 +108,18 @@ def make_driver_executable_if_not(driver_path): def extend_driver(driver): # Extend the driver with new methods driver.default_find_element = driver.find_element + driver.default_find_elements = driver.find_elements DM = sb_driver.DriverMethods(driver) driver.find_element = DM.find_element + driver.find_elements = DM.find_elements driver.locator = DM.locator page = types.SimpleNamespace() page.open = DM.open_url page.click = DM.click page.send_keys = DM.send_keys + page.press_keys = DM.press_keys page.type = DM.update_text + page.submit = DM.submit page.assert_element = DM.assert_element_visible page.assert_element_present = DM.assert_element_present page.assert_element_not_visible = DM.assert_element_not_visible @@ -131,6 +135,9 @@ def extend_driver(driver): page.is_text_visible = DM.is_text_visible page.is_exact_text_visible = DM.is_exact_text_visible page.get_text = DM.get_text + page.find_element = DM.find_element + page.find_elements = DM.find_elements + page.locator = DM.locator driver.page = page js = types.SimpleNamespace() js.js_click = DM.js_click @@ -142,8 +149,11 @@ def extend_driver(driver): driver.js = js driver.open = DM.open_url driver.click = DM.click + driver.click_link = DM.click_link driver.send_keys = DM.send_keys + driver.press_keys = DM.press_keys driver.type = DM.update_text + driver.submit = DM.submit driver.assert_element = DM.assert_element_visible driver.assert_element_present = DM.assert_element_present driver.assert_element_not_visible = DM.assert_element_not_visible @@ -215,7 +225,7 @@ def chromedriver_on_path(): return None -def get_uc_driver_version(): +def get_uc_driver_version(full=False): uc_driver_version = None if os.path.exists(LOCAL_UC_DRIVER): try: @@ -226,9 +236,13 @@ def get_uc_driver_version(): output = output.decode("latin1") else: output = output.decode("utf-8") + full_version = output.split(" ")[1] output = output.split(" ")[1].split(".")[0] if int(output) >= 72: - uc_driver_version = output + if full: + uc_driver_version = full_version + else: + uc_driver_version = output except Exception: pass return uc_driver_version @@ -2148,6 +2162,9 @@ def get_local_driver( } use_version = "latest" major_edge_version = None + saved_mev = None + use_br_version_for_edge = False + use_exact_version_for_edge = False try: if binary_location: try: @@ -2155,7 +2172,9 @@ def get_local_driver( detect_b_ver.get_browser_version_from_binary( binary_location ) - ).split(".")[0] + ) + saved_mev = major_edge_version + major_edge_version = saved_mev.split(".")[0] if len(major_edge_version) < 2: major_edge_version = None except Exception: @@ -2164,11 +2183,25 @@ def get_local_driver( br_app = "edge" major_edge_version = ( detect_b_ver.get_browser_version_from_os(br_app) - ).split(".")[0] + ) + saved_mev = major_edge_version + major_edge_version = major_edge_version.split(".")[0] if int(major_edge_version) < 80: major_edge_version = None + elif int(major_edge_version) >= 115: + if ( + driver_version == "browser" + and saved_mev + and len(saved_mev.split(".")) == 4 + ): + driver_version = saved_mev + use_br_version_for_edge = True except Exception: major_edge_version = None + if driver_version and "." in driver_version: + use_exact_version_for_edge = True + if use_br_version_for_edge: + major_edge_version = saved_mev if major_edge_version: use_version = major_edge_version edge_driver_version = None @@ -2184,19 +2217,20 @@ def get_local_driver( output = output.decode("utf-8") if output.split(" ")[0] == "MSEdgeDriver": # MSEdgeDriver VERSION - output = output.split(" ")[1].split(".")[0] + output = output.split(" ")[1] + if use_exact_version_for_edge: + edge_driver_version = output.split(" ")[0] + output = output.split(".")[0] elif output.split(" ")[0] == "Microsoft": - # Microsoft Edge WebDriver VERSION - if ( - "WebDriver 115.0" in output - and "115.0.1901.183" not in output - ): - edgedriver_upgrade_needed = True - output = output.split(" ")[3].split(".")[0] + output = output.split(" ")[3] + if use_exact_version_for_edge: + edge_driver_version = output.split(" ")[0] + output = output.split(".")[0] else: output = 0 if int(output) >= 2: - edge_driver_version = output + if not use_exact_version_for_edge: + edge_driver_version = output if driver_version == "keep": driver_version = edge_driver_version except Exception: @@ -2629,8 +2663,10 @@ def get_local_driver( ) use_version = "latest" major_chrome_version = None + saved_mcv = None full_ch_version = None full_ch_driver_version = None + use_br_version_for_uc = False try: if chrome_options.binary_location: try: @@ -2638,7 +2674,9 @@ def get_local_driver( detect_b_ver.get_browser_version_from_binary( chrome_options.binary_location, ) - ).split(".")[0] + ) + saved_mcv = major_chrome_version + major_chrome_version = saved_mcv.split(".")[0] if len(major_chrome_version) < 2: major_chrome_version = None except Exception: @@ -2648,6 +2686,7 @@ def get_local_driver( full_ch_version = ( detect_b_ver.get_browser_version_from_os(br_app) ) + saved_mcv = full_ch_version major_chrome_version = full_ch_version.split(".")[0] if int(major_chrome_version) < 67: major_chrome_version = None @@ -2657,6 +2696,15 @@ def get_local_driver( ): # chromedrivers 2.41 - 2.46 could be swapped with 72 major_chrome_version = "72" + elif int(major_chrome_version) >= 115: + if ( + driver_version == "browser" + and saved_mcv + and len(saved_mcv.split(".")) == 4 + ): + driver_version = saved_mcv + if is_using_uc(undetectable, browser_name): + use_br_version_for_uc = True except Exception: major_chrome_version = None if major_chrome_version: @@ -2707,7 +2755,11 @@ def get_local_driver( disable_build_check = True uc_driver_version = None if is_using_uc(undetectable, browser_name): - uc_driver_version = get_uc_driver_version() + if use_br_version_for_uc: + uc_driver_version = get_uc_driver_version(full=True) + full_ch_driver_version = uc_driver_version + else: + uc_driver_version = get_uc_driver_version() if multi_proxy: sb_config.multi_proxy = True if uc_driver_version and driver_version == "keep": @@ -2764,7 +2816,10 @@ def get_local_driver( ): full_ch_v_p = full_ch_version.split(".")[0:2] full_ch_driver_v_p = full_ch_driver_version.split(".")[0:2] - if full_ch_v_p == full_ch_driver_v_p: + if ( + full_ch_v_p == full_ch_driver_v_p + or driver_version == "keep" + ): browser_driver_close_match = True # If not ARM MAC and need to use uc_driver (and it's missing), # and already have chromedriver with the correct version, @@ -2811,6 +2866,12 @@ def get_local_driver( and use_version != "latest" # Browser version detected and uc_driver_version != use_version ) + or ( + full_ch_driver_version # Also used for the uc_driver + and driver_version + and len(str(driver_version).split(".")) == 4 + and full_ch_driver_version != driver_version + ) ): # chromedriver download needed in the seleniumbase/drivers dir from seleniumbase.console_scripts import sb_install diff --git a/seleniumbase/core/detect_b_ver.py b/seleniumbase/core/detect_b_ver.py index a575f167a61..8e728829308 100644 --- a/seleniumbase/core/detect_b_ver.py +++ b/seleniumbase/core/detect_b_ver.py @@ -235,6 +235,10 @@ def get_browser_version_from_binary(binary_location): binary_location = binary_location.replace(" ", r"\ ") cmd_mapping = binary_location + " --version" pattern = r"\d+\.\d+\.\d+" + quad_pattern = r"\d+\.\d+\.\d+\.\d+" + quad_version = read_version_from_cmd(cmd_mapping, quad_pattern) + if quad_version and len(str(quad_version)) >= 9: # Eg. 115.0.0.0 + return quad_version version = read_version_from_cmd(cmd_mapping, pattern) return version except Exception: @@ -332,6 +336,10 @@ def get_browser_version_from_os(browser_type): try: cmd_mapping = cmd_mapping[browser_type][os_name()] pattern = PATTERN[browser_type] + quad_pattern = r"\d+\.\d+\.\d+\.\d+" + quad_version = read_version_from_cmd(cmd_mapping, quad_pattern) + if quad_version and len(str(quad_version)) >= 9: # Eg. 115.0.0.0 + return quad_version version = read_version_from_cmd(cmd_mapping, pattern) return version except Exception: diff --git a/seleniumbase/core/recorder_helper.py b/seleniumbase/core/recorder_helper.py index 5daf08744ee..3a13729710c 100644 --- a/seleniumbase/core/recorder_helper.py +++ b/seleniumbase/core/recorder_helper.py @@ -96,12 +96,15 @@ def generate_sbase_code(srt_actions): action[0] == "input" or action[0] == "js_ty" or action[0] == "jq_ty" + or action[0] == "pkeys" ): method = "type" if action[0] == "js_ty": method = "js_type" elif action[0] == "jq_ty": method = "jquery_type" + elif action[0] == "pkeys": + method = "press_keys" text = action[2].replace("\n", "\\n") if '"' not in action[1] and '"' not in text: sb_actions.append( diff --git a/seleniumbase/core/sb_driver.py b/seleniumbase/core/sb_driver.py index 1175a828faf..3e298d6b26f 100644 --- a/seleniumbase/core/sb_driver.py +++ b/seleniumbase/core/sb_driver.py @@ -8,14 +8,26 @@ class DriverMethods(): def __init__(self, driver): self.driver = driver - def find_element(self, by, value=None): + def find_element(self, by=None, value=None): if not value: value = by by = "css selector" + elif not by: + by = "css selector" else: value, by = page_utils.swap_selector_and_by_if_reversed(value, by) return self.driver.default_find_element(by=by, value=value) + def find_elements(self, by=None, value=None): + if not value: + value = by + by = "css selector" + elif not by: + by = "css selector" + else: + value, by = page_utils.swap_selector_and_by_if_reversed(value, by) + return self.driver.default_find_elements(by=by, value=value) + def locator(self, selector, by=None): if not by: by = "css selector" @@ -35,12 +47,21 @@ def open_url(self, *args, **kwargs): def click(self, *args, **kwargs): page_actions.click(self.driver, *args, **kwargs) + def click_link(self, *args, **kwargs): + page_actions.click_link(self.driver, *args, **kwargs) + def send_keys(self, *args, **kwargs): page_actions.send_keys(self.driver, *args, **kwargs) + def press_keys(self, *args, **kwargs): + page_actions.press_keys(self.driver, *args, **kwargs) + def update_text(self, *args, **kwargs): page_actions.update_text(self.driver, *args, **kwargs) + def submit(self, *args, **kwargs): + page_actions.submit(self.driver, *args, **kwargs) + def assert_element_visible(self, *args, **kwargs): page_actions.assert_element_visible(self.driver, *args, **kwargs) diff --git a/seleniumbase/core/session_helper.py b/seleniumbase/core/session_helper.py index 09868e5324d..886baa8c40a 100644 --- a/seleniumbase/core/session_helper.py +++ b/seleniumbase/core/session_helper.py @@ -1,5 +1,4 @@ from seleniumbase import config as sb_config -from seleniumbase.fixtures import shared_utils def end_reused_class_session_as_needed(): @@ -10,11 +9,8 @@ def end_reused_class_session_as_needed(): and sb_config.shared_driver ): if ( - not shared_utils.is_windows() - or ( - hasattr(sb_config.shared_driver, "service") - and sb_config.shared_driver.service.process - ) + hasattr(sb_config.shared_driver, "service") + and sb_config.shared_driver.service.process ): try: sb_config.shared_driver.quit() diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 79535c8e68a..adbbdd8c438 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1072,11 +1072,54 @@ def send_keys(self, selector, text, by="css selector", timeout=None): selector, by = self.__recalculate_selector(selector, by) self.add_text(selector, text, by=by, timeout=timeout) + def press_keys(self, selector, text, by="css selector", timeout=None): + """Use send_keys() to press one key at a time.""" + self.wait_for_ready_state_complete() + element = self.wait_for_element_clickable( + selector, by=by, timeout=timeout + ) + if self.demo_mode: + selector, by = self.__recalculate_selector(selector, by) + css_selector = self.convert_to_css_selector(selector, by=by) + self.__demo_mode_highlight_if_active(css_selector, By.CSS_SELECTOR) + if self.recorder_mode and self.__current_url_is_recordable(): + if self.get_session_storage_item("pause_recorder") == "no": + css_selector = self.convert_to_css_selector(selector, by=by) + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + sel_tex = [css_selector, text] + action = ["pkeys", sel_tex, origin, time_stamp] + self.__extra_actions.append(action) + press_enter = False + if text.endswith("\n"): + text = text[:-1] + press_enter = True + for key in text: + element.send_keys(key) + if press_enter: + element.send_keys(Keys.RETURN) + if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: + if not self.undetectable: + self.wait_for_ready_state_complete() + else: + time.sleep(0.15) + if self.demo_mode: + if press_enter: + self.__demo_mode_pause_if_active() + else: + self.__demo_mode_pause_if_active(tiny=True) + elif self.slow_mode: + self.__slow_mode_pause_if_active() + elif self.__needs_minimum_wait(): + time.sleep(0.05) + if self.undetectable: + time.sleep(0.02) + def submit(self, selector, by="css selector"): """Alternative to self.driver.find_element_by_*(SELECTOR).submit()""" self.__check_scope() selector, by = self.__recalculate_selector(selector, by) - element = self.wait_for_element_visible( + element = self.wait_for_element_clickable( selector, by=by, timeout=settings.SMALL_TIMEOUT ) element.submit() @@ -4842,6 +4885,7 @@ def __process_recorded_actions(self): ext_actions.append("jq_cl") ext_actions.append("jq_ca") ext_actions.append("jq_ty") + ext_actions.append("pkeys") ext_actions.append("r_clk") ext_actions.append("as_el") ext_actions.append("as_ep") @@ -4908,6 +4952,9 @@ def __process_recorded_actions(self): if srt_actions[n][0] == "jq_ty": srt_actions[n][2] = srt_actions[n][1][1] srt_actions[n][1] = srt_actions[n][1][0] + if srt_actions[n][0] == "pkeys": + srt_actions[n][2] = srt_actions[n][1][1] + srt_actions[n][1] = srt_actions[n][1][0] if srt_actions[n][0] == "e_mfa": srt_actions[n][2] = srt_actions[n][1][1] srt_actions[n][1] = srt_actions[n][1][0] @@ -4922,6 +4969,7 @@ def __process_recorded_actions(self): and ( srt_actions[n - 1][0] == "js_ty" or srt_actions[n - 1][0] == "jq_ty" + or srt_actions[n - 1][0] == "pkeys" ) and srt_actions[n][2] == srt_actions[n - 1][2] ): @@ -5777,18 +5825,26 @@ def js_click( self.__extra_actions.append(action) if not all_matches: if ":contains\\(" not in css_selector: - self.__js_click(selector, by=by) + try: + self.__js_click(selector, by=by) + except Exception: + current_url = self.driver.current_url + if current_url == pre_action_url: + self.__js_click_element(element) else: try: self.__js_click_element(element) except Exception: self.wait_for_ready_state_complete() time.sleep(0.05) - element = self.wait_for_element_present( - selector, by, timeout=settings.SMALL_TIMEOUT - ) + current_url = self.driver.current_url + if current_url == pre_action_url: + element = self.wait_for_element_present( + selector, by, timeout=settings.SMALL_TIMEOUT + ) if ( - self.is_element_visible(selector) + current_url == pre_action_url + and self.is_element_visible(selector) and self.is_element_clickable(selector) ): try: @@ -5801,14 +5857,16 @@ def js_click( selector, by, timeout=settings.MINI_TIMEOUT ) self.__js_click_element(element) - else: + elif current_url == pre_action_url: try: self.__js_click_element(element) except Exception: - element = self.wait_for_element_present( - selector, by, timeout=settings.MINI_TIMEOUT - ) - self.__js_click_element(element) + current_url = self.driver.current_url + if current_url == pre_action_url: + element = self.wait_for_element_present( + selector, by, timeout=settings.MINI_TIMEOUT + ) + self.__js_click_element(element) else: if ":contains\\(" not in css_selector: self.__js_click_all(selector, by=by) @@ -6815,6 +6873,7 @@ def get_browser_downloads_folder(self): elif ( self.driver.capabilities["browserName"].lower() == "chrome" and int(self.get_chromedriver_version().split(".")[0]) >= 110 + and int(self.get_chromedriver_version().split(".")[0]) <= 112 and self.headless ): return os.path.abspath(".") @@ -6822,13 +6881,40 @@ def get_browser_downloads_folder(self): return download_helper.get_downloads_folder() return os.path.join(os.path.expanduser("~"), "downloads") + def get_downloaded_files(self, regex=None, browser=False): + """Returns a list of files in the [Downloads Folder]. + Depending on settings, that dir may have other files. + If regex is provided, uses that to filter results.""" + df = self.get_downloads_folder() + if browser: + df = self.get_browser_downloads_folder() + if not os.path.exists(df): + return [] + elif regex: + return [fn for fn in os.listdir(df) if re.match(regex, fn)] + else: + return os.listdir(df) + def get_path_of_downloaded_file(self, file, browser=False): - """Returns the OS path of the downloaded file.""" + """Returns the full OS path of the downloaded file.""" + self.__check_scope() if browser: return os.path.join(self.get_browser_downloads_folder(), file) else: return os.path.join(self.get_downloads_folder(), file) + def get_data_from_downloaded_file(self, file, timeout=None, browser=False): + """Returns the contents of the downloaded file specified.""" + self.assert_downloaded_file(file, timeout=timeout, browser=browser) + fpath = self.get_path_of_downloaded_file(file, browser=browser) + file_io_lock = fasteners.InterProcessLock( + constants.MultiBrowser.FILE_IO_LOCK + ) + with file_io_lock: + with open(fpath, "r") as f: + data = f.read().strip() + return data + def is_downloaded_file_present(self, file, browser=False): """Returns True if the file exists in the pre-set [Downloads Folder]. For browser click-initiated downloads, SeleniumBase will override @@ -6840,8 +6926,7 @@ def is_downloaded_file_present(self, file, browser=False): file - The filename of the downloaded file. browser - If True, uses the path set by click-initiated downloads. If False, uses the self.download_file(file_url) path. - Those paths are often the same. (browser-dependent) - (Default: False).""" + Those paths are usually the same. (browser-dependent).""" return os.path.exists( self.get_path_of_downloaded_file(file, browser=browser) ) @@ -6853,9 +6938,10 @@ def is_downloaded_file_regex_present(self, regex, browser=False): regex - The filename regex of the downloaded file. browser - If True, uses the path set by click-initiated downloads. If False, uses the self.download_file(file_url) path. - Those paths are often the same. (browser-dependent) - (Default: False).""" + Those paths are usually the same. (browser-dependent).""" df = self.get_downloads_folder() + if browser: + df = self.get_browser_downloads_folder() matches = [fn for fn in os.listdir(df) if re.match(regex, fn)] return len(matches) >= 1 @@ -6870,8 +6956,7 @@ def delete_downloaded_file_if_present(self, file, browser=False): file - The filename to be deleted from the [Downloads Folder]. browser - If True, uses the path set by click-initiated downloads. If False, uses the self.download_file(file_url) path. - Those paths are usually the same. (browser-dependent) - (Default: False).""" + Those paths are usually the same. (browser-dependent).""" if self.is_downloaded_file_present(file, browser=browser): file_path = self.get_path_of_downloaded_file(file, browser=browser) try: @@ -6891,8 +6976,7 @@ def delete_downloaded_file(self, file, browser=False): file - The filename to be deleted from the [Downloads Folder]. browser - If True, uses the path set by click-initiated downloads. If False, uses the self.download_file(file_url) path. - Those paths are usually the same. (browser-dependent) - (Default: False).""" + Those paths are usually the same. (browser-dependent).""" if self.is_downloaded_file_present(file, browser=browser): file_path = self.get_path_of_downloaded_file(file, browser=browser) try: @@ -6912,9 +6996,11 @@ def assert_downloaded_file(self, file, timeout=None, browser=False): timeout - The time (seconds) to wait for the download to complete. browser - If True, uses the path set by click-initiated downloads. If False, uses the self.download_file(file_url) path. - Those paths are often the same. (browser-dependent) - (Default: False).""" + Those paths are usually the same. (browser-dependent).""" self.__check_scope() + df = self.get_downloads_folder() + if browser: + df = self.get_browser_downloads_folder() if not timeout: timeout = settings.LARGE_TIMEOUT if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: @@ -6929,7 +7015,7 @@ def assert_downloaded_file(self, file, timeout=None, browser=False): self.assertTrue( os.path.exists(downloaded_file_path), "File [%s] was not found in the downloads folder [%s]!" - % (file, self.get_downloads_folder()), + % (file, df), ) found = True break @@ -6942,7 +7028,7 @@ def assert_downloaded_file(self, file, timeout=None, browser=False): message = ( "File {%s} was not found in the downloads folder {%s} " "after %s seconds! (Or the download didn't complete!)" - % (file, self.get_downloads_folder(), timeout) + % (file, df, timeout) ) page_actions.timeout_exception("NoSuchFileException", message) if self.recorder_mode and self.__current_url_is_recordable(): @@ -6969,8 +7055,7 @@ def assert_downloaded_file_regex(self, regex, timeout=None, browser=False): timeout - The time (seconds) to wait for the download to complete. browser - If True, uses the path set by click-initiated downloads. If False, uses the self.download_file(file_url) path. - Those paths are often the same. (browser-dependent) - (Default: False).""" + Those paths are usually the same. (browser-dependent).""" self.__check_scope() if not timeout: timeout = settings.LARGE_TIMEOUT @@ -6980,6 +7065,8 @@ def assert_downloaded_file_regex(self, regex, timeout=None, browser=False): stop_ms = start_ms + (timeout * 1000.0) found = False df = self.get_downloads_folder() + if browser: + df = self.get_browser_downloads_folder() for x in range(int(timeout)): shared_utils.check_if_time_limit_exceeded() try: @@ -6987,7 +7074,7 @@ def assert_downloaded_file_regex(self, regex, timeout=None, browser=False): self.assertTrue( len(matches) >= 1, "Regex [%s] was not found in the downloads folder [%s]!" - % (regex, self.get_downloads_folder()), + % (regex, df), ) found = True break @@ -7000,7 +7087,7 @@ def assert_downloaded_file_regex(self, regex, timeout=None, browser=False): message = ( "Regex {%s} was not found in the downloads folder {%s} " "after %s seconds! (Or the download didn't complete!)" - % (regex, self.get_downloads_folder(), timeout) + % (regex, df, timeout) ) page_actions.timeout_exception("NoSuchFileException", message) if self.demo_mode: @@ -7015,6 +7102,21 @@ def assert_downloaded_file_regex(self, regex, timeout=None, browser=False): except Exception: pass + def assert_data_in_downloaded_file( + self, data, file, timeout=None, browser=False + ): + """Assert that the expected data exists in the downloaded file.""" + self.assert_downloaded_file(file, timeout=timeout, browser=browser) + expected = data.strip() + actual = self.get_data_from_downloaded_file(file, browser=browser) + if expected not in actual: + message = ( + "Expected data [%s] is not in downloaded file [%s]!" + % (expected, file) + ) + raise Exception(message) + return True + def assert_true(self, expr, msg=None): """Asserts that the expression is True. Will raise an exception if the statement if False.""" diff --git a/seleniumbase/fixtures/page_actions.py b/seleniumbase/fixtures/page_actions.py index 3a39f67f238..dc6d8ff3511 100644 --- a/seleniumbase/fixtures/page_actions.py +++ b/seleniumbase/fixtures/page_actions.py @@ -28,6 +28,7 @@ from selenium.common.exceptions import NoSuchWindowException from selenium.common.exceptions import StaleElementReferenceException from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.keys import Keys from seleniumbase.common.exceptions import LinkTextNotFoundException from seleniumbase.common.exceptions import TextNotVisibleException from seleniumbase.config import settings @@ -1525,6 +1526,13 @@ def click(driver, selector, by="css selector", timeout=settings.SMALL_TIMEOUT): element.click() +def click_link(driver, link_text, timeout=settings.SMALL_TIMEOUT): + element = wait_for_element_clickable( + driver, link_text, by="link text", timeout=timeout + ) + element.click() + + def js_click( driver, selector, by="css selector", timeout=settings.SMALL_TIMEOUT ): @@ -1564,6 +1572,22 @@ def send_keys( element.submit() +def press_keys( + driver, selector, text, by="css selector", timeout=settings.LARGE_TIMEOUT +): + selector, by = page_utils.recalculate_selector(selector, by) + element = wait_for_element_clickable( + driver, selector, by=by, timeout=timeout + ) + if not text.endswith("\n"): + for key in text: + element.send_keys(key) + else: + for key in text[:-1]: + element.send_keys(key) + element.send_keys(Keys.RETURN) + + def update_text( driver, selector, text, by="css selector", timeout=settings.LARGE_TIMEOUT ): @@ -1579,6 +1603,14 @@ def update_text( element.submit() +def submit(driver, selector, by="css selector"): + selector, by = page_utils.recalculate_selector(selector, by) + element = wait_for_element_clickable( + driver, selector, by=by, timeout=settings.SMALL_TIMEOUT + ) + element.submit() + + def assert_element_visible( driver, selector, by="css selector", timeout=settings.SMALL_TIMEOUT ): diff --git a/seleniumbase/plugins/pytest_plugin.py b/seleniumbase/plugins/pytest_plugin.py index 86f5e66369e..cf50a6edbf4 100644 --- a/seleniumbase/plugins/pytest_plugin.py +++ b/seleniumbase/plugins/pytest_plugin.py @@ -1552,6 +1552,7 @@ def pytest_configure(config): sb_config._SMALL_TIMEOUT = settings.SMALL_TIMEOUT sb_config._LARGE_TIMEOUT = settings.LARGE_TIMEOUT sb_config.pytest_html_report = config.getoption("htmlpath") # --html=FILE + sb_config._sb_class = None # (Used with the sb fixture for "--rcs") sb_config._sb_node = {} # sb node dictionary (Used with the sb fixture) # Dashboard-specific variables sb_config._results = {} # SBase Dashboard test results @@ -2178,6 +2179,7 @@ def sb(request): Usage example: "def test_one(sb):" You may need to use this for tests that use other pytest fixtures.""" from seleniumbase import BaseCase + from seleniumbase.core import session_helper class BaseClass(BaseCase): def setUp(self): @@ -2191,6 +2193,11 @@ def base_method(self): pass if request.cls: + if sb_config.reuse_class_session: + the_class = str(request.cls).split(".")[-1].split("'")[0] + if the_class != sb_config._sb_class: + session_helper.end_reused_class_session_as_needed() + sb_config._sb_class = the_class request.cls.sb = BaseClass("base_method") request.cls.sb.setUp() request.cls.sb._needs_tearDown = True