diff --git a/README.md b/README.md index 543e87f..a0e75f3 100644 --- a/README.md +++ b/README.md @@ -97,9 +97,10 @@ s.driver.get('http://www.samplesite.com/sample/process') The driver object is a Selenium webdriver object, so you can use any of the normal selenium methods plus new methods added by Requestium. ```python -s.driver.find_element_by_xpath("//input[@class='user_name']").send_keys('James Bond', Keys.ENTER) +s.driver.find_element("xpath", "//input[@class='user_name']").send_keys('James Bond', Keys.ENTER) -# New method which waits for element to load instead of failing, useful for single page web apps +# New methods which wait for element to load instead of failing, useful for single page web apps +s.driver.ensure_element("xpath", "//div[@attribute='button']").click() s.driver.ensure_element_by_xpath("//div[@attribute='button']").click() ``` @@ -119,7 +120,7 @@ s.post('http://www.samplesite.com/sample2', data={'key1': 'value1'}) Requestium adds several 'ensure' methods to the driver object, as Selenium is known to be very finicky about selecting elements and cookie handling. ### Wait for element -The `ensure_element_by_` methods waits for the element to be loaded in the browser and returns it as soon as it loads. It's named after Selenium's `find_element_by_` methods (which immediately raise an exception if they can't find the element). +The `ensure_element` and `ensure_element_by_` methods wait for the element to be loaded in the browser and returns it as soon as it loads. They're named after Selenium's `find_element` and `find_element_by_` methods (which immediately raise an exception if they can't find the element). Requestium can wait for an element to be in any of the following states: - present (default) @@ -127,16 +128,17 @@ Requestium can wait for an element to be in any of the following states: - visible - invisible (useful for things like waiting for *loading...* gifs to disappear) -These methods are very useful for single page web apps where the site is dynamically changing its elements. We usually end up completely replacing our `find_element_by_` calls with `ensure_element_by_` calls as they are more flexible. +These methods are very useful for single page web apps where the site is dynamically changing its elements. We usually end up completely replacing our `find_element` and `find_element_by_` calls with `ensure_element` and `ensure_element_by_` calls as they are more flexible. Elements you get using these methods have the new `ensure_click` method which makes the click less prone to failure. This helps with getting through a lot of the problems with Selenium clicking. ```python -s.driver.ensure_element_by_xpath("//li[@class='b1']", state='clickable', timeout=5).ensure_click() +s.driver.ensure_element("xpath", "//li[@class='b1']", state='clickable', timeout=5).ensure_click() # === We also added these methods named in accordance to Selenium's api design === # ensure_element_by_id # ensure_element_by_name +# ensure_element_by_xpath # ensure_element_by_link_text # ensure_element_by_partial_link_text # ensure_element_by_tag_name @@ -187,18 +189,18 @@ reddit_user_name = '' s = Session('./chromedriver', default_timeout=15) s.driver.get('http://reddit.com') -s.driver.find_element_by_xpath("//a[@href='https://www.reddit.com/login']").click() +s.driver.find_element("xpath", "//a[@href='https://www.reddit.com/login']").click() print('Waiting for elements to load...') -s.driver.ensure_element_by_class_name("desktop-onboarding-sign-up__form-toggler", +s.driver.ensure_element("class name", "desktop-onboarding-sign-up__form-toggler", state='visible').click() if reddit_user_name: - s.driver.ensure_element_by_id('user_login').send_keys(reddit_user_name) - s.driver.ensure_element_by_id('passwd_login').send_keys(Keys.BACKSPACE) + s.driver.ensure_element('id', 'user_login').send_keys(reddit_user_name) + s.driver.ensure_element('id', 'passwd_login').send_keys(Keys.BACKSPACE) print('Please log-in in the chrome browser') -s.driver.ensure_element_by_class_name("desktop-onboarding__title", timeout=60, state='invisible') +s.driver.ensure_element("class name", "desktop-onboarding__title", timeout=60, state='invisible') print('Thanks!') if not reddit_user_name: @@ -233,7 +235,7 @@ reddit_user_name = '' driver = webdriver.Chrome('./chromedriver') driver.get('http://reddit.com') -driver.find_element_by_xpath("//a[@href='https://www.reddit.com/login']").click() +driver.find_element("xpath", "//a[@href='https://www.reddit.com/login']").click() print('Waiting for elements to load...') WebDriverWait(driver, 15).until( @@ -244,7 +246,7 @@ if reddit_user_name: WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.ID, 'user_login')) ).send_keys(reddit_user_name) - driver.find_element_by_id('passwd_login').send_keys(Keys.BACKSPACE) + driver.find_element('id', 'passwd_login').send_keys(Keys.BACKSPACE) print('Please log-in in the chrome browser') try: diff --git a/requestium/requestium.py b/requestium/requestium.py index c7ecd86..6941f51 100644 --- a/requestium/requestium.py +++ b/requestium/requestium.py @@ -1,6 +1,7 @@ import functools import time import types +import warnings import requests import tldextract @@ -257,30 +258,30 @@ def is_cookie_in_driver(self, cookie): return False def ensure_element_by_id(self, selector, state="present", timeout=None): - return self.ensure_element('id', selector, state, timeout) + return self.ensure_element(By.ID, selector, state, timeout) def ensure_element_by_name(self, selector, state="present", timeout=None): - return self.ensure_element('name', selector, state, timeout) + return self.ensure_element(By.NAME, selector, state, timeout) def ensure_element_by_xpath(self, selector, state="present", timeout=None): - return self.ensure_element('xpath', selector, state, timeout) + return self.ensure_element(By.XPATH, selector, state, timeout) def ensure_element_by_link_text(self, selector, state="present", timeout=None): - return self.ensure_element('link_text', selector, state, timeout) + return self.ensure_element(By.LINK_TEXT, selector, state, timeout) def ensure_element_by_partial_link_text(self, selector, state="present", timeout=None): - return self.ensure_element('partial_link_text', selector, state, timeout) + return self.ensure_element(By.PARTIAL_LINK_TEXT, selector, state, timeout) def ensure_element_by_tag_name(self, selector, state="present", timeout=None): - return self.ensure_element('tag_name', selector, state, timeout) + return self.ensure_element(By.TAG_NAME, selector, state, timeout) def ensure_element_by_class_name(self, selector, state="present", timeout=None): - return self.ensure_element('class_name', selector, state, timeout) + return self.ensure_element(By.CLASS_NAME, selector, state, timeout) def ensure_element_by_css_selector(self, selector, state="present", timeout=None): - return self.ensure_element('css_selector', selector, state, timeout) + return self.ensure_element(By.CSS_SELECTOR, selector, state, timeout) - def ensure_element(self, locator, selector, state="present", timeout=None): + def ensure_element(self, locator: str, selector: str, state: str = "present", timeout=None): """This method allows us to wait till an element appears or disappears in the browser The webdriver runs in parallel with our scripts, so we must wait for it everytime it @@ -289,6 +290,8 @@ def ensure_element(self, locator, selector, state="present", timeout=None): So we must explicitly wait in that case. The 'locator' argument defines what strategy we use to search for the element. + It expects standard names from the By class in selenium.webdriver.common.by. + https://www.selenium.dev/selenium/docs/api/py/webdriver/selenium.webdriver.common.by.html The 'state' argument allows us to chose between waiting for the element to be visible, clickable, present, or invisible. Presence is more inclusive, but sometimes we want to @@ -299,15 +302,23 @@ def ensure_element(self, locator, selector, state="present", timeout=None): More info at: http://selenium-python.readthedocs.io/waits.html """ - locators = {'id': By.ID, - 'name': By.NAME, - 'xpath': By.XPATH, - 'link_text': By.LINK_TEXT, - 'partial_link_text': By.PARTIAL_LINK_TEXT, - 'tag_name': By.TAG_NAME, - 'class_name': By.CLASS_NAME, - 'css_selector': By.CSS_SELECTOR} - locator = locators[locator] + locators_compatibility = { + 'link_text': By.LINK_TEXT, + 'partial_link_text': By.PARTIAL_LINK_TEXT, + 'tag_name': By.TAG_NAME, + 'class_name': By.CLASS_NAME, + 'css_selector': By.CSS_SELECTOR + } + if locator in locators_compatibility: + warnings.warn( + """ + Support for locator strategy names with underscores is deprecated. + Use strategies from Selenium's By class (importable from selenium.webdriver.common.by). + """, + DeprecationWarning + ) + locator = locators_compatibility[locator] + if not timeout: timeout = self.default_timeout diff --git a/requirements.txt b/requirements.txt index 1004e85..8ff3530 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -parsel>=1.7.0 -requests>=2.28.1 -selenium>=4.6.0 -tldextract>=3.4.0 +parsel>=1.8.1 +requests>=2.31.0 +selenium>=4.15.2 +tldextract>=5.1.1 diff --git a/tests/test_ensure_elements_deprecation.py b/tests/test_ensure_elements_deprecation.py new file mode 100644 index 0000000..96ce208 --- /dev/null +++ b/tests/test_ensure_elements_deprecation.py @@ -0,0 +1,32 @@ +import shutil + +import pytest +import selenium +from selenium.webdriver.common.by import By + +import requestium + +chrome_webdriver_path = shutil.which('chromedriver') + +chrome_webdriver = selenium.webdriver.chrome.webdriver.WebDriver() +firefox_webdriver = selenium.webdriver.firefox.webdriver.WebDriver() + +session_parameters = [ + {'webdriver_path': chrome_webdriver_path}, + {'webdriver_path': chrome_webdriver_path, 'headless': True}, + {'driver': chrome_webdriver}, + {'driver': firefox_webdriver}, +] + + +@pytest.fixture(params=session_parameters) +def session(request): + session = requestium.Session(**request.param) + yield session + session.driver.close() + + +def test_deprecation_warning_for_ensure_element_locators_with_underscores(session): + session.driver.get('http://the-internet.herokuapp.com') + with pytest.warns(DeprecationWarning): + session.driver.ensure_element("class_name", 'no-js') diff --git a/tests/test_requestium.py b/tests/test_requestium.py index 2d0026a..a18caf3 100644 --- a/tests/test_requestium.py +++ b/tests/test_requestium.py @@ -1,16 +1,16 @@ +import shutil + import pytest import selenium -import shutil +from selenium.webdriver.common.by import By import requestium - chrome_webdriver_path = shutil.which('chromedriver') chrome_webdriver = selenium.webdriver.chrome.webdriver.WebDriver() firefox_webdriver = selenium.webdriver.firefox.webdriver.WebDriver() - session_parameters = [ {'webdriver_path': chrome_webdriver_path}, {'webdriver_path': chrome_webdriver_path, 'headless': True}, @@ -28,8 +28,8 @@ def session(request): def test_simple_page_load(session): session.driver.get('http://the-internet.herokuapp.com') - session.driver.ensure_element('id', 'content') + session.driver.ensure_element(By.ID, 'content') title = session.driver.title - heading = session.driver.find_element('xpath', '//*[@id="content"]/h1') + heading = session.driver.find_element(By.XPATH, '//*[@id="content"]/h1') assert title == 'The Internet' assert heading.text == 'Welcome to the-internet'