From 5015c066fafa9cb4ff039d56a7e3501195b0b60a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Mar 2018 22:09:50 -0400 Subject: [PATCH 1/9] Update base_case documentation --- seleniumbase/fixtures/base_case.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 9f26cca510b..17c7bb9d16f 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1,10 +1,13 @@ """ -BaseCase gathers SeleniumBase libraries into a single file for easy calling. +The BaseCase class is the main gateway for using The SeleniumBase Framework. +It inherits Python's unittest.TestCase class, and runs with Pytest or Nose. +All tests using BaseCase automatically launch WebDriver browsers for tests. + Usage: from seleniumbase import BaseCase class MyTestClass(BaseCase): - test_anything(self): + def test_anything(self): # Write your code here. Example: self.open("https://github.com/") self.update_text("input.header-search-input", "SeleniumBase\n") @@ -12,9 +15,9 @@ class MyTestClass(BaseCase): self.assert_element("div.repository-content") .... -The methods here expand and improve existing WebDriver commands. -Improvements include making WebDriver more robust and more reliable. -Page elements are given enough time to load before taking action on them. +SeleniumBase methods expand and improve on existing WebDriver commands. +Improvements include making WebDriver more robust, reliable, and flexible. +Page elements are given enough time to load before WebDriver acts on them. Code becomes greatly simplified and easier to maintain. """ From 0f7b24d442746897160b1b412f07e7ae424e526d Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Mar 2018 22:11:41 -0400 Subject: [PATCH 2/9] Update method names to be more clear --- help_docs/method_summary.md | 4 ++-- seleniumbase/fixtures/base_case.py | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/help_docs/method_summary.md b/help_docs/method_summary.md index 70843c33cfb..7093ff9e332 100755 --- a/help_docs/method_summary.md +++ b/help_docs/method_summary.md @@ -73,9 +73,9 @@ 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.is_element_in_an_iframe(selector, by=By.CSS_SELECTOR) -self.enter_frame_of_element(selector, by=By.CSS_SELECTOR) +self.switch_to_frame_of_element(selector, by=By.CSS_SELECTOR) self.execute_script(script) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 17c7bb9d16f..69d1f73d454 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -615,8 +615,8 @@ def find_visible_elements(self, selector, by=By.CSS_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. + def is_element_in_an_iframe(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): @@ -639,10 +639,11 @@ def is_element_in_frame(self, selector, by=By.CSS_SELECTOR): 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. """ + def switch_to_frame_of_element(self, selector, by=By.CSS_SELECTOR): + """ Set driver control to the iframe of the element (assuming the + element is in a single-nested iframe) and returns the iframe name. + If element is not in an iframe, returns None, and nothing happens. + May not work if multiple iframes are nested within each other. """ selector, by = self._recalculate_selector(selector, by) if self.is_element_present(selector, by=by): return None From b91df9a738181045bf73595f93e846a203cf49ab Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Mar 2018 22:21:49 -0400 Subject: [PATCH 3/9] Adding new methods for handling WebDriver control --- help_docs/method_summary.md | 12 ++++- seleniumbase/fixtures/base_case.py | 81 +++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 19 deletions(-) diff --git a/help_docs/method_summary.md b/help_docs/method_summary.md index 7093ff9e332..974a0def88e 100755 --- a/help_docs/method_summary.md +++ b/help_docs/method_summary.md @@ -248,14 +248,22 @@ self.wait_for_and_switch_to_alert(timeout=settings.LARGE_TIMEOUT) self.switch_to_frame(frame, timeout=settings.SMALL_TIMEOUT) +self.switch_to_default_content() + +self.open_new_window(switch_to=True) + self.switch_to_window(window, timeout=settings.SMALL_TIMEOUT) -self.switch_to_default_content() +self.switch_to_default_window() self.save_screenshot(name, folder=None) self.get_new_driver(browser=None, headless=None, servername=None, port=None, - proxy_string=None) + proxy=None, switch_to=True) + +self.switch_to_driver(driver) + +self.switch_to_default_driver() ######## diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 69d1f73d454..e58a0bc5b4e 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -77,7 +77,8 @@ def __init__(self, *args, **kwargs): self._page_check_count = 0 self._page_check_failures = [] self._html_report_extra = [] - self._extra_drivers = [] + self._default_driver = None + self._drivers_list = [] def open(self, url): self.driver.get(url) @@ -1424,54 +1425,90 @@ def wait_for_and_switch_to_alert(self, timeout=settings.LARGE_TIMEOUT): return page_actions.wait_for_and_switch_to_alert(self.driver, timeout) def switch_to_frame(self, frame, timeout=settings.SMALL_TIMEOUT): + """ Sets driver control to the specified browser frame. """ if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: timeout = self._get_new_timeout(timeout) page_actions.switch_to_frame(self.driver, frame, timeout) + def switch_to_default_content(self): + """ Brings driver control outside the current iframe. + (If driver control is inside an iframe, the driver control + will be set to one level above the current frame. If the driver + control is not currenly in an iframe, nothing will happen.) """ + self.driver.switch_to.default_content() + + def open_new_window(self, switch_to=True): + """ Opens a new browser tab/window and switches to it by default. """ + self.driver.execute_script("window.open('');") + time.sleep(0.01) + if switch_to: + self.switch_to_window(len(self.driver.window_handles) - 1) + def switch_to_window(self, window, timeout=settings.SMALL_TIMEOUT): if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT: timeout = self._get_new_timeout(timeout) page_actions.switch_to_window(self.driver, window, timeout) - def switch_to_default_content(self): - self.driver.switch_to.default_content() + def switch_to_default_window(self): + self.switch_to_window(0) def save_screenshot(self, name, folder=None): return page_actions.save_screenshot(self.driver, name, folder) def get_new_driver(self, browser=None, headless=None, - servername=None, port=None, - proxy_string=None): + servername=None, port=None, proxy=None, switch_to=True): """ This method spins up an extra browser for tests that require more than one. The first browser is already provided by tests - that import base_case.BaseCase from seleniumbase. """ + that import base_case.BaseCase from seleniumbase. If parameters + aren't specified, the method uses the same as the default driver. + @Params + browser - the browser to use. (Ex: "chrome", "firefox") + headless - the option to run webdriver in headless mode + servername - if using a Selenium Grid, set the host address here + port - if using a Selenium Grid, set the host port here + proxy - if using a proxy server, specify the "host:port" combo here + switch_to - the option to switch to the new driver (default = True) + """ if browser is None: browser = self.browser + browser_name = browser if headless is None: headless = self.headless if servername is None: servername = self.servername if port is None: port = self.port - if proxy_string is None: - proxy_string = self.proxy_string use_grid = False if servername != "localhost": - # Use Selenium Grid (Use --server=127.0.0.1 for localhost Grid) + # Use Selenium Grid (Use "127.0.0.1" for localhost Grid) use_grid = True + proxy_string = proxy + if proxy_string is None: + proxy_string = self.proxy_string valid_browsers = constants.ValidBrowsers.valid_browsers - if browser not in valid_browsers: + if browser_name not in valid_browsers: raise Exception("Browser: {%s} is not a valid browser option. " "Valid options = {%s}" % (browser, valid_browsers)) - new_driver = browser_launcher.get_driver(browser, - headless, - use_grid, - servername, - port, - proxy_string) - self._extra_drivers.append(new_driver) + new_driver = browser_launcher.get_driver(browser_name=browser_name, + headless=headless, + use_grid=use_grid, + servername=servername, + port=port, + proxy_string=proxy_string) + self._drivers_list.append(new_driver) + if switch_to: + self.driver = new_driver return new_driver + def switch_to_driver(self, driver): + """ Sets self.driver to the specified driver. + You may need this if using self.get_new_driver() in your code. """ + self.driver = driver + + def switch_to_default_driver(self): + """ Sets self.driver to the default/original driver. """ + self.driver = self._default_driver + ############ def _get_new_timeout(self, timeout): @@ -1850,6 +1887,16 @@ def setUp(self): self.servername, self.port, self.proxy_string) + self._default_driver = self.driver + self._drivers_list.append(self.driver) + if self.headless: + # Make sure the invisible browser window is big enough + try: + self.set_window_size(1920, 1200) + except Exception: + # This shouldn't fail, but in case it does, get safely through + # setUp() so that WebDrivers can get closed during tearDown(). + pass def __insert_test_result(self, state, err): data_payload = TestcaseDataPayload() From 71a9b1d9b387ffe086a05d030c1e5b90ef6123fe Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Mar 2018 22:22:30 -0400 Subject: [PATCH 4/9] Update a ReadMe image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 65c212bb5ec..53a00294a3c 100755 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ SeleniumBase automatically handles common WebDriver actions such as spinning up **Simple Python syntax makes coding easy:**
(By default, [CSS Selectors](https://www.w3schools.com/cssref/css_selectors.asp) are used for finding page elements.) - + **Run tests with Pytest or Nose in any browser:** From a7e2a94f0957ca56f53dd40a9a7a17f08040a72c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Mar 2018 22:24:35 -0400 Subject: [PATCH 5/9] Handle browser-closing in a single method --- seleniumbase/fixtures/base_case.py | 46 +++++++++++------------------- 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index e58a0bc5b4e..76d528e608a 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1931,6 +1931,19 @@ def _add_pytest_html_extra(self): except Exception: pass + def _quit_all_drivers(self): + # Close all open browser windows + self._drivers_list.reverse() # Last In, First Out + for driver in self._drivers_list: + try: + driver.quit() + except AttributeError: + pass + except Exception: + pass + self.driver = None + self._drivers_list = [] + def tearDown(self): """ Be careful if a subclass of BaseCase overrides setUp() @@ -1990,20 +2003,8 @@ def tearDown(self): if self.with_page_source: log_helper.log_page_source( test_logpath, self.driver) - try: - # Finally close the browser - if self._extra_drivers: - for extra_driver in self._extra_drivers: - try: - extra_driver.quit() - except Exception: - pass # Extra driver was already quit - self.driver.quit() - except AttributeError: - pass - except Exception: - pass - self.driver = None + # (Pytest) Finally close all open browser windows + self._quit_all_drivers() if self.headless: if self.headless_active: self.display.stop() @@ -2041,18 +2042,5 @@ def tearDown(self): data_payload.logURL = index_file self.testcase_manager.update_testcase_log_url(data_payload) else: - # Using Nosetests - try: - # Finally close the browser - if self._extra_drivers: - for extra_driver in self._extra_drivers: - try: - extra_driver.quit() - except Exception: - pass # Extra driver was already quit - self.driver.quit() - except AttributeError: - pass - except Exception: - pass - self.driver = None + # (Nosetests) Finally close all open browser windows + self._quit_all_drivers() From 2dec0f46c6b8ed61f6fd4723bb43711603ea754f Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Mar 2018 22:25:42 -0400 Subject: [PATCH 6/9] Add a new example test (GitHub) --- examples/github_test.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100755 examples/github_test.py diff --git a/examples/github_test.py b/examples/github_test.py new file mode 100755 index 00000000000..7f568a5f137 --- /dev/null +++ b/examples/github_test.py @@ -0,0 +1,15 @@ +from seleniumbase import BaseCase + + +class GitHubTests(BaseCase): + + def test_github(self): + self.open("https://github.com/") + self.update_text("input.header-search-input", "SeleniumBase\n") + self.click('a[href="/seleniumbase/SeleniumBase"]') + self.assert_element("div.repository-content") + self.assert_text("SeleniumBase", "h1") + self.click('a[title="seleniumbase"]') + self.click('a[title="fixtures"]') + self.click('a[title="base_case.py"]') + self.assert_text("Code", "nav a.selected") From 7d034310eda3bc544ab9e2066951d21b1fdf05bf Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Mar 2018 22:28:49 -0400 Subject: [PATCH 7/9] Update the deploy script --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a7c8394339f..a50eabbe8b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,12 +13,13 @@ install: before_script: - "flake8 seleniumbase/*.py && flake8 seleniumbase/*/*.py && flake8 seleniumbase/*/*/*.py && flake8 seleniumbase/*/*/*/*.py" - "export DISPLAY=:99.0 && sh -e /etc/init.d/xvfb start" - - "wget https://chromedriver.storage.googleapis.com/2.35/chromedriver_linux64.zip && unzip chromedriver_linux64.zip && sudo cp chromedriver /usr/local/bin/ && sudo chmod +x /usr/local/bin/chromedriver" - - "wget https://github.com/mozilla/geckodriver/releases/download/v0.19.0/geckodriver-v0.19.0-linux64.tar.gz -O /tmp/geckodriver.tar.gz && tar -C /opt -xzf /tmp/geckodriver.tar.gz && sudo chmod 755 /opt/geckodriver && sudo ln -fs /opt/geckodriver /usr/bin/geckodriver && sudo ln -fs /opt/geckodriver /usr/local/bin/geckodriver" + - "wget https://chromedriver.storage.googleapis.com/2.37/chromedriver_linux64.zip && unzip chromedriver_linux64.zip && sudo cp chromedriver /usr/local/bin/ && sudo chmod +x /usr/local/bin/chromedriver" + - "wget https://github.com/mozilla/geckodriver/releases/download/v0.20.0/geckodriver-v0.20.0-linux64.tar.gz -O /tmp/geckodriver.tar.gz && tar -C /opt -xzf /tmp/geckodriver.tar.gz && sudo chmod 755 /opt/geckodriver && sudo ln -fs /opt/geckodriver /usr/bin/geckodriver && sudo ln -fs /opt/geckodriver /usr/local/bin/geckodriver" # - "wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 && tar -xvf ./phantomjs-2.1.1-linux-x86_64.tar.bz2 && export PATH=$PWD/phantomjs-2.1.1-linux-x86_64/bin:$PATH" script: - "pytest examples/my_first_test.py --browser=chrome -s" - "nosetests examples/boilerplates/boilerplate_test.py" - "pytest examples/my_first_test.py --browser=firefox -s" + - "pytest examples/github_test.py --browser=chrome -s" notifications: email: false From cf69fa160287451e2fbe82c0df98d054d5dac2d0 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Mar 2018 22:29:45 -0400 Subject: [PATCH 8/9] Version 1.8.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e6c075cea5d..95cc453302a 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='seleniumbase', - version='1.8.0', + version='1.8.1', description='Web Automation & Testing Framework - http://seleniumbase.com', long_description='Web Automation and Testing Framework - seleniumbase.com', platforms='Mac * Windows * Linux * Docker', From e00e7db134e8f92f44242817916e7da8769c595d Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 26 Mar 2018 23:00:16 -0400 Subject: [PATCH 9/9] Update the deploy script --- .travis.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index a50eabbe8b7..a25cb5574f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,14 +12,14 @@ install: - "sudo rm -f /etc/boto.cfg" before_script: - "flake8 seleniumbase/*.py && flake8 seleniumbase/*/*.py && flake8 seleniumbase/*/*/*.py && flake8 seleniumbase/*/*/*/*.py" - - "export DISPLAY=:99.0 && sh -e /etc/init.d/xvfb start" + # - "export DISPLAY=:99.0 && sh -e /etc/init.d/xvfb start" - "wget https://chromedriver.storage.googleapis.com/2.37/chromedriver_linux64.zip && unzip chromedriver_linux64.zip && sudo cp chromedriver /usr/local/bin/ && sudo chmod +x /usr/local/bin/chromedriver" - "wget https://github.com/mozilla/geckodriver/releases/download/v0.20.0/geckodriver-v0.20.0-linux64.tar.gz -O /tmp/geckodriver.tar.gz && tar -C /opt -xzf /tmp/geckodriver.tar.gz && sudo chmod 755 /opt/geckodriver && sudo ln -fs /opt/geckodriver /usr/bin/geckodriver && sudo ln -fs /opt/geckodriver /usr/local/bin/geckodriver" # - "wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 && tar -xvf ./phantomjs-2.1.1-linux-x86_64.tar.bz2 && export PATH=$PWD/phantomjs-2.1.1-linux-x86_64/bin:$PATH" script: - - "pytest examples/my_first_test.py --browser=chrome -s" - - "nosetests examples/boilerplates/boilerplate_test.py" - - "pytest examples/my_first_test.py --browser=firefox -s" - - "pytest examples/github_test.py --browser=chrome -s" + - "pytest examples/my_first_test.py --browser=chrome -s --headless" + - "nosetests examples/boilerplates/boilerplate_test.py --headless" + - "pytest examples/my_first_test.py --browser=firefox -s --headless" + - "pytest examples/github_test.py --browser=chrome -s --headless" notifications: email: false