From c9280da6db101361b3555147526fd465c1895f2a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 25 Feb 2018 20:31:00 -0500 Subject: [PATCH 01/21] Upgrading to selenium==3.8.1 --- requirements.txt | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/setup.py b/setup.py index 9da80c97db0..9039b36a064 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 @@ -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', From d54651b89a5bf610dbf4fd55df0e710b379db5a0 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 25 Feb 2018 20:32:02 -0500 Subject: [PATCH 02/21] Add a comment to describe pytest default options. --- pytest.ini | 1 + 1 file changed, 1 insertion(+) 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 From 4b52668183e42eacdf4d94b9ce8429e54d4cac70 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 25 Feb 2018 20:33:33 -0500 Subject: [PATCH 03/21] Handle PhantomJS deprecation warnings --- seleniumbase/core/browser_launcher.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) 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() From 84ca2d376e84b3241b9abadb446d0e62036a5eee Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 25 Feb 2018 20:34:43 -0500 Subject: [PATCH 04/21] Add new way of extracting link text from a selector --- seleniumbase/fixtures/page_utils.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/seleniumbase/fixtures/page_utils.py b/seleniumbase/fixtures/page_utils.py index e35d7ff1e8b..fbb34aba111 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:// From 7e65050b8ededa75c9d6ebb30bdde6f9901793f7 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 25 Feb 2018 20:35:37 -0500 Subject: [PATCH 05/21] Add hover_element_and_click() to page_actions --- seleniumbase/fixtures/page_actions.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) 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): """ From c0d73e0e2b761060e052646faf1b4bf6aef8a20a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 25 Feb 2018 20:36:46 -0500 Subject: [PATCH 06/21] Add methods to help with css selector processing --- seleniumbase/fixtures/base_case.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 1d38b9f0531..e2c327ab382 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1321,6 +1321,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: From a23f7eb932cf866368e87eb381c8b4e78f1bc137 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 25 Feb 2018 20:39:49 -0500 Subject: [PATCH 07/21] Handle clicking link text hidden in a dropdown menu. --- seleniumbase/fixtures/base_case.py | 134 ++++++++++++++++++++++------- 1 file changed, 105 insertions(+), 29 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index e2c327ab382..6a61f437421 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: @@ -1283,6 +1333,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): From 2ed5e792469ea250132066e8087c5495c219336a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 25 Feb 2018 20:40:27 -0500 Subject: [PATCH 08/21] Fix BeautifulSoup processing --- seleniumbase/fixtures/base_case.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 6a61f437421..6eca12b53e7 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -273,7 +273,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: From c4e1378ce708e76062331ce0b52cb10e2cc20af3 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 25 Feb 2018 20:42:26 -0500 Subject: [PATCH 09/21] Handling link text --- seleniumbase/fixtures/base_case.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 6eca12b53e7..4993386de01 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -261,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. """ @@ -343,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( @@ -516,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): @@ -961,6 +974,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) @@ -986,6 +1002,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) @@ -1024,6 +1043,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) @@ -1152,6 +1174,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) From 1707ddf30105dd11f44573d441887e8fefcb2083 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 25 Feb 2018 20:43:11 -0500 Subject: [PATCH 10/21] Mostly iFrame processing with new methods --- seleniumbase/fixtures/base_case.py | 54 ++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 4993386de01..f0af4b56669 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -553,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) From 79568087d613b7ad8472d6648e3d7fd7b8f363d7 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 25 Feb 2018 20:44:46 -0500 Subject: [PATCH 11/21] Use the reusable methods. --- seleniumbase/fixtures/base_case.py | 42 ++++++------------------------ 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index f0af4b56669..79eaefe7ffe 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -669,6 +669,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: @@ -678,11 +679,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') @@ -695,11 +692,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) @@ -869,19 +863,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, @@ -899,20 +883,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() From 60cbc991d0fa04754e5747f5027c996d69710446 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 25 Feb 2018 20:48:10 -0500 Subject: [PATCH 12/21] Mostly adding new methods for page object manipulation. --- seleniumbase/fixtures/base_case.py | 66 +++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 79eaefe7ffe..b4b403a1775 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -764,34 +764,70 @@ 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): + 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): + 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): + 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): + 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): + 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): + 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): + 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. """ @@ -901,6 +937,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) From f288bfd1ac4949a2808eafd95e5f17a4335ad6ea Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 25 Feb 2018 20:49:18 -0500 Subject: [PATCH 13/21] Update the SeleniumIDE converter tool --- integrations/selenium_ide/convert_ide.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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)) From 4f47c4c518bd8099a6a1c0c5a35f6a688d1e1df3 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 25 Feb 2018 20:50:19 -0500 Subject: [PATCH 14/21] Add method for bringing a page object to the front with Z-index. --- seleniumbase/fixtures/base_case.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index b4b403a1775..8608d1229a9 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -658,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. From abbffb6e9a4e0d1d42a505f7c2d230b7a3469aa5 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Feb 2018 12:25:22 -0500 Subject: [PATCH 15/21] Add comments to base_case methods --- seleniumbase/fixtures/base_case.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 8608d1229a9..633c585cb8c 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -794,6 +794,7 @@ def jquery_click(self, selector, by=By.CSS_SELECTOR): 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) @@ -801,12 +802,14 @@ def hide_element(self, selector, by=By.CSS_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) @@ -814,12 +817,14 @@ def show_element(self, selector, by=By.CSS_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) @@ -827,6 +832,7 @@ def remove_element(self, selector, by=By.CSS_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 @@ -839,6 +845,9 @@ 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: From bd46f60ab2c40cac15a240a5870deabfbf38ffd2 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Feb 2018 12:38:10 -0500 Subject: [PATCH 16/21] Update method_summary --- help_docs/method_summary.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) 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, From b1c6b95ad168697970af8187e27b4527c4df0c52 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Feb 2018 15:45:44 -0500 Subject: [PATCH 17/21] Update boilerplates --- examples/boilerplates/base_test_case.py | 43 ++++++++++++++++++++ examples/boilerplates/boilerplate_test.py | 10 +++++ examples/boilerplates/master_test_case.py | 37 ----------------- examples/boilerplates/page_objects.py | 9 ++-- examples/boilerplates/samples/google_test.py | 2 +- 5 files changed, 59 insertions(+), 42 deletions(-) create mode 100755 examples/boilerplates/base_test_case.py create mode 100755 examples/boilerplates/boilerplate_test.py delete mode 100755 examples/boilerplates/master_test_case.py 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): From 629ebd97e6ce2250f74e4aac6ab707fd2143ac8c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Feb 2018 18:22:15 -0500 Subject: [PATCH 18/21] Add some special URLs as valid URLs --- seleniumbase/fixtures/page_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/fixtures/page_utils.py b/seleniumbase/fixtures/page_utils.py index fbb34aba111..148fabd07cf 100755 --- a/seleniumbase/fixtures/page_utils.py +++ b/seleniumbase/fixtures/page_utils.py @@ -73,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 From 64a0f12416d68ae0566ada13b429751389d5ccc4 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Feb 2018 18:23:36 -0500 Subject: [PATCH 19/21] Update traffic-generation method --- seleniumbase/fixtures/base_case.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 633c585cb8c..d09809e9316 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1043,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): From 5dc91dd030e2e52b55ee63a4b8dae2be0900a4b4 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Feb 2018 19:40:05 -0500 Subject: [PATCH 20/21] Update docs. --- README.md | 4 +++- _config.yml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 183a2e687d9..d186fdfe40f 100755 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ ![](https://cdn2.hubspot.net/hubfs/100006/images/SB_Logo8s.png "SeleniumBase") +**WebDriver automation simplified by extending Python's unittest framework.** + [![](https://img.shields.io/pypi/v/seleniumbase.svg)](https://pypi.python.org/pypi/seleniumbase) [![Build Status](https://travis-ci.org/seleniumbase/SeleniumBase.svg?branch=master)](https://travis-ci.org/seleniumbase/SeleniumBase) [![Join the chat at https://gitter.im/seleniumbase/SeleniumBase](https://badges.gitter.im/seleniumbase/SeleniumBase.svg)](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. ![](https://cdn2.hubspot.net/hubfs/100006/images/sb_demo.gif "SeleniumBase") 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 From cee9f39fa7f603073692a6af068e6ed8df8540c0 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Feb 2018 19:42:11 -0500 Subject: [PATCH 21/21] Version 1.6.0 --- server_setup.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 9039b36a064..95d8ac101ad 100755 --- a/setup.py +++ b/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',