Skip to content

Commit

Permalink
Standardize and document ensure_element method of Session (#75)
Browse files Browse the repository at this point in the history
* ensure_element documentation updates
* internal use of up-to-date locator strategy names
* backwards compatibility for underscored locator strategies
* test to ensure deprecation warning is raised for old strategy names
* update requirements.txt to match poetry
  • Loading branch information
bmos committed Feb 4, 2024
1 parent 0b9945f commit 3f535ed
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 39 deletions.
26 changes: 14 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
```

Expand All @@ -119,24 +120,25 @@ 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)
- clickable
- 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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand Down
47 changes: 29 additions & 18 deletions requestium/requestium.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import functools
import time
import types
import warnings

import requests
import tldextract
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down
8 changes: 4 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions tests/test_ensure_elements_deprecation.py
Original file line number Diff line number Diff line change
@@ -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')
10 changes: 5 additions & 5 deletions tests/test_requestium.py
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -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'

0 comments on commit 3f535ed

Please sign in to comment.