diff --git a/README.md b/README.md
index 183a2e687d9..d186fdfe40f 100755
--- a/README.md
+++ b/README.md
@@ -1,8 +1,10 @@

+**WebDriver automation simplified by extending Python's unittest framework.**
+
[](https://pypi.python.org/pypi/seleniumbase) [](https://travis-ci.org/seleniumbase/SeleniumBase) [](https://gitter.im/seleniumbase/SeleniumBase?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-**The Future of Web Automation & Testing**
+SeleniumBase simplifies web automation & testing with WebDriver in the same way that jQuery, AnglularJS, and ReactJS simplify web development with JavaScript. All tests using SeleniumBase's BaseCase class inherit Python's unittest.TestCase class, which allows for running tests automatically with Pytest and Nosetest. This framework can use the Page Object Model for test structure, as well as all features of WebDriver and Python's unittest.

diff --git a/_config.yml b/_config.yml
index 1ad8bd0dbff..41a41321f34 100644
--- a/_config.yml
+++ b/_config.yml
@@ -1,3 +1,3 @@
theme: jekyll-theme-cayman
title: SeleniumBase
-description: The Future of Web Automation & Testing
\ No newline at end of file
+description: WebDriver automation simplified by extending Python's unittest framework.
\ No newline at end of file
diff --git a/examples/boilerplates/base_test_case.py b/examples/boilerplates/base_test_case.py
new file mode 100755
index 00000000000..f7c87fe5aad
--- /dev/null
+++ b/examples/boilerplates/base_test_case.py
@@ -0,0 +1,43 @@
+'''
+You can use this as a boilerplate for your test framework.
+Define your customized library methods in a master class like this.
+Then have all your test classes inherit it.
+BaseTestCase will inherit SeleniumBase methods from BaseCase.
+'''
+
+from seleniumbase import BaseCase
+
+
+class BaseTestCase(BaseCase):
+
+ def setUp(self):
+ super(BaseTestCase, self).setUp()
+ # Add custom setUp code for your tests AFTER the super().setUp()
+
+ def tearDown(self):
+ # Add custom tearDown code for your tests BEFORE the super().tearDown()
+ super(BaseTestCase, self).tearDown()
+
+ def login_to_site(self):
+ # <<< Placeholder for actual code. Add your code here. >>>
+ # Add frequently used methods like this in your base test case class.
+ # This reduces the amount of duplicated code in your tests.
+ # If the UI changes, the fix only needs to be applied in one place.
+ pass
+
+ def example_method(self):
+ # <<< Placeholder for actual code. Add your code here. >>>
+ pass
+
+
+'''
+# Now you can do something like this in your test files:
+
+from base_test_case import BaseTestCase
+
+class MyTests(BaseTestCase):
+
+ def test_example(self):
+ self.login_to_site()
+ self.example_method()
+'''
diff --git a/examples/boilerplates/boilerplate_test.py b/examples/boilerplates/boilerplate_test.py
new file mode 100755
index 00000000000..862ccecead0
--- /dev/null
+++ b/examples/boilerplates/boilerplate_test.py
@@ -0,0 +1,10 @@
+from .base_test_case import BaseTestCase
+from .page_objects import HomePage
+
+
+class MyTestClass(BaseTestCase):
+
+ def test_boilerplate(self):
+ self.login_to_site()
+ self.example_method()
+ self.assert_element(HomePage.html)
diff --git a/examples/boilerplates/master_test_case.py b/examples/boilerplates/master_test_case.py
deleted file mode 100755
index a931836cfff..00000000000
--- a/examples/boilerplates/master_test_case.py
+++ /dev/null
@@ -1,37 +0,0 @@
-'''
-You can use this as a boilerplate for your test framework.
-Define your customized library methods in a master class like this.
-Then have all your test classes inherit it.
-MasterTestCase will inherit SeleniumBase methods from BaseCase.
-'''
-
-from seleniumbase import BaseCase
-
-
-class MasterTestCase(BaseCase):
-
- def setUp(self):
- super(MasterTestCase, self).setUp()
-
- def login_to_site(self):
- # Add frequently used methods like this in your master class.
- # This reduces the amount of duplicated code in your tests.
- # If the UI changes, the fix only needs to be applied in one place.
- pass
-
- def example_method(self):
- # Add your code here.
- pass
-
-
-'''
-# Now you can do something like this in your test files:
-
-from master_test_case import MasterTestCase
-
-class MyTests(MasterTestCase):
-
- def test_example(self):
- self.login_to_site()
- self.example_method()
-'''
diff --git a/examples/boilerplates/page_objects.py b/examples/boilerplates/page_objects.py
index 928a1efbc57..3fea9c37ffa 100755
--- a/examples/boilerplates/page_objects.py
+++ b/examples/boilerplates/page_objects.py
@@ -6,6 +6,7 @@
class HomePage(object):
+ html = "html"
ok_button = "#ok"
cancel_button = "#cancel"
see_items_button = "button.items"
@@ -26,13 +27,13 @@ class CheckoutPage(object):
'''
# Now you can do something like this in your test files:
-from master_class import MasterTestCase
-from page_objects import HomePage, ShoppingPage, CheckoutPage
+from .base_test_case import BaseTestCase
+from .page_objects import HomePage, ShoppingPage, CheckoutPage
-class MyTests(MasterTestCase):
+class MyTests(BaseTestCase):
def test_example(self):
- self.open(RANDOM_SHOPPING_WEBSITE)
+ self.login_to_site()
self.click(HomePage.see_items_button)
self.click(ShoppingPage.buyable_item)
self.click(ShoppingPage.add_to_cart)
diff --git a/examples/boilerplates/samples/google_test.py b/examples/boilerplates/samples/google_test.py
index bf4aea0f89d..c84554476b3 100755
--- a/examples/boilerplates/samples/google_test.py
+++ b/examples/boilerplates/samples/google_test.py
@@ -3,7 +3,7 @@
'''
from seleniumbase import BaseCase
-from google_objects import HomePage, ResultsPage
+from .google_objects import HomePage, ResultsPage
class GoogleTests(BaseCase):
diff --git a/help_docs/method_summary.md b/help_docs/method_summary.md
index 6815538d3b2..cbb60565b1b 100755
--- a/help_docs/method_summary.md
+++ b/help_docs/method_summary.md
@@ -16,8 +16,16 @@ self.double_click(selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT)
self.click_chain(selectors_list, by=By.CSS_SELECTOR,
timeout=settings.SMALL_TIMEOUT, spacing=0)
+self.is_link_text_present(link_text)
+
+self.get_href_from_link_text(link_text)
+
+self.wait_for_href_from_link_text(link_text, timeout=settings.SMALL_TIMEOUT)
+
self.click_link_text(link_text, timeout=settings.SMALL_TIMEOUT)
+self.click_link(link_text, timeout=settings.SMALL_TIMEOUT)
+
self.click_partial_link_text(partial_link_text, timeout=settings.SMALL_TIMEOUT)
self.get_text(selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT)
@@ -61,6 +69,10 @@ self.is_text_visible(text, selector, by=By.CSS_SELECTOR)
self.find_visible_elements(selector, by=By.CSS_SELECTOR)
+self.is_element_in_frame(selector, by=By.CSS_SELECTOR)
+
+self.enter_frame_of_element(selector, by=By.CSS_SELECTOR)
+
self.execute_script(script)
self.set_window_size(width, height)
@@ -69,6 +81,8 @@ self.maximize_window()
self.activate_jquery()
+self.bring_to_front(selector, by=By.CSS_SELECTOR)
+
self.highlight(selector, by=By.CSS_SELECTOR, loops=4, scroll=True)
self.scroll_to(selector, by=By.CSS_SELECTOR)
@@ -81,10 +95,24 @@ self.click_xpath(xpath)
self.jquery_click(selector, by=By.CSS_SELECTOR)
+self.hide_element(selector, by=By.CSS_SELECTOR)
+
+self.hide_elements(selector, by=By.CSS_SELECTOR)
+
+self.show_element(selector, by=By.CSS_SELECTOR)
+
+self.show_elements(selector, by=By.CSS_SELECTOR)
+
+self.remove_element(selector, by=By.CSS_SELECTOR)
+
+self.remove_elements(selector, by=By.CSS_SELECTOR)
+
self.jq_format(code)
self.get_domain_url(url)
+self.safe_execute_script(script)
+
self.download_file(file_url, destination_folder=None)
self.save_file_as(file_url, new_file_name, destination_folder=None)
@@ -110,7 +138,7 @@ self.jquery_update_text_value(selector, new_value, by=By.CSS_SELECTOR,
self.jquery_update_text(selector, new_value, by=By.CSS_SELECTOR,
timeout=settings.SMALL_TIMEOUT)
-self.hover_on_element(selector)
+self.hover_on_element(selector, by=By.CSS_SELECTOR)
self.hover_and_click(hover_selector, click_selector,
hover_by=By.CSS_SELECTOR, click_by=By.CSS_SELECTOR,
diff --git a/integrations/selenium_ide/convert_ide.py b/integrations/selenium_ide/convert_ide.py
index 2ad5fcc99a9..753f98bc9e2 100755
--- a/integrations/selenium_ide/convert_ide.py
+++ b/integrations/selenium_ide/convert_ide.py
@@ -256,7 +256,7 @@ def main():
if '(u"' in line:
uni = "u"
has_unicode = True
- command = '''%sself.click_link_text(%s"%s")''' % (
+ command = '''%sself.click(%s"link=%s")''' % (
whitespace, uni, link_text)
seleniumbase_lines.append(command)
continue
@@ -468,7 +468,7 @@ def main():
# quote_type = data.group(1)
link_text = data.group(2)
if int(line_num) < num_lines - 2:
- regex_string = (r'''^\s*self.click_link_text\(["|']'''
+ regex_string = (r'''^\s*self.click\(["|']link='''
+ re.escape(link_text) + r'''["|']\)\s*$''')
data2 = re.match(regex_string, lines[line_num+1])
if data2:
@@ -489,7 +489,7 @@ def main():
out_file = codecs.open(converted_file_name, "w+")
out_file.writelines(seleniumbase_code)
out_file.close()
- print("%s successfully created from %s\n" % (
+ print('>>> "%s" successfully created from %s\n' % (
converted_file_name, webdriver_python_file))
diff --git a/pytest.ini b/pytest.ini
index 080bba9b203..6be9156871a 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,2 +1,3 @@
[pytest]
+# Let console output be seen. Don't override the pytest plugin.
addopts = --capture=no --ignore conftest.py
diff --git a/requirements.txt b/requirements.txt
index 9b3963980ac..7d7f5f46417 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
pip>=9.0.1
setuptools>=38.5.1
ipython==5.4.1
-selenium==3.8.0
+selenium==3.8.1
nose==1.3.7
pytest==3.4.0
pytest-html==1.16.1
diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py
index 50645d07510..daa3caa857f 100755
--- a/seleniumbase/core/browser_launcher.py
+++ b/seleniumbase/core/browser_launcher.py
@@ -1,3 +1,4 @@
+import warnings
from selenium import webdriver
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
@@ -112,10 +113,13 @@ def get_remote_driver(browser_name, headless, servername, port):
desired_capabilities=(
webdriver.DesiredCapabilities.SAFARI))
if browser_name == constants.Browser.PHANTOM_JS:
- return webdriver.Remote(
- command_executor=address,
- desired_capabilities=(
- webdriver.DesiredCapabilities.PHANTOMJS))
+ with warnings.catch_warnings():
+ # Ignore "PhantomJS has been deprecated" UserWarning
+ warnings.simplefilter("ignore", category=UserWarning)
+ return webdriver.Remote(
+ command_executor=address,
+ desired_capabilities=(
+ webdriver.DesiredCapabilities.PHANTOMJS))
def get_local_driver(browser_name, headless):
@@ -158,7 +162,10 @@ def get_local_driver(browser_name, headless):
if browser_name == constants.Browser.SAFARI:
return webdriver.Safari()
if browser_name == constants.Browser.PHANTOM_JS:
- return webdriver.PhantomJS()
+ with warnings.catch_warnings():
+ # Ignore "PhantomJS has been deprecated" UserWarning
+ warnings.simplefilter("ignore", category=UserWarning)
+ return webdriver.PhantomJS()
if browser_name == constants.Browser.GOOGLE_CHROME:
try:
chrome_options = webdriver.ChromeOptions()
diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py
index 1d38b9f0531..d09809e9316 100755
--- a/seleniumbase/fixtures/base_case.py
+++ b/seleniumbase/fixtures/base_case.py
@@ -94,6 +94,13 @@ def click(self, selector, by=By.CSS_SELECTOR,
timeout = self._get_new_timeout(timeout)
if page_utils.is_xpath_selector(selector):
by = By.XPATH
+ if page_utils.is_link_text_selector(selector):
+ selector = page_utils.get_link_text_from_selector(selector)
+ by = By.LINK_TEXT
+ if not self.is_link_text_visible(selector):
+ # Handle a special case of links hidden in dropdowns
+ self.click_link_text(selector, timeout=timeout)
+ return
element = page_actions.wait_for_element_visible(
self.driver, selector, by, timeout=timeout)
self._demo_mode_highlight_if_active(selector, by)
@@ -161,6 +168,59 @@ def click_chain(self, selectors_list, by=By.CSS_SELECTOR,
if spacing > 0:
time.sleep(spacing)
+ def is_link_text_present(self, link_text):
+ """ Returns True if the link text appears in the HTML of the page.
+ The element doesn't need to be visible,
+ such as elements hidden inside a dropdown selection. """
+ self.wait_for_ready_state_complete()
+ source = self.driver.page_source
+ soup = BeautifulSoup(source, "html.parser")
+ html_links = soup.find_all('a')
+ for html_link in html_links:
+ if html_link.text == link_text:
+ if html_link.has_attr('href'):
+ return True
+ return False
+
+ def get_href_from_link_text(self, link_text):
+ self.wait_for_ready_state_complete()
+ source = self.driver.page_source
+ soup = BeautifulSoup(source, "html.parser")
+ html_links = soup.find_all('a')
+ for html_link in html_links:
+ if html_link.text == link_text:
+ if html_link.has_attr('href'):
+ href = html_link.get('href')
+ if href.startswith('//'):
+ link = "http:" + href
+ elif href.startswith('/'):
+ url = self.driver.current_url
+ domain_url = self.get_domain_url(url)
+ link = domain_url + href
+ else:
+ link = href
+ return link
+ raise Exception(
+ 'Could not parse link from link_text [%s]' % link_text)
+ raise Exception("Link Text [%s] was not found!" % link_text)
+
+ def wait_for_href_from_link_text(self, link_text,
+ timeout=settings.SMALL_TIMEOUT):
+ start_ms = time.time() * 1000.0
+ stop_ms = start_ms + (timeout * 1000.0)
+ for x in range(int(timeout * 5)):
+ try:
+ href = self.get_href_from_link_text(link_text)
+ return href
+ except Exception:
+ now_ms = time.time() * 1000.0
+ if now_ms >= stop_ms:
+ break
+ time.sleep(0.2)
+ raise Exception(
+ "Link text [%s] was not present after %s seconds!" % (
+ link_text, timeout))
+
def click_link_text(self, link_text, timeout=settings.SMALL_TIMEOUT):
""" This method clicks link text on a page """
# If using phantomjs, might need to extract and open the link directly
@@ -171,38 +231,28 @@ def click_link_text(self, link_text, timeout=settings.SMALL_TIMEOUT):
element = self.wait_for_link_text_visible(link_text)
element.click()
return
- source = self.driver.page_source
- soup = BeautifulSoup(source, "html.parser")
- html_links = soup.find_all('a')
- for html_link in html_links:
- if html_link.text == link_text:
- if html_link.has_attr('href'):
- href = html_link.get('href')
- if href.startswith('//'):
- link = "http:" + href
- elif href.startswith('/'):
- url = self.driver.current_url
- domain_url = self.get_domain_url(url)
- link = domain_url + href
- else:
- link = href
- self.open(link)
- return
- raise Exception(
- 'Could not parse link from link_text [%s]' % link_text)
- raise Exception("Link text [%s] was not found!" % link_text)
- # Not using phantomjs
- element = self.wait_for_link_text_visible(link_text, timeout=timeout)
- self._demo_mode_highlight_if_active(link_text, by=By.LINK_TEXT)
+ self.open(self.get_href_from_link_text(link_text))
+ return
+ self.wait_for_href_from_link_text(link_text, timeout=timeout)
pre_action_url = self.driver.current_url
try:
- element.click()
- except (StaleElementReferenceException, ENI_Exception):
- self.wait_for_ready_state_complete()
- time.sleep(0.05)
element = self.wait_for_link_text_visible(
- link_text, timeout=timeout)
- element.click()
+ link_text, timeout=0.2)
+ self._demo_mode_highlight_if_active(link_text, by=By.LINK_TEXT)
+ try:
+ element.click()
+ except (StaleElementReferenceException, ENI_Exception):
+ self.wait_for_ready_state_complete()
+ time.sleep(0.05)
+ element = self.wait_for_link_text_visible(
+ link_text, timeout=timeout)
+ element.click()
+ except Exception:
+ # The link text is probably hidden under a dropdown menu
+ if not self._click_dropdown_link_text(link_text):
+ element = self.wait_for_link_text_visible(
+ link_text, timeout=settings.MINI_TIMEOUT)
+ element.click()
if settings.WAIT_FOR_RSC_ON_CLICKS:
self.wait_for_ready_state_complete()
if self.demo_mode:
@@ -211,6 +261,10 @@ def click_link_text(self, link_text, timeout=settings.SMALL_TIMEOUT):
else:
self._demo_mode_pause_if_active(tiny=True)
+ def click_link(self, link_text, timeout=settings.SMALL_TIMEOUT):
+ """ Same as self.click_link_text() """
+ self.click_link_text(link_text, timeout=timeout)
+
def click_partial_link_text(self, partial_link_text,
timeout=settings.SMALL_TIMEOUT):
""" This method clicks the partial link text on a page. """
@@ -223,7 +277,7 @@ def click_partial_link_text(self, partial_link_text,
element.click()
return
source = self.driver.page_source
- soup = BeautifulSoup(source)
+ soup = BeautifulSoup(source, "html.parser")
html_links = soup.fetch('a')
for html_link in html_links:
if partial_link_text in html_link.text:
@@ -293,6 +347,9 @@ def get_attribute(self, selector, attribute, by=By.CSS_SELECTOR,
timeout = self._get_new_timeout(timeout)
if page_utils.is_xpath_selector(selector):
by = By.XPATH
+ if page_utils.is_link_text_selector(selector):
+ selector = page_utils.get_link_text_from_selector(selector)
+ by = By.LINK_TEXT
self.wait_for_ready_state_complete()
time.sleep(0.01)
element = page_actions.wait_for_element_present(
@@ -466,11 +523,17 @@ def update_text(self, selector, new_value, by=By.CSS_SELECTOR,
def is_element_present(self, selector, by=By.CSS_SELECTOR):
if page_utils.is_xpath_selector(selector):
by = By.XPATH
+ if page_utils.is_link_text_selector(selector):
+ selector = page_utils.get_link_text_from_selector(selector)
+ by = By.LINK_TEXT
return page_actions.is_element_present(self.driver, selector, by)
def is_element_visible(self, selector, by=By.CSS_SELECTOR):
if page_utils.is_xpath_selector(selector):
by = By.XPATH
+ if page_utils.is_link_text_selector(selector):
+ selector = page_utils.get_link_text_from_selector(selector)
+ by = By.LINK_TEXT
return page_actions.is_element_visible(self.driver, selector, by)
def is_link_text_visible(self, link_text):
@@ -490,14 +553,68 @@ def is_text_visible(self, text, selector, by=By.CSS_SELECTOR):
time.sleep(0.01)
if page_utils.is_xpath_selector(selector):
by = By.XPATH
+ if page_utils.is_link_text_selector(selector):
+ selector = page_utils.get_link_text_from_selector(selector)
+ by = By.LINK_TEXT
return page_actions.is_text_visible(self.driver, text, selector, by)
def find_visible_elements(self, selector, by=By.CSS_SELECTOR):
""" Returns a list of matching WebElements that are visible. """
if page_utils.is_xpath_selector(selector):
by = By.XPATH
+ if page_utils.is_link_text_selector(selector):
+ selector = page_utils.get_link_text_from_selector(selector)
+ by = By.LINK_TEXT
return page_actions.find_visible_elements(self.driver, selector, by)
+ def is_element_in_frame(self, selector, by=By.CSS_SELECTOR):
+ """ Returns True if the selector's element is located in an iFrame.
+ Otherwise returns False. """
+ selector, by = self._recalculate_selector(selector, by)
+ if self.is_element_present(selector, by=by):
+ return False
+ source = self.driver.page_source
+ soup = BeautifulSoup(source, "html.parser")
+ iframe_list = soup.select('iframe')
+ for iframe in iframe_list:
+ iframe_identifier = None
+ if iframe.has_attr('name') and len(iframe['name']) > 0:
+ iframe_identifier = iframe['name']
+ elif iframe.has_attr('id') and len(iframe['id']) > 0:
+ iframe_identifier = iframe['id']
+ else:
+ continue
+ self.switch_to_frame(iframe_identifier)
+ if self.is_element_present(selector, by=by):
+ self.switch_to_default_content()
+ return True
+ self.switch_to_default_content()
+ return False
+
+ def enter_frame_of_element(self, selector, by=By.CSS_SELECTOR):
+ """ Returns the frame name of the selector's element if in an iFrame.
+ Also enters the iFrame if the element was inside an iFrame.
+ If the element is not in an iFrame, returns None. """
+ selector, by = self._recalculate_selector(selector, by)
+ if self.is_element_present(selector, by=by):
+ return None
+ source = self.driver.page_source
+ soup = BeautifulSoup(source, "html.parser")
+ iframe_list = soup.select('iframe')
+ for iframe in iframe_list:
+ iframe_identifier = None
+ if iframe.has_attr('name') and len(iframe['name']) > 0:
+ iframe_identifier = iframe['name']
+ elif iframe.has_attr('id') and len(iframe['id']) > 0:
+ iframe_identifier = iframe['id']
+ else:
+ continue
+ self.switch_to_frame(iframe_identifier)
+ if self.is_element_present(selector, by=by):
+ return iframe_identifier
+ self.switch_to_default_content()
+ return None
+
def execute_script(self, script):
return self.driver.execute_script(script)
@@ -541,6 +658,24 @@ def activate_jquery(self):
# Since jQuery still isn't activating, give up and raise an exception
raise Exception("Exception: WebDriver could not activate jQuery!")
+ def bring_to_front(self, selector, by=By.CSS_SELECTOR):
+ """ Updates the Z-index of a page element to bring it into view.
+ Useful when getting a WebDriverException, such as the one below:
+ { Element is not clickable at point (#, #).
+ Other element would receive the click: ... } """
+ if page_utils.is_xpath_selector(selector):
+ by = By.XPATH
+ self.find_element(selector, by=by, timeout=settings.SMALL_TIMEOUT)
+ try:
+ selector = self.convert_to_css_selector(selector, by=by)
+ except Exception:
+ # Don't perform action if can't convert to CSS_SELECTOR for jQuery
+ return
+
+ script = ("""document.querySelector('%s').style.zIndex = "1";"""
+ % selector)
+ self.execute_script(script)
+
def highlight(self, selector, by=By.CSS_SELECTOR,
loops=settings.HIGHLIGHTS, scroll=True):
""" This method uses fancy javascript to highlight an element.
@@ -552,6 +687,7 @@ def highlight(self, selector, by=By.CSS_SELECTOR,
(Default: 4. Each loop lasts for about 0.18s)
scroll - the option to scroll to the element first (Default: True)
"""
+ selector, by = self._recalculate_selector(selector, by)
element = self.find_element(
selector, by=by, timeout=settings.SMALL_TIMEOUT)
if scroll:
@@ -561,11 +697,7 @@ def highlight(self, selector, by=By.CSS_SELECTOR,
except Exception:
# Don't highlight if can't convert to CSS_SELECTOR for jQuery
return
-
- # Only get the first match
- last_syllable = selector.split(' ')[-1]
- if ':' not in last_syllable:
- selector += ':first'
+ selector = self._make_css_match_first_element_only(selector)
o_bs = '' # original_box_shadow
style = element.get_attribute('style')
@@ -578,11 +710,8 @@ def highlight(self, selector, by=By.CSS_SELECTOR,
script = """jQuery('%s').css('box-shadow',
'0px 0px 6px 6px rgba(128, 128, 128, 0.5)');""" % selector
- try:
- self.execute_script(script)
- except Exception:
- self.activate_jquery()
- self.execute_script(script)
+ self.safe_execute_script(script)
+
if self.highlights:
loops = self.highlights
loops = int(loops)
@@ -653,34 +782,79 @@ def click_xpath(self, xpath):
self.click(xpath, by=By.XPATH)
def jquery_click(self, selector, by=By.CSS_SELECTOR):
- if page_utils.is_xpath_selector(selector):
- by = By.XPATH
+ selector, by = self._recalculate_selector(selector, by)
selector = self.convert_to_css_selector(selector, by=by)
self.wait_for_element_present(
selector, by=by, timeout=settings.SMALL_TIMEOUT)
if self.is_element_visible(selector, by=by):
self._demo_mode_highlight_if_active(selector, by)
-
- # Only get the first match
- last_syllable = selector.split(' ')[-1]
- if ':' not in last_syllable:
- selector += ':first'
-
+ selector = self._make_css_match_first_element_only(selector)
click_script = """jQuery('%s')[0].click()""" % selector
- try:
- self.execute_script(click_script)
- except Exception:
- # The likely reason this fails is because: "jQuery is not defined"
- self.activate_jquery() # It's a good thing we can define it here
- self.execute_script(click_script)
+ self.safe_execute_script(click_script)
self._demo_mode_pause_if_active()
+ def hide_element(self, selector, by=By.CSS_SELECTOR):
+ """ Hide the first element on the page that matches the selector. """
+ selector, by = self._recalculate_selector(selector, by)
+ selector = self.convert_to_css_selector(selector, by=by)
+ selector = self._make_css_match_first_element_only(selector)
+ hide_script = """jQuery('%s').hide()""" % selector
+ self.safe_execute_script(hide_script)
+
+ def hide_elements(self, selector, by=By.CSS_SELECTOR):
+ """ Hide all elements on the page that match the selector. """
+ selector, by = self._recalculate_selector(selector, by)
+ selector = self.convert_to_css_selector(selector, by=by)
+ hide_script = """jQuery('%s').hide()""" % selector
+ self.safe_execute_script(hide_script)
+
+ def show_element(self, selector, by=By.CSS_SELECTOR):
+ """ Show the first element on the page that matches the selector. """
+ selector, by = self._recalculate_selector(selector, by)
+ selector = self.convert_to_css_selector(selector, by=by)
+ selector = self._make_css_match_first_element_only(selector)
+ show_script = """jQuery('%s').show(0)""" % selector
+ self.safe_execute_script(show_script)
+
+ def show_elements(self, selector, by=By.CSS_SELECTOR):
+ """ Show all elements on the page that match the selector. """
+ selector, by = self._recalculate_selector(selector, by)
+ selector = self.convert_to_css_selector(selector, by=by)
+ show_script = """jQuery('%s').show(0)""" % selector
+ self.safe_execute_script(show_script)
+
+ def remove_element(self, selector, by=By.CSS_SELECTOR):
+ """ Remove the first element on the page that matches the selector. """
+ selector, by = self._recalculate_selector(selector, by)
+ selector = self.convert_to_css_selector(selector, by=by)
+ selector = self._make_css_match_first_element_only(selector)
+ remove_script = """jQuery('%s').remove()""" % selector
+ self.safe_execute_script(remove_script)
+
+ def remove_elements(self, selector, by=By.CSS_SELECTOR):
+ """ Remove all elements on the page that match the selector. """
+ selector, by = self._recalculate_selector(selector, by)
+ selector = self.convert_to_css_selector(selector, by=by)
+ remove_script = """jQuery('%s').remove()""" % selector
+ self.safe_execute_script(remove_script)
+
def jq_format(self, code):
return page_utils.jq_format(code)
def get_domain_url(self, url):
return page_utils.get_domain_url(url)
+ def safe_execute_script(self, script):
+ """ When executing a script that contains a jQuery command,
+ it's important that the jQuery library has been loaded first.
+ This method will load jQuery if it wasn't already loaded. """
+ try:
+ self.execute_script(script)
+ except Exception:
+ # The likely reason this fails is because: "jQuery is not defined"
+ self.activate_jquery() # It's a good thing we can define it here
+ self.execute_script(script)
+
def download_file(self, file_url, destination_folder=None):
""" Downloads the file from the url to the destination folder.
If no destination folder is specified, the default one is used. """
@@ -752,19 +926,9 @@ def set_value(self, selector, new_value, by=By.CSS_SELECTOR,
self._demo_mode_highlight_if_active(selector, by)
self.scroll_to(selector, by=by, timeout=timeout)
value = json.dumps(new_value)
-
- # Only get the first match
- last_syllable = selector.split(' ')[-1]
- if ':' not in last_syllable:
- selector += ':first'
-
+ selector = self._make_css_match_first_element_only(selector)
set_value_script = """jQuery('%s').val(%s)""" % (selector, value)
- try:
- self.execute_script(set_value_script)
- except Exception:
- # The likely reason this fails is because: "jQuery is not defined"
- self.activate_jquery() # It's a good thing we can define it here
- self.execute_script(set_value_script)
+ self.safe_execute_script(set_value_script)
self._demo_mode_pause_if_active()
def jquery_update_text_value(self, selector, new_value, by=By.CSS_SELECTOR,
@@ -782,20 +946,10 @@ def jquery_update_text_value(self, selector, new_value, by=By.CSS_SELECTOR,
self._demo_mode_highlight_if_active(selector, by)
self.scroll_to(selector, by=by)
selector = self.convert_to_css_selector(selector, by=by)
-
- # Only get the first match
- last_syllable = selector.split(' ')[-1]
- if ':' not in last_syllable:
- selector += ':first'
-
+ selector = self._make_css_match_first_element_only(selector)
update_text_script = """jQuery('%s').val('%s')""" % (
selector, self.jq_format(new_value))
- try:
- self.execute_script(update_text_script)
- except Exception:
- # The likely reason this fails is because: "jQuery is not defined"
- self.activate_jquery() # It's a good thing we can define it here
- self.execute_script(update_text_script)
+ self.safe_execute_script(update_text_script)
if new_value.endswith('\n'):
element.send_keys('\n')
self._demo_mode_pause_if_active()
@@ -810,6 +964,8 @@ def jquery_update_text(self, selector, new_value, by=By.CSS_SELECTOR,
selector, new_value, by=by, timeout=timeout)
def hover_on_element(self, selector, by=By.CSS_SELECTOR):
+ if page_utils.is_xpath_selector(selector):
+ by = By.XPATH
self.wait_for_element_visible(
selector, by=by, timeout=settings.SMALL_TIMEOUT)
self._demo_mode_highlight_if_active(selector, by)
@@ -887,12 +1043,14 @@ def generate_referral(self, start_page, destination_page):
% start_page)
self.open(start_page)
time.sleep(0.08)
- referral_link = (''''''
- '''Generate Free Referral!''' % destination_page)
+ referral_link = (''''''
+ '''* Magic Link Button! *''' % destination_page)
self.execute_script(
'''document.body.innerHTML = \"%s\"''' % referral_link)
time.sleep(0.1)
- self.click("a.analytics") # Clicks the generated button
+ self.click("a.analytics.referral.test") # Clicks the generated button
time.sleep(0.12)
def generate_traffic(self, start_page, destination_page, loops=1):
@@ -911,6 +1069,9 @@ def wait_for_element_present(self, selector, by=By.CSS_SELECTOR,
timeout = self._get_new_timeout(timeout)
if page_utils.is_xpath_selector(selector):
by = By.XPATH
+ if page_utils.is_link_text_selector(selector):
+ selector = page_utils.get_link_text_from_selector(selector)
+ by = By.LINK_TEXT
return page_actions.wait_for_element_present(
self.driver, selector, by, timeout)
@@ -936,6 +1097,9 @@ def wait_for_element_visible(self, selector, by=By.CSS_SELECTOR,
The element must be visible (it cannot be hidden). """
if page_utils.is_xpath_selector(selector):
by = By.XPATH
+ if page_utils.is_link_text_selector(selector):
+ selector = page_utils.get_link_text_from_selector(selector)
+ by = By.LINK_TEXT
return page_actions.wait_for_element_visible(
self.driver, selector, by, timeout)
@@ -974,6 +1138,9 @@ def wait_for_text_visible(self, text, selector, by=By.CSS_SELECTOR,
timeout = self._get_new_timeout(timeout)
if page_utils.is_xpath_selector(selector):
by = By.XPATH
+ if page_utils.is_link_text_selector(selector):
+ selector = page_utils.get_link_text_from_selector(selector)
+ by = By.LINK_TEXT
return page_actions.wait_for_text_visible(
self.driver, text, selector, by, timeout)
@@ -1102,6 +1269,9 @@ def wait_for_element_not_visible(self, selector, by=By.CSS_SELECTOR,
timeout = self._get_new_timeout(timeout)
if page_utils.is_xpath_selector(selector):
by = By.XPATH
+ if page_utils.is_link_text_selector(selector):
+ selector = page_utils.get_link_text_from_selector(selector)
+ by = By.LINK_TEXT
return page_actions.wait_for_element_not_visible(
self.driver, selector, by, timeout)
@@ -1283,6 +1453,32 @@ def process_checks(self, print_only=False):
############
+ def _click_dropdown_link_text(self, link_text):
+ """ When a link is hidden under a dropdown menu, use this. """
+ href = self.wait_for_href_from_link_text(link_text)
+ source = self.driver.page_source
+ soup = BeautifulSoup(source, "html.parser")
+ drop_down_list = soup.select('[class*=dropdown]')
+ for item in drop_down_list:
+ if link_text in item.text.split('\n') and href in item.decode():
+ dropdown_css = ""
+ for css_class in item['class']:
+ dropdown_css += '.'
+ dropdown_css += css_class
+ dropdown_css = item.name + dropdown_css
+ link_css = '[href="%s"]' % href
+ matching_dropdowns = self.find_visible_elements(dropdown_css)
+ for dropdown in matching_dropdowns:
+ # The same class names might be used for multiple dropdowns
+ try:
+ page_actions.hover_element_and_click(
+ self.driver, dropdown, link_css,
+ click_by=By.CSS_SELECTOR, timeout=0.2)
+ return True
+ except Exception:
+ pass
+ return False
+
def _pick_select_option(self, dropdown_selector, option,
dropdown_by=By.CSS_SELECTOR, option_by="text",
timeout=settings.SMALL_TIMEOUT):
@@ -1321,6 +1517,22 @@ def _pick_select_option(self, dropdown_selector, option,
############
+ def _recalculate_selector(self, selector, by):
+ # Try to determine the type of selector automatically
+ if page_utils.is_xpath_selector(selector):
+ by = By.XPATH
+ if page_utils.is_link_text_selector(selector):
+ selector = page_utils.get_link_text_from_selector(selector)
+ by = By.LINK_TEXT
+ return (selector, by)
+
+ def _make_css_match_first_element_only(self, selector):
+ # Only get the first match
+ last_syllable = selector.split(' ')[-1]
+ if ':' not in last_syllable and ':contains' not in selector:
+ selector += ':first'
+ return selector
+
def _demo_mode_pause_if_active(self, tiny=False):
if self.demo_mode:
if self.demo_sleep:
diff --git a/seleniumbase/fixtures/page_actions.py b/seleniumbase/fixtures/page_actions.py
index 0c9c959becd..066c3c53c9c 100755
--- a/seleniumbase/fixtures/page_actions.py
+++ b/seleniumbase/fixtures/page_actions.py
@@ -138,6 +138,31 @@ def hover_and_click(driver, hover_selector, click_selector,
(click_selector, timeout))
+def hover_element_and_click(driver, element, click_selector,
+ click_by=By.CSS_SELECTOR,
+ timeout=settings.SMALL_TIMEOUT):
+ """
+ Similar to hover_and_click(), but assumes top element is already found.
+ """
+ start_ms = time.time() * 1000.0
+ stop_ms = start_ms + (timeout * 1000.0)
+ hover = ActionChains(driver).move_to_element(element)
+ hover.perform()
+ for x in range(int(timeout * 10)):
+ try:
+ element = driver.find_element(by=click_by,
+ value="%s" % click_selector).click()
+ return element
+ except Exception:
+ now_ms = time.time() * 1000.0
+ if now_ms >= stop_ms:
+ break
+ time.sleep(0.1)
+ raise NoSuchElementException(
+ "Element {%s} was not present after %s seconds!" %
+ (click_selector, timeout))
+
+
def wait_for_element_present(driver, selector, by=By.CSS_SELECTOR,
timeout=settings.LARGE_TIMEOUT):
"""
diff --git a/seleniumbase/fixtures/page_utils.py b/seleniumbase/fixtures/page_utils.py
index e35d7ff1e8b..148fabd07cf 100755
--- a/seleniumbase/fixtures/page_utils.py
+++ b/seleniumbase/fixtures/page_utils.py
@@ -43,6 +43,27 @@ def is_xpath_selector(selector):
return False
+def is_link_text_selector(selector):
+ """
+ A basic method to determine if a selector is a link text selector.
+ """
+ if (selector.startswith('link=') or
+ selector.startswith('link_text=')):
+ return True
+ return False
+
+
+def get_link_text_from_selector(selector):
+ """
+ A basic method to get the link text from a link text selector.
+ """
+ if selector.startswith('link='):
+ return selector.split('link=')[1]
+ elif selector.startswith('link_text='):
+ return selector.split('link_text=')[1]
+ return selector
+
+
def is_valid_url(url):
regex = re.compile(
r'^(?:http)s?://' # http:// or https://
@@ -52,7 +73,7 @@ def is_valid_url(url):
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
r'(?::\d+)?' # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
- if regex.match(url):
+ if regex.match(url) or url == 'about:blank' or url == 'data:,':
return True
else:
return False
diff --git a/server_setup.py b/server_setup.py
index 05005a56340..d71b71b409c 100755
--- a/server_setup.py
+++ b/server_setup.py
@@ -8,7 +8,7 @@
setup(
name='seleniumbase',
- version='1.5.6',
+ version='1.6.0',
description='Web Automation & Testing Framework - http://seleniumbase.com',
long_description='Web Automation and Testing Framework - seleniumbase.com',
platforms='Mac * Windows * Linux * Docker',
diff --git a/setup.py b/setup.py
index 9da80c97db0..95d8ac101ad 100755
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
"""
The setup package to install SeleniumBase dependencies and plugins
-(Uses the newer Selenium 3.8.0)
+(Uses the newer Selenium 3.8.1)
"""
import os
@@ -8,7 +8,7 @@
setup(
name='seleniumbase',
- version='1.5.6',
+ version='1.6.0',
description='Web Automation & Testing Framework - http://seleniumbase.com',
long_description='Web Automation and Testing Framework - seleniumbase.com',
platforms='Mac * Windows * Linux * Docker',
@@ -21,7 +21,7 @@
'pip>=9.0.1',
'setuptools>=38.5.1',
'ipython==5.4.1',
- 'selenium==3.8.0',
+ 'selenium==3.8.1',
'nose==1.3.7',
'pytest==3.4.0',
'pytest-html==1.16.1',