From e02ea8f8f561d1bf8daccaf658a9a95d917cc55d Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 4 Mar 2014 23:17:05 -0500 Subject: [PATCH 001/219] First Commit --- .gitignore | 29 + README.md | 514 ++++++++++++++++- grid/selenium_server_config_example.cfg | 11 + grid/start-selenium-node.bat | 2 + grid/start-selenium-server.sh | 2 + requirements.pip | 12 + setup.py | 31 + test_framework/__init__.py | 0 test_framework/core/__init__.py | 0 test_framework/core/application_manager.py | 45 ++ test_framework/core/mysql.py | 72 +++ test_framework/core/mysql_conf.py | 18 + test_framework/core/s3_manager.py | 80 +++ test_framework/core/selenium_launcher.py | 86 +++ test_framework/core/testcase_manager.py | 130 +++++ test_framework/core/testcaserepository.sql | 47 ++ test_framework/core/utils.py | 40 ++ test_framework/core/web_driver_retry.py | 28 + test_framework/examples/__init__.py | 0 test_framework/examples/my_first_test.py | 24 + test_framework/examples/run_first_test.bat | 1 + test_framework/examples/run_first_test.sh | 1 + .../examples/run_test_fail_with_debug.sh | 1 + .../examples/run_test_fail_with_logging.sh | 1 + test_framework/examples/test_fail.py | 8 + test_framework/fixtures/__init__.py | 0 test_framework/fixtures/base_case.py | 122 ++++ test_framework/fixtures/constants.py | 41 ++ .../fixtures/delayed_data_manager.py | 139 +++++ test_framework/fixtures/email_manager.py | 533 ++++++++++++++++++ test_framework/fixtures/errors.py | 23 + test_framework/fixtures/page_interactions.py | 146 +++++ test_framework/fixtures/page_loads.py | 199 +++++++ test_framework/fixtures/tools.py | 8 + test_framework/plugins/__init__.py | 0 test_framework/plugins/base_plugin.py | 88 +++ test_framework/plugins/basic_test_info.py | 65 +++ test_framework/plugins/db_reporting_plugin.py | 147 +++++ .../plugins/hipchat_reporting_plugin.py | 119 ++++ test_framework/plugins/page_source.py | 47 ++ test_framework/plugins/s3_logging_plugin.py | 52 ++ test_framework/plugins/screen_shots.py | 57 ++ test_framework/plugins/selenium_plugin.py | 164 ++++++ test_framework/test/__init__.py | 0 44 files changed, 3130 insertions(+), 3 deletions(-) create mode 100644 .gitignore mode change 100644 => 100755 README.md create mode 100644 grid/selenium_server_config_example.cfg create mode 100644 grid/start-selenium-node.bat create mode 100755 grid/start-selenium-server.sh create mode 100755 requirements.pip create mode 100755 setup.py create mode 100755 test_framework/__init__.py create mode 100755 test_framework/core/__init__.py create mode 100755 test_framework/core/application_manager.py create mode 100755 test_framework/core/mysql.py create mode 100755 test_framework/core/mysql_conf.py create mode 100755 test_framework/core/s3_manager.py create mode 100755 test_framework/core/selenium_launcher.py create mode 100755 test_framework/core/testcase_manager.py create mode 100755 test_framework/core/testcaserepository.sql create mode 100755 test_framework/core/utils.py create mode 100755 test_framework/core/web_driver_retry.py create mode 100755 test_framework/examples/__init__.py create mode 100644 test_framework/examples/my_first_test.py create mode 100644 test_framework/examples/run_first_test.bat create mode 100644 test_framework/examples/run_first_test.sh create mode 100644 test_framework/examples/run_test_fail_with_debug.sh create mode 100644 test_framework/examples/run_test_fail_with_logging.sh create mode 100644 test_framework/examples/test_fail.py create mode 100755 test_framework/fixtures/__init__.py create mode 100755 test_framework/fixtures/base_case.py create mode 100755 test_framework/fixtures/constants.py create mode 100755 test_framework/fixtures/delayed_data_manager.py create mode 100755 test_framework/fixtures/email_manager.py create mode 100755 test_framework/fixtures/errors.py create mode 100755 test_framework/fixtures/page_interactions.py create mode 100755 test_framework/fixtures/page_loads.py create mode 100755 test_framework/fixtures/tools.py create mode 100755 test_framework/plugins/__init__.py create mode 100755 test_framework/plugins/base_plugin.py create mode 100755 test_framework/plugins/basic_test_info.py create mode 100755 test_framework/plugins/db_reporting_plugin.py create mode 100755 test_framework/plugins/hipchat_reporting_plugin.py create mode 100755 test_framework/plugins/page_source.py create mode 100755 test_framework/plugins/s3_logging_plugin.py create mode 100755 test_framework/plugins/screen_shots.py create mode 100755 test_framework/plugins/selenium_plugin.py create mode 100755 test_framework/test/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000000..b8a54f7540b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +*.py[cod] + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +__pycache__ + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +# Developer +.project +.pydevproject diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 2abc306b9cff..e0a477d2ce32 --- a/README.md +++ b/README.md @@ -1,4 +1,512 @@ -SeleniumSpot -============ +# SeleniumSpot Test Framework -SeleniumSpot Test Framework +The purpose of this open-sourced Selenium Webdriver-based test framework is to have a complete and fully automated testing system for performing browser-based integration testing. You may be wondering why you'd want to use this as opposed to raw WebDriver scripts. Here are the added benefits: +* MySQL DB integration for storing test data and results +* Amazon S3 manager (to upload logs and screenshots from tests when they fail to see what went wrong) +* Easy to use and integrate with the Jenkins build-server +* An advanced logging system (to organize and store all your test data in Jenkins + S3 + MySQL) +* A system for easily utilizing data generated from previous tests (the delayed-data manager) +* An error-handling system (so that we can process useful data for the log files, eg: the page_source plugin) +* An email manager (so that we can automatically read and parse through emails sent to gmail addresses) +* Libraries for code simplification and reusable code +* Nosetest support (a fast and easy way to run all your tests) +* A plugin to send test failure notifications directly through HipChat (in the event of a test failure) +* Advanced commands that will save you significant time + +To utilize some of the more advanced integrations, you'll need to setup instances and make connections to the following: +MySQL, Jenkins, Amazon S3, Gmail, HipChat, and a Selenium Grid. (More on this later) +We've provided placeholders in the code where you can specify your connection details. You can also use this framework as a bare-bones Selenium WebDriver command executer to automate tasks in a browser without doing any data reporting (and that's also the fastest way to make sure your base setup is working properly). +If you plan on running tests from a build server across multiple cloud machines, connecting to BrowserStack's Selenium cloud may be the least expensive alternative to having your own Selenium Grid. (If you're looking elsewhere, be wary of any Selenium cloud service that charges you by the test minute, as opposed to a flat monthly fee, because it may be a trap - those automated test minutes add up fast, and you don't want to limit the amount of automation you have.) + +Check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot). This is an excellent example of all the pieces coming together. + +In short, developers aren't perfect. Bugs can slip by undetected during deploys even if there are existing unit tests to watch for problems. A fully-capable integration testing solution can provide an added layer of security. + +A working system would be something like this: You have a QA build and a Prod build. After a QA deploy, run all the associated Selenium tests on QA. If everything passes, it's considered safe to deploy to Prod. As an added safety measure, run all those Selenium tests again on Prod after a deploy. If those pass, you should be able to feel safe. For more protection, also run Selenium tests at regular intervals in case something other than deploys breaks the system. + + +## Part I: MAC SETUP INSTRUCTIONS +####(Windows users: Try Powershell. You may need to make some adjustments during installation. This framework works on Windows machines if setup correctly.) + +**Step 0:** Get the requirements: + +[Homebrew](http://brew.sh/) + [Git](http://git-scm.com/) + + ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)" + brew install git + brew update + +[MySQL](http://www.mysql.com/) + + brew install MySQL + +That installs the MySQL library so that you can use db commands in your code. To make that useful, you'll want to have a MySQL DB that you can connect to, and you'll want to put your credentials in the mysql_conf.py file in the test_framework/core folder to access your DB from your tests. You'll also want to add the necessary tables, so to get you started, use the testcaserepository.sql file from the test_framework/core folder. + +[virtualenvwrapper](http://virtualenvwrapper.readthedocs.org/en/latest/) + + cd ~/ + pip install virtualenvwrapper + export WORKON_HOME=~/Envs + mkdir -p $WORKON_HOME + source /usr/local/bin/virtualenvwrapper.sh + +[Chromedriver](http://code.google.com/p/chromedriver/) + + brew install chromedriver + +(There are web drivers for other web browsers as well. This one will get you started.) + + +**Step 1:** Checkout the framework with git: + +```bash +git clone [LOCATION OF GITHUB FOLDER]/SeleniumSpot.git +cd SeleniumSpot +``` + + +**Step 2:** Create a virtualenv for seleniumspot: + +```bash +mkvirtualenv seleniumspot +``` + +(Virtual environments are important because they allow you to have separate configurations from the rest of your system. This will prevent conflicts if you use other tools that require other configurations and settings.) + +If you ever need to leave your virtual environment, use the following command: + +```bash +deactivate +``` + +To get back into your virtual environment, use the following command: + +```bash +workon seleniumspot +``` + + +**Step 3:** Install necessary packages from the SeleniumSpot folder and compile the test framework + +```bash +sudo pip install -r requirements.pip +sudo python setup.py install +``` + +(If you already have root access on the machine you're using, you might not need to add "sudo" before those commands.) + + +**Step 4:** You can verify that Chromedriver and Selenium were successfully installed by checking inside a python command prompt: + +```bash +python +>>> from selenium import webdriver +>>> browser = webdriver.Chrome() +>>> browser.get("http://dev.hubspot.com/blog/the-classic-qa-team-is-obsolete") +>>> browser.close() +>>> exit() +``` + + +**Step 5:** Now to verify the test framework installation by writing a simple Selenium script that performs basic actions such as navigating to a web page, clicking, waiting for page elements to appear, typing in text, scraping text on a page, and verifying text. (copy/paste this into a new file called "my_first_test.py"). This may be a good time to read up on css selectors. If you use Chrome, you can right-click on a page and select "Inspect Element" to see the details you need to create such a script. At a quick glance, dots are for class names and pound signs are for IDs. You'll also see something like "timeout=5" in the script, which tells the script how long to wait before failing (you can skip that argument and have it default to 30 seconds, which can be modified from the test framework code). + +```python +from test_framework.fixtures import base_case + +class MyTestClass(base_case.BaseCase): + + def test_basic(self): + self.driver.get("http://www.wikipedia.org/") + self.wait_for_element_visible("a[href='//en.wikipedia.org/']", timeout=5).click() + self.wait_for_element_visible("div#simpleSearch", timeout=5) + self.wait_for_element_visible("input[name='search']", timeout=5) + self.update_text_value("input[name='search']", "Boston\n") + text = self.wait_for_element_visible("div#mw-content-text", timeout=5).text + self.assertTrue("The Charles River separates Boston from " in text) + self.wait_for_element_visible("a[title='Find out about Wikipedia']").click() + self.wait_for_text_visible("Since its creation in 2001", "div#mw-content-text", timeout=5) + + self.driver.get("http://www.wikimedia.org/") + self.wait_for_element_visible('img[alt="Wikivoyage"]', timeout=5).click() + self.wait_for_element_visible("a[href='//en.wikivoyage.org/']", timeout=5).click() + self.wait_for_element_visible('a[title="Visit the main page"]', timeout=5) + self.wait_for_element_visible('input#searchInput', timeout=5) + self.update_text_value("input#searchInput", "Israel\n") + self.wait_for_element_visible("div#contentSub", timeout=5) + text = self.wait_for_element_visible("div#mw-content-text", timeout=5).text + self.assertTrue("The state of Israel" in text) +``` + +Now run the script: + +```bash +nosetests my_first_test.py --browser=chrome --with-selenium -s +``` + +After the test completes, in the console output you'll see a dot on a new line, representing a passing test. (On test failures you'll see an F instead, and on test errors you'll see an E). It looks more like a moving progress bar when you're running a ton of unit tests side by side. This is part of nosetests. After all tests complete (in this case there is only one), you'll see the "Ran 1 test in ..." line, followed by an "OK" if all nosetests passed. +If the example is moving too fast for your eyes to see what's going on, there are 2 things you can do. Add either of the following: + +```python +import time; time.sleep(5) # sleep for 5 seconds (add this after the line you want to pause on) +import pdb; pdb.set_trace() # waits for your command. n = next line of current method, c = continue, s = step / next executed line (will jump) +``` + +You may also want to have your test sleep in other situations where you need to have your test wait for something. If you know what you're waiting for, you should be specific by using a command that waits for something specific to happen. + +If you need to debug things on the fly (in case of errors), use this line to run the code: + +```bash +nosetests my_first_test.py --browser=chrome --with-selenium --pdb --pdb-failures -s +``` + +The above code will leave your browser window open in case there's a failure, which is possible if the web pages from the example change the data that's displayed on the page. (pdb commands: 'c', 's', 'n' => continue, step, next). + + +**Step 6:** Complete the setup + +If you're planning on using the full power of this test framework, there are a few more things you'll want to do: + +* Setup your [Jenkins](http://jenkins-ci.org/) build server for running your tests at regular intervals. (Or you can use any build server you want.) + +* Setup an [Amazon S3](http://aws.amazon.com/s3/) account for saving your log files and screenshots for future viewing. This test framework already has the code you need to connect to it. (Modify the s3_manager.py file from the test_framework/core folder with connection details to your instance.) + +* Install [MySQL Workbench](http://dev.mysql.com/downloads/tools/workbench/) to make life easier by giving you a nice GUI tool that you can use to read & write from your DB directly. + +* Setup your Selenium Grid and update your *.cfg file to point there. An example config file called selenium_server_config_example.cfg has been provided for you in the grid folder. The start-selenium-node.bat and start-selenium-server.sh files are for running your grid. In an example situation, your Selenium Grid server might live on a unix box and your Selenium Grid nodes might live on EC2 Windows virtual machines. When your build server runs a Selenium test, it would connect to your Selenium Grid to find out which Grid browser nodes are available to run that test. To simplify things, you can just use [Browser Stack](https://www.browserstack.com/automate) as your entire Selenium Grid (and let them do all the fun work of maintaining the grid for you). + +* If you use [HipChat](https://www.hipchat.com/), you can have test alerts go there when tests fail. If that sounds good to you, update the db_reporting_plugin.py file from the plugins folder with your credentials. + +* Be sure to tell SeleniumSpot to use these added features when you set them up. That's easy to do. You would be running tests like this: + +```bash +nosetests [YOUR_TEST_FILE].py --browser=chrome --with-selenium --with-testing_base --with-basic_test_info --with-page_source --with-screen_shots --with-db_reporting --with-s3_logging -s +``` + +(When the testing_base plugin is used, if there's a test failure, the basic_test_info plugin records test logs, the page_source plugin records the page source of the last web page seen by the test, and the screen_shots plugin records the image of the last page seen by the test where the failure occurred. Make sure you always include testing_base whenever you include a plugin that logs test data. The db_reporting plugin records the status of all tests as long as you've setup your MySQL DB properly and you've also updated your test_framework/core/mysql_conf.py file with your DB credentials.) +To simplify that long run command, you can create a *.cfg file, such as the one provided in the example, and enter your plugins there so that you can run everything just by typing: + +```bash +nosetests [YOUR_TEST_FILE].py --config=[MY_CONFIG_FILE].cfg -s +``` + +So much easier on the eyes :) +Remember, nosetests will run every method in that python file that starts with "test" in the method name. You can be more specific on what to run by doing something like: + +```bash +nosetests [YOUR_TEST_FILE].py:[SOME_CLASS_NAME].test_[SOME_TEST_NAME] --config=[MY_CONFIG_FILE].cfg -s +``` + +Let's try an example of a test that fails. Copy the following into a file called fail_test.py: +```python +""" test_fail.py """ +from test_framework.fixtures import base_case + +class MyTestClass(base_case.BaseCase): + + def test_find_google_on_bing(self): + self.driver.get("http://bing.com") + self.wait_for_element_visible("div#google_is_here", timeout=3) # This should fail +``` +Now run it: + +```bash +nosetests test_fail.py --browser=chrome --with-selenium --with-testing_base --with-basic_test_info --with-page_source --with-screen_shots -s +``` + +You'll notice that a logs folder was created to hold information about the failing test, and screenshots. Take a look at what you get. Remember, this data can be saved in your MySQL DB and in S3 if you include the necessary plugins in your run command (and if you set up the neccessary connections properly). For future test runs, past test results will get stored in the archived_logs folder. + +Have you made it this far? Congratulations!!! Now you're ready to dive in at full speed! + + +## Part II: Detailed Method Specifications, Examples + +Important Note: Make sure you include the following import in your code to use the framework commands: + +```python +from test_framework.fixtures import base_case +``` + +#### Navigating to a Page, Plus Some Other Useful Related Commands + +```python +self.driver.get("https://xkcd.com/378/") # Instant navigation to any web page - just specify the url. + +self.driver.refresh() # refresh/reload the current page. + +where_am_i = self.driver.current_url # this variable changes as the current page changes. + +source = self.driver.page_source # this variable changes as the page source changes. +``` + +**ProTip™:** You may need to use the page_source method along with Python's find() command to parse through the source to find something that Selenium wouldn't be able to. (You may want to brush up on your Python programming skills if you're confused.) +Ex: +```python +source = self.driver.page_source +first_image_open_tag = source.find('') +first_image_close_tag = source.find'', first_image_open_tag) +everything_inside_first_image_tags = source[first_image_open_tag+len(''):first_image_close_tag] +``` + +#### Clicking + +To click an element on the page: + +```python +self.click("div#my_id") +``` + +#### Asserting existance of an element on a page within some number of seconds: + +```python +self.wait_for_element_present("div.my_class", timeout=10) +``` + +#### Asserting visibility of an element on a page within some number of seconds: + +```python +self.wait_for_element_visible("a.my_class", timeout=5) +``` + +You can even combine visibility checking and clicking into one statement like so: + +```python +self.wait_for_element_visible("a.my_class", timeout=5).click() +``` + +#### Asserting visibility of text inside an element on a page within some number of seconds: + +```python +self.wait_for_text_visible("Make it so!", "div#trek div.picard div.quotes", timeout=3) +self.wait_for_text_visible("Tea. Earl Grey. Hot.", "div#trek div.picard div.quotes", timeout=1) +``` + +#### Asserting Anything + +```python +self.assertTrue(myvar1 == something) + +self.assertEqual(var1, var2) +``` + +#### Useful Conditional Statements (with creative examples in action) + +is_element_visible(selector) # is an element visible on a page +```python +import logging +if self.is_element_visible('div#warning'): + logging.debug("Red Alert: Something bad might be happening!") +``` + +is_element_present(selector) # is an element present on a page +```python +if self.is_element_present('div#top_secret img.tracking_cookie'): + self.contact_cookie_monster() # Not a real method unless you define it somewhere +else: + current_url = self.driver.current_url + self.contact_the_nsa(url=current_url, message="Dark Zone Found") # Not a real method unless you define it somewhere +``` +Another example: +```python +def is_there_a_cloaked_klingon_ship_on_this_page(): + if self.is_element_present("div.ships div.klingon"): + return not self.is_element_visible("div.ships div.klingon") + return False +``` + +is_text_visible(text, selector) # is text visible on a page +```python +def get_mirror_universe_captain_picard_superbowl_ad(superbowl_year): + selector = "div.superbowl_%s div.commercials div.transcript div.picard" % superbowl_year + if self.is_text_visible("For the Love of Marketing and Earl Grey Tea!", selector): + return "Picard HubSpot Superbowl Ad 2015" + elif self.is_text_visible("Delivery Drones... Engage", selector): + return "Picard Amazon Superbowl Ad 2015" + elif self.is_text_visible("Bing it on Screen!", selector): + return "Picard Microsoft Superbowl Ad 2015" + elif self.is_text_visible("OK Glass, Make it So!", selector): + return "Picard Google Superbowl Ad 2015" + elif self.is_text_visible("Number One, I've Never Seen Anything Like It.", selector): + return "Picard Tesla Superbowl Ad 2015" + elif self.is_text_visible("""With the first link, the chain is forged. + The first speech censored, the first thought forbidden, + the first freedom denied, chains us all irrevocably.""", selector): + return "Picard Wikimedia Superbowl Ad 2015" + elif self.is_text_visible("Let us make sure history never forgets the name ... Facebook", selector): + return "Picard Facebook Superbowl Ad 2015" + else: + raise Exception("Reports of my assimilation are greatly exaggerated.") +``` + +#### Typing Text + +update_text_value(selector, text) # updates the text from the specified element with the specified value. Exception raised if element missing or field not editable. Example: + +```python +self.update_text_value("input#id_value", "2012") +``` + +You can also use the WebDriver .send_keys() command, but it won't clear the text box first if there's already text inside. +If you want to type in special keys, that's easy too. Here's an example: + +```python +from selenium.webdriver.common.keys import Keys +self.wait_for_element_visible("textarea").send_keys(Keys.SPACE + Keys.BACK_SPACE + '\n') # the backspace should cancel out the space, leaving you with the newline +``` + +#### Switching Tabs + +So what if your test opens up a new tab/window and now you have more than one page? No problem. You just need to specify which one you currently want Selenium to use. Switching between them is easy: +Ex: + +```python +self.driver.switch_to_window(self.driver.window_handles[1]) # this switches to the new tab +``` + +driver.window_handles is a list that will continually get updated when new windows/tabs appear (index numbering is auto-incrementing from 0, which represents the main window) + +**ProTip™:** iFrames follow the same principle as new windows - you need to specify the iFrame if you want to take action on something in there +Ex: + +```python +self.driver.switch_to_frame('ContentManagerTextBody_ifr') +# Now you can act inside the iFrame +# Do something cool (here) +self.driver.switch_to_default_content() # exit the iFrame when you're done +``` + +#### Executing Custom jQuery Scripts: + +jQuery is a powerful JavaScript library that allows you to perform advanced actions in a web browser. +If the web page you're on already has jQuery loaded, you can start executing jQuery scripts immediately. +You'd know this because the web page would contain something like the following in the HTML: + +```html + +``` + +It's OK if you want to use jQuery on a page that doesn't have it loaded yet. To do so, you need to run the following command first: + +```python +self.driver.execute_script('var script = document.createElement("script"); script.src = "https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"; document.getElementsByTagName("head")[0].appendChild(script);') +``` + +Here are some examples: +```python +self.driver.execute_script('jQuery, window.scrollTo(0, 600)') # Scrolling the page + +self.driver.execute_script("jQuery('#annoying-widget').hide()") # Hiding elements on a page + +self.driver.execute_script("jQuery('#annoying-button a').remove()") # Removing elements on a page + +self.driver.execute_script("jQuery('%s').mouseover()" % (mouse_over_item)) # Mouse-over elements on a page + +self.driver.execute_script("jQuery('input#the_id').val('my_text')") # Fast text input on a page + +self.driver.execute_script("jQuery('div#dropdown a.link').click()") # Click elements on a page + +self.driver.execute_script("return jQuery('div#amazing')[0].text") # Returns the css "text" of the element given + +self.driver.execute_script("return jQuery('textarea')[2].value") # Returns the css "value" of the 3rd textarea element on the page +``` + +In the following more-complex example, jQuery is used to plant code on a page that Selenium can then touch after that: +```python +self.driver.get(SOME_PAGE_TO_PLAY_WITH) +referral_link = 'Free-Referral Button!' % DESTINATION_URL +self.driver.execute_script("document.body.innerHTML = \"%s\"" % referral_link) +self.driver.find_element_by_css_selector("a.analytics").click() # Clicks the generated button +``` + +## Part III: Explanations + Advanced Abilities + +So by now you may be wondering how the nosetests code works? Nosetests will automatically run any test that starts with "test" from the file you selected. You can also be more specific and run specific tests in a file or any test in a specific class. For example, the code in the early examples could've been run using "nosetests my_first_test.py:MyTestClass.test_basic ... ...". If you just wanted to run all tests in MyTestClass, you can use: "nosetests my_first_test.py:MyTestClass ... ...", which is useful when you have multiple tests in the same file. Don't forget the plugins. (In the beginning example, since there was only one test in that file, this won't change anything.) And if you want better logging in the console output, that's what the "-s" is for. + +To use the test framework calls, don't forget to include the following import: + +```python +from test_framework.fixtures import base_case +``` + +And you'll need to inherit the base case in your classes like so: + +```python +class MyTestClass(base_case.BaseCase): +``` + +To understand the full scope of the test framework, we have to take a peek inside. From the top-level folder that contained the requirements.pip and setup.py files, there are two other major folders: "grid" and "test_framework". The Selenium "Grid" is what maintains the remote machines running selenium tests for "selenium.hubteam.com/jenkins". Machines can be spun up through Amazon EC2, and each one is capable of running 5 simultaneous browser tests. The other major folder, "test_framework", is what contains everything else. The "test_framework" folder contains all the major components such as "Core", "Fixtures", and "Plugins". For all intensive purposes, those sections are all equally important. They contain all the code and libraries that make our test framework useful (because otherwise we'd just be writing tests using raw selenium calls without any special add-ons or support). + + +#### Checking Email: +So let's say you have a test that sends an email, and now you want to check that the email was received: + +```python +from test_framework.fixtures.email_manager import EmailManager, EmailException +num_email_results = 0 +email_subject = "This is the subject to search for (maybe include a timestamp)" +email_manager = EmailManager("[YOUR SELENIUM GMAIL EMAIL ADDRESS]") # the password for this is elsewhere (in the library) because this is a default email account +try: + html_text = email_manager.search(SUBJECT="%s" % email_subject, timeout=300) + num_email_results = len(html_text) +except EmailException: + num_email_results = 0 +self.assertTrue(num_email_results) # true if not zero +``` + +Now you can parse through the email if you're looking for specific text or want to navigate to a link listed there. + + +#### Database Powers: +Let's say you have a test that needs to access the database. First make sure you already have a table ready. Then, Boom: +Ex: + +```python +from test_framework.core.mysql import DatabaseManager +def write_data_to_db(self, theId, theValue, theUrl): + db = DatabaseManager() + query = """INSERT INTO myTable(theId,theValue,theUrl) + VALUES (%(theId)s,%(theValue)s,%(theUrl)s)""" + db.execute_query_and_close(query, {"theId":theId, + "theValue":theValue, + "theUrl":theUrl}) +``` + +Access credentials are stored in your library file for your convenience (you have to add them first). + +The following example below (taken from the Delayed Data Manager) shows how data can be pulled from the database. + +```python +import logging +from test_framework.core.mysql import DatabaseManager + +def get_delayed_test_data(self, testcase_address, done=0): + """ Returns a list of rows """ + db = DatabaseManager() + query = """SELECT guid,testcaseAddress,insertedAt,expectedResult,done + FROM delayedTestData + WHERE testcaseAddress=%(testcase_address)s + AND done=%(done)s""" + data = db.fetchall_query_and_close(query, {"testcase_address":testcase_address, "done":done}) + if data: + return data + else: + logging.debug("Could not find any rows in delayedTestData.") + logging.debug("DB Query = " + query % {"testcase_address":testcase_address, "done":done}) + return [] +``` + +And now you know how to pull data from the DB. + +You may also be wondering when you would use the Delayed Data Manager. Here's one example: If you scheduled an email to go out 12 hours from now and you wanted to check that the email gets received (but you don't want the Selenium test of a Jenkins job to sit idle for 12 hours) you can store the email credentials as a unique time-stamp for the email subject in the DB (along with a time for when it's safe for the email to be searched for) and then a later-running test can do the checking after the right amount of time has passed. + + +Congratulations! If you've made it this far, it means you have a pretty good idea about how to move forward! +Feel free to check out other exciting open source projects on GitHub: +[https://github.com/hubspot](https://github.com/hubspot) + +Happy Automating! + +~ Michael Mintz (AKA MintzWorld / DrSelenium / your friendly neighborhood automation wizard) + + +### Legal Disclaimer +Automation is a powerful tool. It allows you to take full control of web browsers and do just about anything that a human could do, but faster. It can be used for both good and evil. With great power comes great responsibility. You are fully responsible for how you use this framework and the automation that you create. You may also want to see an expert when it comes to setting up your automation environment if you require assistance. diff --git a/grid/selenium_server_config_example.cfg b/grid/selenium_server_config_example.cfg new file mode 100644 index 000000000000..ce1d0b157835 --- /dev/null +++ b/grid/selenium_server_config_example.cfg @@ -0,0 +1,11 @@ +[nosetests] +with-xunit=1 +with-selenium=1 +server=[IF NOT RUNNING THE TESTS LOCALLY, ENTER_YOUR_SELENIUM_SERVER_HOSTNAME_HERE - MIGHT BE YOUR OWN, OR ON AN EC2 MACHINE, PORT 4444 LIKELY, OR YOU MIGHT BE USING BROWSERSTACK: *.browserstack.com, PORT 80 LIKELY. IF RUNNING LOCALLY REMOVE THIS ENTIRE LINE AND THE LINE WITH THE "PORT"!] +port=4444 +with-page_source=1 +with-screen_shots=1 +with-s3_logging=1 +with-db_reporting=1 +with-basic_test_info=1 +nocapture=0 diff --git a/grid/start-selenium-node.bat b/grid/start-selenium-node.bat new file mode 100644 index 000000000000..ffffcf4e82f5 --- /dev/null +++ b/grid/start-selenium-node.bat @@ -0,0 +1,2 @@ +cd c:\ +java -jar selenium-server-standalone-2.40.0.jar -port 5555 -host [ENTER HOST OF THIS WORKER MACHINE ex: ec2-***-**-**-***.compute-1.amazonaws.com] -browser browserName=chrome,maxInstances=5 \ No newline at end of file diff --git a/grid/start-selenium-server.sh b/grid/start-selenium-server.sh new file mode 100755 index 000000000000..490335d26c3d --- /dev/null +++ b/grid/start-selenium-server.sh @@ -0,0 +1,2 @@ +#!/bin/bash +screen -S grid-server java -jar selenium-server-standalone-2.40.0.jar -role hub -host [ENTER YOUR MASTER SELENIUM SERVER HOST HERE] -port 4444 \ No newline at end of file diff --git a/requirements.pip b/requirements.pip new file mode 100755 index 000000000000..8aa337f7cab1 --- /dev/null +++ b/requirements.pip @@ -0,0 +1,12 @@ +python==2.7.5 +selenium==2.40.0 +MySQL_python +boto +SOAPpy +nose==1.3.0 +requests +BeautifulSoup +unittest2 +simplejson +chardet +errplane diff --git a/setup.py b/setup.py new file mode 100755 index 000000000000..1ff074158948 --- /dev/null +++ b/setup.py @@ -0,0 +1,31 @@ +""" +The setup package to install the SeleniumSpot Test Framework plugins. +""" + +from setuptools import setup, find_packages + +setup( + name = 'test_framework', + version = '1.0.0', + author = 'HubSpot Test Automation', + author_email = '@hubspot.com', + maintainer = 'Michael Mintz', + description = 'The SeleniumSpot Test Framework. (Powered by Python, WebDriver, and more...)', + license = 'Public', + packages = ['test_framework', + 'test_framework.core', + 'test_framework.plugins', + 'test_framework.fixtures'], + entry_points = { + 'nose.plugins': [ + 'base_plugin = test_framework.plugins.base_plugin:Base', + 'selenium = test_framework.plugins.selenium_plugin:SeleniumBase', + 'page_source = test_framework.plugins.page_source:PageSource', + 'screen_shots = test_framework.plugins.screen_shots:ScreenShots', + 'test_info = test_framework.plugins.basic_test_info:BasicTestInfo', + 'db_reporting = test_framework.plugins.db_reporting_plugin:DBReporting', + 's3_logging = test_framework.plugins.s3_logging_plugin:S3Logging', + 'hipchat_reporting = test_framework.plugins.hipchat_reporting_plugin:HipchatReporting', + ] + } + ) diff --git a/test_framework/__init__.py b/test_framework/__init__.py new file mode 100755 index 000000000000..e69de29bb2d1 diff --git a/test_framework/core/__init__.py b/test_framework/core/__init__.py new file mode 100755 index 000000000000..e69de29bb2d1 diff --git a/test_framework/core/application_manager.py b/test_framework/core/application_manager.py new file mode 100755 index 000000000000..8b68564473a7 --- /dev/null +++ b/test_framework/core/application_manager.py @@ -0,0 +1,45 @@ +""" +Methods for generating and parsing application strings used +in the Testcase Database +""" + +import time + +class ApplicationManager: + """ + This class contains methods to generate application strings. We build + it from available test data + """ + + @classmethod + def generate_application_string(cls, test): + """generate an application string based on any of the given information + that can be pulled from the test object: app_name, app_env, + unique_id, user""" + + app_name = '' + app_env = 'test' + unique_id = '' + user = '' + + if hasattr(test, 'app_name'): + app_name = test.app_name + + if hasattr(test, 'unique_id'): + unique_id = test.unique_id + else: + unique_id = int(time.time() * 1000) + + if hasattr(test, 'user'): + user = test.user + + return "%s.%s.%s.%s" % (app_name, app_env, unique_id, user) + + + @classmethod + def parse_application_string(cls, string): + """parse a generated application string into its parts: + app_name, app_env, unique_id, user """ + + pieces = string.split('.') + return pieces[0], pieces[1], pieces[2], pieces[3] diff --git a/test_framework/core/mysql.py b/test_framework/core/mysql.py new file mode 100755 index 000000000000..76bb90e636f8 --- /dev/null +++ b/test_framework/core/mysql.py @@ -0,0 +1,72 @@ +""" +Wrapper for MySQL functions to make life easier +""" + +import time +import MySQLdb +import mysql_conf as conf + + +class DatabaseManager(): + """ + This class wraps database fucntions for us for easy use. + It connects to the test case database + """ + + def __init__(self, database_env='test', conf_creds=None): + """ + Gets database information from mysql_conf.py and creates a connection. + """ + db_server, db_user, db_pass, db_schema = \ + conf.APP_CREDS[conf.Apps.TESTCASE_REPOSITORY][database_env] + retry_count = 3 + backoff = 10 + count = 0 + while count < retry_count: + try: + self.conn = MySQLdb.connect(host=db_server, + user=db_user, + passwd=db_pass, + db=db_schema) + self.conn.autocommit(True) + self.cursor = self.conn.cursor() + return + except Exception: + time.sleep(backoff) + count = count + 1 + if retry_count == 3: + raise Exception("Unable to connect to Database after 3 retries.") + + + def fetchall_query_and_close(self, query, values): + """ + Executes a query, gets all the values and then closes up the connection + """ + self.cursor.execute(query, values) + retval = self.cursor.fetchall() + self.__close_db() + return retval + + + def fetchone_query_and_close(self, query, values): + """ + Executes a query, gets the first value and then closes up the connection + """ + self.cursor.execute(query, values) + retval = self.cursor.fetchone() + self.__close_db() + return retval + + + def execute_query_and_close(self, query, values): + """ + Executes a query and closes the connection + """ + retval = self.cursor.execute(query, values) + self.__close_db() + return retval + + + def __close_db(self): + self.cursor.close() + self.conn.close() diff --git a/test_framework/core/mysql_conf.py b/test_framework/core/mysql_conf.py new file mode 100755 index 000000000000..9714ba285745 --- /dev/null +++ b/test_framework/core/mysql_conf.py @@ -0,0 +1,18 @@ +""" +This file contains database credentials for the various databases the tests need to access +""" + +# Environments +TEST = "test" + +class Apps: + TESTCASE_REPOSITORY = "testcase_repository" + +APP_CREDS = { + + Apps.TESTCASE_REPOSITORY: { + TEST: ("[TEST DB HOST]", + "[TEST DB USERNAME]", "[TEST DB PASSWORD]", "[TEST DB SCHEMA]") + }, + +} diff --git a/test_framework/core/s3_manager.py b/test_framework/core/s3_manager.py new file mode 100755 index 000000000000..23e7e2e19878 --- /dev/null +++ b/test_framework/core/s3_manager.py @@ -0,0 +1,80 @@ +""" +Manager for dealing with uploading/managing files on S3 +""" +from boto.s3.connection import S3Connection +from boto.s3.key import Key + +already_uploaded_files = [] + +class S3LoggingBucket(object): + """ + A class to upload our log files from tests to S3, from + whence we can share them. + """ + + def __init__(self, + log_bucket = "[ENTER LOG BUCKET FOLDER NAME HERE]", + bucket_url = "http://[ENTER SUBDOMAIN OF AMAZON BUCKET URL HERE].s3-[ENTER S3 REGION HERE].amazonaws.com/", + selenium_access_key = "[ENTER YOUR S3 ACCESS KEY FOR SELENIUM HERE]", + selenium_secret_key = "[ENTER YOUR S3 SECRET KEY FOR SELENIUM HERE]"): + + self.conn = S3Connection(selenium_access_key, + selenium_secret_key) + self.bucket = self.conn.get_bucket(log_bucket) + self.bucket_url = bucket_url + + + def get_key(self, _name): + """create a new Key instance with the given name""" + return Key(bucket=self.bucket, name=_name) + + + def get_bucket(self): + """return the bucket we're using""" + return self.bucket + + + def upload_file(self, file_name, file_path): + """upload a given file from the file_path to the bucket + with the new name/path file_name""" + upload_key = Key(bucket=self.bucket, name=file_name) + content_type = "text/plain" + if file_name.endswith(".html"): + content_type = "text/html" + if file_name.endswith(".jpg"): + content_type = "image/jpg" + upload_key.set_contents_from_filename(file_path, + headers={"Content-Type":content_type}) + upload_key.url = \ + upload_key.generate_url(expires_in=3600).split("?")[0] + try: + upload_key.make_public() + except: + pass # we get a http version error here, we're going to pass + return file_name + + + def upload_index_file(self, test_address, timestamp): + """create an index.html file with links to all the log files we + just uploaded""" + global already_uploaded_files + already_uploaded_files = list(set(already_uploaded_files)) + already_uploaded_files.sort() + file_name = "%s/%s/index.html" % (test_address, timestamp) + index = self.get_key(file_name) + index_str = [] + for completed_file in already_uploaded_files: + index_str.append("%s"%(completed_file, completed_file)) + index.set_contents_from_string("
".join(index_str), + headers={"Content-Type":"text/html"}) + index.make_public() + return "%s%s" % (self.bucket_url, file_name) + + + def save_uploaded_file_names(self, files): + """We keep record of file names that have been uploaded. We upload log + files related to each test after its execution. Once we're done, we + use already_uploaded_files to create an index file""" + global already_uploaded_files + already_uploaded_files.extend(files) diff --git a/test_framework/core/selenium_launcher.py b/test_framework/core/selenium_launcher.py new file mode 100755 index 000000000000..f4808eb16d36 --- /dev/null +++ b/test_framework/core/selenium_launcher.py @@ -0,0 +1,86 @@ +"""Download and run the selenium jar file""" + +import subprocess +import os +import socket +import urllib +import time + +SELENIUM_JAR = "http://selenium.googlecode.com/files/selenium-server-standalone-2.40.0.jar" +JAR_FILE = "selenium-server-2.40.0.jar" + +def download_selenium(): + """ + downloads the selenium jar file from its online location and stores it locally + """ + try: + local_file = open(JAR_FILE, 'wb') + remote_file = urllib.urlopen(SELENIUM_JAR) + print 'Please wait, downloading Selenium...\n' + local_file.write(remote_file.read()) + local_file.close() + remote_file.close() + except Exception, details: + raise Exception("Error while downloading Selenium Server. Details: "+details) + + +def is_running_locally(host, port): + socket_s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + socket_s.connect((host, port)) + socket_s.close() + return True + except: + return False + + +def is_available_locally(): + return os.path.isfile(JAR_FILE) + + +def start_selenium_server(selenium_jar_location, port, file_path): + + """ + Starts selenium on the specified port + and configures the output and error files. + Throws an exeption if the server does not start. + """ + + process_args = None + process_args = ["java", "-jar", selenium_jar_location, "-port", port] + selenium_exec = subprocess.Popen(process_args, + stdout=open("%s/log_seleniumOutput.txt"%(file_path),"w"), + stderr=open("%s/log_seleniumError.txt"%(file_path),"w")) + time.sleep(2) + if selenium_exec.poll() == 1: + raise StartSeleniumException("The selenium server did not start." +\ + "Do you already have one runing?") + return selenium_exec + + +def stop_selenium_server(selenium_server_process): + """Kills the selenium server. We are expecting an error 143""" + + try: + selenium_server_process.terminate() + return selenium_server_process.poll() == 143 + except Exception, details: + raise Exception("Cannot kill selenium process, details: "+details) + + +class StartSeleniumException(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + + +def execute_selenium(host, port, file_path): + if is_running_locally(host, port): + return + if not is_available_locally(): + download_selenium() + try: + return start_selenium_server(JAR_FILE, port, file_path) + except StartSeleniumException: + print "Selenium Server might already be running. Continuing" diff --git a/test_framework/core/testcase_manager.py b/test_framework/core/testcase_manager.py new file mode 100755 index 000000000000..9bc246fb141f --- /dev/null +++ b/test_framework/core/testcase_manager.py @@ -0,0 +1,130 @@ +""" +Testcase database related methods +""" + +from test_framework.core.mysql import DatabaseManager + +class TestcaseManager: + """ + Helper for Testcase related DB stuff + """ + + def __init__(self, database_env): + self.database_env = database_env + + + def insert_execution_data(self, execution_query_payload): + """Inserts an execution into the database, returns the execution guid""" + + query = """INSERT INTO execution + (guid, executionStart, totalExecutionTime, username) + VALUES (%(guid)s,%(execution_start_time)s, + %(total_execution_time)s,%(username)s)""" + DatabaseManager(self.database_env).execute_query_and_close(query, + execution_query_payload.get_params()) + return execution_query_payload.guid + + + def update_execution_data(self, execution_guid, execution_time): + """updates an existing execution in the database""" + + query = """UPDATE execution SET + totalExecutionTime=%(execution_time)s + WHERE guid=%(execution_guid)s """ + DatabaseManager(self.database_env).execute_query_and_close(query, + {"execution_guid":execution_guid, + "execution_time":execution_time}) + + + def insert_testcase_data(self, testcase_run_payload): + """inserts all data for the test case, returns the new row guid""" + + query = """INSERT INTO testcaseRunData + (guid, browser, state, execution_guid, application, + testcaseAddress, runtime, retryCount, message, stackTrace) + VALUES ( + %(guid)s, + %(browser)s, + %(state)s, + %(execution_guid)s, + %(application)s, + %(testcaseAddress)s, + %(runtime)s, + %(retryCount)s, + %(message)s, + %(stackTrace)s) """ + DatabaseManager(self.database_env).execute_query_and_close(query, testcase_run_payload.get_params()) + + + def update_testcase_data(self, testcase_payload): + """updates an existing testcase run in the database""" + + query = """UPDATE testcaseRunData SET + runtime=%(runtime)s, + state=%(state)s, + retryCount=%(retryCount)s, + stackTrace=%(stackTrace)s, + message=%(message)s + WHERE guid=%(guid)s """ + DatabaseManager(self.database_env).execute_query_and_close(query, testcase_payload.get_params()) + + + def update_testcase_log_url(self, testcase_payload): + """updates an existing testcase run's logging URL in the database""" + + query = """UPDATE testcaseRunData SET + logURL=%(logURL)s + WHERE guid=%(guid)s """ + DatabaseManager(self.database_env).execute_query_and_close(query, testcase_payload.get_params()) + + +class ExecutionQueryPayload: + """ Helper class for containing the execution query data """ + def __init__(self): + self.execution_start_time = None + self.total_execution_time = -1 + self.username = "Default" + self.guid = None + + + def get_params(self): + """ Returns a params object for use with the pool """ + return { + "execution_start_time": self.execution_start_time, + "total_execution_time": self.total_execution_time, + "username": self.username, + "guid": self.guid + } + + +class TestcaseDataPayload: + """ Helper class for containing all the testcase query data """ + def __init__(self): + self.guid = None + self.testcaseAddress = None + self.browser = None + self.state = None + self.execution_guid = None + self.application = None + self.runtime = None + self.retry_count = 0 + self.stack_trace = None + self.message = None + self.logURL = None + + + def get_params(self): + """ Returns a params object for use with the pool """ + return { + "guid": self.guid, + "testcaseAddress": self.testcaseAddress, + "browser": self.browser, + "state": self.state, + "execution_guid": self.execution_guid, + "application": self.application, + "runtime": self.runtime, + "retryCount": self.retry_count, + "stackTrace": self.stack_trace, + "message": self.message, + "logURL": self.logURL + } diff --git a/test_framework/core/testcaserepository.sql b/test_framework/core/testcaserepository.sql new file mode 100755 index 000000000000..ae38bac37b18 --- /dev/null +++ b/test_framework/core/testcaserepository.sql @@ -0,0 +1,47 @@ +# table delayedTestData +# ----------------------------------- +CREATE TABLE `delayedTestData` ( + `guid` varchar(64) NOT NULL DEFAULT '', + `testcaseAddress` varchar(1024) NOT NULL DEFAULT '', + `insertedAt` bigint(20) NOT NULL, + `expectedResult` text, + `done` tinyint(1) DEFAULT '0', + `expiresAt` bigint(20) DEFAULT NULL, + PRIMARY KEY (`guid`), + UNIQUE KEY `uuid` (`guid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +# table exceptionMap +# ----------------------------------- +CREATE TABLE `exceptionMap` ( + `guid` varchar(64) NOT NULL DEFAULT '', + `jiraIssue` varchar(64) DEFAULT NULL, + PRIMARY KEY (`guid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +# table execution +# ----------------------------------- +CREATE TABLE `execution` ( + `guid` varchar(64) NOT NULL DEFAULT '', + `totalExecutionTime` int(11), + `username` varchar(255) DEFAULT NULL, + `executionStart` bigint(20) DEFAULT '0', + PRIMARY KEY (`guid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +# table testcaseRunData +# ----------------------------------- +CREATE TABLE `testcaseRunData` ( + `guid` varchar(64) NOT NULL DEFAULT '', + `testcaseAddress` varchar(1024) DEFAULT NULL, + `application` varchar(1024) DEFAULT NULL, + `execution_guid` varchar(64) DEFAULT NULL, + `runtime` int(11), + `state` varchar(255) DEFAULT NULL, + `browser` varchar(255) DEFAULT NULL, + `stackTrace` text, + `retryCount` int(11) DEFAULT '0', + `exceptionMap_guid` varchar(64) DEFAULT NULL, + `logURL` text, + PRIMARY KEY (`guid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/test_framework/core/utils.py b/test_framework/core/utils.py new file mode 100755 index 000000000000..5f42ca976ef6 --- /dev/null +++ b/test_framework/core/utils.py @@ -0,0 +1,40 @@ +from functools import wraps +import logging + + +def retry_if_unsuccessful(max_tries=2): + """Decorator for a test method to make it automatically re-run on errors and failures. + + @max_tries: The maximum number of times the test will run before failing (default 2) + + This decorator will also run the test class's setUp/tearDown methods before/after + each attempt at running the decorated test method. + + Please only add this decorator to a test if you have already made a best effort + to fix flapping within the test, and have a good reason why more effort + won't fix the flapping. + """ + def _retry_if_unsuccessful(func): + @wraps(func) + def test_wrapper(self): + for x in range(0, max_tries): + # setUp is called automatically before the first run, and tearDown + # is called automatically after the last, so we need to skip calling + # setUp on the first iteration and tearDown on the last. + setup_successful = x == 0 + try: + if x > 0: + self.setUp() + setup_successful = True + func(self) + except Exception, e: + logging.exception('Exception running test: %s, retrying' % e) + if x < max_tries-1 and setup_successful: + self.tearDown() + if x + 1 >= max_tries: + raise + else: + return + return test_wrapper + + return _retry_if_unsuccessful diff --git a/test_framework/core/web_driver_retry.py b/test_framework/core/web_driver_retry.py new file mode 100755 index 000000000000..d2927018a3e0 --- /dev/null +++ b/test_framework/core/web_driver_retry.py @@ -0,0 +1,28 @@ +from selenium.common.exceptions import WebDriverException +import time + + +def web_driver_retry(in_func, timeout=30): + tic = time.time() + end_time = tic + timeout + timeout = 5 + while time.time() < end_time: + try: + return in_func() + except (WebDriverException, AssertionError), e: + if (time.time() - tic) < 5: + sleep_for = 0.25 + else: + sleep_for = 1 + time.sleep(sleep_for) + raise e + + +def asserts_visible(in_func): + def out_func(): + element = in_func() + if len(element) and element[0].is_displayed(): + return element[0] + raise WebDriverException + return out_func + diff --git a/test_framework/examples/__init__.py b/test_framework/examples/__init__.py new file mode 100755 index 000000000000..e69de29bb2d1 diff --git a/test_framework/examples/my_first_test.py b/test_framework/examples/my_first_test.py new file mode 100644 index 000000000000..4749dfb33aaf --- /dev/null +++ b/test_framework/examples/my_first_test.py @@ -0,0 +1,24 @@ +from test_framework.fixtures import base_case + +class MyTestClass(base_case.BaseCase): + + def test_basic(self): + self.driver.get("http://www.wikipedia.org/") + self.wait_for_element_visible("a[href='//en.wikipedia.org/']", timeout=5).click() + self.wait_for_element_visible("div#simpleSearch", timeout=5) + self.wait_for_element_visible("input[name='search']", timeout=5) + self.update_text_value("input[name='search']", "Boston\n") + text = self.wait_for_element_visible("div#mw-content-text", timeout=5).text + self.assertTrue("The Charles River separates Boston from " in text) + self.wait_for_element_visible("a[title='Find out about Wikipedia']").click() + self.wait_for_text_visible("Since its creation in 2001", "div#mw-content-text", timeout=5) + + self.driver.get("http://www.wikimedia.org/") + self.wait_for_element_visible('img[alt="Wikivoyage"]', timeout=5).click() + self.wait_for_element_visible("a[href='//en.wikivoyage.org/']", timeout=5).click() + self.wait_for_element_visible('a[title="Visit the main page"]', timeout=5) + self.wait_for_element_visible('input#searchInput', timeout=5) + self.update_text_value("input#searchInput", "Israel\n") + self.wait_for_element_visible("div#contentSub", timeout=5) + text = self.wait_for_element_visible("div#mw-content-text", timeout=5).text + self.assertTrue("The state of Israel" in text) diff --git a/test_framework/examples/run_first_test.bat b/test_framework/examples/run_first_test.bat new file mode 100644 index 000000000000..61c70a109d09 --- /dev/null +++ b/test_framework/examples/run_first_test.bat @@ -0,0 +1 @@ +nosetests my_first_test.py --browser=chrome --with-selenium -s \ No newline at end of file diff --git a/test_framework/examples/run_first_test.sh b/test_framework/examples/run_first_test.sh new file mode 100644 index 000000000000..61c70a109d09 --- /dev/null +++ b/test_framework/examples/run_first_test.sh @@ -0,0 +1 @@ +nosetests my_first_test.py --browser=chrome --with-selenium -s \ No newline at end of file diff --git a/test_framework/examples/run_test_fail_with_debug.sh b/test_framework/examples/run_test_fail_with_debug.sh new file mode 100644 index 000000000000..f2d5f87835a1 --- /dev/null +++ b/test_framework/examples/run_test_fail_with_debug.sh @@ -0,0 +1 @@ +nosetests test_fail.py --browser=chrome --with-selenium --pdb --pdb-failures -s \ No newline at end of file diff --git a/test_framework/examples/run_test_fail_with_logging.sh b/test_framework/examples/run_test_fail_with_logging.sh new file mode 100644 index 000000000000..eb0761e7d99c --- /dev/null +++ b/test_framework/examples/run_test_fail_with_logging.sh @@ -0,0 +1 @@ +nosetests test_fail.py --browser=chrome --with-selenium --with-testing_base --with-basic_test_info --with-page_source --with-screen_shots -s \ No newline at end of file diff --git a/test_framework/examples/test_fail.py b/test_framework/examples/test_fail.py new file mode 100644 index 000000000000..feeb12e3504b --- /dev/null +++ b/test_framework/examples/test_fail.py @@ -0,0 +1,8 @@ +""" test_fail.py """ +from test_framework.fixtures import base_case + +class MyTestClass(base_case.BaseCase): + + def test_find_google_on_bing(self): + self.driver.get("http://bing.com") + self.wait_for_element_visible("div#google_is_here", timeout=3) diff --git a/test_framework/fixtures/__init__.py b/test_framework/fixtures/__init__.py new file mode 100755 index 000000000000..e69de29bb2d1 diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py new file mode 100755 index 000000000000..69a028121aa1 --- /dev/null +++ b/test_framework/fixtures/base_case.py @@ -0,0 +1,122 @@ +import json +import time +import logging +import unittest +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.common.by import By +import page_loads, page_interactions + + +class BaseCase(unittest.TestCase): + ''' + A base test case that wraps a bunch of methods from tools + for easier access. You can also add your own methods here. + ''' + + def __init__(self, *args, **kwargs): + super(BaseCase, self).__init__(*args, **kwargs) + try: + self.driver = WebDriver() + except Exception: + pass + self.environment = None + + + def find_visible_elements(self, selector, by=By.CSS_SELECTOR): + return page_interactions.find_visible_elements(self.driver, selector, by) + + + def hover_on_element(self, selector): + return page_interactions.hover_on_element(self.driver, selector) + + + def hover_and_click(self, hover_selector, click_selector, click_by=By.CSS_SELECTOR, timeout=5): + return page_interactions.hover_and_click(self.driver, hover_selector, click_selector, click_by, timeout) + + + def is_element_present(self, selector, by=By.CSS_SELECTOR): + return page_interactions.is_element_present(self.driver, selector, by) + + + def is_element_visible(self, selector, by=By.CSS_SELECTOR): + return page_interactions.is_element_visible(self.driver, selector, by) + + + def is_text_visible(self, text, selector, by=By.CSS_SELECTOR): + return page_interactions.is_text_visible(self.driver, text, selector, by) + + + def jquery_click(self, selector): + return self.driver.execute_script("jQuery('%s').click()" % selector) + + + def click(self, selector): + ele = self.driver.find_element(by=By.CSS_SELECTOR, value=selector) + return ele.click() + + + def scroll_to(self, selector): + self.driver.execute_script("jQuery('%s')[0].scrollIntoView()" % selector) + + + def scroll_click(self, selector): + self.scroll_to(selector) + time.sleep(0.1) + self.click(selector) + + + def jq_format(self, code): + """ Use before throwing raw code such as 'div[tab="advanced"]' into jQuery. Similar to "json.dumps(value)". + The first replace should take care of everything. Now see what else there is. """ + code = code.replace('\\','\\\\').replace('\t',' ').replace('\n', '\\n').replace('\"','\\\"').replace('\'','\\\'').replace('\r', '\\r').replace('\v', '\\v').replace('\a', '\\a').replace('\f', '\\f').replace('\b', '\\b').replace('\u', '\\u') + return code + + + def set_value(self, selector, value): + val = json.dumps(value) + return self.driver.execute_script("jQuery('%s').val(%s)" % (selector, val)) + + + def update_text_value(self, selector, new_value, timeout=5, retry=False): + """ This method updates a selector's text value with a new value + @Params + selector - the selector with the value to change + new_value - the new value for the text field where the selector points to + timeout - how long to want for the selector to be visible before timing out + retry - if text update fails, try the jQuery version (Warning: don't use this if update_text_value() takes you to + a new page, or if it resets the value (such as using [backslash n] for the enter key) """ + element = self.wait_for_element_visible(selector, timeout=timeout) + element.clear() + element.send_keys(new_value) + + if retry: + if element.get_attribute('value') != new_value: + logging.debug('update_text_value is falling back to jQuery!') + # Since selectors with quotes inside of quotes such as 'div[data-tab-name="advanced"]' break jQuery, format them first + selector = self.jq_format(selector) + self.set_value(selector, new_value) + time.sleep(0.5) + + + def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, timeout=30): + return page_loads.wait_for_element_present(self.driver, selector, by, timeout) + + + def wait_for_element_visible(self, selector, by=By.CSS_SELECTOR, timeout=30): + return page_loads.wait_for_element_visible(self.driver, selector, by, timeout) + + + def wait_for_text_visible(self, text, selector, by=By.CSS_SELECTOR, timeout=30): + return page_loads.wait_for_text_visible(self.driver, text, selector, by, timeout) + + + def wait_for_element_absent(self, selector, by=By.CSS_SELECTOR, timeout=30): + return page_loads.wait_for_element_absent(self.driver, selector, by, timeout) + + + def wait_for_element_not_visible(self, selector, by=By.CSS_SELECTOR, timeout=30): + return page_loads.wait_for_element_not_visible(self.driver, selector, by, timeout) + + + def wait_for_and_switch_to_alert(self, timeout=30): + return page_loads.wait_for_and_switch_to_alert(self.driver, timeout) diff --git a/test_framework/fixtures/constants.py b/test_framework/fixtures/constants.py new file mode 100755 index 000000000000..da04878b7200 --- /dev/null +++ b/test_framework/fixtures/constants.py @@ -0,0 +1,41 @@ +""" +This class containts some frequently-used constants +""" + +class Environment: + QA = "qa" + PRODUCTION = "production" + LOCAL = "local" + TEST = "test" + + +class Browser: + FIREFOX = "firefox" + INTERNET_EXPLORER = "ie" + SAFARI = "safari" + GOOGLE_CHROME = "chrome" + HTML_UNIT = "htmlunit" + + VERSION = { + "firefox" : None, + "ie" : ["8", "9"], + "chrome" : None, + "htmlunit" : None + } + + LATEST = { + "firefox": None, + "ie": "9", + "chrome": None, + "htmlunit": None + } + + +class State: + NOTRUN = "NotRun" + ERROR = "Error" + FAILURE = "Fail" + PASS = "Pass" + SKIP = "Skip" + BLOCKED = "Blocked" + DEPRECATED = "Deprecated" diff --git a/test_framework/fixtures/delayed_data_manager.py b/test_framework/fixtures/delayed_data_manager.py new file mode 100755 index 000000000000..0b0b065cb7d3 --- /dev/null +++ b/test_framework/fixtures/delayed_data_manager.py @@ -0,0 +1,139 @@ +import json +import logging +import time +import uuid + +from test_framework.core.mysql import DatabaseManager + +DEFAULT_EXPIRATION = 1000 * 60 * 60 * 48 + +class DelayedTestStorage: + """ The database-calling methods of the Delayed Test Framework """ + + @classmethod + def get_delayed_test_data(self, testcase_address, done=0): + """ This method queries the delayedTestData table in the DB and + then returns a list of rows with the matching parameters. + + :param testcase_address: The ID (address) of the test case. + :param done: (0 for test not done or 1 for test done) + + :returns: A list of rows found with the matching testcase_address. None otherwise. + """ + db = DatabaseManager() + query = """SELECT guid,testcaseAddress,insertedAt,expectedResult,done + FROM delayedTestData + WHERE testcaseAddress=%(testcase_address)s + AND done=%(done)s""" + data = db.fetchall_query_and_close(query, {"testcase_address":testcase_address, + "done":done}) + if data: + return data + else: + logging.debug("Could not find any rows in delayedTestData.") + logging.debug("DB Query = " + query % {"testcase_address":testcase_address, "done":done}) + return [] + + + @classmethod + def insert_delayed_test_data(self, guid_, testcase_address, expected_result, done=0, expires_at=DEFAULT_EXPIRATION): + """ This method inserts rows into the delayedTestData table in the DB based on the + given parameters where inserted_at (Date format) is automatically set in this method. + + :param guid_: The guid that is provided by the test case. (Format: str(uuid.uuid4())) + :param testcase_address: The ID (address) of the test case. + :param expected_result: The result string of persistent data that will be stored in the DB. + :param done: (0 for test not done or 1 for test done) + + :returns: True (when no exceptions or errors occur) + """ + inserted_at = int(time.time() * 1000) + + db = DatabaseManager() + query = """INSERT INTO delayedTestData(guid,testcaseAddress,insertedAt,expectedResult,done,expiresAt) + VALUES (%(guid)s,%(testcaseAddress)s,%(inserted_at)s,%(expected_result)s,%(done)s,%(expires_at)s)""" + + db.execute_query_and_close(query, {"guid":guid_, + "testcaseAddress":testcase_address, + "inserted_at":inserted_at, + "expected_result":expected_result, + "done":done, + "expires_at":inserted_at + expires_at}) + return True + + + @classmethod + def set_delayed_test_to_done(self, guid_): + """ This method updates the delayedTestData table in the DB + to set the test with the selected guid to done. + + :param guid_: The guid that is provided by the test case. (Format: str(uuid.uuid4())) + + :returns: True (when no exceptions or errors occur) + """ + db = DatabaseManager() + query = """UPDATE delayedTestData + SET done=TRUE + WHERE guid=%(guid)s + AND done=FALSE""" + db.execute_query_and_close(query, {"guid":guid_}) + return True + + +class DelayedTestAssistant: + """ Some methods for assisting tests (that don't call the DB directly) """ + + @classmethod + def get_delayed_results(self, test_id, seconds, set_done=True): + """ + This method gets the delayed_test_data and sets the applicable rows in the DB to done. + The results is a list of dicts where each list item contains + item[0] = guid + item[1] = testcaseAddress + item[2] = seconds from epoch + item[3] = expected results dict encoded in json + + :param test_id: the self.id() of the test + :param seconds: the wait period until the data can be checked + + :returns: the results for the specific test where enough time has passed + """ + delayed_test_data = DelayedTestStorage.get_delayed_test_data(testcase_address=test_id) + now = int(time.time() * 1000) + results_to_check = [] + if delayed_test_data is None: + return results_to_check + for item in delayed_test_data: + if item[2] < now - (seconds * 1000): + results_to_check.append(item) + if set_done: + DelayedTestStorage.set_delayed_test_to_done(item[0]) + return results_to_check + + + @classmethod + def store_delayed_data(self, test_id, expected_result_dict, expires_at=DEFAULT_EXPIRATION): + """ + Loads the dictionary of information into the delayed test database + + :param test_id: the self.id() of the test + :param expected_result_dict: a dictionary of what is to be checked later + """ + expected_result_json = json.JSONEncoder().encode(expected_result_dict) + DelayedTestStorage.insert_delayed_test_data(str(uuid.uuid4()), + test_id, + expected_result_json, + 0, + expires_at) + + + @classmethod + def set_test_done(self, test_guid): + """ This method calls set_delayed_test_to_done to set a row in the db to done. + + :param test_guid: The guid that is provided by the test. (Format: str(uuid.uuid4())) + + :returns: True (when no exceptions or errors occur) + """ + DelayedTestStorage.set_delayed_test_to_done(test_guid) + return True diff --git a/test_framework/fixtures/email_manager.py b/test_framework/fixtures/email_manager.py new file mode 100755 index 000000000000..73a4ad35a11e --- /dev/null +++ b/test_framework/fixtures/email_manager.py @@ -0,0 +1,533 @@ +""" +EmailManager - a helper class to login, search for, and delete emails. +The default selenium account is '[YOUR-SELENIUM-EMAIL-USERNAME]@gmail.com' with password +'[YOUR-SELENIUM-EMAIL-PASSWORD]'. +""" + +import email +import htmlentitydefs +import imaplib +import quopri +import re +import time + + +class EmailManager: + """ A helper class to interface with an Email account. Our imap methods + can search and fetch messages without needing a browser. + + Example: + + em = EmailManager() + result = em.check_for_recipient("[GMAIL.USER]+[SOME CODE OR TIMESTAMP KEY]@gmail.com") + """ + + HTML = "text/html" + PLAIN = "text/plain" + TIMEOUT = 1800 + + def __init__(self, uname="[YOUR SELENIUM GMAIL USERNAME]@gmail.com", pwd='[YOUR SELENIUM GMAIL PASSWORD]', + imap_string="imap.gmail.com", port=993): + self.uname = uname + self.pwd = pwd + self.imap_string = imap_string + self.port = port + + + def imap_connect(self): + """ + Connect to our IMAP mailbox. + """ + self.mailbox = imaplib.IMAP4_SSL(self.imap_string, self.port) + self.mailbox.login(self.uname, self.pwd) + self.mailbox.select() + + + def imap_disconnect(self): + """ + Disconnect from the IMAP mailbox. + """ + self.mailbox.close() + self.mailbox.logout() + + + def __imap_search(self, ** criteria_dict): + """ Searches for query in the given IMAP criteria and returns + the message numbers that match as a list of strings. + + Criteria without values (eg DELETED) should be keyword args + with KEY=True, or else not passed. Criteria with values should + be keyword args of the form KEY="VALUE" where KEY is a valid + IMAP key. + + IMAP default is to AND all criteria together. We don't support + other logic quite yet. + + All valid keys: ALL, ANSWERED, BCC , BEFORE , + BODY , CC , DELETED, DRAFT, FLAGGED, FROM + , HEADER (UNTESTED), KEYWORD + , LARGER , NEW, NOT , OLD, ON , + OR (UNTESTED), RECENT, SEEN, + SENTBEFORE , SENTON , SENTSINCE , SINCE , + SMALLER , SUBJECT , TEXT , TO , + UID , UNANSWERED, UNDELETED, UNDRAFT, UNFLAGGED, + UNKEYWORD , UNSEEN. + + For details on keys and their values, see + http://tools.ietf.org/html/rfc3501#section-6.4.4 + + :param criteria_dict: dictionary of search criteria keywords + + :raises: EmailException if something in IMAP breaks + + :returns: List of message numbers as strings matched by given criteria + """ + self.imap_connect() + + criteria = [] + for key in criteria_dict: + if criteria_dict[key] == True: + criteria.append('(%s)' % key) + else: + criteria.append('(%s "%s")' % (key, criteria_dict[key])) + + # If any of these criteria are not valid IMAP keys, IMAP will tell us. + status, msg_nums = self.mailbox.search('UTF-8', * criteria) + self.imap_disconnect() + + if 0 == len(msg_nums): + msg_nums = [] + + if 'OK' in status: + return self.__parse_imap_search_result(msg_nums) + else: + raise EmailException("IMAP status is " + str(status)) + + + def remove_formatting(self, html): + """ + Clean out any whitespace + @Params + html - String of html to remove whitespace from + @Returns + Cleaned string + """ + return ' '.join(html.split()) + + + def __parse_imap_search_result(self, result): + """ + This takes the result of imap_search and returns SANE results + @Params + result - result from an imap_search call + @Returns + List of IMAP search results + """ + if type(result) == type([]): + if len(result) == 1: + return self.__parse_imap_search_result(result[0]) + else: + return result + elif type(result) == type(""): + return result.split() + else: + # Fail silently assuming tests will fail if emails are not found + return [] + + + def fetch_html(self, msg_nums): + """ + Given a message number that we found with imap_search, + get the text/html content. + @Params + msg_nums - message number to get html message for + @Returns + HTML content of message matched by message number + """ + if not msg_nums: + raise Exception("Invalid Message Number!") + + return self.__imap_fetch_content_type(msg_nums, self.HTML) + + + def fetch_plaintext(self, msg_nums): + """ + Given a message number that we found with imap_search, + get the text/plain content. + @Params + msg_nums - message number to get message for + @Returns + Plaintext content of message matched by message number + """ + if not msg_nums: + raise Exception("Invalid Message Number!") + + return self.__imap_fetch_content_type(msg_nums, self.PLAIN) + + + def __imap_fetch_content_type(self, msg_nums, content_type): + """ + Given a message number that we found with imap_search, fetch the + whole source, dump that into an email object, and pick out the part + that matches the content type specified. Return that, if we got + multiple emails, return dict of all the parts. + @Params + msg_nums - message number to search for + content_type - content type of email message to return + @Returns + Specified content type string or dict of all content types of matched + email. + """ + + if not msg_nums: + raise Exception("Invalid Message Number!") + if not content_type: + raise Exception("Need a content type!") + + contents = {} + self.imap_connect() + for num in msg_nums: + status, data = self.mailbox.fetch(num, "(RFC822)") + for response_part in data: + if isinstance(response_part, tuple): + msg = email.message_from_string(response_part[1]) + for part in msg.walk(): + if str(part.get_content_type()) == content_type: + content = str(part.get_payload(decode=True)) + contents[int(num)] = content + self.imap_disconnect() + return contents + + + def fetch_html_by_subject(self, email_name): + """ + Get the html of an email, searching by subject. + @Params + email_name - the subject to search for + @Returns + HTML content of the matched email + """ + if not email_name: + raise EmailException("Subject cannot be null") + + results = self.__imap_search(SUBJECT=email_name) + sources = self.fetch_html(results) + + return sources + + + def fetch_plaintext_by_subject(self, email_name): + """ + Get the plain text of an email, searching by subject. + @Params + email_name - the subject to search for + @Returns + Plaintext content of the matched email + """ + if not email_name: + raise EmailException("Subject cannot be null") + + results = self.__imap_search(SUBJECT=email_name) + sources = self.fetch_plaintext(results) + + return sources + + + def search_for_recipient(self, email, timeout=None, content_type=None): + """ + Get content of emails, sent to a specific email address. + @Params + email - the recipient email address to search for + timeout - seconds to try beore timing out + content_type - type of email string to return + @Returns + Content of the matched email in the given content type + """ + return self.search(timeout=timeout, + content_type=content_type, TO=email) + + + def search_for_subject(self, subject, timeout=None, content_type=None): + """ + Get content of emails, sent to a specific email address. + @Params + email - the recipient email address to search for + timeout - seconds to try beore timing out + content_type - type of email string to return + @Returns + Content of the matched email in the given content type + """ + return self.search(timeout=timeout, + content_type=content_type, SUBJECT=subject) + + + def search_for_count(self, ** args): + """ + A search that keeps searching up until timeout for a + specific number of matches to a search. If timeout is not + specified we use the default. If count= is not specified we + will fail. Return values are the same as search(), except for count=0, + where we will return an empty list. Use this if you need to wait for a + number of emails other than 1. + + @Params + args - dict of arguments to use in search: + count - number of emails to search for + timeout - seconds to try search before timing out + @Returns + List of message numbers matched by search + """ + if "timeout" not in args.keys(): + timeout = self.TIMEOUT + elif args["timeout"]: + timeout = args["timeout"] + args["timeout"] = timeout / 15 + + if "count" not in args.keys(): + raise EmailException("Count param not defined!") + else: + count = int(args["count"]) + del args["count"] + + results = None + timer = timeout + count = 0 + while count < timer: + try: + results = self.search(** args) + except EmailException: + if count == 0: + return [] + + if results and len(results) == count: + return results + else: + time.sleep(15) + count += 15 + if count >= timer: + raise EmailException("Failed to match criteria %s in %s minutes" % \ + (args, timeout / 60)) + + + def __check_msg_for_headers(self, msg, ** email_headers): + """ + Checks an Email.Message object for the headers in email_headers. + + Following are acceptable header names: ['Delivered-To', + 'Received', 'Return-Path', 'Received-SPF', + 'Authentication-Results', 'DKIM-Signature', + 'DomainKey-Signature', 'From', 'To', 'Message-ID', + 'Subject', 'MIME-Version', 'Content-Type', 'Date', + 'X-Sendgrid-EID', 'Sender']. + + @Params + msg - the Email.message object to check + email_headers - list of headers to check against + @Returns + Boolean whether all the headers were found + """ + all_headers_found = False + email_headers['Delivered-To'] = email_headers['To'] + email_headers.pop('To') + all_headers_found = all(k in msg.keys() for k in email_headers) + + return all_headers_found + + + def fetch_message(self, msgnum): + """ + Given a message number, return the Email.Message object. + @Params + msgnum - message number to find + @Returns + Email.Message object for the given message number + """ + self.imap_connect() + status, data = self.mailbox.fetch(msgnum, "(RFC822)") + self.imap_disconnect() + + for response_part in data: + if isinstance(response_part, tuple): + return email.message_from_string(response_part[1]) + + + def get_content_type(self, msg, content_type="HTML"): + """ + Given an Email.Message object, gets the content-type payload + as specified by @content_type. This is the actual body of the + email. + @Params + msg - Email.Message object to get message content for + content_type - Type of content to get from the email + @Return + String content of the email in the given type + """ + if "HTML" in content_type.upper(): + content_type = self.HTML + elif "PLAIN" in content_type.upper(): + content_type = self.PLAIN + + for part in msg.walk(): + if str(part.get_content_type()) == content_type: + return str(part.get_payload(decode=True)) + + + def search(self, ** args): + """ + Checks email inbox every 15 seconds that match the criteria + up until timeout. + + Search criteria should be keyword args eg + TO="selenium@gmail.com". See __imap_search docstring for list + of valid criteria. If content_type is not defined, will return + a list of msg numbers. + + Options: + - fetch: will return a dict of Message objects, keyed on msgnum, + which can be used to look at headers and other parts of the complete + message. (http://docs.python.org/library/email.message.html) + - timeout: will replace the default module timeout with the + value in SECONDS. + - content_type: should be either "PLAIN" or + "HTML". If defined returns the source of the matched messages + as a dict of msgnum:content. If not defined we return a list + of msg nums. + """ + + if "content_type" not in args.keys(): + content_type = None + elif "HTML" in args["content_type"]: + content_type = self.HTML + del args["content_type"] + elif "PLAIN" in args["content_type"]: + content_type = self.PLAIN + del args["content_type"] + elif args["content_type"]: + content_type = args['content_type'] + del args["content_type"] + + if "timeout" not in args.keys(): + timeout = self.TIMEOUT + elif "timeout" in args: + timeout = args["timeout"] + del args["timeout"] + + fetch = False + if "fetch" in args.keys(): + fetch = True + del args["fetch"] + + results = None + timer = timeout + count = 0 + while count < timer: + results = self.__imap_search( ** args) + if len(results) > 0: + if fetch: + msgs = {} + for msgnum in results: + msgs[msgnum] = self.fetch_message(msgnum) + return msgs + elif not content_type: + return results + else: + return self.__imap_fetch_content_type(results, + content_type) + else: + time.sleep(15) + count += 15 + if count >= timer: + raise EmailException( + "Failed to find message for criteria %s in %s minutes" % \ + (args, timeout / 60)) + + + def remove_whitespace(self, html): + """ + Clean whitespace from html + @Params + html - html source to remove whitespace from + @Returns + String html without whitespace + """ + # Does python have a better way to do exactly this? + clean_html = html + for char in ("\r", "\n", "\t"): + clean_html = clean_html.replace(char, "") + return clean_html + + + def remove_control_chars(self, html): + """ + Clean control characters from html + @Params + html - html source to remove control characters from + @Returns + String html without control characters + """ + return self.remove_whitespace(html) + + + def replace_entities(self, html): + """ + Replace htmlentities with unicode characters + @Params + html - html source to replace entities in + @Returns + String html with entities replaced + """ + def fixup(text): + """replace the htmlentities in some text""" + text = text.group(0) + if text[:2] == "&#": + # character reference + try: + if text[:3] == "&#x": + return unichr(int(text[3:-1], 16)) + else: + return unichr(int(text[2:-1])) + except ValueError: + pass + else: + # named entity + try: + text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) + except KeyError: + pass + return text # leave as is + return re.sub("&#?\w+;", fixup, html) + + + def decode_quoted_printable(self, html): + """ + Decoding from Quoted-printable, or QP encoding, that uses ASCII 7bit + chars to encode 8 bit chars, resulting in =3D to represent '='. Python + supports UTF-8 so we decode. Also removes line breaks with '= at the + end.' + @Params + html - html source to decode + @Returns + String decoded HTML source + """ + return self.replace_entities(quopri.decodestring(html)) + + + def html_bleach(self, html): + """ + Cleanup and get rid of all extraneous stuff for better comparison + later. Turns formatted into into a single line string. + @Params + html - HTML source to clean up + @Returns + String cleaned up HTML source + """ + return self.decode_quoted_printable(html) + + +class EmailException(Exception): + """Raised when we have an Email-related problem.""" + def __init__(self, value): + self.parameter = value + + def __str__(self): + return repr(self.parameter) diff --git a/test_framework/fixtures/errors.py b/test_framework/fixtures/errors.py new file mode 100755 index 000000000000..49baa7b32f75 --- /dev/null +++ b/test_framework/fixtures/errors.py @@ -0,0 +1,23 @@ +""" +This module contains test state related exceptions. +Raising one of these in a test will cause the state of the test to +be logged appropriately. +""" + +#this makes DeprecatedTest and SkipTest available, but nose currently +#(for 3 years) has a bug that won't let us handle them properly so +#we define our own + +class BlockedTest(Exception): + """Raise this to mark a test as Blocked""" + pass + + +class SkipTest(Exception): + """Raise this to mark a test as Skipped.""" + pass + + +class DeprecatedTest(Exception): + """Raise this to mark a test as Deprecated.""" + pass diff --git a/test_framework/fixtures/page_interactions.py b/test_framework/fixtures/page_interactions.py new file mode 100755 index 000000000000..c67e63448187 --- /dev/null +++ b/test_framework/fixtures/page_interactions.py @@ -0,0 +1,146 @@ +""" +This module contains a set of methods that can be used for loading pages and +waiting for elements to come in. + +The default option we use to search for elements is CSS Selector. +This can be changed by setting the by paramter. The enum class for options is: +from selenium.webdriver.common.by import By + +Options are +By.CSS_SELECTOR +By.CLASS_NAME +By.ID +By.NAME +By.LINK_TEXT +By.XPATH +By.TAG_NAME +By.PARTIAL_LINK_TEXT +""" + +import time +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.errorhandler import ElementNotVisibleException +from selenium.webdriver.remote.errorhandler import NoSuchElementException + + +def is_element_present(driver, selector, by=By.CSS_SELECTOR): + """ + Searches for the specified element by the given selector. Returns whether + the element object if the element is present on the page. + @Params + driver - the webdriver object (required) + selector - the locator that is used (required) + by - the method to search for hte locator (Default- By.CSS_SELECTOR) + + @returns + Boolean Whether the element is present + """ + try: + driver.find_element(by=by, value=selector) + return True + except Exception: + return False + + +def is_element_visible(driver, selector, by=By.CSS_SELECTOR): + """ + Searches for the specified element by the given selector. Returns whether + the element object if the element is present and visible on the page. + @Params + driver - the webdriver object (required) + selector - the locator that is used (required) + by - the method to search for hte locator (Default- By.CSS_SELECTOR) + + @returns + Boolean Whether the element is present and visible + """ + try: + element = driver.find_element(by=by, value=selector) + return element.is_displayed() + except Exception: + return False + + +def is_text_visible(driver, text, selector, by=By.CSS_SELECTOR): + """ + Searches for the specified element by the given selector. Returns whether + the element object if the element is present and visible on the page and + contains the given text. + @Params + driver - the webdriver object (required) + text - the text string to search for + selector - the locator that is used (required) + by - the method to search for hte locator (Default- By.CSS_SELECTOR) + + @returns + Boolean Whether the element is present and visible + """ + try: + element = driver.find_element(by=by, value=selector) + return element.is_displayed() and text in element.text + except Exception: + return False + + +def find_visible_element(driver, selector, by=By.CSS_SELECTOR): + """ + Finds a WebElement that matches a selector and is visible. + Similar to webdriver.find_element. + @Params + driver - the webdriver object (required) + selector - the locator that is used to search the DOM (required) + by - the method to search for hte locator (Default- By.CSS_SELECTOR) + """ + element = driver.find_element(by=by, value=selector) + if element.is_displayed(): + return element + else: + return None + + +def find_visible_elements(driver, selector, by=By.CSS_SELECTOR): + """ + Finds all WebElements that matche a selector and are visible. + Similar to webdriver.find_elements. + @Params + driver - the webdriver object (required) + selector - the locator that is used to search the DOM (required) + by - the method to search for hte locator (Default- By.CSS_SELECTOR) + """ + elements = driver.find_elements(by=by, value=selector) + + return [element for element in elements if element.is_displayed()] + + +def hover_on_element(driver, selector): + """ + Fires the hover event for the specified element by the given selector. + @Params + driver - the webdriver object (required) + selector - the locator (css selector) that is used (required) + """ + driver.execute_script("jQuery('%s').mouseover()" % selector) + + +def hover_and_click(driver, hover_selector, click_selector, + click_by=By.CSS_SELECTOR, timeout=5): + """ + Fires the hover event for a specified element by a given selector, then clicks on + another element specified. Useful for dropdown hover based menus. + @Params + driver - the webdriver object (required) + hover_selector - the locator (css selector) that is used to hover (required) + click_selector - the locator that is used to click (required) + click_by - the method to search for hte locator (Default- By.CSS_SELECTOR) + timeout - number of seconds to wait between hover and click (Default- 5 seconds) + """ + driver.execute_script("jQuery('%s').mouseover()" % (hover_selector)) + for x in range(timeout): + try: + driver.find_element(by=click_by, value="%s" % + click_selector).click() + return + except Exception: + time.sleep(1) + raise NoSuchElementException("Element %s was not present in %s" % + (click_selector, timeout)) diff --git a/test_framework/fixtures/page_loads.py b/test_framework/fixtures/page_loads.py new file mode 100755 index 000000000000..0a587e03ef02 --- /dev/null +++ b/test_framework/fixtures/page_loads.py @@ -0,0 +1,199 @@ +""" +This module contains a set of methods that can be used for loading pages and +waiting for elements to come in. + +The default option we use to search for elements is CSS Selector. +This can be changed by setting the by paramter. The enum class for options is: +from selenium.webdriver.common.by import By + +Options are +By.CSS_SELECTOR +By.CLASS_NAME +By.ID +By.NAME +By.LINK_TEXT +By.XPATH +By.TAG_NAME +By.PARTIAL_LINK_TEXT +""" + +import time +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.errorhandler import ElementNotVisibleException, \ + NoSuchElementException, \ + NoAlertPresentException, \ + UnexpectedAlertPresentException + + +def wait_for_element_present(driver, selector, + by=By.CSS_SELECTOR, timeout=30): + """ + Searches for the specified element by the given selector. Returns the + element object if the element is present on the page. The element can be + invisible. Raises an exception if the element does not appear in the + specified timeout. + @Params + driver - the webdriver object + selector - the locator that is used (required) + by - the method to search for hte locator (Default- By.CSS_SELECTOR) + timeout - the time to wait for the element in seconds (Default- 30 seconds) + + @returns + A web element object + """ + + element = None + for x in range(timeout): + try: + element = driver.find_element(by=by, value=selector) + return element + except Exception: + time.sleep(1) + if not element: + raise NoSuchElementException("Element %s was not present in %s seconds!" % + (selector, timeout)) + + +def wait_for_element_visible(driver, selector, + by=By.CSS_SELECTOR, timeout=30): + """ + Searches for the specified element by the given selector. Returns the + element object if the element is present and visible on the page. + Raises an exception if the element does not appear in the + specified timeout. + @Params + driver - the webdriver object (required) + selector - the locator that is used (required) + by - the method to search for hte locator (Default- By.CSS_SELECTOR) + timeout - the time to wait for the element in seconds (Default- 30 seconds) + + @returns + A web element object + """ + + element = None + for x in range(timeout): + try: + element = driver.find_element(by=by, value=selector) + if element.is_displayed(): + return element + else: + element = None + time.sleep(1) + except Exception: + time.sleep(1) + if not element: + raise ElementNotVisibleException("Element %s was not visible in %s seconds!"\ + % (selector, timeout)) + + +def wait_for_text_visible(driver, text, selector, + by=By.CSS_SELECTOR, timeout=30): + """ + Searches for the specified element by the given selector. Returns the + element object if the text is present in the element and visible + on the page. Raises an exception if the text or element do not appear + in the specified timeout. + @Params + driver - the webdriver object (required) + text - the text that is being searched for in the element (required) + selector - the locator that is used (required) + by - the method to search for hte locator (Default- By.CSS_SELECTOR) + timeout - the time to wait for the element in seconds (Default- 30 seconds) + + @returns + A web element object that contains the text searched for + """ + + element = None + for x in range(timeout): + try: + element = driver.find_element(by=by, value=selector) + if element.is_displayed(): + if text in element.text: + return element + else: + element = None + time.sleep(1) + except Exception: + time.sleep(1) + if not element: + raise ElementNotVisibleException("Expected text for element %s was not visible in %s seconds!"\ + % (selector, timeout)) + + +def wait_for_element_absent(driver, selector, + by=By.CSS_SELECTOR, timeout=30): + """ + Searches for the specified element by the given selector. Returns void when + element is no longer present on the page. Raises an exception if the + element does still exist after the specified timeout. + @Params + driver - the webdriver object + selector - the locator that is used (required) + by - the method to search for hte locator (Default- By.CSS_SELECTOR) + timeout - the time to wait for the element in seconds (Default- 30 seconds) + """ + + for x in range(timeout + 1): + try: + driver.find_element(by=by, value=selector) + time.sleep(1) + except Exception: + return + raise Exception("Element %s was still present after %s seconds!" % + (selector, timeout)) + + +def wait_for_element_not_visible(driver, selector, + by=By.CSS_SELECTOR, timeout=30): + """ + Searches for the specified element by the given selector. Returns void when + element is no longer visible on the page (or if the element is not + present). Raises an exception if the element is still visible after the + specified timeout. + @Params + driver - the webdriver object (required) + selector - the locator that is used (required) + by - the method to search for hte locator (Default- By.CSS_SELECTOR) + timeout - the time to wait for the element in seconds (Default - 30 seconds) + """ + + for x in range(timeout + 1): + try: + element = driver.find_element(by=by, value=selector) + if element.is_displayed(): + time.sleep(1) + else: + return + time.sleep(1) + except Exception: + return + raise Exception("Element %s was still visible after %s seconds!"\ + % (selector, timeout)) + + +def wait_for_and_switch_to_alert(driver, timeout=30): + """ + Wait for a browser alert to appear, and switch to it. This should be usable + as a drop-in replacement for driver.switch_to_alert() when the alert box may + not exist yet. + @params + driver - the webdriver object (required) + timeout - the time to wait for the alert in seconds (Default - 30 seconds) + """ + + for x in range(timeout + 1): + try: + driver.switch_to_alert() + alert = driver.switch_to_alert() + alert_text = alert.text # this is where the exception is thrown + return alert + except NoAlertPresentException: + try: + time.sleep(1) + except UnexpectedAlertPresentException: + pass + + raise Exception("Alert was not present after %s seconds!"\ + % (selector, timeout)) diff --git a/test_framework/fixtures/tools.py b/test_framework/fixtures/tools.py new file mode 100755 index 000000000000..ac2cf431f0ab --- /dev/null +++ b/test_framework/fixtures/tools.py @@ -0,0 +1,8 @@ +""" +This module imports all the commonly used fixtures in one place so that +every test doesn't need to import a number of different fixtures. +""" + +from test_framework.fixtures.page_loads import * +from test_framework.fixtures.page_interactions import * +from test_framework.fixtures.errors import * diff --git a/test_framework/plugins/__init__.py b/test_framework/plugins/__init__.py new file mode 100755 index 000000000000..e69de29bb2d1 diff --git a/test_framework/plugins/base_plugin.py b/test_framework/plugins/base_plugin.py new file mode 100755 index 000000000000..ab6477a6bfc6 --- /dev/null +++ b/test_framework/plugins/base_plugin.py @@ -0,0 +1,88 @@ +""" +This plugin is for saving logs and setting a test environment. +Vars include "env" and "log_path". +You can have tests behave differently based on the environment. +You can access the values of these variables from the tests. +""" + +import os +import shutil +import time +from nose.plugins import Plugin +from nose.exc import SkipTest +from test_framework.fixtures import constants, errors + + +class Base(Plugin): + """ + The base_plugin includes the following variables: + self.options.env -- the environment you pass in (--env) + self.options.log_path -- the directory in which the log files are saved (--log_path) + """ + name = 'testing_base' # Usage: --with-testing_base + + + def options(self, parser, env): + super(Base, self).options(parser, env=env) + parser.add_option('--env', action='store', + dest='environment', + choices=(constants.Environment.QA, constants.Environment.PRODUCTION, constants.Environment.LOCAL, constants.Environment.TEST), + default=constants.Environment.TEST, + help="The environment to run the test") + parser.add_option('--log_path', dest='log_path', + default='logs/', + help='Where the log files are saved.') + + + def configure(self, options, conf): + super(Base, self).configure(options, conf) + if not self.enabled: + return + self.options = options + if options.log_path.endswith("/"): + options.log_path = options.log_path[:-1] + if not os.path.exists(options.log_path): + os.makedirs(options.log_path) + else: + if not os.path.exists("%s/../archived_logs/" % options.log_path): + os.makedirs("%s/../archived_logs/" % options.log_path) + shutil.move(options.log_path, "%s/../archived_logs/logs_%s"%( + options.log_path, int(time.time()))) + os.makedirs(options.log_path) + + + def beforeTest(self, test): + test_logpath = self.options.log_path + "/" + test.id() + if not os.path.exists(test_logpath): + os.makedirs(test_logpath) + + + def addError(self, test, err, capt=None): + """ + Since Skip, Blocked, and Deprecated are all technically errors, but not + error states, we want to make sure that they don't show up in nose output + as errors. + """ + if (err[0] == errors.BlockedTest or + err[0] == errors.SkipTest or + err[0] == errors.DeprecatedTest): + print err[1].__str__().split('-------------------- >> begin captured logging << --------------------', 1)[0] + + + def handleError(self, test, err, capt=None): + """ + If the database plugin is not present, we have to handle capturing + "errors" that shouldn't be reported as such in base. + """ + if not hasattr(test.test, "testcase_guid"): + if err[0] == errors.BlockedTest: + raise SkipTest(err[1]) + return True + + elif err[0] == errors.DeprecatedTest: + raise SkipTest(err[1]) + return True + + elif err[0] == errors.SkipTest: + raise SkipTest(err[1]) + return True diff --git a/test_framework/plugins/basic_test_info.py b/test_framework/plugins/basic_test_info.py new file mode 100755 index 000000000000..4c20795bd2d8 --- /dev/null +++ b/test_framework/plugins/basic_test_info.py @@ -0,0 +1,65 @@ +""" +The plugin for saving basic test info to the logs for Selenium tests. +The created file will be saved in the default logs folder (in .../logs) +Data to be saved includes: +* Last page url +* Browser +* Server +* Error +* Traceback +""" + +import os +import codecs +import traceback +from nose.plugins import Plugin + +class BasicTestInfo(Plugin): + """ + This plugin will capture basic info when a test fails or + raises an error. It will store that basic test info in + the default logs or in the file specified by the user. + """ + name = "basic_test_info" # Usage: --with-basic_test_info + + logfile_name = "basic_test_info.log" + + def options(self, parser, env): + super(BasicTestInfo, self).options(parser, env=env) + + + def configure(self, options, conf): + super(BasicTestInfo, self).configure(options, conf) + if not self.enabled: + return + self.options = options + + + def addError(self, test, err, capt=None): + test_logpath = self.options.log_path + "/" + test.id() + if not os.path.exists(test_logpath): + os.makedirs(test_logpath) + file_name = "%s/%s" % (test_logpath, self.logfile_name) + basic_info_file = codecs.open(file_name, "w+", "utf-8") + self.__log_test_error_data(basic_info_file, test, err, "Error") + basic_info_file.close() + + + def addFailure(self, test, err, capt=None, tbinfo=None): + test_logpath = self.options.log_path + "/" + test.id() + if not os.path.exists(test_logpath): + os.makedirs(test_logpath) + file_name = "%s/%s" % (test_logpath, self.logfile_name) + basic_info_file = codecs.open(file_name, "w+", "utf-8") + self.__log_test_error_data(basic_info_file, test, err, "Error") + basic_info_file.close() + + + def __log_test_error_data(self, log_file, test, err, type): + data_to_save = [] + data_to_save.append("Last_Page: %s" % test.driver.current_url) + data_to_save.append("Browser: %s " % self.options.browser) + data_to_save.append("Server: %s " % self.options.servername) + data_to_save.append("%s: %s" % (type, err[0])) + data_to_save.append("Traceback: " + ''.join(traceback.format_exception(*err))) + log_file.writelines("\r\n".join(data_to_save)) diff --git a/test_framework/plugins/db_reporting_plugin.py b/test_framework/plugins/db_reporting_plugin.py new file mode 100755 index 000000000000..ecd6d09c9828 --- /dev/null +++ b/test_framework/plugins/db_reporting_plugin.py @@ -0,0 +1,147 @@ +""" +The Database test reporting plugin for recording all test run data in the database. +""" + +import getpass +import time +import uuid +from optparse import SUPPRESS_HELP +from nose.plugins import Plugin +from nose.exc import SkipTest +from test_framework.core.application_manager import ApplicationManager +from test_framework.core.testcase_manager import ExecutionQueryPayload +from test_framework.core.testcase_manager import TestcaseDataPayload +from test_framework.core.testcase_manager import TestcaseManager +from test_framework.fixtures import constants +from test_framework.fixtures import errors + + +class DBReporting(Plugin): + """ + The plugin for reporting test results in the database. + """ + name = 'db_reporting' # Usage: --with-db_reporting + + def __init__(self): + """initialize some variables""" + Plugin.__init__(self) + self.execution_guid = str(uuid.uuid4()) + self.testcase_guid = None + self.execution_start_time = 0 + self.case_start_time = 0 + self.application = None + self.testcase_manager = None + self.error_handled = False + + + def options(self, parser, env): + super(DBReporting, self).options(parser, env=env) + parser.add_option('--database_environment', action='store', + dest='database_env', + choices=('prod', 'qa', 'test'), + default='test', + help=SUPPRESS_HELP) + + + #Plugin methods + def configure(self, options, conf): + """get the options""" + super(DBReporting, self).configure(options, conf) + self.options = options + self.testcase_manager = TestcaseManager(self.options.database_env) + + + def begin(self): + """At the start of the run, we want to record the + execution information to the database.""" + exec_payload = ExecutionQueryPayload() + exec_payload.execution_start_time = int(time.time() * 1000) + self.execution_start_time = exec_payload.execution_start_time + exec_payload.guid = self.execution_guid + exec_payload.username = getpass.getuser() + self.testcase_manager.insert_execution_data(exec_payload) + + + def startTest(self, test): + """at the start of the test, set the test case details""" + data_payload = TestcaseDataPayload() + self.testcase_guid = str(uuid.uuid4()) + data_payload.guid = self.testcase_guid + data_payload.execution_guid = self.execution_guid + if hasattr(test, "browser"): + data_payload.browser = test.browser + else: + data_payload.browser = "N/A" + data_payload.testcaseAddress = test.id() + data_payload.application = \ + ApplicationManager.generate_application_string(test) + data_payload.state = constants.State.NOTRUN + self.testcase_manager.insert_testcase_data(data_payload) + self.case_start_time = int(time.time() * 1000) + # Make the testcase guid available to other plugins + test.testcase_guid = self.testcase_guid + + + def finalize(self, result): + """At the end of the run, we want to + update that row with the execution time.""" + runtime = int(time.time() * 1000) - self.execution_start_time + self.testcase_manager.update_execution_data(self.execution_guid, + runtime) + + + def addSuccess(self, test, capt): + """ + After sucess of a test, we want to record the testcase run information. + """ + self.__insert_test_result(constants.State.PASS, test) + + + def addError(self, test, err, capt=None): + """ + After error of a test, we want to record the testcase run information. + """ + self.__insert_test_result(constants.State.ERROR, test, err) + + + def handleError(self, test, err, capt=None): + """ + After error of a test, we want to record the testcase run information. + "Error" also encompasses any states other than Pass or Fail, so we + check for those first. + """ + if err[0] == errors.BlockedTest: + self.__insert_test_result(constants.State.BLOCKED, test, err) + self.error_handled = True + raise SkipTest(err[1]) + return True + + elif err[0] == errors.DeprecatedTest: + self.__insert_test_result(constants.State.DEPRECATED, test, err) + self.error_handled = True + raise SkipTest(err[1]) + return True + + elif err[0] == errors.SkipTest: + self.__insert_test_result(constants.State.SKIP, test, err) + self.error_handled = True + raise SkipTest(err[1]) + return True + + + def addFailure(self, test, err, capt=None, tbinfo=None): + """ + After failure of a test, we want to record the testcase run information. + """ + self.__insert_test_result(constants.State.FAILURE, test, err) + + + def __insert_test_result(self, state, test, err=None): + data_payload = TestcaseDataPayload() + data_payload.runtime = int(time.time() * 1000) - self.case_start_time + data_payload.guid = self.testcase_guid + data_payload.execution_guid = self.execution_guid + data_payload.state = state + if err is not None: + data_payload.message = err[1].__str__().split('-------------------- >> begin captured logging << --------------------', 1)[0] + self.testcase_manager.update_testcase_data(data_payload) diff --git a/test_framework/plugins/hipchat_reporting_plugin.py b/test_framework/plugins/hipchat_reporting_plugin.py new file mode 100755 index 000000000000..b3dd1968d494 --- /dev/null +++ b/test_framework/plugins/hipchat_reporting_plugin.py @@ -0,0 +1,119 @@ +""" This plugin allows you to receive test notifications through HipChat. +HipChat @ mentions will only occur during normal business hours. (You can change this.) +By default, only failure notifications will be sent. +""" + +import os +import requests +import logging +import datetime +from nose.plugins import Plugin + + +HIPCHAT_URL = 'https://api.hipchat.com/v1/rooms/message' +HIPCHAT_AUTH_TOKEN = '[ENTER YOUR HIPCHAT AUTH TOKEN HERE]' + + +class HipchatReporting(Plugin): + name = 'hipchat_reporting' # Usage: --with-hipchat_reporting --room_id=[HIPCHAT ROOM ID] --owner_to_mention=[HIPCHAT @NAME] + + def __init__(self): + super(HipchatReporting, self).__init__() + self.room_id = None + self.owner_to_mention = None + self.notify_on_success = False + self.build_url = os.environ.get("BUILD_URL") + self.successes = [] + self.failures = [] + self.errors = [] + + + def options(self, parser, env): + super(HipchatReporting, self).options(parser, env=env) + parser.add_option('--room_id', action='store', + dest='room_id', + help="The hipchat room ID notifications will be sent to.", + default=None) + parser.add_option('--owner_to_mention', action='store', + dest='owner_to_mention', + help="The hipchat username to @mention in notifications.", + default=None) + parser.add_option('--notify_on_success', action='store_true', default=False, + dest='notify_on_success', + help='Flag for including success notifications. If not specified, only notifies on errors/failures by default.') + + + def configure(self, options, conf): + super(HipchatReporting, self).configure(options, conf) + if not self.enabled: + return + if not options.room_id: + raise Exception("A hipchat room ID to notify must be specified when using the hipchat reporting plugin.") + else: + self.room_id = options.room_id + self.owner_to_mention = options.owner_to_mention or None + self.notify_on_success = options.notify_on_success + + + def addSuccess(self, test, capt): + self.successes.append(test.id()) + + + def addError(self, test, err, capt=None): + self.errors.append("ERROR: " + test.id()) + + + def addFailure(self, test, err, capt=None, tbinfo=None): + self.failures.append("FAILED: " + test.id()) + + + def finalize(self, result): + message = '' + success = True + if not result.wasSuccessful(): + success = False + if self.owner_to_mention and self._is_during_business_hours(): + message += "@" + self.owner_to_mention + '\n' + + if self.failures: + message += "\n".join(self.failures) + if self.errors: + message += '\n' + if self.errors: + message += "\n".join(self.errors) + + if self.build_url: + message += '\n' + self.build_url + + elif self.notify_on_success and self.successes: + message = "SUCCESS! The following tests ran successfully:\n+ " + message += "\n+ ".join(self.successes) + + if message: + self._send_hipchat_notification(message, success=success) + + + def _is_during_business_hours(self): + now = datetime.datetime.now() + # Mon - Fri, 9am-6pm + return now.weekday() <= 4 and now.hour >= 9 and now.hour <= 18 + + + def _send_hipchat_notification(self, message, success=True, sender='Selenium'): + response = requests.post(HIPCHAT_URL, params={ + 'auth_token': HIPCHAT_AUTH_TOKEN, + 'room_id': self.room_id, + 'from': sender, + 'message': message, + 'message_format': 'text', # @mentions are only supported in text format + 'color': 'green' if success else 'red', + 'notify': '0', + 'format': 'json' + }) + + if response.status_code == 200: + logging.debug("Notification sent to room %s", self.room_id) + return True + else: + logging.error("Failed to send notification to room %s", self.room_id) + return False diff --git a/test_framework/plugins/page_source.py b/test_framework/plugins/page_source.py new file mode 100755 index 000000000000..d71e32d3de8c --- /dev/null +++ b/test_framework/plugins/page_source.py @@ -0,0 +1,47 @@ +""" +The plugin for capturing and storing the page source on errors and failures. +""" + +import os +import codecs +from nose.plugins import Plugin + +class PageSource(Plugin): + """ + This plugin will capture the page source when a test fails + or raises an error. It will store the page source in the + logs file specified, along with default test information. + """ + name = "page_source" # Usage: --with-page_source + + logfile_name = "page_source.html" + + def options(self, parser, env): + super(PageSource, self).options(parser, env=env) + + + def configure(self, options, conf): + super(PageSource, self).configure(options, conf) + if not self.enabled: + return + self.options = options + + + def addError(self, test, err,capt=None): + test_logpath = self.options.log_path + "/" + test.id() + if not os.path.exists(test_logpath): + os.makedirs(test_logpath) + html_file_name = "%s/%s" % (test_logpath, self.logfile_name) + html_file = codecs.open(html_file_name, "w+","utf-8") + html_file.write(test.driver.page_source) + html_file.close() + + + def addFailure(self, test, err, capt=None,tbinfo = None): + test_logpath = self.options.log_path + "/" + test.id() + if not os.path.exists(test_logpath): + os.makedirs(test_logpath) + html_file_name = "%s/%s"%(test_logpath,self.logfile_name) + html_file = codecs.open(html_file_name, "w+","utf-8") + html_file.write(test.driver.page_source) + html_file.close() diff --git a/test_framework/plugins/s3_logging_plugin.py b/test_framework/plugins/s3_logging_plugin.py new file mode 100755 index 000000000000..d0a396ec902b --- /dev/null +++ b/test_framework/plugins/s3_logging_plugin.py @@ -0,0 +1,52 @@ +""" +The S3 Logging Plugin to upload all logs to the S3 bucket specifed. +""" + +import uuid +import logging +import os +from test_framework.core.s3_manager import S3LoggingBucket +from nose.plugins import Plugin + + +class S3Logging(Plugin): + """ + The plugin for uploading test logs to the S3 bucket specified. + """ + name = 's3_logging' # Usage: --with-s3_logging + + + def configure(self, options, conf): + """ Get the options. """ + super(S3Logging, self).configure(options, conf) + self.options = options + + + def afterTest(self, test): + """ After each testcase, upload logs to the S3 bucket. """ + s3_bucket = S3LoggingBucket() + guid = str(uuid.uuid4().hex) + path = "%s/%s" % (self.options.log_path, + test.test.id()) + uploaded_files = [] + for logfile in os.listdir(path): + logfile_name = "%s/%s/%s" % (guid, + test.test.id(), + logfile.split(path)[-1]) + s3_bucket.upload_file(logfile_name, + "%s/%s" % (path, logfile)) + uploaded_files.append(logfile_name) + s3_bucket.save_uploaded_file_names(uploaded_files) + index_file = s3_bucket.upload_index_file(test.id(), guid) + print "Log files uploaded: %s" % index_file + logging.error("Log files uploaded: %s" % index_file) + + # If the database plugin is running, attach a link to the logs index database row + if hasattr(test.test, "testcase_guid"): + from test_framework.core.testcase_manager \ + import TestcaseDataPayload, TestcaseManager + self.testcase_manager = TestcaseManager(self.options.database_env) + data_payload = TestcaseDataPayload() + data_payload.guid = test.test.testcase_guid + data_payload.logURL = index_file + self.testcase_manager.update_testcase_log_url(data_payload) diff --git a/test_framework/plugins/screen_shots.py b/test_framework/plugins/screen_shots.py new file mode 100755 index 000000000000..1895f8d03139 --- /dev/null +++ b/test_framework/plugins/screen_shots.py @@ -0,0 +1,57 @@ +""" +Contains the plugin for screenshots for the selenium tests. +""" + +import os +import time +import base64 +from nose.plugins import Plugin + +class ScreenShots(Plugin): + """ + This plugin will take a screenshot when a test raises an error + or when a test fails. It will store that screenshot either in + the default logs file or another file of the user's specification + along with default test and time ran info. + """ + + name = "screen_shots" + logfile_name = "screenshot.jpg" + logfile_name_2 = "screenshot_fullscreen.jpg" # Selenium browser windows aren't always maximized, so this may show you more details + + def options(self, parser, env): + super(ScreenShots, self).options(parser, env=env) + + + def configure(self, options, conf): + super(ScreenShots, self).configure(options, conf) + if not self.enabled: + return + self.options = options + + + def add_screenshot(self, test, err, capt=None, tbinfo=None): + test_logpath = self.options.log_path + "/" + test.id() + if not os.path.exists(test_logpath): + os.makedirs(test_logpath) + screenshot_file = "%s/%s" % (test_logpath, self.logfile_name) + test.driver.get_screenshot_as_file(screenshot_file) + try: + test.driver.maximize_window() + except Exception: + pass + screen_b64 = test.driver.get_screenshot_as_base64() + screen = base64.decodestring(screen_b64) + time.sleep(0.3) + screenshot_file_2 = "%s/%s" % (test_logpath, self.logfile_name_2) + f1 = open(screenshot_file_2, 'w+') + f1.write(screen) + f1.close() + + + def addError(self, test, err, capt=None): + self.add_screenshot(test, err, capt=capt) + + + def addFailure(self, test, err, capt=None, tbinfo=None): + self.add_screenshot(test, err, capt=capt, tbinfo=tbinfo) diff --git a/test_framework/plugins/selenium_plugin.py b/test_framework/plugins/selenium_plugin.py new file mode 100755 index 000000000000..165cd89dbc5a --- /dev/null +++ b/test_framework/plugins/selenium_plugin.py @@ -0,0 +1,164 @@ +""" +This is the Selenium plugin. It takes in some default parameters that tests need. +It also provides a WebDriver object for the tests to use. +""" + +import time +import os +from nose.plugins import Plugin +from selenium import webdriver +from selenium.webdriver import DesiredCapabilities +from test_framework.core import selenium_launcher +from test_framework.fixtures import constants + + +class SeleniumBase(Plugin): + """ + The plugin for Selenium tests. Takes in key arguments and then + creates a WebDriver object. All arguments are passed to the tests. + + The following variables are made to the tests: + self.options.browser -- the browser to use (--browser) + self.options.server -- the server used by the test (--server) + self.options.port -- the port used by thest (--port) + """ + name = 'selenium' # Usage: --with-selenium + + def options(self, parser, env): + super(SeleniumBase, self).options(parser, env=env) + + parser.add_option('--browser', action='store', + dest='browser', + choices=constants.Browser.VERSION.keys(), + default=constants.Browser.FIREFOX, + help="""Specifies the browser to use. Default = FireFox. + If you want to use Chrome, explicitly indicate that.""") + parser.add_option('--browser_version', action='store', + dest='browser_version', + default="latest", + help="""The browser version to use. Explicitly select + a version number or use "latest".""") + parser.add_option('--server', action='store', dest='servername', + default='localhost', + help="Designates the server used by the test. Default: localhost.") + parser.add_option('--port', action='store', dest='port', + default='4444', + help="Designates the port used by the test. Default: 4444.") + + + def configure(self, options, conf): + super(SeleniumBase, self).configure(options, conf) + if not self.enabled: + return + + # Determine the browser version to use, and create a DesiredCapabilities dict + self.browser_settings = { + "browserName": options.browser, + 'name': self.conf.testNames[0], + 'build': os.getenv('BUILD_TAG'), + 'project': os.getenv('JOB_NAME') + } + + if options.browser == constants.Browser.INTERNET_EXPLORER: + self.browser_settings["platform"] = "WINDOWS" + self.browser_settings["browserName"] = "internet explorer" + + if options.browser_version == 'latest': + version = constants.Browser.LATEST[options.browser] + if version is not None: + self.browser_settings["version"] = version + else: + version_options = constants.Browser.VERSION[options.browser] + if (version_options is not None and + options.browser_version in version_options): + self.browser_settings["version"] = options.browser_version + + self.options = options + ### print 'OPTIONS = ' + str(self.options) # Try this for debugging if needed + + if (self.options.servername == "localhost" and + self.options.browser == constants.Browser.HTML_UNIT): + selenium_launcher.execute_selenium(self.options.servername, + self.options.port, + self.options.log_path) + time.sleep(20) + try: + driver = webdriver.Remote("http://%s:%s/wd/hub" % + (self.options.servername, + self.options.port), + DesiredCapabilities.HTML_UNIT) + driver.quit() + except: + raise Exception ("Selenium did not launch. Try again.") + + + def beforeTest(self, test): + """ Running Selenium locally will be handled differently + from how Selenium is run remotely, such as from Jenkins. """ + + if self.options.servername == "localhost": + try: + self.driver = self.__select_browser(self.options.browser) + test.test.driver = self.driver + if "version" in self.browser_settings.keys(): + version = self.browser_settings["version"] + else: + version = "" + test.test.browser = "%s%s" % (self.options.browser, version) + except Exception as err: + print "Error starting/connecting to Selenium:" + print err + os.kill(os.getpid(), 9) + else: + connected = False + for i in range(1, 4): + try: + self.driver = self.__select_browser(self.options.browser) + test.test.driver = self.driver + if "version" in self.browser_settings.keys(): + version = self.browser_settings["version"] + else: + version = "" + test.test.browser = "%s%s" % (self.options.browser, version) + connected = True + break + except Exception as err: + # nose eats beforeTest exceptions, so this gets the word out if something breaks here + print "Attempt #%s to connect to Selenium failed" % i + if i < 3: + print "Retrying in 15 seconds..." + time.sleep(15) + if not connected: + print "Error starting/connecting to Selenium:" + print err + print "\n\n\n" + os.kill(os.getpid(), 9) + + + def afterTest(self, test): + try: + self.driver.quit() + except: + print "No driver to quit." + + + def __select_browser(self, browser_name): + if (self.options.servername != "localhost" or + self.options.browser == constants.Browser.HTML_UNIT): + return webdriver.Remote("http://%s:%s/wd/hub" % + (self.options.servername, + self.options.port), + self.browser_settings) + else: + if browser_name == constants.Browser.FIREFOX: + return webdriver.Firefox() + if browser_name == constants.Browser.INTERNET_EXPLORER: + return webdriver.Ie() + if browser_name == constants.Browser.GOOGLE_CHROME: + try: + # Make it possible for Chrome to save screenshot files to disk. + chrome_options = webdriver.ChromeOptions() + chrome_options.add_argument("--allow-file-access-from-files") + return webdriver.Chrome(chrome_options=chrome_options) + except Exception: + return webdriver.Chrome() diff --git a/test_framework/test/__init__.py b/test_framework/test/__init__.py new file mode 100755 index 000000000000..e69de29bb2d1 From 85306b0c24782b4b435389a6b3755cfb5a3613a2 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 5 Mar 2014 16:18:23 -0500 Subject: [PATCH 002/219] Read the Docs: Fork the repository before cloning it --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e0a477d2ce32..248cb8ad2969 100755 --- a/README.md +++ b/README.md @@ -57,10 +57,12 @@ That installs the MySQL library so that you can use db commands in your code. To (There are web drivers for other web browsers as well. This one will get you started.) -**Step 1:** Checkout the framework with git: +**Step 1:** Checkout the SeleniumSpot Test Framework with Git or a Git GUI tool: + +First you'll want to fork the repository on GitHub to create your own copy. This is important because you'll want to add your own configurations, credentials, settings, etc. Now clone your forked SeleniumSpot repository to your development machine. You can use a tool such as [SourceTree](http://www.sourcetreeapp.com/) to make things easier by providing you with a simple-to-use user interface for viewing and managing your git commits and status. ```bash -git clone [LOCATION OF GITHUB FOLDER]/SeleniumSpot.git +git clone [LOCATION OF YOUR FORKED SELENIUMSPOT GITHUB FOLDER]/SeleniumSpot.git cd SeleniumSpot ``` From 75cdf05379e96e01cf7244853fcaf593d51d7e66 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 5 Mar 2014 16:26:37 -0500 Subject: [PATCH 003/219] Update the remote selenium config file (missing req) --- grid/selenium_server_config_example.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/grid/selenium_server_config_example.cfg b/grid/selenium_server_config_example.cfg index ce1d0b157835..6fb4f5ee698e 100644 --- a/grid/selenium_server_config_example.cfg +++ b/grid/selenium_server_config_example.cfg @@ -3,6 +3,7 @@ with-xunit=1 with-selenium=1 server=[IF NOT RUNNING THE TESTS LOCALLY, ENTER_YOUR_SELENIUM_SERVER_HOSTNAME_HERE - MIGHT BE YOUR OWN, OR ON AN EC2 MACHINE, PORT 4444 LIKELY, OR YOU MIGHT BE USING BROWSERSTACK: *.browserstack.com, PORT 80 LIKELY. IF RUNNING LOCALLY REMOVE THIS ENTIRE LINE AND THE LINE WITH THE "PORT"!] port=4444 +with-testing_base=1 with-page_source=1 with-screen_shots=1 with-s3_logging=1 From e7fcd2ac58e28438dbfb033099138d5da7a95fe6 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 25 Mar 2014 21:43:02 -0400 Subject: [PATCH 004/219] errplane is now influxdb (useful integration for capturing metrics) --- requirements.pip | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.pip b/requirements.pip index 8aa337f7cab1..8f7c403628a1 100755 --- a/requirements.pip +++ b/requirements.pip @@ -9,4 +9,4 @@ BeautifulSoup unittest2 simplejson chardet -errplane +influxdb From f25b2ce1ebcc6ce5932bad44e0052ff0600c8f54 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 31 Mar 2014 16:59:23 -0400 Subject: [PATCH 005/219] Update testcaserepository.sql --- test_framework/core/testcaserepository.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test_framework/core/testcaserepository.sql b/test_framework/core/testcaserepository.sql index ae38bac37b18..9b4f38c47715 100755 --- a/test_framework/core/testcaserepository.sql +++ b/test_framework/core/testcaserepository.sql @@ -40,8 +40,9 @@ CREATE TABLE `testcaseRunData` ( `state` varchar(255) DEFAULT NULL, `browser` varchar(255) DEFAULT NULL, `stackTrace` text, + `message` text, `retryCount` int(11) DEFAULT '0', `exceptionMap_guid` varchar(64) DEFAULT NULL, `logURL` text, PRIMARY KEY (`guid`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file +) ENGINE=InnoDB DEFAULT CHARSET=utf8; From 5ca1fae0443e8b37ef7e886e0ff0c52bdd8b2eb3 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 31 Mar 2014 17:06:53 -0400 Subject: [PATCH 006/219] Update testcaserepository.sql --- test_framework/core/testcaserepository.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_framework/core/testcaserepository.sql b/test_framework/core/testcaserepository.sql index 9b4f38c47715..bf6d8796b067 100755 --- a/test_framework/core/testcaserepository.sql +++ b/test_framework/core/testcaserepository.sql @@ -39,8 +39,8 @@ CREATE TABLE `testcaseRunData` ( `runtime` int(11), `state` varchar(255) DEFAULT NULL, `browser` varchar(255) DEFAULT NULL, - `stackTrace` text, `message` text, + `stackTrace` text, `retryCount` int(11) DEFAULT '0', `exceptionMap_guid` varchar(64) DEFAULT NULL, `logURL` text, From 2ccfa7db7286fda163d7d967c32108db13c43a59 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Apr 2014 10:19:14 -0400 Subject: [PATCH 007/219] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 248cb8ad2969..38ef2583f4a9 100755 --- a/README.md +++ b/README.md @@ -97,6 +97,8 @@ sudo python setup.py install (If you already have root access on the machine you're using, you might not need to add "sudo" before those commands.) +(If the pip install gives you a "clang error: unknown argument: '-mno-fused-madd'", see: http://stackoverflow.com/questions/22313407/clang-error-unknown-argument-mno-fused-madd-python-package-installation-fa) + **Step 4:** You can verify that Chromedriver and Selenium were successfully installed by checking inside a python command prompt: From da61000326adc6417d28f7615eb9e63f654dd537 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 3 Apr 2014 13:39:14 -0400 Subject: [PATCH 008/219] Use phantomjs for headless browser automation --- test_framework/plugins/selenium_plugin.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/test_framework/plugins/selenium_plugin.py b/test_framework/plugins/selenium_plugin.py index 165cd89dbc5a..f4259cb72f3b 100755 --- a/test_framework/plugins/selenium_plugin.py +++ b/test_framework/plugins/selenium_plugin.py @@ -7,7 +7,6 @@ import os from nose.plugins import Plugin from selenium import webdriver -from selenium.webdriver import DesiredCapabilities from test_framework.core import selenium_launcher from test_framework.fixtures import constants @@ -51,7 +50,7 @@ def configure(self, options, conf): if not self.enabled: return - # Determine the browser version to use, and create a DesiredCapabilities dict + # Determine the browser version to use, and configure settings self.browser_settings = { "browserName": options.browser, 'name': self.conf.testNames[0], @@ -81,15 +80,6 @@ def configure(self, options, conf): selenium_launcher.execute_selenium(self.options.servername, self.options.port, self.options.log_path) - time.sleep(20) - try: - driver = webdriver.Remote("http://%s:%s/wd/hub" % - (self.options.servername, - self.options.port), - DesiredCapabilities.HTML_UNIT) - driver.quit() - except: - raise Exception ("Selenium did not launch. Try again.") def beforeTest(self, test): @@ -154,6 +144,8 @@ def __select_browser(self, browser_name): return webdriver.Firefox() if browser_name == constants.Browser.INTERNET_EXPLORER: return webdriver.Ie() + if browser_name == constants.Browser.PHANTOM_JS: + return webdriver.PhantomJS() if browser_name == constants.Browser.GOOGLE_CHROME: try: # Make it possible for Chrome to save screenshot files to disk. From df59ab03c347a1c4e7252d2481a3dab8c8f7331a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 3 Apr 2014 13:40:22 -0400 Subject: [PATCH 009/219] Use phantomjs for headless browser automation --- test_framework/fixtures/constants.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test_framework/fixtures/constants.py b/test_framework/fixtures/constants.py index da04878b7200..c265c78a48c0 100755 --- a/test_framework/fixtures/constants.py +++ b/test_framework/fixtures/constants.py @@ -14,19 +14,22 @@ class Browser: INTERNET_EXPLORER = "ie" SAFARI = "safari" GOOGLE_CHROME = "chrome" + PHANTOM_JS = "phantomjs" HTML_UNIT = "htmlunit" VERSION = { "firefox" : None, - "ie" : ["8", "9"], + "ie" : ["8", "9", "10", "11"], "chrome" : None, + "phantomjs" : None, "htmlunit" : None } LATEST = { "firefox": None, - "ie": "9", + "ie": "11", "chrome": None, + "phantomjs" : None, "htmlunit": None } From 5638c911b447564c60c8030ea8c4a34e1a709854 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 3 Apr 2014 20:33:38 -0400 Subject: [PATCH 010/219] Making args available to tests --- test_framework/plugins/base_plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test_framework/plugins/base_plugin.py b/test_framework/plugins/base_plugin.py index ab6477a6bfc6..ff2fb17b2f10 100755 --- a/test_framework/plugins/base_plugin.py +++ b/test_framework/plugins/base_plugin.py @@ -55,6 +55,8 @@ def beforeTest(self, test): test_logpath = self.options.log_path + "/" + test.id() if not os.path.exists(test_logpath): os.makedirs(test_logpath) + test.test.environment = self.options.environment + test.test.args = self.options def addError(self, test, err, capt=None): From 43bcd94c98d7fa81ea66d4426d0b3471cd6ab857 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 3 Apr 2014 20:46:42 -0400 Subject: [PATCH 011/219] Add PhantomJS to the README. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 38ef2583f4a9..3d3c9015604f 100755 --- a/README.md +++ b/README.md @@ -50,11 +50,11 @@ That installs the MySQL library so that you can use db commands in your code. To mkdir -p $WORKON_HOME source /usr/local/bin/virtualenvwrapper.sh -[Chromedriver](http://code.google.com/p/chromedriver/) +[Chromedriver](http://code.google.com/p/chromedriver/) and [PhantomJS](http://phantomjs.org/) - brew install chromedriver + brew install chromedriver phantomjs -(There are web drivers for other web browsers as well. This one will get you started.) +(There are web drivers for other web browsers as well. These two will get you started.) **Step 1:** Checkout the SeleniumSpot Test Framework with Git or a Git GUI tool: From 1f23d7cb817f29bc876e30a0c469cae696cd9c75 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 4 Apr 2014 15:26:30 -0400 Subject: [PATCH 012/219] Fail fast if you can't connect to your MySQL DB --- test_framework/core/mysql.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test_framework/core/mysql.py b/test_framework/core/mysql.py index 76bb90e636f8..1394a7d9ba06 100755 --- a/test_framework/core/mysql.py +++ b/test_framework/core/mysql.py @@ -9,8 +9,8 @@ class DatabaseManager(): """ - This class wraps database fucntions for us for easy use. - It connects to the test case database + This class wraps database functions for easy use. + It connects to the testcase database. """ def __init__(self, database_env='test', conf_creds=None): @@ -20,7 +20,7 @@ def __init__(self, database_env='test', conf_creds=None): db_server, db_user, db_pass, db_schema = \ conf.APP_CREDS[conf.Apps.TESTCASE_REPOSITORY][database_env] retry_count = 3 - backoff = 10 + backoff = 3 # Time to wait (in seconds) between retries count = 0 while count < retry_count: try: From 75ab670c2e81b67729007f45966ff2b641df53e5 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 4 Apr 2014 18:54:23 -0400 Subject: [PATCH 013/219] Updates for the DB --- test_framework/core/testcase_manager.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test_framework/core/testcase_manager.py b/test_framework/core/testcase_manager.py index 9bc246fb141f..a114370c5fe5 100755 --- a/test_framework/core/testcase_manager.py +++ b/test_framework/core/testcase_manager.py @@ -40,14 +40,15 @@ def insert_testcase_data(self, testcase_run_payload): """inserts all data for the test case, returns the new row guid""" query = """INSERT INTO testcaseRunData - (guid, browser, state, execution_guid, application, + (guid, browser, state, execution_guid, env, start_time, testcaseAddress, runtime, retryCount, message, stackTrace) VALUES ( %(guid)s, %(browser)s, %(state)s, %(execution_guid)s, - %(application)s, + %(env)s, + %(start_time)s, %(testcaseAddress)s, %(runtime)s, %(retryCount)s, @@ -105,7 +106,8 @@ def __init__(self): self.browser = None self.state = None self.execution_guid = None - self.application = None + self.env = None + self.start_time = None self.runtime = None self.retry_count = 0 self.stack_trace = None @@ -121,7 +123,8 @@ def get_params(self): "browser": self.browser, "state": self.state, "execution_guid": self.execution_guid, - "application": self.application, + "env": self.env, + "start_time": self.start_time, "runtime": self.runtime, "retryCount": self.retry_count, "stackTrace": self.stack_trace, From 9fd0fee7ce52fc4d2ff406046c447fddc8105fb6 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 4 Apr 2014 18:55:29 -0400 Subject: [PATCH 014/219] Updates for the DB --- test_framework/plugins/db_reporting_plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test_framework/plugins/db_reporting_plugin.py b/test_framework/plugins/db_reporting_plugin.py index ecd6d09c9828..8fde5e7f987e 100755 --- a/test_framework/plugins/db_reporting_plugin.py +++ b/test_framework/plugins/db_reporting_plugin.py @@ -73,8 +73,9 @@ def startTest(self, test): else: data_payload.browser = "N/A" data_payload.testcaseAddress = test.id() - data_payload.application = \ - ApplicationManager.generate_application_string(test) + application = ApplicationManager.generate_application_string(test) + data_payload.env = application.split('.')[0] + data_payload.start_time = application.split('.')[1] data_payload.state = constants.State.NOTRUN self.testcase_manager.insert_testcase_data(data_payload) self.case_start_time = int(time.time() * 1000) From 4485229826f7c2fa5d1f13bf7e0ba6d8d0d2b200 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 4 Apr 2014 18:56:21 -0400 Subject: [PATCH 015/219] Updates for the DB --- test_framework/core/application_manager.py | 40 ++++++---------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/test_framework/core/application_manager.py b/test_framework/core/application_manager.py index 8b68564473a7..e020dc2701d7 100755 --- a/test_framework/core/application_manager.py +++ b/test_framework/core/application_manager.py @@ -1,45 +1,25 @@ """ -Methods for generating and parsing application strings used -in the Testcase Database +Method for generating application strings used in the Testcase Database. """ import time class ApplicationManager: """ - This class contains methods to generate application strings. We build - it from available test data + This class contains methods to generate application strings. """ @classmethod def generate_application_string(cls, test): - """generate an application string based on any of the given information - that can be pulled from the test object: app_name, app_env, - unique_id, user""" + """ Generate an application string based on some of the given information + that can be pulled from the test object: app_env, start_time. """ - app_name = '' app_env = 'test' - unique_id = '' - user = '' + if hasattr(test, 'env'): + app_env = test.env + elif hasattr(test, 'environment'): + app_env = test.environment - if hasattr(test, 'app_name'): - app_name = test.app_name + start_time = int(time.time() * 1000) - if hasattr(test, 'unique_id'): - unique_id = test.unique_id - else: - unique_id = int(time.time() * 1000) - - if hasattr(test, 'user'): - user = test.user - - return "%s.%s.%s.%s" % (app_name, app_env, unique_id, user) - - - @classmethod - def parse_application_string(cls, string): - """parse a generated application string into its parts: - app_name, app_env, unique_id, user """ - - pieces = string.split('.') - return pieces[0], pieces[1], pieces[2], pieces[3] + return "%s.%s" % (app_env, start_time) From 828bd1c0a1b347d1a37b7f4f2292033fda44a15a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 4 Apr 2014 18:57:03 -0400 Subject: [PATCH 016/219] Updates for the DB --- test_framework/core/testcaserepository.sql | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test_framework/core/testcaserepository.sql b/test_framework/core/testcaserepository.sql index bf6d8796b067..6628a5702c7e 100755 --- a/test_framework/core/testcaserepository.sql +++ b/test_framework/core/testcaserepository.sql @@ -33,12 +33,13 @@ CREATE TABLE `execution` ( # ----------------------------------- CREATE TABLE `testcaseRunData` ( `guid` varchar(64) NOT NULL DEFAULT '', - `testcaseAddress` varchar(1024) DEFAULT NULL, - `application` varchar(1024) DEFAULT NULL, + `testcaseAddress` varchar(255) DEFAULT NULL, + `env` varchar(64) DEFAULT NULL, + `start_time` varchar(64) DEFAULT NULL, `execution_guid` varchar(64) DEFAULT NULL, `runtime` int(11), - `state` varchar(255) DEFAULT NULL, - `browser` varchar(255) DEFAULT NULL, + `state` varchar(64) DEFAULT NULL, + `browser` varchar(64) DEFAULT NULL, `message` text, `stackTrace` text, `retryCount` int(11) DEFAULT '0', From c47ffb1e10e1c9f988a29246d5392aa57dd3c9ec Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 7 Apr 2014 18:15:23 -0400 Subject: [PATCH 017/219] Drop an unused table and save some memory. --- test_framework/core/testcaserepository.sql | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/test_framework/core/testcaserepository.sql b/test_framework/core/testcaserepository.sql index 6628a5702c7e..e8bb2d612ae4 100755 --- a/test_framework/core/testcaserepository.sql +++ b/test_framework/core/testcaserepository.sql @@ -2,7 +2,7 @@ # ----------------------------------- CREATE TABLE `delayedTestData` ( `guid` varchar(64) NOT NULL DEFAULT '', - `testcaseAddress` varchar(1024) NOT NULL DEFAULT '', + `testcaseAddress` varchar(255) NOT NULL DEFAULT '', `insertedAt` bigint(20) NOT NULL, `expectedResult` text, `done` tinyint(1) DEFAULT '0', @@ -11,14 +11,6 @@ CREATE TABLE `delayedTestData` ( UNIQUE KEY `uuid` (`guid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -# table exceptionMap -# ----------------------------------- -CREATE TABLE `exceptionMap` ( - `guid` varchar(64) NOT NULL DEFAULT '', - `jiraIssue` varchar(64) DEFAULT NULL, - PRIMARY KEY (`guid`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - # table execution # ----------------------------------- CREATE TABLE `execution` ( From 72767d89652e6fadba891c4c575102505c2fc356 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 10 Jul 2014 11:17:24 -0400 Subject: [PATCH 018/219] Update page_loads.py Too much sleep. (In wait_for_element_not_visible) --- test_framework/fixtures/page_loads.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test_framework/fixtures/page_loads.py b/test_framework/fixtures/page_loads.py index 0a587e03ef02..ca66c62dddf7 100755 --- a/test_framework/fixtures/page_loads.py +++ b/test_framework/fixtures/page_loads.py @@ -166,7 +166,6 @@ def wait_for_element_not_visible(driver, selector, time.sleep(1) else: return - time.sleep(1) except Exception: return raise Exception("Element %s was still visible after %s seconds!"\ From dedd2a4e0a5e24ba1305e2448d9361c2d39a0053 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 1 Jun 2015 13:33:52 -0400 Subject: [PATCH 019/219] ipdb is the improved version of pdb --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3d3c9015604f..c7d2286e879c 100755 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ If the example is moving too fast for your eyes to see what's going on, there ar ```python import time; time.sleep(5) # sleep for 5 seconds (add this after the line you want to pause on) -import pdb; pdb.set_trace() # waits for your command. n = next line of current method, c = continue, s = step / next executed line (will jump) +import ipdb; ipdb.set_trace() # waits for your command. n = next line of current method, c = continue, s = step / next executed line (will jump) ``` You may also want to have your test sleep in other situations where you need to have your test wait for something. If you know what you're waiting for, you should be specific by using a command that waits for something specific to happen. @@ -163,7 +163,7 @@ If you need to debug things on the fly (in case of errors), use this line to run nosetests my_first_test.py --browser=chrome --with-selenium --pdb --pdb-failures -s ``` -The above code will leave your browser window open in case there's a failure, which is possible if the web pages from the example change the data that's displayed on the page. (pdb commands: 'c', 's', 'n' => continue, step, next). +The above code will leave your browser window open in case there's a failure, which is possible if the web pages from the example change the data that's displayed on the page. (ipdb commands: 'c', 's', 'n' => continue, step, next). **Step 6:** Complete the setup From 197232c945adacff762217404c7c197f1c1dfdf8 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 15 Jun 2015 23:55:27 -0400 Subject: [PATCH 020/219] Updated Homebrew install line --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c7d2286e879c..932ac70c7ffa 100755 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ A working system would be something like this: You have a QA build and a Prod bu [Homebrew](http://brew.sh/) + [Git](http://git-scm.com/) - ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)" + ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew install git brew update From 4f1067e2c3c62116a0f54ec9f23ef9810499ccfc Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 15 Jun 2015 23:55:59 -0400 Subject: [PATCH 021/219] Simplify example test --- README.md | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/README.md b/README.md index 932ac70c7ffa..0a6307ab21a4 100755 --- a/README.md +++ b/README.md @@ -120,25 +120,12 @@ from test_framework.fixtures import base_case class MyTestClass(base_case.BaseCase): def test_basic(self): - self.driver.get("http://www.wikipedia.org/") - self.wait_for_element_visible("a[href='//en.wikipedia.org/']", timeout=5).click() - self.wait_for_element_visible("div#simpleSearch", timeout=5) - self.wait_for_element_visible("input[name='search']", timeout=5) + self.driver.get("http://en.wikipedia.org/wiki/Main_Page") self.update_text_value("input[name='search']", "Boston\n") text = self.wait_for_element_visible("div#mw-content-text", timeout=5).text self.assertTrue("The Charles River separates Boston from " in text) self.wait_for_element_visible("a[title='Find out about Wikipedia']").click() self.wait_for_text_visible("Since its creation in 2001", "div#mw-content-text", timeout=5) - - self.driver.get("http://www.wikimedia.org/") - self.wait_for_element_visible('img[alt="Wikivoyage"]', timeout=5).click() - self.wait_for_element_visible("a[href='//en.wikivoyage.org/']", timeout=5).click() - self.wait_for_element_visible('a[title="Visit the main page"]', timeout=5) - self.wait_for_element_visible('input#searchInput', timeout=5) - self.update_text_value("input#searchInput", "Israel\n") - self.wait_for_element_visible("div#contentSub", timeout=5) - text = self.wait_for_element_visible("div#mw-content-text", timeout=5).text - self.assertTrue("The state of Israel" in text) ``` Now run the script: From 0f990a17025a073d516a5466ed611f22199a7f19 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 12 Jul 2015 18:47:40 -0400 Subject: [PATCH 022/219] Add MIT License --- LICENSE | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000000..aaa102c5a426 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Michael Mintz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + From d3b82672dc4f3bf740161ed2f8864c8346b6220e Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 12 Jul 2015 19:02:30 -0400 Subject: [PATCH 023/219] Clean up requirements --- requirements.pip | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/requirements.pip b/requirements.pip index 8f7c403628a1..a1c2e6daf57e 100755 --- a/requirements.pip +++ b/requirements.pip @@ -1,12 +1,13 @@ -python==2.7.5 -selenium==2.40.0 -MySQL_python -boto -SOAPpy +python==2.7.6 +selenium==2.46.0 nose==1.3.0 -requests -BeautifulSoup -unittest2 -simplejson -chardet -influxdb +requests==2.7.0 +urllib3==1.10.4 +BeautifulSoup==3.2.1 +unittest2==1.0.1 +chardet==2.3.0 +simplejson==3.7.3 +boto==2.38.0 +MySQL-python==1.2.5 +pdb==0.1 +ipdb==0.8.1 From c5facbff27ad68c9afa488aa55c68de1423ccf32 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 12 Jul 2015 19:21:02 -0400 Subject: [PATCH 024/219] Save space by not including the selenium server jar file with the repo --- grid/ReadMe.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 grid/ReadMe.txt diff --git a/grid/ReadMe.txt b/grid/ReadMe.txt new file mode 100644 index 000000000000..a0e7d27e4467 --- /dev/null +++ b/grid/ReadMe.txt @@ -0,0 +1,4 @@ +You may need to download selenium-server-standalone-2.40.0.jar (or the latest version) separately. +File not present with this repo to save space. +See: https://pypi.python.org/pypi/selenium/2.40.0 +You can download from here: http://selenium-release.storage.googleapis.com/2.40/selenium-server-standalone-2.40.0.jar From a8c2d211ca2f0f107af30fe74a3ba6b43d5ad50c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 12 Jul 2015 20:00:46 -0400 Subject: [PATCH 025/219] Shorten the default timeouts (before failing) in the base_case --- test_framework/fixtures/base_case.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py index 69a028121aa1..65a54abbf3ac 100755 --- a/test_framework/fixtures/base_case.py +++ b/test_framework/fixtures/base_case.py @@ -98,25 +98,25 @@ def update_text_value(self, selector, new_value, timeout=5, retry=False): time.sleep(0.5) - def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, timeout=30): + def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, timeout=10): return page_loads.wait_for_element_present(self.driver, selector, by, timeout) - def wait_for_element_visible(self, selector, by=By.CSS_SELECTOR, timeout=30): + def wait_for_element_visible(self, selector, by=By.CSS_SELECTOR, timeout=10): return page_loads.wait_for_element_visible(self.driver, selector, by, timeout) - def wait_for_text_visible(self, text, selector, by=By.CSS_SELECTOR, timeout=30): + def wait_for_text_visible(self, text, selector, by=By.CSS_SELECTOR, timeout=10): return page_loads.wait_for_text_visible(self.driver, text, selector, by, timeout) - def wait_for_element_absent(self, selector, by=By.CSS_SELECTOR, timeout=30): + def wait_for_element_absent(self, selector, by=By.CSS_SELECTOR, timeout=10): return page_loads.wait_for_element_absent(self.driver, selector, by, timeout) - def wait_for_element_not_visible(self, selector, by=By.CSS_SELECTOR, timeout=30): + def wait_for_element_not_visible(self, selector, by=By.CSS_SELECTOR, timeout=10): return page_loads.wait_for_element_not_visible(self.driver, selector, by, timeout) - def wait_for_and_switch_to_alert(self, timeout=30): + def wait_for_and_switch_to_alert(self, timeout=10): return page_loads.wait_for_and_switch_to_alert(self.driver, timeout) From 496ab3841f3db5b86c3086560c9fda4cc2d09869 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 12 Jul 2015 20:01:46 -0400 Subject: [PATCH 026/219] Wait before click --- test_framework/fixtures/base_case.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py index 65a54abbf3ac..5866d95926fa 100755 --- a/test_framework/fixtures/base_case.py +++ b/test_framework/fixtures/base_case.py @@ -50,9 +50,9 @@ def jquery_click(self, selector): return self.driver.execute_script("jQuery('%s').click()" % selector) - def click(self, selector): - ele = self.driver.find_element(by=By.CSS_SELECTOR, value=selector) - return ele.click() + def click(self, selector, by=By.CSS_SELECTOR, timeout=5): + element = page_loads.wait_for_element_visible(self.driver, selector, by, timeout=timeout) + return element.click() def scroll_to(self, selector): From 93819db1e846bd6fa3037c1f01fa0e8e1b5eeba1 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 12 Jul 2015 20:02:16 -0400 Subject: [PATCH 027/219] Simplify the readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0a6307ab21a4..7518bd8acbbc 100755 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ python ``` -**Step 5:** Now to verify the test framework installation by writing a simple Selenium script that performs basic actions such as navigating to a web page, clicking, waiting for page elements to appear, typing in text, scraping text on a page, and verifying text. (copy/paste this into a new file called "my_first_test.py"). This may be a good time to read up on css selectors. If you use Chrome, you can right-click on a page and select "Inspect Element" to see the details you need to create such a script. At a quick glance, dots are for class names and pound signs are for IDs. You'll also see something like "timeout=5" in the script, which tells the script how long to wait before failing (you can skip that argument and have it default to 30 seconds, which can be modified from the test framework code). +**Step 5:** Now to verify the test framework installation by writing a simple Selenium script that performs basic actions such as navigating to a web page, clicking, waiting for page elements to appear, typing in text, scraping text on a page, and verifying text. (copy/paste this into a new file called "my_first_test.py"). This may be a good time to read up on css selectors. If you use Chrome, you can right-click on a page and select "Inspect Element" to see the details you need to create such a script. At a quick glance, dots are for class names and pound signs are for IDs. ```python from test_framework.fixtures import base_case @@ -122,10 +122,10 @@ class MyTestClass(base_case.BaseCase): def test_basic(self): self.driver.get("http://en.wikipedia.org/wiki/Main_Page") self.update_text_value("input[name='search']", "Boston\n") - text = self.wait_for_element_visible("div#mw-content-text", timeout=5).text + text = self.wait_for_element_visible("div#mw-content-text").text self.assertTrue("The Charles River separates Boston from " in text) - self.wait_for_element_visible("a[title='Find out about Wikipedia']").click() - self.wait_for_text_visible("Since its creation in 2001", "div#mw-content-text", timeout=5) + self.click("a[title='Find out about Wikipedia']") + self.wait_for_text_visible("Since its creation in 2001", "div#mw-content-text") ``` Now run the script: From 003f3f9c2b46305fee138064d6781f827ca9cba0 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 12 Jul 2015 20:29:50 -0400 Subject: [PATCH 028/219] Save time in the future by using .bash_profile --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 7518bd8acbbc..5c9ac0f80ec3 100755 --- a/README.md +++ b/README.md @@ -50,6 +50,12 @@ That installs the MySQL library so that you can use db commands in your code. To mkdir -p $WORKON_HOME source /usr/local/bin/virtualenvwrapper.sh +To save time from having to source virtualenvwrapper again when you open a new window, you can do the following: +1. Go to your home directory ("cd ~/") +2. Create a file called ".bash_profile" ("vi .bash_profile") +3. Add the line "source /usr/local/bin/virtualenvwrapper.sh" to the file. +("vi" is a fast text editor - "i" for insert-text mode, "esc" to get out of insert-text mode, ":wq"+[Enter] to save & exit the file) + [Chromedriver](http://code.google.com/p/chromedriver/) and [PhantomJS](http://phantomjs.org/) brew install chromedriver phantomjs From d284ad58106cb90aaa66f549813883b73ba80b77 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 12 Jul 2015 20:50:40 -0400 Subject: [PATCH 029/219] Make it easy to activate jquery on pages that don't have it on by default --- test_framework/fixtures/base_case.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py index 5866d95926fa..4b50b17e25ba 100755 --- a/test_framework/fixtures/base_case.py +++ b/test_framework/fixtures/base_case.py @@ -55,6 +55,11 @@ def click(self, selector, by=By.CSS_SELECTOR, timeout=5): return element.click() + def activate_jquery(self): + """ (It's not on by default on all website pages.) """ + self.driver.execute_script('var script = document.createElement("script"); script.src = "https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"; document.getElementsByTagName("head")[0].appendChild(script);') + + def scroll_to(self, selector): self.driver.execute_script("jQuery('%s')[0].scrollIntoView()" % selector) From 74b2b91e3877184bf2365275be72d8ad38958eb1 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 12 Jul 2015 21:02:23 -0400 Subject: [PATCH 030/219] Version updates --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 1ff074158948..3f0d521b48b5 100755 --- a/setup.py +++ b/setup.py @@ -6,12 +6,12 @@ setup( name = 'test_framework', - version = '1.0.0', - author = 'HubSpot Test Automation', - author_email = '@hubspot.com', + version = '1.1.0', + author = 'Michael Mintz', + author_email = '@mintzworld', maintainer = 'Michael Mintz', description = 'The SeleniumSpot Test Framework. (Powered by Python, WebDriver, and more...)', - license = 'Public', + license = 'The MIT License', packages = ['test_framework', 'test_framework.core', 'test_framework.plugins', From e24ab6049deb6ad452af2becfdf349f344d8ba99 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 12 Jul 2015 21:08:58 -0400 Subject: [PATCH 031/219] Keeping things real --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c9ac0f80ec3..027bd202e86e 100755 --- a/README.md +++ b/README.md @@ -502,7 +502,7 @@ Feel free to check out other exciting open source projects on GitHub: Happy Automating! -~ Michael Mintz (AKA MintzWorld / DrSelenium / your friendly neighborhood automation wizard) +~ Michael Mintz (https://github.com/mdmintz) ### Legal Disclaimer From 99c4cb994668e3fe4f1302a2ecdc85db9a78f16f Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 12 Jul 2015 21:24:46 -0400 Subject: [PATCH 032/219] Simplify the instructions for updating .bash_profile --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 027bd202e86e..05764cfe4451 100755 --- a/README.md +++ b/README.md @@ -50,10 +50,12 @@ That installs the MySQL library so that you can use db commands in your code. To mkdir -p $WORKON_HOME source /usr/local/bin/virtualenvwrapper.sh -To save time from having to source virtualenvwrapper again when you open a new window, you can do the following: -1. Go to your home directory ("cd ~/") -2. Create a file called ".bash_profile" ("vi .bash_profile") -3. Add the line "source /usr/local/bin/virtualenvwrapper.sh" to the file. +To save time from having to source virtualenvwrapper again when you open a new window, you can add the line "source /usr/local/bin/virtualenvwrapper.sh" to a file called ".bash_profile" in your home directory. + + cd ~/ + vi .bash_profile + source /usr/local/bin/virtualenvwrapper.sh + ("vi" is a fast text editor - "i" for insert-text mode, "esc" to get out of insert-text mode, ":wq"+[Enter] to save & exit the file) [Chromedriver](http://code.google.com/p/chromedriver/) and [PhantomJS](http://phantomjs.org/) From ce66eb6dfbe003a9a324b2efa842f86e2cd2b2cc Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 12 Jul 2015 22:07:07 -0400 Subject: [PATCH 033/219] self.open(url) as an alternative to self.driver.get(url) --- test_framework/fixtures/base_case.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py index 4b50b17e25ba..064f2456ecdb 100755 --- a/test_framework/fixtures/base_case.py +++ b/test_framework/fixtures/base_case.py @@ -55,6 +55,10 @@ def click(self, selector, by=By.CSS_SELECTOR, timeout=5): return element.click() + def open(self, url): + self.driver.get(url) + + def activate_jquery(self): """ (It's not on by default on all website pages.) """ self.driver.execute_script('var script = document.createElement("script"); script.src = "https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"; document.getElementsByTagName("head")[0].appendChild(script);') From 9c1f47e871a6d78b40db4ac95a50882617f12b90 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 12 Jul 2015 22:14:35 -0400 Subject: [PATCH 034/219] Have the README.md example use the simplified open(url) call --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 05764cfe4451..c93606cef881 100755 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ from test_framework.fixtures import base_case class MyTestClass(base_case.BaseCase): def test_basic(self): - self.driver.get("http://en.wikipedia.org/wiki/Main_Page") + self.open("http://en.wikipedia.org/wiki/Main_Page") self.update_text_value("input[name='search']", "Boston\n") text = self.wait_for_element_visible("div#mw-content-text").text self.assertTrue("The Charles River separates Boston from " in text) From 6877c6d89b5334176f825d96784334cd78590071 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 15 Jul 2015 02:34:46 -0400 Subject: [PATCH 035/219] Talk about lsvirtualenv in the readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index c93606cef881..82f934a1775d 100755 --- a/README.md +++ b/README.md @@ -95,6 +95,11 @@ To get back into your virtual environment, use the following command: workon seleniumspot ``` +To see a list of environments that exist on your system, use the following command: + +```bash +lsvirtualenv +``` **Step 3:** Install necessary packages from the SeleniumSpot folder and compile the test framework From 8424b65f5784f5421a04a27f4737c7c5eab49726 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 15 Jul 2015 02:36:07 -0400 Subject: [PATCH 036/219] Talk about "pip freeze" in the readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 82f934a1775d..bfd0b77d5633 100755 --- a/README.md +++ b/README.md @@ -112,6 +112,12 @@ sudo python setup.py install (If the pip install gives you a "clang error: unknown argument: '-mno-fused-madd'", see: http://stackoverflow.com/questions/22313407/clang-error-unknown-argument-mno-fused-madd-python-package-installation-fa) +In some cames, certain packages will have other dependencies as requirements, and those will get installed automatically. You'll be able to see all installed packages in your virtual environment by using the following command: + +```bash +pip freeze +``` + **Step 4:** You can verify that Chromedriver and Selenium were successfully installed by checking inside a python command prompt: From 3bb94eaa9a49a2d2b2cc30d19ba4f10e36f5410a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 15 Jul 2015 02:39:13 -0400 Subject: [PATCH 037/219] Handling pop-up alerts in the browser --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bfd0b77d5633..b2c3e7d0ab03 100755 --- a/README.md +++ b/README.md @@ -365,7 +365,7 @@ self.wait_for_element_visible("textarea").send_keys(Keys.SPACE + Keys.BACK_SPACE #### Switching Tabs -So what if your test opens up a new tab/window and now you have more than one page? No problem. You just need to specify which one you currently want Selenium to use. Switching between them is easy: +What if your test opens up a new tab/window and now you have more than one page? No problem. You just need to specify which one you currently want Selenium to use. Switching between tabs/windows is easy: Ex: ```python @@ -384,6 +384,19 @@ self.driver.switch_to_frame('ContentManagerTextBody_ifr') self.driver.switch_to_default_content() # exit the iFrame when you're done ``` +#### Handle Pop-Up Alerts + +What if your test makes an alert pop up in your browser? No problem. You just need to switch to it and either accept it or dismiss it: +Ex: + +```python +self.driver.switch_to_alert().accept() + +self.driver.switch_to_alert().dismiss() +``` + +If you're not sure whether there's an alert before trying to accept or dismiss it, one way to handle that is to wrap your alert-handling code in a try/except block. Other methods such as .text and .send_keys() will also work with alerts. + #### Executing Custom jQuery Scripts: jQuery is a powerful JavaScript library that allows you to perform advanced actions in a web browser. From bcbce186539762e0d36cc8900e6ecb1c81d34003 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 15 Jul 2015 02:40:00 -0400 Subject: [PATCH 038/219] Figuring out what the default timeouts should be. --- test_framework/fixtures/base_case.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py index 064f2456ecdb..3e065ea6a4a3 100755 --- a/test_framework/fixtures/base_case.py +++ b/test_framework/fixtures/base_case.py @@ -30,7 +30,7 @@ def hover_on_element(self, selector): return page_interactions.hover_on_element(self.driver, selector) - def hover_and_click(self, hover_selector, click_selector, click_by=By.CSS_SELECTOR, timeout=5): + def hover_and_click(self, hover_selector, click_selector, click_by=By.CSS_SELECTOR, timeout=7): return page_interactions.hover_and_click(self.driver, hover_selector, click_selector, click_by, timeout) @@ -50,7 +50,7 @@ def jquery_click(self, selector): return self.driver.execute_script("jQuery('%s').click()" % selector) - def click(self, selector, by=By.CSS_SELECTOR, timeout=5): + def click(self, selector, by=By.CSS_SELECTOR, timeout=7): element = page_loads.wait_for_element_visible(self.driver, selector, by, timeout=timeout) return element.click() @@ -70,7 +70,6 @@ def scroll_to(self, selector): def scroll_click(self, selector): self.scroll_to(selector) - time.sleep(0.1) self.click(selector) @@ -86,7 +85,7 @@ def set_value(self, selector, value): return self.driver.execute_script("jQuery('%s').val(%s)" % (selector, val)) - def update_text_value(self, selector, new_value, timeout=5, retry=False): + def update_text_value(self, selector, new_value, timeout=7, retry=False): """ This method updates a selector's text value with a new value @Params selector - the selector with the value to change @@ -104,28 +103,27 @@ def update_text_value(self, selector, new_value, timeout=5, retry=False): # Since selectors with quotes inside of quotes such as 'div[data-tab-name="advanced"]' break jQuery, format them first selector = self.jq_format(selector) self.set_value(selector, new_value) - time.sleep(0.5) - def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, timeout=10): + def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, timeout=14): return page_loads.wait_for_element_present(self.driver, selector, by, timeout) - def wait_for_element_visible(self, selector, by=By.CSS_SELECTOR, timeout=10): + def wait_for_element_visible(self, selector, by=By.CSS_SELECTOR, timeout=14): return page_loads.wait_for_element_visible(self.driver, selector, by, timeout) - def wait_for_text_visible(self, text, selector, by=By.CSS_SELECTOR, timeout=10): + def wait_for_text_visible(self, text, selector, by=By.CSS_SELECTOR, timeout=14): return page_loads.wait_for_text_visible(self.driver, text, selector, by, timeout) - def wait_for_element_absent(self, selector, by=By.CSS_SELECTOR, timeout=10): + def wait_for_element_absent(self, selector, by=By.CSS_SELECTOR, timeout=14): return page_loads.wait_for_element_absent(self.driver, selector, by, timeout) - def wait_for_element_not_visible(self, selector, by=By.CSS_SELECTOR, timeout=10): + def wait_for_element_not_visible(self, selector, by=By.CSS_SELECTOR, timeout=14): return page_loads.wait_for_element_not_visible(self.driver, selector, by, timeout) - def wait_for_and_switch_to_alert(self, timeout=10): + def wait_for_and_switch_to_alert(self, timeout=14): return page_loads.wait_for_and_switch_to_alert(self.driver, timeout) From bf2658dd5bc55729b0ed5684de3274ce8928c5b8 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 15 Jul 2015 02:41:11 -0400 Subject: [PATCH 039/219] Updated example that uses three different web browsers --- README.md | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b2c3e7d0ab03..4272aced2ea1 100755 --- a/README.md +++ b/README.md @@ -139,18 +139,27 @@ from test_framework.fixtures import base_case class MyTestClass(base_case.BaseCase): def test_basic(self): - self.open("http://en.wikipedia.org/wiki/Main_Page") - self.update_text_value("input[name='search']", "Boston\n") - text = self.wait_for_element_visible("div#mw-content-text").text - self.assertTrue("The Charles River separates Boston from " in text) - self.click("a[title='Find out about Wikipedia']") - self.wait_for_text_visible("Since its creation in 2001", "div#mw-content-text") + self.open("http://xkcd.com/1513/") + self.click('a[href="http://blag.xkcd.com"]') + self.wait_for_text_visible("The blag of the webcomic", "#site-description") + self.update_text_value("input#s", "Robots!\n") + self.wait_for_text_visible("Hooray robots!", "#content") + self.open("http://xkcd.com/1481/") + self.wait_for_element_visible("div#comic") + self.click('a[rel="license"]') + text = self.wait_for_element_visible('center').text + self.assertTrue("reuse any of my drawings" in text) + self.assertTrue("You can use them freely" in text) ``` -Now run the script: +Now try running the script using various web browsers: ```bash nosetests my_first_test.py --browser=chrome --with-selenium -s + +nosetests my_first_test.py --browser=phantomjs --with-selenium -s + +nosetests my_first_test.py --browser=firefox --with-selenium -s ``` After the test completes, in the console output you'll see a dot on a new line, representing a passing test. (On test failures you'll see an F instead, and on test errors you'll see an E). It looks more like a moving progress bar when you're running a ton of unit tests side by side. This is part of nosetests. After all tests complete (in this case there is only one), you'll see the "Ran 1 test in ..." line, followed by an "OK" if all nosetests passed. From 55506a70c79664c2eb6c21d33136be2f5ffc1e76 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 15 Jul 2015 20:00:31 -0400 Subject: [PATCH 040/219] Xvfb info for headless browsing from Jenkins --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 4272aced2ea1..aeae16a00525 100755 --- a/README.md +++ b/README.md @@ -193,6 +193,12 @@ If you're planning on using the full power of this test framework, there are a f * Setup your Selenium Grid and update your *.cfg file to point there. An example config file called selenium_server_config_example.cfg has been provided for you in the grid folder. The start-selenium-node.bat and start-selenium-server.sh files are for running your grid. In an example situation, your Selenium Grid server might live on a unix box and your Selenium Grid nodes might live on EC2 Windows virtual machines. When your build server runs a Selenium test, it would connect to your Selenium Grid to find out which Grid browser nodes are available to run that test. To simplify things, you can just use [Browser Stack](https://www.browserstack.com/automate) as your entire Selenium Grid (and let them do all the fun work of maintaining the grid for you). +* There are ways of running your tests from Jenkins without having to utilize a remote machine. One way is by using PhantomJS as your browser (it runs headlessly). Another way is by using Xvfb (another headless system). There's a plugin for Xvfb in Jenkins: https://wiki.jenkins-ci.org/display/JENKINS/Xvfb+Plugin +Here are some more helpful resources I found regarding the use of Xvfb: +http://stackoverflow.com/questions/6183276/how-do-i-run-selenium-in-xvfb +http://qxf2.com/blog/xvfb-plugin-for-jenkins-selenium/ +http://stackoverflow.com/questions/27202131/firefox-started-by-selenium-ignores-the-display-created-by-pyvirtualdisplay + * If you use [HipChat](https://www.hipchat.com/), you can have test alerts go there when tests fail. If that sounds good to you, update the db_reporting_plugin.py file from the plugins folder with your credentials. * Be sure to tell SeleniumSpot to use these added features when you set them up. That's easy to do. You would be running tests like this: From 74335c1c9491147cc4abdf517e4e143b5f767687 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 15 Jul 2015 20:01:20 -0400 Subject: [PATCH 041/219] Mini-tutorial on how to remove unwanted virtual envs --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index aeae16a00525..8b80c199cd74 100755 --- a/README.md +++ b/README.md @@ -101,6 +101,12 @@ To see a list of environments that exist on your system, use the following comma lsvirtualenv ``` +To delete a virtual environment that you no longer need, use the following command: + +```bash +rmvirtualenv [NAME OF VIRTUAL ENV TO REMOVE] +``` + **Step 3:** Install necessary packages from the SeleniumSpot folder and compile the test framework ```bash From b438ebee066799bc7c2a347ce0213adaf7541258 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 16 Jul 2015 13:06:39 -0400 Subject: [PATCH 042/219] Include other useful nosetest arguments in ReadMe --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 8b80c199cd74..ff1fcf010fb8 100755 --- a/README.md +++ b/README.md @@ -186,6 +186,14 @@ nosetests my_first_test.py --browser=chrome --with-selenium --pdb --pdb-failures The above code will leave your browser window open in case there's a failure, which is possible if the web pages from the example change the data that's displayed on the page. (ipdb commands: 'c', 's', 'n' => continue, step, next). +Here are some other useful nosetest arguments that you may want to append to your run commands: + +```bash +--logging-level=INFO # Hide DEBUG messages, which can be overwhelming. +-x # Stop running the tests after the first failure is reached. +-v # Prints the full test name rather than just a dot for each test. +--with-id # If -v is also used, will number the tests for easy counting. +``` **Step 6:** Complete the setup From 107d7423825c4098921d513cda1a088248ec0edc Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 16 Jul 2015 14:55:43 -0400 Subject: [PATCH 043/219] It's important to be aware of hidden files. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index ff1fcf010fb8..97dfdd16858b 100755 --- a/README.md +++ b/README.md @@ -124,6 +124,14 @@ In some cames, certain packages will have other dependencies as requirements, an pip freeze ``` +By default, some files may be hidden on a MAC, such as .gitignore (which is used to tell Git which files to ignore for staging commits). To view all files from the Finder window, run the following command in a terminal window: + +```bash +defaults write com.apple.finder AppleShowAllFiles -bool true +``` + +(You may need to reopen the MAC Finder window to see changes from that.) + **Step 4:** You can verify that Chromedriver and Selenium were successfully installed by checking inside a python command prompt: From d131e2cff3f7f8f3c479b00ade20c828c3da93fa Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 16 Jul 2015 18:34:55 -0400 Subject: [PATCH 044/219] Disable Firefox Reader View --- test_framework/plugins/selenium_plugin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test_framework/plugins/selenium_plugin.py b/test_framework/plugins/selenium_plugin.py index f4259cb72f3b..377bf1e73af7 100755 --- a/test_framework/plugins/selenium_plugin.py +++ b/test_framework/plugins/selenium_plugin.py @@ -141,7 +141,12 @@ def __select_browser(self, browser_name): self.browser_settings) else: if browser_name == constants.Browser.FIREFOX: - return webdriver.Firefox() + try: + profile = webdriver.FirefoxProfile() + profile.set_preference("reader.parse-on-load.enabled", False) + return webdriver.Firefox(profile) + except: + return webdriver.Firefox() if browser_name == constants.Browser.INTERNET_EXPLORER: return webdriver.Ie() if browser_name == constants.Browser.PHANTOM_JS: From 7eeebe5999acb10fb2f827c2cb27f6e13d1ca4d9 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 16 Jul 2015 20:07:06 -0400 Subject: [PATCH 045/219] Slack integration, and updates to existing documentation. --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 97dfdd16858b..a75f442fcb55 100755 --- a/README.md +++ b/README.md @@ -215,13 +215,14 @@ If you're planning on using the full power of this test framework, there are a f * Setup your Selenium Grid and update your *.cfg file to point there. An example config file called selenium_server_config_example.cfg has been provided for you in the grid folder. The start-selenium-node.bat and start-selenium-server.sh files are for running your grid. In an example situation, your Selenium Grid server might live on a unix box and your Selenium Grid nodes might live on EC2 Windows virtual machines. When your build server runs a Selenium test, it would connect to your Selenium Grid to find out which Grid browser nodes are available to run that test. To simplify things, you can just use [Browser Stack](https://www.browserstack.com/automate) as your entire Selenium Grid (and let them do all the fun work of maintaining the grid for you). -* There are ways of running your tests from Jenkins without having to utilize a remote machine. One way is by using PhantomJS as your browser (it runs headlessly). Another way is by using Xvfb (another headless system). There's a plugin for Xvfb in Jenkins: https://wiki.jenkins-ci.org/display/JENKINS/Xvfb+Plugin -Here are some more helpful resources I found regarding the use of Xvfb: -http://stackoverflow.com/questions/6183276/how-do-i-run-selenium-in-xvfb -http://qxf2.com/blog/xvfb-plugin-for-jenkins-selenium/ -http://stackoverflow.com/questions/27202131/firefox-started-by-selenium-ignores-the-display-created-by-pyvirtualdisplay +* There are ways of running your tests from Jenkins without having to utilize a remote machine. One way is by using PhantomJS as your browser (it runs headlessly). Another way is by using Xvfb (another headless system). [There's a plugin for Xvfb in Jenkins](https://wiki.jenkins-ci.org/display/JENKINS/Xvfb+Plugin). Here are some more helpful resources I found regarding the use of Xvfb: +1. http://stackoverflow.com/questions/6183276/how-do-i-run-selenium-in-xvfb +2. http://qxf2.com/blog/xvfb-plugin-for-jenkins-selenium/ +3. http://stackoverflow.com/questions/27202131/firefox-started-by-selenium-ignores-the-display-created-by-pyvirtualdisplay -* If you use [HipChat](https://www.hipchat.com/), you can have test alerts go there when tests fail. If that sounds good to you, update the db_reporting_plugin.py file from the plugins folder with your credentials. +* If you use [Slack](https://slack.com), you can easily have your Jenkins jobs display results there by using the [Jenkins Slack Plugin](https://github.com/jenkinsci/slack-plugin). Another way to send messages from your tests to Slack is by using [Slack's Incoming Webhooks API](https://api.slack.com/incoming-webhooks). + +* If you use [HipChat](https://www.hipchat.com/), you can have test alerts go there when tests fail by using the db_reporting plugin. Be sure to first update the db_reporting_plugin.py file from the plugins folder with your HipChat credentials. * Be sure to tell SeleniumSpot to use these added features when you set them up. That's easy to do. You would be running tests like this: From 27ae5e138d6d4a8e73ff77bb88f370ca9c71eaed Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 19 Jul 2015 00:58:11 -0400 Subject: [PATCH 046/219] Clean up spacing issues with page_source.py --- test_framework/plugins/page_source.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test_framework/plugins/page_source.py b/test_framework/plugins/page_source.py index d71e32d3de8c..8495a2769255 100755 --- a/test_framework/plugins/page_source.py +++ b/test_framework/plugins/page_source.py @@ -27,7 +27,7 @@ def configure(self, options, conf): self.options = options - def addError(self, test, err,capt=None): + def addError(self, test, err, capt=None): test_logpath = self.options.log_path + "/" + test.id() if not os.path.exists(test_logpath): os.makedirs(test_logpath) @@ -37,11 +37,11 @@ def addError(self, test, err,capt=None): html_file.close() - def addFailure(self, test, err, capt=None,tbinfo = None): + def addFailure(self, test, err, capt=None, tbinfo=None): test_logpath = self.options.log_path + "/" + test.id() if not os.path.exists(test_logpath): os.makedirs(test_logpath) - html_file_name = "%s/%s"%(test_logpath,self.logfile_name) - html_file = codecs.open(html_file_name, "w+","utf-8") + html_file_name = "%s/%s" % (test_logpath, self.logfile_name) + html_file = codecs.open(html_file_name, "w+", "utf-8") html_file.write(test.driver.page_source) html_file.close() From a176e4d5ddbf9831f20b4f74293795d8c6ad97a7 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 19 Jul 2015 01:27:13 -0400 Subject: [PATCH 047/219] Description updates in screen_shots.py --- test_framework/plugins/screen_shots.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test_framework/plugins/screen_shots.py b/test_framework/plugins/screen_shots.py index 1895f8d03139..251d1b885ed7 100755 --- a/test_framework/plugins/screen_shots.py +++ b/test_framework/plugins/screen_shots.py @@ -1,5 +1,5 @@ """ -Contains the plugin for screenshots for the selenium tests. +Contains the screenshot plugin for the selenium tests. """ import os @@ -9,15 +9,15 @@ class ScreenShots(Plugin): """ - This plugin will take a screenshot when a test raises an error - or when a test fails. It will store that screenshot either in - the default logs file or another file of the user's specification - along with default test and time ran info. + This plugin will take a screenshot when either a test fails + or raises an error. It will store that screenshot either in + the default logs file or in another file of the user's specification. """ name = "screen_shots" logfile_name = "screenshot.jpg" - logfile_name_2 = "screenshot_fullscreen.jpg" # Selenium browser windows aren't always maximized, so this may show you more details + # Browser windows aren't always maximized. This may display more details. + logfile_name_2 = "screenshot_fullscreen.jpg" def options(self, parser, env): super(ScreenShots, self).options(parser, env=env) From 8c602ce11f9389482d0322a40c58ce848729db7c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 19 Jul 2015 01:48:52 -0400 Subject: [PATCH 048/219] Browser versioning --- test_framework/fixtures/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_framework/fixtures/constants.py b/test_framework/fixtures/constants.py index c265c78a48c0..a5b8b16dbec4 100755 --- a/test_framework/fixtures/constants.py +++ b/test_framework/fixtures/constants.py @@ -19,7 +19,7 @@ class Browser: VERSION = { "firefox" : None, - "ie" : ["8", "9", "10", "11"], + "ie" : None, "chrome" : None, "phantomjs" : None, "htmlunit" : None @@ -27,7 +27,7 @@ class Browser: LATEST = { "firefox": None, - "ie": "11", + "ie": None, "chrome": None, "phantomjs" : None, "htmlunit": None From d3c0b9cc3f1ac5572e0cf2bb358bd714984817c1 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 19 Jul 2015 01:53:32 -0400 Subject: [PATCH 049/219] A wider selection of default environments to choose from. --- test_framework/fixtures/constants.py | 2 ++ test_framework/plugins/base_plugin.py | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/test_framework/fixtures/constants.py b/test_framework/fixtures/constants.py index a5b8b16dbec4..6100bc392a73 100755 --- a/test_framework/fixtures/constants.py +++ b/test_framework/fixtures/constants.py @@ -4,7 +4,9 @@ class Environment: QA = "qa" + STAGING = "staging" PRODUCTION = "production" + MASTER = "master" LOCAL = "local" TEST = "test" diff --git a/test_framework/plugins/base_plugin.py b/test_framework/plugins/base_plugin.py index ff2fb17b2f10..f262fa1d0cb9 100755 --- a/test_framework/plugins/base_plugin.py +++ b/test_framework/plugins/base_plugin.py @@ -26,9 +26,14 @@ def options(self, parser, env): super(Base, self).options(parser, env=env) parser.add_option('--env', action='store', dest='environment', - choices=(constants.Environment.QA, constants.Environment.PRODUCTION, constants.Environment.LOCAL, constants.Environment.TEST), + choices=(constants.Environment.QA, + constants.Environment.STAGING, + constants.Environment.PRODUCTION, + constants.Environment.MASTER, + constants.Environment.LOCAL, + constants.Environment.TEST), default=constants.Environment.TEST, - help="The environment to run the test") + help="The environment to run the tests in.") parser.add_option('--log_path', dest='log_path', default='logs/', help='Where the log files are saved.') From f347ae14b16a8536afe2afe3cf1ea535f21d7e77 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 19 Jul 2015 15:09:33 -0400 Subject: [PATCH 050/219] Easy to simplify the key import statement (can skip "fixtures") --- README.md | 8 ++++---- test_framework/__init__.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a75f442fcb55..79244b2d4042 100755 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ python **Step 5:** Now to verify the test framework installation by writing a simple Selenium script that performs basic actions such as navigating to a web page, clicking, waiting for page elements to appear, typing in text, scraping text on a page, and verifying text. (copy/paste this into a new file called "my_first_test.py"). This may be a good time to read up on css selectors. If you use Chrome, you can right-click on a page and select "Inspect Element" to see the details you need to create such a script. At a quick glance, dots are for class names and pound signs are for IDs. ```python -from test_framework.fixtures import base_case +from test_framework import base_case class MyTestClass(base_case.BaseCase): @@ -247,7 +247,7 @@ nosetests [YOUR_TEST_FILE].py:[SOME_CLASS_NAME].test_[SOME_TEST_NAME] --config=[ Let's try an example of a test that fails. Copy the following into a file called fail_test.py: ```python """ test_fail.py """ -from test_framework.fixtures import base_case +from test_framework import base_case class MyTestClass(base_case.BaseCase): @@ -271,7 +271,7 @@ Have you made it this far? Congratulations!!! Now you're ready to dive in at ful Important Note: Make sure you include the following import in your code to use the framework commands: ```python -from test_framework.fixtures import base_case +from test_framework import base_case ``` #### Navigating to a Page, Plus Some Other Useful Related Commands @@ -485,7 +485,7 @@ So by now you may be wondering how the nosetests code works? Nosetests will auto To use the test framework calls, don't forget to include the following import: ```python -from test_framework.fixtures import base_case +from test_framework import base_case ``` And you'll need to inherit the base case in your classes like so: diff --git a/test_framework/__init__.py b/test_framework/__init__.py index e69de29bb2d1..e38a96551071 100755 --- a/test_framework/__init__.py +++ b/test_framework/__init__.py @@ -0,0 +1 @@ +from test_framework.fixtures import base_case From a798615f8956e54e0dd559838d76242a90751e8d Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 19 Jul 2015 17:02:49 -0400 Subject: [PATCH 051/219] Simplify the import process even more. --- README.md | 14 +++++++------- test_framework/__init__.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 79244b2d4042..bb96e5a34cc0 100755 --- a/README.md +++ b/README.md @@ -148,9 +148,9 @@ python **Step 5:** Now to verify the test framework installation by writing a simple Selenium script that performs basic actions such as navigating to a web page, clicking, waiting for page elements to appear, typing in text, scraping text on a page, and verifying text. (copy/paste this into a new file called "my_first_test.py"). This may be a good time to read up on css selectors. If you use Chrome, you can right-click on a page and select "Inspect Element" to see the details you need to create such a script. At a quick glance, dots are for class names and pound signs are for IDs. ```python -from test_framework import base_case +import test_framework -class MyTestClass(base_case.BaseCase): +class MyTestClass(test_framework.BaseCase): def test_basic(self): self.open("http://xkcd.com/1513/") @@ -247,9 +247,9 @@ nosetests [YOUR_TEST_FILE].py:[SOME_CLASS_NAME].test_[SOME_TEST_NAME] --config=[ Let's try an example of a test that fails. Copy the following into a file called fail_test.py: ```python """ test_fail.py """ -from test_framework import base_case +import test_framework -class MyTestClass(base_case.BaseCase): +class MyTestClass(test_framework.BaseCase): def test_find_google_on_bing(self): self.driver.get("http://bing.com") @@ -271,7 +271,7 @@ Have you made it this far? Congratulations!!! Now you're ready to dive in at ful Important Note: Make sure you include the following import in your code to use the framework commands: ```python -from test_framework import base_case +import test_framework ``` #### Navigating to a Page, Plus Some Other Useful Related Commands @@ -485,13 +485,13 @@ So by now you may be wondering how the nosetests code works? Nosetests will auto To use the test framework calls, don't forget to include the following import: ```python -from test_framework import base_case +import test_framework ``` And you'll need to inherit the base case in your classes like so: ```python -class MyTestClass(base_case.BaseCase): +class MyTestClass(test_framework.BaseCase): ``` To understand the full scope of the test framework, we have to take a peek inside. From the top-level folder that contained the requirements.pip and setup.py files, there are two other major folders: "grid" and "test_framework". The Selenium "Grid" is what maintains the remote machines running selenium tests for "selenium.hubteam.com/jenkins". Machines can be spun up through Amazon EC2, and each one is capable of running 5 simultaneous browser tests. The other major folder, "test_framework", is what contains everything else. The "test_framework" folder contains all the major components such as "Core", "Fixtures", and "Plugins". For all intensive purposes, those sections are all equally important. They contain all the code and libraries that make our test framework useful (because otherwise we'd just be writing tests using raw selenium calls without any special add-ons or support). diff --git a/test_framework/__init__.py b/test_framework/__init__.py index e38a96551071..6f1182d9e7c6 100755 --- a/test_framework/__init__.py +++ b/test_framework/__init__.py @@ -1 +1 @@ -from test_framework.fixtures import base_case +from test_framework.fixtures.base_case import BaseCase From 2dec5b6bb07456387fe583c51d07b5fe7522b407 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 19 Jul 2015 17:28:55 -0400 Subject: [PATCH 052/219] Updates for the latest version of the selenium server. --- grid/ReadMe.txt | 9 +++++---- grid/start-selenium-node.bat | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/grid/ReadMe.txt b/grid/ReadMe.txt index a0e7d27e4467..f5ad1794de76 100644 --- a/grid/ReadMe.txt +++ b/grid/ReadMe.txt @@ -1,4 +1,5 @@ -You may need to download selenium-server-standalone-2.40.0.jar (or the latest version) separately. -File not present with this repo to save space. -See: https://pypi.python.org/pypi/selenium/2.40.0 -You can download from here: http://selenium-release.storage.googleapis.com/2.40/selenium-server-standalone-2.40.0.jar +You may need to download selenium-server-standalone-2.46.0.jar (or the latest version) separately. +That file is not present with this repository to save space. +See: https://pypi.python.org/pypi/selenium/2.46.0 +You can download that file directly from here: http://selenium-release.storage.googleapis.com/2.46/selenium-server-standalone-2.46.0.jar +Once you have downloaded the jar file, put it in this folder. diff --git a/grid/start-selenium-node.bat b/grid/start-selenium-node.bat index ffffcf4e82f5..6896a06f22ca 100644 --- a/grid/start-selenium-node.bat +++ b/grid/start-selenium-node.bat @@ -1,2 +1,2 @@ cd c:\ -java -jar selenium-server-standalone-2.40.0.jar -port 5555 -host [ENTER HOST OF THIS WORKER MACHINE ex: ec2-***-**-**-***.compute-1.amazonaws.com] -browser browserName=chrome,maxInstances=5 \ No newline at end of file +java -jar selenium-server-standalone-2.46.0.jar -port 5555 -host [ENTER HOST OF THIS WORKER MACHINE ex: ec2-***-**-**-***.compute-1.amazonaws.com] -browser browserName=chrome,maxInstances=5 \ No newline at end of file From 6621eae961fa726a17c47bf7cd890310d2cb4704 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 19 Jul 2015 17:42:35 -0400 Subject: [PATCH 053/219] Change the grid folder naming scheme. --- {grid => selenium_grid}/ReadMe.txt | 0 {grid => selenium_grid}/selenium_server_config_example.cfg | 0 {grid => selenium_grid}/start-selenium-node.bat | 0 {grid => selenium_grid}/start-selenium-server.sh | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {grid => selenium_grid}/ReadMe.txt (100%) rename {grid => selenium_grid}/selenium_server_config_example.cfg (100%) rename {grid => selenium_grid}/start-selenium-node.bat (100%) rename {grid => selenium_grid}/start-selenium-server.sh (100%) diff --git a/grid/ReadMe.txt b/selenium_grid/ReadMe.txt similarity index 100% rename from grid/ReadMe.txt rename to selenium_grid/ReadMe.txt diff --git a/grid/selenium_server_config_example.cfg b/selenium_grid/selenium_server_config_example.cfg similarity index 100% rename from grid/selenium_server_config_example.cfg rename to selenium_grid/selenium_server_config_example.cfg diff --git a/grid/start-selenium-node.bat b/selenium_grid/start-selenium-node.bat similarity index 100% rename from grid/start-selenium-node.bat rename to selenium_grid/start-selenium-node.bat diff --git a/grid/start-selenium-server.sh b/selenium_grid/start-selenium-server.sh similarity index 100% rename from grid/start-selenium-server.sh rename to selenium_grid/start-selenium-server.sh From 210b3c100159b79b05c23d51c861dae744f345fa Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 20 Jul 2015 00:40:15 -0400 Subject: [PATCH 054/219] Using the name given to the test framework --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3f0d521b48b5..2797dffd7161 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages setup( - name = 'test_framework', + name = 'seleniumspot', version = '1.1.0', author = 'Michael Mintz', author_email = '@mintzworld', From 5d6a5502af644d0b4e763494f444609724df9679 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 20 Jul 2015 00:42:48 -0400 Subject: [PATCH 055/219] Renaming the grid folder. --- {selenium_grid => grid_files}/ReadMe.txt | 0 {selenium_grid => grid_files}/selenium_server_config_example.cfg | 0 {selenium_grid => grid_files}/start-selenium-node.bat | 0 {selenium_grid => grid_files}/start-selenium-server.sh | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {selenium_grid => grid_files}/ReadMe.txt (100%) rename {selenium_grid => grid_files}/selenium_server_config_example.cfg (100%) rename {selenium_grid => grid_files}/start-selenium-node.bat (100%) rename {selenium_grid => grid_files}/start-selenium-server.sh (100%) diff --git a/selenium_grid/ReadMe.txt b/grid_files/ReadMe.txt similarity index 100% rename from selenium_grid/ReadMe.txt rename to grid_files/ReadMe.txt diff --git a/selenium_grid/selenium_server_config_example.cfg b/grid_files/selenium_server_config_example.cfg similarity index 100% rename from selenium_grid/selenium_server_config_example.cfg rename to grid_files/selenium_server_config_example.cfg diff --git a/selenium_grid/start-selenium-node.bat b/grid_files/start-selenium-node.bat similarity index 100% rename from selenium_grid/start-selenium-node.bat rename to grid_files/start-selenium-node.bat diff --git a/selenium_grid/start-selenium-server.sh b/grid_files/start-selenium-server.sh similarity index 100% rename from selenium_grid/start-selenium-server.sh rename to grid_files/start-selenium-server.sh From e81a5ce26373d50ab4c8e5bd66456f1f3d01be16 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 20 Jul 2015 03:52:07 -0400 Subject: [PATCH 056/219] A "common" area for decorators. --- common/__init__.py | 0 common/decorators.py | 43 ++++++++++++++++++++++++++++++++++++ setup.py | 3 ++- test_framework/core/utils.py | 40 --------------------------------- 4 files changed, 45 insertions(+), 41 deletions(-) create mode 100755 common/__init__.py create mode 100755 common/decorators.py delete mode 100755 test_framework/core/utils.py diff --git a/common/__init__.py b/common/__init__.py new file mode 100755 index 000000000000..e69de29bb2d1 diff --git a/common/decorators.py b/common/decorators.py new file mode 100755 index 000000000000..6792b6eaf6b7 --- /dev/null +++ b/common/decorators.py @@ -0,0 +1,43 @@ +import logging +import math +import time +from functools import wraps + + +def retry_on_exception(tries=6, delay=1, backoff=2, max_delay=32): + ''' + Decorator for implementing exponential backoff for retrying on failures. + + tries: Max number of tries to execute the wrapped function before failing. + delay: Delay time in seconds before the FIRST retry. + backoff: Multiplier to extend the initial delay by for each retry. + max_delay: Max time in seconds to wait between retries. + ''' + tries = math.floor(tries) + if tries < 1: + raise ValueError('"tries" must be greater than or equal to 1.') + if delay < 0: + raise ValueError('"delay" must be greater than or equal to 0.') + if backoff < 1: + raise ValueError('"backoff" must be greater than or equal to 1.') + if max_delay < delay: + raise ValueError('"max_delay" must be greater than or equal to delay.') + + def decorated_function_with_retry(func): + @wraps(func) + def function_to_retry(*args, **kwargs): + local_tries, local_delay = tries, delay + while local_tries > 1: + try: + return func(*args, **kwargs) + except Exception, e: + if local_delay > max_delay: + local_delay = max_delay + logging.exception('%s: Retrying in %d seconds...' + % (str(e), local_delay)) + time.sleep(local_delay) + local_tries -= 1 + local_delay *= backoff + return func(*args, **kwargs) + return function_to_retry + return decorated_function_with_retry diff --git a/setup.py b/setup.py index 2797dffd7161..1166f1ba820d 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,8 @@ packages = ['test_framework', 'test_framework.core', 'test_framework.plugins', - 'test_framework.fixtures'], + 'test_framework.fixtures', + 'common'], entry_points = { 'nose.plugins': [ 'base_plugin = test_framework.plugins.base_plugin:Base', diff --git a/test_framework/core/utils.py b/test_framework/core/utils.py deleted file mode 100755 index 5f42ca976ef6..000000000000 --- a/test_framework/core/utils.py +++ /dev/null @@ -1,40 +0,0 @@ -from functools import wraps -import logging - - -def retry_if_unsuccessful(max_tries=2): - """Decorator for a test method to make it automatically re-run on errors and failures. - - @max_tries: The maximum number of times the test will run before failing (default 2) - - This decorator will also run the test class's setUp/tearDown methods before/after - each attempt at running the decorated test method. - - Please only add this decorator to a test if you have already made a best effort - to fix flapping within the test, and have a good reason why more effort - won't fix the flapping. - """ - def _retry_if_unsuccessful(func): - @wraps(func) - def test_wrapper(self): - for x in range(0, max_tries): - # setUp is called automatically before the first run, and tearDown - # is called automatically after the last, so we need to skip calling - # setUp on the first iteration and tearDown on the last. - setup_successful = x == 0 - try: - if x > 0: - self.setUp() - setup_successful = True - func(self) - except Exception, e: - logging.exception('Exception running test: %s, retrying' % e) - if x < max_tries-1 and setup_successful: - self.tearDown() - if x + 1 >= max_tries: - raise - else: - return - return test_wrapper - - return _retry_if_unsuccessful From 836cb3cabe30fedbadde731519ad688b37f1770a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 20 Jul 2015 03:57:42 -0400 Subject: [PATCH 057/219] A slightly different way of handling the imports. --- README.md | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index bb96e5a34cc0..ed7fd3efb3ea 100755 --- a/README.md +++ b/README.md @@ -148,9 +148,9 @@ python **Step 5:** Now to verify the test framework installation by writing a simple Selenium script that performs basic actions such as navigating to a web page, clicking, waiting for page elements to appear, typing in text, scraping text on a page, and verifying text. (copy/paste this into a new file called "my_first_test.py"). This may be a good time to read up on css selectors. If you use Chrome, you can right-click on a page and select "Inspect Element" to see the details you need to create such a script. At a quick glance, dots are for class names and pound signs are for IDs. ```python -import test_framework +from test_framework import BaseCase -class MyTestClass(test_framework.BaseCase): +class MyTestClass(BaseCase): def test_basic(self): self.open("http://xkcd.com/1513/") @@ -247,9 +247,9 @@ nosetests [YOUR_TEST_FILE].py:[SOME_CLASS_NAME].test_[SOME_TEST_NAME] --config=[ Let's try an example of a test that fails. Copy the following into a file called fail_test.py: ```python """ test_fail.py """ -import test_framework +from test_framework import BaseCase -class MyTestClass(test_framework.BaseCase): +class MyTestClass(BaseCase): def test_find_google_on_bing(self): self.driver.get("http://bing.com") @@ -268,12 +268,6 @@ Have you made it this far? Congratulations!!! Now you're ready to dive in at ful ## Part II: Detailed Method Specifications, Examples -Important Note: Make sure you include the following import in your code to use the framework commands: - -```python -import test_framework -``` - #### Navigating to a Page, Plus Some Other Useful Related Commands ```python @@ -485,13 +479,13 @@ So by now you may be wondering how the nosetests code works? Nosetests will auto To use the test framework calls, don't forget to include the following import: ```python -import test_framework +from test_framework import BaseCase ``` -And you'll need to inherit the base case in your classes like so: +And you'll need to inherit BaseCase in your classes like so: ```python -class MyTestClass(test_framework.BaseCase): +class MyTestClass(BaseCase): ``` To understand the full scope of the test framework, we have to take a peek inside. From the top-level folder that contained the requirements.pip and setup.py files, there are two other major folders: "grid" and "test_framework". The Selenium "Grid" is what maintains the remote machines running selenium tests for "selenium.hubteam.com/jenkins". Machines can be spun up through Amazon EC2, and each one is capable of running 5 simultaneous browser tests. The other major folder, "test_framework", is what contains everything else. The "test_framework" folder contains all the major components such as "Core", "Fixtures", and "Plugins". For all intensive purposes, those sections are all equally important. They contain all the code and libraries that make our test framework useful (because otherwise we'd just be writing tests using raw selenium calls without any special add-ons or support). From f011e37b9174b97f5a1ce597a468dc4a65e9cf2d Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 20 Jul 2015 04:13:58 -0400 Subject: [PATCH 058/219] Moving the examples over to a new location. --- .../examples => examples}/__init__.py | 0 examples/my_first_test.py | 16 +++++++++++++ .../examples => examples}/run_first_test.bat | 0 .../examples => examples}/run_first_test.sh | 0 .../run_test_fail_with_debug.sh | 0 .../run_test_fail_with_logging.sh | 0 .../examples => examples}/test_fail.py | 0 test_framework/examples/my_first_test.py | 24 ------------------- test_framework/test/__init__.py | 0 9 files changed, 16 insertions(+), 24 deletions(-) rename {test_framework/examples => examples}/__init__.py (100%) create mode 100644 examples/my_first_test.py rename {test_framework/examples => examples}/run_first_test.bat (100%) mode change 100644 => 100755 rename {test_framework/examples => examples}/run_first_test.sh (100%) mode change 100644 => 100755 rename {test_framework/examples => examples}/run_test_fail_with_debug.sh (100%) mode change 100644 => 100755 rename {test_framework/examples => examples}/run_test_fail_with_logging.sh (100%) mode change 100644 => 100755 rename {test_framework/examples => examples}/test_fail.py (100%) delete mode 100644 test_framework/examples/my_first_test.py delete mode 100755 test_framework/test/__init__.py diff --git a/test_framework/examples/__init__.py b/examples/__init__.py similarity index 100% rename from test_framework/examples/__init__.py rename to examples/__init__.py diff --git a/examples/my_first_test.py b/examples/my_first_test.py new file mode 100644 index 000000000000..d4d8db0cd135 --- /dev/null +++ b/examples/my_first_test.py @@ -0,0 +1,16 @@ +from test_framework import BaseCase + +class MyTestClass(BaseCase): + + def test_basic(self): + self.open("http://xkcd.com/1513/") + self.click('a[href="http://blag.xkcd.com"]') + self.wait_for_text_visible("The blag of the webcomic", "#site-description") + self.update_text_value("input#s", "Robots!\n") + self.wait_for_text_visible("Hooray robots!", "#content") + self.open("http://xkcd.com/1481/") + self.wait_for_element_visible("div#comic") + self.click('a[rel="license"]') + text = self.wait_for_element_visible('center').text + self.assertTrue("reuse any of my drawings" in text) + self.assertTrue("You can use them freely" in text) diff --git a/test_framework/examples/run_first_test.bat b/examples/run_first_test.bat old mode 100644 new mode 100755 similarity index 100% rename from test_framework/examples/run_first_test.bat rename to examples/run_first_test.bat diff --git a/test_framework/examples/run_first_test.sh b/examples/run_first_test.sh old mode 100644 new mode 100755 similarity index 100% rename from test_framework/examples/run_first_test.sh rename to examples/run_first_test.sh diff --git a/test_framework/examples/run_test_fail_with_debug.sh b/examples/run_test_fail_with_debug.sh old mode 100644 new mode 100755 similarity index 100% rename from test_framework/examples/run_test_fail_with_debug.sh rename to examples/run_test_fail_with_debug.sh diff --git a/test_framework/examples/run_test_fail_with_logging.sh b/examples/run_test_fail_with_logging.sh old mode 100644 new mode 100755 similarity index 100% rename from test_framework/examples/run_test_fail_with_logging.sh rename to examples/run_test_fail_with_logging.sh diff --git a/test_framework/examples/test_fail.py b/examples/test_fail.py similarity index 100% rename from test_framework/examples/test_fail.py rename to examples/test_fail.py diff --git a/test_framework/examples/my_first_test.py b/test_framework/examples/my_first_test.py deleted file mode 100644 index 4749dfb33aaf..000000000000 --- a/test_framework/examples/my_first_test.py +++ /dev/null @@ -1,24 +0,0 @@ -from test_framework.fixtures import base_case - -class MyTestClass(base_case.BaseCase): - - def test_basic(self): - self.driver.get("http://www.wikipedia.org/") - self.wait_for_element_visible("a[href='//en.wikipedia.org/']", timeout=5).click() - self.wait_for_element_visible("div#simpleSearch", timeout=5) - self.wait_for_element_visible("input[name='search']", timeout=5) - self.update_text_value("input[name='search']", "Boston\n") - text = self.wait_for_element_visible("div#mw-content-text", timeout=5).text - self.assertTrue("The Charles River separates Boston from " in text) - self.wait_for_element_visible("a[title='Find out about Wikipedia']").click() - self.wait_for_text_visible("Since its creation in 2001", "div#mw-content-text", timeout=5) - - self.driver.get("http://www.wikimedia.org/") - self.wait_for_element_visible('img[alt="Wikivoyage"]', timeout=5).click() - self.wait_for_element_visible("a[href='//en.wikivoyage.org/']", timeout=5).click() - self.wait_for_element_visible('a[title="Visit the main page"]', timeout=5) - self.wait_for_element_visible('input#searchInput', timeout=5) - self.update_text_value("input#searchInput", "Israel\n") - self.wait_for_element_visible("div#contentSub", timeout=5) - text = self.wait_for_element_visible("div#mw-content-text", timeout=5).text - self.assertTrue("The state of Israel" in text) diff --git a/test_framework/test/__init__.py b/test_framework/test/__init__.py deleted file mode 100755 index e69de29bb2d1..000000000000 From f74fa33733d14236512e163a4832fae66b59d7aa Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 20 Jul 2015 04:23:24 -0400 Subject: [PATCH 059/219] Set logging level to INFO for the examples. --- examples/run_first_test.bat | 2 +- examples/run_first_test.sh | 2 +- examples/run_test_fail_with_debug.sh | 2 +- examples/run_test_fail_with_logging.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/run_first_test.bat b/examples/run_first_test.bat index 61c70a109d09..dfd41c2c6585 100755 --- a/examples/run_first_test.bat +++ b/examples/run_first_test.bat @@ -1 +1 @@ -nosetests my_first_test.py --browser=chrome --with-selenium -s \ No newline at end of file +nosetests my_first_test.py --browser=chrome --with-selenium --logging-level=INFO -s \ No newline at end of file diff --git a/examples/run_first_test.sh b/examples/run_first_test.sh index 61c70a109d09..dfd41c2c6585 100755 --- a/examples/run_first_test.sh +++ b/examples/run_first_test.sh @@ -1 +1 @@ -nosetests my_first_test.py --browser=chrome --with-selenium -s \ No newline at end of file +nosetests my_first_test.py --browser=chrome --with-selenium --logging-level=INFO -s \ No newline at end of file diff --git a/examples/run_test_fail_with_debug.sh b/examples/run_test_fail_with_debug.sh index f2d5f87835a1..56d24f62e1a4 100755 --- a/examples/run_test_fail_with_debug.sh +++ b/examples/run_test_fail_with_debug.sh @@ -1 +1 @@ -nosetests test_fail.py --browser=chrome --with-selenium --pdb --pdb-failures -s \ No newline at end of file +nosetests test_fail.py --browser=chrome --with-selenium --logging-level=INFO --pdb --pdb-failures -s \ No newline at end of file diff --git a/examples/run_test_fail_with_logging.sh b/examples/run_test_fail_with_logging.sh index eb0761e7d99c..f92a81b9d766 100755 --- a/examples/run_test_fail_with_logging.sh +++ b/examples/run_test_fail_with_logging.sh @@ -1 +1 @@ -nosetests test_fail.py --browser=chrome --with-selenium --with-testing_base --with-basic_test_info --with-page_source --with-screen_shots -s \ No newline at end of file +nosetests test_fail.py --browser=chrome --with-selenium --logging-level=INFO --with-testing_base --with-basic_test_info --with-page_source --with-screen_shots -s \ No newline at end of file From e6ed50d0d15c9b05295d10efd8766727a6e8c887 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 20 Jul 2015 05:03:45 -0400 Subject: [PATCH 060/219] Screenshot naming --- test_framework/plugins/screen_shots.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_framework/plugins/screen_shots.py b/test_framework/plugins/screen_shots.py index 251d1b885ed7..dd3a0d001d9a 100755 --- a/test_framework/plugins/screen_shots.py +++ b/test_framework/plugins/screen_shots.py @@ -17,7 +17,7 @@ class ScreenShots(Plugin): name = "screen_shots" logfile_name = "screenshot.jpg" # Browser windows aren't always maximized. This may display more details. - logfile_name_2 = "screenshot_fullscreen.jpg" + logfile_name_2 = "full_screenshot.jpg" def options(self, parser, env): super(ScreenShots, self).options(parser, env=env) @@ -38,11 +38,11 @@ def add_screenshot(self, test, err, capt=None, tbinfo=None): test.driver.get_screenshot_as_file(screenshot_file) try: test.driver.maximize_window() + time.sleep(0.2) # Make sure the screen is ready except Exception: pass screen_b64 = test.driver.get_screenshot_as_base64() screen = base64.decodestring(screen_b64) - time.sleep(0.3) screenshot_file_2 = "%s/%s" % (test_logpath, self.logfile_name_2) f1 = open(screenshot_file_2, 'w+') f1.write(screen) From 15e71bd26c06eb1bcb0a9d07b56b97b6da95ab6e Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 20 Jul 2015 05:04:10 -0400 Subject: [PATCH 061/219] More for .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b8a54f7540b5..bd0694f4ba76 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ *.egg-info dist build +logs +archived_logs eggs parts bin @@ -18,6 +20,7 @@ __pycache__ # Installer logs pip-log.txt +.swp # Unit test / coverage reports .coverage From 6bfb1a37a45da14cfe4ab579f0807a25ffc1135a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 20 Jul 2015 05:08:14 -0400 Subject: [PATCH 062/219] Upping the version number. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1166f1ba820d..054291bf59ec 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name = 'seleniumspot', - version = '1.1.0', + version = '1.1.1', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', From 8b3404bb1ec485c0109ed30a713683bf9f2c0b02 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 20 Jul 2015 15:20:59 -0400 Subject: [PATCH 063/219] Upping the requirements. --- requirements.pip | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.pip b/requirements.pip index a1c2e6daf57e..485b3186fa61 100755 --- a/requirements.pip +++ b/requirements.pip @@ -1,10 +1,10 @@ python==2.7.6 -selenium==2.46.0 -nose==1.3.0 +selenium==2.46.1 +nose==1.3.7 requests==2.7.0 urllib3==1.10.4 BeautifulSoup==3.2.1 -unittest2==1.0.1 +unittest2==1.1.0 chardet==2.3.0 simplejson==3.7.3 boto==2.38.0 From bb2db760c66704e6fad76149daf8431d4ef1e6b8 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 20 Jul 2015 19:14:12 -0400 Subject: [PATCH 064/219] Use the newer version of Selenium: 2.46.1 --- grid_files/ReadMe.txt | 5 ++--- grid_files/start-selenium-node.bat | 2 +- grid_files/start-selenium-server.sh | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/grid_files/ReadMe.txt b/grid_files/ReadMe.txt index f5ad1794de76..9b2f55aefcab 100644 --- a/grid_files/ReadMe.txt +++ b/grid_files/ReadMe.txt @@ -1,5 +1,4 @@ -You may need to download selenium-server-standalone-2.46.0.jar (or the latest version) separately. +You may need to download selenium-server-standalone-2.46.1.jar (or the latest version) separately. That file is not present with this repository to save space. -See: https://pypi.python.org/pypi/selenium/2.46.0 -You can download that file directly from here: http://selenium-release.storage.googleapis.com/2.46/selenium-server-standalone-2.46.0.jar +You can download that file from here: https://pypi.python.org/pypi/selenium/2.46.1 Once you have downloaded the jar file, put it in this folder. diff --git a/grid_files/start-selenium-node.bat b/grid_files/start-selenium-node.bat index 6896a06f22ca..9121587a1b8d 100644 --- a/grid_files/start-selenium-node.bat +++ b/grid_files/start-selenium-node.bat @@ -1,2 +1,2 @@ cd c:\ -java -jar selenium-server-standalone-2.46.0.jar -port 5555 -host [ENTER HOST OF THIS WORKER MACHINE ex: ec2-***-**-**-***.compute-1.amazonaws.com] -browser browserName=chrome,maxInstances=5 \ No newline at end of file +java -jar selenium-server-standalone-2.46.1.jar -port 5555 -host [ENTER HOST OF THIS WORKER MACHINE ex: ec2-***-**-**-***.compute-1.amazonaws.com] -browser browserName=chrome,maxInstances=5 \ No newline at end of file diff --git a/grid_files/start-selenium-server.sh b/grid_files/start-selenium-server.sh index 490335d26c3d..86821efd2139 100755 --- a/grid_files/start-selenium-server.sh +++ b/grid_files/start-selenium-server.sh @@ -1,2 +1,2 @@ #!/bin/bash -screen -S grid-server java -jar selenium-server-standalone-2.40.0.jar -role hub -host [ENTER YOUR MASTER SELENIUM SERVER HOST HERE] -port 4444 \ No newline at end of file +screen -S grid-server java -jar selenium-server-standalone-2.46.1.jar -role hub -host [ENTER YOUR MASTER SELENIUM SERVER HOST HERE] -port 4444 \ No newline at end of file From 935de6f20f4c52b33ebb483f08df3da2ed2d1753 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 21 Jul 2015 18:49:18 -0400 Subject: [PATCH 065/219] Precision waiting. --- test_framework/fixtures/page_loads.py | 49 ++++++++++++--------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/test_framework/fixtures/page_loads.py b/test_framework/fixtures/page_loads.py index ca66c62dddf7..5b3a7a91ad6f 100755 --- a/test_framework/fixtures/page_loads.py +++ b/test_framework/fixtures/page_loads.py @@ -25,8 +25,7 @@ UnexpectedAlertPresentException -def wait_for_element_present(driver, selector, - by=By.CSS_SELECTOR, timeout=30): +def wait_for_element_present(driver, selector, by=By.CSS_SELECTOR, timeout=30): """ Searches for the specified element by the given selector. Returns the element object if the element is present on the page. The element can be @@ -43,19 +42,18 @@ def wait_for_element_present(driver, selector, """ element = None - for x in range(timeout): + for x in range(timeout * 10): try: element = driver.find_element(by=by, value=selector) return element except Exception: - time.sleep(1) + time.sleep(0.1) if not element: raise NoSuchElementException("Element %s was not present in %s seconds!" % (selector, timeout)) -def wait_for_element_visible(driver, selector, - by=By.CSS_SELECTOR, timeout=30): +def wait_for_element_visible(driver, selector, by=By.CSS_SELECTOR, timeout=30): """ Searches for the specified element by the given selector. Returns the element object if the element is present and visible on the page. @@ -72,23 +70,21 @@ def wait_for_element_visible(driver, selector, """ element = None - for x in range(timeout): + for x in range(timeout * 10): try: element = driver.find_element(by=by, value=selector) if element.is_displayed(): return element else: - element = None - time.sleep(1) + raise Exception() except Exception: - time.sleep(1) + time.sleep(0.1) if not element: raise ElementNotVisibleException("Element %s was not visible in %s seconds!"\ % (selector, timeout)) -def wait_for_text_visible(driver, text, selector, - by=By.CSS_SELECTOR, timeout=30): +def wait_for_text_visible(driver, text, selector, by=By.CSS_SELECTOR, timeout=30): """ Searches for the specified element by the given selector. Returns the element object if the text is present in the element and visible @@ -106,24 +102,22 @@ def wait_for_text_visible(driver, text, selector, """ element = None - for x in range(timeout): + for x in range(timeout * 10): try: element = driver.find_element(by=by, value=selector) if element.is_displayed(): if text in element.text: return element else: - element = None - time.sleep(1) + raise Exception() except Exception: - time.sleep(1) + time.sleep(0.1) if not element: - raise ElementNotVisibleException("Expected text for element %s was not visible in %s seconds!"\ - % (selector, timeout)) + raise ElementNotVisibleException("Expected text [%s] for element %s was not visible in %s seconds!"\ + % (text, selector, timeout)) -def wait_for_element_absent(driver, selector, - by=By.CSS_SELECTOR, timeout=30): +def wait_for_element_absent(driver, selector, by=By.CSS_SELECTOR, timeout=30): """ Searches for the specified element by the given selector. Returns void when element is no longer present on the page. Raises an exception if the @@ -135,18 +129,17 @@ def wait_for_element_absent(driver, selector, timeout - the time to wait for the element in seconds (Default- 30 seconds) """ - for x in range(timeout + 1): + for x in range(timeout * 10): try: driver.find_element(by=by, value=selector) - time.sleep(1) + time.sleep(0.1) except Exception: return raise Exception("Element %s was still present after %s seconds!" % (selector, timeout)) -def wait_for_element_not_visible(driver, selector, - by=By.CSS_SELECTOR, timeout=30): +def wait_for_element_not_visible(driver, selector, by=By.CSS_SELECTOR, timeout=30): """ Searches for the specified element by the given selector. Returns void when element is no longer visible on the page (or if the element is not @@ -159,11 +152,11 @@ def wait_for_element_not_visible(driver, selector, timeout - the time to wait for the element in seconds (Default - 30 seconds) """ - for x in range(timeout + 1): + for x in range(timeout * 10): try: element = driver.find_element(by=by, value=selector) if element.is_displayed(): - time.sleep(1) + time.sleep(0.1) else: return except Exception: @@ -182,7 +175,7 @@ def wait_for_and_switch_to_alert(driver, timeout=30): timeout - the time to wait for the alert in seconds (Default - 30 seconds) """ - for x in range(timeout + 1): + for x in range(timeout * 10): try: driver.switch_to_alert() alert = driver.switch_to_alert() @@ -190,7 +183,7 @@ def wait_for_and_switch_to_alert(driver, timeout=30): return alert except NoAlertPresentException: try: - time.sleep(1) + time.sleep(0.1) except UnexpectedAlertPresentException: pass From 3aa5e219146ed3594dd1345d112034d7066a0bc5 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 21 Jul 2015 19:03:29 -0400 Subject: [PATCH 066/219] Add open_url() as an alternative to open() --- test_framework/fixtures/base_case.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py index 3e065ea6a4a3..62e8f0320498 100755 --- a/test_framework/fixtures/base_case.py +++ b/test_framework/fixtures/base_case.py @@ -59,6 +59,11 @@ def open(self, url): self.driver.get(url) + def open_url(self, url): + """ In case people are mixing up self.open() with open() """ + self.driver.get(url) + + def activate_jquery(self): """ (It's not on by default on all website pages.) """ self.driver.execute_script('var script = document.createElement("script"); script.src = "https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"; document.getElementsByTagName("head")[0].appendChild(script);') From 6b2068aafd783e1cb58baecbcd9874dd4b32f76a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 22 Jul 2015 21:24:12 -0400 Subject: [PATCH 067/219] Moving "common". Adding "config". Updating "setup.py" --- setup.py | 3 ++- {common => test_framework/common}/__init__.py | 0 {common => test_framework/common}/decorators.py | 0 test_framework/config/__init__.py | 0 test_framework/config/settings.py | 7 +++++++ 5 files changed, 9 insertions(+), 1 deletion(-) rename {common => test_framework/common}/__init__.py (100%) rename {common => test_framework/common}/decorators.py (100%) create mode 100755 test_framework/config/__init__.py create mode 100755 test_framework/config/settings.py diff --git a/setup.py b/setup.py index 054291bf59ec..f9465eccf975 100755 --- a/setup.py +++ b/setup.py @@ -16,7 +16,8 @@ 'test_framework.core', 'test_framework.plugins', 'test_framework.fixtures', - 'common'], + 'test_framework.common', + 'test_framework.config'], entry_points = { 'nose.plugins': [ 'base_plugin = test_framework.plugins.base_plugin:Base', diff --git a/common/__init__.py b/test_framework/common/__init__.py similarity index 100% rename from common/__init__.py rename to test_framework/common/__init__.py diff --git a/common/decorators.py b/test_framework/common/decorators.py similarity index 100% rename from common/decorators.py rename to test_framework/common/decorators.py diff --git a/test_framework/config/__init__.py b/test_framework/config/__init__.py new file mode 100755 index 000000000000..e69de29bb2d1 diff --git a/test_framework/config/settings.py b/test_framework/config/settings.py new file mode 100755 index 000000000000..221baeefeb32 --- /dev/null +++ b/test_framework/config/settings.py @@ -0,0 +1,7 @@ +""" +You'll probably want to customize this to your enviroment and needs. +""" + +# Default time to wait for page elements to appear before performing actions +SMALL_TIMEOUT = 7 +LARGE_TIMEOUT = 14 From 895952561a7a7e71477543f109ba1f564dcb52d6 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 22 Jul 2015 21:25:05 -0400 Subject: [PATCH 068/219] Using the new config file for timeouts. --- test_framework/fixtures/base_case.py | 19 ++++++++------- test_framework/fixtures/page_interactions.py | 7 +++--- test_framework/fixtures/page_loads.py | 25 ++++++++++---------- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py index 62e8f0320498..8d6a854e2d6c 100755 --- a/test_framework/fixtures/base_case.py +++ b/test_framework/fixtures/base_case.py @@ -2,6 +2,7 @@ import time import logging import unittest +from test_framework.config import settings from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.common.by import By import page_loads, page_interactions @@ -30,7 +31,7 @@ def hover_on_element(self, selector): return page_interactions.hover_on_element(self.driver, selector) - def hover_and_click(self, hover_selector, click_selector, click_by=By.CSS_SELECTOR, timeout=7): + def hover_and_click(self, hover_selector, click_selector, click_by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): return page_interactions.hover_and_click(self.driver, hover_selector, click_selector, click_by, timeout) @@ -50,7 +51,7 @@ def jquery_click(self, selector): return self.driver.execute_script("jQuery('%s').click()" % selector) - def click(self, selector, by=By.CSS_SELECTOR, timeout=7): + def click(self, selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): element = page_loads.wait_for_element_visible(self.driver, selector, by, timeout=timeout) return element.click() @@ -90,7 +91,7 @@ def set_value(self, selector, value): return self.driver.execute_script("jQuery('%s').val(%s)" % (selector, val)) - def update_text_value(self, selector, new_value, timeout=7, retry=False): + def update_text_value(self, selector, new_value, timeout=settings.SMALL_TIMEOUT, retry=False): """ This method updates a selector's text value with a new value @Params selector - the selector with the value to change @@ -110,25 +111,25 @@ def update_text_value(self, selector, new_value, timeout=7, retry=False): self.set_value(selector, new_value) - def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, timeout=14): + def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): return page_loads.wait_for_element_present(self.driver, selector, by, timeout) - def wait_for_element_visible(self, selector, by=By.CSS_SELECTOR, timeout=14): + def wait_for_element_visible(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): return page_loads.wait_for_element_visible(self.driver, selector, by, timeout) - def wait_for_text_visible(self, text, selector, by=By.CSS_SELECTOR, timeout=14): + def wait_for_text_visible(self, text, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): return page_loads.wait_for_text_visible(self.driver, text, selector, by, timeout) - def wait_for_element_absent(self, selector, by=By.CSS_SELECTOR, timeout=14): + def wait_for_element_absent(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): return page_loads.wait_for_element_absent(self.driver, selector, by, timeout) - def wait_for_element_not_visible(self, selector, by=By.CSS_SELECTOR, timeout=14): + def wait_for_element_not_visible(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): return page_loads.wait_for_element_not_visible(self.driver, selector, by, timeout) - def wait_for_and_switch_to_alert(self, timeout=14): + def wait_for_and_switch_to_alert(self, timeout=settings.LARGE_TIMEOUT): return page_loads.wait_for_and_switch_to_alert(self.driver, timeout) diff --git a/test_framework/fixtures/page_interactions.py b/test_framework/fixtures/page_interactions.py index c67e63448187..75120ab896c9 100755 --- a/test_framework/fixtures/page_interactions.py +++ b/test_framework/fixtures/page_interactions.py @@ -18,6 +18,7 @@ """ import time +from test_framework.config import settings from selenium.webdriver.common.by import By from selenium.webdriver.remote.errorhandler import ElementNotVisibleException from selenium.webdriver.remote.errorhandler import NoSuchElementException @@ -123,7 +124,7 @@ def hover_on_element(driver, selector): def hover_and_click(driver, hover_selector, click_selector, - click_by=By.CSS_SELECTOR, timeout=5): + click_by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): """ Fires the hover event for a specified element by a given selector, then clicks on another element specified. Useful for dropdown hover based menus. @@ -135,12 +136,12 @@ def hover_and_click(driver, hover_selector, click_selector, timeout - number of seconds to wait between hover and click (Default- 5 seconds) """ driver.execute_script("jQuery('%s').mouseover()" % (hover_selector)) - for x in range(timeout): + for x in range(timeout * 10): try: driver.find_element(by=click_by, value="%s" % click_selector).click() return except Exception: - time.sleep(1) + time.sleep(0.1) raise NoSuchElementException("Element %s was not present in %s" % (click_selector, timeout)) diff --git a/test_framework/fixtures/page_loads.py b/test_framework/fixtures/page_loads.py index 5b3a7a91ad6f..0f2fb2289d07 100755 --- a/test_framework/fixtures/page_loads.py +++ b/test_framework/fixtures/page_loads.py @@ -18,6 +18,7 @@ """ import time +from test_framework.config import settings from selenium.webdriver.common.by import By from selenium.webdriver.remote.errorhandler import ElementNotVisibleException, \ NoSuchElementException, \ @@ -25,7 +26,7 @@ UnexpectedAlertPresentException -def wait_for_element_present(driver, selector, by=By.CSS_SELECTOR, timeout=30): +def wait_for_element_present(driver, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): """ Searches for the specified element by the given selector. Returns the element object if the element is present on the page. The element can be @@ -35,7 +36,7 @@ def wait_for_element_present(driver, selector, by=By.CSS_SELECTOR, timeout=30): driver - the webdriver object selector - the locator that is used (required) by - the method to search for hte locator (Default- By.CSS_SELECTOR) - timeout - the time to wait for the element in seconds (Default- 30 seconds) + timeout - the time to wait for elements in seconds @returns A web element object @@ -53,7 +54,7 @@ def wait_for_element_present(driver, selector, by=By.CSS_SELECTOR, timeout=30): (selector, timeout)) -def wait_for_element_visible(driver, selector, by=By.CSS_SELECTOR, timeout=30): +def wait_for_element_visible(driver, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): """ Searches for the specified element by the given selector. Returns the element object if the element is present and visible on the page. @@ -63,7 +64,7 @@ def wait_for_element_visible(driver, selector, by=By.CSS_SELECTOR, timeout=30): driver - the webdriver object (required) selector - the locator that is used (required) by - the method to search for hte locator (Default- By.CSS_SELECTOR) - timeout - the time to wait for the element in seconds (Default- 30 seconds) + timeout - the time to wait for elements in seconds @returns A web element object @@ -84,7 +85,7 @@ def wait_for_element_visible(driver, selector, by=By.CSS_SELECTOR, timeout=30): % (selector, timeout)) -def wait_for_text_visible(driver, text, selector, by=By.CSS_SELECTOR, timeout=30): +def wait_for_text_visible(driver, text, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): """ Searches for the specified element by the given selector. Returns the element object if the text is present in the element and visible @@ -95,7 +96,7 @@ def wait_for_text_visible(driver, text, selector, by=By.CSS_SELECTOR, timeout=30 text - the text that is being searched for in the element (required) selector - the locator that is used (required) by - the method to search for hte locator (Default- By.CSS_SELECTOR) - timeout - the time to wait for the element in seconds (Default- 30 seconds) + timeout - the time to wait for elements in seconds @returns A web element object that contains the text searched for @@ -117,7 +118,7 @@ def wait_for_text_visible(driver, text, selector, by=By.CSS_SELECTOR, timeout=30 % (text, selector, timeout)) -def wait_for_element_absent(driver, selector, by=By.CSS_SELECTOR, timeout=30): +def wait_for_element_absent(driver, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): """ Searches for the specified element by the given selector. Returns void when element is no longer present on the page. Raises an exception if the @@ -126,7 +127,7 @@ def wait_for_element_absent(driver, selector, by=By.CSS_SELECTOR, timeout=30): driver - the webdriver object selector - the locator that is used (required) by - the method to search for hte locator (Default- By.CSS_SELECTOR) - timeout - the time to wait for the element in seconds (Default- 30 seconds) + timeout - the time to wait for elements in seconds """ for x in range(timeout * 10): @@ -139,7 +140,7 @@ def wait_for_element_absent(driver, selector, by=By.CSS_SELECTOR, timeout=30): (selector, timeout)) -def wait_for_element_not_visible(driver, selector, by=By.CSS_SELECTOR, timeout=30): +def wait_for_element_not_visible(driver, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): """ Searches for the specified element by the given selector. Returns void when element is no longer visible on the page (or if the element is not @@ -149,7 +150,7 @@ def wait_for_element_not_visible(driver, selector, by=By.CSS_SELECTOR, timeout=3 driver - the webdriver object (required) selector - the locator that is used (required) by - the method to search for hte locator (Default- By.CSS_SELECTOR) - timeout - the time to wait for the element in seconds (Default - 30 seconds) + timeout - the time to wait for elements in seconds """ for x in range(timeout * 10): @@ -165,14 +166,14 @@ def wait_for_element_not_visible(driver, selector, by=By.CSS_SELECTOR, timeout=3 % (selector, timeout)) -def wait_for_and_switch_to_alert(driver, timeout=30): +def wait_for_and_switch_to_alert(driver, timeout=settings.LARGE_TIMEOUT): """ Wait for a browser alert to appear, and switch to it. This should be usable as a drop-in replacement for driver.switch_to_alert() when the alert box may not exist yet. @params driver - the webdriver object (required) - timeout - the time to wait for the alert in seconds (Default - 30 seconds) + timeout - the time to wait for the alert in seconds """ for x in range(timeout * 10): From 2910c02dc306d8bd51e94bdc3b3e9021f88d4402 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 22 Jul 2015 21:28:34 -0400 Subject: [PATCH 069/219] Running an example test with a config file. --- examples/example_config.cfg | 9 +++++++++ examples/run_with_config.sh | 1 + 2 files changed, 10 insertions(+) create mode 100644 examples/example_config.cfg create mode 100755 examples/run_with_config.sh diff --git a/examples/example_config.cfg b/examples/example_config.cfg new file mode 100644 index 000000000000..f30d9032b3e6 --- /dev/null +++ b/examples/example_config.cfg @@ -0,0 +1,9 @@ +[nosetests] +with-selenium=1 +with-testing_base=1 +with-page_source=1 +with-screen_shots=1 +with-basic_test_info=1 +nocapture=1 +logging-level=INFO +browser=chrome diff --git a/examples/run_with_config.sh b/examples/run_with_config.sh new file mode 100755 index 000000000000..259f6f6798c6 --- /dev/null +++ b/examples/run_with_config.sh @@ -0,0 +1 @@ +nosetests my_first_test.py --config=example_config.cfg \ No newline at end of file From 81435711235061d819ee0c4bd23f8d22ad7d5d31 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 22 Jul 2015 21:42:56 -0400 Subject: [PATCH 070/219] Moving Amazon S3 credentials to a separate config file. --- test_framework/config/settings.py | 6 ++++++ test_framework/core/s3_manager.py | 9 +++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/test_framework/config/settings.py b/test_framework/config/settings.py index 221baeefeb32..1173b0bc4baa 100755 --- a/test_framework/config/settings.py +++ b/test_framework/config/settings.py @@ -5,3 +5,9 @@ # Default time to wait for page elements to appear before performing actions SMALL_TIMEOUT = 7 LARGE_TIMEOUT = 14 + +# Amazon S3 Bucket Credentials (where screenshots and other log files get saved) +LOG_BUCKET = "[ENTER LOG BUCKET FOLDER NAME HERE]" +BUCKET_URL = "http://[ENTER SUBDOMAIN OF AMAZON BUCKET URL HERE].s3-[ENTER S3 REGION HERE].amazonaws.com/" +SELENIUM_ACCESS_KEY = "[ENTER YOUR S3 ACCESS KEY FOR SELENIUM HERE]" +SELENIUM_SECRET_KEY = "[ENTER YOUR S3 SECRET KEY FOR SELENIUM HERE]" diff --git a/test_framework/core/s3_manager.py b/test_framework/core/s3_manager.py index 23e7e2e19878..c9a7988e663f 100755 --- a/test_framework/core/s3_manager.py +++ b/test_framework/core/s3_manager.py @@ -3,6 +3,7 @@ """ from boto.s3.connection import S3Connection from boto.s3.key import Key +from test_framework.config import settings already_uploaded_files = [] @@ -13,10 +14,10 @@ class S3LoggingBucket(object): """ def __init__(self, - log_bucket = "[ENTER LOG BUCKET FOLDER NAME HERE]", - bucket_url = "http://[ENTER SUBDOMAIN OF AMAZON BUCKET URL HERE].s3-[ENTER S3 REGION HERE].amazonaws.com/", - selenium_access_key = "[ENTER YOUR S3 ACCESS KEY FOR SELENIUM HERE]", - selenium_secret_key = "[ENTER YOUR S3 SECRET KEY FOR SELENIUM HERE]"): + log_bucket = settings.LOG_BUCKET, + bucket_url = settings.BUCKET_URL, + selenium_access_key = settings.SELENIUM_ACCESS_KEY, + selenium_secret_key = settings.SELENIUM_SECRET_KEY): self.conn = S3Connection(selenium_access_key, selenium_secret_key) From e71dc7866fe94e73a1e3fdb33a3df399cad58980 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 22 Jul 2015 21:54:55 -0400 Subject: [PATCH 071/219] Move the MySQL DB credentials to a separate config file. --- test_framework/config/settings.py | 6 ++++++ test_framework/core/mysql_conf.py | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/test_framework/config/settings.py b/test_framework/config/settings.py index 1173b0bc4baa..2d5330f51005 100755 --- a/test_framework/config/settings.py +++ b/test_framework/config/settings.py @@ -11,3 +11,9 @@ BUCKET_URL = "http://[ENTER SUBDOMAIN OF AMAZON BUCKET URL HERE].s3-[ENTER S3 REGION HERE].amazonaws.com/" SELENIUM_ACCESS_KEY = "[ENTER YOUR S3 ACCESS KEY FOR SELENIUM HERE]" SELENIUM_SECRET_KEY = "[ENTER YOUR S3 SECRET KEY FOR SELENIUM HERE]" + +# MySQL DB Credentials (where data from tests gets saved) +DB_HOST = "[TEST DB HOST]" +DB_USERNAME = "[TEST DB USERNAME]" +DB_PASSWORD = "[TEST DB PASSWORD]" +DB_SCHEMA = "[TEST DB SCHEMA]" diff --git a/test_framework/core/mysql_conf.py b/test_framework/core/mysql_conf.py index 9714ba285745..041bf28f7398 100755 --- a/test_framework/core/mysql_conf.py +++ b/test_framework/core/mysql_conf.py @@ -2,6 +2,8 @@ This file contains database credentials for the various databases the tests need to access """ +from test_framework.config import settings + # Environments TEST = "test" @@ -11,8 +13,7 @@ class Apps: APP_CREDS = { Apps.TESTCASE_REPOSITORY: { - TEST: ("[TEST DB HOST]", - "[TEST DB USERNAME]", "[TEST DB PASSWORD]", "[TEST DB SCHEMA]") + TEST: (settings.DB_HOST, settings.DB_USERNAME, settings.DB_PASSWORD, settings.DB_SCHEMA) }, } From 7d86e612b49c12daf9b41ecb06d6990e06d6ef8a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 22 Jul 2015 21:57:48 -0400 Subject: [PATCH 072/219] Updating the naming of Amazon S3 credentials. --- test_framework/config/settings.py | 8 ++++---- test_framework/core/s3_manager.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test_framework/config/settings.py b/test_framework/config/settings.py index 2d5330f51005..8e7efc8d637c 100755 --- a/test_framework/config/settings.py +++ b/test_framework/config/settings.py @@ -7,10 +7,10 @@ LARGE_TIMEOUT = 14 # Amazon S3 Bucket Credentials (where screenshots and other log files get saved) -LOG_BUCKET = "[ENTER LOG BUCKET FOLDER NAME HERE]" -BUCKET_URL = "http://[ENTER SUBDOMAIN OF AMAZON BUCKET URL HERE].s3-[ENTER S3 REGION HERE].amazonaws.com/" -SELENIUM_ACCESS_KEY = "[ENTER YOUR S3 ACCESS KEY FOR SELENIUM HERE]" -SELENIUM_SECRET_KEY = "[ENTER YOUR S3 SECRET KEY FOR SELENIUM HERE]" +S3_LOG_BUCKET = "[ENTER LOG BUCKET FOLDER NAME HERE]" +S3_BUCKET_URL = "http://[ENTER SUBDOMAIN OF AMAZON BUCKET URL HERE].s3-[ENTER S3 REGION HERE].amazonaws.com/" +S3_SELENIUM_ACCESS_KEY = "[ENTER YOUR S3 ACCESS KEY FOR SELENIUM HERE]" +S3_SELENIUM_SECRET_KEY = "[ENTER YOUR S3 SECRET KEY FOR SELENIUM HERE]" # MySQL DB Credentials (where data from tests gets saved) DB_HOST = "[TEST DB HOST]" diff --git a/test_framework/core/s3_manager.py b/test_framework/core/s3_manager.py index c9a7988e663f..2579d7380154 100755 --- a/test_framework/core/s3_manager.py +++ b/test_framework/core/s3_manager.py @@ -14,10 +14,10 @@ class S3LoggingBucket(object): """ def __init__(self, - log_bucket = settings.LOG_BUCKET, - bucket_url = settings.BUCKET_URL, - selenium_access_key = settings.SELENIUM_ACCESS_KEY, - selenium_secret_key = settings.SELENIUM_SECRET_KEY): + log_bucket = settings.S3_LOG_BUCKET, + bucket_url = settings.S3_BUCKET_URL, + selenium_access_key = settings.S3_SELENIUM_ACCESS_KEY, + selenium_secret_key = settings.S3_SELENIUM_SECRET_KEY): self.conn = S3Connection(selenium_access_key, selenium_secret_key) From f4a504fb08358744efed0340cfe1c332fc3e3dde Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 22 Jul 2015 22:29:19 -0400 Subject: [PATCH 073/219] Adding a rate-limiting decorator. --- test_framework/common/decorators.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test_framework/common/decorators.py b/test_framework/common/decorators.py index 6792b6eaf6b7..1f316d89f3ff 100755 --- a/test_framework/common/decorators.py +++ b/test_framework/common/decorators.py @@ -1,5 +1,7 @@ import logging import math +import requests +import threading import time from functools import wraps @@ -41,3 +43,25 @@ def function_to_retry(*args, **kwargs): return func(*args, **kwargs) return function_to_retry return decorated_function_with_retry + + +def rate_limited(max_per_second): + min_interval = 1.0 / float(max_per_second) + + def decorate(func): + last_time_called = [0.0] + rate_lock = threading.Lock() # To support multi-threading + + def rate_limited_function(*args, **kargs): + try: + rate_lock.acquire(True) + elapsed = time.clock() - last_time_called[0] + wait_time_remaining = min_interval - elapsed + if wait_time_remaining > 0: + time.sleep(wait_time_remaining) + last_time_called[0] = time.clock() + finally: + rate_lock.release() + return func(*args, **kargs) + return rate_limited_function + return decorate From c35c7ad6fd19360c614de6f24c88b8f946654266 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 22 Jul 2015 22:31:14 -0400 Subject: [PATCH 074/219] Adding a rate-limited decorator example. --- examples/decorator_test.py | 13 +++++++++++++ examples/run_rate_limiting_test.sh | 1 + 2 files changed, 14 insertions(+) create mode 100644 examples/decorator_test.py create mode 100755 examples/run_rate_limiting_test.sh diff --git a/examples/decorator_test.py b/examples/decorator_test.py new file mode 100644 index 000000000000..bd0a9db728d1 --- /dev/null +++ b/examples/decorator_test.py @@ -0,0 +1,13 @@ +from test_framework import BaseCase +from test_framework.common import decorators + + +class MyTestClass(BaseCase): + + @decorators.rate_limited(4) + def print_item(self, item): + print item + + def test_rate_limited_printing(self): + for item in xrange(10): + self.print_item(item) diff --git a/examples/run_rate_limiting_test.sh b/examples/run_rate_limiting_test.sh new file mode 100755 index 000000000000..323f3224580e --- /dev/null +++ b/examples/run_rate_limiting_test.sh @@ -0,0 +1 @@ +nosetests decorator_test.py -s \ No newline at end of file From 22573be1aa6e51bdb1bc0fabb87d1e51df7b44f0 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 23 Jul 2015 10:33:06 -0400 Subject: [PATCH 075/219] Revamp the "examples" folder. --- examples/ReadMe.txt | 9 +++++++++ examples/{decorator_test.py => rate_limiting_test.py} | 0 examples/{run_first_test.bat => run_my_first_test.bat} | 0 examples/{run_first_test.sh => run_my_first_test.sh} | 0 ...n_with_config.sh => run_my_first_test_with_config.sh} | 0 5 files changed, 9 insertions(+) create mode 100644 examples/ReadMe.txt rename examples/{decorator_test.py => rate_limiting_test.py} (100%) rename examples/{run_first_test.bat => run_my_first_test.bat} (100%) rename examples/{run_first_test.sh => run_my_first_test.sh} (100%) rename examples/{run_with_config.sh => run_my_first_test_with_config.sh} (100%) diff --git a/examples/ReadMe.txt b/examples/ReadMe.txt new file mode 100644 index 000000000000..0cf7b323ec56 --- /dev/null +++ b/examples/ReadMe.txt @@ -0,0 +1,9 @@ +The python tests here are in nosetest format, which means that you CANNOT run them by using “python [NAME OF .PY FILE]” in the command prompt. To make running these files easy, .sh files have been created. Those contain the run commands to properly execute the python tests. + +On a MAC or Unix-based system, you can execute .sh files by using ./[NAME OF .SH FILE] in a command prompt from the folder that the .sh files are located in. On a Windows-based system .bat files work the same way. You can switch the file extensions from .sh to .bat if you need to. One .bat file has been included in this folder. + +You may have trouble opening .cfg files if you want to try viewing/editing them because the file extension may be unrecognized on your system. If so, use the Right-Click “Open With” option, or just drag & drop the file into a text-editing program. + +If you run scripts with logging enabled, you’ll see two folders appear: “logs” and “archived logs”. The “logs” folder will contain log files from the most recent test run, but logs will only be created if the test run is failing. Afterwards, logs from the “logs” folder will get pushed to the “archived_logs” folder. + +You may also see .pyc files appear as you run tests. That’s compiled bytecode, which is a natural result of running Python code. diff --git a/examples/decorator_test.py b/examples/rate_limiting_test.py similarity index 100% rename from examples/decorator_test.py rename to examples/rate_limiting_test.py diff --git a/examples/run_first_test.bat b/examples/run_my_first_test.bat similarity index 100% rename from examples/run_first_test.bat rename to examples/run_my_first_test.bat diff --git a/examples/run_first_test.sh b/examples/run_my_first_test.sh similarity index 100% rename from examples/run_first_test.sh rename to examples/run_my_first_test.sh diff --git a/examples/run_with_config.sh b/examples/run_my_first_test_with_config.sh similarity index 100% rename from examples/run_with_config.sh rename to examples/run_my_first_test_with_config.sh From 36b135a3557d59c19aa9f7395558e16420a0ed02 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 23 Jul 2015 11:57:24 -0400 Subject: [PATCH 076/219] Update HipChat settings --- README.md | 2 +- test_framework/config/settings.py | 14 ++++- .../plugins/hipchat_reporting_plugin.py | 51 ++++++++++--------- 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index ed7fd3efb3ea..742bd8e75688 100755 --- a/README.md +++ b/README.md @@ -222,7 +222,7 @@ If you're planning on using the full power of this test framework, there are a f * If you use [Slack](https://slack.com), you can easily have your Jenkins jobs display results there by using the [Jenkins Slack Plugin](https://github.com/jenkinsci/slack-plugin). Another way to send messages from your tests to Slack is by using [Slack's Incoming Webhooks API](https://api.slack.com/incoming-webhooks). -* If you use [HipChat](https://www.hipchat.com/), you can have test alerts go there when tests fail by using the db_reporting plugin. Be sure to first update the db_reporting_plugin.py file from the plugins folder with your HipChat credentials. +* If you use [HipChat](https://www.hipchat.com/), you can easily have your Jenkins jobs display results there by using the [Jenkins HipChat Plugin](https://wiki.jenkins-ci.org/display/JENKINS/HipChat+Plugin). Another way is by using the hipchat_reporting plugin, which is included with this test framework. * Be sure to tell SeleniumSpot to use these added features when you set them up. That's easy to do. You would be running tests like this: diff --git a/test_framework/config/settings.py b/test_framework/config/settings.py index 8e7efc8d637c..d32701af2de3 100755 --- a/test_framework/config/settings.py +++ b/test_framework/config/settings.py @@ -1,11 +1,17 @@ """ -You'll probably want to customize this to your enviroment and needs. +You'll probably want to customize this to your own environment and needs. """ -# Default time to wait for page elements to appear before performing actions + +##### REQUIRED SETTINGS ##### + +# Default times to wait for page elements to appear before performing actions SMALL_TIMEOUT = 7 LARGE_TIMEOUT = 14 + +##### OPTIONAL SETTINGS ##### + # Amazon S3 Bucket Credentials (where screenshots and other log files get saved) S3_LOG_BUCKET = "[ENTER LOG BUCKET FOLDER NAME HERE]" S3_BUCKET_URL = "http://[ENTER SUBDOMAIN OF AMAZON BUCKET URL HERE].s3-[ENTER S3 REGION HERE].amazonaws.com/" @@ -17,3 +23,7 @@ DB_USERNAME = "[TEST DB USERNAME]" DB_PASSWORD = "[TEST DB PASSWORD]" DB_SCHEMA = "[TEST DB SCHEMA]" + +# HipChat Reporting Credentials (for HipChat notifications) +# Other info such as room id and owner to mention get entered during nosetest options +HIPCHAT_AUTH_TOKEN = "[ENTER YOUR HIPCHAT AUTH TOKEN HERE]" diff --git a/test_framework/plugins/hipchat_reporting_plugin.py b/test_framework/plugins/hipchat_reporting_plugin.py index b3dd1968d494..62fa1ee52927 100755 --- a/test_framework/plugins/hipchat_reporting_plugin.py +++ b/test_framework/plugins/hipchat_reporting_plugin.py @@ -8,21 +8,22 @@ import logging import datetime from nose.plugins import Plugin +from test_framework.config import settings HIPCHAT_URL = 'https://api.hipchat.com/v1/rooms/message' -HIPCHAT_AUTH_TOKEN = '[ENTER YOUR HIPCHAT AUTH TOKEN HERE]' +HIPCHAT_AUTH_TOKEN = settings.HIPCHAT_AUTH_TOKEN class HipchatReporting(Plugin): - name = 'hipchat_reporting' # Usage: --with-hipchat_reporting --room_id=[HIPCHAT ROOM ID] --owner_to_mention=[HIPCHAT @NAME] + name = 'hipchat_reporting' # Usage: --with-hipchat_reporting --hipchat_room_id=[HIPCHAT ROOM ID] --hipchat_owner_to_mention=[HIPCHAT @NAME] def __init__(self): super(HipchatReporting, self).__init__() - self.room_id = None - self.owner_to_mention = None - self.notify_on_success = False - self.build_url = os.environ.get("BUILD_URL") + self.hipchat_room_id = None + self.hipchat_owner_to_mention = None + self.hipchat_notify_on_success = False + self.build_url = os.environ.get('BUILD_URL') self.successes = [] self.failures = [] self.errors = [] @@ -30,16 +31,16 @@ def __init__(self): def options(self, parser, env): super(HipchatReporting, self).options(parser, env=env) - parser.add_option('--room_id', action='store', - dest='room_id', - help="The hipchat room ID notifications will be sent to.", + parser.add_option('--hipchat_room_id', action='store', + dest='hipchat_room_id', + help='The hipchat room ID notifications will be sent to.', default=None) - parser.add_option('--owner_to_mention', action='store', - dest='owner_to_mention', - help="The hipchat username to @mention in notifications.", + parser.add_option('--hipchat_owner_to_mention', action='store', + dest='hipchat_owner_to_mention', + help='The hipchat username to @mention in notifications.', default=None) - parser.add_option('--notify_on_success', action='store_true', default=False, - dest='notify_on_success', + parser.add_option('--hipchat_notify_on_success', action='store_true', default=False, + dest='hipchat_notify_on_success', help='Flag for including success notifications. If not specified, only notifies on errors/failures by default.') @@ -47,12 +48,12 @@ def configure(self, options, conf): super(HipchatReporting, self).configure(options, conf) if not self.enabled: return - if not options.room_id: - raise Exception("A hipchat room ID to notify must be specified when using the hipchat reporting plugin.") + if not options.hipchat_room_id: + raise Exception('A hipchat room ID to notify must be specified when using the hipchat reporting plugin.') else: - self.room_id = options.room_id - self.owner_to_mention = options.owner_to_mention or None - self.notify_on_success = options.notify_on_success + self.hipchat_room_id = options.hipchat_room_id + self.hipchat_owner_to_mention = options.hipchat_owner_to_mention or None + self.hipchat_notify_on_success = options.hipchat_notify_on_success def addSuccess(self, test, capt): @@ -72,8 +73,8 @@ def finalize(self, result): success = True if not result.wasSuccessful(): success = False - if self.owner_to_mention and self._is_during_business_hours(): - message += "@" + self.owner_to_mention + '\n' + if self.hipchat_owner_to_mention and self._is_during_business_hours(): + message += "@" + self.hipchat_owner_to_mention + '\n' if self.failures: message += "\n".join(self.failures) @@ -85,7 +86,7 @@ def finalize(self, result): if self.build_url: message += '\n' + self.build_url - elif self.notify_on_success and self.successes: + elif self.hipchat_notify_on_success and self.successes: message = "SUCCESS! The following tests ran successfully:\n+ " message += "\n+ ".join(self.successes) @@ -102,7 +103,7 @@ def _is_during_business_hours(self): def _send_hipchat_notification(self, message, success=True, sender='Selenium'): response = requests.post(HIPCHAT_URL, params={ 'auth_token': HIPCHAT_AUTH_TOKEN, - 'room_id': self.room_id, + 'room_id': self.hipchat_room_id, 'from': sender, 'message': message, 'message_format': 'text', # @mentions are only supported in text format @@ -112,8 +113,8 @@ def _send_hipchat_notification(self, message, success=True, sender='Selenium'): }) if response.status_code == 200: - logging.debug("Notification sent to room %s", self.room_id) + logging.debug("Notification sent to room %s", self.hipchat_room_id) return True else: - logging.error("Failed to send notification to room %s", self.room_id) + logging.error("Failed to send notification to room %s", self.hipchat_room_id) return False From d91477280628dbf3786e2e66ec274fdbfb5873cf Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 23 Jul 2015 12:34:16 -0400 Subject: [PATCH 077/219] Email Manager and settings configuration. --- test_framework/config/settings.py | 13 +++++++++++-- test_framework/fixtures/email_manager.py | 15 +++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/test_framework/config/settings.py b/test_framework/config/settings.py index d32701af2de3..3cf5adecfb2f 100755 --- a/test_framework/config/settings.py +++ b/test_framework/config/settings.py @@ -10,7 +10,7 @@ LARGE_TIMEOUT = 14 -##### OPTIONAL SETTINGS ##### +##### RECOMMENDED SETTINGS ##### # Amazon S3 Bucket Credentials (where screenshots and other log files get saved) S3_LOG_BUCKET = "[ENTER LOG BUCKET FOLDER NAME HERE]" @@ -24,6 +24,15 @@ DB_PASSWORD = "[TEST DB PASSWORD]" DB_SCHEMA = "[TEST DB SCHEMA]" -# HipChat Reporting Credentials (for HipChat notifications) + +##### OPTIONAL SETTINGS ##### + +# Default Email Credentials (if tests send out emails, you can scan through and verify them by using IMAP) +EMAIL_USERNAME = "[TEST ACCOUNT GMAIL USERNAME]@gmail.com" +EMAIL_PASSWORD = "[TEST ACCOUNT GMAIL PASSWORD]" +EMAIL_IMAP_STRING = "imap.gmail.com" +EMAIL_IMAP_PORT = 993 + +# HipChat Reporting Credentials (for HipChat notifications if your team uses HipChat) # Other info such as room id and owner to mention get entered during nosetest options HIPCHAT_AUTH_TOKEN = "[ENTER YOUR HIPCHAT AUTH TOKEN HERE]" diff --git a/test_framework/fixtures/email_manager.py b/test_framework/fixtures/email_manager.py index 73a4ad35a11e..1a7d67edd2fa 100755 --- a/test_framework/fixtures/email_manager.py +++ b/test_framework/fixtures/email_manager.py @@ -1,7 +1,5 @@ """ -EmailManager - a helper class to login, search for, and delete emails. -The default selenium account is '[YOUR-SELENIUM-EMAIL-USERNAME]@gmail.com' with password -'[YOUR-SELENIUM-EMAIL-PASSWORD]'. +EmailManager - a helper class to login, search for, and delete emails. """ import email @@ -10,11 +8,12 @@ import quopri import re import time +from test_framework.config import settings class EmailManager: - """ A helper class to interface with an Email account. Our imap methods - can search and fetch messages without needing a browser. + """ A helper class to interface with an Email account. These imap methods + can search for and fetch messages without needing a browser. Example: @@ -26,8 +25,8 @@ class EmailManager: PLAIN = "text/plain" TIMEOUT = 1800 - def __init__(self, uname="[YOUR SELENIUM GMAIL USERNAME]@gmail.com", pwd='[YOUR SELENIUM GMAIL PASSWORD]', - imap_string="imap.gmail.com", port=993): + def __init__(self, uname=settings.EMAIL_USERNAME, pwd=settings.EMAIL_PASSWORD, + imap_string=settings.EMAIL_IMAP_STRING, port=settings.EMAIL_IMAP_PORT): self.uname = uname self.pwd = pwd self.imap_string = imap_string @@ -36,7 +35,7 @@ def __init__(self, uname="[YOUR SELENIUM GMAIL USERNAME]@gmail.com", pwd='[YOUR def imap_connect(self): """ - Connect to our IMAP mailbox. + Connect to the IMAP mailbox. """ self.mailbox = imaplib.IMAP4_SSL(self.imap_string, self.port) self.mailbox.login(self.uname, self.pwd) From 7dde7cf7e46f83a153fb977a595c19c9b1dd4a00 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 23 Jul 2015 13:40:53 -0400 Subject: [PATCH 078/219] Edits to fixtures. --- test_framework/fixtures/page_interactions.py | 6 +++--- test_framework/fixtures/page_loads.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/test_framework/fixtures/page_interactions.py b/test_framework/fixtures/page_interactions.py index 75120ab896c9..d20d1e7aef39 100755 --- a/test_framework/fixtures/page_interactions.py +++ b/test_framework/fixtures/page_interactions.py @@ -138,9 +138,9 @@ def hover_and_click(driver, hover_selector, click_selector, driver.execute_script("jQuery('%s').mouseover()" % (hover_selector)) for x in range(timeout * 10): try: - driver.find_element(by=click_by, value="%s" % - click_selector).click() - return + element = driver.find_element(by=click_by, value="%s" % + click_selector).click() + return element except Exception: time.sleep(0.1) raise NoSuchElementException("Element %s was not present in %s" % diff --git a/test_framework/fixtures/page_loads.py b/test_framework/fixtures/page_loads.py index 0f2fb2289d07..42d1ecaa45ed 100755 --- a/test_framework/fixtures/page_loads.py +++ b/test_framework/fixtures/page_loads.py @@ -180,7 +180,7 @@ def wait_for_and_switch_to_alert(driver, timeout=settings.LARGE_TIMEOUT): try: driver.switch_to_alert() alert = driver.switch_to_alert() - alert_text = alert.text # this is where the exception is thrown + alert_text = alert.text return alert except NoAlertPresentException: try: @@ -188,5 +188,4 @@ def wait_for_and_switch_to_alert(driver, timeout=settings.LARGE_TIMEOUT): except UnexpectedAlertPresentException: pass - raise Exception("Alert was not present after %s seconds!"\ - % (selector, timeout)) + raise Exception("Alert was not present after %s seconds!" % timeout) From d7d57d8905571373916ce768714dd36f678de0d8 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 23 Jul 2015 13:42:00 -0400 Subject: [PATCH 079/219] Adding wait_for_ready_state_complete() --- test_framework/config/settings.py | 1 + test_framework/fixtures/base_case.py | 4 ++++ test_framework/fixtures/page_loads.py | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/test_framework/config/settings.py b/test_framework/config/settings.py index 3cf5adecfb2f..19d35a5eb02e 100755 --- a/test_framework/config/settings.py +++ b/test_framework/config/settings.py @@ -8,6 +8,7 @@ # Default times to wait for page elements to appear before performing actions SMALL_TIMEOUT = 7 LARGE_TIMEOUT = 14 +EXTREME_TIMEOUT = 42 # "Bueller? ... Bueller?" - (Ferris Bueller's Day Off, 1986) ##### RECOMMENDED SETTINGS ##### diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py index 8d6a854e2d6c..c946e82fb4ba 100755 --- a/test_framework/fixtures/base_case.py +++ b/test_framework/fixtures/base_case.py @@ -131,5 +131,9 @@ def wait_for_element_not_visible(self, selector, by=By.CSS_SELECTOR, timeout=set return page_loads.wait_for_element_not_visible(self.driver, selector, by, timeout) + def wait_for_ready_state_complete(self, timeout=settings.EXTREME_TIMEOUT): + return page_loads.wait_for_ready_state_complete(self.driver, timeout) + + def wait_for_and_switch_to_alert(self, timeout=settings.LARGE_TIMEOUT): return page_loads.wait_for_and_switch_to_alert(self.driver, timeout) diff --git a/test_framework/fixtures/page_loads.py b/test_framework/fixtures/page_loads.py index 42d1ecaa45ed..3a74762077b2 100755 --- a/test_framework/fixtures/page_loads.py +++ b/test_framework/fixtures/page_loads.py @@ -166,6 +166,24 @@ def wait_for_element_not_visible(driver, selector, by=By.CSS_SELECTOR, timeout=s % (selector, timeout)) +def wait_for_ready_state_complete(driver, timeout=settings.EXTREME_TIMEOUT): + """ + The DOM (Document Object Model) has a property called "readyState". + When the value of this becomes "complete", page resources are considered + fully loaded (although AJAX and other loads might still be happening). + This method will wait until document.readyState == "complete". + """ + + for x in range(timeout * 10): + ready_state = driver.execute_script("return document.readyState") + if ready_state == u'complete': + return True + else: + time.sleep(0.1) + raise Exception("Page elements never fully loaded after %s seconds!"\ + % timeout) + + def wait_for_and_switch_to_alert(driver, timeout=settings.LARGE_TIMEOUT): """ Wait for a browser alert to appear, and switch to it. This should be usable From 6b057238b6cfc0def9f297a3f48146a724928567 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 23 Jul 2015 14:17:19 -0400 Subject: [PATCH 080/219] Moving jq_format() to page_utils --- test_framework/fixtures/base_case.py | 7 ++----- test_framework/fixtures/page_utils.py | 11 +++++++++++ test_framework/fixtures/tools.py | 1 + 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100755 test_framework/fixtures/page_utils.py diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py index c946e82fb4ba..bf2762a94854 100755 --- a/test_framework/fixtures/base_case.py +++ b/test_framework/fixtures/base_case.py @@ -5,7 +5,7 @@ from test_framework.config import settings from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.common.by import By -import page_loads, page_interactions +import page_loads, page_interactions, page_utils class BaseCase(unittest.TestCase): @@ -80,10 +80,7 @@ def scroll_click(self, selector): def jq_format(self, code): - """ Use before throwing raw code such as 'div[tab="advanced"]' into jQuery. Similar to "json.dumps(value)". - The first replace should take care of everything. Now see what else there is. """ - code = code.replace('\\','\\\\').replace('\t',' ').replace('\n', '\\n').replace('\"','\\\"').replace('\'','\\\'').replace('\r', '\\r').replace('\v', '\\v').replace('\a', '\\a').replace('\f', '\\f').replace('\b', '\\b').replace('\u', '\\u') - return code + return page_utils.jq_format(code) def set_value(self, selector, value): diff --git a/test_framework/fixtures/page_utils.py b/test_framework/fixtures/page_utils.py new file mode 100755 index 000000000000..580d131d8399 --- /dev/null +++ b/test_framework/fixtures/page_utils.py @@ -0,0 +1,11 @@ +""" +This module contains useful utility methods. +""" + +def jq_format(code): + """ + Use before throwing raw code such as 'div[tab="advanced"]' into jQuery. + Similar to "json.dumps(value)". + """ + code = code.replace('\\','\\\\').replace('\t',' ').replace('\n', '\\n').replace('\"','\\\"').replace('\'','\\\'').replace('\r', '\\r').replace('\v', '\\v').replace('\a', '\\a').replace('\f', '\\f').replace('\b', '\\b').replace('\u', '\\u') + return code diff --git a/test_framework/fixtures/tools.py b/test_framework/fixtures/tools.py index ac2cf431f0ab..22bb093deeec 100755 --- a/test_framework/fixtures/tools.py +++ b/test_framework/fixtures/tools.py @@ -5,4 +5,5 @@ from test_framework.fixtures.page_loads import * from test_framework.fixtures.page_interactions import * +from test_framework.fixtures.page_utils import * from test_framework.fixtures.errors import * From 8cd0ab9f20d6a8986147988b8177407c1a3c3c76 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 23 Jul 2015 14:22:06 -0400 Subject: [PATCH 081/219] Update comments in errors.py --- test_framework/fixtures/errors.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test_framework/fixtures/errors.py b/test_framework/fixtures/errors.py index 49baa7b32f75..4b3eb807f43a 100755 --- a/test_framework/fixtures/errors.py +++ b/test_framework/fixtures/errors.py @@ -1,12 +1,9 @@ """ -This module contains test state related exceptions. -Raising one of these in a test will cause the state of the test to -be logged appropriately. +This module contains test-state related exceptions. +Raising one of these in a test will cause the +test-state to be logged appropriately. """ -#this makes DeprecatedTest and SkipTest available, but nose currently -#(for 3 years) has a bug that won't let us handle them properly so -#we define our own class BlockedTest(Exception): """Raise this to mark a test as Blocked""" From c61ec805b64452ee111659acb8ee0993b31bc5a1 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 23 Jul 2015 14:38:23 -0400 Subject: [PATCH 082/219] Might be safe to use a real tab rather than 4 spaces. --- test_framework/fixtures/page_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_framework/fixtures/page_utils.py b/test_framework/fixtures/page_utils.py index 580d131d8399..b92f953f0cad 100755 --- a/test_framework/fixtures/page_utils.py +++ b/test_framework/fixtures/page_utils.py @@ -7,5 +7,5 @@ def jq_format(code): Use before throwing raw code such as 'div[tab="advanced"]' into jQuery. Similar to "json.dumps(value)". """ - code = code.replace('\\','\\\\').replace('\t',' ').replace('\n', '\\n').replace('\"','\\\"').replace('\'','\\\'').replace('\r', '\\r').replace('\v', '\\v').replace('\a', '\\a').replace('\f', '\\f').replace('\b', '\\b').replace('\u', '\\u') + code = code.replace('\\','\\\\').replace('\t','\\t').replace('\n', '\\n').replace('\"','\\\"').replace('\'','\\\'').replace('\r', '\\r').replace('\v', '\\v').replace('\a', '\\a').replace('\f', '\\f').replace('\b', '\\b').replace('\u', '\\u') return code From 54c81cd506134a6eba7f7f9d565e8ada1367240b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 23 Jul 2015 15:33:56 -0400 Subject: [PATCH 083/219] Update comments for page_utils.py --- test_framework/fixtures/page_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test_framework/fixtures/page_utils.py b/test_framework/fixtures/page_utils.py index b92f953f0cad..cbafbb4c6a41 100755 --- a/test_framework/fixtures/page_utils.py +++ b/test_framework/fixtures/page_utils.py @@ -5,7 +5,8 @@ def jq_format(code): """ Use before throwing raw code such as 'div[tab="advanced"]' into jQuery. - Similar to "json.dumps(value)". + Selectors with quotes inside of quotes would otherwise break jQuery. (One example) + This is similar to "json.dumps(value)". """ code = code.replace('\\','\\\\').replace('\t','\\t').replace('\n', '\\n').replace('\"','\\\"').replace('\'','\\\'').replace('\r', '\\r').replace('\v', '\\v').replace('\a', '\\a').replace('\f', '\\f').replace('\b', '\\b').replace('\u', '\\u') return code From 05f94a0d530a796475ef0a5b73707a97ee7fd65c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 23 Jul 2015 15:34:45 -0400 Subject: [PATCH 084/219] Updating methods that update text values. --- test_framework/fixtures/base_case.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py index bf2762a94854..40c940d4e406 100755 --- a/test_framework/fixtures/base_case.py +++ b/test_framework/fixtures/base_case.py @@ -91,21 +91,26 @@ def set_value(self, selector, value): def update_text_value(self, selector, new_value, timeout=settings.SMALL_TIMEOUT, retry=False): """ This method updates a selector's text value with a new value @Params - selector - the selector with the value to change - new_value - the new value for the text field where the selector points to - timeout - how long to want for the selector to be visible before timing out - retry - if text update fails, try the jQuery version (Warning: don't use this if update_text_value() takes you to - a new page, or if it resets the value (such as using [backslash n] for the enter key) """ + selector - the selector with the value to update + new_value - the new value for setting the text field + timeout - how long to wait for the selector to be visible + retry - if True, use jquery if the selenium text update fails + """ element = self.wait_for_element_visible(selector, timeout=timeout) element.clear() element.send_keys(new_value) + if retry and element.get_attribute('value') != new_value and not new_value.endswith('\n'): + logging.debug('update_text_value is falling back to jQuery!') + selector = self.jq_format(selector) + self.set_value(selector, new_value) - if retry: - if element.get_attribute('value') != new_value: - logging.debug('update_text_value is falling back to jQuery!') - # Since selectors with quotes inside of quotes such as 'div[data-tab-name="advanced"]' break jQuery, format them first - selector = self.jq_format(selector) - self.set_value(selector, new_value) + + def jquery_update_text_value(self, selector, new_value, timeout=settings.SMALL_TIMEOUT): + element = self.wait_for_element_visible(selector, timeout=timeout) + self.driver.execute_script("""jQuery('%s').val('%s')""" + % (selector, self.jq_format(new_value))) + if new_value.endswith('\n'): + element.send_keys('\n') def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): From ae68476775f5ec18b32b3ca5a603e106f9f91c03 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 23 Jul 2015 17:07:34 -0400 Subject: [PATCH 085/219] The option to add wait_for_ready_state_complete() after various actions. --- test_framework/config/settings.py | 12 +++++++++--- test_framework/fixtures/base_case.py | 6 +++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/test_framework/config/settings.py b/test_framework/config/settings.py index 19d35a5eb02e..07b2c52e0509 100755 --- a/test_framework/config/settings.py +++ b/test_framework/config/settings.py @@ -3,15 +3,21 @@ """ -##### REQUIRED SETTINGS ##### +#####>>>>>----- REQUIRED SETTINGS -----<<<<<##### # Default times to wait for page elements to appear before performing actions SMALL_TIMEOUT = 7 LARGE_TIMEOUT = 14 EXTREME_TIMEOUT = 42 # "Bueller? ... Bueller?" - (Ferris Bueller's Day Off, 1986) +# The option of adding wait_for_ready_state_complete() after various actions. +# By default, Selenium waits for the 'interactive' state, which might not be enough. +# Setting these values to True might make your tests more reliable, but it also might slightly slow them down. +WAIT_FOR_RSC_ON_PAGE_LOADS = False # When using self.open(url) or self.open_url(url), NOT self.driver.open(url) +WAIT_FOR_RSC_ON_CLICKS = False # When using self.click(selector), NOT element.click() -##### RECOMMENDED SETTINGS ##### + +#####>>>>>----- RECOMMENDED SETTINGS -----<<<<<##### # Amazon S3 Bucket Credentials (where screenshots and other log files get saved) S3_LOG_BUCKET = "[ENTER LOG BUCKET FOLDER NAME HERE]" @@ -26,7 +32,7 @@ DB_SCHEMA = "[TEST DB SCHEMA]" -##### OPTIONAL SETTINGS ##### +#####>>>>>----- OPTIONAL SETTINGS -----<<<<<##### # Default Email Credentials (if tests send out emails, you can scan through and verify them by using IMAP) EMAIL_USERNAME = "[TEST ACCOUNT GMAIL USERNAME]@gmail.com" diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py index 40c940d4e406..f63b204697e9 100755 --- a/test_framework/fixtures/base_case.py +++ b/test_framework/fixtures/base_case.py @@ -53,16 +53,20 @@ def jquery_click(self, selector): def click(self, selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): element = page_loads.wait_for_element_visible(self.driver, selector, by, timeout=timeout) + if settings.WAIT_FOR_RSC_ON_CLICKS: + self.wait_for_ready_state_complete() return element.click() def open(self, url): self.driver.get(url) + if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: + self.wait_for_ready_state_complete() def open_url(self, url): """ In case people are mixing up self.open() with open() """ - self.driver.get(url) + self.open(url) def activate_jquery(self): From b52c1c36dc5adf238693ebc6f8fb1ef268139a7f Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 23 Jul 2015 19:56:01 -0400 Subject: [PATCH 086/219] Improvements to alert-handling code. --- test_framework/fixtures/base_case.py | 8 ++++++ test_framework/fixtures/page_loads.py | 37 ++++++++++++++++++++------- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py index f63b204697e9..5b86c53408fa 100755 --- a/test_framework/fixtures/base_case.py +++ b/test_framework/fixtures/base_case.py @@ -141,5 +141,13 @@ def wait_for_ready_state_complete(self, timeout=settings.EXTREME_TIMEOUT): return page_loads.wait_for_ready_state_complete(self.driver, timeout) + def wait_for_and_accept_alert(self, timeout=settings.LARGE_TIMEOUT): + return page_loads.wait_for_and_accept_alert(self.driver, timeout) + + + def wait_for_and_dismiss_alert(self, timeout=settings.LARGE_TIMEOUT): + return page_loads.wait_for_and_dismiss_alert(self.driver, timeout) + + def wait_for_and_switch_to_alert(self, timeout=settings.LARGE_TIMEOUT): return page_loads.wait_for_and_switch_to_alert(self.driver, timeout) diff --git a/test_framework/fixtures/page_loads.py b/test_framework/fixtures/page_loads.py index 3a74762077b2..7e472f784ac1 100755 --- a/test_framework/fixtures/page_loads.py +++ b/test_framework/fixtures/page_loads.py @@ -22,8 +22,7 @@ from selenium.webdriver.common.by import By from selenium.webdriver.remote.errorhandler import ElementNotVisibleException, \ NoSuchElementException, \ - NoAlertPresentException, \ - UnexpectedAlertPresentException + NoAlertPresentException def wait_for_element_present(driver, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): @@ -184,6 +183,32 @@ def wait_for_ready_state_complete(driver, timeout=settings.EXTREME_TIMEOUT): % timeout) +def wait_for_and_accept_alert(driver, timeout=settings.LARGE_TIMEOUT): + """ + Wait for and accept an alert. Returns the text from the alert. + @params + driver - the webdriver object (required) + timeout - the time to wait for the alert in seconds + """ + alert = wait_for_and_switch_to_alert(driver, timeout) + alert_text = alert.text + alert.accept() + return alert_text + + +def wait_for_and_dismiss_alert(driver, timeout=settings.LARGE_TIMEOUT): + """ + Wait for and dismiss an alert. Returns the text from the alert. + @params + driver - the webdriver object (required) + timeout - the time to wait for the alert in seconds + """ + alert = wait_for_and_switch_to_alert(driver, timeout) + alert_text = alert.text + alert.dismiss() + return alert_text + + def wait_for_and_switch_to_alert(driver, timeout=settings.LARGE_TIMEOUT): """ Wait for a browser alert to appear, and switch to it. This should be usable @@ -196,14 +221,8 @@ def wait_for_and_switch_to_alert(driver, timeout=settings.LARGE_TIMEOUT): for x in range(timeout * 10): try: - driver.switch_to_alert() alert = driver.switch_to_alert() - alert_text = alert.text return alert except NoAlertPresentException: - try: - time.sleep(0.1) - except UnexpectedAlertPresentException: - pass - + time.sleep(0.1) raise Exception("Alert was not present after %s seconds!" % timeout) From b0cda3da05763f4714ebc0d20d4dde53db8af872 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 23 Jul 2015 19:56:29 -0400 Subject: [PATCH 087/219] Adding returns and shortcuts. --- test_framework/fixtures/base_case.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py index 5b86c53408fa..c930830c6749 100755 --- a/test_framework/fixtures/base_case.py +++ b/test_framework/fixtures/base_case.py @@ -55,7 +55,8 @@ def click(self, selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): element = page_loads.wait_for_element_visible(self.driver, selector, by, timeout=timeout) if settings.WAIT_FOR_RSC_ON_CLICKS: self.wait_for_ready_state_complete() - return element.click() + element.click() + return element def open(self, url): @@ -69,18 +70,32 @@ def open_url(self, url): self.open(url) + def execute_script(self, script): + return self.driver.execute_script(script) + + + def find_element_by_link_text(self, link_text, timeout=settings.SMALL_TIMEOUT): + return self.wait_for_element_visible(link_text, by=By.LINK_TEXT, timeout=timeout) + + + def click_link_text(self, link_text, timeout=settings.SMALL_TIMEOUT): + element = self.find_element_by_link_text(link_text, timeout=timeout) + element.click() + return element + + def activate_jquery(self): """ (It's not on by default on all website pages.) """ - self.driver.execute_script('var script = document.createElement("script"); script.src = "https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"; document.getElementsByTagName("head")[0].appendChild(script);') + return self.driver.execute_script('var script = document.createElement("script"); script.src = "https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"; document.getElementsByTagName("head")[0].appendChild(script);') def scroll_to(self, selector): - self.driver.execute_script("jQuery('%s')[0].scrollIntoView()" % selector) + return self.driver.execute_script("jQuery('%s')[0].scrollIntoView()" % selector) def scroll_click(self, selector): self.scroll_to(selector) - self.click(selector) + return self.click(selector) def jq_format(self, code): @@ -107,6 +122,7 @@ def update_text_value(self, selector, new_value, timeout=settings.SMALL_TIMEOUT, logging.debug('update_text_value is falling back to jQuery!') selector = self.jq_format(selector) self.set_value(selector, new_value) + return element def jquery_update_text_value(self, selector, new_value, timeout=settings.SMALL_TIMEOUT): @@ -115,6 +131,7 @@ def jquery_update_text_value(self, selector, new_value, timeout=settings.SMALL_T % (selector, self.jq_format(new_value))) if new_value.endswith('\n'): element.send_keys('\n') + return element def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): From 7c86a05be63785d203fa2d8ad28a6360f0f8e7e5 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 24 Jul 2015 10:50:26 -0400 Subject: [PATCH 088/219] Updates to base_case.py --- test_framework/fixtures/base_case.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/test_framework/fixtures/base_case.py b/test_framework/fixtures/base_case.py index c930830c6749..1f9146e11b43 100755 --- a/test_framework/fixtures/base_case.py +++ b/test_framework/fixtures/base_case.py @@ -43,20 +43,23 @@ def is_element_visible(self, selector, by=By.CSS_SELECTOR): return page_interactions.is_element_visible(self.driver, selector, by) + def is_link_text_visible(self, link_text): + return page_interactions.is_element_visible(self.driver, link_text, by=By.LINK_TEXT) + + def is_text_visible(self, text, selector, by=By.CSS_SELECTOR): return page_interactions.is_text_visible(self.driver, text, selector, by) def jquery_click(self, selector): - return self.driver.execute_script("jQuery('%s').click()" % selector) + self.driver.execute_script("jQuery('%s').click()" % selector) def click(self, selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): element = page_loads.wait_for_element_visible(self.driver, selector, by, timeout=timeout) + element.click() if settings.WAIT_FOR_RSC_ON_CLICKS: self.wait_for_ready_state_complete() - element.click() - return element def open(self, url): @@ -66,7 +69,7 @@ def open(self, url): def open_url(self, url): - """ In case people are mixing up self.open() with open() """ + """ In case people are mixing up self.open() with open(), use this alternative. """ self.open(url) @@ -74,28 +77,29 @@ def execute_script(self, script): return self.driver.execute_script(script) - def find_element_by_link_text(self, link_text, timeout=settings.SMALL_TIMEOUT): + def wait_for_link_text_visible(self, link_text, timeout=settings.LARGE_TIMEOUT): return self.wait_for_element_visible(link_text, by=By.LINK_TEXT, timeout=timeout) def click_link_text(self, link_text, timeout=settings.SMALL_TIMEOUT): - element = self.find_element_by_link_text(link_text, timeout=timeout) + element = self.wait_for_link_text_visible(link_text, timeout=timeout) element.click() - return element + if settings.WAIT_FOR_RSC_ON_CLICKS: + self.wait_for_ready_state_complete() def activate_jquery(self): """ (It's not on by default on all website pages.) """ - return self.driver.execute_script('var script = document.createElement("script"); script.src = "https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"; document.getElementsByTagName("head")[0].appendChild(script);') + self.driver.execute_script('var script = document.createElement("script"); script.src = "https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"; document.getElementsByTagName("head")[0].appendChild(script);') def scroll_to(self, selector): - return self.driver.execute_script("jQuery('%s')[0].scrollIntoView()" % selector) + self.driver.execute_script("jQuery('%s')[0].scrollIntoView()" % selector) def scroll_click(self, selector): self.scroll_to(selector) - return self.click(selector) + self.click(selector) def jq_format(self, code): @@ -104,7 +108,7 @@ def jq_format(self, code): def set_value(self, selector, value): val = json.dumps(value) - return self.driver.execute_script("jQuery('%s').val(%s)" % (selector, val)) + self.driver.execute_script("jQuery('%s').val(%s)" % (selector, val)) def update_text_value(self, selector, new_value, timeout=settings.SMALL_TIMEOUT, retry=False): @@ -122,7 +126,6 @@ def update_text_value(self, selector, new_value, timeout=settings.SMALL_TIMEOUT, logging.debug('update_text_value is falling back to jQuery!') selector = self.jq_format(selector) self.set_value(selector, new_value) - return element def jquery_update_text_value(self, selector, new_value, timeout=settings.SMALL_TIMEOUT): @@ -131,7 +134,6 @@ def jquery_update_text_value(self, selector, new_value, timeout=settings.SMALL_T % (selector, self.jq_format(new_value))) if new_value.endswith('\n'): element.send_keys('\n') - return element def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): From 2e7e969fe8a3f8de0ae8c714d4c9aa2c9e128fa3 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 24 Jul 2015 16:27:28 -0400 Subject: [PATCH 089/219] Updates to the ReadMe.md --- README.md | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 742bd8e75688..d2d2da2c7249 100755 --- a/README.md +++ b/README.md @@ -1,28 +1,26 @@ # SeleniumSpot Test Framework -The purpose of this open-sourced Selenium Webdriver-based test framework is to have a complete and fully automated testing system for performing browser-based integration testing. You may be wondering why you'd want to use this as opposed to raw WebDriver scripts. Here are the added benefits: +"The best way of creating test automation for your web apps." + +Features include: * MySQL DB integration for storing test data and results -* Amazon S3 manager (to upload logs and screenshots from tests when they fail to see what went wrong) -* Easy to use and integrate with the Jenkins build-server -* An advanced logging system (to organize and store all your test data in Jenkins + S3 + MySQL) -* A system for easily utilizing data generated from previous tests (the delayed-data manager) -* An error-handling system (so that we can process useful data for the log files, eg: the page_source plugin) -* An email manager (so that we can automatically read and parse through emails sent to gmail addresses) +* Amazon S3 manager for uploading logs and screenshots +* Easy integration with the Jenkins build-server +* A powerful logging system for tracking failures +* A system for utilizing data from previous tests +* An email manager for parsing through sent emails * Libraries for code simplification and reusable code -* Nosetest support (a fast and easy way to run all your tests) -* A plugin to send test failure notifications directly through HipChat (in the event of a test failure) -* Advanced commands that will save you significant time +* Nosetest support for running your tests with ease +* Advanced commands for saving you significant time -To utilize some of the more advanced integrations, you'll need to setup instances and make connections to the following: -MySQL, Jenkins, Amazon S3, Gmail, HipChat, and a Selenium Grid. (More on this later) -We've provided placeholders in the code where you can specify your connection details. You can also use this framework as a bare-bones Selenium WebDriver command executer to automate tasks in a browser without doing any data reporting (and that's also the fastest way to make sure your base setup is working properly). -If you plan on running tests from a build server across multiple cloud machines, connecting to BrowserStack's Selenium cloud may be the least expensive alternative to having your own Selenium Grid. (If you're looking elsewhere, be wary of any Selenium cloud service that charges you by the test minute, as opposed to a flat monthly fee, because it may be a trap - those automated test minutes add up fast, and you don't want to limit the amount of automation you have.) -Check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot). This is an excellent example of all the pieces coming together. +To utilize some of the more advanced integrations, you'll want to setup instances and make connections to the following: +MySQL, Jenkins, Amazon S3, and the Selenium Grid. (More on this later) +We've provided placeholders in the code where you can specify your connection details. You can also use this framework as a bare-bones Selenium WebDriver command executer to automate tasks in a browser without doing any data reporting (and that's also the fastest way to make sure your base setup is working properly). +If you plan on running tests from a build server across multiple cloud machines, you can connect to your own Selenium Grid or use a cloud provider such as BrowserStack. -In short, developers aren't perfect. Bugs can slip by undetected during deploys even if there are existing unit tests to watch for problems. A fully-capable integration testing solution can provide an added layer of security. -A working system would be something like this: You have a QA build and a Prod build. After a QA deploy, run all the associated Selenium tests on QA. If everything passes, it's considered safe to deploy to Prod. As an added safety measure, run all those Selenium tests again on Prod after a deploy. If those pass, you should be able to feel safe. For more protection, also run Selenium tests at regular intervals in case something other than deploys breaks the system. +For an excellent example of all the pieces coming together, check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot). ## Part I: MAC SETUP INSTRUCTIONS From 2bd28198050bac31dc7a106d7628a81c3302f655 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 24 Jul 2015 18:40:33 -0400 Subject: [PATCH 090/219] Install the requirements right from setup.py --- setup.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/setup.py b/setup.py index f9465eccf975..c52d94b695da 100755 --- a/setup.py +++ b/setup.py @@ -4,11 +4,28 @@ from setuptools import setup, find_packages +REQUIREMENTS = [ + 'python==2.7.6', + 'selenium==2.46.1', + 'nose==1.3.7', + 'requests==2.7.0', + 'urllib3==1.10.4', + 'BeautifulSoup==3.2.1', + 'unittest2==1.1.0', + 'chardet==2.3.0', + 'simplejson==3.7.3', + 'boto==2.38.0', + 'MySQL-python==1.2.5', + 'pdb==0.1', + 'ipdb==0.8.1' +] + setup( name = 'seleniumspot', version = '1.1.1', author = 'Michael Mintz', author_email = '@mintzworld', + install_requires=REQUIREMENTS, maintainer = 'Michael Mintz', description = 'The SeleniumSpot Test Framework. (Powered by Python, WebDriver, and more...)', license = 'The MIT License', From 4fff1132e5dd041c751deff5207f232d06eaaf6c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 26 Jul 2015 18:55:40 -0400 Subject: [PATCH 091/219] Creating a local install that doesn't require MySQL --- local_requirements.pip | 11 ++++++++++ local_setup.py | 50 ++++++++++++++++++++++++++++++++++++++++++ requirements.pip | 1 - setup.py | 6 +++-- 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100755 local_requirements.pip create mode 100755 local_setup.py diff --git a/local_requirements.pip b/local_requirements.pip new file mode 100755 index 000000000000..87cf48b839ec --- /dev/null +++ b/local_requirements.pip @@ -0,0 +1,11 @@ +selenium==2.46.1 +nose==1.3.7 +requests==2.7.0 +urllib3==1.10.4 +BeautifulSoup==3.2.1 +unittest2==1.1.0 +chardet==2.3.0 +simplejson==3.7.3 +boto==2.38.0 +pdb==0.1 +ipdb==0.8.1 diff --git a/local_setup.py b/local_setup.py new file mode 100755 index 000000000000..c9afa553a53d --- /dev/null +++ b/local_setup.py @@ -0,0 +1,50 @@ +""" +The setup package to install the SeleniumSpot Test Framework plugins +on a development machine that DOES NOT intend to write to +a MySQL DB during test runs. +""" + +from setuptools import setup, find_packages + +REQUIREMENTS = [ + #'python==2.7.6', # You'll want that pre-installed before running this file + 'selenium==2.46.1', + 'nose==1.3.7', + 'requests==2.7.0', + 'urllib3==1.10.4', + 'BeautifulSoup==3.2.1', + 'unittest2==1.1.0', + 'chardet==2.3.0', + 'simplejson==3.7.3', + 'boto==2.38.0', + 'pdb==0.1', + 'ipdb==0.8.1' +] + +setup( + name = 'seleniumspot', + version = '1.1.1', + author = 'Michael Mintz', + author_email = '@mintzworld', + install_requires=REQUIREMENTS, + maintainer = 'Michael Mintz', + description = 'The SeleniumSpot Test Framework. (Powered by Python, WebDriver, and more...)', + license = 'The MIT License', + packages = ['test_framework', + 'test_framework.core', + 'test_framework.plugins', + 'test_framework.fixtures', + 'test_framework.common', + 'test_framework.config'], + entry_points = { + 'nose.plugins': [ + 'base_plugin = test_framework.plugins.base_plugin:Base', + 'selenium = test_framework.plugins.selenium_plugin:SeleniumBase', + 'page_source = test_framework.plugins.page_source:PageSource', + 'screen_shots = test_framework.plugins.screen_shots:ScreenShots', + 'test_info = test_framework.plugins.basic_test_info:BasicTestInfo', + 's3_logging = test_framework.plugins.s3_logging_plugin:S3Logging', + 'hipchat_reporting = test_framework.plugins.hipchat_reporting_plugin:HipchatReporting', + ] + } + ) diff --git a/requirements.pip b/requirements.pip index 485b3186fa61..60a5036cdb00 100755 --- a/requirements.pip +++ b/requirements.pip @@ -1,4 +1,3 @@ -python==2.7.6 selenium==2.46.1 nose==1.3.7 requests==2.7.0 diff --git a/setup.py b/setup.py index c52d94b695da..4b5bd899e805 100755 --- a/setup.py +++ b/setup.py @@ -1,11 +1,13 @@ """ -The setup package to install the SeleniumSpot Test Framework plugins. +The setup package to install the SeleniumSpot Test Framework plugins +on a server machine (or a development machine that intends to write to +a MySQL DB during test runs). """ from setuptools import setup, find_packages REQUIREMENTS = [ - 'python==2.7.6', + #'python==2.7.6', # You'll want that pre-installed before running this file 'selenium==2.46.1', 'nose==1.3.7', 'requests==2.7.0', From 83abf299334a96c6a03998ad5fa41955f90096e2 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 26 Jul 2015 18:56:33 -0400 Subject: [PATCH 092/219] Giving Windows users some love --- README.md | 37 ++++++++++++++++++++++--- examples/run_my_first_test.bat | 2 +- examples/run_test_fail_with_debug.bat | 1 + examples/run_test_fail_with_logging.bat | 1 + 4 files changed, 36 insertions(+), 5 deletions(-) create mode 100755 examples/run_test_fail_with_debug.bat create mode 100755 examples/run_test_fail_with_logging.bat diff --git a/README.md b/README.md index d2d2da2c7249..dd8f1fc25b13 100755 --- a/README.md +++ b/README.md @@ -24,21 +24,33 @@ For an excellent example of all the pieces coming together, check out HubSpot's ## Part I: MAC SETUP INSTRUCTIONS -####(Windows users: Try Powershell. You may need to make some adjustments during installation. This framework works on Windows machines if setup correctly.) +####(WINDOWS users: You'll need to make a few modifications to the setup steps listed here. For starters, you won't be able to use the "brew install" command since that's MAC-only. Instead, download the requirements mentioned directly from the web. I'll provide you with links to save you time. You'll also want to put downloaded files into your [PATH](http://java.com/en/download/help/path.xml).) **Step 0:** Get the requirements: +[Python 2.*](https://www.python.org/downloads/) + +If you're a MAC user, that should already come preinstalled on your machine. Although Python 3 exists, you'll want Python 2 (both of these major versions are being improved in parallel). Python 2.7.6 is the one I've been using on my Mac. + +If you're a WINDOWS user, [download the latest 2.* version from here](https://www.python.org/downloads/release/python-2710/). + [Homebrew](http://brew.sh/) + [Git](http://git-scm.com/) ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew install git brew update +(WINDOWS users: Skip the Homebrew part and just [download Git here](http://git-scm.com/download).) + [MySQL](http://www.mysql.com/) +(NOTE: If you're using this test framework from a local development machine and don't plan on writing to the MySQL DB from your local test runs, you can skip this step.) + brew install MySQL -That installs the MySQL library so that you can use db commands in your code. To make that useful, you'll want to have a MySQL DB that you can connect to, and you'll want to put your credentials in the mysql_conf.py file in the test_framework/core folder to access your DB from your tests. You'll also want to add the necessary tables, so to get you started, use the testcaserepository.sql file from the test_framework/core folder. +That installs the MySQL library so that you can use db commands in your code. To make that useful, you'll want to have a MySQL DB that you can connect to. You'll also want to use the testcaserepository.sql file from the test_framework/core folder to add the necessary tables. + +(WINDOWS users: [Download MySQL here](http://dev.mysql.com/downloads/windows/). If you want a visual tool to help make your MySQL life easier, [try MySQL Workbench](http://dev.mysql.com/downloads/workbench/).) [virtualenvwrapper](http://virtualenvwrapper.readthedocs.org/en/latest/) @@ -60,7 +72,9 @@ To save time from having to source virtualenvwrapper again when you open a new w brew install chromedriver phantomjs -(There are web drivers for other web browsers as well. These two will get you started.) +(NOTE: There are web drivers for other web browsers as well. These two will get you started.) + +(WINDOWS users: [Download Chromedriver](https://sites.google.com/a/chromium.org/chromedriver/downloads) and put it in your PATH. Next, [Download PhantomJS](https://bitbucket.org/ariya/phantomjs/downloads) and also put that in your PATH.) **Step 1:** Checkout the SeleniumSpot Test Framework with Git or a Git GUI tool: @@ -72,6 +86,8 @@ git clone [LOCATION OF YOUR FORKED SELENIUMSPOT GITHUB FOLDER]/SeleniumSpot.git cd SeleniumSpot ``` +(NOTE: You can also download the SeleniumSpot repository right from GitHub and skip all the git-related commands. That's probably the fastest way if you want to quickly get a live demo of this tool up and running.) + **Step 2:** Create a virtualenv for seleniumspot: @@ -107,14 +123,27 @@ rmvirtualenv [NAME OF VIRTUAL ENV TO REMOVE] **Step 3:** Install necessary packages from the SeleniumSpot folder and compile the test framework +If you're NOT connecting to a MySQL DB from your local test runs (based on the path you chose above), use these steps: + +```bash +sudo pip install -r local_requirements.pip +sudo python local_setup.py install +``` + +If you ARE connecting to a MySQL DB from your local test runs, use these steps: + ```bash sudo pip install -r requirements.pip sudo python setup.py install ``` +NOTE: + +(As of now, you can skip the pip install requirements line because the python setup install line will now install those requirements automatically.) + (If you already have root access on the machine you're using, you might not need to add "sudo" before those commands.) -(If the pip install gives you a "clang error: unknown argument: '-mno-fused-madd'", see: http://stackoverflow.com/questions/22313407/clang-error-unknown-argument-mno-fused-madd-python-package-installation-fa) +(If the pip install gives you a "clang error: unknown argument: '-mno-fused-madd'", [try this](http://stackoverflow.com/questions/22313407/clang-error-unknown-argument-mno-fused-madd-python-package-installation-fa).) In some cames, certain packages will have other dependencies as requirements, and those will get installed automatically. You'll be able to see all installed packages in your virtual environment by using the following command: diff --git a/examples/run_my_first_test.bat b/examples/run_my_first_test.bat index dfd41c2c6585..a859824678f8 100755 --- a/examples/run_my_first_test.bat +++ b/examples/run_my_first_test.bat @@ -1 +1 @@ -nosetests my_first_test.py --browser=chrome --with-selenium --logging-level=INFO -s \ No newline at end of file +nosetests my_first_test.py --browser=firefox --with-selenium --logging-level=INFO -s \ No newline at end of file diff --git a/examples/run_test_fail_with_debug.bat b/examples/run_test_fail_with_debug.bat new file mode 100755 index 000000000000..660a8f54d526 --- /dev/null +++ b/examples/run_test_fail_with_debug.bat @@ -0,0 +1 @@ +nosetests test_fail.py --browser=firefox --with-selenium --logging-level=INFO --pdb --pdb-failures -s \ No newline at end of file diff --git a/examples/run_test_fail_with_logging.bat b/examples/run_test_fail_with_logging.bat new file mode 100755 index 000000000000..fc97ca8d9178 --- /dev/null +++ b/examples/run_test_fail_with_logging.bat @@ -0,0 +1 @@ +nosetests test_fail.py --browser=firefox --with-selenium --logging-level=INFO --with-testing_base --with-basic_test_info --with-page_source --with-screen_shots -s \ No newline at end of file From 56d24d1d09e1314787b380c2d2f6aee8392ca19c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 26 Jul 2015 19:39:20 -0400 Subject: [PATCH 093/219] Browsers that you'll need to run the example tests. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index dd8f1fc25b13..af200e60c244 100755 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ To save time from having to source virtualenvwrapper again when you open a new w (WINDOWS users: [Download Chromedriver](https://sites.google.com/a/chromium.org/chromedriver/downloads) and put it in your PATH. Next, [Download PhantomJS](https://bitbucket.org/ariya/phantomjs/downloads) and also put that in your PATH.) +If you haven't already, you'll want to [Download Firefox](https://www.mozilla.org/en-US/firefox/new/) and either [Download Chrome](https://www.google.com/chrome/browser/desktop/index.html) or [Download Chromium](https://download-chromium.appspot.com/). + **Step 1:** Checkout the SeleniumSpot Test Framework with Git or a Git GUI tool: From 09baf44d31b5c664f8213570115f3bc6a50bab89 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 26 Jul 2015 21:13:36 -0400 Subject: [PATCH 094/219] Force using pip to install the requirements. --- README.md | 2 -- local_setup.py | 16 ---------------- setup.py | 17 ----------------- 3 files changed, 35 deletions(-) diff --git a/README.md b/README.md index af200e60c244..e6a16cb347af 100755 --- a/README.md +++ b/README.md @@ -141,8 +141,6 @@ sudo python setup.py install NOTE: -(As of now, you can skip the pip install requirements line because the python setup install line will now install those requirements automatically.) - (If you already have root access on the machine you're using, you might not need to add "sudo" before those commands.) (If the pip install gives you a "clang error: unknown argument: '-mno-fused-madd'", [try this](http://stackoverflow.com/questions/22313407/clang-error-unknown-argument-mno-fused-madd-python-package-installation-fa).) diff --git a/local_setup.py b/local_setup.py index c9afa553a53d..09cdb5edc88f 100755 --- a/local_setup.py +++ b/local_setup.py @@ -6,27 +6,11 @@ from setuptools import setup, find_packages -REQUIREMENTS = [ - #'python==2.7.6', # You'll want that pre-installed before running this file - 'selenium==2.46.1', - 'nose==1.3.7', - 'requests==2.7.0', - 'urllib3==1.10.4', - 'BeautifulSoup==3.2.1', - 'unittest2==1.1.0', - 'chardet==2.3.0', - 'simplejson==3.7.3', - 'boto==2.38.0', - 'pdb==0.1', - 'ipdb==0.8.1' -] - setup( name = 'seleniumspot', version = '1.1.1', author = 'Michael Mintz', author_email = '@mintzworld', - install_requires=REQUIREMENTS, maintainer = 'Michael Mintz', description = 'The SeleniumSpot Test Framework. (Powered by Python, WebDriver, and more...)', license = 'The MIT License', diff --git a/setup.py b/setup.py index 4b5bd899e805..62a2533e6d6c 100755 --- a/setup.py +++ b/setup.py @@ -6,28 +6,11 @@ from setuptools import setup, find_packages -REQUIREMENTS = [ - #'python==2.7.6', # You'll want that pre-installed before running this file - 'selenium==2.46.1', - 'nose==1.3.7', - 'requests==2.7.0', - 'urllib3==1.10.4', - 'BeautifulSoup==3.2.1', - 'unittest2==1.1.0', - 'chardet==2.3.0', - 'simplejson==3.7.3', - 'boto==2.38.0', - 'MySQL-python==1.2.5', - 'pdb==0.1', - 'ipdb==0.8.1' -] - setup( name = 'seleniumspot', version = '1.1.1', author = 'Michael Mintz', author_email = '@mintzworld', - install_requires=REQUIREMENTS, maintainer = 'Michael Mintz', description = 'The SeleniumSpot Test Framework. (Powered by Python, WebDriver, and more...)', license = 'The MIT License', From 583debd91464e43040fdb40951322b03a613f31b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 26 Jul 2015 21:35:55 -0400 Subject: [PATCH 095/219] Say it the right way. --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e6a16cb347af..3041b11554b4 100755 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ If you're a WINDOWS user, [download the latest 2.* version from here](https://ww brew install git brew update -(WINDOWS users: Skip the Homebrew part and just [download Git here](http://git-scm.com/download).) +(WINDOWS users: Skip the Homebrew part and [download Git here](http://git-scm.com/download).) [MySQL](http://www.mysql.com/) @@ -226,7 +226,7 @@ Here are some other useful nosetest arguments that you may want to append to you ```bash --logging-level=INFO # Hide DEBUG messages, which can be overwhelming. -x # Stop running the tests after the first failure is reached. --v # Prints the full test name rather than just a dot for each test. +-v # Prints the full test name rather than a dot for each test. --with-id # If -v is also used, will number the tests for easy counting. ``` @@ -240,7 +240,7 @@ If you're planning on using the full power of this test framework, there are a f * Install [MySQL Workbench](http://dev.mysql.com/downloads/tools/workbench/) to make life easier by giving you a nice GUI tool that you can use to read & write from your DB directly. -* Setup your Selenium Grid and update your *.cfg file to point there. An example config file called selenium_server_config_example.cfg has been provided for you in the grid folder. The start-selenium-node.bat and start-selenium-server.sh files are for running your grid. In an example situation, your Selenium Grid server might live on a unix box and your Selenium Grid nodes might live on EC2 Windows virtual machines. When your build server runs a Selenium test, it would connect to your Selenium Grid to find out which Grid browser nodes are available to run that test. To simplify things, you can just use [Browser Stack](https://www.browserstack.com/automate) as your entire Selenium Grid (and let them do all the fun work of maintaining the grid for you). +* Setup your Selenium Grid and update your *.cfg file to point there. An example config file called selenium_server_config_example.cfg has been provided for you in the grid folder. The start-selenium-node.bat and start-selenium-server.sh files are for running your grid. In an example situation, your Selenium Grid server might live on a unix box and your Selenium Grid nodes might live on EC2 Windows virtual machines. When your build server runs a Selenium test, it would connect to your Selenium Grid to find out which Grid browser nodes are available to run that test. To simplify things, you can use [Browser Stack](https://www.browserstack.com/automate) as your entire Selenium Grid (and let them do all the fun work of maintaining the grid for you). * There are ways of running your tests from Jenkins without having to utilize a remote machine. One way is by using PhantomJS as your browser (it runs headlessly). Another way is by using Xvfb (another headless system). [There's a plugin for Xvfb in Jenkins](https://wiki.jenkins-ci.org/display/JENKINS/Xvfb+Plugin). Here are some more helpful resources I found regarding the use of Xvfb: 1. http://stackoverflow.com/questions/6183276/how-do-i-run-selenium-in-xvfb @@ -258,7 +258,7 @@ nosetests [YOUR_TEST_FILE].py --browser=chrome --with-selenium --with-testing_ba ``` (When the testing_base plugin is used, if there's a test failure, the basic_test_info plugin records test logs, the page_source plugin records the page source of the last web page seen by the test, and the screen_shots plugin records the image of the last page seen by the test where the failure occurred. Make sure you always include testing_base whenever you include a plugin that logs test data. The db_reporting plugin records the status of all tests as long as you've setup your MySQL DB properly and you've also updated your test_framework/core/mysql_conf.py file with your DB credentials.) -To simplify that long run command, you can create a *.cfg file, such as the one provided in the example, and enter your plugins there so that you can run everything just by typing: +To simplify that long run command, you can create a *.cfg file, such as the one provided in the example, and enter your plugins there so that you can run everything by typing: ```bash nosetests [YOUR_TEST_FILE].py --config=[MY_CONFIG_FILE].cfg -s @@ -298,7 +298,7 @@ Have you made it this far? Congratulations!!! Now you're ready to dive in at ful #### Navigating to a Page, Plus Some Other Useful Related Commands ```python -self.driver.get("https://xkcd.com/378/") # Instant navigation to any web page - just specify the url. +self.driver.get("https://xkcd.com/378/") # Instant navigation to any web page. self.driver.refresh() # refresh/reload the current page. @@ -424,7 +424,7 @@ self.wait_for_element_visible("textarea").send_keys(Keys.SPACE + Keys.BACK_SPACE #### Switching Tabs -What if your test opens up a new tab/window and now you have more than one page? No problem. You just need to specify which one you currently want Selenium to use. Switching between tabs/windows is easy: +What if your test opens up a new tab/window and now you have more than one page? No problem. You need to specify which one you currently want Selenium to use. Switching between tabs/windows is easy: Ex: ```python @@ -445,7 +445,7 @@ self.driver.switch_to_default_content() # exit the iFrame when you're done #### Handle Pop-Up Alerts -What if your test makes an alert pop up in your browser? No problem. You just need to switch to it and either accept it or dismiss it: +What if your test makes an alert pop up in your browser? No problem. You need to switch to it and either accept it or dismiss it: Ex: ```python @@ -501,7 +501,7 @@ self.driver.find_element_by_css_selector("a.analytics").click() # Clicks the ge ## Part III: Explanations + Advanced Abilities -So by now you may be wondering how the nosetests code works? Nosetests will automatically run any test that starts with "test" from the file you selected. You can also be more specific and run specific tests in a file or any test in a specific class. For example, the code in the early examples could've been run using "nosetests my_first_test.py:MyTestClass.test_basic ... ...". If you just wanted to run all tests in MyTestClass, you can use: "nosetests my_first_test.py:MyTestClass ... ...", which is useful when you have multiple tests in the same file. Don't forget the plugins. (In the beginning example, since there was only one test in that file, this won't change anything.) And if you want better logging in the console output, that's what the "-s" is for. +So by now you may be wondering how the nosetests code works? Nosetests will automatically run any test that starts with "test" from the file you selected. You can also be more specific and run specific tests in a file or any test in a specific class. For example, the code in the early examples could've been run using "nosetests my_first_test.py:MyTestClass.test_basic ... ...". If you wanted to run all tests in MyTestClass, you can use: "nosetests my_first_test.py:MyTestClass ... ...", which is useful when you have multiple tests in the same file. Don't forget the plugins. (In the beginning example, since there was only one test in that file, this won't change anything.) And if you want better logging in the console output, that's what the "-s" is for. To use the test framework calls, don't forget to include the following import: @@ -515,7 +515,7 @@ And you'll need to inherit BaseCase in your classes like so: class MyTestClass(BaseCase): ``` -To understand the full scope of the test framework, we have to take a peek inside. From the top-level folder that contained the requirements.pip and setup.py files, there are two other major folders: "grid" and "test_framework". The Selenium "Grid" is what maintains the remote machines running selenium tests for "selenium.hubteam.com/jenkins". Machines can be spun up through Amazon EC2, and each one is capable of running 5 simultaneous browser tests. The other major folder, "test_framework", is what contains everything else. The "test_framework" folder contains all the major components such as "Core", "Fixtures", and "Plugins". For all intensive purposes, those sections are all equally important. They contain all the code and libraries that make our test framework useful (because otherwise we'd just be writing tests using raw selenium calls without any special add-ons or support). +To understand the full scope of the test framework, we have to take a peek inside. From the top-level folder that contained the requirements.pip and setup.py files, there are two other major folders: "grid" and "test_framework". The Selenium "Grid" is what maintains the remote machines running selenium tests for "selenium.hubteam.com/jenkins". Machines can be spun up through Amazon EC2, and each one is capable of running 5 simultaneous browser tests. The other major folder, "test_framework", is what contains everything else. The "test_framework" folder contains all the major components such as "Core", "Fixtures", and "Plugins". For all intensive purposes, those sections are all equally important. They contain all the code and libraries that make our test framework useful (because otherwise we'd be writing tests using raw selenium calls without any special add-ons or support). #### Checking Email: @@ -591,4 +591,4 @@ Happy Automating! ### Legal Disclaimer -Automation is a powerful tool. It allows you to take full control of web browsers and do just about anything that a human could do, but faster. It can be used for both good and evil. With great power comes great responsibility. You are fully responsible for how you use this framework and the automation that you create. You may also want to see an expert when it comes to setting up your automation environment if you require assistance. +Automation is a powerful tool. It allows you to take full control of web browsers and do almost anything that a human could do, but faster. It can be used for both good and evil. With great power comes great responsibility. You are fully responsible for how you use this framework and the automation that you create. You may also want to see an expert when it comes to setting up your automation environment if you require assistance. From 3e9d18f7cb2f4500657e5ac944b1d6e18d3422eb Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 26 Jul 2015 21:42:40 -0400 Subject: [PATCH 096/219] Time to use those marketing skills. --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3041b11554b4..5a031c852320 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # SeleniumSpot Test Framework -"The best way of creating test automation for your web apps." +### Build, Run, Verify + +Here's what users are saying: +* "The best way of creating test automation for your web apps." +* "It's like a [Docker](https://www.docker.com/) for automated tests." +* "Our team was able to start writing & running tests immediately." Features include: * MySQL DB integration for storing test data and results From e28072e518a4798fba0000bfa21f7e13b9f9007b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 26 Jul 2015 22:10:44 -0400 Subject: [PATCH 097/219] Build, Automate, Verify --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a031c852320..48813c31b7e2 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SeleniumSpot Test Framework -### Build, Run, Verify +### Build, Automate, Verify Here's what users are saying: * "The best way of creating test automation for your web apps." From 6c5cc379777a41ba98f853d2dae87d14bd44735b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 26 Jul 2015 22:49:47 -0400 Subject: [PATCH 098/219] The download alternative to git clone --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 48813c31b7e2..bfea2ed91ed0 100755 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ If you're a WINDOWS user, [download the latest 2.* version from here](https://ww [Homebrew](http://brew.sh/) + [Git](http://git-scm.com/) +(NOTE: You can download the SeleniumSpot repository right from GitHub and skip all the git-related commands. That's probably the fastest way if you want to quickly get a live demo of this tool up and running.) + ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew install git brew update @@ -84,17 +86,16 @@ To save time from having to source virtualenvwrapper again when you open a new w If you haven't already, you'll want to [Download Firefox](https://www.mozilla.org/en-US/firefox/new/) and either [Download Chrome](https://www.google.com/chrome/browser/desktop/index.html) or [Download Chromium](https://download-chromium.appspot.com/). -**Step 1:** Checkout the SeleniumSpot Test Framework with Git or a Git GUI tool: +**Step 1:** Download or Clone SeleniumSpot to your local machine: -First you'll want to fork the repository on GitHub to create your own copy. This is important because you'll want to add your own configurations, credentials, settings, etc. Now clone your forked SeleniumSpot repository to your development machine. You can use a tool such as [SourceTree](http://www.sourcetreeapp.com/) to make things easier by providing you with a simple-to-use user interface for viewing and managing your git commits and status. +If you're using Git, you can fork the repository on GitHub to create your personal copy. This is important because you'll want to add your own configurations, credentials, settings, etc. Now you can clone your forked copy to your personal computer. You can use a tool such as [SourceTree](http://www.sourcetreeapp.com/) to make things easier by providing you with a simple-to-use user interface for viewing and managing your git commits and status. ```bash -git clone [LOCATION OF YOUR FORKED SELENIUMSPOT GITHUB FOLDER]/SeleniumSpot.git -cd SeleniumSpot +git clone [LOCATION OF YOUR FORKED SELENIUMSPOT GITHUB FOLDER]/seleniumspot.git +cd seleniumspot ``` -(NOTE: You can also download the SeleniumSpot repository right from GitHub and skip all the git-related commands. That's probably the fastest way if you want to quickly get a live demo of this tool up and running.) - +(NOTE: If you decided to download SeleniumSpot rather than Git-cloning it, you can skip the above step.) **Step 2:** Create a virtualenv for seleniumspot: From b7555dfa4c4ffebc59c5db1f274dca4fe5d58538 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 28 Jul 2015 11:24:05 -0400 Subject: [PATCH 099/219] Some documentation updates. --- README.md | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index bfea2ed91ed0..c52724253459 100755 --- a/README.md +++ b/README.md @@ -472,44 +472,44 @@ You'd know this because the web page would contain something like the following ``` -It's OK if you want to use jQuery on a page that doesn't have it loaded yet. To do so, you need to run the following command first: +It's OK if you want to use jQuery on a page that doesn't have it loaded yet. To do so, run the following command first: ```python -self.driver.execute_script('var script = document.createElement("script"); script.src = "https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"; document.getElementsByTagName("head")[0].appendChild(script);') +self.activate_jquery() ``` -Here are some examples: +Here are some examples of using jQuery in your scripts: ```python -self.driver.execute_script('jQuery, window.scrollTo(0, 600)') # Scrolling the page +self.execute_script('jQuery, window.scrollTo(0, 600)') # Scrolling the page -self.driver.execute_script("jQuery('#annoying-widget').hide()") # Hiding elements on a page +self.execute_script("jQuery('#annoying-widget').hide()") # Hiding elements on a page -self.driver.execute_script("jQuery('#annoying-button a').remove()") # Removing elements on a page +self.execute_script("jQuery('#annoying-button a').remove()") # Removing elements on a page -self.driver.execute_script("jQuery('%s').mouseover()" % (mouse_over_item)) # Mouse-over elements on a page +self.execute_script("jQuery('%s').mouseover()" % (mouse_over_item)) # Mouse-over elements on a page -self.driver.execute_script("jQuery('input#the_id').val('my_text')") # Fast text input on a page +self.execute_script("jQuery('input#the_id').val('my_text')") # Fast text input on a page -self.driver.execute_script("jQuery('div#dropdown a.link').click()") # Click elements on a page +self.execute_script("jQuery('div#dropdown a.link').click()") # Click elements on a page -self.driver.execute_script("return jQuery('div#amazing')[0].text") # Returns the css "text" of the element given +self.execute_script("return jQuery('div#amazing')[0].text") # Returns the css "text" of the element given -self.driver.execute_script("return jQuery('textarea')[2].value") # Returns the css "value" of the 3rd textarea element on the page +self.execute_script("return jQuery('textarea')[2].value") # Returns the css "value" of the 3rd textarea element on the page ``` In the following more-complex example, jQuery is used to plant code on a page that Selenium can then touch after that: ```python self.driver.get(SOME_PAGE_TO_PLAY_WITH) referral_link = 'Free-Referral Button!' % DESTINATION_URL -self.driver.execute_script("document.body.innerHTML = \"%s\"" % referral_link) -self.driver.find_element_by_css_selector("a.analytics").click() # Clicks the generated button +self.execute_script("document.body.innerHTML = \"%s\"" % referral_link) +self.click("a.analytics") # Clicks the generated button ``` -## Part III: Explanations + Advanced Abilities +## Part III: More Details -So by now you may be wondering how the nosetests code works? Nosetests will automatically run any test that starts with "test" from the file you selected. You can also be more specific and run specific tests in a file or any test in a specific class. For example, the code in the early examples could've been run using "nosetests my_first_test.py:MyTestClass.test_basic ... ...". If you wanted to run all tests in MyTestClass, you can use: "nosetests my_first_test.py:MyTestClass ... ...", which is useful when you have multiple tests in the same file. Don't forget the plugins. (In the beginning example, since there was only one test in that file, this won't change anything.) And if you want better logging in the console output, that's what the "-s" is for. +Nosetests automatically runs any python method that starts with "test" from the file you selected. You can also select specific tests to run from files or classes. For example, the code in the early examples could've been run using "nosetests my_first_test.py:MyTestClass.test_basic ... ...". If you wanted to run all tests in MyTestClass, you can use: "nosetests my_first_test.py:MyTestClass ... ...", which is useful when you have multiple tests in the same file. Don't forget the plugins. Use "-s" if you want better logging in the console output. -To use the test framework calls, don't forget to include the following import: +To use the SeleniumSpot Test Framework calls, don't forget to include the following import: ```python from test_framework import BaseCase @@ -521,11 +521,8 @@ And you'll need to inherit BaseCase in your classes like so: class MyTestClass(BaseCase): ``` -To understand the full scope of the test framework, we have to take a peek inside. From the top-level folder that contained the requirements.pip and setup.py files, there are two other major folders: "grid" and "test_framework". The Selenium "Grid" is what maintains the remote machines running selenium tests for "selenium.hubteam.com/jenkins". Machines can be spun up through Amazon EC2, and each one is capable of running 5 simultaneous browser tests. The other major folder, "test_framework", is what contains everything else. The "test_framework" folder contains all the major components such as "Core", "Fixtures", and "Plugins". For all intensive purposes, those sections are all equally important. They contain all the code and libraries that make our test framework useful (because otherwise we'd be writing tests using raw selenium calls without any special add-ons or support). - - #### Checking Email: -So let's say you have a test that sends an email, and now you want to check that the email was received: +Let's say you have a test that sends an email, and now you want to check that the email was received: ```python from test_framework.fixtures.email_manager import EmailManager, EmailException @@ -544,8 +541,7 @@ Now you can parse through the email if you're looking for specific text or want #### Database Powers: -Let's say you have a test that needs to access the database. First make sure you already have a table ready. Then, Boom: -Ex: +Let's say you have a test that needs to access the database. First make sure you already have a table ready. Then try this example: ```python from test_framework.core.mysql import DatabaseManager @@ -582,7 +578,7 @@ def get_delayed_test_data(self, testcase_address, done=0): return [] ``` -And now you know how to pull data from the DB. +Now you know how to pull data from your MySQL DB. You may also be wondering when you would use the Delayed Data Manager. Here's one example: If you scheduled an email to go out 12 hours from now and you wanted to check that the email gets received (but you don't want the Selenium test of a Jenkins job to sit idle for 12 hours) you can store the email credentials as a unique time-stamp for the email subject in the DB (along with a time for when it's safe for the email to be searched for) and then a later-running test can do the checking after the right amount of time has passed. From 437c44a5f79da60c336d5982cdff7b933cde375d Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 28 Jul 2015 11:24:57 -0400 Subject: [PATCH 100/219] More documentation updates. --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c52724253459..8b8c922a66c3 100755 --- a/README.md +++ b/README.md @@ -19,10 +19,7 @@ Features include: * Advanced commands for saving you significant time -To utilize some of the more advanced integrations, you'll want to setup instances and make connections to the following: -MySQL, Jenkins, Amazon S3, and the Selenium Grid. (More on this later) -We've provided placeholders in the code where you can specify your connection details. You can also use this framework as a bare-bones Selenium WebDriver command executer to automate tasks in a browser without doing any data reporting (and that's also the fastest way to make sure your base setup is working properly). -If you plan on running tests from a build server across multiple cloud machines, you can connect to your own Selenium Grid or use a cloud provider such as BrowserStack. +To utilize some of the more advanced integrations, you'll want to setup instances and make connections to MySQL, Jenkins, Amazon S3, and the Selenium Grid. We've provided placeholders in the code where you can specify your connection details for those (see settings.py in the test_framework/config folder). You can also use this framework as a bare-bones Selenium WebDriver command executer to automate tasks in a browser without doing any data reporting (and that's also the fastest way to make sure your base setup is working properly). If you plan on running tests from a build server across multiple cloud machines, you can connect to your own Selenium Grid or use a cloud provider such as BrowserStack. For an excellent example of all the pieces coming together, check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot). @@ -51,7 +48,7 @@ If you're a WINDOWS user, [download the latest 2.* version from here](https://ww [MySQL](http://www.mysql.com/) -(NOTE: If you're using this test framework from a local development machine and don't plan on writing to the MySQL DB from your local test runs, you can skip this step.) +(NOTE: If you're using this test framework from a local development machine and don't plan on writing to a MySQL DB from your local test runs, you can skip this step.) brew install MySQL From e3096f9244607789383e7cd52f1be82da0d5b077 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 30 Jul 2015 11:54:45 -0400 Subject: [PATCH 101/219] Make sure that only integers are passed into range() --- test_framework/fixtures/page_loads.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test_framework/fixtures/page_loads.py b/test_framework/fixtures/page_loads.py index 7e472f784ac1..4e0afb22af36 100755 --- a/test_framework/fixtures/page_loads.py +++ b/test_framework/fixtures/page_loads.py @@ -42,7 +42,7 @@ def wait_for_element_present(driver, selector, by=By.CSS_SELECTOR, timeout=setti """ element = None - for x in range(timeout * 10): + for x in range(int(timeout * 10)): try: element = driver.find_element(by=by, value=selector) return element @@ -70,7 +70,7 @@ def wait_for_element_visible(driver, selector, by=By.CSS_SELECTOR, timeout=setti """ element = None - for x in range(timeout * 10): + for x in range(int(timeout * 10)): try: element = driver.find_element(by=by, value=selector) if element.is_displayed(): @@ -102,7 +102,7 @@ def wait_for_text_visible(driver, text, selector, by=By.CSS_SELECTOR, timeout=se """ element = None - for x in range(timeout * 10): + for x in range(int(timeout * 10)): try: element = driver.find_element(by=by, value=selector) if element.is_displayed(): @@ -129,7 +129,7 @@ def wait_for_element_absent(driver, selector, by=By.CSS_SELECTOR, timeout=settin timeout - the time to wait for elements in seconds """ - for x in range(timeout * 10): + for x in range(int(timeout * 10)): try: driver.find_element(by=by, value=selector) time.sleep(0.1) @@ -152,7 +152,7 @@ def wait_for_element_not_visible(driver, selector, by=By.CSS_SELECTOR, timeout=s timeout - the time to wait for elements in seconds """ - for x in range(timeout * 10): + for x in range(int(timeout * 10)): try: element = driver.find_element(by=by, value=selector) if element.is_displayed(): @@ -173,7 +173,7 @@ def wait_for_ready_state_complete(driver, timeout=settings.EXTREME_TIMEOUT): This method will wait until document.readyState == "complete". """ - for x in range(timeout * 10): + for x in range(int(timeout * 10)): ready_state = driver.execute_script("return document.readyState") if ready_state == u'complete': return True @@ -219,7 +219,7 @@ def wait_for_and_switch_to_alert(driver, timeout=settings.LARGE_TIMEOUT): timeout - the time to wait for the alert in seconds """ - for x in range(timeout * 10): + for x in range(int(timeout * 10)): try: alert = driver.switch_to_alert() return alert From 3ba20ef7e447dc16ca9ce7b423d3caf3c0f11dd1 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 3 Aug 2015 15:53:19 -0400 Subject: [PATCH 102/219] Rebranding SeleniumSpot as SeleniumBase --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8b8c922a66c3..28995d249948 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# SeleniumSpot Test Framework +# SeleniumBase Test Framework ### Build, Automate, Verify @@ -38,7 +38,7 @@ If you're a WINDOWS user, [download the latest 2.* version from here](https://ww [Homebrew](http://brew.sh/) + [Git](http://git-scm.com/) -(NOTE: You can download the SeleniumSpot repository right from GitHub and skip all the git-related commands. That's probably the fastest way if you want to quickly get a live demo of this tool up and running.) +(NOTE: You can download the SeleniumBase repository right from GitHub and skip all the git-related commands. That's probably the fastest way if you want to quickly get a live demo of this tool up and running.) ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew install git @@ -83,21 +83,21 @@ To save time from having to source virtualenvwrapper again when you open a new w If you haven't already, you'll want to [Download Firefox](https://www.mozilla.org/en-US/firefox/new/) and either [Download Chrome](https://www.google.com/chrome/browser/desktop/index.html) or [Download Chromium](https://download-chromium.appspot.com/). -**Step 1:** Download or Clone SeleniumSpot to your local machine: +**Step 1:** Download or Clone SeleniumBase to your local machine: If you're using Git, you can fork the repository on GitHub to create your personal copy. This is important because you'll want to add your own configurations, credentials, settings, etc. Now you can clone your forked copy to your personal computer. You can use a tool such as [SourceTree](http://www.sourcetreeapp.com/) to make things easier by providing you with a simple-to-use user interface for viewing and managing your git commits and status. ```bash -git clone [LOCATION OF YOUR FORKED SELENIUMSPOT GITHUB FOLDER]/seleniumspot.git -cd seleniumspot +git clone [LOCATION OF YOUR FORKED SELENIUMBASE GITHUB FOLDER]/seleniumbase.git +cd seleniumbase ``` -(NOTE: If you decided to download SeleniumSpot rather than Git-cloning it, you can skip the above step.) +(NOTE: If you decided to download SeleniumBase rather than Git-cloning it, you can skip the above step.) -**Step 2:** Create a virtualenv for seleniumspot: +**Step 2:** Create a virtualenv for seleniumbase: ```bash -mkvirtualenv seleniumspot +mkvirtualenv seleniumbase ``` (Virtual environments are important because they allow you to have separate configurations from the rest of your system. This will prevent conflicts if you use other tools that require other configurations and settings.) @@ -111,7 +111,7 @@ deactivate To get back into your virtual environment, use the following command: ```bash -workon seleniumspot +workon seleniumbase ``` To see a list of environments that exist on your system, use the following command: @@ -126,7 +126,7 @@ To delete a virtual environment that you no longer need, use the following comma rmvirtualenv [NAME OF VIRTUAL ENV TO REMOVE] ``` -**Step 3:** Install necessary packages from the SeleniumSpot folder and compile the test framework +**Step 3:** Install necessary packages from the SeleniumBase folder and compile the test framework If you're NOT connecting to a MySQL DB from your local test runs (based on the path you chose above), use these steps: @@ -254,7 +254,7 @@ If you're planning on using the full power of this test framework, there are a f * If you use [HipChat](https://www.hipchat.com/), you can easily have your Jenkins jobs display results there by using the [Jenkins HipChat Plugin](https://wiki.jenkins-ci.org/display/JENKINS/HipChat+Plugin). Another way is by using the hipchat_reporting plugin, which is included with this test framework. -* Be sure to tell SeleniumSpot to use these added features when you set them up. That's easy to do. You would be running tests like this: +* Be sure to tell SeleniumBase to use these added features when you set them up. That's easy to do. You would be running tests like this: ```bash nosetests [YOUR_TEST_FILE].py --browser=chrome --with-selenium --with-testing_base --with-basic_test_info --with-page_source --with-screen_shots --with-db_reporting --with-s3_logging -s @@ -506,7 +506,7 @@ self.click("a.analytics") # Clicks the generated button Nosetests automatically runs any python method that starts with "test" from the file you selected. You can also select specific tests to run from files or classes. For example, the code in the early examples could've been run using "nosetests my_first_test.py:MyTestClass.test_basic ... ...". If you wanted to run all tests in MyTestClass, you can use: "nosetests my_first_test.py:MyTestClass ... ...", which is useful when you have multiple tests in the same file. Don't forget the plugins. Use "-s" if you want better logging in the console output. -To use the SeleniumSpot Test Framework calls, don't forget to include the following import: +To use the SeleniumBase Test Framework calls, don't forget to include the following import: ```python from test_framework import BaseCase From 461b56846f58474344e1ce20a92c135af8529eda Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 3 Aug 2015 16:14:18 -0400 Subject: [PATCH 103/219] More needed for the rebranding --- local_setup.py | 8 ++++---- setup.py | 8 ++++---- test_framework/plugins/selenium_plugin.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/local_setup.py b/local_setup.py index 09cdb5edc88f..b6a357988fd1 100755 --- a/local_setup.py +++ b/local_setup.py @@ -1,5 +1,5 @@ """ -The setup package to install the SeleniumSpot Test Framework plugins +The setup package to install the SeleniumBase Test Framework plugins on a development machine that DOES NOT intend to write to a MySQL DB during test runs. """ @@ -7,12 +7,12 @@ from setuptools import setup, find_packages setup( - name = 'seleniumspot', + name = 'seleniumbase', version = '1.1.1', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', - description = 'The SeleniumSpot Test Framework. (Powered by Python, WebDriver, and more...)', + description = 'The SeleniumBase Test Framework. (Powered by Python, WebDriver, and more...)', license = 'The MIT License', packages = ['test_framework', 'test_framework.core', @@ -23,7 +23,7 @@ entry_points = { 'nose.plugins': [ 'base_plugin = test_framework.plugins.base_plugin:Base', - 'selenium = test_framework.plugins.selenium_plugin:SeleniumBase', + 'selenium = test_framework.plugins.selenium_plugin:SeleniumBrowser', 'page_source = test_framework.plugins.page_source:PageSource', 'screen_shots = test_framework.plugins.screen_shots:ScreenShots', 'test_info = test_framework.plugins.basic_test_info:BasicTestInfo', diff --git a/setup.py b/setup.py index 62a2533e6d6c..fe2e7da5e4c8 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ """ -The setup package to install the SeleniumSpot Test Framework plugins +The setup package to install the SeleniumBase Test Framework plugins on a server machine (or a development machine that intends to write to a MySQL DB during test runs). """ @@ -7,12 +7,12 @@ from setuptools import setup, find_packages setup( - name = 'seleniumspot', + name = 'seleniumbase', version = '1.1.1', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', - description = 'The SeleniumSpot Test Framework. (Powered by Python, WebDriver, and more...)', + description = 'The SeleniumBase Test Framework. (Powered by Python, WebDriver, and more...)', license = 'The MIT License', packages = ['test_framework', 'test_framework.core', @@ -23,7 +23,7 @@ entry_points = { 'nose.plugins': [ 'base_plugin = test_framework.plugins.base_plugin:Base', - 'selenium = test_framework.plugins.selenium_plugin:SeleniumBase', + 'selenium = test_framework.plugins.selenium_plugin:SeleniumBrowser', 'page_source = test_framework.plugins.page_source:PageSource', 'screen_shots = test_framework.plugins.screen_shots:ScreenShots', 'test_info = test_framework.plugins.basic_test_info:BasicTestInfo', diff --git a/test_framework/plugins/selenium_plugin.py b/test_framework/plugins/selenium_plugin.py index 377bf1e73af7..7fa0d03310ba 100755 --- a/test_framework/plugins/selenium_plugin.py +++ b/test_framework/plugins/selenium_plugin.py @@ -11,7 +11,7 @@ from test_framework.fixtures import constants -class SeleniumBase(Plugin): +class SeleniumBrowser(Plugin): """ The plugin for Selenium tests. Takes in key arguments and then creates a WebDriver object. All arguments are passed to the tests. @@ -24,7 +24,7 @@ class SeleniumBase(Plugin): name = 'selenium' # Usage: --with-selenium def options(self, parser, env): - super(SeleniumBase, self).options(parser, env=env) + super(SeleniumBrowser, self).options(parser, env=env) parser.add_option('--browser', action='store', dest='browser', @@ -46,7 +46,7 @@ def options(self, parser, env): def configure(self, options, conf): - super(SeleniumBase, self).configure(options, conf) + super(SeleniumBrowser, self).configure(options, conf) if not self.enabled: return From 0653bed478732aa601f26cde792f3c3ac0c769ed Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 3 Aug 2015 16:59:02 -0400 Subject: [PATCH 104/219] Using the latest selenium server, 2.47.0 --- grid_files/ReadMe.txt | 4 ++-- grid_files/start-selenium-node.bat | 2 +- grid_files/start-selenium-server.sh | 2 +- local_requirements.pip | 2 +- requirements.pip | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/grid_files/ReadMe.txt b/grid_files/ReadMe.txt index 9b2f55aefcab..141776917d2d 100644 --- a/grid_files/ReadMe.txt +++ b/grid_files/ReadMe.txt @@ -1,4 +1,4 @@ -You may need to download selenium-server-standalone-2.46.1.jar (or the latest version) separately. +You may need to download selenium-server-standalone-2.47.0.jar (or the latest version) separately. That file is not present with this repository to save space. -You can download that file from here: https://pypi.python.org/pypi/selenium/2.46.1 +You can download that file from here: https://pypi.python.org/pypi/selenium/2.47.0 Once you have downloaded the jar file, put it in this folder. diff --git a/grid_files/start-selenium-node.bat b/grid_files/start-selenium-node.bat index 9121587a1b8d..d1cd20679ded 100644 --- a/grid_files/start-selenium-node.bat +++ b/grid_files/start-selenium-node.bat @@ -1,2 +1,2 @@ cd c:\ -java -jar selenium-server-standalone-2.46.1.jar -port 5555 -host [ENTER HOST OF THIS WORKER MACHINE ex: ec2-***-**-**-***.compute-1.amazonaws.com] -browser browserName=chrome,maxInstances=5 \ No newline at end of file +java -jar selenium-server-standalone-2.47.0.jar -port 5555 -host [ENTER HOST OF THIS WORKER MACHINE ex: ec2-***-**-**-***.compute-1.amazonaws.com] -browser browserName=chrome,maxInstances=5 \ No newline at end of file diff --git a/grid_files/start-selenium-server.sh b/grid_files/start-selenium-server.sh index 86821efd2139..b860ae0dea73 100755 --- a/grid_files/start-selenium-server.sh +++ b/grid_files/start-selenium-server.sh @@ -1,2 +1,2 @@ #!/bin/bash -screen -S grid-server java -jar selenium-server-standalone-2.46.1.jar -role hub -host [ENTER YOUR MASTER SELENIUM SERVER HOST HERE] -port 4444 \ No newline at end of file +screen -S grid-server java -jar selenium-server-standalone-2.47.0.jar -role hub -host [ENTER YOUR MASTER SELENIUM SERVER HOST HERE] -port 4444 \ No newline at end of file diff --git a/local_requirements.pip b/local_requirements.pip index 87cf48b839ec..93049e151856 100755 --- a/local_requirements.pip +++ b/local_requirements.pip @@ -1,4 +1,4 @@ -selenium==2.46.1 +selenium==2.47.0 nose==1.3.7 requests==2.7.0 urllib3==1.10.4 diff --git a/requirements.pip b/requirements.pip index 60a5036cdb00..be35838358bf 100755 --- a/requirements.pip +++ b/requirements.pip @@ -1,4 +1,4 @@ -selenium==2.46.1 +selenium==2.47.0 nose==1.3.7 requests==2.7.0 urllib3==1.10.4 From b97f4542221bcd83deebaf79d35bc2e4601f87bd Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 3 Aug 2015 17:28:06 -0400 Subject: [PATCH 105/219] Give correct location of selenium-server-standalone --- grid_files/ReadMe.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid_files/ReadMe.txt b/grid_files/ReadMe.txt index 141776917d2d..d1ff7ce25252 100644 --- a/grid_files/ReadMe.txt +++ b/grid_files/ReadMe.txt @@ -1,4 +1,4 @@ You may need to download selenium-server-standalone-2.47.0.jar (or the latest version) separately. That file is not present with this repository to save space. -You can download that file from here: https://pypi.python.org/pypi/selenium/2.47.0 +You can download that file from here: http://selenium-release.storage.googleapis.com/index.html?path=2.47/ Once you have downloaded the jar file, put it in this folder. From 4cfbe9d2ee56bd15e5c0f45d90b9822f6284de52 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 3 Aug 2015 17:34:12 -0400 Subject: [PATCH 106/219] Time for another version update. Now 1.1.2 --- local_setup.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/local_setup.py b/local_setup.py index b6a357988fd1..04ebadc0e014 100755 --- a/local_setup.py +++ b/local_setup.py @@ -8,7 +8,7 @@ setup( name = 'seleniumbase', - version = '1.1.1', + version = '1.1.2', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', diff --git a/setup.py b/setup.py index fe2e7da5e4c8..7d32d00e5829 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name = 'seleniumbase', - version = '1.1.1', + version = '1.1.2', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', From 3ea3f6e56fa4fb6962e87316552f62ca740dfcdc Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 3 Aug 2015 18:19:18 -0400 Subject: [PATCH 107/219] Update grid info. --- grid_files/ReadMe.txt | 2 ++ grid_files/start-selenium-node.bat | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/grid_files/ReadMe.txt b/grid_files/ReadMe.txt index d1ff7ce25252..87ecee2a9588 100644 --- a/grid_files/ReadMe.txt +++ b/grid_files/ReadMe.txt @@ -2,3 +2,5 @@ You may need to download selenium-server-standalone-2.47.0.jar (or the latest ve That file is not present with this repository to save space. You can download that file from here: http://selenium-release.storage.googleapis.com/index.html?path=2.47/ Once you have downloaded the jar file, put it in this folder. + +More detailed info can be found here: https://theintern.github.io/intern/#selenium-grid diff --git a/grid_files/start-selenium-node.bat b/grid_files/start-selenium-node.bat index d1cd20679ded..4b840bc6b060 100644 --- a/grid_files/start-selenium-node.bat +++ b/grid_files/start-selenium-node.bat @@ -1,2 +1,2 @@ cd c:\ -java -jar selenium-server-standalone-2.47.0.jar -port 5555 -host [ENTER HOST OF THIS WORKER MACHINE ex: ec2-***-**-**-***.compute-1.amazonaws.com] -browser browserName=chrome,maxInstances=5 \ No newline at end of file +java -jar selenium-server-standalone-2.47.0.jar -port 5555 -host [ENTER HOST OF THIS WORKER MACHINE ex: ec2-***-**-**-***.compute-1.amazonaws.com] -hub http://[ENTER URL OF THE GRID HUB SERVER]:4444/grid/register -browser browserName=chrome,maxInstances=5 \ No newline at end of file From 011f615a934ee951c0b3412e48aaa85ebe3308e3 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 3 Aug 2015 18:22:23 -0400 Subject: [PATCH 108/219] Another URL to get the selenium server. --- grid_files/ReadMe.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid_files/ReadMe.txt b/grid_files/ReadMe.txt index 87ecee2a9588..9e80af4c94e5 100644 --- a/grid_files/ReadMe.txt +++ b/grid_files/ReadMe.txt @@ -1,6 +1,6 @@ You may need to download selenium-server-standalone-2.47.0.jar (or the latest version) separately. That file is not present with this repository to save space. -You can download that file from here: http://selenium-release.storage.googleapis.com/index.html?path=2.47/ +You can download that file from here: http://docs.seleniumhq.org/download/ or here: http://selenium-release.storage.googleapis.com/index.html?path=2.47/ Once you have downloaded the jar file, put it in this folder. More detailed info can be found here: https://theintern.github.io/intern/#selenium-grid From 8701fc8f29ce85fd710c88d25b646129bbbb14d6 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 4 Aug 2015 17:31:07 -0400 Subject: [PATCH 109/219] More detailed Selenium Grid Hub readme --- grid_files/ReadMe.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/grid_files/ReadMe.txt b/grid_files/ReadMe.txt index 9e80af4c94e5..4a19793d31b7 100644 --- a/grid_files/ReadMe.txt +++ b/grid_files/ReadMe.txt @@ -3,4 +3,5 @@ That file is not present with this repository to save space. You can download that file from here: http://docs.seleniumhq.org/download/ or here: http://selenium-release.storage.googleapis.com/index.html?path=2.47/ Once you have downloaded the jar file, put it in this folder. -More detailed info can be found here: https://theintern.github.io/intern/#selenium-grid +More detailed info about connecting to the Selenium Grid Hub can be found here: https://theintern.github.io/intern/#selenium-grid and here: https://github.com/SeleniumHQ/selenium/wiki/Grid2 +For even more information, try here: https://github.com/SeleniumHQ/selenium/wiki From b2ff3395328a3006f8a22a9ef219abc7e7c49f21 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 4 Aug 2015 17:42:19 -0400 Subject: [PATCH 110/219] Simplify the Selenium node-to-grid code --- grid_files/start-selenium-node.bat | 2 +- grid_files/start-selenium-server.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/grid_files/start-selenium-node.bat b/grid_files/start-selenium-node.bat index 4b840bc6b060..001ab1300882 100644 --- a/grid_files/start-selenium-node.bat +++ b/grid_files/start-selenium-node.bat @@ -1,2 +1,2 @@ cd c:\ -java -jar selenium-server-standalone-2.47.0.jar -port 5555 -host [ENTER HOST OF THIS WORKER MACHINE ex: ec2-***-**-**-***.compute-1.amazonaws.com] -hub http://[ENTER URL OF THE GRID HUB SERVER]:4444/grid/register -browser browserName=chrome,maxInstances=5 \ No newline at end of file +java -jar selenium-server-standalone-2.47.0.jar -role node -hub http://[ENTER URL OF THE GRID HUB SERVER]:4444/grid/register -browser browserName=chrome,maxInstances=5 \ No newline at end of file diff --git a/grid_files/start-selenium-server.sh b/grid_files/start-selenium-server.sh index b860ae0dea73..b6f1cc2a095d 100755 --- a/grid_files/start-selenium-server.sh +++ b/grid_files/start-selenium-server.sh @@ -1,2 +1,2 @@ #!/bin/bash -screen -S grid-server java -jar selenium-server-standalone-2.47.0.jar -role hub -host [ENTER YOUR MASTER SELENIUM SERVER HOST HERE] -port 4444 \ No newline at end of file +java -jar selenium-server-standalone-2.47.0.jar -role hub \ No newline at end of file From 7d4190f93523f3e59945288d4eaed7e3b98b19df Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 4 Aug 2015 17:47:27 -0400 Subject: [PATCH 111/219] Use the newer version of the selenium-server-standalone --- grid_files/ReadMe.txt | 2 +- grid_files/start-selenium-node.bat | 2 +- grid_files/start-selenium-server.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/grid_files/ReadMe.txt b/grid_files/ReadMe.txt index 4a19793d31b7..e07c9b89dd8d 100644 --- a/grid_files/ReadMe.txt +++ b/grid_files/ReadMe.txt @@ -1,4 +1,4 @@ -You may need to download selenium-server-standalone-2.47.0.jar (or the latest version) separately. +You may need to download selenium-server-standalone-2.47.1.jar (or the latest version) separately. That file is not present with this repository to save space. You can download that file from here: http://docs.seleniumhq.org/download/ or here: http://selenium-release.storage.googleapis.com/index.html?path=2.47/ Once you have downloaded the jar file, put it in this folder. diff --git a/grid_files/start-selenium-node.bat b/grid_files/start-selenium-node.bat index 001ab1300882..168e56b4359a 100644 --- a/grid_files/start-selenium-node.bat +++ b/grid_files/start-selenium-node.bat @@ -1,2 +1,2 @@ cd c:\ -java -jar selenium-server-standalone-2.47.0.jar -role node -hub http://[ENTER URL OF THE GRID HUB SERVER]:4444/grid/register -browser browserName=chrome,maxInstances=5 \ No newline at end of file +java -jar selenium-server-standalone-2.47.1.jar -role node -hub http://[ENTER URL OF THE GRID HUB SERVER]:4444/grid/register -browser browserName=chrome,maxInstances=5 \ No newline at end of file diff --git a/grid_files/start-selenium-server.sh b/grid_files/start-selenium-server.sh index b6f1cc2a095d..122b6f405bc2 100755 --- a/grid_files/start-selenium-server.sh +++ b/grid_files/start-selenium-server.sh @@ -1,2 +1,2 @@ #!/bin/bash -java -jar selenium-server-standalone-2.47.0.jar -role hub \ No newline at end of file +java -jar selenium-server-standalone-2.47.1.jar -role hub \ No newline at end of file From 5fa321f315c5d4b05c3957a80221c7f04065b085 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 5 Aug 2015 14:30:00 -0400 Subject: [PATCH 112/219] Update default timeouts. --- test_framework/config/settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test_framework/config/settings.py b/test_framework/config/settings.py index 07b2c52e0509..35f58384cf4e 100755 --- a/test_framework/config/settings.py +++ b/test_framework/config/settings.py @@ -6,9 +6,9 @@ #####>>>>>----- REQUIRED SETTINGS -----<<<<<##### # Default times to wait for page elements to appear before performing actions -SMALL_TIMEOUT = 7 -LARGE_TIMEOUT = 14 -EXTREME_TIMEOUT = 42 # "Bueller? ... Bueller?" - (Ferris Bueller's Day Off, 1986) +SMALL_TIMEOUT = 5 +LARGE_TIMEOUT = 10 +EXTREME_TIMEOUT = 30 # The option of adding wait_for_ready_state_complete() after various actions. # By default, Selenium waits for the 'interactive' state, which might not be enough. From 260d6b3a083bcfc38ae645dc142ee1bc12347d7b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 5 Aug 2015 15:09:39 -0400 Subject: [PATCH 113/219] "test_framework" is now "seleniumbase" --- README.md | 20 ++++++------- examples/my_first_test.py | 2 +- examples/rate_limiting_test.py | 4 +-- examples/test_fail.py | 2 +- local_setup.py | 26 ++++++++--------- seleniumbase/__init__.py | 1 + .../common/__init__.py | 0 .../common/decorators.py | 0 .../config/__init__.py | 0 .../config/settings.py | 0 .../core/__init__.py | 0 .../core/application_manager.py | 0 .../core/mysql.py | 0 .../core/mysql_conf.py | 2 +- .../core/s3_manager.py | 2 +- .../core/selenium_launcher.py | 0 .../core/testcase_manager.py | 2 +- .../core/testcaserepository.sql | 0 .../core/web_driver_retry.py | 0 .../fixtures/__init__.py | 0 .../fixtures/base_case.py | 2 +- .../fixtures/constants.py | 0 .../fixtures/delayed_data_manager.py | 2 +- .../fixtures/email_manager.py | 2 +- .../fixtures/errors.py | 0 .../fixtures/page_interactions.py | 2 +- .../fixtures/page_loads.py | 2 +- .../fixtures/page_utils.py | 0 seleniumbase/fixtures/tools.py | 9 ++++++ .../plugins/__init__.py | 0 .../plugins/base_plugin.py | 2 +- .../plugins/basic_test_info.py | 0 .../plugins/db_reporting_plugin.py | 12 ++++---- .../plugins/hipchat_reporting_plugin.py | 2 +- .../plugins/page_source.py | 0 .../plugins/s3_logging_plugin.py | 4 +-- .../plugins/screen_shots.py | 0 .../plugins/selenium_plugin.py | 4 +-- setup.py | 28 +++++++++---------- test_framework/__init__.py | 1 - test_framework/fixtures/tools.py | 9 ------ 41 files changed, 71 insertions(+), 71 deletions(-) create mode 100755 seleniumbase/__init__.py rename {test_framework => seleniumbase}/common/__init__.py (100%) rename {test_framework => seleniumbase}/common/decorators.py (100%) rename {test_framework => seleniumbase}/config/__init__.py (100%) rename {test_framework => seleniumbase}/config/settings.py (100%) rename {test_framework => seleniumbase}/core/__init__.py (100%) rename {test_framework => seleniumbase}/core/application_manager.py (100%) rename {test_framework => seleniumbase}/core/mysql.py (100%) rename {test_framework => seleniumbase}/core/mysql_conf.py (88%) rename {test_framework => seleniumbase}/core/s3_manager.py (98%) rename {test_framework => seleniumbase}/core/selenium_launcher.py (100%) rename {test_framework => seleniumbase}/core/testcase_manager.py (98%) rename {test_framework => seleniumbase}/core/testcaserepository.sql (100%) rename {test_framework => seleniumbase}/core/web_driver_retry.py (100%) rename {test_framework => seleniumbase}/fixtures/__init__.py (100%) rename {test_framework => seleniumbase}/fixtures/base_case.py (99%) rename {test_framework => seleniumbase}/fixtures/constants.py (100%) rename {test_framework => seleniumbase}/fixtures/delayed_data_manager.py (99%) rename {test_framework => seleniumbase}/fixtures/email_manager.py (99%) rename {test_framework => seleniumbase}/fixtures/errors.py (100%) rename {test_framework => seleniumbase}/fixtures/page_interactions.py (99%) rename {test_framework => seleniumbase}/fixtures/page_loads.py (99%) rename {test_framework => seleniumbase}/fixtures/page_utils.py (100%) create mode 100755 seleniumbase/fixtures/tools.py rename {test_framework => seleniumbase}/plugins/__init__.py (100%) rename {test_framework => seleniumbase}/plugins/base_plugin.py (98%) rename {test_framework => seleniumbase}/plugins/basic_test_info.py (100%) rename {test_framework => seleniumbase}/plugins/db_reporting_plugin.py (93%) rename {test_framework => seleniumbase}/plugins/hipchat_reporting_plugin.py (99%) rename {test_framework => seleniumbase}/plugins/page_source.py (100%) rename {test_framework => seleniumbase}/plugins/s3_logging_plugin.py (94%) rename {test_framework => seleniumbase}/plugins/screen_shots.py (100%) rename {test_framework => seleniumbase}/plugins/selenium_plugin.py (98%) delete mode 100755 test_framework/__init__.py delete mode 100755 test_framework/fixtures/tools.py diff --git a/README.md b/README.md index 28995d249948..bbabb8cedb5b 100755 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Features include: * Advanced commands for saving you significant time -To utilize some of the more advanced integrations, you'll want to setup instances and make connections to MySQL, Jenkins, Amazon S3, and the Selenium Grid. We've provided placeholders in the code where you can specify your connection details for those (see settings.py in the test_framework/config folder). You can also use this framework as a bare-bones Selenium WebDriver command executer to automate tasks in a browser without doing any data reporting (and that's also the fastest way to make sure your base setup is working properly). If you plan on running tests from a build server across multiple cloud machines, you can connect to your own Selenium Grid or use a cloud provider such as BrowserStack. +To utilize some of the more advanced integrations, you'll want to setup instances and make connections to MySQL, Jenkins, Amazon S3, and the Selenium Grid. We've provided placeholders in the code where you can specify your connection details for those (see settings.py in the seleniumbase/config folder). You can also use this framework as a bare-bones Selenium WebDriver command executer to automate tasks in a browser without doing any data reporting (and that's also the fastest way to make sure your base setup is working properly). If you plan on running tests from a build server across multiple cloud machines, you can connect to your own Selenium Grid or use a cloud provider such as BrowserStack. For an excellent example of all the pieces coming together, check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot). @@ -52,7 +52,7 @@ If you're a WINDOWS user, [download the latest 2.* version from here](https://ww brew install MySQL -That installs the MySQL library so that you can use db commands in your code. To make that useful, you'll want to have a MySQL DB that you can connect to. You'll also want to use the testcaserepository.sql file from the test_framework/core folder to add the necessary tables. +That installs the MySQL library so that you can use db commands in your code. To make that useful, you'll want to have a MySQL DB that you can connect to. You'll also want to use the testcaserepository.sql file from the seleniumbase/core folder to add the necessary tables. (WINDOWS users: [Download MySQL here](http://dev.mysql.com/downloads/windows/). If you want a visual tool to help make your MySQL life easier, [try MySQL Workbench](http://dev.mysql.com/downloads/workbench/).) @@ -178,7 +178,7 @@ python **Step 5:** Now to verify the test framework installation by writing a simple Selenium script that performs basic actions such as navigating to a web page, clicking, waiting for page elements to appear, typing in text, scraping text on a page, and verifying text. (copy/paste this into a new file called "my_first_test.py"). This may be a good time to read up on css selectors. If you use Chrome, you can right-click on a page and select "Inspect Element" to see the details you need to create such a script. At a quick glance, dots are for class names and pound signs are for IDs. ```python -from test_framework import BaseCase +from seleniumbase import BaseCase class MyTestClass(BaseCase): @@ -239,7 +239,7 @@ If you're planning on using the full power of this test framework, there are a f * Setup your [Jenkins](http://jenkins-ci.org/) build server for running your tests at regular intervals. (Or you can use any build server you want.) -* Setup an [Amazon S3](http://aws.amazon.com/s3/) account for saving your log files and screenshots for future viewing. This test framework already has the code you need to connect to it. (Modify the s3_manager.py file from the test_framework/core folder with connection details to your instance.) +* Setup an [Amazon S3](http://aws.amazon.com/s3/) account for saving your log files and screenshots for future viewing. This test framework already has the code you need to connect to it. (Modify the s3_manager.py file from the seleniumbase/core folder with connection details to your instance.) * Install [MySQL Workbench](http://dev.mysql.com/downloads/tools/workbench/) to make life easier by giving you a nice GUI tool that you can use to read & write from your DB directly. @@ -260,7 +260,7 @@ If you're planning on using the full power of this test framework, there are a f nosetests [YOUR_TEST_FILE].py --browser=chrome --with-selenium --with-testing_base --with-basic_test_info --with-page_source --with-screen_shots --with-db_reporting --with-s3_logging -s ``` -(When the testing_base plugin is used, if there's a test failure, the basic_test_info plugin records test logs, the page_source plugin records the page source of the last web page seen by the test, and the screen_shots plugin records the image of the last page seen by the test where the failure occurred. Make sure you always include testing_base whenever you include a plugin that logs test data. The db_reporting plugin records the status of all tests as long as you've setup your MySQL DB properly and you've also updated your test_framework/core/mysql_conf.py file with your DB credentials.) +(When the testing_base plugin is used, if there's a test failure, the basic_test_info plugin records test logs, the page_source plugin records the page source of the last web page seen by the test, and the screen_shots plugin records the image of the last page seen by the test where the failure occurred. Make sure you always include testing_base whenever you include a plugin that logs test data. The db_reporting plugin records the status of all tests as long as you've setup your MySQL DB properly and you've also updated your seleniumbase/core/mysql_conf.py file with your DB credentials.) To simplify that long run command, you can create a *.cfg file, such as the one provided in the example, and enter your plugins there so that you can run everything by typing: ```bash @@ -277,7 +277,7 @@ nosetests [YOUR_TEST_FILE].py:[SOME_CLASS_NAME].test_[SOME_TEST_NAME] --config=[ Let's try an example of a test that fails. Copy the following into a file called fail_test.py: ```python """ test_fail.py """ -from test_framework import BaseCase +from seleniumbase import BaseCase class MyTestClass(BaseCase): @@ -509,7 +509,7 @@ Nosetests automatically runs any python method that starts with "test" from the To use the SeleniumBase Test Framework calls, don't forget to include the following import: ```python -from test_framework import BaseCase +from seleniumbase import BaseCase ``` And you'll need to inherit BaseCase in your classes like so: @@ -522,7 +522,7 @@ class MyTestClass(BaseCase): Let's say you have a test that sends an email, and now you want to check that the email was received: ```python -from test_framework.fixtures.email_manager import EmailManager, EmailException +from seleniumbase.fixtures.email_manager import EmailManager, EmailException num_email_results = 0 email_subject = "This is the subject to search for (maybe include a timestamp)" email_manager = EmailManager("[YOUR SELENIUM GMAIL EMAIL ADDRESS]") # the password for this is elsewhere (in the library) because this is a default email account @@ -541,7 +541,7 @@ Now you can parse through the email if you're looking for specific text or want Let's say you have a test that needs to access the database. First make sure you already have a table ready. Then try this example: ```python -from test_framework.core.mysql import DatabaseManager +from seleniumbase.core.mysql import DatabaseManager def write_data_to_db(self, theId, theValue, theUrl): db = DatabaseManager() query = """INSERT INTO myTable(theId,theValue,theUrl) @@ -557,7 +557,7 @@ The following example below (taken from the Delayed Data Manager) shows how data ```python import logging -from test_framework.core.mysql import DatabaseManager +from seleniumbase.core.mysql import DatabaseManager def get_delayed_test_data(self, testcase_address, done=0): """ Returns a list of rows """ diff --git a/examples/my_first_test.py b/examples/my_first_test.py index d4d8db0cd135..b89cc240b985 100644 --- a/examples/my_first_test.py +++ b/examples/my_first_test.py @@ -1,4 +1,4 @@ -from test_framework import BaseCase +from seleniumbase import BaseCase class MyTestClass(BaseCase): diff --git a/examples/rate_limiting_test.py b/examples/rate_limiting_test.py index bd0a9db728d1..19d4c422edf2 100644 --- a/examples/rate_limiting_test.py +++ b/examples/rate_limiting_test.py @@ -1,5 +1,5 @@ -from test_framework import BaseCase -from test_framework.common import decorators +from seleniumbase import BaseCase +from seleniumbase.common import decorators class MyTestClass(BaseCase): diff --git a/examples/test_fail.py b/examples/test_fail.py index feeb12e3504b..9ac5b950344d 100644 --- a/examples/test_fail.py +++ b/examples/test_fail.py @@ -1,5 +1,5 @@ """ test_fail.py """ -from test_framework.fixtures import base_case +from seleniumbase.fixtures import base_case class MyTestClass(base_case.BaseCase): diff --git a/local_setup.py b/local_setup.py index 04ebadc0e014..c25e2e7e8723 100755 --- a/local_setup.py +++ b/local_setup.py @@ -14,21 +14,21 @@ maintainer = 'Michael Mintz', description = 'The SeleniumBase Test Framework. (Powered by Python, WebDriver, and more...)', license = 'The MIT License', - packages = ['test_framework', - 'test_framework.core', - 'test_framework.plugins', - 'test_framework.fixtures', - 'test_framework.common', - 'test_framework.config'], + packages = ['seleniumbase', + 'seleniumbase.core', + 'seleniumbase.plugins', + 'seleniumbase.fixtures', + 'seleniumbase.common', + 'seleniumbase.config'], entry_points = { 'nose.plugins': [ - 'base_plugin = test_framework.plugins.base_plugin:Base', - 'selenium = test_framework.plugins.selenium_plugin:SeleniumBrowser', - 'page_source = test_framework.plugins.page_source:PageSource', - 'screen_shots = test_framework.plugins.screen_shots:ScreenShots', - 'test_info = test_framework.plugins.basic_test_info:BasicTestInfo', - 's3_logging = test_framework.plugins.s3_logging_plugin:S3Logging', - 'hipchat_reporting = test_framework.plugins.hipchat_reporting_plugin:HipchatReporting', + 'base_plugin = seleniumbase.plugins.base_plugin:Base', + 'selenium = seleniumbase.plugins.selenium_plugin:SeleniumBrowser', + 'page_source = seleniumbase.plugins.page_source:PageSource', + 'screen_shots = seleniumbase.plugins.screen_shots:ScreenShots', + 'test_info = seleniumbase.plugins.basic_test_info:BasicTestInfo', + 's3_logging = seleniumbase.plugins.s3_logging_plugin:S3Logging', + 'hipchat_reporting = seleniumbase.plugins.hipchat_reporting_plugin:HipchatReporting', ] } ) diff --git a/seleniumbase/__init__.py b/seleniumbase/__init__.py new file mode 100755 index 000000000000..f21fc4a5eb70 --- /dev/null +++ b/seleniumbase/__init__.py @@ -0,0 +1 @@ +from seleniumbase.fixtures.base_case import BaseCase diff --git a/test_framework/common/__init__.py b/seleniumbase/common/__init__.py similarity index 100% rename from test_framework/common/__init__.py rename to seleniumbase/common/__init__.py diff --git a/test_framework/common/decorators.py b/seleniumbase/common/decorators.py similarity index 100% rename from test_framework/common/decorators.py rename to seleniumbase/common/decorators.py diff --git a/test_framework/config/__init__.py b/seleniumbase/config/__init__.py similarity index 100% rename from test_framework/config/__init__.py rename to seleniumbase/config/__init__.py diff --git a/test_framework/config/settings.py b/seleniumbase/config/settings.py similarity index 100% rename from test_framework/config/settings.py rename to seleniumbase/config/settings.py diff --git a/test_framework/core/__init__.py b/seleniumbase/core/__init__.py similarity index 100% rename from test_framework/core/__init__.py rename to seleniumbase/core/__init__.py diff --git a/test_framework/core/application_manager.py b/seleniumbase/core/application_manager.py similarity index 100% rename from test_framework/core/application_manager.py rename to seleniumbase/core/application_manager.py diff --git a/test_framework/core/mysql.py b/seleniumbase/core/mysql.py similarity index 100% rename from test_framework/core/mysql.py rename to seleniumbase/core/mysql.py diff --git a/test_framework/core/mysql_conf.py b/seleniumbase/core/mysql_conf.py similarity index 88% rename from test_framework/core/mysql_conf.py rename to seleniumbase/core/mysql_conf.py index 041bf28f7398..1ba5c0e7fde5 100755 --- a/test_framework/core/mysql_conf.py +++ b/seleniumbase/core/mysql_conf.py @@ -2,7 +2,7 @@ This file contains database credentials for the various databases the tests need to access """ -from test_framework.config import settings +from seleniumbase.config import settings # Environments TEST = "test" diff --git a/test_framework/core/s3_manager.py b/seleniumbase/core/s3_manager.py similarity index 98% rename from test_framework/core/s3_manager.py rename to seleniumbase/core/s3_manager.py index 2579d7380154..2052cacf1e4a 100755 --- a/test_framework/core/s3_manager.py +++ b/seleniumbase/core/s3_manager.py @@ -3,7 +3,7 @@ """ from boto.s3.connection import S3Connection from boto.s3.key import Key -from test_framework.config import settings +from seleniumbase.config import settings already_uploaded_files = [] diff --git a/test_framework/core/selenium_launcher.py b/seleniumbase/core/selenium_launcher.py similarity index 100% rename from test_framework/core/selenium_launcher.py rename to seleniumbase/core/selenium_launcher.py diff --git a/test_framework/core/testcase_manager.py b/seleniumbase/core/testcase_manager.py similarity index 98% rename from test_framework/core/testcase_manager.py rename to seleniumbase/core/testcase_manager.py index a114370c5fe5..bfa75e5cb063 100755 --- a/test_framework/core/testcase_manager.py +++ b/seleniumbase/core/testcase_manager.py @@ -2,7 +2,7 @@ Testcase database related methods """ -from test_framework.core.mysql import DatabaseManager +from seleniumbase.core.mysql import DatabaseManager class TestcaseManager: """ diff --git a/test_framework/core/testcaserepository.sql b/seleniumbase/core/testcaserepository.sql similarity index 100% rename from test_framework/core/testcaserepository.sql rename to seleniumbase/core/testcaserepository.sql diff --git a/test_framework/core/web_driver_retry.py b/seleniumbase/core/web_driver_retry.py similarity index 100% rename from test_framework/core/web_driver_retry.py rename to seleniumbase/core/web_driver_retry.py diff --git a/test_framework/fixtures/__init__.py b/seleniumbase/fixtures/__init__.py similarity index 100% rename from test_framework/fixtures/__init__.py rename to seleniumbase/fixtures/__init__.py diff --git a/test_framework/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py similarity index 99% rename from test_framework/fixtures/base_case.py rename to seleniumbase/fixtures/base_case.py index 1f9146e11b43..c92b13364b68 100755 --- a/test_framework/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -2,7 +2,7 @@ import time import logging import unittest -from test_framework.config import settings +from seleniumbase.config import settings from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.common.by import By import page_loads, page_interactions, page_utils diff --git a/test_framework/fixtures/constants.py b/seleniumbase/fixtures/constants.py similarity index 100% rename from test_framework/fixtures/constants.py rename to seleniumbase/fixtures/constants.py diff --git a/test_framework/fixtures/delayed_data_manager.py b/seleniumbase/fixtures/delayed_data_manager.py similarity index 99% rename from test_framework/fixtures/delayed_data_manager.py rename to seleniumbase/fixtures/delayed_data_manager.py index 0b0b065cb7d3..c1fc71bc77ec 100755 --- a/test_framework/fixtures/delayed_data_manager.py +++ b/seleniumbase/fixtures/delayed_data_manager.py @@ -3,7 +3,7 @@ import time import uuid -from test_framework.core.mysql import DatabaseManager +from seleniumbase.core.mysql import DatabaseManager DEFAULT_EXPIRATION = 1000 * 60 * 60 * 48 diff --git a/test_framework/fixtures/email_manager.py b/seleniumbase/fixtures/email_manager.py similarity index 99% rename from test_framework/fixtures/email_manager.py rename to seleniumbase/fixtures/email_manager.py index 1a7d67edd2fa..9ff0090df0ad 100755 --- a/test_framework/fixtures/email_manager.py +++ b/seleniumbase/fixtures/email_manager.py @@ -8,7 +8,7 @@ import quopri import re import time -from test_framework.config import settings +from seleniumbase.config import settings class EmailManager: diff --git a/test_framework/fixtures/errors.py b/seleniumbase/fixtures/errors.py similarity index 100% rename from test_framework/fixtures/errors.py rename to seleniumbase/fixtures/errors.py diff --git a/test_framework/fixtures/page_interactions.py b/seleniumbase/fixtures/page_interactions.py similarity index 99% rename from test_framework/fixtures/page_interactions.py rename to seleniumbase/fixtures/page_interactions.py index d20d1e7aef39..c057d82f1535 100755 --- a/test_framework/fixtures/page_interactions.py +++ b/seleniumbase/fixtures/page_interactions.py @@ -18,7 +18,7 @@ """ import time -from test_framework.config import settings +from seleniumbase.config import settings from selenium.webdriver.common.by import By from selenium.webdriver.remote.errorhandler import ElementNotVisibleException from selenium.webdriver.remote.errorhandler import NoSuchElementException diff --git a/test_framework/fixtures/page_loads.py b/seleniumbase/fixtures/page_loads.py similarity index 99% rename from test_framework/fixtures/page_loads.py rename to seleniumbase/fixtures/page_loads.py index 4e0afb22af36..01d51e5e376e 100755 --- a/test_framework/fixtures/page_loads.py +++ b/seleniumbase/fixtures/page_loads.py @@ -18,7 +18,7 @@ """ import time -from test_framework.config import settings +from seleniumbase.config import settings from selenium.webdriver.common.by import By from selenium.webdriver.remote.errorhandler import ElementNotVisibleException, \ NoSuchElementException, \ diff --git a/test_framework/fixtures/page_utils.py b/seleniumbase/fixtures/page_utils.py similarity index 100% rename from test_framework/fixtures/page_utils.py rename to seleniumbase/fixtures/page_utils.py diff --git a/seleniumbase/fixtures/tools.py b/seleniumbase/fixtures/tools.py new file mode 100755 index 000000000000..086023dbfd17 --- /dev/null +++ b/seleniumbase/fixtures/tools.py @@ -0,0 +1,9 @@ +""" +This module imports all the commonly used fixtures in one place so that +every test doesn't need to import a number of different fixtures. +""" + +from seleniumbase.fixtures.page_loads import * +from seleniumbase.fixtures.page_interactions import * +from seleniumbase.fixtures.page_utils import * +from seleniumbase.fixtures.errors import * diff --git a/test_framework/plugins/__init__.py b/seleniumbase/plugins/__init__.py similarity index 100% rename from test_framework/plugins/__init__.py rename to seleniumbase/plugins/__init__.py diff --git a/test_framework/plugins/base_plugin.py b/seleniumbase/plugins/base_plugin.py similarity index 98% rename from test_framework/plugins/base_plugin.py rename to seleniumbase/plugins/base_plugin.py index f262fa1d0cb9..81774af51a8d 100755 --- a/test_framework/plugins/base_plugin.py +++ b/seleniumbase/plugins/base_plugin.py @@ -10,7 +10,7 @@ import time from nose.plugins import Plugin from nose.exc import SkipTest -from test_framework.fixtures import constants, errors +from seleniumbase.fixtures import constants, errors class Base(Plugin): diff --git a/test_framework/plugins/basic_test_info.py b/seleniumbase/plugins/basic_test_info.py similarity index 100% rename from test_framework/plugins/basic_test_info.py rename to seleniumbase/plugins/basic_test_info.py diff --git a/test_framework/plugins/db_reporting_plugin.py b/seleniumbase/plugins/db_reporting_plugin.py similarity index 93% rename from test_framework/plugins/db_reporting_plugin.py rename to seleniumbase/plugins/db_reporting_plugin.py index 8fde5e7f987e..aaf6c178336b 100755 --- a/test_framework/plugins/db_reporting_plugin.py +++ b/seleniumbase/plugins/db_reporting_plugin.py @@ -8,12 +8,12 @@ from optparse import SUPPRESS_HELP from nose.plugins import Plugin from nose.exc import SkipTest -from test_framework.core.application_manager import ApplicationManager -from test_framework.core.testcase_manager import ExecutionQueryPayload -from test_framework.core.testcase_manager import TestcaseDataPayload -from test_framework.core.testcase_manager import TestcaseManager -from test_framework.fixtures import constants -from test_framework.fixtures import errors +from seleniumbase.core.application_manager import ApplicationManager +from seleniumbase.core.testcase_manager import ExecutionQueryPayload +from seleniumbase.core.testcase_manager import TestcaseDataPayload +from seleniumbase.core.testcase_manager import TestcaseManager +from seleniumbase.fixtures import constants +from seleniumbase.fixtures import errors class DBReporting(Plugin): diff --git a/test_framework/plugins/hipchat_reporting_plugin.py b/seleniumbase/plugins/hipchat_reporting_plugin.py similarity index 99% rename from test_framework/plugins/hipchat_reporting_plugin.py rename to seleniumbase/plugins/hipchat_reporting_plugin.py index 62fa1ee52927..165cb34315d9 100755 --- a/test_framework/plugins/hipchat_reporting_plugin.py +++ b/seleniumbase/plugins/hipchat_reporting_plugin.py @@ -8,7 +8,7 @@ import logging import datetime from nose.plugins import Plugin -from test_framework.config import settings +from seleniumbase.config import settings HIPCHAT_URL = 'https://api.hipchat.com/v1/rooms/message' diff --git a/test_framework/plugins/page_source.py b/seleniumbase/plugins/page_source.py similarity index 100% rename from test_framework/plugins/page_source.py rename to seleniumbase/plugins/page_source.py diff --git a/test_framework/plugins/s3_logging_plugin.py b/seleniumbase/plugins/s3_logging_plugin.py similarity index 94% rename from test_framework/plugins/s3_logging_plugin.py rename to seleniumbase/plugins/s3_logging_plugin.py index d0a396ec902b..079f8f13d424 100755 --- a/test_framework/plugins/s3_logging_plugin.py +++ b/seleniumbase/plugins/s3_logging_plugin.py @@ -5,7 +5,7 @@ import uuid import logging import os -from test_framework.core.s3_manager import S3LoggingBucket +from seleniumbase.core.s3_manager import S3LoggingBucket from nose.plugins import Plugin @@ -43,7 +43,7 @@ def afterTest(self, test): # If the database plugin is running, attach a link to the logs index database row if hasattr(test.test, "testcase_guid"): - from test_framework.core.testcase_manager \ + from seleniumbase.core.testcase_manager \ import TestcaseDataPayload, TestcaseManager self.testcase_manager = TestcaseManager(self.options.database_env) data_payload = TestcaseDataPayload() diff --git a/test_framework/plugins/screen_shots.py b/seleniumbase/plugins/screen_shots.py similarity index 100% rename from test_framework/plugins/screen_shots.py rename to seleniumbase/plugins/screen_shots.py diff --git a/test_framework/plugins/selenium_plugin.py b/seleniumbase/plugins/selenium_plugin.py similarity index 98% rename from test_framework/plugins/selenium_plugin.py rename to seleniumbase/plugins/selenium_plugin.py index 7fa0d03310ba..87c37041995c 100755 --- a/test_framework/plugins/selenium_plugin.py +++ b/seleniumbase/plugins/selenium_plugin.py @@ -7,8 +7,8 @@ import os from nose.plugins import Plugin from selenium import webdriver -from test_framework.core import selenium_launcher -from test_framework.fixtures import constants +from seleniumbase.core import selenium_launcher +from seleniumbase.fixtures import constants class SeleniumBrowser(Plugin): diff --git a/setup.py b/setup.py index 7d32d00e5829..07ba9086c4b2 100755 --- a/setup.py +++ b/setup.py @@ -14,22 +14,22 @@ maintainer = 'Michael Mintz', description = 'The SeleniumBase Test Framework. (Powered by Python, WebDriver, and more...)', license = 'The MIT License', - packages = ['test_framework', - 'test_framework.core', - 'test_framework.plugins', - 'test_framework.fixtures', - 'test_framework.common', - 'test_framework.config'], + packages = ['seleniumbase', + 'seleniumbase.core', + 'seleniumbase.plugins', + 'seleniumbase.fixtures', + 'seleniumbase.common', + 'seleniumbase.config'], entry_points = { 'nose.plugins': [ - 'base_plugin = test_framework.plugins.base_plugin:Base', - 'selenium = test_framework.plugins.selenium_plugin:SeleniumBrowser', - 'page_source = test_framework.plugins.page_source:PageSource', - 'screen_shots = test_framework.plugins.screen_shots:ScreenShots', - 'test_info = test_framework.plugins.basic_test_info:BasicTestInfo', - 'db_reporting = test_framework.plugins.db_reporting_plugin:DBReporting', - 's3_logging = test_framework.plugins.s3_logging_plugin:S3Logging', - 'hipchat_reporting = test_framework.plugins.hipchat_reporting_plugin:HipchatReporting', + 'base_plugin = seleniumbase.plugins.base_plugin:Base', + 'selenium = seleniumbase.plugins.selenium_plugin:SeleniumBrowser', + 'page_source = seleniumbase.plugins.page_source:PageSource', + 'screen_shots = seleniumbase.plugins.screen_shots:ScreenShots', + 'test_info = seleniumbase.plugins.basic_test_info:BasicTestInfo', + 'db_reporting = seleniumbase.plugins.db_reporting_plugin:DBReporting', + 's3_logging = seleniumbase.plugins.s3_logging_plugin:S3Logging', + 'hipchat_reporting = seleniumbase.plugins.hipchat_reporting_plugin:HipchatReporting', ] } ) diff --git a/test_framework/__init__.py b/test_framework/__init__.py deleted file mode 100755 index 6f1182d9e7c6..000000000000 --- a/test_framework/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from test_framework.fixtures.base_case import BaseCase diff --git a/test_framework/fixtures/tools.py b/test_framework/fixtures/tools.py deleted file mode 100755 index 22bb093deeec..000000000000 --- a/test_framework/fixtures/tools.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -This module imports all the commonly used fixtures in one place so that -every test doesn't need to import a number of different fixtures. -""" - -from test_framework.fixtures.page_loads import * -from test_framework.fixtures.page_interactions import * -from test_framework.fixtures.page_utils import * -from test_framework.fixtures.errors import * From 57fba193bab81aa83745bdecc3ba9f2325215447 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 5 Aug 2015 15:26:56 -0400 Subject: [PATCH 114/219] All the browsers for grid nodes. --- grid_files/start-selenium-node.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid_files/start-selenium-node.bat b/grid_files/start-selenium-node.bat index 168e56b4359a..cf8de77fbdcc 100644 --- a/grid_files/start-selenium-node.bat +++ b/grid_files/start-selenium-node.bat @@ -1,2 +1,2 @@ cd c:\ -java -jar selenium-server-standalone-2.47.1.jar -role node -hub http://[ENTER URL OF THE GRID HUB SERVER]:4444/grid/register -browser browserName=chrome,maxInstances=5 \ No newline at end of file +java -jar selenium-server-standalone-2.47.1.jar -role node -hub http://[ENTER URL OF THE GRID HUB SERVER]:4444/grid/register -browser browserName=chrome,maxInstances=5 -browser browserName=firefox,maxInstances=5 -browser browserName="internet explorer",maxInstances=1 \ No newline at end of file From 0062ee2d720be79be997c34b49ec3c053cc59201 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 5 Aug 2015 15:33:03 -0400 Subject: [PATCH 115/219] Simplify call to driver.set_window_size(width, height) --- seleniumbase/fixtures/base_case.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index c92b13364b68..7b06ca9b39e1 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -77,6 +77,10 @@ def execute_script(self, script): return self.driver.execute_script(script) + def set_window_size(self, width, height): + return self.driver.set_window_size(width, height) + + def wait_for_link_text_visible(self, link_text, timeout=settings.LARGE_TIMEOUT): return self.wait_for_element_visible(link_text, by=By.LINK_TEXT, timeout=timeout) From 11031b61a8ea8e2989e48a7965e521192adaddaf Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 5 Aug 2015 17:28:57 -0400 Subject: [PATCH 116/219] One less screenshot, but keep a little delay. --- seleniumbase/plugins/screen_shots.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/seleniumbase/plugins/screen_shots.py b/seleniumbase/plugins/screen_shots.py index dd3a0d001d9a..603ba15ad699 100755 --- a/seleniumbase/plugins/screen_shots.py +++ b/seleniumbase/plugins/screen_shots.py @@ -37,16 +37,18 @@ def add_screenshot(self, test, err, capt=None, tbinfo=None): screenshot_file = "%s/%s" % (test_logpath, self.logfile_name) test.driver.get_screenshot_as_file(screenshot_file) try: + # Let humans see any errors on screen before closing the window test.driver.maximize_window() time.sleep(0.2) # Make sure the screen is ready except Exception: pass - screen_b64 = test.driver.get_screenshot_as_base64() + # Second screenshot at fullscreen might not be necessary + '''screen_b64 = test.driver.get_screenshot_as_base64() screen = base64.decodestring(screen_b64) screenshot_file_2 = "%s/%s" % (test_logpath, self.logfile_name_2) f1 = open(screenshot_file_2, 'w+') f1.write(screen) - f1.close() + f1.close()''' def addError(self, test, err, capt=None): From ba91ad4b339340d5a86e1aa1997c36cefa1e21dc Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 5 Aug 2015 17:51:01 -0400 Subject: [PATCH 117/219] Update method waiting for text visibility. --- seleniumbase/fixtures/page_loads.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/seleniumbase/fixtures/page_loads.py b/seleniumbase/fixtures/page_loads.py index 01d51e5e376e..d4feea391895 100755 --- a/seleniumbase/fixtures/page_loads.py +++ b/seleniumbase/fixtures/page_loads.py @@ -109,11 +109,12 @@ def wait_for_text_visible(driver, text, selector, by=By.CSS_SELECTOR, timeout=se if text in element.text: return element else: + element = None raise Exception() except Exception: time.sleep(0.1) if not element: - raise ElementNotVisibleException("Expected text [%s] for element %s was not visible in %s seconds!"\ + raise ElementNotVisibleException("Expected text [%s] for element [%s] was not visible in %s seconds!"\ % (text, selector, timeout)) From 1a6aa9814cc15d0a49fbe25317fd6015527f0cd9 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 5 Aug 2015 17:54:41 -0400 Subject: [PATCH 118/219] Simplify maximization of window. --- seleniumbase/fixtures/base_case.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 7b06ca9b39e1..cae98d574286 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -81,6 +81,10 @@ def set_window_size(self, width, height): return self.driver.set_window_size(width, height) + def maximize_window(self): + return self.driver.maximize_window() + + def wait_for_link_text_visible(self, link_text, timeout=settings.LARGE_TIMEOUT): return self.wait_for_element_visible(link_text, by=By.LINK_TEXT, timeout=timeout) From 7766fbf5a24ece4c147a347297c91b74dc654599 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 5 Aug 2015 18:17:52 -0400 Subject: [PATCH 119/219] Update Fail example --- README.md | 6 +++--- examples/test_fail.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index bbabb8cedb5b..68cba7457c77 100755 --- a/README.md +++ b/README.md @@ -281,9 +281,9 @@ from seleniumbase import BaseCase class MyTestClass(BaseCase): - def test_find_google_on_bing(self): - self.driver.get("http://bing.com") - self.wait_for_element_visible("div#google_is_here", timeout=3) # This should fail + def test_find_army_of_robots_on_xkcd_desert_island(self): + self.driver.get("http://xkcd.com/731/") + self.wait_for_element_visible("div#ARMY_OF_ROBOTS", timeout=3) # This should fail ``` Now run it: diff --git a/examples/test_fail.py b/examples/test_fail.py index 9ac5b950344d..d680d3580cd1 100644 --- a/examples/test_fail.py +++ b/examples/test_fail.py @@ -1,8 +1,8 @@ """ test_fail.py """ -from seleniumbase.fixtures import base_case +from seleniumbase import BaseCase -class MyTestClass(base_case.BaseCase): +class MyTestClass(BaseCase): - def test_find_google_on_bing(self): - self.driver.get("http://bing.com") - self.wait_for_element_visible("div#google_is_here", timeout=3) + def test_find_army_of_robots_on_xkcd_desert_island(self): + self.driver.get("http://xkcd.com/731/") + self.wait_for_element_visible("div#ARMY_OF_ROBOTS", timeout=3) From 323bd8326dd89d8335a9b88868d056cfae78a8cd Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 5 Aug 2015 18:39:11 -0400 Subject: [PATCH 120/219] A lot of changes means a new version update: Now 1.1.3 --- local_setup.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/local_setup.py b/local_setup.py index c25e2e7e8723..5c7bee04ab5c 100755 --- a/local_setup.py +++ b/local_setup.py @@ -8,7 +8,7 @@ setup( name = 'seleniumbase', - version = '1.1.2', + version = '1.1.3', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', diff --git a/setup.py b/setup.py index 07ba9086c4b2..879c5f39ffdc 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name = 'seleniumbase', - version = '1.1.2', + version = '1.1.3', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', From f513a47ffc72d34c608317451a923fa09b9f002a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 10 Aug 2015 13:22:46 -0400 Subject: [PATCH 121/219] Fix bug with waiting for an alert to appear. --- seleniumbase/fixtures/page_loads.py | 1 + 1 file changed, 1 insertion(+) diff --git a/seleniumbase/fixtures/page_loads.py b/seleniumbase/fixtures/page_loads.py index d4feea391895..ac8ba3ca5c47 100755 --- a/seleniumbase/fixtures/page_loads.py +++ b/seleniumbase/fixtures/page_loads.py @@ -223,6 +223,7 @@ def wait_for_and_switch_to_alert(driver, timeout=settings.LARGE_TIMEOUT): for x in range(int(timeout * 10)): try: alert = driver.switch_to_alert() + dummy_variable = alert.text # Raises exception if no alert present return alert except NoAlertPresentException: time.sleep(0.1) From a151732a751874182d570951f3d26205284aeb4d Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 10 Aug 2015 13:27:18 -0400 Subject: [PATCH 122/219] Up version to 1.1.4 --- local_setup.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/local_setup.py b/local_setup.py index 5c7bee04ab5c..3e21609d9d62 100755 --- a/local_setup.py +++ b/local_setup.py @@ -8,7 +8,7 @@ setup( name = 'seleniumbase', - version = '1.1.3', + version = '1.1.4', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', diff --git a/setup.py b/setup.py index 879c5f39ffdc..22df4c056494 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name = 'seleniumbase', - version = '1.1.3', + version = '1.1.4', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', From 7201940150b985f851247aa2e5cfaa523f075dcf Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 12 Aug 2015 10:18:11 -0400 Subject: [PATCH 123/219] Use the latest version of selenium (2.47.1) --- local_requirements.pip | 2 +- requirements.pip | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/local_requirements.pip b/local_requirements.pip index 93049e151856..a1e410ed022e 100755 --- a/local_requirements.pip +++ b/local_requirements.pip @@ -1,4 +1,4 @@ -selenium==2.47.0 +selenium==2.47.1 nose==1.3.7 requests==2.7.0 urllib3==1.10.4 diff --git a/requirements.pip b/requirements.pip index be35838358bf..d10f14092467 100755 --- a/requirements.pip +++ b/requirements.pip @@ -1,4 +1,4 @@ -selenium==2.47.0 +selenium==2.47.1 nose==1.3.7 requests==2.7.0 urllib3==1.10.4 From 6a68a6948bc96dc626ee178630415142c5d2d499 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 14 Aug 2015 14:11:59 -0400 Subject: [PATCH 124/219] Fix bug waiting for an element that's present but not visible. --- seleniumbase/fixtures/page_loads.py | 1 + 1 file changed, 1 insertion(+) diff --git a/seleniumbase/fixtures/page_loads.py b/seleniumbase/fixtures/page_loads.py index ac8ba3ca5c47..56233b07ae7f 100755 --- a/seleniumbase/fixtures/page_loads.py +++ b/seleniumbase/fixtures/page_loads.py @@ -76,6 +76,7 @@ def wait_for_element_visible(driver, selector, by=By.CSS_SELECTOR, timeout=setti if element.is_displayed(): return element else: + element = None raise Exception() except Exception: time.sleep(0.1) From 455b9e191cdb6b8bd5ebbfb126bd6f7cdd15c280 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 19 Aug 2015 12:10:38 -0400 Subject: [PATCH 125/219] A quick way to pass more data to tests from the command line. --- seleniumbase/plugins/base_plugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/seleniumbase/plugins/base_plugin.py b/seleniumbase/plugins/base_plugin.py index 81774af51a8d..a138ba166d78 100755 --- a/seleniumbase/plugins/base_plugin.py +++ b/seleniumbase/plugins/base_plugin.py @@ -34,6 +34,9 @@ def options(self, parser, env): constants.Environment.TEST), default=constants.Environment.TEST, help="The environment to run the tests in.") + parser.add_option('--data', dest='data', + default=None, + help='Extra data to pass from the command line.') parser.add_option('--log_path', dest='log_path', default='logs/', help='Where the log files are saved.') @@ -61,6 +64,7 @@ def beforeTest(self, test): if not os.path.exists(test_logpath): os.makedirs(test_logpath) test.test.environment = self.options.environment + test.test.data = self.options.data test.test.args = self.options From 62302d2003b34d909f5772eab3d1f32c69f596e3 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 19 Aug 2015 14:01:49 -0400 Subject: [PATCH 126/219] Update comments for base_plugin --- seleniumbase/plugins/base_plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/seleniumbase/plugins/base_plugin.py b/seleniumbase/plugins/base_plugin.py index a138ba166d78..e277e793c0b8 100755 --- a/seleniumbase/plugins/base_plugin.py +++ b/seleniumbase/plugins/base_plugin.py @@ -16,8 +16,9 @@ class Base(Plugin): """ The base_plugin includes the following variables: - self.options.env -- the environment you pass in (--env) - self.options.log_path -- the directory in which the log files are saved (--log_path) + self.options.env -- the environment for the tests to use (--env=ENV) + self.options.data -- any extra data to pass to the tests (--data=DATA) + self.options.log_path -- the directory in which the log files are saved (--log_path=LOG_PATH) """ name = 'testing_base' # Usage: --with-testing_base From 7f284ab563ee71001b6295ff9b370554efab9573 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 19 Aug 2015 14:17:42 -0400 Subject: [PATCH 127/219] Update Selenium Launcher --- seleniumbase/core/selenium_launcher.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/seleniumbase/core/selenium_launcher.py b/seleniumbase/core/selenium_launcher.py index f4808eb16d36..b850153257f8 100755 --- a/seleniumbase/core/selenium_launcher.py +++ b/seleniumbase/core/selenium_launcher.py @@ -1,4 +1,4 @@ -"""Download and run the selenium jar file""" +""" Download and run the selenium server jar file """ import subprocess import os @@ -6,12 +6,12 @@ import urllib import time -SELENIUM_JAR = "http://selenium.googlecode.com/files/selenium-server-standalone-2.40.0.jar" -JAR_FILE = "selenium-server-2.40.0.jar" +SELENIUM_JAR = "http://selenium-release.storage.googleapis.com/2.47/selenium-server-standalone-2.47.1.jar" +JAR_FILE = "selenium-server-standalone-2.47.1.jar" def download_selenium(): """ - downloads the selenium jar file from its online location and stores it locally + Downloads the selenium server jar file from its online location and stores it locally. """ try: local_file = open(JAR_FILE, 'wb') @@ -49,8 +49,8 @@ def start_selenium_server(selenium_jar_location, port, file_path): process_args = None process_args = ["java", "-jar", selenium_jar_location, "-port", port] selenium_exec = subprocess.Popen(process_args, - stdout=open("%s/log_seleniumOutput.txt"%(file_path),"w"), - stderr=open("%s/log_seleniumError.txt"%(file_path),"w")) + stdout=open("%s/log_seleniumOutput.txt" % (file_path),"w"), + stderr=open("%s/log_seleniumError.txt" % (file_path),"w")) time.sleep(2) if selenium_exec.poll() == 1: raise StartSeleniumException("The selenium server did not start." +\ @@ -65,7 +65,7 @@ def stop_selenium_server(selenium_server_process): selenium_server_process.terminate() return selenium_server_process.poll() == 143 except Exception, details: - raise Exception("Cannot kill selenium process, details: "+details) + raise Exception("Cannot kill selenium process, details: " + details) class StartSeleniumException(Exception): @@ -83,4 +83,4 @@ def execute_selenium(host, port, file_path): try: return start_selenium_server(JAR_FILE, port, file_path) except StartSeleniumException: - print "Selenium Server might already be running. Continuing" + print "Selenium Server might already be running. Continuing... " From fc70fda512b2ae2a8d7cc2785588636656bfb04e Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 19 Aug 2015 14:22:06 -0400 Subject: [PATCH 128/219] Update version to 1.1.5 --- local_setup.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/local_setup.py b/local_setup.py index 3e21609d9d62..80f362367539 100755 --- a/local_setup.py +++ b/local_setup.py @@ -8,7 +8,7 @@ setup( name = 'seleniumbase', - version = '1.1.4', + version = '1.1.5', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', diff --git a/setup.py b/setup.py index 22df4c056494..b6d46d8749d2 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name = 'seleniumbase', - version = '1.1.4', + version = '1.1.5', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', From 8b6ed517c90fb5fee0b376faf5988e4f3147f8da Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 21 Aug 2015 19:58:02 -0400 Subject: [PATCH 129/219] Add a version of setup.py that also installs the requirements. --- local_reqs_setup.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100755 local_reqs_setup.py diff --git a/local_reqs_setup.py b/local_reqs_setup.py new file mode 100755 index 000000000000..7f040e6e88eb --- /dev/null +++ b/local_reqs_setup.py @@ -0,0 +1,44 @@ +""" +The setup package to install the SeleniumBase Test Framework plugins +on a development machine that DOES NOT intend to write to +a MySQL DB during test runs. +(Includes requirements installation without pip.) +""" + +from setuptools import setup, find_packages + +setup( + name = 'seleniumbase', + version = '1.1.5', + author = 'Michael Mintz', + author_email = '@mintzworld', + maintainer = 'Michael Mintz', + description = 'The SeleniumBase Test Framework. (Powered by Python, WebDriver, and more...)', + license = 'The MIT License', + install_requires = ['selenium==2.47.1', + 'nose==1.3.7', + 'requests==2.7.0', + 'urllib3==1.10.4', + 'BeautifulSoup==3.2.1', + 'unittest2==1.1.0', + 'chardet==2.3.0', + 'simplejson==3.7.3', + 'boto==2.38.0'], + packages = ['seleniumbase', + 'seleniumbase.core', + 'seleniumbase.plugins', + 'seleniumbase.fixtures', + 'seleniumbase.common', + 'seleniumbase.config'], + entry_points = { + 'nose.plugins': [ + 'base_plugin = seleniumbase.plugins.base_plugin:Base', + 'selenium = seleniumbase.plugins.selenium_plugin:SeleniumBrowser', + 'page_source = seleniumbase.plugins.page_source:PageSource', + 'screen_shots = seleniumbase.plugins.screen_shots:ScreenShots', + 'test_info = seleniumbase.plugins.basic_test_info:BasicTestInfo', + 's3_logging = seleniumbase.plugins.s3_logging_plugin:S3Logging', + 'hipchat_reporting = seleniumbase.plugins.hipchat_reporting_plugin:HipchatReporting', + ] + } + ) From a2ff0daae972b60dc69653087bde24621167002c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 6 Sep 2015 11:15:17 -0400 Subject: [PATCH 130/219] Let scroll_to() wait if element is not immediately visible --- seleniumbase/fixtures/base_case.py | 1 + 1 file changed, 1 insertion(+) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index cae98d574286..45f53cf9e05b 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -102,6 +102,7 @@ def activate_jquery(self): def scroll_to(self, selector): + self.wait_for_element_visible(selector, timeout=settings.SMALL_TIMEOUT) self.driver.execute_script("jQuery('%s')[0].scrollIntoView()" % selector) From 541e38ba47fef1565633faacceaa6aff672ff17b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sat, 14 Nov 2015 21:11:15 -0500 Subject: [PATCH 131/219] Simplify requirements setup and use standard naming --- local_reqs_setup.py | 44 --------------------- local_requirements.pip => requirements.txt | 3 +- requirements.pip => server_requirements.txt | 3 +- local_setup.py => server_setup.py | 5 ++- setup.py | 5 +-- 5 files changed, 9 insertions(+), 51 deletions(-) delete mode 100755 local_reqs_setup.py rename local_requirements.pip => requirements.txt (87%) rename requirements.pip => server_requirements.txt (88%) rename local_setup.py => server_setup.py (87%) diff --git a/local_reqs_setup.py b/local_reqs_setup.py deleted file mode 100755 index 7f040e6e88eb..000000000000 --- a/local_reqs_setup.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -The setup package to install the SeleniumBase Test Framework plugins -on a development machine that DOES NOT intend to write to -a MySQL DB during test runs. -(Includes requirements installation without pip.) -""" - -from setuptools import setup, find_packages - -setup( - name = 'seleniumbase', - version = '1.1.5', - author = 'Michael Mintz', - author_email = '@mintzworld', - maintainer = 'Michael Mintz', - description = 'The SeleniumBase Test Framework. (Powered by Python, WebDriver, and more...)', - license = 'The MIT License', - install_requires = ['selenium==2.47.1', - 'nose==1.3.7', - 'requests==2.7.0', - 'urllib3==1.10.4', - 'BeautifulSoup==3.2.1', - 'unittest2==1.1.0', - 'chardet==2.3.0', - 'simplejson==3.7.3', - 'boto==2.38.0'], - packages = ['seleniumbase', - 'seleniumbase.core', - 'seleniumbase.plugins', - 'seleniumbase.fixtures', - 'seleniumbase.common', - 'seleniumbase.config'], - entry_points = { - 'nose.plugins': [ - 'base_plugin = seleniumbase.plugins.base_plugin:Base', - 'selenium = seleniumbase.plugins.selenium_plugin:SeleniumBrowser', - 'page_source = seleniumbase.plugins.page_source:PageSource', - 'screen_shots = seleniumbase.plugins.screen_shots:ScreenShots', - 'test_info = seleniumbase.plugins.basic_test_info:BasicTestInfo', - 's3_logging = seleniumbase.plugins.s3_logging_plugin:S3Logging', - 'hipchat_reporting = seleniumbase.plugins.hipchat_reporting_plugin:HipchatReporting', - ] - } - ) diff --git a/local_requirements.pip b/requirements.txt similarity index 87% rename from local_requirements.pip rename to requirements.txt index a1e410ed022e..b4a46348e2d5 100755 --- a/local_requirements.pip +++ b/requirements.txt @@ -1,4 +1,4 @@ -selenium==2.47.1 +selenium==2.48.0 nose==1.3.7 requests==2.7.0 urllib3==1.10.4 @@ -9,3 +9,4 @@ simplejson==3.7.3 boto==2.38.0 pdb==0.1 ipdb==0.8.1 +-e . diff --git a/requirements.pip b/server_requirements.txt similarity index 88% rename from requirements.pip rename to server_requirements.txt index d10f14092467..f7b7d02d72c4 100755 --- a/requirements.pip +++ b/server_requirements.txt @@ -1,4 +1,4 @@ -selenium==2.47.1 +selenium==2.48.0 nose==1.3.7 requests==2.7.0 urllib3==1.10.4 @@ -10,3 +10,4 @@ boto==2.38.0 MySQL-python==1.2.5 pdb==0.1 ipdb==0.8.1 +-e . diff --git a/local_setup.py b/server_setup.py similarity index 87% rename from local_setup.py rename to server_setup.py index 80f362367539..b6d46d8749d2 100755 --- a/local_setup.py +++ b/server_setup.py @@ -1,7 +1,7 @@ """ The setup package to install the SeleniumBase Test Framework plugins -on a development machine that DOES NOT intend to write to -a MySQL DB during test runs. +on a server machine (or a development machine that intends to write to +a MySQL DB during test runs). """ from setuptools import setup, find_packages @@ -27,6 +27,7 @@ 'page_source = seleniumbase.plugins.page_source:PageSource', 'screen_shots = seleniumbase.plugins.screen_shots:ScreenShots', 'test_info = seleniumbase.plugins.basic_test_info:BasicTestInfo', + 'db_reporting = seleniumbase.plugins.db_reporting_plugin:DBReporting', 's3_logging = seleniumbase.plugins.s3_logging_plugin:S3Logging', 'hipchat_reporting = seleniumbase.plugins.hipchat_reporting_plugin:HipchatReporting', ] diff --git a/setup.py b/setup.py index b6d46d8749d2..80f362367539 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ """ The setup package to install the SeleniumBase Test Framework plugins -on a server machine (or a development machine that intends to write to -a MySQL DB during test runs). +on a development machine that DOES NOT intend to write to +a MySQL DB during test runs. """ from setuptools import setup, find_packages @@ -27,7 +27,6 @@ 'page_source = seleniumbase.plugins.page_source:PageSource', 'screen_shots = seleniumbase.plugins.screen_shots:ScreenShots', 'test_info = seleniumbase.plugins.basic_test_info:BasicTestInfo', - 'db_reporting = seleniumbase.plugins.db_reporting_plugin:DBReporting', 's3_logging = seleniumbase.plugins.s3_logging_plugin:S3Logging', 'hipchat_reporting = seleniumbase.plugins.hipchat_reporting_plugin:HipchatReporting', ] From faf3ed8aaac33ca39f7f0239705c42a0376f1c0b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sat, 14 Nov 2015 21:12:30 -0500 Subject: [PATCH 132/219] A way to install pip without using pip --- pip_setup.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100755 pip_setup.py diff --git a/pip_setup.py b/pip_setup.py new file mode 100755 index 000000000000..e1ba24d5b0b7 --- /dev/null +++ b/pip_setup.py @@ -0,0 +1,16 @@ +""" +Install Pip by typing "python pip_setup.py install" +""" + +from setuptools import setup, find_packages + +setup( + name = 'pip', + version = '7.1.2', + author = 'Pip', + author_email = '@pip', + maintainer = 'Pip', + description = 'Install Pip by typing "python pip_setup.py install".', + license = 'Pip', + install_requires = ['pip==7.1.2'], + ) From 0ef37d338d5d34c341ab062c90adbc5907c76391 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sat, 14 Nov 2015 21:30:20 -0500 Subject: [PATCH 133/219] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 68cba7457c77..275c32465a78 100755 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ For an excellent example of all the pieces coming together, check out HubSpot's [Python 2.*](https://www.python.org/downloads/) -If you're a MAC user, that should already come preinstalled on your machine. Although Python 3 exists, you'll want Python 2 (both of these major versions are being improved in parallel). Python 2.7.6 is the one I've been using on my Mac. +If you're a MAC user, that should already come preinstalled on your machine. Although Python 3 exists, you'll want Python 2 (both of these major versions are being improved in parallel). Python 2.7.10 is the one I've been using on my Mac. -If you're a WINDOWS user, [download the latest 2.* version from here](https://www.python.org/downloads/release/python-2710/). +If you're a WINDOWS user, [download the latest 2.* version from here](https://www.python.org/downloads/release/python-2710/). Depending on which version of Python you have installed, you may need to install "pip" if your Python installation didn't come with it. To do that easily, run "python pip_setup.py install" from the SeleniumBase home directory. If that didn't do the trick for you, [get pip here](https://pip.pypa.io/en/latest/installing/). [Homebrew](http://brew.sh/) + [Git](http://git-scm.com/) @@ -131,15 +131,15 @@ rmvirtualenv [NAME OF VIRTUAL ENV TO REMOVE] If you're NOT connecting to a MySQL DB from your local test runs (based on the path you chose above), use these steps: ```bash -sudo pip install -r local_requirements.pip -sudo python local_setup.py install +sudo pip install -r requirements.txt +sudo python setup.py install ``` If you ARE connecting to a MySQL DB from your local test runs, use these steps: ```bash -sudo pip install -r requirements.pip -sudo python setup.py install +sudo pip install -r server_requirements.txt +sudo python server_setup.py install ``` NOTE: From 205c93c5f018f5d7916ca566745bc314fcf4fac8 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sat, 14 Nov 2015 21:35:46 -0500 Subject: [PATCH 134/219] Update version. --- 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 b6d46d8749d2..df7c7a4e3ccf 100755 --- a/server_setup.py +++ b/server_setup.py @@ -8,7 +8,7 @@ setup( name = 'seleniumbase', - version = '1.1.5', + version = '1.1.6', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', diff --git a/setup.py b/setup.py index 80f362367539..7c95894d3508 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name = 'seleniumbase', - version = '1.1.5', + version = '1.1.6', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', From 9864173375a61419d2ea9d04662a67d4396eac6e Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 15 Nov 2015 03:00:49 -0500 Subject: [PATCH 135/219] Add Docker support --- Dockerfile | 94 +++++++++++++++++++ docker/docker-entrypoint.sh | 4 + docker/docker_requirements.txt | 12 +++ docker/docker_setup.py | 34 +++++++ docker/docker_test.sh | 6 ++ .../plugins/docker_selenium_plugin.py | 85 +++++++++++++++++ 6 files changed, 235 insertions(+) create mode 100755 Dockerfile create mode 100755 docker/docker-entrypoint.sh create mode 100755 docker/docker_requirements.txt create mode 100755 docker/docker_setup.py create mode 100755 docker/docker_test.sh create mode 100755 seleniumbase/plugins/docker_selenium_plugin.py diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 000000000000..14a41b3a4403 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,94 @@ +# SeleniumBase Docker Image +FROM ubuntu:14.04 + +# Install Python and Basic Python Tools +RUN apt-get update && apt-get install -y python python-pip python-setuptools python-dev python-distribute + +#======================== +# Miscellaneous packages +# Includes minimal runtime used for executing selenium with firefox +#======================== +ENV BUILD_DEPS '\ + build-essential \ + libmysqlclient-dev \ + libpython-dev \ + libyaml-dev \ + libxml2-dev \ + libxslt1-dev \ + libxslt-dev \ + zlib1g-dev \ + ' + +RUN apt-get update -qqy \ + && apt-get -qy --no-install-recommends install \ + locales \ + language-pack-en \ + sudo \ + unzip \ + wget \ + curl \ + vim \ + xvfb \ + libaio1 \ + libxml2 \ + libxslt1.1 \ + mysql-client \ + ${BUILD_DEPS} \ + && rm -rf /var/lib/apt/lists/* + +#============================== +# Locale and encoding settings +#============================== +ENV LANGUAGE en_US.UTF-8 +ENV LANG ${LANGUAGE} +RUN locale-gen ${LANGUAGE} \ + && dpkg-reconfigure --frontend noninteractive locales + +#==================== +# Firefox Latest ESR +#==================== +RUN apt-get update -qqy \ + && apt-get -qy --no-install-recommends install \ + $(apt-cache depends firefox | grep Depends | sed "s/.*ends:\ //" | tr '\n' ' ') \ + && rm -rf /var/lib/apt/lists/* \ + && mkdir -p /tmp/ff \ + && wget -P /tmp/ff/ --no-check-certificate -r -l 1 -A bz2 -nH --cut-dirs=8 \ + https://ftp.mozilla.org/pub/mozilla.org/firefox/releases/latest-esr/linux-x86_64/en-US/ \ + && tar -xjf /tmp/ff/firefox-*esr.tar.bz2 -C /opt/ \ + && ln -s /opt/firefox/firefox /usr/bin/firefox \ + && rm -rf /tmp/ff/ + +#=================== +# Timezone settings +#=================== +# Full list at http://en.wikipedia.org/wiki/List_of_tz_database_time_zones +# e.g. "US/Pacific" for Los Angeles, California, USA +ENV TZ "America/New_York" +# Apply TimeZone +RUN echo $TZ | tee /etc/timezone \ + && dpkg-reconfigure --frontend noninteractive tzdata + +#======================================== +# Add normal user with passwordless sudo +#======================================== +RUN sudo useradd seluser --shell /bin/bash --create-home \ + && sudo usermod -a -G sudo seluser \ + && echo 'ALL ALL = (ALL) NOPASSWD: ALL' >> /etc/sudoers + +#===================== +# Set up SeleniumBase +#===================== +COPY docker/docker_requirements.txt /SeleniumBase/ +COPY docker/docker_setup.py /SeleniumBase/ +COPY seleniumbase /SeleniumBase/seleniumbase/ +COPY examples /SeleniumBase/examples/ +RUN cd /SeleniumBase && ls && sudo pip install -r docker_requirements.txt +RUN cd /SeleniumBase && ls && sudo python docker_setup.py install + +#========================================= +# Create entrypoint and grab example test +#========================================= +COPY docker/docker-entrypoint.sh / +ENTRYPOINT ["/docker-entrypoint.sh"] +COPY docker/docker_test.sh / +CMD ["/bin/bash"] diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100755 index 000000000000..b4756f920b72 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +echo "***** SeleniumBase Docker Machine *****" +exec "$@" diff --git a/docker/docker_requirements.txt b/docker/docker_requirements.txt new file mode 100755 index 000000000000..824d58ad3a98 --- /dev/null +++ b/docker/docker_requirements.txt @@ -0,0 +1,12 @@ +selenium==2.48.0 +nose==1.3.7 +requests==2.7.0 +urllib3==1.10.4 +BeautifulSoup==3.2.1 +unittest2==1.1.0 +chardet==2.3.0 +simplejson==3.7.3 +boto==2.38.0 +pdb==0.1 +ipdb==0.8.1 +pyvirtualdisplay==0.1.5 diff --git a/docker/docker_setup.py b/docker/docker_setup.py new file mode 100755 index 000000000000..e34486b54e81 --- /dev/null +++ b/docker/docker_setup.py @@ -0,0 +1,34 @@ +""" +The setup package to install the SeleniumBase Test Framework plugins +on a development machine that DOES NOT intend to write to +a MySQL DB during test runs. +""" + +from setuptools import setup, find_packages + +setup( + name = 'seleniumbase', + version = '1.1.7', + author = 'Michael Mintz', + author_email = '@mintzworld', + maintainer = 'Michael Mintz', + description = 'The SeleniumBase Test Framework. (Powered by Python, WebDriver, and more...)', + license = 'The MIT License', + packages = ['seleniumbase', + 'seleniumbase.core', + 'seleniumbase.plugins', + 'seleniumbase.fixtures', + 'seleniumbase.common', + 'seleniumbase.config'], + entry_points = { + 'nose.plugins': [ + 'base_plugin = seleniumbase.plugins.base_plugin:Base', + 'selenium_docker = seleniumbase.plugins.docker_selenium_plugin:SeleniumBrowser', + 'page_source = seleniumbase.plugins.page_source:PageSource', + 'screen_shots = seleniumbase.plugins.screen_shots:ScreenShots', + 'test_info = seleniumbase.plugins.basic_test_info:BasicTestInfo', + 's3_logging = seleniumbase.plugins.s3_logging_plugin:S3Logging', + 'hipchat_reporting = seleniumbase.plugins.hipchat_reporting_plugin:HipchatReporting', + ] + } + ) diff --git a/docker/docker_test.sh b/docker/docker_test.sh new file mode 100755 index 000000000000..65a10734efd4 --- /dev/null +++ b/docker/docker_test.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e +# Run example test from inside Docker image +echo "Running example SeleniumBase test from Docker..." +cd /SeleniumBase/examples/ && nosetests my_first_test.py --logging-level=INFO -s --with-testing_base --with-selenium_docker +exec "$@" diff --git a/seleniumbase/plugins/docker_selenium_plugin.py b/seleniumbase/plugins/docker_selenium_plugin.py new file mode 100755 index 000000000000..cb28b7f8e6ea --- /dev/null +++ b/seleniumbase/plugins/docker_selenium_plugin.py @@ -0,0 +1,85 @@ +""" +This is the Docker version of the Selenium plugin. +""" + +import time +import os +from nose.plugins import Plugin +from pyvirtualdisplay import Display +from selenium import webdriver +from seleniumbase.core import selenium_launcher +from seleniumbase.fixtures import constants + + +class SeleniumBrowser(Plugin): + """ + The plugin for Selenium tests. Takes in key arguments and then + creates a WebDriver object. All arguments are passed to the tests. + + The following variables are made to the tests: + self.options.browser -- the browser to use (--browser) + self.options.server -- the server used by the test (--server) + self.options.port -- the port used by thest (--port) + """ + name = 'selenium_docker' # Usage: --with-selenium_docker + + def options(self, parser, env): + super(SeleniumBrowser, self).options(parser, env=env) + + parser.add_option('--browser', action='store', + dest='browser', + choices=constants.Browser.VERSION.keys(), + default=constants.Browser.FIREFOX, + help="""Specifies the browser to use. Default = FireFox. + If you want to use Chrome, explicitly indicate that.""") + parser.add_option('--browser_version', action='store', + dest='browser_version', + default="latest", + help="""The browser version to use. Explicitly select + a version number or use "latest".""") + parser.add_option('--server', action='store', dest='servername', + default='localhost', + help="Designates the server used by the test. Default: localhost.") + parser.add_option('--port', action='store', dest='port', + default='4444', + help="Designates the port used by the test. Default: 4444.") + + + def configure(self, options, conf): + super(SeleniumBrowser, self).configure(options, conf) + self.display = Display(visible=0, size=(1200, 800)) + self.display.start() + self.driver = self.__select_browser() + self.options = options + + + def beforeTest(self, test): + """ Running Selenium locally will be handled differently + from how Selenium is run remotely, such as from Jenkins. """ + + try: + self.driver = self.__select_browser() + test.test.driver = self.driver + test.test.browser = "firefox" + except Exception as err: + print "Error starting/connecting to Selenium:" + print err + os.kill(os.getpid(), 9) + return self.driver + + + def afterTest(self, test): + try: + self.driver.quit() + self.display.stop() + except: + print "No driver to quit." + + + def __select_browser(self): + try: + profile = webdriver.FirefoxProfile() + profile.set_preference("reader.parse-on-load.enabled", False) + return webdriver.Firefox(profile) + except: + return webdriver.Firefox() From e5a9370e471d3948235af54323d56267c585718b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 15 Nov 2015 03:04:12 -0500 Subject: [PATCH 136/219] Add instructions for running SeleniumBase tests in Docker --- docker_readme.txt | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100755 docker_readme.txt diff --git a/docker_readme.txt b/docker_readme.txt new file mode 100755 index 000000000000..f34596475336 --- /dev/null +++ b/docker_readme.txt @@ -0,0 +1,37 @@ +Follow these instructions for running tests in Docker on your machine: + +1. Get the Docker Toolbox from https://www.docker.com/toolbox and install it. + +2. Setup your Docker environment: + $ docker-machine create --driver virtualbox default + +3. Start up your Docker environment: + $ docker-machine restart default + +4. Configure your shell: + $ eval "$(docker-machine env default)" + +5. Go to the SeleniumBase home directory. (That's where "Dockerfile" is located) + +6. Create your Docker image from your Dockerfile: (Get ready to wait awhile) + $ docker build -t seleniumbase . + +7. Run your Docker image: (The "-i" keeps you inside the Docker shell) + $ docker run -i seleniumbase + +8. Run an example test from inside your Docker shell: (Takes a few seconds) + $ ./docker_test.sh + +9. When you're satisfied, you may exit the Docker shell: + $ exit + +10. (Optional) Since Docker images and containers take up a lot of space, you may want to clean up your machine from time to time when they’re not being used: +http://stackoverflow.com/questions/17236796/how-to-remove-old-docker-containers +Here are a few of those cleanup commands: +$ docker images | grep "" | awk '{print $3}' | xargs docker rmi +$ docker rm `docker ps --no-trunc -aq` + +11. (Optional) More reading on Docker can be found here: +https://docs.docker.com/mac/started/ +https://docs.docker.com/installation/mac/ +https://docs.docker.com From 93ae31081b93d7033aa125dccb7dbf8deced15d7 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 15 Nov 2015 04:12:33 -0500 Subject: [PATCH 137/219] Update the Readme --- README.md | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 275c32465a78..62b115f7464c 100755 --- a/README.md +++ b/README.md @@ -2,24 +2,21 @@ ### Build, Automate, Verify +(Now with [Docker](https://www.docker.com/) support for running all your automation!) + Here's what users are saying: * "The best way of creating test automation for your web apps." -* "It's like a [Docker](https://www.docker.com/) for automated tests." * "Our team was able to start writing & running tests immediately." +* "We love the flexibility and ease-of-use of SeleniumBase." Features include: -* MySQL DB integration for storing test data and results -* Amazon S3 manager for uploading logs and screenshots -* Easy integration with the Jenkins build-server -* A powerful logging system for tracking failures -* A system for utilizing data from previous tests -* An email manager for parsing through sent emails -* Libraries for code simplification and reusable code -* Nosetest support for running your tests with ease -* Advanced commands for saving you significant time - - -To utilize some of the more advanced integrations, you'll want to setup instances and make connections to MySQL, Jenkins, Amazon S3, and the Selenium Grid. We've provided placeholders in the code where you can specify your connection details for those (see settings.py in the seleniumbase/config folder). You can also use this framework as a bare-bones Selenium WebDriver command executer to automate tasks in a browser without doing any data reporting (and that's also the fastest way to make sure your base setup is working properly). If you plan on running tests from a build server across multiple cloud machines, you can connect to your own Selenium Grid or use a cloud provider such as BrowserStack. +* All the power of Python, Webdriver, and Nosetests blended together +* A flexible logging system for storing test data/results/screenshots in MySQL and S3 +* Libraries for code-simplification, time-saving, and reusability +* Easy integration with a Jenkins build-server, Selenium Grid, and Docker + + +Placeholders have been provided in the code for specifing custom details such as DB connection info and default configuration (see settings.py in the seleniumbase/config folder). For an excellent example of all the pieces coming together, check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot). From ed8a06335aaed773c84330014963764ca092efa5 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 15 Nov 2015 04:12:52 -0500 Subject: [PATCH 138/219] Up the version number --- 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 df7c7a4e3ccf..a3ce091d8335 100755 --- a/server_setup.py +++ b/server_setup.py @@ -8,7 +8,7 @@ setup( name = 'seleniumbase', - version = '1.1.6', + version = '1.1.7', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', diff --git a/setup.py b/setup.py index 7c95894d3508..195fda70d9b0 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name = 'seleniumbase', - version = '1.1.6', + version = '1.1.7', author = 'Michael Mintz', author_email = '@mintzworld', maintainer = 'Michael Mintz', From f7abaa1ff8dbb0f26bfbbc07739660cd0ae19d14 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 15 Nov 2015 12:32:02 -0500 Subject: [PATCH 139/219] Updates to Docker readme --- docker_readme.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker_readme.txt b/docker_readme.txt index f34596475336..a45528eab714 100755 --- a/docker_readme.txt +++ b/docker_readme.txt @@ -19,7 +19,7 @@ Follow these instructions for running tests in Docker on your machine: 7. Run your Docker image: (The "-i" keeps you inside the Docker shell) $ docker run -i seleniumbase -8. Run an example test from inside your Docker shell: (Takes a few seconds) +8. Run the example test from inside your Docker shell: (Takes a few seconds) $ ./docker_test.sh 9. When you're satisfied, you may exit the Docker shell: @@ -29,7 +29,7 @@ Follow these instructions for running tests in Docker on your machine: http://stackoverflow.com/questions/17236796/how-to-remove-old-docker-containers Here are a few of those cleanup commands: $ docker images | grep "" | awk '{print $3}' | xargs docker rmi -$ docker rm `docker ps --no-trunc -aq` +$ docker rm 'docker ps --no-trunc -aq' 11. (Optional) More reading on Docker can be found here: https://docs.docker.com/mac/started/ From 2196100a0af65184c1a9dbb65406273a18dd4864 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 15 Nov 2015 17:02:04 -0500 Subject: [PATCH 140/219] File permissions update --- examples/ReadMe.txt | 0 examples/example_config.cfg | 0 examples/my_first_test.py | 0 examples/rate_limiting_test.py | 0 examples/test_fail.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 examples/ReadMe.txt mode change 100644 => 100755 examples/example_config.cfg mode change 100644 => 100755 examples/my_first_test.py mode change 100644 => 100755 examples/rate_limiting_test.py mode change 100644 => 100755 examples/test_fail.py diff --git a/examples/ReadMe.txt b/examples/ReadMe.txt old mode 100644 new mode 100755 diff --git a/examples/example_config.cfg b/examples/example_config.cfg old mode 100644 new mode 100755 diff --git a/examples/my_first_test.py b/examples/my_first_test.py old mode 100644 new mode 100755 diff --git a/examples/rate_limiting_test.py b/examples/rate_limiting_test.py old mode 100644 new mode 100755 diff --git a/examples/test_fail.py b/examples/test_fail.py old mode 100644 new mode 100755 From 10bed87880e738e48858f808a902c7bcbe50ab44 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 15 Nov 2015 17:06:00 -0500 Subject: [PATCH 141/219] ignore ghostdriver.log => .gitignore update --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bd0694f4ba76..153ba5069b7d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ dist build logs archived_logs +ghostdriver.log eggs parts bin From 09ce707971c14113fdf3bf9983f7f2c408d2fe8c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 15 Nov 2015 17:55:55 -0500 Subject: [PATCH 142/219] Renaming example test scripts --- .../{run_my_first_test.sh => run_my_first_test_in_chrome.sh} | 0 .../{run_my_first_test.bat => run_my_first_test_in_firefox.sh} | 0 examples/run_my_first_test_in_phantomjs.sh | 1 + ...ail_with_debug.bat => run_test_fail_with_debug_in_firefox.sh} | 0 ...with_logging.bat => run_test_fail_with_logging_in_firefox.sh} | 0 5 files changed, 1 insertion(+) rename examples/{run_my_first_test.sh => run_my_first_test_in_chrome.sh} (100%) rename examples/{run_my_first_test.bat => run_my_first_test_in_firefox.sh} (100%) create mode 100755 examples/run_my_first_test_in_phantomjs.sh rename examples/{run_test_fail_with_debug.bat => run_test_fail_with_debug_in_firefox.sh} (100%) rename examples/{run_test_fail_with_logging.bat => run_test_fail_with_logging_in_firefox.sh} (100%) diff --git a/examples/run_my_first_test.sh b/examples/run_my_first_test_in_chrome.sh similarity index 100% rename from examples/run_my_first_test.sh rename to examples/run_my_first_test_in_chrome.sh diff --git a/examples/run_my_first_test.bat b/examples/run_my_first_test_in_firefox.sh similarity index 100% rename from examples/run_my_first_test.bat rename to examples/run_my_first_test_in_firefox.sh diff --git a/examples/run_my_first_test_in_phantomjs.sh b/examples/run_my_first_test_in_phantomjs.sh new file mode 100755 index 000000000000..a859824678f8 --- /dev/null +++ b/examples/run_my_first_test_in_phantomjs.sh @@ -0,0 +1 @@ +nosetests my_first_test.py --browser=firefox --with-selenium --logging-level=INFO -s \ No newline at end of file diff --git a/examples/run_test_fail_with_debug.bat b/examples/run_test_fail_with_debug_in_firefox.sh similarity index 100% rename from examples/run_test_fail_with_debug.bat rename to examples/run_test_fail_with_debug_in_firefox.sh diff --git a/examples/run_test_fail_with_logging.bat b/examples/run_test_fail_with_logging_in_firefox.sh similarity index 100% rename from examples/run_test_fail_with_logging.bat rename to examples/run_test_fail_with_logging_in_firefox.sh From 4224cf2eb0eb6b90afcb122f0ae667ecc4401442 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 15 Nov 2015 19:23:36 -0500 Subject: [PATCH 143/219] Improve Docker integration --- Dockerfile | 3 ++- docker/docker_config.cfg | 7 +++++++ docker/docker_test.sh | 3 ++- docker_readme.txt | 15 +++++++++------ 4 files changed, 20 insertions(+), 8 deletions(-) create mode 100755 docker/docker_config.cfg diff --git a/Dockerfile b/Dockerfile index 14a41b3a4403..fd51508120ba 100755 --- a/Dockerfile +++ b/Dockerfile @@ -89,6 +89,7 @@ RUN cd /SeleniumBase && ls && sudo python docker_setup.py install # Create entrypoint and grab example test #========================================= COPY docker/docker-entrypoint.sh / -ENTRYPOINT ["/docker-entrypoint.sh"] COPY docker/docker_test.sh / +COPY docker/docker_config.cfg /SeleniumBase/examples/ +ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["/bin/bash"] diff --git a/docker/docker_config.cfg b/docker/docker_config.cfg new file mode 100755 index 000000000000..3bf736a9d36f --- /dev/null +++ b/docker/docker_config.cfg @@ -0,0 +1,7 @@ +[nosetests] +with-selenium_docker=1 +with-testing_base=1 +with-basic_test_info=1 +nocapture=1 +logging-level=INFO +browser=firefox diff --git a/docker/docker_test.sh b/docker/docker_test.sh index 65a10734efd4..f374ad90f845 100755 --- a/docker/docker_test.sh +++ b/docker/docker_test.sh @@ -2,5 +2,6 @@ set -e # Run example test from inside Docker image echo "Running example SeleniumBase test from Docker..." -cd /SeleniumBase/examples/ && nosetests my_first_test.py --logging-level=INFO -s --with-testing_base --with-selenium_docker +# cd /SeleniumBase/examples/ && nosetests my_first_test.py --logging-level=INFO -s --with-testing_base --with-selenium_docker +cd /SeleniumBase/examples/ && nosetests my_first_test.py --config=docker_config.cfg exec "$@" diff --git a/docker_readme.txt b/docker_readme.txt index a45528eab714..0aecd67df23f 100755 --- a/docker_readme.txt +++ b/docker_readme.txt @@ -16,22 +16,25 @@ Follow these instructions for running tests in Docker on your machine: 6. Create your Docker image from your Dockerfile: (Get ready to wait awhile) $ docker build -t seleniumbase . -7. Run your Docker image: (The "-i" keeps you inside the Docker shell) - $ docker run -i seleniumbase +7. Run a test inside your Docker: (Once the test completes after a few seconds, you'll automatically exit the Docker shell) + $ docker run seleniumbase ./docker_test.sh -8. Run the example test from inside your Docker shell: (Takes a few seconds) +8. You can also enter Docker and stay inside the shell: + $ docker run -i -t seleniumbase + +9. Now you can run the example test from inside the Docker shell: $ ./docker_test.sh -9. When you're satisfied, you may exit the Docker shell: +10. When you're satisfied, you may exit the Docker shell: $ exit -10. (Optional) Since Docker images and containers take up a lot of space, you may want to clean up your machine from time to time when they’re not being used: +11. (Optional) Since Docker images and containers take up a lot of space, you may want to clean up your machine from time to time when they’re not being used: http://stackoverflow.com/questions/17236796/how-to-remove-old-docker-containers Here are a few of those cleanup commands: $ docker images | grep "" | awk '{print $3}' | xargs docker rmi $ docker rm 'docker ps --no-trunc -aq' -11. (Optional) More reading on Docker can be found here: +12. (Optional) More reading on Docker can be found here: https://docs.docker.com/mac/started/ https://docs.docker.com/installation/mac/ https://docs.docker.com From 727c15a53795b422649a9a9cadef1de67edc137d Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 16 Nov 2015 20:43:16 -0500 Subject: [PATCH 144/219] Install PhantomJS with Docker --- Dockerfile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Dockerfile b/Dockerfile index fd51508120ba..dfbedd49b538 100755 --- a/Dockerfile +++ b/Dockerfile @@ -75,6 +75,15 @@ RUN sudo useradd seluser --shell /bin/bash --create-home \ && sudo usermod -a -G sudo seluser \ && echo 'ALL ALL = (ALL) NOPASSWD: ALL' >> /etc/sudoers +#=================== +# Install PhantomJS +#=================== +RUN cd /usr/local/share && wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.7-linux-x86_64.tar.bz2 +RUN cd /usr/local/share && tar xjf phantomjs-1.9.7-linux-x86_64.tar.bz2 +RUN ln -s /usr/local/share/phantomjs-1.9.7-linux-x86_64/bin/phantomjs /usr/local/share/phantomjs +RUN ln -s /usr/local/share/phantomjs-1.9.7-linux-x86_64/bin/phantomjs /usr/local/bin/phantomjs +RUN ln -s /usr/local/share/phantomjs-1.9.7-linux-x86_64/bin/phantomjs /usr/bin/phantomjs + #===================== # Set up SeleniumBase #===================== From 2b2ee922f0326b3d71491eebd652667e1a4ab766 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 16 Nov 2015 20:45:15 -0500 Subject: [PATCH 145/219] Better readme info on deleting Docker images and containers --- docker_readme.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docker_readme.txt b/docker_readme.txt index 0aecd67df23f..5773ef0d4672 100755 --- a/docker_readme.txt +++ b/docker_readme.txt @@ -31,8 +31,11 @@ Follow these instructions for running tests in Docker on your machine: 11. (Optional) Since Docker images and containers take up a lot of space, you may want to clean up your machine from time to time when they’re not being used: http://stackoverflow.com/questions/17236796/how-to-remove-old-docker-containers Here are a few of those cleanup commands: -$ docker images | grep "" | awk '{print $3}' | xargs docker rmi -$ docker rm 'docker ps --no-trunc -aq' + $ docker images | grep "" | awk '{print $3}' | xargs docker rmi + $ docker rm 'docker ps --no-trunc -aq' +If you want to completely remove all your docker containers and images: + $ docker rm $(docker ps -a -q) + $ docker rmi $(docker images -q) 12. (Optional) More reading on Docker can be found here: https://docs.docker.com/mac/started/ From 5a10dba11d6f84750e89b160837d8c069fc8cc98 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 16 Nov 2015 20:50:58 -0500 Subject: [PATCH 146/219] Docker example tests now run in Firefox and PhantomJS --- Dockerfile | 3 ++- docker/docker_config.cfg | 1 - docker/docker_test.sh | 7 ------- docker/run_docker_test_in_firefox.sh | 6 ++++++ docker/run_docker_test_in_phantomjs.sh | 6 ++++++ 5 files changed, 14 insertions(+), 9 deletions(-) delete mode 100755 docker/docker_test.sh create mode 100755 docker/run_docker_test_in_firefox.sh create mode 100755 docker/run_docker_test_in_phantomjs.sh diff --git a/Dockerfile b/Dockerfile index dfbedd49b538..f163096e5d02 100755 --- a/Dockerfile +++ b/Dockerfile @@ -98,7 +98,8 @@ RUN cd /SeleniumBase && ls && sudo python docker_setup.py install # Create entrypoint and grab example test #========================================= COPY docker/docker-entrypoint.sh / -COPY docker/docker_test.sh / +COPY docker/run_docker_test_in_firefox.sh / +COPY docker/run_docker_test_in_phantomjs.sh / COPY docker/docker_config.cfg /SeleniumBase/examples/ ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["/bin/bash"] diff --git a/docker/docker_config.cfg b/docker/docker_config.cfg index 3bf736a9d36f..48c63cc77c8c 100755 --- a/docker/docker_config.cfg +++ b/docker/docker_config.cfg @@ -4,4 +4,3 @@ with-testing_base=1 with-basic_test_info=1 nocapture=1 logging-level=INFO -browser=firefox diff --git a/docker/docker_test.sh b/docker/docker_test.sh deleted file mode 100755 index f374ad90f845..000000000000 --- a/docker/docker_test.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -e -# Run example test from inside Docker image -echo "Running example SeleniumBase test from Docker..." -# cd /SeleniumBase/examples/ && nosetests my_first_test.py --logging-level=INFO -s --with-testing_base --with-selenium_docker -cd /SeleniumBase/examples/ && nosetests my_first_test.py --config=docker_config.cfg -exec "$@" diff --git a/docker/run_docker_test_in_firefox.sh b/docker/run_docker_test_in_firefox.sh new file mode 100755 index 000000000000..a4e93e5350c8 --- /dev/null +++ b/docker/run_docker_test_in_firefox.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e +# Run example test from inside Docker image +echo "Running example SeleniumBase test from Docker with headless Firefox..." +cd /SeleniumBase/examples/ && nosetests my_first_test.py --config=docker_config.cfg --browser=firefox +exec "$@" diff --git a/docker/run_docker_test_in_phantomjs.sh b/docker/run_docker_test_in_phantomjs.sh new file mode 100755 index 000000000000..7e903e86c54c --- /dev/null +++ b/docker/run_docker_test_in_phantomjs.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e +# Run example test from inside Docker image +echo "Running example SeleniumBase test from Docker with PhantomJS..." +cd /SeleniumBase/examples/ && nosetests my_first_test.py --config=docker_config.cfg --browser=phantomjs +exec "$@" From 3c9fbd2516577a09d9a64ccaaf6e1736a7edd523 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 16 Nov 2015 21:20:38 -0500 Subject: [PATCH 147/219] Have the PhantomJS script use PhantomJS --- examples/run_my_first_test_in_phantomjs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/run_my_first_test_in_phantomjs.sh b/examples/run_my_first_test_in_phantomjs.sh index a859824678f8..7af38aef582b 100755 --- a/examples/run_my_first_test_in_phantomjs.sh +++ b/examples/run_my_first_test_in_phantomjs.sh @@ -1 +1 @@ -nosetests my_first_test.py --browser=firefox --with-selenium --logging-level=INFO -s \ No newline at end of file +nosetests my_first_test.py --browser=phantomjs --with-selenium --logging-level=INFO -s From 3971533f842e648d69f1b34b62763e7c37b0f2e8 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 16 Nov 2015 21:24:33 -0500 Subject: [PATCH 148/219] Update the first example test --- examples/my_first_test.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/my_first_test.py b/examples/my_first_test.py index b89cc240b985..6d2d0d981739 100755 --- a/examples/my_first_test.py +++ b/examples/my_first_test.py @@ -4,13 +4,15 @@ class MyTestClass(BaseCase): def test_basic(self): self.open("http://xkcd.com/1513/") - self.click('a[href="http://blag.xkcd.com"]') - self.wait_for_text_visible("The blag of the webcomic", "#site-description") - self.update_text_value("input#s", "Robots!\n") - self.wait_for_text_visible("Hooray robots!", "#content") - self.open("http://xkcd.com/1481/") self.wait_for_element_visible("div#comic") self.click('a[rel="license"]') text = self.wait_for_element_visible('center').text self.assertTrue("reuse any of my drawings" in text) self.assertTrue("You can use them freely" in text) + self.open("http://xkcd.com/1481/") + self.click_link_text('Blag') + self.wait_for_text_visible("The blag of the webcomic", "#site-description") + self.update_text_value("input#s", "Robots!\n") + self.wait_for_text_visible("Hooray robots!", "#content") + self.open("http://xkcd.com/1319/") + self.wait_for_text_visible("Automation", "div#ctitle") From 778138d76c3c740a4aad96421a72ab6db79f5ca2 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 16 Nov 2015 21:25:20 -0500 Subject: [PATCH 149/219] Update the Docker readme --- docker_readme.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker_readme.txt b/docker_readme.txt index 5773ef0d4672..91ad032c8038 100755 --- a/docker_readme.txt +++ b/docker_readme.txt @@ -17,13 +17,13 @@ Follow these instructions for running tests in Docker on your machine: $ docker build -t seleniumbase . 7. Run a test inside your Docker: (Once the test completes after a few seconds, you'll automatically exit the Docker shell) - $ docker run seleniumbase ./docker_test.sh + $ docker run seleniumbase ./run_docker_test_in_firefox.sh 8. You can also enter Docker and stay inside the shell: $ docker run -i -t seleniumbase -9. Now you can run the example test from inside the Docker shell: - $ ./docker_test.sh +9. Now you can run the example test from inside the Docker shell: (This time using PhantomJS): + $ ./run_docker_test_in_phantomjs.sh 10. When you're satisfied, you may exit the Docker shell: $ exit From 8f953a102bddd5cf5c6c4034cd2d849814fa1b4c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 16 Nov 2015 23:19:08 -0500 Subject: [PATCH 150/219] Get to the point faster in the readme --- README.md | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 62b115f7464c..d6c6a8c7080e 100755 --- a/README.md +++ b/README.md @@ -1,25 +1,15 @@ # SeleniumBase Test Framework -### Build, Automate, Verify - -(Now with [Docker](https://www.docker.com/) support for running all your automation!) - -Here's what users are saying: -* "The best way of creating test automation for your web apps." -* "Our team was able to start writing & running tests immediately." -* "We love the flexibility and ease-of-use of SeleniumBase." +## Your personal web automation solution Features include: -* All the power of Python, Webdriver, and Nosetests blended together -* A flexible logging system for storing test data/results/screenshots in MySQL and S3 +* ALL the power of Python, WebDriver, and Nosetests +* Easy integration with [Jenkins](http://jenkins-ci.org/), [Selenium Grid](http://docs.seleniumhq.org/projects/grid/), [Docker](https://www.docker.com/), and [AWS](http://aws.amazon.com/) +* A flexible logging system for storing test data, results, and screenshots * Libraries for code-simplification, time-saving, and reusability -* Easy integration with a Jenkins build-server, Selenium Grid, and Docker - - -Placeholders have been provided in the code for specifing custom details such as DB connection info and default configuration (see settings.py in the seleniumbase/config folder). - +* Fully customizable (see [settings.py](https://github.com/mdmintz/SeleniumBase/blob/master/seleniumbase/config/settings.py) in the seleniumbase/config folder) -For an excellent example of all the pieces coming together, check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot). +Looking for a real-world business example? Check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot) to learn how all the pieces of web automation come together. For more, read [The Classic "QA Team" is Obsolete](http://product.hubspot.com/blog/the-classic-qa-team-is-obsolete) ## Part I: MAC SETUP INSTRUCTIONS From ace3f43d221a7446167f4d1f2e422007f222597b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 16 Nov 2015 23:24:15 -0500 Subject: [PATCH 151/219] Getting picky with details in the readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d6c6a8c7080e..18c39aee5be9 100755 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ ## Your personal web automation solution -Features include: +#### Features include: * ALL the power of Python, WebDriver, and Nosetests * Easy integration with [Jenkins](http://jenkins-ci.org/), [Selenium Grid](http://docs.seleniumhq.org/projects/grid/), [Docker](https://www.docker.com/), and [AWS](http://aws.amazon.com/) * A flexible logging system for storing test data, results, and screenshots * Libraries for code-simplification, time-saving, and reusability * Fully customizable (see [settings.py](https://github.com/mdmintz/SeleniumBase/blob/master/seleniumbase/config/settings.py) in the seleniumbase/config folder) -Looking for a real-world business example? Check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot) to learn how all the pieces of web automation come together. For more, read [The Classic "QA Team" is Obsolete](http://product.hubspot.com/blog/the-classic-qa-team-is-obsolete) +Looking for a real-world business example? Check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot) to learn how all the pieces of web automation come together. For more, read [The Classic "QA Team" is Obsolete](http://product.hubspot.com/blog/the-classic-qa-team-is-obsolete). ## Part I: MAC SETUP INSTRUCTIONS From dcbf65033680c614148c3d0208a0fda55da2613c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 17 Nov 2015 00:11:09 -0500 Subject: [PATCH 152/219] Update the readme --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 18c39aee5be9..9d176dc93440 100755 --- a/README.md +++ b/README.md @@ -115,14 +115,17 @@ rmvirtualenv [NAME OF VIRTUAL ENV TO REMOVE] **Step 3:** Install necessary packages from the SeleniumBase folder and compile the test framework -If you're NOT connecting to a MySQL DB from your local test runs (based on the path you chose above), use these steps: +If you don't desire connecting to a MySQL DB to record the results of your local test runs, run this command: ```bash sudo pip install -r requirements.txt +``` +The "-e ." at the end of requirements.txt should automatically trigger setup.py installation from the following command: +```bash sudo python setup.py install ``` -If you ARE connecting to a MySQL DB from your local test runs, use these steps: +If you do desire connecting to a MySQL DB to record the results of your test runs, run both of these commands: (Make sure you already have MySQL installed from either Brew or web-download) ```bash sudo pip install -r server_requirements.txt From 50c7d32bf0b793c6a65cef19f4723858674938da Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 17 Nov 2015 00:22:07 -0500 Subject: [PATCH 153/219] More readme updates --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9d176dc93440..c6fc467e42db 100755 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ * Libraries for code-simplification, time-saving, and reusability * Fully customizable (see [settings.py](https://github.com/mdmintz/SeleniumBase/blob/master/seleniumbase/config/settings.py) in the seleniumbase/config folder) -Looking for a real-world business example? Check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot) to learn how all the pieces of web automation come together. For more, read [The Classic "QA Team" is Obsolete](http://product.hubspot.com/blog/the-classic-qa-team-is-obsolete). +*Looking for a real-world business example? Check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot) to learn how all the pieces of web automation come together. For more, read [The Classic "QA Team" is Obsolete](http://product.hubspot.com/blog/the-classic-qa-team-is-obsolete).* ## Part I: MAC SETUP INSTRUCTIONS @@ -59,7 +59,7 @@ To save time from having to source virtualenvwrapper again when you open a new w ("vi" is a fast text editor - "i" for insert-text mode, "esc" to get out of insert-text mode, ":wq"+[Enter] to save & exit the file) -[Chromedriver](http://code.google.com/p/chromedriver/) and [PhantomJS](http://phantomjs.org/) +[Chromedriver](https://sites.google.com/a/chromium.org/chromedriver/) and [PhantomJS](http://phantomjs.org/) brew install chromedriver phantomjs From 35e6180de6af487fd901592c501a50bdc856a41b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 17 Nov 2015 20:39:30 -0500 Subject: [PATCH 154/219] Readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c6fc467e42db..2deddd4360f6 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SeleniumBase Test Framework -## Your personal web automation solution +## **Web automation for everything** #### Features include: * ALL the power of Python, WebDriver, and Nosetests From 2d0c7bf746b1694d97b25dce76c3c61848186de3 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 17 Nov 2015 20:40:44 -0500 Subject: [PATCH 155/219] Readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2deddd4360f6..d75487db63f6 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SeleniumBase Test Framework -## **Web automation for everything** +## **Web Automation For Everything** #### Features include: * ALL the power of Python, WebDriver, and Nosetests From bc86a110f031e35fdfb3e0d835857bcc470977e3 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 18 Nov 2015 00:25:58 -0500 Subject: [PATCH 156/219] Update readme code example --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d75487db63f6..417563ddf758 100755 --- a/README.md +++ b/README.md @@ -174,16 +174,18 @@ class MyTestClass(BaseCase): def test_basic(self): self.open("http://xkcd.com/1513/") - self.click('a[href="http://blag.xkcd.com"]') - self.wait_for_text_visible("The blag of the webcomic", "#site-description") - self.update_text_value("input#s", "Robots!\n") - self.wait_for_text_visible("Hooray robots!", "#content") - self.open("http://xkcd.com/1481/") self.wait_for_element_visible("div#comic") self.click('a[rel="license"]') text = self.wait_for_element_visible('center').text self.assertTrue("reuse any of my drawings" in text) self.assertTrue("You can use them freely" in text) + self.open("http://xkcd.com/1481/") + self.click_link_text('Blag') + self.wait_for_text_visible("The blag of the webcomic", "#site-description") + self.update_text_value("input#s", "Robots!\n") + self.wait_for_text_visible("Hooray robots!", "#content") + self.open("http://xkcd.com/1319/") + self.wait_for_text_visible("Automation", "div#ctitle") ``` Now try running the script using various web browsers: From 4770ff6ef909b8c5cda2f93ab13683295c03dae4 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 19 Nov 2015 00:46:27 -0500 Subject: [PATCH 157/219] One setup.py for both DB and non-DB usage --- README.md | 5 +++-- server_setup.py | 35 ----------------------------------- setup.py | 3 +-- 3 files changed, 4 insertions(+), 39 deletions(-) delete mode 100755 server_setup.py diff --git a/README.md b/README.md index 417563ddf758..f790ea5c5521 100755 --- a/README.md +++ b/README.md @@ -125,13 +125,14 @@ The "-e ." at the end of requirements.txt should automatically trigger setup.py sudo python setup.py install ``` -If you do desire connecting to a MySQL DB to record the results of your test runs, run both of these commands: (Make sure you already have MySQL installed from either Brew or web-download) +If you do desire connecting to a MySQL DB to record the results of your test runs, run this command: (Make sure you already have MySQL installed from either Brew or web-download) ```bash sudo pip install -r server_requirements.txt -sudo python server_setup.py install ``` +As mentioned before, the "-e ." in that file should automatically trigger installation of setup.py + NOTE: (If you already have root access on the machine you're using, you might not need to add "sudo" before those commands.) diff --git a/server_setup.py b/server_setup.py deleted file mode 100755 index a3ce091d8335..000000000000 --- a/server_setup.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -The setup package to install the SeleniumBase Test Framework plugins -on a server machine (or a development machine that intends to write to -a MySQL DB during test runs). -""" - -from setuptools import setup, find_packages - -setup( - name = 'seleniumbase', - version = '1.1.7', - author = 'Michael Mintz', - author_email = '@mintzworld', - maintainer = 'Michael Mintz', - description = 'The SeleniumBase Test Framework. (Powered by Python, WebDriver, and more...)', - license = 'The MIT License', - packages = ['seleniumbase', - 'seleniumbase.core', - 'seleniumbase.plugins', - 'seleniumbase.fixtures', - 'seleniumbase.common', - 'seleniumbase.config'], - entry_points = { - 'nose.plugins': [ - 'base_plugin = seleniumbase.plugins.base_plugin:Base', - 'selenium = seleniumbase.plugins.selenium_plugin:SeleniumBrowser', - 'page_source = seleniumbase.plugins.page_source:PageSource', - 'screen_shots = seleniumbase.plugins.screen_shots:ScreenShots', - 'test_info = seleniumbase.plugins.basic_test_info:BasicTestInfo', - 'db_reporting = seleniumbase.plugins.db_reporting_plugin:DBReporting', - 's3_logging = seleniumbase.plugins.s3_logging_plugin:S3Logging', - 'hipchat_reporting = seleniumbase.plugins.hipchat_reporting_plugin:HipchatReporting', - ] - } - ) diff --git a/setup.py b/setup.py index 195fda70d9b0..e0b02d84f331 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,5 @@ """ The setup package to install the SeleniumBase Test Framework plugins -on a development machine that DOES NOT intend to write to -a MySQL DB during test runs. """ from setuptools import setup, find_packages @@ -27,6 +25,7 @@ 'page_source = seleniumbase.plugins.page_source:PageSource', 'screen_shots = seleniumbase.plugins.screen_shots:ScreenShots', 'test_info = seleniumbase.plugins.basic_test_info:BasicTestInfo', + 'db_reporting = seleniumbase.plugins.db_reporting_plugin:DBReporting', 's3_logging = seleniumbase.plugins.s3_logging_plugin:S3Logging', 'hipchat_reporting = seleniumbase.plugins.hipchat_reporting_plugin:HipchatReporting', ] From c2d26d257614fafdf32c33e27c74e28290bef293 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 19 Nov 2015 00:48:03 -0500 Subject: [PATCH 158/219] Clang error resolved... No need for workaround --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index f790ea5c5521..5e6e7f6af2bb 100755 --- a/README.md +++ b/README.md @@ -137,8 +137,6 @@ NOTE: (If you already have root access on the machine you're using, you might not need to add "sudo" before those commands.) -(If the pip install gives you a "clang error: unknown argument: '-mno-fused-madd'", [try this](http://stackoverflow.com/questions/22313407/clang-error-unknown-argument-mno-fused-madd-python-package-installation-fa).) - In some cames, certain packages will have other dependencies as requirements, and those will get installed automatically. You'll be able to see all installed packages in your virtual environment by using the following command: ```bash From f42247814499360d798103951f6c14672ba7f523 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 19 Nov 2015 01:22:31 -0500 Subject: [PATCH 159/219] Pip comes with the latest python. --- pip_setup.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100755 pip_setup.py diff --git a/pip_setup.py b/pip_setup.py deleted file mode 100755 index e1ba24d5b0b7..000000000000 --- a/pip_setup.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Install Pip by typing "python pip_setup.py install" -""" - -from setuptools import setup, find_packages - -setup( - name = 'pip', - version = '7.1.2', - author = 'Pip', - author_email = '@pip', - maintainer = 'Pip', - description = 'Install Pip by typing "python pip_setup.py install".', - license = 'Pip', - install_requires = ['pip==7.1.2'], - ) From 28d0c5870662ebd0173988eab0db6a2012c361e0 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 19 Nov 2015 01:47:50 -0500 Subject: [PATCH 160/219] Flake8 whitespace and unused imports --- seleniumbase/plugins/base_plugin.py | 10 +++++----- seleniumbase/plugins/db_reporting_plugin.py | 14 +++++++------- seleniumbase/plugins/docker_selenium_plugin.py | 2 -- seleniumbase/plugins/page_source.py | 2 +- seleniumbase/plugins/s3_logging_plugin.py | 8 ++++---- seleniumbase/plugins/screen_shots.py | 2 +- seleniumbase/plugins/selenium_plugin.py | 4 ++-- 7 files changed, 20 insertions(+), 22 deletions(-) diff --git a/seleniumbase/plugins/base_plugin.py b/seleniumbase/plugins/base_plugin.py index e277e793c0b8..20993663f2b8 100755 --- a/seleniumbase/plugins/base_plugin.py +++ b/seleniumbase/plugins/base_plugin.py @@ -43,7 +43,7 @@ def options(self, parser, env): help='Where the log files are saved.') - def configure(self, options, conf): + def configure(self, options, conf): super(Base, self).configure(options, conf) if not self.enabled: return @@ -55,7 +55,7 @@ def configure(self, options, conf): else: if not os.path.exists("%s/../archived_logs/" % options.log_path): os.makedirs("%s/../archived_logs/" % options.log_path) - shutil.move(options.log_path, "%s/../archived_logs/logs_%s"%( + shutil.move(options.log_path, "%s/../archived_logs/logs_%s" % ( options.log_path, int(time.time()))) os.makedirs(options.log_path) @@ -75,7 +75,7 @@ def addError(self, test, err, capt=None): error states, we want to make sure that they don't show up in nose output as errors. """ - if (err[0] == errors.BlockedTest or + if (err[0] == errors.BlockedTest or err[0] == errors.SkipTest or err[0] == errors.DeprecatedTest): print err[1].__str__().split('-------------------- >> begin captured logging << --------------------', 1)[0] @@ -90,11 +90,11 @@ def handleError(self, test, err, capt=None): if err[0] == errors.BlockedTest: raise SkipTest(err[1]) return True - + elif err[0] == errors.DeprecatedTest: raise SkipTest(err[1]) return True - + elif err[0] == errors.SkipTest: raise SkipTest(err[1]) return True diff --git a/seleniumbase/plugins/db_reporting_plugin.py b/seleniumbase/plugins/db_reporting_plugin.py index aaf6c178336b..6a5cc39f228d 100755 --- a/seleniumbase/plugins/db_reporting_plugin.py +++ b/seleniumbase/plugins/db_reporting_plugin.py @@ -36,7 +36,7 @@ def __init__(self): def options(self, parser, env): super(DBReporting, self).options(parser, env=env) - parser.add_option('--database_environment', action='store', + parser.add_option('--database_environment', action='store', dest='database_env', choices=('prod', 'qa', 'test'), default='test', @@ -52,7 +52,7 @@ def configure(self, options, conf): def begin(self): - """At the start of the run, we want to record the + """At the start of the run, we want to record the execution information to the database.""" exec_payload = ExecutionQueryPayload() exec_payload.execution_start_time = int(time.time() * 1000) @@ -84,10 +84,10 @@ def startTest(self, test): def finalize(self, result): - """At the end of the run, we want to + """At the end of the run, we want to update that row with the execution time.""" runtime = int(time.time() * 1000) - self.execution_start_time - self.testcase_manager.update_execution_data(self.execution_guid, + self.testcase_manager.update_execution_data(self.execution_guid, runtime) @@ -116,13 +116,13 @@ def handleError(self, test, err, capt=None): self.error_handled = True raise SkipTest(err[1]) return True - + elif err[0] == errors.DeprecatedTest: self.__insert_test_result(constants.State.DEPRECATED, test, err) self.error_handled = True raise SkipTest(err[1]) return True - + elif err[0] == errors.SkipTest: self.__insert_test_result(constants.State.SKIP, test, err) self.error_handled = True @@ -134,7 +134,7 @@ def addFailure(self, test, err, capt=None, tbinfo=None): """ After failure of a test, we want to record the testcase run information. """ - self.__insert_test_result(constants.State.FAILURE, test, err) + self.__insert_test_result(constants.State.FAILURE, test, err) def __insert_test_result(self, state, test, err=None): diff --git a/seleniumbase/plugins/docker_selenium_plugin.py b/seleniumbase/plugins/docker_selenium_plugin.py index cb28b7f8e6ea..5c465113a251 100755 --- a/seleniumbase/plugins/docker_selenium_plugin.py +++ b/seleniumbase/plugins/docker_selenium_plugin.py @@ -2,12 +2,10 @@ This is the Docker version of the Selenium plugin. """ -import time import os from nose.plugins import Plugin from pyvirtualdisplay import Display from selenium import webdriver -from seleniumbase.core import selenium_launcher from seleniumbase.fixtures import constants diff --git a/seleniumbase/plugins/page_source.py b/seleniumbase/plugins/page_source.py index 8495a2769255..d02edbb5048b 100755 --- a/seleniumbase/plugins/page_source.py +++ b/seleniumbase/plugins/page_source.py @@ -32,7 +32,7 @@ def addError(self, test, err, capt=None): if not os.path.exists(test_logpath): os.makedirs(test_logpath) html_file_name = "%s/%s" % (test_logpath, self.logfile_name) - html_file = codecs.open(html_file_name, "w+","utf-8") + html_file = codecs.open(html_file_name, "w+", "utf-8") html_file.write(test.driver.page_source) html_file.close() diff --git a/seleniumbase/plugins/s3_logging_plugin.py b/seleniumbase/plugins/s3_logging_plugin.py index 079f8f13d424..a086fea82db5 100755 --- a/seleniumbase/plugins/s3_logging_plugin.py +++ b/seleniumbase/plugins/s3_logging_plugin.py @@ -26,14 +26,14 @@ def afterTest(self, test): """ After each testcase, upload logs to the S3 bucket. """ s3_bucket = S3LoggingBucket() guid = str(uuid.uuid4().hex) - path = "%s/%s" % (self.options.log_path, + path = "%s/%s" % (self.options.log_path, test.test.id()) uploaded_files = [] for logfile in os.listdir(path): - logfile_name = "%s/%s/%s" % (guid, - test.test.id(), + logfile_name = "%s/%s/%s" % (guid, + test.test.id(), logfile.split(path)[-1]) - s3_bucket.upload_file(logfile_name, + s3_bucket.upload_file(logfile_name, "%s/%s" % (path, logfile)) uploaded_files.append(logfile_name) s3_bucket.save_uploaded_file_names(uploaded_files) diff --git a/seleniumbase/plugins/screen_shots.py b/seleniumbase/plugins/screen_shots.py index 603ba15ad699..3c35a688f437 100755 --- a/seleniumbase/plugins/screen_shots.py +++ b/seleniumbase/plugins/screen_shots.py @@ -4,7 +4,6 @@ import os import time -import base64 from nose.plugins import Plugin class ScreenShots(Plugin): @@ -43,6 +42,7 @@ def add_screenshot(self, test, err, capt=None, tbinfo=None): except Exception: pass # Second screenshot at fullscreen might not be necessary + # import base64 '''screen_b64 = test.driver.get_screenshot_as_base64() screen = base64.decodestring(screen_b64) screenshot_file_2 = "%s/%s" % (test_logpath, self.logfile_name_2) diff --git a/seleniumbase/plugins/selenium_plugin.py b/seleniumbase/plugins/selenium_plugin.py index 87c37041995c..4ad9fde8519c 100755 --- a/seleniumbase/plugins/selenium_plugin.py +++ b/seleniumbase/plugins/selenium_plugin.py @@ -61,14 +61,14 @@ def configure(self, options, conf): if options.browser == constants.Browser.INTERNET_EXPLORER: self.browser_settings["platform"] = "WINDOWS" self.browser_settings["browserName"] = "internet explorer" - + if options.browser_version == 'latest': version = constants.Browser.LATEST[options.browser] if version is not None: self.browser_settings["version"] = version else: version_options = constants.Browser.VERSION[options.browser] - if (version_options is not None and + if (version_options is not None and options.browser_version in version_options): self.browser_settings["version"] = options.browser_version From 47398c16d01e84727a24c663538c4bbedced1ed8 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 19 Nov 2015 02:01:38 -0500 Subject: [PATCH 161/219] OS compatibility instructions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e6e7f6af2bb..5d9e8d86ebfd 100755 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ The "-e ." at the end of requirements.txt should automatically trigger setup.py sudo python setup.py install ``` -If you do desire connecting to a MySQL DB to record the results of your test runs, run this command: (Make sure you already have MySQL installed from either Brew or web-download) +If you do desire connecting to a MySQL DB to record the results of your test runs, run this command: (Make sure you already have MySQL installed from either Brew or web-download. If you're a WINDOWS user, you may have problems on the MySQL installation part. To get around this, you can either follow the instructions from the error message given, or you can pip install the previous requirements.txt file.) ```bash sudo pip install -r server_requirements.txt From 92ab809ce9dcb03fe5ca9d230236f135a4fff28b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 19 Nov 2015 16:21:26 -0500 Subject: [PATCH 162/219] Move a few DB imports to only where needed. --- seleniumbase/core/mysql.py | 2 +- seleniumbase/plugins/db_reporting_plugin.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/seleniumbase/core/mysql.py b/seleniumbase/core/mysql.py index 1394a7d9ba06..c31ebfec3a61 100755 --- a/seleniumbase/core/mysql.py +++ b/seleniumbase/core/mysql.py @@ -3,7 +3,6 @@ """ import time -import MySQLdb import mysql_conf as conf @@ -17,6 +16,7 @@ def __init__(self, database_env='test', conf_creds=None): """ Gets database information from mysql_conf.py and creates a connection. """ + import MySQLdb db_server, db_user, db_pass, db_schema = \ conf.APP_CREDS[conf.Apps.TESTCASE_REPOSITORY][database_env] retry_count = 3 diff --git a/seleniumbase/plugins/db_reporting_plugin.py b/seleniumbase/plugins/db_reporting_plugin.py index 6a5cc39f228d..1de99641e890 100755 --- a/seleniumbase/plugins/db_reporting_plugin.py +++ b/seleniumbase/plugins/db_reporting_plugin.py @@ -9,9 +9,6 @@ from nose.plugins import Plugin from nose.exc import SkipTest from seleniumbase.core.application_manager import ApplicationManager -from seleniumbase.core.testcase_manager import ExecutionQueryPayload -from seleniumbase.core.testcase_manager import TestcaseDataPayload -from seleniumbase.core.testcase_manager import TestcaseManager from seleniumbase.fixtures import constants from seleniumbase.fixtures import errors @@ -46,6 +43,7 @@ def options(self, parser, env): #Plugin methods def configure(self, options, conf): """get the options""" + from seleniumbase.core.testcase_manager import TestcaseManager super(DBReporting, self).configure(options, conf) self.options = options self.testcase_manager = TestcaseManager(self.options.database_env) @@ -54,6 +52,7 @@ def configure(self, options, conf): def begin(self): """At the start of the run, we want to record the execution information to the database.""" + from seleniumbase.core.testcase_manager import ExecutionQueryPayload exec_payload = ExecutionQueryPayload() exec_payload.execution_start_time = int(time.time() * 1000) self.execution_start_time = exec_payload.execution_start_time @@ -64,6 +63,7 @@ def begin(self): def startTest(self, test): """at the start of the test, set the test case details""" + from seleniumbase.core.testcase_manager import TestcaseDataPayload data_payload = TestcaseDataPayload() self.testcase_guid = str(uuid.uuid4()) data_payload.guid = self.testcase_guid @@ -138,6 +138,7 @@ def addFailure(self, test, err, capt=None, tbinfo=None): def __insert_test_result(self, state, test, err=None): + from seleniumbase.core.testcase_manager import TestcaseDataPayload data_payload = TestcaseDataPayload() data_payload.runtime = int(time.time() * 1000) - self.case_start_time data_payload.guid = self.testcase_guid From 96e3eba1cdffc71b1f56d76c38e455299c0ed65b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 19 Nov 2015 16:26:55 -0500 Subject: [PATCH 163/219] Move a few of the DB imports back to the top. --- seleniumbase/plugins/db_reporting_plugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/seleniumbase/plugins/db_reporting_plugin.py b/seleniumbase/plugins/db_reporting_plugin.py index 1de99641e890..6a5cc39f228d 100755 --- a/seleniumbase/plugins/db_reporting_plugin.py +++ b/seleniumbase/plugins/db_reporting_plugin.py @@ -9,6 +9,9 @@ from nose.plugins import Plugin from nose.exc import SkipTest from seleniumbase.core.application_manager import ApplicationManager +from seleniumbase.core.testcase_manager import ExecutionQueryPayload +from seleniumbase.core.testcase_manager import TestcaseDataPayload +from seleniumbase.core.testcase_manager import TestcaseManager from seleniumbase.fixtures import constants from seleniumbase.fixtures import errors @@ -43,7 +46,6 @@ def options(self, parser, env): #Plugin methods def configure(self, options, conf): """get the options""" - from seleniumbase.core.testcase_manager import TestcaseManager super(DBReporting, self).configure(options, conf) self.options = options self.testcase_manager = TestcaseManager(self.options.database_env) @@ -52,7 +54,6 @@ def configure(self, options, conf): def begin(self): """At the start of the run, we want to record the execution information to the database.""" - from seleniumbase.core.testcase_manager import ExecutionQueryPayload exec_payload = ExecutionQueryPayload() exec_payload.execution_start_time = int(time.time() * 1000) self.execution_start_time = exec_payload.execution_start_time @@ -63,7 +64,6 @@ def begin(self): def startTest(self, test): """at the start of the test, set the test case details""" - from seleniumbase.core.testcase_manager import TestcaseDataPayload data_payload = TestcaseDataPayload() self.testcase_guid = str(uuid.uuid4()) data_payload.guid = self.testcase_guid @@ -138,7 +138,6 @@ def addFailure(self, test, err, capt=None, tbinfo=None): def __insert_test_result(self, state, test, err=None): - from seleniumbase.core.testcase_manager import TestcaseDataPayload data_payload = TestcaseDataPayload() data_payload.runtime = int(time.time() * 1000) - self.case_start_time data_payload.guid = self.testcase_guid From cb97b28a2f897694a29ea449927d08417494c05a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 19 Nov 2015 16:50:26 -0500 Subject: [PATCH 164/219] A few Flake8 updates. --- seleniumbase/core/testcase_manager.py | 39 ++++++++++++++------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/seleniumbase/core/testcase_manager.py b/seleniumbase/core/testcase_manager.py index bfa75e5cb063..8998734073ab 100755 --- a/seleniumbase/core/testcase_manager.py +++ b/seleniumbase/core/testcase_manager.py @@ -4,6 +4,7 @@ from seleniumbase.core.mysql import DatabaseManager + class TestcaseManager: """ Helper for Testcase related DB stuff @@ -17,31 +18,33 @@ def insert_execution_data(self, execution_query_payload): """Inserts an execution into the database, returns the execution guid""" query = """INSERT INTO execution - (guid, executionStart, totalExecutionTime, username) + (guid, executionStart, totalExecutionTime, username) VALUES (%(guid)s,%(execution_start_time)s, %(total_execution_time)s,%(username)s)""" - DatabaseManager(self.database_env).execute_query_and_close(query, - execution_query_payload.get_params()) + DatabaseManager(self.database_env).execute_query_and_close( + query, + execution_query_payload.get_params()) return execution_query_payload.guid def update_execution_data(self, execution_guid, execution_time): """updates an existing execution in the database""" - - query = """UPDATE execution SET - totalExecutionTime=%(execution_time)s - WHERE guid=%(execution_guid)s """ - DatabaseManager(self.database_env).execute_query_and_close(query, - {"execution_guid":execution_guid, - "execution_time":execution_time}) + + query = """UPDATE execution + SET totalExecutionTime=%(execution_time)s + WHERE guid=%(execution_guid)s """ + DatabaseManager(self.database_env).execute_query_and_close( + query, + {"execution_guid": execution_guid, + "execution_time": execution_time}) def insert_testcase_data(self, testcase_run_payload): """inserts all data for the test case, returns the new row guid""" - query = """INSERT INTO testcaseRunData - (guid, browser, state, execution_guid, env, start_time, - testcaseAddress, runtime, retryCount, message, stackTrace) + query = """INSERT INTO testcaseRunData + (guid, browser, state, execution_guid, env, start_time, + testcaseAddress, runtime, retryCount, message, stackTrace) VALUES ( %(guid)s, %(browser)s, @@ -59,7 +62,7 @@ def insert_testcase_data(self, testcase_run_payload): def update_testcase_data(self, testcase_payload): """updates an existing testcase run in the database""" - + query = """UPDATE testcaseRunData SET runtime=%(runtime)s, state=%(state)s, @@ -72,10 +75,10 @@ def update_testcase_data(self, testcase_payload): def update_testcase_log_url(self, testcase_payload): """updates an existing testcase run's logging URL in the database""" - - query = """UPDATE testcaseRunData SET - logURL=%(logURL)s - WHERE guid=%(guid)s """ + + query = """UPDATE testcaseRunData + SET logURL=%(logURL)s + WHERE guid=%(guid)s """ DatabaseManager(self.database_env).execute_query_and_close(query, testcase_payload.get_params()) From cd734b5ca0630cd3f09f5c5f91c9a4f766058591 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 19 Nov 2015 18:40:38 -0500 Subject: [PATCH 165/219] Update the Dockerfile --- Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index f163096e5d02..9620a0425491 100755 --- a/Dockerfile +++ b/Dockerfile @@ -51,12 +51,12 @@ RUN apt-get update -qqy \ && apt-get -qy --no-install-recommends install \ $(apt-cache depends firefox | grep Depends | sed "s/.*ends:\ //" | tr '\n' ' ') \ && rm -rf /var/lib/apt/lists/* \ - && mkdir -p /tmp/ff \ - && wget -P /tmp/ff/ --no-check-certificate -r -l 1 -A bz2 -nH --cut-dirs=8 \ - https://ftp.mozilla.org/pub/mozilla.org/firefox/releases/latest-esr/linux-x86_64/en-US/ \ - && tar -xjf /tmp/ff/firefox-*esr.tar.bz2 -C /opt/ \ + && cd /tmp \ + && wget --no-check-certificate -O firefox-esr.tar.bz2 \ + 'https://download.mozilla.org/?product=firefox-esr-latest&os=linux64&lang=en-US' \ + && tar -xjf firefox-esr.tar.bz2 -C /opt/ \ && ln -s /opt/firefox/firefox /usr/bin/firefox \ - && rm -rf /tmp/ff/ + && rm -f /tmp/firefox-esr.tar.bz2 #=================== # Timezone settings From cfaac11556ec540d8a8882a8b001800506408252 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 20 Nov 2015 17:52:18 -0500 Subject: [PATCH 166/219] Converting Docker readme into a markdown file --- Docker_README.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++ docker_readme.txt | 43 ------------------------------------- 2 files changed, 54 insertions(+), 43 deletions(-) create mode 100755 Docker_README.md delete mode 100755 docker_readme.txt diff --git a/Docker_README.md b/Docker_README.md new file mode 100755 index 000000000000..78b7a4b975d2 --- /dev/null +++ b/Docker_README.md @@ -0,0 +1,54 @@ +## Follow these instructions for running SeleniumBase tests in Docker on your machine: + +#### 1. Get the Docker Toolbox from https://www.docker.com/toolbox and install it. + +#### 2. Setup your Docker environment: + + docker-machine create --driver virtualbox default + +#### 3. Start up your Docker environment: + + docker-machine restart default + +#### 4. Configure your shell: + + eval "$(docker-machine env default)" + +#### 5. Go to the SeleniumBase home directory. (That's where "Dockerfile" is located) + +#### 6. Create your Docker image from your Dockerfile: (Get ready to wait awhile) + + docker build -t seleniumbase . + +#### 7. Run a test inside your Docker: (Once the test completes after a few seconds, you'll automatically exit the Docker shell) + + docker run seleniumbase ./run_docker_test_in_firefox.sh + +#### 8. You can also enter Docker and stay inside the shell: + + docker run -i -t seleniumbase + +#### 9. Now you can run the example test from inside the Docker shell: (This time using PhantomJS) + + ./run_docker_test_in_phantomjs.sh + +#### 10. When you're satisfied, you may exit the Docker shell: + + exit + +#### 11. (Optional) Since Docker images and containers take up a lot of space, you may want to clean up your machine from time to time when they’re not being used: +http://stackoverflow.com/questions/17236796/how-to-remove-old-docker-containers +Here are a few of those cleanup commands: + + docker images | grep "" | awk '{print $3}' | xargs docker rmi + docker rm 'docker ps --no-trunc -aq' + +If you want to completely remove all of your docker containers and images, use these commands: (If there's nothing to delete, those commands will return an error.) + + docker rm $(docker ps -a -q) + docker rmi $(docker images -q) + +#### 12. (Optional) More reading on Docker can be found here: +https://docs.docker.com/mac/started/ +https://docs.docker.com/installation/mac/ +https://docs.docker.com diff --git a/docker_readme.txt b/docker_readme.txt deleted file mode 100755 index 91ad032c8038..000000000000 --- a/docker_readme.txt +++ /dev/null @@ -1,43 +0,0 @@ -Follow these instructions for running tests in Docker on your machine: - -1. Get the Docker Toolbox from https://www.docker.com/toolbox and install it. - -2. Setup your Docker environment: - $ docker-machine create --driver virtualbox default - -3. Start up your Docker environment: - $ docker-machine restart default - -4. Configure your shell: - $ eval "$(docker-machine env default)" - -5. Go to the SeleniumBase home directory. (That's where "Dockerfile" is located) - -6. Create your Docker image from your Dockerfile: (Get ready to wait awhile) - $ docker build -t seleniumbase . - -7. Run a test inside your Docker: (Once the test completes after a few seconds, you'll automatically exit the Docker shell) - $ docker run seleniumbase ./run_docker_test_in_firefox.sh - -8. You can also enter Docker and stay inside the shell: - $ docker run -i -t seleniumbase - -9. Now you can run the example test from inside the Docker shell: (This time using PhantomJS): - $ ./run_docker_test_in_phantomjs.sh - -10. When you're satisfied, you may exit the Docker shell: - $ exit - -11. (Optional) Since Docker images and containers take up a lot of space, you may want to clean up your machine from time to time when they’re not being used: -http://stackoverflow.com/questions/17236796/how-to-remove-old-docker-containers -Here are a few of those cleanup commands: - $ docker images | grep "" | awk '{print $3}' | xargs docker rmi - $ docker rm 'docker ps --no-trunc -aq' -If you want to completely remove all your docker containers and images: - $ docker rm $(docker ps -a -q) - $ docker rmi $(docker images -q) - -12. (Optional) More reading on Docker can be found here: -https://docs.docker.com/mac/started/ -https://docs.docker.com/installation/mac/ -https://docs.docker.com From 70855d8b8c1380275dd84f48fac21e7666cb0e5a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 20 Nov 2015 17:58:30 -0500 Subject: [PATCH 167/219] Update Docker readme --- Docker_README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Docker_README.md b/Docker_README.md index 78b7a4b975d2..55c392ca3a6d 100755 --- a/Docker_README.md +++ b/Docker_README.md @@ -49,6 +49,6 @@ If you want to completely remove all of your docker containers and images, use t docker rmi $(docker images -q) #### 12. (Optional) More reading on Docker can be found here: -https://docs.docker.com/mac/started/ -https://docs.docker.com/installation/mac/ -https://docs.docker.com +* https://docs.docker.com +* https://docs.docker.com/installation/mac/ +* https://docs.docker.com/mac/started/ From 453482e27ba8bc400ed734866ce6a0e14c87e17a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 20 Nov 2015 18:12:43 -0500 Subject: [PATCH 168/219] Update the Docker readme --- Docker_README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Docker_README.md b/Docker_README.md index 55c392ca3a6d..70eb3c7f05a2 100755 --- a/Docker_README.md +++ b/Docker_README.md @@ -1,4 +1,4 @@ -## Follow these instructions for running SeleniumBase tests in Docker on your machine: +## Docker setup instructions for SeleniumBase #### 1. Get the Docker Toolbox from https://www.docker.com/toolbox and install it. @@ -50,5 +50,5 @@ If you want to completely remove all of your docker containers and images, use t #### 12. (Optional) More reading on Docker can be found here: * https://docs.docker.com -* https://docs.docker.com/installation/mac/ * https://docs.docker.com/mac/started/ +* https://docs.docker.com/installation/mac/ From d5eef54ccc77ee4ea96c11e97cd7510a31c79c57 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 20 Nov 2015 18:27:00 -0500 Subject: [PATCH 169/219] Add Docker_README link in README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5d9e8d86ebfd..c0a3294730cb 100755 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ ## Part I: MAC SETUP INSTRUCTIONS ####(WINDOWS users: You'll need to make a few modifications to the setup steps listed here. For starters, you won't be able to use the "brew install" command since that's MAC-only. Instead, download the requirements mentioned directly from the web. I'll provide you with links to save you time. You'll also want to put downloaded files into your [PATH](http://java.com/en/download/help/path.xml).) +####(DOCKER users: If you want to run browser automation with Docker, read the [Docker_README](https://github.com/mdmintz/SeleniumBase/blob/master/Docker_README.md)) **Step 0:** Get the requirements: From d3a54b57e20e003f2e4d54759d49fa0f2428920b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 20 Nov 2015 18:28:37 -0500 Subject: [PATCH 170/219] Minor update to the readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c0a3294730cb..d18fd866d75e 100755 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ ## Part I: MAC SETUP INSTRUCTIONS ####(WINDOWS users: You'll need to make a few modifications to the setup steps listed here. For starters, you won't be able to use the "brew install" command since that's MAC-only. Instead, download the requirements mentioned directly from the web. I'll provide you with links to save you time. You'll also want to put downloaded files into your [PATH](http://java.com/en/download/help/path.xml).) -####(DOCKER users: If you want to run browser automation with Docker, read the [Docker_README](https://github.com/mdmintz/SeleniumBase/blob/master/Docker_README.md)) +####(DOCKER users: If you want to run browser automation with Docker, see the [Docker_README](https://github.com/mdmintz/SeleniumBase/blob/master/Docker_README.md)) **Step 0:** Get the requirements: From 934942aafb673865af1b4211310465a5a9c46dad Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 20 Nov 2015 18:40:26 -0500 Subject: [PATCH 171/219] Update the Readme --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d18fd866d75e..92c7e5d48fa1 100755 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@ ####(WINDOWS users: You'll need to make a few modifications to the setup steps listed here. For starters, you won't be able to use the "brew install" command since that's MAC-only. Instead, download the requirements mentioned directly from the web. I'll provide you with links to save you time. You'll also want to put downloaded files into your [PATH](http://java.com/en/download/help/path.xml).) ####(DOCKER users: If you want to run browser automation with Docker, see the [Docker_README](https://github.com/mdmintz/SeleniumBase/blob/master/Docker_README.md)) -**Step 0:** Get the requirements: +### **Step 0:** Get the requirements: [Python 2.*](https://www.python.org/downloads/) If you're a MAC user, that should already come preinstalled on your machine. Although Python 3 exists, you'll want Python 2 (both of these major versions are being improved in parallel). Python 2.7.10 is the one I've been using on my Mac. -If you're a WINDOWS user, [download the latest 2.* version from here](https://www.python.org/downloads/release/python-2710/). Depending on which version of Python you have installed, you may need to install "pip" if your Python installation didn't come with it. To do that easily, run "python pip_setup.py install" from the SeleniumBase home directory. If that didn't do the trick for you, [get pip here](https://pip.pypa.io/en/latest/installing/). +If you're a WINDOWS user, [download the latest 2.* version from here](https://www.python.org/downloads/release/python-2710/). Depending on which version of Python you have installed, you may need to install "pip" if your Python installation didn't come with it. If you don't have it installed, you can [get pip here](https://pip.pypa.io/en/latest/installing/). [Homebrew](http://brew.sh/) + [Git](http://git-scm.com/) @@ -71,7 +71,7 @@ To save time from having to source virtualenvwrapper again when you open a new w If you haven't already, you'll want to [Download Firefox](https://www.mozilla.org/en-US/firefox/new/) and either [Download Chrome](https://www.google.com/chrome/browser/desktop/index.html) or [Download Chromium](https://download-chromium.appspot.com/). -**Step 1:** Download or Clone SeleniumBase to your local machine: +### **Step 1:** Download or Clone SeleniumBase to your local machine: If you're using Git, you can fork the repository on GitHub to create your personal copy. This is important because you'll want to add your own configurations, credentials, settings, etc. Now you can clone your forked copy to your personal computer. You can use a tool such as [SourceTree](http://www.sourcetreeapp.com/) to make things easier by providing you with a simple-to-use user interface for viewing and managing your git commits and status. @@ -82,7 +82,7 @@ cd seleniumbase (NOTE: If you decided to download SeleniumBase rather than Git-cloning it, you can skip the above step.) -**Step 2:** Create a virtualenv for seleniumbase: +### **Step 2:** Create a virtualenv for seleniumbase: ```bash mkvirtualenv seleniumbase @@ -114,7 +114,7 @@ To delete a virtual environment that you no longer need, use the following comma rmvirtualenv [NAME OF VIRTUAL ENV TO REMOVE] ``` -**Step 3:** Install necessary packages from the SeleniumBase folder and compile the test framework +### **Step 3:** Install necessary packages from the SeleniumBase folder and compile the test framework If you don't desire connecting to a MySQL DB to record the results of your local test runs, run this command: @@ -153,7 +153,7 @@ defaults write com.apple.finder AppleShowAllFiles -bool true (You may need to reopen the MAC Finder window to see changes from that.) -**Step 4:** You can verify that Chromedriver and Selenium were successfully installed by checking inside a python command prompt: +### **Step 4:** You can verify that Chromedriver and Selenium were successfully installed by checking inside a python command prompt: ```bash python @@ -165,7 +165,7 @@ python ``` -**Step 5:** Now to verify the test framework installation by writing a simple Selenium script that performs basic actions such as navigating to a web page, clicking, waiting for page elements to appear, typing in text, scraping text on a page, and verifying text. (copy/paste this into a new file called "my_first_test.py"). This may be a good time to read up on css selectors. If you use Chrome, you can right-click on a page and select "Inspect Element" to see the details you need to create such a script. At a quick glance, dots are for class names and pound signs are for IDs. +### **Step 5:** Now to verify the test framework installation by writing a simple Selenium script that performs basic actions such as navigating to a web page, clicking, waiting for page elements to appear, typing in text, scraping text on a page, and verifying text. (copy/paste this into a new file called "my_first_test.py"). This may be a good time to read up on css selectors. If you use Chrome, you can right-click on a page and select "Inspect Element" to see the details you need to create such a script. At a quick glance, dots are for class names and pound signs are for IDs. ```python from seleniumbase import BaseCase @@ -225,7 +225,7 @@ Here are some other useful nosetest arguments that you may want to append to you --with-id # If -v is also used, will number the tests for easy counting. ``` -**Step 6:** Complete the setup +### **Step 6:** Complete the setup If you're planning on using the full power of this test framework, there are a few more things you'll want to do: From 15414491b2a9a28e4a8f3d6df640bd9e0b9215aa Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 20 Nov 2015 18:50:48 -0500 Subject: [PATCH 172/219] Update the Readme --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 92c7e5d48fa1..9600ad7304f3 100755 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ####(WINDOWS users: You'll need to make a few modifications to the setup steps listed here. For starters, you won't be able to use the "brew install" command since that's MAC-only. Instead, download the requirements mentioned directly from the web. I'll provide you with links to save you time. You'll also want to put downloaded files into your [PATH](http://java.com/en/download/help/path.xml).) ####(DOCKER users: If you want to run browser automation with Docker, see the [Docker_README](https://github.com/mdmintz/SeleniumBase/blob/master/Docker_README.md)) -### **Step 0:** Get the requirements: +#### **Step 0:** Get the requirements: [Python 2.*](https://www.python.org/downloads/) @@ -71,7 +71,7 @@ To save time from having to source virtualenvwrapper again when you open a new w If you haven't already, you'll want to [Download Firefox](https://www.mozilla.org/en-US/firefox/new/) and either [Download Chrome](https://www.google.com/chrome/browser/desktop/index.html) or [Download Chromium](https://download-chromium.appspot.com/). -### **Step 1:** Download or Clone SeleniumBase to your local machine: +#### **Step 1:** Download or Clone SeleniumBase to your local machine: If you're using Git, you can fork the repository on GitHub to create your personal copy. This is important because you'll want to add your own configurations, credentials, settings, etc. Now you can clone your forked copy to your personal computer. You can use a tool such as [SourceTree](http://www.sourcetreeapp.com/) to make things easier by providing you with a simple-to-use user interface for viewing and managing your git commits and status. @@ -82,7 +82,7 @@ cd seleniumbase (NOTE: If you decided to download SeleniumBase rather than Git-cloning it, you can skip the above step.) -### **Step 2:** Create a virtualenv for seleniumbase: +#### **Step 2:** Create a virtualenv for seleniumbase: ```bash mkvirtualenv seleniumbase @@ -114,7 +114,7 @@ To delete a virtual environment that you no longer need, use the following comma rmvirtualenv [NAME OF VIRTUAL ENV TO REMOVE] ``` -### **Step 3:** Install necessary packages from the SeleniumBase folder and compile the test framework +#### **Step 3:** Install necessary packages from the SeleniumBase folder and compile the test framework If you don't desire connecting to a MySQL DB to record the results of your local test runs, run this command: @@ -153,7 +153,7 @@ defaults write com.apple.finder AppleShowAllFiles -bool true (You may need to reopen the MAC Finder window to see changes from that.) -### **Step 4:** You can verify that Chromedriver and Selenium were successfully installed by checking inside a python command prompt: +#### **Step 4:** You can verify that Chromedriver and Selenium were successfully installed by checking inside a python command prompt: ```bash python @@ -165,7 +165,9 @@ python ``` -### **Step 5:** Now to verify the test framework installation by writing a simple Selenium script that performs basic actions such as navigating to a web page, clicking, waiting for page elements to appear, typing in text, scraping text on a page, and verifying text. (copy/paste this into a new file called "my_first_test.py"). This may be a good time to read up on css selectors. If you use Chrome, you can right-click on a page and select "Inspect Element" to see the details you need to create such a script. At a quick glance, dots are for class names and pound signs are for IDs. +#### **Step 5:** Verify that SeleniumBase was successfully installed by running examples + +You can verify the installation of SeleniumBase by writing a simple script to perform basic actions such as navigating to a web page, clicking, waiting for page elements to appear, typing in text, scraping text on a page, and verifying text. (Copy/paste the following code into a new file called "my_first_test.py"). This may be a good time to read up on css selectors. If you use Chrome, you can right-click on a page and select "Inspect Element" to see the details you need to create such a script. At a quick glance, dots are for class names and pound signs are for IDs. ```python from seleniumbase import BaseCase @@ -225,7 +227,7 @@ Here are some other useful nosetest arguments that you may want to append to you --with-id # If -v is also used, will number the tests for easy counting. ``` -### **Step 6:** Complete the setup +#### **Step 6:** Complete the setup If you're planning on using the full power of this test framework, there are a few more things you'll want to do: From 088c650aba07916310f92a01246dedb47a5283b3 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 20 Nov 2015 18:54:03 -0500 Subject: [PATCH 173/219] Update the Readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9600ad7304f3..a93d42052f9d 100755 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ defaults write com.apple.finder AppleShowAllFiles -bool true (You may need to reopen the MAC Finder window to see changes from that.) -#### **Step 4:** You can verify that Chromedriver and Selenium were successfully installed by checking inside a python command prompt: +#### **Step 4:** Verify that Selenium and Chromedriver were successfully installed by checking inside a python command prompt: ```bash python @@ -165,7 +165,7 @@ python ``` -#### **Step 5:** Verify that SeleniumBase was successfully installed by running examples +#### **Step 5:** Verify that SeleniumBase was successfully installed by running example tests You can verify the installation of SeleniumBase by writing a simple script to perform basic actions such as navigating to a web page, clicking, waiting for page elements to appear, typing in text, scraping text on a page, and verifying text. (Copy/paste the following code into a new file called "my_first_test.py"). This may be a good time to read up on css selectors. If you use Chrome, you can right-click on a page and select "Inspect Element" to see the details you need to create such a script. At a quick glance, dots are for class names and pound signs are for IDs. From 5f9d5f75585357a2ed6d5a9dca8552beb661fdf5 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 20 Nov 2015 18:59:04 -0500 Subject: [PATCH 174/219] Update the Readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a93d42052f9d..b418daa39f32 100755 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ####(WINDOWS users: You'll need to make a few modifications to the setup steps listed here. For starters, you won't be able to use the "brew install" command since that's MAC-only. Instead, download the requirements mentioned directly from the web. I'll provide you with links to save you time. You'll also want to put downloaded files into your [PATH](http://java.com/en/download/help/path.xml).) ####(DOCKER users: If you want to run browser automation with Docker, see the [Docker_README](https://github.com/mdmintz/SeleniumBase/blob/master/Docker_README.md)) -#### **Step 0:** Get the requirements: +#### **Step 0:** Get the requirements [Python 2.*](https://www.python.org/downloads/) @@ -71,7 +71,7 @@ To save time from having to source virtualenvwrapper again when you open a new w If you haven't already, you'll want to [Download Firefox](https://www.mozilla.org/en-US/firefox/new/) and either [Download Chrome](https://www.google.com/chrome/browser/desktop/index.html) or [Download Chromium](https://download-chromium.appspot.com/). -#### **Step 1:** Download or Clone SeleniumBase to your local machine: +#### **Step 1:** Download or Clone SeleniumBase to your local machine If you're using Git, you can fork the repository on GitHub to create your personal copy. This is important because you'll want to add your own configurations, credentials, settings, etc. Now you can clone your forked copy to your personal computer. You can use a tool such as [SourceTree](http://www.sourcetreeapp.com/) to make things easier by providing you with a simple-to-use user interface for viewing and managing your git commits and status. @@ -82,7 +82,7 @@ cd seleniumbase (NOTE: If you decided to download SeleniumBase rather than Git-cloning it, you can skip the above step.) -#### **Step 2:** Create a virtualenv for seleniumbase: +#### **Step 2:** Create a virtualenv for seleniumbase ```bash mkvirtualenv seleniumbase @@ -153,7 +153,7 @@ defaults write com.apple.finder AppleShowAllFiles -bool true (You may need to reopen the MAC Finder window to see changes from that.) -#### **Step 4:** Verify that Selenium and Chromedriver were successfully installed by checking inside a python command prompt: +#### **Step 4:** Verify that Selenium and Chromedriver were successfully installed by checking inside a python command prompt ```bash python From fc313bfd64068f3b9ee995599e53fd3ad0571c2f Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sat, 21 Nov 2015 13:14:13 -0500 Subject: [PATCH 175/219] Use the latest Selenium Server --- grid_files/start-selenium-node.bat | 2 +- grid_files/start-selenium-server.sh | 2 +- seleniumbase/core/selenium_launcher.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/grid_files/start-selenium-node.bat b/grid_files/start-selenium-node.bat index cf8de77fbdcc..6668d1bf4ea6 100644 --- a/grid_files/start-selenium-node.bat +++ b/grid_files/start-selenium-node.bat @@ -1,2 +1,2 @@ cd c:\ -java -jar selenium-server-standalone-2.47.1.jar -role node -hub http://[ENTER URL OF THE GRID HUB SERVER]:4444/grid/register -browser browserName=chrome,maxInstances=5 -browser browserName=firefox,maxInstances=5 -browser browserName="internet explorer",maxInstances=1 \ No newline at end of file +java -jar selenium-server-standalone-2.48.2.jar -role node -hub http://[ENTER URL OF THE GRID HUB SERVER]:4444/grid/register -browser browserName=chrome,maxInstances=5 -browser browserName=firefox,maxInstances=5 -browser browserName="internet explorer",maxInstances=1 \ No newline at end of file diff --git a/grid_files/start-selenium-server.sh b/grid_files/start-selenium-server.sh index 122b6f405bc2..ce62a2b5f767 100755 --- a/grid_files/start-selenium-server.sh +++ b/grid_files/start-selenium-server.sh @@ -1,2 +1,2 @@ #!/bin/bash -java -jar selenium-server-standalone-2.47.1.jar -role hub \ No newline at end of file +java -jar selenium-server-standalone-2.48.2.jar -role hub \ No newline at end of file diff --git a/seleniumbase/core/selenium_launcher.py b/seleniumbase/core/selenium_launcher.py index b850153257f8..5bc10840780a 100755 --- a/seleniumbase/core/selenium_launcher.py +++ b/seleniumbase/core/selenium_launcher.py @@ -6,8 +6,8 @@ import urllib import time -SELENIUM_JAR = "http://selenium-release.storage.googleapis.com/2.47/selenium-server-standalone-2.47.1.jar" -JAR_FILE = "selenium-server-standalone-2.47.1.jar" +SELENIUM_JAR = "http://selenium-release.storage.googleapis.com/2.48/selenium-server-standalone-2.48.2.jar" +JAR_FILE = "selenium-server-standalone-2.48.2.jar" def download_selenium(): """ From 0f7ac0b458006aa9dbce8aef7bf5e79e169634cd Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sat, 21 Nov 2015 13:50:49 -0500 Subject: [PATCH 176/219] Improve the Readme for the Selenium Grid Hub --- grid_files/Grid_Hub_Server_README.md | 18 ++++++++++++++++++ grid_files/ReadMe.txt | 7 ------- 2 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 grid_files/Grid_Hub_Server_README.md delete mode 100644 grid_files/ReadMe.txt diff --git a/grid_files/Grid_Hub_Server_README.md b/grid_files/Grid_Hub_Server_README.md new file mode 100644 index 000000000000..0cae70a70b9e --- /dev/null +++ b/grid_files/Grid_Hub_Server_README.md @@ -0,0 +1,18 @@ +## Notes on using the Selenium Grid Hub + +The Selenium Grid Hub allows you to distribute tests to run in parallel across multiple machines. Each machine can then run its own allocation of tests in parallel. This allows you to run an entire test suite quickly, which may be important if you have a lot of tests to run. Machines can be personal computers, data centers, or virtual machines in the cloud. You can also create your own virtual machine by using a tool such as Docker (see the [Docker_README](https://github.com/mdmintz/SeleniumBase/blob/master/Docker_README.md)). + +### Running the Selenium Grid Hub + +You may need to download selenium-server-standalone-2.48.2.jar (or the latest version) separately. That file is not present with this repository to save space. You can download that file from here: +http://docs.seleniumhq.org/download/ +or here: +http://selenium-release.storage.googleapis.com/index.html?path=2.48/ +Once you have downloaded the jar file, put it in this folder (the "grid_files" folder). + +More detailed info about connecting to the Selenium Grid Hub can be found here: +https://theintern.github.io/intern/#selenium-grid +and here: +https://github.com/SeleniumHQ/selenium/wiki/Grid2 +For even more information, try here: +https://github.com/SeleniumHQ/selenium/wiki diff --git a/grid_files/ReadMe.txt b/grid_files/ReadMe.txt deleted file mode 100644 index e07c9b89dd8d..000000000000 --- a/grid_files/ReadMe.txt +++ /dev/null @@ -1,7 +0,0 @@ -You may need to download selenium-server-standalone-2.47.1.jar (or the latest version) separately. -That file is not present with this repository to save space. -You can download that file from here: http://docs.seleniumhq.org/download/ or here: http://selenium-release.storage.googleapis.com/index.html?path=2.47/ -Once you have downloaded the jar file, put it in this folder. - -More detailed info about connecting to the Selenium Grid Hub can be found here: https://theintern.github.io/intern/#selenium-grid and here: https://github.com/SeleniumHQ/selenium/wiki/Grid2 -For even more information, try here: https://github.com/SeleniumHQ/selenium/wiki From 8be2d95283e16e9577068727e6a5bf2a408df2e3 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sat, 21 Nov 2015 13:55:24 -0500 Subject: [PATCH 177/219] Update the Grid Hub Server Readme --- grid_files/Grid_Hub_Server_README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/grid_files/Grid_Hub_Server_README.md b/grid_files/Grid_Hub_Server_README.md index 0cae70a70b9e..5d10cc283b70 100644 --- a/grid_files/Grid_Hub_Server_README.md +++ b/grid_files/Grid_Hub_Server_README.md @@ -5,14 +5,14 @@ The Selenium Grid Hub allows you to distribute tests to run in parallel across m ### Running the Selenium Grid Hub You may need to download selenium-server-standalone-2.48.2.jar (or the latest version) separately. That file is not present with this repository to save space. You can download that file from here: -http://docs.seleniumhq.org/download/ +* http://docs.seleniumhq.org/download/ or here: -http://selenium-release.storage.googleapis.com/index.html?path=2.48/ +* http://selenium-release.storage.googleapis.com/index.html?path=2.48/ Once you have downloaded the jar file, put it in this folder (the "grid_files" folder). More detailed info about connecting to the Selenium Grid Hub can be found here: -https://theintern.github.io/intern/#selenium-grid +* https://theintern.github.io/intern/#selenium-grid and here: -https://github.com/SeleniumHQ/selenium/wiki/Grid2 -For even more information, try here: -https://github.com/SeleniumHQ/selenium/wiki +* https://github.com/SeleniumHQ/selenium/wiki/Grid2 +For even more information, look here: +* https://github.com/SeleniumHQ/selenium/wiki From cd4450b6f4c56afa20de6ef1b3a99c5f36af677e Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Nov 2015 03:00:01 -0500 Subject: [PATCH 178/219] Adding pytest support --- .gitignore | 4 ++++ README.md | 16 +++++++++++++-- conftest.py | 23 +++++++++++++++++++++ requirements.txt | 1 + seleniumbase/core/browser_launcher.py | 27 +++++++++++++++++++++++++ seleniumbase/fixtures/base_case.py | 23 +++++++++++++++++++++ seleniumbase/plugins/selenium_plugin.py | 21 ++----------------- server_requirements.txt | 1 + 8 files changed, 95 insertions(+), 21 deletions(-) create mode 100755 conftest.py create mode 100755 seleniumbase/core/browser_launcher.py diff --git a/.gitignore b/.gitignore index 153ba5069b7d..f55b1c54c83e 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,10 @@ pip-log.txt .tox nosetests.xml +# py.test +.cache/* +.pytest_config + # Developer .project .pydevproject diff --git a/README.md b/README.md index b418daa39f32..18cd493b4dc3 100755 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## **Web Automation For Everything** #### Features include: -* ALL the power of Python, WebDriver, and Nosetests +* ALL the power of Python, WebDriver, and Nosetests (pytest also supported) * Easy integration with [Jenkins](http://jenkins-ci.org/), [Selenium Grid](http://docs.seleniumhq.org/projects/grid/), [Docker](https://www.docker.com/), and [AWS](http://aws.amazon.com/) * A flexible logging system for storing test data, results, and screenshots * Libraries for code-simplification, time-saving, and reusability @@ -216,7 +216,7 @@ If you need to debug things on the fly (in case of errors), use this line to run nosetests my_first_test.py --browser=chrome --with-selenium --pdb --pdb-failures -s ``` -The above code will leave your browser window open in case there's a failure, which is possible if the web pages from the example change the data that's displayed on the page. (ipdb commands: 'c', 's', 'n' => continue, step, next). +The above code (with --pdb) will leave your browser window open in case there's a failure, which is possible if the web pages from the example change the data that's displayed on the page. (ipdb commands: 'c', 's', 'n' => continue, step, next). Here are some other useful nosetest arguments that you may want to append to your run commands: @@ -227,6 +227,18 @@ Here are some other useful nosetest arguments that you may want to append to you --with-id # If -v is also used, will number the tests for easy counting. ``` +Due to high demand, pytest support has been added. You can run the above sample script in pytest like this: + +```bash +py.test my_first_test.py --browser=chrome -s + +py.test my_first_test.py --browser=phantomjs -s + +py.test my_first_test.py --browser=firefox -s +``` + +(NOTE: If you want to run tests using pytest, you won't have access to the nosetest plugins that are specially defined in SeleniumBase. The nosetest plugins all start with "--with-" and are appended to the test selection in the run commands.) + #### **Step 6:** Complete the setup If you're planning on using the full power of this test framework, there are a few more things you'll want to do: diff --git a/conftest.py b/conftest.py new file mode 100755 index 000000000000..359dac59bc0a --- /dev/null +++ b/conftest.py @@ -0,0 +1,23 @@ +from seleniumbase.fixtures import constants + +""" +pytest options: +browser => The browser type to spin up. Example: (--browser=chrome) +""" + +def pytest_addoption(parser): + parser.addoption('--browser', action="store", + dest='browser', + choices=constants.Browser.VERSION.keys(), + default=constants.Browser.FIREFOX, + help="""Specifies the browser to use. Default = FireFox. + If you want to use Chrome, explicitly indicate that.""") + parse_args(parser) + + +def parse_args(config): + browser = config._getparser().parse_known_args()[0].browser + pytest_config = '.pytest_config' + config_file = open(pytest_config, 'w+') + config_file.write(browser) + config_file.close() diff --git a/requirements.txt b/requirements.txt index b4a46348e2d5..18f2b33dbb51 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ selenium==2.48.0 nose==1.3.7 +pytest==2.8.3 requests==2.7.0 urllib3==1.10.4 BeautifulSoup==3.2.1 diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py new file mode 100755 index 000000000000..75d2d637b3ba --- /dev/null +++ b/seleniumbase/core/browser_launcher.py @@ -0,0 +1,27 @@ +from selenium import webdriver +from seleniumbase.fixtures import constants + +def get_driver(browser_name): + ''' + Spins up a new web browser and returns the driver. + Tests that run with pytest spin up the browser from here. + Can also be used to spin up additional browsers for the same test. + ''' + if browser_name == constants.Browser.FIREFOX: + try: + profile = webdriver.FirefoxProfile() + profile.set_preference("reader.parse-on-load.enabled", False) + return webdriver.Firefox(profile) + except: + return webdriver.Firefox() + if browser_name == constants.Browser.INTERNET_EXPLORER: + return webdriver.Ie() + if browser_name == constants.Browser.PHANTOM_JS: + return webdriver.PhantomJS() + if browser_name == constants.Browser.GOOGLE_CHROME: + try: + chrome_options = webdriver.ChromeOptions() + chrome_options.add_argument("--allow-file-access-from-files") + return webdriver.Chrome(chrome_options=chrome_options) + except Exception: + return webdriver.Chrome() diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 45f53cf9e05b..da30c742a520 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1,8 +1,10 @@ import json import time +import pytest import logging import unittest from seleniumbase.config import settings +from seleniumbase.core import browser_launcher from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.common.by import By import page_loads, page_interactions, page_utils @@ -21,6 +23,27 @@ def __init__(self, *args, **kwargs): except Exception: pass self.environment = None + self.is_pytest = None + + + def setUp(self): + try: + pytest._preloadplugins() + self.is_pytest = True + except Exception: + # Not using pytest (probably nosetests) + self.is_pytest = False + return + if self.is_pytest: + pytest_config = open('.pytest_config', 'r') + browser_name = pytest_config.read() + pytest_config.close() + self.driver = browser_launcher.get_driver(browser_name) + + + def tearDown(self): + if self.is_pytest: + self.driver.quit() def find_visible_elements(self, selector, by=By.CSS_SELECTOR): diff --git a/seleniumbase/plugins/selenium_plugin.py b/seleniumbase/plugins/selenium_plugin.py index 4ad9fde8519c..9e741fc2434c 100755 --- a/seleniumbase/plugins/selenium_plugin.py +++ b/seleniumbase/plugins/selenium_plugin.py @@ -8,6 +8,7 @@ from nose.plugins import Plugin from selenium import webdriver from seleniumbase.core import selenium_launcher +from seleniumbase.core import browser_launcher from seleniumbase.fixtures import constants @@ -140,22 +141,4 @@ def __select_browser(self, browser_name): self.options.port), self.browser_settings) else: - if browser_name == constants.Browser.FIREFOX: - try: - profile = webdriver.FirefoxProfile() - profile.set_preference("reader.parse-on-load.enabled", False) - return webdriver.Firefox(profile) - except: - return webdriver.Firefox() - if browser_name == constants.Browser.INTERNET_EXPLORER: - return webdriver.Ie() - if browser_name == constants.Browser.PHANTOM_JS: - return webdriver.PhantomJS() - if browser_name == constants.Browser.GOOGLE_CHROME: - try: - # Make it possible for Chrome to save screenshot files to disk. - chrome_options = webdriver.ChromeOptions() - chrome_options.add_argument("--allow-file-access-from-files") - return webdriver.Chrome(chrome_options=chrome_options) - except Exception: - return webdriver.Chrome() + return browser_launcher.get_driver(browser_name) diff --git a/server_requirements.txt b/server_requirements.txt index f7b7d02d72c4..5b036db75a23 100755 --- a/server_requirements.txt +++ b/server_requirements.txt @@ -1,5 +1,6 @@ selenium==2.48.0 nose==1.3.7 +pytest==2.8.3 requests==2.7.0 urllib3==1.10.4 BeautifulSoup==3.2.1 From b8a404dbc3c590aafc78df2b32eeca0566ad63eb Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Nov 2015 03:38:05 -0500 Subject: [PATCH 179/219] Update details in the pytest configuration file --- conftest.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/conftest.py b/conftest.py index 359dac59bc0a..dc1cc233f143 100755 --- a/conftest.py +++ b/conftest.py @@ -1,17 +1,16 @@ +""" This is the pytest configuration file """ + from seleniumbase.fixtures import constants -""" -pytest options: -browser => The browser type to spin up. Example: (--browser=chrome) -""" def pytest_addoption(parser): parser.addoption('--browser', action="store", dest='browser', choices=constants.Browser.VERSION.keys(), default=constants.Browser.FIREFOX, - help="""Specifies the browser to use. Default = FireFox. - If you want to use Chrome, explicitly indicate that.""") + help="""Specifies the web browser to use. Default = FireFox. + If you want to use Chrome, explicitly indicate that. + Example: (--browser=chrome)""") parse_args(parser) From c8544d331519e41b6919fc4160ee2445f1d75fbe Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Nov 2015 03:44:45 -0500 Subject: [PATCH 180/219] Add pytest to Docker requirements --- docker/docker_requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/docker_requirements.txt b/docker/docker_requirements.txt index 824d58ad3a98..f8001f4bdb47 100755 --- a/docker/docker_requirements.txt +++ b/docker/docker_requirements.txt @@ -1,5 +1,6 @@ selenium==2.48.0 nose==1.3.7 +pytest==2.8.3 requests==2.7.0 urllib3==1.10.4 BeautifulSoup==3.2.1 From 88e418ab47ad5b462f0138a6a85579b198c0d0c2 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Nov 2015 15:05:23 -0500 Subject: [PATCH 181/219] Make the pytest code more robust --- conftest.py | 29 +++++++++++++++++++++++++---- seleniumbase/fixtures/base_case.py | 5 ++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/conftest.py b/conftest.py index dc1cc233f143..e813013d95af 100755 --- a/conftest.py +++ b/conftest.py @@ -1,5 +1,6 @@ """ This is the pytest configuration file """ +import os from seleniumbase.fixtures import constants @@ -8,15 +9,35 @@ def pytest_addoption(parser): dest='browser', choices=constants.Browser.VERSION.keys(), default=constants.Browser.FIREFOX, - help="""Specifies the web browser to use. Default = FireFox. + help="""Specifies the web browser to use. Default=FireFox. If you want to use Chrome, explicitly indicate that. Example: (--browser=chrome)""") - parse_args(parser) + parser.addoption('--is_pytest', action="store_true", + dest='is_pytest', + default=True, + help="""This is used by the BaseCase class to tell apart + pytest runs from nosetest runs.""") -def parse_args(config): - browser = config._getparser().parse_known_args()[0].browser +def pytest_configure(config): + browser = config.getoption('browser') pytest_config = '.pytest_config' config_file = open(pytest_config, 'w+') config_file.write(browser) config_file.close() + + +def pytest_unconfigure(): + pytest_config = '.pytest_config' + if os.path.isfile(pytest_config): + os.remove(pytest_config) + + +def pytest_runtest_setup(): + # A placeholder for a method that runs before every test with pytest + pass + + +def pytest_runtest_teardown(): + # A placeholder for a method that runs after every test with pytest + pass diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index da30c742a520..97d7a9cc1e07 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -28,12 +28,11 @@ def __init__(self, *args, **kwargs): def setUp(self): try: - pytest._preloadplugins() - self.is_pytest = True + # This raises an exception if the test is not coming from pytest + self.is_pytest = pytest.config.option.is_pytest except Exception: # Not using pytest (probably nosetests) self.is_pytest = False - return if self.is_pytest: pytest_config = open('.pytest_config', 'r') browser_name = pytest_config.read() From 803ac38bf88b6e794299c1ba3145c45bdb48e195 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Nov 2015 16:07:02 -0500 Subject: [PATCH 182/219] Updated test description --- examples/rate_limiting_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/rate_limiting_test.py b/examples/rate_limiting_test.py index 19d4c422edf2..30eb7637ba37 100755 --- a/examples/rate_limiting_test.py +++ b/examples/rate_limiting_test.py @@ -9,5 +9,6 @@ def print_item(self, item): print item def test_rate_limited_printing(self): + print "\nRunning rate-limited print test:" for item in xrange(10): self.print_item(item) From bb0bac78e7cf8b6089a7bbff779c940c4f1333c5 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Nov 2015 16:10:17 -0500 Subject: [PATCH 183/219] Include a setup.cfg file for configuration simplification --- README.md | 4 +++- examples/setup.cfg | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100755 examples/setup.cfg diff --git a/README.md b/README.md index 18cd493b4dc3..b1c9471e9858 100755 --- a/README.md +++ b/README.md @@ -274,7 +274,9 @@ nosetests [YOUR_TEST_FILE].py --config=[MY_CONFIG_FILE].cfg -s ``` So much easier on the eyes :) -Remember, nosetests will run every method in that python file that starts with "test" in the method name. You can be more specific on what to run by doing something like: +You can simplify that even more by using a setup.cfg file, such as the one provided for you in the examples folder. If you kick off a test run from within the folder that setup.cfg is location in, that file will automatically be used as your configuration, meaning that you wouldn't have to type out all the plugins that you want to use (or include a config file) everytime you run tests. + +If you tell nosetests to run an entire file, it will run every method in that python file that starts with "test". You can be more specific on what to run by doing something like: ```bash nosetests [YOUR_TEST_FILE].py:[SOME_CLASS_NAME].test_[SOME_TEST_NAME] --config=[MY_CONFIG_FILE].cfg -s diff --git a/examples/setup.cfg b/examples/setup.cfg new file mode 100755 index 000000000000..061dbdd10827 --- /dev/null +++ b/examples/setup.cfg @@ -0,0 +1,6 @@ +[nosetests] + +; This is the config file for default values used during nosetest runs + +nocapture=1 ; Displays print statements from output. Undo this by using: --nologcapture +logging-level=INFO ; INFO keeps the logs much cleaner than using DEBUG From d60dbaf0b9860894bda84ee17b49e1957a11f746 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Nov 2015 18:15:11 -0500 Subject: [PATCH 184/219] Updated Docker readme --- Docker_README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Docker_README.md b/Docker_README.md index 70eb3c7f05a2..2e3f5cf7bb43 100755 --- a/Docker_README.md +++ b/Docker_README.md @@ -1,18 +1,18 @@ ## Docker setup instructions for SeleniumBase -#### 1. Get the Docker Toolbox from https://www.docker.com/toolbox and install it. +#### 1. Get the Docker Toolbox from https://www.docker.com/docker-toolbox and install it. #### 2. Setup your Docker environment: - docker-machine create --driver virtualbox default + docker-machine create --driver virtualbox seleniumbase #### 3. Start up your Docker environment: - docker-machine restart default + docker-machine restart seleniumbase #### 4. Configure your shell: - eval "$(docker-machine env default)" + eval "$(docker-machine env seleniumbase)" #### 5. Go to the SeleniumBase home directory. (That's where "Dockerfile" is located) @@ -43,11 +43,16 @@ Here are a few of those cleanup commands: docker images | grep "" | awk '{print $3}' | xargs docker rmi docker rm 'docker ps --no-trunc -aq' -If you want to completely remove all of your docker containers and images, use these commands: (If there's nothing to delete, those commands will return an error.) +If you want to completely remove all of your Docker containers and images, use these commands: (If there's nothing to delete, those commands will return an error.) docker rm $(docker ps -a -q) docker rmi $(docker images -q) +Finally, if you want to wipe out your SeleniumBase Docker virtualbox, use these commands: + + docker-machine kill seleniumbase + docker-machine rm seleniumbase + #### 12. (Optional) More reading on Docker can be found here: * https://docs.docker.com * https://docs.docker.com/mac/started/ From 4b6d5ce5adaa294ccfb5a70da108d2ab135d04bb Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Nov 2015 19:09:49 -0500 Subject: [PATCH 185/219] Docker readme update --- Docker_README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Docker_README.md b/Docker_README.md index 2e3f5cf7bb43..bb6be3bd2e61 100755 --- a/Docker_README.md +++ b/Docker_README.md @@ -2,11 +2,11 @@ #### 1. Get the Docker Toolbox from https://www.docker.com/docker-toolbox and install it. -#### 2. Setup your Docker environment: +#### 2. Create your SeleniumBase Docker environment: docker-machine create --driver virtualbox seleniumbase -#### 3. Start up your Docker environment: +#### 3. If your Docker environment ever goes down for any reason, you can bring it back up with a restart: docker-machine restart seleniumbase From 97342e0f05f12d0b67d95b89054c2655a4cc108b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 24 Nov 2015 01:29:21 -0500 Subject: [PATCH 186/219] Adding useful comments for pytest-related methods --- seleniumbase/fixtures/base_case.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 97d7a9cc1e07..bf159c98d960 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -26,6 +26,8 @@ def __init__(self, *args, **kwargs): self.is_pytest = None + # pytest browser management + # [Be careful if a subclass of BaseCase overrides setUp()] def setUp(self): try: # This raises an exception if the test is not coming from pytest @@ -40,6 +42,8 @@ def setUp(self): self.driver = browser_launcher.get_driver(browser_name) + # pytest browser management + # [Be careful if a subclass of BaseCase overrides tearDown()] def tearDown(self): if self.is_pytest: self.driver.quit() From 6797ac50f65cc79adfb77ff4c808541aa48bbd51 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 24 Nov 2015 19:55:45 -0500 Subject: [PATCH 187/219] Choosing a different xkcd comic for the example test --- README.md | 2 +- examples/my_first_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b1c9471e9858..fca8cb38522f 100755 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ from seleniumbase import BaseCase class MyTestClass(BaseCase): def test_basic(self): - self.open("http://xkcd.com/1513/") + self.open("http://xkcd.com/353/") self.wait_for_element_visible("div#comic") self.click('a[rel="license"]') text = self.wait_for_element_visible('center').text diff --git a/examples/my_first_test.py b/examples/my_first_test.py index 6d2d0d981739..9a34d0384eaa 100755 --- a/examples/my_first_test.py +++ b/examples/my_first_test.py @@ -3,7 +3,7 @@ class MyTestClass(BaseCase): def test_basic(self): - self.open("http://xkcd.com/1513/") + self.open("http://xkcd.com/353/") self.wait_for_element_visible("div#comic") self.click('a[rel="license"]') text = self.wait_for_element_visible('center').text From a07d145f6f4417bfab489fe158aa88c869ddca40 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 30 Nov 2015 18:19:10 -0500 Subject: [PATCH 188/219] Add option to pass extra data to tests from the command line (pytest) --- conftest.py | 9 ++++++++- seleniumbase/fixtures/base_case.py | 11 ++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/conftest.py b/conftest.py index e813013d95af..1b79b7c606b5 100755 --- a/conftest.py +++ b/conftest.py @@ -17,13 +17,20 @@ def pytest_addoption(parser): default=True, help="""This is used by the BaseCase class to tell apart pytest runs from nosetest runs.""") + parser.addoption('--data', dest='data', + default=None, + help='Extra data to pass from the command line.') def pytest_configure(config): browser = config.getoption('browser') + data = '' + if config.getoption('data') is not None: + data = config.getoption('data') pytest_config = '.pytest_config' config_file = open(pytest_config, 'w+') - config_file.write(browser) + config_file.write("browser:::%s\n" % browser) + config_file.write("data:::%s\n" % data) config_file.close() diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index bf159c98d960..f2f812ae12f5 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -37,7 +37,16 @@ def setUp(self): self.is_pytest = False if self.is_pytest: pytest_config = open('.pytest_config', 'r') - browser_name = pytest_config.read() + file_data = pytest_config.read() + file_lines = file_data.split('\n') + for line in file_lines: + line_items = line.split(':::') + if line_items[0] == 'browser': + browser_name = line_items[1] + elif line_items[0] == 'data': + data = line_items[1] + else: + pass pytest_config.close() self.driver = browser_launcher.get_driver(browser_name) From c2ac9515e789d46360a68ddb2345e1eb65adc86e Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 30 Nov 2015 19:42:13 -0500 Subject: [PATCH 189/219] Add pytest way of using the nosetests --with-selenium plugin --- README.md | 6 +++--- conftest.py | 7 +++++++ seleniumbase/fixtures/base_case.py | 29 +++++++++++------------------ 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index fca8cb38522f..5f3d0f78746e 100755 --- a/README.md +++ b/README.md @@ -230,11 +230,11 @@ Here are some other useful nosetest arguments that you may want to append to you Due to high demand, pytest support has been added. You can run the above sample script in pytest like this: ```bash -py.test my_first_test.py --browser=chrome -s +py.test my_first_test.py --with-selenium --browser=chrome -s -py.test my_first_test.py --browser=phantomjs -s +py.test my_first_test.py --with-selenium --browser=phantomjs -s -py.test my_first_test.py --browser=firefox -s +py.test my_first_test.py --with-selenium --browser=firefox -s ``` (NOTE: If you want to run tests using pytest, you won't have access to the nosetest plugins that are specially defined in SeleniumBase. The nosetest plugins all start with "--with-" and are appended to the test selection in the run commands.) diff --git a/conftest.py b/conftest.py index 1b79b7c606b5..731d19ba31be 100755 --- a/conftest.py +++ b/conftest.py @@ -20,15 +20,22 @@ def pytest_addoption(parser): parser.addoption('--data', dest='data', default=None, help='Extra data to pass from the command line.') + parser.addoption('--with-selenium', action="store_true", + dest='with_selenium', + default=False, + help="Input if tests need to be run with a web browser.") def pytest_configure(config): + with_selenium = config.getoption('with_selenium') browser = config.getoption('browser') data = '' if config.getoption('data') is not None: data = config.getoption('data') + # Create a temporary config file while tests are running pytest_config = '.pytest_config' config_file = open(pytest_config, 'w+') + config_file.write("with_selenium:::%s\n" % with_selenium) config_file.write("browser:::%s\n" % browser) config_file.write("data:::%s\n" % data) config_file.close() diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index f2f812ae12f5..dd866461294c 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -26,7 +26,7 @@ def __init__(self, *args, **kwargs): self.is_pytest = None - # pytest browser management + # pytest-specific config # [Be careful if a subclass of BaseCase overrides setUp()] def setUp(self): try: @@ -36,26 +36,19 @@ def setUp(self): # Not using pytest (probably nosetests) self.is_pytest = False if self.is_pytest: - pytest_config = open('.pytest_config', 'r') - file_data = pytest_config.read() - file_lines = file_data.split('\n') - for line in file_lines: - line_items = line.split(':::') - if line_items[0] == 'browser': - browser_name = line_items[1] - elif line_items[0] == 'data': - data = line_items[1] - else: - pass - pytest_config.close() - self.driver = browser_launcher.get_driver(browser_name) - - - # pytest browser management + self.with_selenium = pytest.config.option.with_selenium + self.browser = pytest.config.option.browser + self.data = pytest.config.option.data + if self.with_selenium: + self.driver = browser_launcher.get_driver(self.browser) + + + # pytest-specific config # [Be careful if a subclass of BaseCase overrides tearDown()] def tearDown(self): if self.is_pytest: - self.driver.quit() + if self.with_selenium: + self.driver.quit() def find_visible_elements(self, selector, by=By.CSS_SELECTOR): From 209263f7354f305aedd19f17cb7ba15af680b7da Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Dec 2015 18:56:42 -0500 Subject: [PATCH 190/219] Flake8 and organization --- seleniumbase/fixtures/base_case.py | 2 +- seleniumbase/fixtures/constants.py | 13 +-- seleniumbase/fixtures/delayed_data_manager.py | 88 +++++++-------- seleniumbase/fixtures/email_manager.py | 100 +++++++----------- seleniumbase/fixtures/errors.py | 2 +- seleniumbase/fixtures/page_interactions.py | 80 ++++++-------- seleniumbase/fixtures/page_loads.py | 96 +++++++++-------- seleniumbase/fixtures/page_utils.py | 10 +- seleniumbase/fixtures/tools.py | 10 +- 9 files changed, 187 insertions(+), 214 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index dd866461294c..e6ad7a9fa229 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -23,12 +23,12 @@ def __init__(self, *args, **kwargs): except Exception: pass self.environment = None - self.is_pytest = None # pytest-specific config # [Be careful if a subclass of BaseCase overrides setUp()] def setUp(self): + self.is_pytest = None try: # This raises an exception if the test is not coming from pytest self.is_pytest = pytest.config.option.is_pytest diff --git a/seleniumbase/fixtures/constants.py b/seleniumbase/fixtures/constants.py index 6100bc392a73..a09c45b9e7c5 100755 --- a/seleniumbase/fixtures/constants.py +++ b/seleniumbase/fixtures/constants.py @@ -2,6 +2,7 @@ This class containts some frequently-used constants """ + class Environment: QA = "qa" STAGING = "staging" @@ -20,18 +21,18 @@ class Browser: HTML_UNIT = "htmlunit" VERSION = { - "firefox" : None, - "ie" : None, - "chrome" : None, - "phantomjs" : None, - "htmlunit" : None + "firefox": None, + "ie": None, + "chrome": None, + "phantomjs": None, + "htmlunit": None } LATEST = { "firefox": None, "ie": None, "chrome": None, - "phantomjs" : None, + "phantomjs": None, "htmlunit": None } diff --git a/seleniumbase/fixtures/delayed_data_manager.py b/seleniumbase/fixtures/delayed_data_manager.py index c1fc71bc77ec..be995c2c07a3 100755 --- a/seleniumbase/fixtures/delayed_data_manager.py +++ b/seleniumbase/fixtures/delayed_data_manager.py @@ -2,11 +2,11 @@ import logging import time import uuid - from seleniumbase.core.mysql import DatabaseManager DEFAULT_EXPIRATION = 1000 * 60 * 60 * 48 + class DelayedTestStorage: """ The database-calling methods of the Delayed Test Framework """ @@ -14,61 +14,65 @@ class DelayedTestStorage: def get_delayed_test_data(self, testcase_address, done=0): """ This method queries the delayedTestData table in the DB and then returns a list of rows with the matching parameters. - :param testcase_address: The ID (address) of the test case. :param done: (0 for test not done or 1 for test done) - - :returns: A list of rows found with the matching testcase_address. None otherwise. + :returns: A list of rows found with the matching testcase_address. """ db = DatabaseManager() query = """SELECT guid,testcaseAddress,insertedAt,expectedResult,done FROM delayedTestData WHERE testcaseAddress=%(testcase_address)s AND done=%(done)s""" - data = db.fetchall_query_and_close(query, {"testcase_address":testcase_address, - "done":done}) + data = db.fetchall_query_and_close( + query, {"testcase_address": testcase_address, + "done": done}) if data: return data else: logging.debug("Could not find any rows in delayedTestData.") - logging.debug("DB Query = " + query % {"testcase_address":testcase_address, "done":done}) + logging.debug("DB Query = " + query % + {"testcase_address": testcase_address, "done": done}) return [] - @classmethod - def insert_delayed_test_data(self, guid_, testcase_address, expected_result, done=0, expires_at=DEFAULT_EXPIRATION): - """ This method inserts rows into the delayedTestData table in the DB based on the - given parameters where inserted_at (Date format) is automatically set in this method. - - :param guid_: The guid that is provided by the test case. (Format: str(uuid.uuid4())) + def insert_delayed_test_data(self, guid_, testcase_address, + expected_result, done=0, + expires_at=DEFAULT_EXPIRATION): + """ This method inserts rows into the delayedTestData table + in the DB based on the given parameters where + inserted_at (Date format) is automatically set in this method. + :param guid_: The guid that is provided by the test case. + (Format: str(uuid.uuid4())) :param testcase_address: The ID (address) of the test case. - :param expected_result: The result string of persistent data that will be stored in the DB. + :param expected_result: The result string of persistent data + that will be stored in the DB. :param done: (0 for test not done or 1 for test done) - :returns: True (when no exceptions or errors occur) """ inserted_at = int(time.time() * 1000) db = DatabaseManager() - query = """INSERT INTO delayedTestData(guid,testcaseAddress,insertedAt,expectedResult,done,expiresAt) - VALUES (%(guid)s,%(testcaseAddress)s,%(inserted_at)s,%(expected_result)s,%(done)s,%(expires_at)s)""" - - db.execute_query_and_close(query, {"guid":guid_, - "testcaseAddress":testcase_address, - "inserted_at":inserted_at, - "expected_result":expected_result, - "done":done, - "expires_at":inserted_at + expires_at}) + query = """INSERT INTO delayedTestData( + guid,testcaseAddress,insertedAt, + expectedResult,done,expiresAt) + VALUES (%(guid)s,%(testcaseAddress)s,%(inserted_at)s, + %(expected_result)s,%(done)s,%(expires_at)s)""" + + db.execute_query_and_close( + query, {"guid": guid_, + "testcaseAddress": testcase_address, + "inserted_at": inserted_at, + "expected_result": expected_result, + "done": done, + "expires_at": inserted_at + expires_at}) return True - @classmethod def set_delayed_test_to_done(self, guid_): """ This method updates the delayedTestData table in the DB to set the test with the selected guid to done. - - :param guid_: The guid that is provided by the test case. (Format: str(uuid.uuid4())) - + :param guid_: The guid that is provided by the test case. + (Format: str(uuid.uuid4())) :returns: True (when no exceptions or errors occur) """ db = DatabaseManager() @@ -76,7 +80,7 @@ def set_delayed_test_to_done(self, guid_): SET done=TRUE WHERE guid=%(guid)s AND done=FALSE""" - db.execute_query_and_close(query, {"guid":guid_}) + db.execute_query_and_close(query, {"guid": guid_}) return True @@ -86,19 +90,19 @@ class DelayedTestAssistant: @classmethod def get_delayed_results(self, test_id, seconds, set_done=True): """ - This method gets the delayed_test_data and sets the applicable rows in the DB to done. + This method gets the delayed_test_data and sets the applicable rows + in the DB to done. The results is a list of dicts where each list item contains item[0] = guid item[1] = testcaseAddress item[2] = seconds from epoch item[3] = expected results dict encoded in json - :param test_id: the self.id() of the test :param seconds: the wait period until the data can be checked - - :returns: the results for the specific test where enough time has passed + :returns: the results for a specific test where enough time has passed """ - delayed_test_data = DelayedTestStorage.get_delayed_test_data(testcase_address=test_id) + delayed_test_data = DelayedTestStorage.get_delayed_test_data( + testcase_address=test_id) now = int(time.time() * 1000) results_to_check = [] if delayed_test_data is None: @@ -110,14 +114,13 @@ def get_delayed_results(self, test_id, seconds, set_done=True): DelayedTestStorage.set_delayed_test_to_done(item[0]) return results_to_check - @classmethod - def store_delayed_data(self, test_id, expected_result_dict, expires_at=DEFAULT_EXPIRATION): + def store_delayed_data(self, test_id, expected_result_dict, + expires_at=DEFAULT_EXPIRATION): """ Loads the dictionary of information into the delayed test database - :param test_id: the self.id() of the test - :param expected_result_dict: a dictionary of what is to be checked later + :param expected_result_dict: a dictionary of what's to be checked later """ expected_result_json = json.JSONEncoder().encode(expected_result_dict) DelayedTestStorage.insert_delayed_test_data(str(uuid.uuid4()), @@ -126,13 +129,12 @@ def store_delayed_data(self, test_id, expected_result_dict, expires_at=DEFAULT_E 0, expires_at) - @classmethod def set_test_done(self, test_guid): - """ This method calls set_delayed_test_to_done to set a row in the db to done. - - :param test_guid: The guid that is provided by the test. (Format: str(uuid.uuid4())) - + """ This method calls set_delayed_test_to_done to set a + row in the db to done. + :param test_guid: The guid that is provided by the test. + (Format: str(uuid.uuid4())) :returns: True (when no exceptions or errors occur) """ DelayedTestStorage.set_delayed_test_to_done(test_guid) diff --git a/seleniumbase/fixtures/email_manager.py b/seleniumbase/fixtures/email_manager.py index 9ff0090df0ad..720766f49d43 100755 --- a/seleniumbase/fixtures/email_manager.py +++ b/seleniumbase/fixtures/email_manager.py @@ -8,6 +8,7 @@ import quopri import re import time +import types from seleniumbase.config import settings @@ -18,30 +19,31 @@ class EmailManager: Example: em = EmailManager() - result = em.check_for_recipient("[GMAIL.USER]+[SOME CODE OR TIMESTAMP KEY]@gmail.com") + result = em.check_for_recipient( + "[GMAIL.USER]+[SOME CODE OR TIMESTAMP KEY]@gmail.com") """ HTML = "text/html" PLAIN = "text/plain" TIMEOUT = 1800 - def __init__(self, uname=settings.EMAIL_USERNAME, pwd=settings.EMAIL_PASSWORD, - imap_string=settings.EMAIL_IMAP_STRING, port=settings.EMAIL_IMAP_PORT): + def __init__(self, uname=settings.EMAIL_USERNAME, + pwd=settings.EMAIL_PASSWORD, + imap_string=settings.EMAIL_IMAP_STRING, + port=settings.EMAIL_IMAP_PORT): self.uname = uname self.pwd = pwd self.imap_string = imap_string self.port = port - def imap_connect(self): """ - Connect to the IMAP mailbox. + Connect to the IMAP mailbox. """ self.mailbox = imaplib.IMAP4_SSL(self.imap_string, self.port) self.mailbox.login(self.uname, self.pwd) self.mailbox.select() - def imap_disconnect(self): """ Disconnect from the IMAP mailbox. @@ -49,7 +51,6 @@ def imap_disconnect(self): self.mailbox.close() self.mailbox.logout() - def __imap_search(self, ** criteria_dict): """ Searches for query in the given IMAP criteria and returns the message numbers that match as a list of strings. @@ -76,16 +77,14 @@ def __imap_search(self, ** criteria_dict): http://tools.ietf.org/html/rfc3501#section-6.4.4 :param criteria_dict: dictionary of search criteria keywords - :raises: EmailException if something in IMAP breaks - :returns: List of message numbers as strings matched by given criteria """ self.imap_connect() criteria = [] for key in criteria_dict: - if criteria_dict[key] == True: + if criteria_dict[key] is True: criteria.append('(%s)' % key) else: criteria.append('(%s "%s")' % (key, criteria_dict[key])) @@ -102,7 +101,6 @@ def __imap_search(self, ** criteria_dict): else: raise EmailException("IMAP status is " + str(status)) - def remove_formatting(self, html): """ Clean out any whitespace @@ -113,34 +111,34 @@ def remove_formatting(self, html): """ return ' '.join(html.split()) - def __parse_imap_search_result(self, result): """ This takes the result of imap_search and returns SANE results @Params result - result from an imap_search call @Returns - List of IMAP search results + List of IMAP search results """ - if type(result) == type([]): + if isinstance(result, types.ListType): + # Above is same as "type(result) == types.ListType" if len(result) == 1: return self.__parse_imap_search_result(result[0]) else: return result - elif type(result) == type(""): + elif isinstance(result, types.StringType): + # Above is same as "type(result) == types.StringType" return result.split() else: # Fail silently assuming tests will fail if emails are not found return [] - def fetch_html(self, msg_nums): - """ - Given a message number that we found with imap_search, + """ + Given a message number that we found with imap_search, get the text/html content. @Params msg_nums - message number to get html message for - @Returns + @Returns HTML content of message matched by message number """ if not msg_nums: @@ -148,14 +146,13 @@ def fetch_html(self, msg_nums): return self.__imap_fetch_content_type(msg_nums, self.HTML) - def fetch_plaintext(self, msg_nums): - """ - Given a message number that we found with imap_search, + """ + Given a message number that we found with imap_search, get the text/plain content. @Params msg_nums - message number to get message for - @Returns + @Returns Plaintext content of message matched by message number """ if not msg_nums: @@ -163,12 +160,11 @@ def fetch_plaintext(self, msg_nums): return self.__imap_fetch_content_type(msg_nums, self.PLAIN) - def __imap_fetch_content_type(self, msg_nums, content_type): - """ - Given a message number that we found with imap_search, fetch the - whole source, dump that into an email object, and pick out the part - that matches the content type specified. Return that, if we got + """ + Given a message number that we found with imap_search, fetch the + whole source, dump that into an email object, and pick out the part + that matches the content type specified. Return that, if we got multiple emails, return dict of all the parts. @Params msg_nums - message number to search for @@ -197,7 +193,6 @@ def __imap_fetch_content_type(self, msg_nums, content_type): self.imap_disconnect() return contents - def fetch_html_by_subject(self, email_name): """ Get the html of an email, searching by subject. @@ -214,7 +209,6 @@ def fetch_html_by_subject(self, email_name): return sources - def fetch_plaintext_by_subject(self, email_name): """ Get the plain text of an email, searching by subject. @@ -231,46 +225,43 @@ def fetch_plaintext_by_subject(self, email_name): return sources - def search_for_recipient(self, email, timeout=None, content_type=None): """ Get content of emails, sent to a specific email address. @Params - email - the recipient email address to search for + email - the recipient email address to search for timeout - seconds to try beore timing out content_type - type of email string to return @Returns Content of the matched email in the given content type """ - return self.search(timeout=timeout, + return self.search(timeout=timeout, content_type=content_type, TO=email) - def search_for_subject(self, subject, timeout=None, content_type=None): """ Get content of emails, sent to a specific email address. @Params - email - the recipient email address to search for + email - the recipient email address to search for timeout - seconds to try beore timing out content_type - type of email string to return @Returns Content of the matched email in the given content type """ - return self.search(timeout=timeout, + return self.search(timeout=timeout, content_type=content_type, SUBJECT=subject) - def search_for_count(self, ** args): - """ + """ A search that keeps searching up until timeout for a specific number of matches to a search. If timeout is not specified we use the default. If count= is not specified we will fail. Return values are the same as search(), except for count=0, - where we will return an empty list. Use this if you need to wait for a + where we will return an empty list. Use this if you need to wait for a number of emails other than 1. @Params - args - dict of arguments to use in search: + args - dict of arguments to use in search: count - number of emails to search for timeout - seconds to try search before timing out @Returns @@ -304,10 +295,9 @@ def search_for_count(self, ** args): time.sleep(15) count += 15 if count >= timer: - raise EmailException("Failed to match criteria %s in %s minutes" % \ + raise EmailException("Failed to match criteria %s in %s minutes" % (args, timeout / 60)) - def __check_msg_for_headers(self, msg, ** email_headers): """ Checks an Email.Message object for the headers in email_headers. @@ -319,7 +309,7 @@ def __check_msg_for_headers(self, msg, ** email_headers): 'Subject', 'MIME-Version', 'Content-Type', 'Date', 'X-Sendgrid-EID', 'Sender']. - @Params + @Params msg - the Email.message object to check email_headers - list of headers to check against @Returns @@ -332,7 +322,6 @@ def __check_msg_for_headers(self, msg, ** email_headers): return all_headers_found - def fetch_message(self, msgnum): """ Given a message number, return the Email.Message object. @@ -349,7 +338,6 @@ def fetch_message(self, msgnum): if isinstance(response_part, tuple): return email.message_from_string(response_part[1]) - def get_content_type(self, msg, content_type="HTML"): """ Given an Email.Message object, gets the content-type payload @@ -370,7 +358,6 @@ def get_content_type(self, msg, content_type="HTML"): if str(part.get_content_type()) == content_type: return str(part.get_payload(decode=True)) - def search(self, ** args): """ Checks email inbox every 15 seconds that match the criteria @@ -420,7 +407,7 @@ def search(self, ** args): timer = timeout count = 0 while count < timer: - results = self.__imap_search( ** args) + results = self.__imap_search(** args) if len(results) > 0: if fetch: msgs = {} @@ -430,16 +417,15 @@ def search(self, ** args): elif not content_type: return results else: - return self.__imap_fetch_content_type(results, + return self.__imap_fetch_content_type(results, content_type) else: time.sleep(15) count += 15 if count >= timer: raise EmailException( - "Failed to find message for criteria %s in %s minutes" % \ - (args, timeout / 60)) - + "Failed to find message for criteria %s in %s minutes" % + (args, timeout / 60)) def remove_whitespace(self, html): """ @@ -455,7 +441,6 @@ def remove_whitespace(self, html): clean_html = clean_html.replace(char, "") return clean_html - def remove_control_chars(self, html): """ Clean control characters from html @@ -466,12 +451,11 @@ def remove_control_chars(self, html): """ return self.remove_whitespace(html) - def replace_entities(self, html): """ Replace htmlentities with unicode characters @Params - html - html source to replace entities in + html - html source to replace entities in @Returns String html with entities replaced """ @@ -493,10 +477,9 @@ def fixup(text): text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) except KeyError: pass - return text # leave as is + return text # leave as is return re.sub("&#?\w+;", fixup, html) - def decode_quoted_printable(self, html): """ Decoding from Quoted-printable, or QP encoding, that uses ASCII 7bit @@ -510,14 +493,13 @@ def decode_quoted_printable(self, html): """ return self.replace_entities(quopri.decodestring(html)) - def html_bleach(self, html): - """ + """ Cleanup and get rid of all extraneous stuff for better comparison later. Turns formatted into into a single line string. @Params html - HTML source to clean up - @Returns + @Returns String cleaned up HTML source """ return self.decode_quoted_printable(html) diff --git a/seleniumbase/fixtures/errors.py b/seleniumbase/fixtures/errors.py index 4b3eb807f43a..d3ff3c2bc786 100755 --- a/seleniumbase/fixtures/errors.py +++ b/seleniumbase/fixtures/errors.py @@ -1,5 +1,5 @@ """ -This module contains test-state related exceptions. +This module contains test-state related exceptions. Raising one of these in a test will cause the test-state to be logged appropriately. """ diff --git a/seleniumbase/fixtures/page_interactions.py b/seleniumbase/fixtures/page_interactions.py index c057d82f1535..682699af6f69 100755 --- a/seleniumbase/fixtures/page_interactions.py +++ b/seleniumbase/fixtures/page_interactions.py @@ -1,6 +1,6 @@ """ This module contains a set of methods that can be used for loading pages and -waiting for elements to come in. +waiting for elements to appear on the page. The default option we use to search for elements is CSS Selector. This can be changed by setting the by paramter. The enum class for options is: @@ -20,21 +20,19 @@ import time from seleniumbase.config import settings from selenium.webdriver.common.by import By -from selenium.webdriver.remote.errorhandler import ElementNotVisibleException from selenium.webdriver.remote.errorhandler import NoSuchElementException def is_element_present(driver, selector, by=By.CSS_SELECTOR): """ - Searches for the specified element by the given selector. Returns whether + Searches for the specified element by the given selector. Returns whether the element object if the element is present on the page. @Params driver - the webdriver object (required) selector - the locator that is used (required) - by - the method to search for hte locator (Default- By.CSS_SELECTOR) - - @returns - Boolean Whether the element is present + by - the method to search for the locator (Default: By.CSS_SELECTOR) + @Returns + Boolean (is element present) """ try: driver.find_element(by=by, value=selector) @@ -45,15 +43,14 @@ def is_element_present(driver, selector, by=By.CSS_SELECTOR): def is_element_visible(driver, selector, by=By.CSS_SELECTOR): """ - Searches for the specified element by the given selector. Returns whether + Searches for the specified element by the given selector. Returns whether the element object if the element is present and visible on the page. @Params driver - the webdriver object (required) selector - the locator that is used (required) - by - the method to search for hte locator (Default- By.CSS_SELECTOR) - - @returns - Boolean Whether the element is present and visible + by - the method to search for the locator (Default: By.CSS_SELECTOR) + @Returns + Boolean (is element visible) """ try: element = driver.find_element(by=by, value=selector) @@ -64,17 +61,16 @@ def is_element_visible(driver, selector, by=By.CSS_SELECTOR): def is_text_visible(driver, text, selector, by=By.CSS_SELECTOR): """ - Searches for the specified element by the given selector. Returns whether - the element object if the element is present and visible on the page and + Searches for the specified element by the given selector. Returns whether + the element object if the element is present and visible on the page and contains the given text. @Params driver - the webdriver object (required) text - the text string to search for selector - the locator that is used (required) - by - the method to search for hte locator (Default- By.CSS_SELECTOR) - - @returns - Boolean Whether the element is present and visible + by - the method to search for the locator (Default: By.CSS_SELECTOR) + @Returns + Boolean (is text visible) """ try: element = driver.find_element(by=by, value=selector) @@ -83,39 +79,22 @@ def is_text_visible(driver, text, selector, by=By.CSS_SELECTOR): return False -def find_visible_element(driver, selector, by=By.CSS_SELECTOR): - """ - Finds a WebElement that matches a selector and is visible. - Similar to webdriver.find_element. - @Params - driver - the webdriver object (required) - selector - the locator that is used to search the DOM (required) - by - the method to search for hte locator (Default- By.CSS_SELECTOR) - """ - element = driver.find_element(by=by, value=selector) - if element.is_displayed(): - return element - else: - return None - - def find_visible_elements(driver, selector, by=By.CSS_SELECTOR): """ - Finds all WebElements that matche a selector and are visible. + Finds all WebElements that match a selector and are visible. Similar to webdriver.find_elements. @Params driver - the webdriver object (required) selector - the locator that is used to search the DOM (required) - by - the method to search for hte locator (Default- By.CSS_SELECTOR) + by - the method to search for the locator (Default: By.CSS_SELECTOR) """ elements = driver.find_elements(by=by, value=selector) - return [element for element in elements if element.is_displayed()] def hover_on_element(driver, selector): """ - Fires the hover event for the specified element by the given selector. + Fires the hover event for the specified element by the given selector. @Params driver - the webdriver object (required) selector - the locator (css selector) that is used (required) @@ -123,25 +102,26 @@ def hover_on_element(driver, selector): driver.execute_script("jQuery('%s').mouseover()" % selector) -def hover_and_click(driver, hover_selector, click_selector, +def hover_and_click(driver, hover_selector, click_selector, click_by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): """ - Fires the hover event for a specified element by a given selector, then clicks on - another element specified. Useful for dropdown hover based menus. + Fires the hover event for a specified element by a given selector, then + clicks on another element specified. Useful for dropdown hover based menus. @Params driver - the webdriver object (required) - hover_selector - the locator (css selector) that is used to hover (required) - click_selector - the locator that is used to click (required) - click_by - the method to search for hte locator (Default- By.CSS_SELECTOR) - timeout - number of seconds to wait between hover and click (Default- 5 seconds) + hover_selector - the css selector to hover over (required) + click_selector - the css selector to click on (required) + click_by - the method to search by (Default: By.CSS_SELECTOR) + timeout - number of seconds to wait for click element to appear after hover """ driver.execute_script("jQuery('%s').mouseover()" % (hover_selector)) - for x in range(timeout * 10): + for x in range(int(timeout * 10)): try: - element = driver.find_element(by=click_by, value="%s" % - click_selector).click() + element = driver.find_element(by=click_by, + value="%s" % click_selector).click() return element except Exception: time.sleep(0.1) - raise NoSuchElementException("Element %s was not present in %s" % - (click_selector, timeout)) + raise NoSuchElementException( + "Element %s was not present after %s seconds!" % + (click_selector, timeout)) diff --git a/seleniumbase/fixtures/page_loads.py b/seleniumbase/fixtures/page_loads.py index 56233b07ae7f..e94d4ffb54b6 100755 --- a/seleniumbase/fixtures/page_loads.py +++ b/seleniumbase/fixtures/page_loads.py @@ -1,6 +1,6 @@ """ This module contains a set of methods that can be used for loading pages and -waiting for elements to come in. +waiting for elements to appear on the page. The default option we use to search for elements is CSS Selector. This can be changed by setting the by paramter. The enum class for options is: @@ -20,24 +20,24 @@ import time from seleniumbase.config import settings from selenium.webdriver.common.by import By -from selenium.webdriver.remote.errorhandler import ElementNotVisibleException, \ - NoSuchElementException, \ - NoAlertPresentException +from selenium.webdriver.remote.errorhandler import ElementNotVisibleException +from selenium.webdriver.remote.errorhandler import NoSuchElementException +from selenium.webdriver.remote.errorhandler import NoAlertPresentException -def wait_for_element_present(driver, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): +def wait_for_element_present(driver, selector, by=By.CSS_SELECTOR, + timeout=settings.LARGE_TIMEOUT): """ - Searches for the specified element by the given selector. Returns the - element object if the element is present on the page. The element can be - invisible. Raises an exception if the element does not appear in the + Searches for the specified element by the given selector. Returns the + element object if the element is present on the page. The element can be + invisible. Raises an exception if the element does not appear in the specified timeout. @Params driver - the webdriver object selector - the locator that is used (required) - by - the method to search for hte locator (Default- By.CSS_SELECTOR) + by - the method to search for the locator (Default: By.CSS_SELECTOR) timeout - the time to wait for elements in seconds - - @returns + @Returns A web element object """ @@ -49,23 +49,24 @@ def wait_for_element_present(driver, selector, by=By.CSS_SELECTOR, timeout=setti except Exception: time.sleep(0.1) if not element: - raise NoSuchElementException("Element %s was not present in %s seconds!" % - (selector, timeout)) + raise NoSuchElementException( + "Element %s was not present in %s seconds!" % (selector, timeout)) -def wait_for_element_visible(driver, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): +def wait_for_element_visible(driver, selector, by=By.CSS_SELECTOR, + timeout=settings.LARGE_TIMEOUT): """ - Searches for the specified element by the given selector. Returns the + Searches for the specified element by the given selector. Returns the element object if the element is present and visible on the page. Raises an exception if the element does not appear in the specified timeout. @Params driver - the webdriver object (required) selector - the locator that is used (required) - by - the method to search for hte locator (Default- By.CSS_SELECTOR) + by - the method to search for the locator (Default: By.CSS_SELECTOR) timeout - the time to wait for elements in seconds - @returns + @Returns A web element object """ @@ -81,11 +82,12 @@ def wait_for_element_visible(driver, selector, by=By.CSS_SELECTOR, timeout=setti except Exception: time.sleep(0.1) if not element: - raise ElementNotVisibleException("Element %s was not visible in %s seconds!"\ - % (selector, timeout)) + raise ElementNotVisibleException( + "Element %s was not visible in %s seconds!" % (selector, timeout)) -def wait_for_text_visible(driver, text, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): +def wait_for_text_visible(driver, text, selector, by=By.CSS_SELECTOR, + timeout=settings.LARGE_TIMEOUT): """ Searches for the specified element by the given selector. Returns the element object if the text is present in the element and visible @@ -95,10 +97,9 @@ def wait_for_text_visible(driver, text, selector, by=By.CSS_SELECTOR, timeout=se driver - the webdriver object (required) text - the text that is being searched for in the element (required) selector - the locator that is used (required) - by - the method to search for hte locator (Default- By.CSS_SELECTOR) + by - the method to search for the locator (Default: By.CSS_SELECTOR) timeout - the time to wait for elements in seconds - - @returns + @Returns A web element object that contains the text searched for """ @@ -115,19 +116,21 @@ def wait_for_text_visible(driver, text, selector, by=By.CSS_SELECTOR, timeout=se except Exception: time.sleep(0.1) if not element: - raise ElementNotVisibleException("Expected text [%s] for element [%s] was not visible in %s seconds!"\ - % (text, selector, timeout)) + raise ElementNotVisibleException( + "Expected text [%s] for [%s] was not visible after %s seconds!" % + (text, selector, timeout)) -def wait_for_element_absent(driver, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): +def wait_for_element_absent(driver, selector, by=By.CSS_SELECTOR, + timeout=settings.LARGE_TIMEOUT): """ - Searches for the specified element by the given selector. Returns void when - element is no longer present on the page. Raises an exception if the - element does still exist after the specified timeout. + Searches for the specified element by the given selector. + Raises an exception if the element is still present after the + specified timeout. @Params driver - the webdriver object selector - the locator that is used (required) - by - the method to search for hte locator (Default- By.CSS_SELECTOR) + by - the method to search for the locator (Default: By.CSS_SELECTOR) timeout - the time to wait for elements in seconds """ @@ -141,17 +144,17 @@ def wait_for_element_absent(driver, selector, by=By.CSS_SELECTOR, timeout=settin (selector, timeout)) -def wait_for_element_not_visible(driver, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): +def wait_for_element_not_visible(driver, selector, by=By.CSS_SELECTOR, + timeout=settings.LARGE_TIMEOUT): """ - Searches for the specified element by the given selector. Returns void when - element is no longer visible on the page (or if the element is not - present). Raises an exception if the element is still visible after the + Searches for the specified element by the given selector. + Raises an exception if the element is still visible after the specified timeout. @Params driver - the webdriver object (required) selector - the locator that is used (required) - by - the method to search for hte locator (Default- By.CSS_SELECTOR) - timeout - the time to wait for elements in seconds + by - the method to search for the locator (Default: By.CSS_SELECTOR) + timeout - the time to wait for the element in seconds """ for x in range(int(timeout * 10)): @@ -163,8 +166,8 @@ def wait_for_element_not_visible(driver, selector, by=By.CSS_SELECTOR, timeout=s return except Exception: return - raise Exception("Element %s was still visible after %s seconds!"\ - % (selector, timeout)) + raise Exception( + "Element %s was still visible after %s seconds!" % (selector, timeout)) def wait_for_ready_state_complete(driver, timeout=settings.EXTREME_TIMEOUT): @@ -181,14 +184,14 @@ def wait_for_ready_state_complete(driver, timeout=settings.EXTREME_TIMEOUT): return True else: time.sleep(0.1) - raise Exception("Page elements never fully loaded after %s seconds!"\ - % timeout) + raise Exception( + "Page elements never fully loaded after %s seconds!" % timeout) def wait_for_and_accept_alert(driver, timeout=settings.LARGE_TIMEOUT): """ Wait for and accept an alert. Returns the text from the alert. - @params + @Params driver - the webdriver object (required) timeout - the time to wait for the alert in seconds """ @@ -201,7 +204,7 @@ def wait_for_and_accept_alert(driver, timeout=settings.LARGE_TIMEOUT): def wait_for_and_dismiss_alert(driver, timeout=settings.LARGE_TIMEOUT): """ Wait for and dismiss an alert. Returns the text from the alert. - @params + @Params driver - the webdriver object (required) timeout - the time to wait for the alert in seconds """ @@ -214,9 +217,9 @@ def wait_for_and_dismiss_alert(driver, timeout=settings.LARGE_TIMEOUT): def wait_for_and_switch_to_alert(driver, timeout=settings.LARGE_TIMEOUT): """ Wait for a browser alert to appear, and switch to it. This should be usable - as a drop-in replacement for driver.switch_to_alert() when the alert box may - not exist yet. - @params + as a drop-in replacement for driver.switch_to_alert() when the alert box + may not exist yet. + @Params driver - the webdriver object (required) timeout - the time to wait for the alert in seconds """ @@ -224,7 +227,8 @@ def wait_for_and_switch_to_alert(driver, timeout=settings.LARGE_TIMEOUT): for x in range(int(timeout * 10)): try: alert = driver.switch_to_alert() - dummy_variable = alert.text # Raises exception if no alert present + # Raises exception if no alert present + dummy_variable = alert.text # noqa return alert except NoAlertPresentException: time.sleep(0.1) diff --git a/seleniumbase/fixtures/page_utils.py b/seleniumbase/fixtures/page_utils.py index cbafbb4c6a41..ec9ec870e6ca 100755 --- a/seleniumbase/fixtures/page_utils.py +++ b/seleniumbase/fixtures/page_utils.py @@ -2,11 +2,15 @@ This module contains useful utility methods. """ + def jq_format(code): """ Use before throwing raw code such as 'div[tab="advanced"]' into jQuery. - Selectors with quotes inside of quotes would otherwise break jQuery. (One example) - This is similar to "json.dumps(value)". + Selectors with quotes inside of quotes would otherwise break jQuery. + This is similar to "json.dumps(value)", but with one less layer of quotes. """ - code = code.replace('\\','\\\\').replace('\t','\\t').replace('\n', '\\n').replace('\"','\\\"').replace('\'','\\\'').replace('\r', '\\r').replace('\v', '\\v').replace('\a', '\\a').replace('\f', '\\f').replace('\b', '\\b').replace('\u', '\\u') + code = code.replace('\\', '\\\\').replace('\t', '\\t').replace('\n', '\\n') + code = code.replace('\"', '\\\"').replace('\'', '\\\'') + code = code.replace('\v', '\\v').replace('\a', '\\a').replace('\f', '\\f') + code = code.replace('\b', '\\b').replace('\u', '\\u').replace('\r', '\\r') return code diff --git a/seleniumbase/fixtures/tools.py b/seleniumbase/fixtures/tools.py index 086023dbfd17..4452804f5a3a 100755 --- a/seleniumbase/fixtures/tools.py +++ b/seleniumbase/fixtures/tools.py @@ -1,9 +1,9 @@ """ -This module imports all the commonly used fixtures in one place so that +This module imports all the commonly used fixtures in one place so that every test doesn't need to import a number of different fixtures. """ -from seleniumbase.fixtures.page_loads import * -from seleniumbase.fixtures.page_interactions import * -from seleniumbase.fixtures.page_utils import * -from seleniumbase.fixtures.errors import * +from seleniumbase.fixtures.page_loads import * # noqa +from seleniumbase.fixtures.page_interactions import * # noqa +from seleniumbase.fixtures.page_utils import * # noqa +from seleniumbase.fixtures.errors import * # noqa From 11c147901b90b787ae5663c7143835fe4d9f9dde Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Dec 2015 19:24:11 -0500 Subject: [PATCH 191/219] pytest comments for setUp() and tearDown() methods of subclasses --- seleniumbase/fixtures/base_case.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index e6ad7a9fa229..97a7f5e90deb 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -26,7 +26,9 @@ def __init__(self, *args, **kwargs): # pytest-specific config - # [Be careful if a subclass of BaseCase overrides setUp()] + # Be careful if a subclass of BaseCase overrides setUp() + # You'll need to add the following line to the subclass setUp() method: + # super(SubClassOfBaseCase, self).setUp() def setUp(self): self.is_pytest = None try: @@ -44,7 +46,9 @@ def setUp(self): # pytest-specific config - # [Be careful if a subclass of BaseCase overrides tearDown()] + # Be careful if a subclass of BaseCase overrides setUp() + # You'll need to add the following line to the subclass tearDown() method: + # super(SubClassOfBaseCase, self).tearDown() def tearDown(self): if self.is_pytest: if self.with_selenium: From 30fb26cc1299e77509af53291483f472e59a1517 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Dec 2015 19:29:10 -0500 Subject: [PATCH 192/219] Combining page_loads.py and page_interactions.py into page_actions.py --- seleniumbase/fixtures/base_case.py | 37 ++--- .../{page_loads.py => page_actions.py} | 104 ++++++++++++++ seleniumbase/fixtures/page_interactions.py | 127 ------------------ seleniumbase/fixtures/tools.py | 3 +- 4 files changed, 124 insertions(+), 147 deletions(-) rename seleniumbase/fixtures/{page_loads.py => page_actions.py} (68%) delete mode 100755 seleniumbase/fixtures/page_interactions.py diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 97a7f5e90deb..337a8169725c 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -7,7 +7,8 @@ from seleniumbase.core import browser_launcher from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.common.by import By -import page_loads, page_interactions, page_utils +import page_actions +import page_utils class BaseCase(unittest.TestCase): @@ -56,31 +57,31 @@ def tearDown(self): def find_visible_elements(self, selector, by=By.CSS_SELECTOR): - return page_interactions.find_visible_elements(self.driver, selector, by) + return page_actions.find_visible_elements(self.driver, selector, by) def hover_on_element(self, selector): - return page_interactions.hover_on_element(self.driver, selector) + return page_actions.hover_on_element(self.driver, selector) def hover_and_click(self, hover_selector, click_selector, click_by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): - return page_interactions.hover_and_click(self.driver, hover_selector, click_selector, click_by, timeout) + return page_actions.hover_and_click(self.driver, hover_selector, click_selector, click_by, timeout) def is_element_present(self, selector, by=By.CSS_SELECTOR): - return page_interactions.is_element_present(self.driver, selector, by) + return page_actions.is_element_present(self.driver, selector, by) def is_element_visible(self, selector, by=By.CSS_SELECTOR): - return page_interactions.is_element_visible(self.driver, selector, by) + return page_actions.is_element_visible(self.driver, selector, by) def is_link_text_visible(self, link_text): - return page_interactions.is_element_visible(self.driver, link_text, by=By.LINK_TEXT) + return page_actions.is_element_visible(self.driver, link_text, by=By.LINK_TEXT) def is_text_visible(self, text, selector, by=By.CSS_SELECTOR): - return page_interactions.is_text_visible(self.driver, text, selector, by) + return page_actions.is_text_visible(self.driver, text, selector, by) def jquery_click(self, selector): @@ -88,7 +89,7 @@ def jquery_click(self, selector): def click(self, selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): - element = page_loads.wait_for_element_visible(self.driver, selector, by, timeout=timeout) + element = page_actions.wait_for_element_visible(self.driver, selector, by, timeout=timeout) element.click() if settings.WAIT_FOR_RSC_ON_CLICKS: self.wait_for_ready_state_complete() @@ -178,36 +179,36 @@ def jquery_update_text_value(self, selector, new_value, timeout=settings.SMALL_T def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): - return page_loads.wait_for_element_present(self.driver, selector, by, timeout) + return page_actions.wait_for_element_present(self.driver, selector, by, timeout) def wait_for_element_visible(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): - return page_loads.wait_for_element_visible(self.driver, selector, by, timeout) + return page_actions.wait_for_element_visible(self.driver, selector, by, timeout) def wait_for_text_visible(self, text, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): - return page_loads.wait_for_text_visible(self.driver, text, selector, by, timeout) + return page_actions.wait_for_text_visible(self.driver, text, selector, by, timeout) def wait_for_element_absent(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): - return page_loads.wait_for_element_absent(self.driver, selector, by, timeout) + return page_actions.wait_for_element_absent(self.driver, selector, by, timeout) def wait_for_element_not_visible(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): - return page_loads.wait_for_element_not_visible(self.driver, selector, by, timeout) + return page_actions.wait_for_element_not_visible(self.driver, selector, by, timeout) def wait_for_ready_state_complete(self, timeout=settings.EXTREME_TIMEOUT): - return page_loads.wait_for_ready_state_complete(self.driver, timeout) + return page_actions.wait_for_ready_state_complete(self.driver, timeout) def wait_for_and_accept_alert(self, timeout=settings.LARGE_TIMEOUT): - return page_loads.wait_for_and_accept_alert(self.driver, timeout) + return page_actions.wait_for_and_accept_alert(self.driver, timeout) def wait_for_and_dismiss_alert(self, timeout=settings.LARGE_TIMEOUT): - return page_loads.wait_for_and_dismiss_alert(self.driver, timeout) + return page_actions.wait_for_and_dismiss_alert(self.driver, timeout) def wait_for_and_switch_to_alert(self, timeout=settings.LARGE_TIMEOUT): - return page_loads.wait_for_and_switch_to_alert(self.driver, timeout) + return page_actions.wait_for_and_switch_to_alert(self.driver, timeout) diff --git a/seleniumbase/fixtures/page_loads.py b/seleniumbase/fixtures/page_actions.py similarity index 68% rename from seleniumbase/fixtures/page_loads.py rename to seleniumbase/fixtures/page_actions.py index e94d4ffb54b6..536ec75e7d14 100755 --- a/seleniumbase/fixtures/page_loads.py +++ b/seleniumbase/fixtures/page_actions.py @@ -25,6 +25,110 @@ from selenium.webdriver.remote.errorhandler import NoAlertPresentException +def is_element_present(driver, selector, by=By.CSS_SELECTOR): + """ + Searches for the specified element by the given selector. Returns whether + the element object if the element is present on the page. + @Params + driver - the webdriver object (required) + selector - the locator that is used (required) + by - the method to search for the locator (Default: By.CSS_SELECTOR) + @Returns + Boolean (is element present) + """ + try: + driver.find_element(by=by, value=selector) + return True + except Exception: + return False + + +def is_element_visible(driver, selector, by=By.CSS_SELECTOR): + """ + Searches for the specified element by the given selector. Returns whether + the element object if the element is present and visible on the page. + @Params + driver - the webdriver object (required) + selector - the locator that is used (required) + by - the method to search for the locator (Default: By.CSS_SELECTOR) + @Returns + Boolean (is element visible) + """ + try: + element = driver.find_element(by=by, value=selector) + return element.is_displayed() + except Exception: + return False + + +def is_text_visible(driver, text, selector, by=By.CSS_SELECTOR): + """ + Searches for the specified element by the given selector. Returns whether + the element object if the element is present and visible on the page and + contains the given text. + @Params + driver - the webdriver object (required) + text - the text string to search for + selector - the locator that is used (required) + by - the method to search for the locator (Default: By.CSS_SELECTOR) + @Returns + Boolean (is text visible) + """ + try: + element = driver.find_element(by=by, value=selector) + return element.is_displayed() and text in element.text + except Exception: + return False + + +def find_visible_elements(driver, selector, by=By.CSS_SELECTOR): + """ + Finds all WebElements that match a selector and are visible. + Similar to webdriver.find_elements. + @Params + driver - the webdriver object (required) + selector - the locator that is used to search the DOM (required) + by - the method to search for the locator (Default: By.CSS_SELECTOR) + """ + elements = driver.find_elements(by=by, value=selector) + return [element for element in elements if element.is_displayed()] + + +def hover_on_element(driver, selector): + """ + Fires the hover event for the specified element by the given selector. + @Params + driver - the webdriver object (required) + selector - the locator (css selector) that is used (required) + """ + driver.execute_script("jQuery('%s').mouseover()" % selector) + + +def hover_and_click(driver, hover_selector, click_selector, + click_by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): + """ + Fires the hover event for a specified element by a given selector, then + clicks on another element specified. Useful for dropdown hover based menus. + @Params + driver - the webdriver object (required) + hover_selector - the css selector to hover over (required) + click_selector - the css selector to click on (required) + click_by - the method to search by (Default: By.CSS_SELECTOR) + timeout - number of seconds to wait for click element to appear after hover + """ + driver.execute_script("jQuery('%s').mouseover()" % (hover_selector)) + for x in range(int(timeout * 10)): + try: + element = driver.find_element(by=click_by, + value="%s" % click_selector).click() + return element + except Exception: + 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_interactions.py b/seleniumbase/fixtures/page_interactions.py deleted file mode 100755 index 682699af6f69..000000000000 --- a/seleniumbase/fixtures/page_interactions.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -This module contains a set of methods that can be used for loading pages and -waiting for elements to appear on the page. - -The default option we use to search for elements is CSS Selector. -This can be changed by setting the by paramter. The enum class for options is: -from selenium.webdriver.common.by import By - -Options are -By.CSS_SELECTOR -By.CLASS_NAME -By.ID -By.NAME -By.LINK_TEXT -By.XPATH -By.TAG_NAME -By.PARTIAL_LINK_TEXT -""" - -import time -from seleniumbase.config import settings -from selenium.webdriver.common.by import By -from selenium.webdriver.remote.errorhandler import NoSuchElementException - - -def is_element_present(driver, selector, by=By.CSS_SELECTOR): - """ - Searches for the specified element by the given selector. Returns whether - the element object if the element is present on the page. - @Params - driver - the webdriver object (required) - selector - the locator that is used (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) - @Returns - Boolean (is element present) - """ - try: - driver.find_element(by=by, value=selector) - return True - except Exception: - return False - - -def is_element_visible(driver, selector, by=By.CSS_SELECTOR): - """ - Searches for the specified element by the given selector. Returns whether - the element object if the element is present and visible on the page. - @Params - driver - the webdriver object (required) - selector - the locator that is used (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) - @Returns - Boolean (is element visible) - """ - try: - element = driver.find_element(by=by, value=selector) - return element.is_displayed() - except Exception: - return False - - -def is_text_visible(driver, text, selector, by=By.CSS_SELECTOR): - """ - Searches for the specified element by the given selector. Returns whether - the element object if the element is present and visible on the page and - contains the given text. - @Params - driver - the webdriver object (required) - text - the text string to search for - selector - the locator that is used (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) - @Returns - Boolean (is text visible) - """ - try: - element = driver.find_element(by=by, value=selector) - return element.is_displayed() and text in element.text - except Exception: - return False - - -def find_visible_elements(driver, selector, by=By.CSS_SELECTOR): - """ - Finds all WebElements that match a selector and are visible. - Similar to webdriver.find_elements. - @Params - driver - the webdriver object (required) - selector - the locator that is used to search the DOM (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) - """ - elements = driver.find_elements(by=by, value=selector) - return [element for element in elements if element.is_displayed()] - - -def hover_on_element(driver, selector): - """ - Fires the hover event for the specified element by the given selector. - @Params - driver - the webdriver object (required) - selector - the locator (css selector) that is used (required) - """ - driver.execute_script("jQuery('%s').mouseover()" % selector) - - -def hover_and_click(driver, hover_selector, click_selector, - click_by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): - """ - Fires the hover event for a specified element by a given selector, then - clicks on another element specified. Useful for dropdown hover based menus. - @Params - driver - the webdriver object (required) - hover_selector - the css selector to hover over (required) - click_selector - the css selector to click on (required) - click_by - the method to search by (Default: By.CSS_SELECTOR) - timeout - number of seconds to wait for click element to appear after hover - """ - driver.execute_script("jQuery('%s').mouseover()" % (hover_selector)) - for x in range(int(timeout * 10)): - try: - element = driver.find_element(by=click_by, - value="%s" % click_selector).click() - return element - except Exception: - time.sleep(0.1) - raise NoSuchElementException( - "Element %s was not present after %s seconds!" % - (click_selector, timeout)) diff --git a/seleniumbase/fixtures/tools.py b/seleniumbase/fixtures/tools.py index 4452804f5a3a..53d8d4034a2c 100755 --- a/seleniumbase/fixtures/tools.py +++ b/seleniumbase/fixtures/tools.py @@ -3,7 +3,6 @@ every test doesn't need to import a number of different fixtures. """ -from seleniumbase.fixtures.page_loads import * # noqa -from seleniumbase.fixtures.page_interactions import * # noqa +from seleniumbase.fixtures.page_actions import * # noqa from seleniumbase.fixtures.page_utils import * # noqa from seleniumbase.fixtures.errors import * # noqa From 49bdf135c747031732fc12a90ed513335844ec18 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Dec 2015 20:12:49 -0500 Subject: [PATCH 193/219] Comments and sorting methods by significance --- seleniumbase/fixtures/base_case.py | 111 ++++++++++++++------------ seleniumbase/fixtures/page_actions.py | 39 ++++----- 2 files changed, 79 insertions(+), 71 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 337a8169725c..65593f84bf17 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1,3 +1,8 @@ +""" +These methods improve on and expand existing WebDriver commands. +Improvements include making WebDriver commands more robust and reliable. +""" + import json import time import pytest @@ -56,16 +61,48 @@ def tearDown(self): self.driver.quit() - def find_visible_elements(self, selector, by=By.CSS_SELECTOR): - return page_actions.find_visible_elements(self.driver, selector, by) + def open(self, url): + self.driver.get(url) + if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: + self.wait_for_ready_state_complete() - def hover_on_element(self, selector): - return page_actions.hover_on_element(self.driver, selector) + def open_url(self, url): + """ In case people are mixing up self.open() with open(), use this alternative. """ + self.open(url) - def hover_and_click(self, hover_selector, click_selector, click_by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): - return page_actions.hover_and_click(self.driver, hover_selector, click_selector, click_by, timeout) + def click(self, selector, by=By.CSS_SELECTOR, + timeout=settings.SMALL_TIMEOUT): + element = page_actions.wait_for_element_visible( + self.driver, selector, by, timeout=timeout) + element.click() + if settings.WAIT_FOR_RSC_ON_CLICKS: + self.wait_for_ready_state_complete() + + + def click_link_text(self, link_text, timeout=settings.SMALL_TIMEOUT): + element = self.wait_for_link_text_visible(link_text, timeout=timeout) + element.click() + if settings.WAIT_FOR_RSC_ON_CLICKS: + self.wait_for_ready_state_complete() + + + def update_text_value(self, selector, new_value, timeout=settings.SMALL_TIMEOUT, retry=False): + """ This method updates a selector's text value with a new value + @Params + selector - the selector with the value to update + new_value - the new value for setting the text field + timeout - how long to wait for the selector to be visible + retry - if True, use jquery if the selenium text update fails + """ + element = self.wait_for_element_visible(selector, timeout=timeout) + element.clear() + element.send_keys(new_value) + if retry and element.get_attribute('value') != new_value and not new_value.endswith('\n'): + logging.debug('update_text_value is falling back to jQuery!') + selector = self.jq_format(selector) + self.set_value(selector, new_value) def is_element_present(self, selector, by=By.CSS_SELECTOR): @@ -84,26 +121,12 @@ def is_text_visible(self, text, selector, by=By.CSS_SELECTOR): return page_actions.is_text_visible(self.driver, text, selector, by) - def jquery_click(self, selector): - self.driver.execute_script("jQuery('%s').click()" % selector) - - - def click(self, selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): - element = page_actions.wait_for_element_visible(self.driver, selector, by, timeout=timeout) - element.click() - if settings.WAIT_FOR_RSC_ON_CLICKS: - self.wait_for_ready_state_complete() - - - def open(self, url): - self.driver.get(url) - if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: - self.wait_for_ready_state_complete() + def find_visible_elements(self, selector, by=By.CSS_SELECTOR): + return page_actions.find_visible_elements(self.driver, selector, by) - def open_url(self, url): - """ In case people are mixing up self.open() with open(), use this alternative. """ - self.open(url) + def jquery_click(self, selector): + self.driver.execute_script("jQuery('%s').click()" % selector) def execute_script(self, script): @@ -118,17 +141,6 @@ def maximize_window(self): return self.driver.maximize_window() - def wait_for_link_text_visible(self, link_text, timeout=settings.LARGE_TIMEOUT): - return self.wait_for_element_visible(link_text, by=By.LINK_TEXT, timeout=timeout) - - - def click_link_text(self, link_text, timeout=settings.SMALL_TIMEOUT): - element = self.wait_for_link_text_visible(link_text, timeout=timeout) - element.click() - if settings.WAIT_FOR_RSC_ON_CLICKS: - self.wait_for_ready_state_complete() - - def activate_jquery(self): """ (It's not on by default on all website pages.) """ self.driver.execute_script('var script = document.createElement("script"); script.src = "https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"; document.getElementsByTagName("head")[0].appendChild(script);') @@ -144,6 +156,14 @@ def scroll_click(self, selector): self.click(selector) + def hover_on_element(self, selector): + return page_actions.hover_on_element(self.driver, selector) + + + def hover_and_click(self, hover_selector, click_selector, click_by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): + return page_actions.hover_and_click(self.driver, hover_selector, click_selector, click_by, timeout) + + def jq_format(self, code): return page_utils.jq_format(code) @@ -153,23 +173,6 @@ def set_value(self, selector, value): self.driver.execute_script("jQuery('%s').val(%s)" % (selector, val)) - def update_text_value(self, selector, new_value, timeout=settings.SMALL_TIMEOUT, retry=False): - """ This method updates a selector's text value with a new value - @Params - selector - the selector with the value to update - new_value - the new value for setting the text field - timeout - how long to wait for the selector to be visible - retry - if True, use jquery if the selenium text update fails - """ - element = self.wait_for_element_visible(selector, timeout=timeout) - element.clear() - element.send_keys(new_value) - if retry and element.get_attribute('value') != new_value and not new_value.endswith('\n'): - logging.debug('update_text_value is falling back to jQuery!') - selector = self.jq_format(selector) - self.set_value(selector, new_value) - - def jquery_update_text_value(self, selector, new_value, timeout=settings.SMALL_TIMEOUT): element = self.wait_for_element_visible(selector, timeout=timeout) self.driver.execute_script("""jQuery('%s').val('%s')""" @@ -190,6 +193,10 @@ def wait_for_text_visible(self, text, selector, by=By.CSS_SELECTOR, timeout=sett return page_actions.wait_for_text_visible(self.driver, text, selector, by, timeout) + def wait_for_link_text_visible(self, link_text, timeout=settings.LARGE_TIMEOUT): + return self.wait_for_element_visible(link_text, by=By.LINK_TEXT, timeout=timeout) + + def wait_for_element_absent(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): return page_actions.wait_for_element_absent(self.driver, selector, by, timeout) diff --git a/seleniumbase/fixtures/page_actions.py b/seleniumbase/fixtures/page_actions.py index 536ec75e7d14..2e37f19d4774 100755 --- a/seleniumbase/fixtures/page_actions.py +++ b/seleniumbase/fixtures/page_actions.py @@ -1,12 +1,13 @@ """ -This module contains a set of methods that can be used for loading pages and -waiting for elements to appear on the page. +This module contains a set of methods that can be used for page loads and +for waiting for elements to appear on a page. -The default option we use to search for elements is CSS Selector. -This can be changed by setting the by paramter. The enum class for options is: -from selenium.webdriver.common.by import By +These methods improve on and expand existing WebDriver commands. +Improvements include making WebDriver commands more robust and reliable. -Options are +The default option for searching for elements is by CSS Selector. +This can be changed by overriding the "By" parameter. +Options are: By.CSS_SELECTOR By.CLASS_NAME By.ID @@ -81,19 +82,6 @@ def is_text_visible(driver, text, selector, by=By.CSS_SELECTOR): return False -def find_visible_elements(driver, selector, by=By.CSS_SELECTOR): - """ - Finds all WebElements that match a selector and are visible. - Similar to webdriver.find_elements. - @Params - driver - the webdriver object (required) - selector - the locator that is used to search the DOM (required) - by - the method to search for the locator (Default: By.CSS_SELECTOR) - """ - elements = driver.find_elements(by=by, value=selector) - return [element for element in elements if element.is_displayed()] - - def hover_on_element(driver, selector): """ Fires the hover event for the specified element by the given selector. @@ -274,6 +262,19 @@ def wait_for_element_not_visible(driver, selector, by=By.CSS_SELECTOR, "Element %s was still visible after %s seconds!" % (selector, timeout)) +def find_visible_elements(driver, selector, by=By.CSS_SELECTOR): + """ + Finds all WebElements that match a selector and are visible. + Similar to webdriver.find_elements. + @Params + driver - the webdriver object (required) + selector - the locator that is used to search the DOM (required) + by - the method to search for the locator (Default: By.CSS_SELECTOR) + """ + elements = driver.find_elements(by=by, value=selector) + return [element for element in elements if element.is_displayed()] + + def wait_for_ready_state_complete(driver, timeout=settings.EXTREME_TIMEOUT): """ The DOM (Document Object Model) has a property called "readyState". From b4cba62c2624c9dde643a02a60ae54e47aa00a77 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Dec 2015 20:14:15 -0500 Subject: [PATCH 194/219] There's no time --- seleniumbase/fixtures/base_case.py | 1 - 1 file changed, 1 deletion(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 65593f84bf17..f8bcad0b6ca1 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -4,7 +4,6 @@ """ import json -import time import pytest import logging import unittest From 99b436683508fecb8c7bc250f3df8a2c0b30627f Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Dec 2015 20:31:41 -0500 Subject: [PATCH 195/219] Flake8 and comments --- seleniumbase/fixtures/base_case.py | 121 ++++++++++++++--------------- 1 file changed, 58 insertions(+), 63 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index f8bcad0b6ca1..f4e2b72cc819 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -29,12 +29,13 @@ def __init__(self, *args, **kwargs): pass self.environment = None - - # pytest-specific config - # Be careful if a subclass of BaseCase overrides setUp() - # You'll need to add the following line to the subclass setUp() method: - # super(SubClassOfBaseCase, self).setUp() def setUp(self): + """ + pytest-specific code + Be careful if a subclass of BaseCase overrides setUp() + You'll need to add the following line to the subclass setUp() method: + super(SubClassOfBaseCase, self).setUp() + """ self.is_pytest = None try: # This raises an exception if the test is not coming from pytest @@ -49,28 +50,27 @@ def setUp(self): if self.with_selenium: self.driver = browser_launcher.get_driver(self.browser) - - # pytest-specific config - # Be careful if a subclass of BaseCase overrides setUp() - # You'll need to add the following line to the subclass tearDown() method: - # super(SubClassOfBaseCase, self).tearDown() def tearDown(self): + """ + pytest-specific code + Be careful if a subclass of BaseCase overrides setUp() + You'll need to add the following line to the subclass's tearDown(): + super(SubClassOfBaseCase, self).tearDown() + """ if self.is_pytest: if self.with_selenium: self.driver.quit() - def open(self, url): self.driver.get(url) if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: self.wait_for_ready_state_complete() - def open_url(self, url): - """ In case people are mixing up self.open() with open(), use this alternative. """ + """ In case people are mixing up self.open() with open(), + use this alternative. """ self.open(url) - def click(self, selector, by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): element = page_actions.wait_for_element_visible( @@ -79,15 +79,14 @@ def click(self, selector, by=By.CSS_SELECTOR, if settings.WAIT_FOR_RSC_ON_CLICKS: self.wait_for_ready_state_complete() - def click_link_text(self, link_text, timeout=settings.SMALL_TIMEOUT): element = self.wait_for_link_text_visible(link_text, timeout=timeout) element.click() if settings.WAIT_FOR_RSC_ON_CLICKS: self.wait_for_ready_state_complete() - - def update_text_value(self, selector, new_value, timeout=settings.SMALL_TIMEOUT, retry=False): + def update_text_value(self, selector, new_value, + timeout=settings.SMALL_TIMEOUT, retry=False): """ This method updates a selector's text value with a new value @Params selector - the selector with the value to update @@ -98,123 +97,119 @@ def update_text_value(self, selector, new_value, timeout=settings.SMALL_TIMEOUT, element = self.wait_for_element_visible(selector, timeout=timeout) element.clear() element.send_keys(new_value) - if retry and element.get_attribute('value') != new_value and not new_value.endswith('\n'): + if (retry and element.get_attribute('value') != new_value + and not new_value.endswith('\n')): logging.debug('update_text_value is falling back to jQuery!') selector = self.jq_format(selector) self.set_value(selector, new_value) - def is_element_present(self, selector, by=By.CSS_SELECTOR): return page_actions.is_element_present(self.driver, selector, by) - def is_element_visible(self, selector, by=By.CSS_SELECTOR): return page_actions.is_element_visible(self.driver, selector, by) - def is_link_text_visible(self, link_text): - return page_actions.is_element_visible(self.driver, link_text, by=By.LINK_TEXT) - + return page_actions.is_element_visible(self.driver, link_text, + by=By.LINK_TEXT) def is_text_visible(self, text, selector, by=By.CSS_SELECTOR): return page_actions.is_text_visible(self.driver, text, selector, by) - def find_visible_elements(self, selector, by=By.CSS_SELECTOR): return page_actions.find_visible_elements(self.driver, selector, by) - def jquery_click(self, selector): self.driver.execute_script("jQuery('%s').click()" % selector) - def execute_script(self, script): return self.driver.execute_script(script) - def set_window_size(self, width, height): return self.driver.set_window_size(width, height) - def maximize_window(self): return self.driver.maximize_window() - def activate_jquery(self): """ (It's not on by default on all website pages.) """ - self.driver.execute_script('var script = document.createElement("script"); script.src = "https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"; document.getElementsByTagName("head")[0].appendChild(script);') - + self.driver.execute_script( + '''var script = document.createElement("script"); ''' + '''script.src = "https://ajax.googleapis.com/ajax/libs/jquery/1/''' + '''jquery.min.js"; document.getElementsByTagName("head")[0]''' + '''.appendChild(script);''') def scroll_to(self, selector): self.wait_for_element_visible(selector, timeout=settings.SMALL_TIMEOUT) - self.driver.execute_script("jQuery('%s')[0].scrollIntoView()" % selector) - + self.driver.execute_script( + "jQuery('%s')[0].scrollIntoView()" % selector) def scroll_click(self, selector): self.scroll_to(selector) self.click(selector) - def hover_on_element(self, selector): return page_actions.hover_on_element(self.driver, selector) - - def hover_and_click(self, hover_selector, click_selector, click_by=By.CSS_SELECTOR, timeout=settings.SMALL_TIMEOUT): - return page_actions.hover_and_click(self.driver, hover_selector, click_selector, click_by, timeout) - + def hover_and_click(self, hover_selector, click_selector, + click_by=By.CSS_SELECTOR, + timeout=settings.SMALL_TIMEOUT): + return page_actions.hover_and_click(self.driver, hover_selector, + click_selector, click_by, timeout) def jq_format(self, code): return page_utils.jq_format(code) - def set_value(self, selector, value): val = json.dumps(value) self.driver.execute_script("jQuery('%s').val(%s)" % (selector, val)) - - def jquery_update_text_value(self, selector, new_value, timeout=settings.SMALL_TIMEOUT): + def jquery_update_text_value(self, selector, new_value, + timeout=settings.SMALL_TIMEOUT): element = self.wait_for_element_visible(selector, timeout=timeout) self.driver.execute_script("""jQuery('%s').val('%s')""" % (selector, self.jq_format(new_value))) if new_value.endswith('\n'): element.send_keys('\n') + def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, + timeout=settings.LARGE_TIMEOUT): + return page_actions.wait_for_element_present( + self.driver, selector, by, timeout) - def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): - return page_actions.wait_for_element_present(self.driver, selector, by, timeout) - + def wait_for_element_visible(self, selector, by=By.CSS_SELECTOR, + timeout=settings.LARGE_TIMEOUT): + return page_actions.wait_for_element_visible( + self.driver, selector, by, timeout) - def wait_for_element_visible(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): - return page_actions.wait_for_element_visible(self.driver, selector, by, timeout) + def wait_for_text_visible(self, text, selector, by=By.CSS_SELECTOR, + timeout=settings.LARGE_TIMEOUT): + return page_actions.wait_for_text_visible( + self.driver, text, selector, by, timeout) + def wait_for_link_text_visible(self, link_text, + timeout=settings.LARGE_TIMEOUT): + return self.wait_for_element_visible( + link_text, by=By.LINK_TEXT, timeout=timeout) - def wait_for_text_visible(self, text, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): - return page_actions.wait_for_text_visible(self.driver, text, selector, by, timeout) - - - def wait_for_link_text_visible(self, link_text, timeout=settings.LARGE_TIMEOUT): - return self.wait_for_element_visible(link_text, by=By.LINK_TEXT, timeout=timeout) - - - def wait_for_element_absent(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): - return page_actions.wait_for_element_absent(self.driver, selector, by, timeout) - - - def wait_for_element_not_visible(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): - return page_actions.wait_for_element_not_visible(self.driver, selector, by, timeout) + def wait_for_element_absent(self, selector, by=By.CSS_SELECTOR, + timeout=settings.LARGE_TIMEOUT): + return page_actions.wait_for_element_absent( + self.driver, selector, by, timeout) + def wait_for_element_not_visible(self, selector, by=By.CSS_SELECTOR, + timeout=settings.LARGE_TIMEOUT): + return page_actions.wait_for_element_not_visible( + self.driver, selector, by, timeout) def wait_for_ready_state_complete(self, timeout=settings.EXTREME_TIMEOUT): return page_actions.wait_for_ready_state_complete(self.driver, timeout) - def wait_for_and_accept_alert(self, timeout=settings.LARGE_TIMEOUT): return page_actions.wait_for_and_accept_alert(self.driver, timeout) - def wait_for_and_dismiss_alert(self, timeout=settings.LARGE_TIMEOUT): return page_actions.wait_for_and_dismiss_alert(self.driver, timeout) - def wait_for_and_switch_to_alert(self, timeout=settings.LARGE_TIMEOUT): return page_actions.wait_for_and_switch_to_alert(self.driver, timeout) From 063508fdbbeb8086573de52ebc6861a483f3c8a0 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Dec 2015 21:30:55 -0500 Subject: [PATCH 196/219] Reorganizing method ordering --- seleniumbase/fixtures/base_case.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index f4e2b72cc819..2ec7ce817324 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -119,9 +119,6 @@ def is_text_visible(self, text, selector, by=By.CSS_SELECTOR): def find_visible_elements(self, selector, by=By.CSS_SELECTOR): return page_actions.find_visible_elements(self.driver, selector, by) - def jquery_click(self, selector): - self.driver.execute_script("jQuery('%s').click()" % selector) - def execute_script(self, script): return self.driver.execute_script(script) @@ -148,14 +145,8 @@ def scroll_click(self, selector): self.scroll_to(selector) self.click(selector) - def hover_on_element(self, selector): - return page_actions.hover_on_element(self.driver, selector) - - def hover_and_click(self, hover_selector, click_selector, - click_by=By.CSS_SELECTOR, - timeout=settings.SMALL_TIMEOUT): - return page_actions.hover_and_click(self.driver, hover_selector, - click_selector, click_by, timeout) + def jquery_click(self, selector): + self.driver.execute_script("jQuery('%s').click()" % selector) def jq_format(self, code): return page_utils.jq_format(code) @@ -172,6 +163,15 @@ def jquery_update_text_value(self, selector, new_value, if new_value.endswith('\n'): element.send_keys('\n') + def hover_on_element(self, selector): + return page_actions.hover_on_element(self.driver, selector) + + def hover_and_click(self, hover_selector, click_selector, + click_by=By.CSS_SELECTOR, + timeout=settings.SMALL_TIMEOUT): + return page_actions.hover_and_click(self.driver, hover_selector, + click_selector, click_by, timeout) + def wait_for_element_present(self, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT): return page_actions.wait_for_element_present( From 03b168fb387d5b096b72de2fa87e066bd2464d60 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Dec 2015 22:26:22 -0500 Subject: [PATCH 197/219] Update the comments --- seleniumbase/fixtures/base_case.py | 3 ++- seleniumbase/fixtures/page_actions.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 2ec7ce817324..0606623f3f92 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1,6 +1,7 @@ """ These methods improve on and expand existing WebDriver commands. -Improvements include making WebDriver commands more robust and reliable. +Improvements include making WebDriver commands more robust and more reliable +by giving page elements enough time to load before taking action on them. """ import json diff --git a/seleniumbase/fixtures/page_actions.py b/seleniumbase/fixtures/page_actions.py index 2e37f19d4774..4ee93d7f7da1 100755 --- a/seleniumbase/fixtures/page_actions.py +++ b/seleniumbase/fixtures/page_actions.py @@ -3,7 +3,8 @@ for waiting for elements to appear on a page. These methods improve on and expand existing WebDriver commands. -Improvements include making WebDriver commands more robust and reliable. +Improvements include making WebDriver commands more robust and more reliable +by giving page elements enough time to load before taking action on them. The default option for searching for elements is by CSS Selector. This can be changed by overriding the "By" parameter. From 7608bb82c8ad74e70051d256d974d5972b381d1f Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Dec 2015 23:04:30 -0500 Subject: [PATCH 198/219] On a flake8 crusade --- seleniumbase/plugins/base_plugin.py | 20 +++--- seleniumbase/plugins/basic_test_info.py | 8 +-- seleniumbase/plugins/db_reporting_plugin.py | 21 +++---- .../plugins/docker_selenium_plugin.py | 14 ++--- .../plugins/hipchat_reporting_plugin.py | 62 +++++++++++-------- seleniumbase/plugins/page_source.py | 5 +- seleniumbase/plugins/s3_logging_plugin.py | 9 ++- seleniumbase/plugins/screen_shots.py | 5 +- seleniumbase/plugins/selenium_plugin.py | 39 ++++++------ 9 files changed, 84 insertions(+), 99 deletions(-) diff --git a/seleniumbase/plugins/base_plugin.py b/seleniumbase/plugins/base_plugin.py index 20993663f2b8..d531655d9f9b 100755 --- a/seleniumbase/plugins/base_plugin.py +++ b/seleniumbase/plugins/base_plugin.py @@ -18,11 +18,11 @@ class Base(Plugin): The base_plugin includes the following variables: self.options.env -- the environment for the tests to use (--env=ENV) self.options.data -- any extra data to pass to the tests (--data=DATA) - self.options.log_path -- the directory in which the log files are saved (--log_path=LOG_PATH) + self.options.log_path -- the directory in which the log files + are saved (--log_path=LOG_PATH) """ name = 'testing_base' # Usage: --with-testing_base - def options(self, parser, env): super(Base, self).options(parser, env=env) parser.add_option('--env', action='store', @@ -42,7 +42,6 @@ def options(self, parser, env): default='logs/', help='Where the log files are saved.') - def configure(self, options, conf): super(Base, self).configure(options, conf) if not self.enabled: @@ -59,7 +58,6 @@ def configure(self, options, conf): options.log_path, int(time.time()))) os.makedirs(options.log_path) - def beforeTest(self, test): test_logpath = self.options.log_path + "/" + test.id() if not os.path.exists(test_logpath): @@ -68,18 +66,18 @@ def beforeTest(self, test): test.test.data = self.options.data test.test.args = self.options - def addError(self, test, err, capt=None): """ Since Skip, Blocked, and Deprecated are all technically errors, but not - error states, we want to make sure that they don't show up in nose output - as errors. + error states, we want to make sure that they don't show up in + the nose output as errors. """ if (err[0] == errors.BlockedTest or - err[0] == errors.SkipTest or - err[0] == errors.DeprecatedTest): - print err[1].__str__().split('-------------------- >> begin captured logging << --------------------', 1)[0] - + err[0] == errors.SkipTest or + err[0] == errors.DeprecatedTest): + print err[1].__str__().split('''-------------------- >> ''' + '''begin captured logging''' + ''' << --------------------''', 1)[0] def handleError(self, test, err, capt=None): """ diff --git a/seleniumbase/plugins/basic_test_info.py b/seleniumbase/plugins/basic_test_info.py index 4c20795bd2d8..3cce8d131dc2 100755 --- a/seleniumbase/plugins/basic_test_info.py +++ b/seleniumbase/plugins/basic_test_info.py @@ -14,6 +14,7 @@ import traceback from nose.plugins import Plugin + class BasicTestInfo(Plugin): """ This plugin will capture basic info when a test fails or @@ -27,14 +28,12 @@ class BasicTestInfo(Plugin): def options(self, parser, env): super(BasicTestInfo, self).options(parser, env=env) - def configure(self, options, conf): super(BasicTestInfo, self).configure(options, conf) if not self.enabled: return self.options = options - def addError(self, test, err, capt=None): test_logpath = self.options.log_path + "/" + test.id() if not os.path.exists(test_logpath): @@ -44,7 +43,6 @@ def addError(self, test, err, capt=None): self.__log_test_error_data(basic_info_file, test, err, "Error") basic_info_file.close() - def addFailure(self, test, err, capt=None, tbinfo=None): test_logpath = self.options.log_path + "/" + test.id() if not os.path.exists(test_logpath): @@ -54,12 +52,12 @@ def addFailure(self, test, err, capt=None, tbinfo=None): self.__log_test_error_data(basic_info_file, test, err, "Error") basic_info_file.close() - def __log_test_error_data(self, log_file, test, err, type): data_to_save = [] data_to_save.append("Last_Page: %s" % test.driver.current_url) data_to_save.append("Browser: %s " % self.options.browser) data_to_save.append("Server: %s " % self.options.servername) data_to_save.append("%s: %s" % (type, err[0])) - data_to_save.append("Traceback: " + ''.join(traceback.format_exception(*err))) + data_to_save.append("Traceback: " + ''.join( + traceback.format_exception(*err))) log_file.writelines("\r\n".join(data_to_save)) diff --git a/seleniumbase/plugins/db_reporting_plugin.py b/seleniumbase/plugins/db_reporting_plugin.py index 6a5cc39f228d..b25ecdfb7c69 100755 --- a/seleniumbase/plugins/db_reporting_plugin.py +++ b/seleniumbase/plugins/db_reporting_plugin.py @@ -1,5 +1,6 @@ """ -The Database test reporting plugin for recording all test run data in the database. +This is the Database test reporting plugin for +recording all test run data in the database. """ import getpass @@ -33,7 +34,6 @@ def __init__(self): self.testcase_manager = None self.error_handled = False - def options(self, parser, env): super(DBReporting, self).options(parser, env=env) parser.add_option('--database_environment', action='store', @@ -42,15 +42,12 @@ def options(self, parser, env): default='test', help=SUPPRESS_HELP) - - #Plugin methods def configure(self, options, conf): """get the options""" super(DBReporting, self).configure(options, conf) self.options = options self.testcase_manager = TestcaseManager(self.options.database_env) - def begin(self): """At the start of the run, we want to record the execution information to the database.""" @@ -61,7 +58,6 @@ def begin(self): exec_payload.username = getpass.getuser() self.testcase_manager.insert_execution_data(exec_payload) - def startTest(self, test): """at the start of the test, set the test case details""" data_payload = TestcaseDataPayload() @@ -82,7 +78,6 @@ def startTest(self, test): # Make the testcase guid available to other plugins test.testcase_guid = self.testcase_guid - def finalize(self, result): """At the end of the run, we want to update that row with the execution time.""" @@ -90,21 +85,18 @@ def finalize(self, result): self.testcase_manager.update_execution_data(self.execution_guid, runtime) - def addSuccess(self, test, capt): """ After sucess of a test, we want to record the testcase run information. """ self.__insert_test_result(constants.State.PASS, test) - def addError(self, test, err, capt=None): """ After error of a test, we want to record the testcase run information. """ self.__insert_test_result(constants.State.ERROR, test, err) - def handleError(self, test, err, capt=None): """ After error of a test, we want to record the testcase run information. @@ -129,14 +121,12 @@ def handleError(self, test, err, capt=None): raise SkipTest(err[1]) return True - def addFailure(self, test, err, capt=None, tbinfo=None): """ - After failure of a test, we want to record the testcase run information. + After failure of a test, we want to record testcase run information. """ self.__insert_test_result(constants.State.FAILURE, test, err) - def __insert_test_result(self, state, test, err=None): data_payload = TestcaseDataPayload() data_payload.runtime = int(time.time() * 1000) - self.case_start_time @@ -144,5 +134,8 @@ def __insert_test_result(self, state, test, err=None): data_payload.execution_guid = self.execution_guid data_payload.state = state if err is not None: - data_payload.message = err[1].__str__().split('-------------------- >> begin captured logging << --------------------', 1)[0] + data_payload.message = err[1].__str__().split( + '''-------------------- >> ''' + '''begin captured logging''' + ''' << --------------------''', 1)[0] self.testcase_manager.update_testcase_data(data_payload) diff --git a/seleniumbase/plugins/docker_selenium_plugin.py b/seleniumbase/plugins/docker_selenium_plugin.py index 5c465113a251..a567538a93ea 100755 --- a/seleniumbase/plugins/docker_selenium_plugin.py +++ b/seleniumbase/plugins/docker_selenium_plugin.py @@ -28,8 +28,8 @@ def options(self, parser, env): dest='browser', choices=constants.Browser.VERSION.keys(), default=constants.Browser.FIREFOX, - help="""Specifies the browser to use. Default = FireFox. - If you want to use Chrome, explicitly indicate that.""") + help="""Specifies the browser. Default: FireFox. + If you want to use Chrome, indicate that.""") parser.add_option('--browser_version', action='store', dest='browser_version', default="latest", @@ -37,11 +37,12 @@ def options(self, parser, env): a version number or use "latest".""") parser.add_option('--server', action='store', dest='servername', default='localhost', - help="Designates the server used by the test. Default: localhost.") + help="""Designates the server used by the test. + Default: localhost.""") parser.add_option('--port', action='store', dest='port', default='4444', - help="Designates the port used by the test. Default: 4444.") - + help="""Designates the port used by the test. + Default: 4444.""") def configure(self, options, conf): super(SeleniumBrowser, self).configure(options, conf) @@ -50,7 +51,6 @@ def configure(self, options, conf): self.driver = self.__select_browser() self.options = options - def beforeTest(self, test): """ Running Selenium locally will be handled differently from how Selenium is run remotely, such as from Jenkins. """ @@ -65,7 +65,6 @@ def beforeTest(self, test): os.kill(os.getpid(), 9) return self.driver - def afterTest(self, test): try: self.driver.quit() @@ -73,7 +72,6 @@ def afterTest(self, test): except: print "No driver to quit." - def __select_browser(self): try: profile = webdriver.FirefoxProfile() diff --git a/seleniumbase/plugins/hipchat_reporting_plugin.py b/seleniumbase/plugins/hipchat_reporting_plugin.py index 165cb34315d9..62d2fb1bbfa2 100755 --- a/seleniumbase/plugins/hipchat_reporting_plugin.py +++ b/seleniumbase/plugins/hipchat_reporting_plugin.py @@ -1,5 +1,6 @@ """ This plugin allows you to receive test notifications through HipChat. -HipChat @ mentions will only occur during normal business hours. (You can change this.) +HipChat @ mentions will only occur during normal +business hours. (You can change this) By default, only failure notifications will be sent. """ @@ -16,7 +17,11 @@ class HipchatReporting(Plugin): - name = 'hipchat_reporting' # Usage: --with-hipchat_reporting --hipchat_room_id=[HIPCHAT ROOM ID] --hipchat_owner_to_mention=[HIPCHAT @NAME] + ''' + Usage: --with-hipchat_reporting --hipchat_room_id=[HIPCHAT ROOM ID] + --hipchat_owner_to_mention=[HIPCHAT @NAME] + ''' + name = 'hipchat_reporting' def __init__(self): super(HipchatReporting, self).__init__() @@ -28,52 +33,55 @@ def __init__(self): self.failures = [] self.errors = [] - def options(self, parser, env): super(HipchatReporting, self).options(parser, env=env) - parser.add_option('--hipchat_room_id', action='store', - dest='hipchat_room_id', - help='The hipchat room ID notifications will be sent to.', - default=None) - parser.add_option('--hipchat_owner_to_mention', action='store', - dest='hipchat_owner_to_mention', - help='The hipchat username to @mention in notifications.', - default=None) - parser.add_option('--hipchat_notify_on_success', action='store_true', default=False, - dest='hipchat_notify_on_success', - help='Flag for including success notifications. If not specified, only notifies on errors/failures by default.') - + parser.add_option( + '--hipchat_room_id', action='store', + dest='hipchat_room_id', + help='The hipchat room ID notifications will be sent to.', + default=None) + parser.add_option( + '--hipchat_owner_to_mention', action='store', + dest='hipchat_owner_to_mention', + help='The hipchat username to @mention in notifications.', + default=None) + parser.add_option( + '--hipchat_notify_on_success', action='store_true', + default=False, + dest='hipchat_notify_on_success', + help='''Flag for including success notifications. + If not specified, only notifies on errors/failures + by default.''') def configure(self, options, conf): super(HipchatReporting, self).configure(options, conf) if not self.enabled: return if not options.hipchat_room_id: - raise Exception('A hipchat room ID to notify must be specified when using the hipchat reporting plugin.') + raise Exception('''A hipchat room ID to notify must be specified + when using the hipchat reporting plugin.''') else: self.hipchat_room_id = options.hipchat_room_id - self.hipchat_owner_to_mention = options.hipchat_owner_to_mention or None + self.hipchat_owner_to_mention = (options.hipchat_owner_to_mention + or None) self.hipchat_notify_on_success = options.hipchat_notify_on_success - def addSuccess(self, test, capt): self.successes.append(test.id()) - def addError(self, test, err, capt=None): self.errors.append("ERROR: " + test.id()) - def addFailure(self, test, err, capt=None, tbinfo=None): self.failures.append("FAILED: " + test.id()) - def finalize(self, result): message = '' success = True if not result.wasSuccessful(): success = False - if self.hipchat_owner_to_mention and self._is_during_business_hours(): + if (self.hipchat_owner_to_mention and + self._is_during_business_hours()): message += "@" + self.hipchat_owner_to_mention + '\n' if self.failures: @@ -93,20 +101,19 @@ def finalize(self, result): if message: self._send_hipchat_notification(message, success=success) - def _is_during_business_hours(self): now = datetime.datetime.now() # Mon - Fri, 9am-6pm return now.weekday() <= 4 and now.hour >= 9 and now.hour <= 18 - - def _send_hipchat_notification(self, message, success=True, sender='Selenium'): + def _send_hipchat_notification(self, message, success=True, + sender='Selenium'): response = requests.post(HIPCHAT_URL, params={ 'auth_token': HIPCHAT_AUTH_TOKEN, 'room_id': self.hipchat_room_id, 'from': sender, 'message': message, - 'message_format': 'text', # @mentions are only supported in text format + 'message_format': 'text', 'color': 'green' if success else 'red', 'notify': '0', 'format': 'json' @@ -116,5 +123,6 @@ def _send_hipchat_notification(self, message, success=True, sender='Selenium'): logging.debug("Notification sent to room %s", self.hipchat_room_id) return True else: - logging.error("Failed to send notification to room %s", self.hipchat_room_id) + logging.error("Failed to send notification to room %s", + self.hipchat_room_id) return False diff --git a/seleniumbase/plugins/page_source.py b/seleniumbase/plugins/page_source.py index d02edbb5048b..fcc151e6a8c2 100755 --- a/seleniumbase/plugins/page_source.py +++ b/seleniumbase/plugins/page_source.py @@ -6,6 +6,7 @@ import codecs from nose.plugins import Plugin + class PageSource(Plugin): """ This plugin will capture the page source when a test fails @@ -13,20 +14,17 @@ class PageSource(Plugin): logs file specified, along with default test information. """ name = "page_source" # Usage: --with-page_source - logfile_name = "page_source.html" def options(self, parser, env): super(PageSource, self).options(parser, env=env) - def configure(self, options, conf): super(PageSource, self).configure(options, conf) if not self.enabled: return self.options = options - def addError(self, test, err, capt=None): test_logpath = self.options.log_path + "/" + test.id() if not os.path.exists(test_logpath): @@ -36,7 +34,6 @@ def addError(self, test, err, capt=None): html_file.write(test.driver.page_source) html_file.close() - def addFailure(self, test, err, capt=None, tbinfo=None): test_logpath = self.options.log_path + "/" + test.id() if not os.path.exists(test_logpath): diff --git a/seleniumbase/plugins/s3_logging_plugin.py b/seleniumbase/plugins/s3_logging_plugin.py index a086fea82db5..8b6022db70b8 100755 --- a/seleniumbase/plugins/s3_logging_plugin.py +++ b/seleniumbase/plugins/s3_logging_plugin.py @@ -15,13 +15,11 @@ class S3Logging(Plugin): """ name = 's3_logging' # Usage: --with-s3_logging - def configure(self, options, conf): """ Get the options. """ super(S3Logging, self).configure(options, conf) self.options = options - def afterTest(self, test): """ After each testcase, upload logs to the S3 bucket. """ s3_bucket = S3LoggingBucket() @@ -31,8 +29,8 @@ def afterTest(self, test): uploaded_files = [] for logfile in os.listdir(path): logfile_name = "%s/%s/%s" % (guid, - test.test.id(), - logfile.split(path)[-1]) + test.test.id(), + logfile.split(path)[-1]) s3_bucket.upload_file(logfile_name, "%s/%s" % (path, logfile)) uploaded_files.append(logfile_name) @@ -41,7 +39,8 @@ def afterTest(self, test): print "Log files uploaded: %s" % index_file logging.error("Log files uploaded: %s" % index_file) - # If the database plugin is running, attach a link to the logs index database row + # If the database plugin is running, attach a link + # to the logs index database row if hasattr(test.test, "testcase_guid"): from seleniumbase.core.testcase_manager \ import TestcaseDataPayload, TestcaseManager diff --git a/seleniumbase/plugins/screen_shots.py b/seleniumbase/plugins/screen_shots.py index 3c35a688f437..9a4f11d2823b 100755 --- a/seleniumbase/plugins/screen_shots.py +++ b/seleniumbase/plugins/screen_shots.py @@ -6,6 +6,7 @@ import time from nose.plugins import Plugin + class ScreenShots(Plugin): """ This plugin will take a screenshot when either a test fails @@ -21,14 +22,12 @@ class ScreenShots(Plugin): def options(self, parser, env): super(ScreenShots, self).options(parser, env=env) - def configure(self, options, conf): super(ScreenShots, self).configure(options, conf) if not self.enabled: return self.options = options - def add_screenshot(self, test, err, capt=None, tbinfo=None): test_logpath = self.options.log_path + "/" + test.id() if not os.path.exists(test_logpath): @@ -50,10 +49,8 @@ def add_screenshot(self, test, err, capt=None, tbinfo=None): f1.write(screen) f1.close()''' - def addError(self, test, err, capt=None): self.add_screenshot(test, err, capt=capt) - def addFailure(self, test, err, capt=None, tbinfo=None): self.add_screenshot(test, err, capt=capt, tbinfo=tbinfo) diff --git a/seleniumbase/plugins/selenium_plugin.py b/seleniumbase/plugins/selenium_plugin.py index 9e741fc2434c..9917e30c1db2 100755 --- a/seleniumbase/plugins/selenium_plugin.py +++ b/seleniumbase/plugins/selenium_plugin.py @@ -1,6 +1,6 @@ """ -This is the Selenium plugin. It takes in some default parameters that tests need. -It also provides a WebDriver object for the tests to use. +This plugin gives the power of Selenium to nosetests +by providing a WebDriver object for the tests to use. """ import time @@ -31,20 +31,21 @@ def options(self, parser, env): dest='browser', choices=constants.Browser.VERSION.keys(), default=constants.Browser.FIREFOX, - help="""Specifies the browser to use. Default = FireFox. - If you want to use Chrome, explicitly indicate that.""") + help="""Specifies the browser. Default: FireFox. + If you want to use Chrome, indicate that.""") parser.add_option('--browser_version', action='store', dest='browser_version', default="latest", help="""The browser version to use. Explicitly select - a version number or use "latest".""") + a version number or use "latest".""") parser.add_option('--server', action='store', dest='servername', default='localhost', - help="Designates the server used by the test. Default: localhost.") + help="""Designates the server used by the test. + Default: localhost.""") parser.add_option('--port', action='store', dest='port', default='4444', - help="Designates the port used by the test. Default: 4444.") - + help="""Designates the port used by the test. + Default: 4444.""") def configure(self, options, conf): super(SeleniumBrowser, self).configure(options, conf) @@ -70,19 +71,17 @@ def configure(self, options, conf): else: version_options = constants.Browser.VERSION[options.browser] if (version_options is not None and - options.browser_version in version_options): + options.browser_version in version_options): self.browser_settings["version"] = options.browser_version self.options = options - ### print 'OPTIONS = ' + str(self.options) # Try this for debugging if needed if (self.options.servername == "localhost" and - self.options.browser == constants.Browser.HTML_UNIT): + self.options.browser == constants.Browser.HTML_UNIT): selenium_launcher.execute_selenium(self.options.servername, self.options.port, self.options.log_path) - def beforeTest(self, test): """ Running Selenium locally will be handled differently from how Selenium is run remotely, such as from Jenkins. """ @@ -110,11 +109,11 @@ def beforeTest(self, test): version = self.browser_settings["version"] else: version = "" - test.test.browser = "%s%s" % (self.options.browser, version) + test.test.browser = "%s%s" % ( + self.options.browser, version) connected = True break except Exception as err: - # nose eats beforeTest exceptions, so this gets the word out if something breaks here print "Attempt #%s to connect to Selenium failed" % i if i < 3: print "Retrying in 15 seconds..." @@ -125,20 +124,18 @@ def beforeTest(self, test): print "\n\n\n" os.kill(os.getpid(), 9) - def afterTest(self, test): try: self.driver.quit() except: print "No driver to quit." - def __select_browser(self, browser_name): if (self.options.servername != "localhost" or - self.options.browser == constants.Browser.HTML_UNIT): - return webdriver.Remote("http://%s:%s/wd/hub" % - (self.options.servername, - self.options.port), - self.browser_settings) + self.options.browser == constants.Browser.HTML_UNIT): + return webdriver.Remote("http://%s:%s/wd/hub" % ( + self.options.servername, + self.options.port), + self.browser_settings) else: return browser_launcher.get_driver(browser_name) From fc8a0f1dab8df57cf637ab16752dda5558bba361 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Dec 2015 23:12:12 -0500 Subject: [PATCH 199/219] pyflakes are part of a healthy code diet --- docker/docker_requirements.txt | 1 + requirements.txt | 1 + server_requirements.txt | 1 + 3 files changed, 3 insertions(+) diff --git a/docker/docker_requirements.txt b/docker/docker_requirements.txt index f8001f4bdb47..b9667ac00572 100755 --- a/docker/docker_requirements.txt +++ b/docker/docker_requirements.txt @@ -1,6 +1,7 @@ selenium==2.48.0 nose==1.3.7 pytest==2.8.3 +flake8==2.5.0 requests==2.7.0 urllib3==1.10.4 BeautifulSoup==3.2.1 diff --git a/requirements.txt b/requirements.txt index 18f2b33dbb51..0304f628c8e2 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ selenium==2.48.0 nose==1.3.7 pytest==2.8.3 +flake8==2.5.0 requests==2.7.0 urllib3==1.10.4 BeautifulSoup==3.2.1 diff --git a/server_requirements.txt b/server_requirements.txt index 5b036db75a23..e0d27b5cf03c 100755 --- a/server_requirements.txt +++ b/server_requirements.txt @@ -1,6 +1,7 @@ selenium==2.48.0 nose==1.3.7 pytest==2.8.3 +flake8==2.5.0 requests==2.7.0 urllib3==1.10.4 BeautifulSoup==3.2.1 From f62f94cb48826d587b6b5b596e80e227583a9b5c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 2 Dec 2015 01:19:08 -0500 Subject: [PATCH 200/219] Flake8 updates --- seleniumbase/__init__.py | 2 +- seleniumbase/common/decorators.py | 1 - setup.py | 37 +++++++++++++++++-------------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/seleniumbase/__init__.py b/seleniumbase/__init__.py index f21fc4a5eb70..c733c1ab9667 100755 --- a/seleniumbase/__init__.py +++ b/seleniumbase/__init__.py @@ -1 +1 @@ -from seleniumbase.fixtures.base_case import BaseCase +from seleniumbase.fixtures.base_case import BaseCase # noqa diff --git a/seleniumbase/common/decorators.py b/seleniumbase/common/decorators.py index 1f316d89f3ff..8c08deb1348d 100755 --- a/seleniumbase/common/decorators.py +++ b/seleniumbase/common/decorators.py @@ -1,6 +1,5 @@ import logging import math -import requests import threading import time from functools import wraps diff --git a/setup.py b/setup.py index e0b02d84f331..657ae8603d5d 100755 --- a/setup.py +++ b/setup.py @@ -2,32 +2,35 @@ The setup package to install the SeleniumBase Test Framework plugins """ -from setuptools import setup, find_packages +from setuptools import setup, find_packages # noqa setup( - name = 'seleniumbase', - version = '1.1.7', - author = 'Michael Mintz', - author_email = '@mintzworld', - maintainer = 'Michael Mintz', - description = 'The SeleniumBase Test Framework. (Powered by Python, WebDriver, and more...)', - license = 'The MIT License', - packages = ['seleniumbase', - 'seleniumbase.core', - 'seleniumbase.plugins', - 'seleniumbase.fixtures', - 'seleniumbase.common', - 'seleniumbase.config'], - entry_points = { + name='seleniumbase', + version='1.1.7', + author='Michael Mintz', + author_email='@mintzworld', + maintainer='Michael Mintz', + description='''The SeleniumBase Test Framework. + (Powered by Python, WebDriver, and more...)''', + license='The MIT License', + packages=['seleniumbase', + 'seleniumbase.core', + 'seleniumbase.plugins', + 'seleniumbase.fixtures', + 'seleniumbase.common', + 'seleniumbase.config'], + entry_points={ 'nose.plugins': [ 'base_plugin = seleniumbase.plugins.base_plugin:Base', 'selenium = seleniumbase.plugins.selenium_plugin:SeleniumBrowser', 'page_source = seleniumbase.plugins.page_source:PageSource', 'screen_shots = seleniumbase.plugins.screen_shots:ScreenShots', 'test_info = seleniumbase.plugins.basic_test_info:BasicTestInfo', - 'db_reporting = seleniumbase.plugins.db_reporting_plugin:DBReporting', + 'db_reporting = ' + 'seleniumbase.plugins.db_reporting_plugin:DBReporting', 's3_logging = seleniumbase.plugins.s3_logging_plugin:S3Logging', - 'hipchat_reporting = seleniumbase.plugins.hipchat_reporting_plugin:HipchatReporting', + 'hipchat_reporting = seleniumbase.plugins' + '.hipchat_reporting_plugin:HipchatReporting', ] } ) From f6ee7a4c6d3766dbb12e6029aab6ccb9befa7f54 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 2 Dec 2015 01:55:46 -0500 Subject: [PATCH 201/219] Pytest plugin to save logs/screenshots when tests fail --- conftest.py | 36 +++++++++++++++++++++++++----- seleniumbase/fixtures/base_case.py | 19 +++++++++++++++- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/conftest.py b/conftest.py index 731d19ba31be..4699cf101664 100755 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,9 @@ """ This is the pytest configuration file """ import os +import shutil +import pytest +import time from seleniumbase.fixtures import constants @@ -21,14 +24,23 @@ def pytest_addoption(parser): default=None, help='Extra data to pass from the command line.') parser.addoption('--with-selenium', action="store_true", - dest='with_selenium', - default=False, - help="Input if tests need to be run with a web browser.") + dest='with_selenium', + default=False, + help="Use if tests need to be run with a web browser.") + parser.addoption('--with-testing_base', action="store_true", + dest='with_testing_base', + default=False, + help="Use to save logs (screenshots) when tests fail.") + parser.addoption('--log_path', dest='log_path', + default='logs/', + help='Where the log files are saved.') def pytest_configure(config): with_selenium = config.getoption('with_selenium') + with_testing_base = config.getoption('with_testing_base') browser = config.getoption('browser') + log_path = config.getoption('log_path') data = '' if config.getoption('data') is not None: data = config.getoption('data') @@ -38,6 +50,8 @@ def pytest_configure(config): config_file.write("with_selenium:::%s\n" % with_selenium) config_file.write("browser:::%s\n" % browser) config_file.write("data:::%s\n" % data) + config_file.write("with_testing_base:::%s\n" % with_testing_base) + config_file.write("log_path:::%s\n" % log_path) config_file.close() @@ -48,8 +62,20 @@ def pytest_unconfigure(): def pytest_runtest_setup(): - # A placeholder for a method that runs before every test with pytest - pass + # Handle Logging + with_testing_base = pytest.config.getoption('with_testing_base') + if with_testing_base: + log_path = pytest.config.getoption('log_path') + if log_path.endswith("/"): + log_path = log_path[:-1] + if not os.path.exists(log_path): + os.makedirs(log_path) + else: + if not os.path.exists("%s/../archived_logs/" % log_path): + os.makedirs("%s/../archived_logs/" % log_path) + shutil.move(log_path, "%s/../archived_logs/logs_%s" % ( + log_path, int(time.time()))) + os.makedirs(log_path) def pytest_runtest_teardown(): diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 0606623f3f92..9c5bfe07ef8d 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -5,8 +5,10 @@ """ import json -import pytest import logging +import os +import pytest +import sys import unittest from seleniumbase.config import settings from seleniumbase.core import browser_launcher @@ -46,6 +48,8 @@ def setUp(self): self.is_pytest = False if self.is_pytest: self.with_selenium = pytest.config.option.with_selenium + self.with_testing_base = pytest.config.option.with_testing_base + self.log_path = pytest.config.option.log_path self.browser = pytest.config.option.browser self.data = pytest.config.option.data if self.with_selenium: @@ -60,6 +64,19 @@ def tearDown(self): """ if self.is_pytest: if self.with_selenium: + # Save a screenshot if logging is on when an exception occurs + if self.with_testing_base and (sys.exc_info()[1] is not None): + test_id = "%s.%s.%s" % (self.__class__.__module__, + self.__class__.__name__, + self._testMethodName) + logfile_name = "screenshot.jpg" + test_logpath = self.log_path + "/" + test_id + if not os.path.exists(test_logpath): + os.makedirs(test_logpath) + screenshot_file = "%s/%s" % (test_logpath, logfile_name) + self.driver.get_screenshot_as_file(screenshot_file) + + # Finally close the browser self.driver.quit() def open(self, url): From a5393349319a3fc1eaeaa231da6869d8e466de4c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 2 Dec 2015 20:50:32 -0500 Subject: [PATCH 202/219] Add basic test info to log file after test failures (pytest) --- seleniumbase/core/pytest_helper.py | 19 +++++++++++++++++++ seleniumbase/fixtures/base_case.py | 17 ++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) create mode 100755 seleniumbase/core/pytest_helper.py diff --git a/seleniumbase/core/pytest_helper.py b/seleniumbase/core/pytest_helper.py new file mode 100755 index 000000000000..bdaf93ebe02e --- /dev/null +++ b/seleniumbase/core/pytest_helper.py @@ -0,0 +1,19 @@ +import sys +import traceback + + +def log_test_error_data(log_file, driver, browser): + try: + last_page = driver.current_url + except Exception: + last_page = '[WARNING! Browser Not Open!]' + if len(last_page) < 5: + last_page = '[WARNING! Browser Not Open!]' + data_to_save = [] + data_to_save.append("Last_Page: %s" % last_page) + data_to_save.append("Browser: %s " % browser) + data_to_save.append("Traceback: " + ''.join( + traceback.format_exception(sys.exc_info()[0], + sys.exc_info()[1], + sys.exc_info()[2]))) + log_file.writelines("\r\n".join(data_to_save)) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 9c5bfe07ef8d..ac00aeb915ac 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -4,6 +4,7 @@ by giving page elements enough time to load before taking action on them. """ +import codecs import json import logging import os @@ -12,6 +13,7 @@ import unittest from seleniumbase.config import settings from seleniumbase.core import browser_launcher +from seleniumbase.core import pytest_helper from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.common.by import By import page_actions @@ -69,12 +71,21 @@ def tearDown(self): test_id = "%s.%s.%s" % (self.__class__.__module__, self.__class__.__name__, self._testMethodName) - logfile_name = "screenshot.jpg" test_logpath = self.log_path + "/" + test_id if not os.path.exists(test_logpath): os.makedirs(test_logpath) - screenshot_file = "%s/%s" % (test_logpath, logfile_name) - self.driver.get_screenshot_as_file(screenshot_file) + # Handle screenshot logging + screenshot_name = "screenshot.jpg" + screenshot_path = "%s/%s" % (test_logpath, screenshot_name) + self.driver.get_screenshot_as_file(screenshot_path) + # Handle basic test info logging + basic_info_name = "basic_test_info.log" + basic_file_path = "%s/%s" % (test_logpath, basic_info_name) + basic_info_file = codecs.open( + basic_file_path, "w+", "utf-8") + pytest_helper.log_test_error_data( + basic_info_file, self.driver, self.browser) + basic_info_file.close() # Finally close the browser self.driver.quit() From 482b6b4d92d197ffb2f75b63b799ce29d9f03533 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 2 Dec 2015 20:53:31 -0500 Subject: [PATCH 203/219] Text-based log files will use the .txt extension (rather than .log) --- seleniumbase/fixtures/base_case.py | 2 +- seleniumbase/plugins/basic_test_info.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index ac00aeb915ac..70c86f43e904 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -79,7 +79,7 @@ def tearDown(self): screenshot_path = "%s/%s" % (test_logpath, screenshot_name) self.driver.get_screenshot_as_file(screenshot_path) # Handle basic test info logging - basic_info_name = "basic_test_info.log" + basic_info_name = "basic_test_info.txt" basic_file_path = "%s/%s" % (test_logpath, basic_info_name) basic_info_file = codecs.open( basic_file_path, "w+", "utf-8") diff --git a/seleniumbase/plugins/basic_test_info.py b/seleniumbase/plugins/basic_test_info.py index 3cce8d131dc2..d8dc523e47a7 100755 --- a/seleniumbase/plugins/basic_test_info.py +++ b/seleniumbase/plugins/basic_test_info.py @@ -23,7 +23,7 @@ class BasicTestInfo(Plugin): """ name = "basic_test_info" # Usage: --with-basic_test_info - logfile_name = "basic_test_info.log" + logfile_name = "basic_test_info.txt" def options(self, parser, env): super(BasicTestInfo, self).options(parser, env=env) From 881567d1aec1b4bf1de162a49570e3a258bccc29 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 2 Dec 2015 23:29:14 -0500 Subject: [PATCH 204/219] Flake8 updates --- seleniumbase/core/application_manager.py | 1 + seleniumbase/core/browser_launcher.py | 1 + seleniumbase/core/mysql.py | 6 +--- seleniumbase/core/mysql_conf.py | 10 +++++-- seleniumbase/core/s3_manager.py | 38 +++++++++++------------- seleniumbase/core/selenium_launcher.py | 27 ++++++++++------- seleniumbase/core/testcase_manager.py | 19 +++++------- seleniumbase/core/web_driver_retry.py | 28 ----------------- 8 files changed, 54 insertions(+), 76 deletions(-) delete mode 100755 seleniumbase/core/web_driver_retry.py diff --git a/seleniumbase/core/application_manager.py b/seleniumbase/core/application_manager.py index e020dc2701d7..c6b9112b6a4d 100755 --- a/seleniumbase/core/application_manager.py +++ b/seleniumbase/core/application_manager.py @@ -4,6 +4,7 @@ import time + class ApplicationManager: """ This class contains methods to generate application strings. diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 75d2d637b3ba..dae574dee729 100755 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -1,6 +1,7 @@ from selenium import webdriver from seleniumbase.fixtures import constants + def get_driver(browser_name): ''' Spins up a new web browser and returns the driver. diff --git a/seleniumbase/core/mysql.py b/seleniumbase/core/mysql.py index c31ebfec3a61..6907d01cce01 100755 --- a/seleniumbase/core/mysql.py +++ b/seleniumbase/core/mysql.py @@ -37,7 +37,6 @@ def __init__(self, database_env='test', conf_creds=None): if retry_count == 3: raise Exception("Unable to connect to Database after 3 retries.") - def fetchall_query_and_close(self, query, values): """ Executes a query, gets all the values and then closes up the connection @@ -47,17 +46,15 @@ def fetchall_query_and_close(self, query, values): self.__close_db() return retval - def fetchone_query_and_close(self, query, values): """ - Executes a query, gets the first value and then closes up the connection + Executes a query, gets the first value, and closes up the connection """ self.cursor.execute(query, values) retval = self.cursor.fetchone() self.__close_db() return retval - def execute_query_and_close(self, query, values): """ Executes a query and closes the connection @@ -66,7 +63,6 @@ def execute_query_and_close(self, query, values): self.__close_db() return retval - def __close_db(self): self.cursor.close() self.conn.close() diff --git a/seleniumbase/core/mysql_conf.py b/seleniumbase/core/mysql_conf.py index 1ba5c0e7fde5..57fc5ceff1b3 100755 --- a/seleniumbase/core/mysql_conf.py +++ b/seleniumbase/core/mysql_conf.py @@ -1,5 +1,6 @@ """ -This file contains database credentials for the various databases the tests need to access +This file contains database credentials for the various databases +that the tests need to access """ from seleniumbase.config import settings @@ -7,13 +8,18 @@ # Environments TEST = "test" + class Apps: TESTCASE_REPOSITORY = "testcase_repository" APP_CREDS = { Apps.TESTCASE_REPOSITORY: { - TEST: (settings.DB_HOST, settings.DB_USERNAME, settings.DB_PASSWORD, settings.DB_SCHEMA) + TEST: ( + settings.DB_HOST, + settings.DB_USERNAME, + settings.DB_PASSWORD, + settings.DB_SCHEMA) }, } diff --git a/seleniumbase/core/s3_manager.py b/seleniumbase/core/s3_manager.py index 2052cacf1e4a..289c1068457f 100755 --- a/seleniumbase/core/s3_manager.py +++ b/seleniumbase/core/s3_manager.py @@ -7,34 +7,32 @@ already_uploaded_files = [] + class S3LoggingBucket(object): """ A class to upload our log files from tests to S3, from whence we can share them. """ - def __init__(self, - log_bucket = settings.S3_LOG_BUCKET, - bucket_url = settings.S3_BUCKET_URL, - selenium_access_key = settings.S3_SELENIUM_ACCESS_KEY, - selenium_secret_key = settings.S3_SELENIUM_SECRET_KEY): + def __init__(self, + log_bucket=settings.S3_LOG_BUCKET, + bucket_url=settings.S3_BUCKET_URL, + selenium_access_key=settings.S3_SELENIUM_ACCESS_KEY, + selenium_secret_key=settings.S3_SELENIUM_SECRET_KEY): self.conn = S3Connection(selenium_access_key, selenium_secret_key) self.bucket = self.conn.get_bucket(log_bucket) - self.bucket_url = bucket_url - + self.bucket_url = bucket_url def get_key(self, _name): """create a new Key instance with the given name""" return Key(bucket=self.bucket, name=_name) - def get_bucket(self): """return the bucket we're using""" return self.bucket - def upload_file(self, file_name, file_path): """upload a given file from the file_path to the bucket with the new name/path file_name""" @@ -44,17 +42,17 @@ def upload_file(self, file_name, file_path): content_type = "text/html" if file_name.endswith(".jpg"): content_type = "image/jpg" - upload_key.set_contents_from_filename(file_path, - headers={"Content-Type":content_type}) + upload_key.set_contents_from_filename( + file_path, + headers={"Content-Type": content_type}) upload_key.url = \ upload_key.generate_url(expires_in=3600).split("?")[0] try: upload_key.make_public() except: - pass # we get a http version error here, we're going to pass + pass return file_name - def upload_index_file(self, test_address, timestamp): """create an index.html file with links to all the log files we just uploaded""" @@ -65,17 +63,17 @@ def upload_index_file(self, test_address, timestamp): index = self.get_key(file_name) index_str = [] for completed_file in already_uploaded_files: - index_str.append("%s"%(completed_file, completed_file)) - index.set_contents_from_string("
".join(index_str), - headers={"Content-Type":"text/html"}) + index_str.append("%s" % (completed_file, completed_file)) + index.set_contents_from_string( + "
".join(index_str), + headers={"Content-Type": "text/html"}) index.make_public() return "%s%s" % (self.bucket_url, file_name) - def save_uploaded_file_names(self, files): - """We keep record of file names that have been uploaded. We upload log - files related to each test after its execution. Once we're done, we + """We keep record of file names that have been uploaded. We upload log + files related to each test after its execution. Once we're done, we use already_uploaded_files to create an index file""" global already_uploaded_files already_uploaded_files.extend(files) diff --git a/seleniumbase/core/selenium_launcher.py b/seleniumbase/core/selenium_launcher.py index 5bc10840780a..d578c021fb83 100755 --- a/seleniumbase/core/selenium_launcher.py +++ b/seleniumbase/core/selenium_launcher.py @@ -6,12 +6,15 @@ import urllib import time -SELENIUM_JAR = "http://selenium-release.storage.googleapis.com/2.48/selenium-server-standalone-2.48.2.jar" +SELENIUM_JAR = ("http://selenium-release.storage.googleapis.com" + "/2.48/selenium-server-standalone-2.48.2.jar") JAR_FILE = "selenium-server-standalone-2.48.2.jar" + def download_selenium(): """ - Downloads the selenium server jar file from its online location and stores it locally. + Downloads the selenium server jar file from its + online location and stores it locally. """ try: local_file = open(JAR_FILE, 'wb') @@ -19,9 +22,10 @@ def download_selenium(): print 'Please wait, downloading Selenium...\n' local_file.write(remote_file.read()) local_file.close() - remote_file.close() + remote_file.close() except Exception, details: - raise Exception("Error while downloading Selenium Server. Details: "+details) + raise Exception("Error while downloading Selenium Server. Details: %s" + % details) def is_running_locally(host, port): @@ -48,19 +52,20 @@ def start_selenium_server(selenium_jar_location, port, file_path): process_args = None process_args = ["java", "-jar", selenium_jar_location, "-port", port] - selenium_exec = subprocess.Popen(process_args, - stdout=open("%s/log_seleniumOutput.txt" % (file_path),"w"), - stderr=open("%s/log_seleniumError.txt" % (file_path),"w")) + selenium_exec = subprocess.Popen( + process_args, + stdout=open("%s/log_seleniumOutput.txt" % (file_path), "w"), + stderr=open("%s/log_seleniumError.txt" % (file_path), "w")) time.sleep(2) if selenium_exec.poll() == 1: - raise StartSeleniumException("The selenium server did not start." +\ + raise StartSeleniumException("The selenium server did not start." "Do you already have one runing?") return selenium_exec def stop_selenium_server(selenium_server_process): """Kills the selenium server. We are expecting an error 143""" - + try: selenium_server_process.terminate() return selenium_server_process.poll() == 143 @@ -69,8 +74,10 @@ def stop_selenium_server(selenium_server_process): class StartSeleniumException(Exception): + def __init__(self, value): self.value = value + def __str__(self): return repr(self.value) @@ -78,7 +85,7 @@ def __str__(self): def execute_selenium(host, port, file_path): if is_running_locally(host, port): return - if not is_available_locally(): + if not is_available_locally(): download_selenium() try: return start_selenium_server(JAR_FILE, port, file_path) diff --git a/seleniumbase/core/testcase_manager.py b/seleniumbase/core/testcase_manager.py index 8998734073ab..633d4e676ae7 100755 --- a/seleniumbase/core/testcase_manager.py +++ b/seleniumbase/core/testcase_manager.py @@ -13,9 +13,9 @@ class TestcaseManager: def __init__(self, database_env): self.database_env = database_env - def insert_execution_data(self, execution_query_payload): - """Inserts an execution into the database, returns the execution guid""" + """ Inserts an execution into the database. + Returns the execution guid. """ query = """INSERT INTO execution (guid, executionStart, totalExecutionTime, username) @@ -26,7 +26,6 @@ def insert_execution_data(self, execution_query_payload): execution_query_payload.get_params()) return execution_query_payload.guid - def update_execution_data(self, execution_guid, execution_time): """updates an existing execution in the database""" @@ -38,7 +37,6 @@ def update_execution_data(self, execution_guid, execution_time): {"execution_guid": execution_guid, "execution_time": execution_time}) - def insert_testcase_data(self, testcase_run_payload): """inserts all data for the test case, returns the new row guid""" @@ -57,8 +55,8 @@ def insert_testcase_data(self, testcase_run_payload): %(retryCount)s, %(message)s, %(stackTrace)s) """ - DatabaseManager(self.database_env).execute_query_and_close(query, testcase_run_payload.get_params()) - + DatabaseManager(self.database_env).execute_query_and_close( + query, testcase_run_payload.get_params()) def update_testcase_data(self, testcase_payload): """updates an existing testcase run in the database""" @@ -70,8 +68,8 @@ def update_testcase_data(self, testcase_payload): stackTrace=%(stackTrace)s, message=%(message)s WHERE guid=%(guid)s """ - DatabaseManager(self.database_env).execute_query_and_close(query, testcase_payload.get_params()) - + DatabaseManager(self.database_env).execute_query_and_close( + query, testcase_payload.get_params()) def update_testcase_log_url(self, testcase_payload): """updates an existing testcase run's logging URL in the database""" @@ -79,7 +77,8 @@ def update_testcase_log_url(self, testcase_payload): query = """UPDATE testcaseRunData SET logURL=%(logURL)s WHERE guid=%(guid)s """ - DatabaseManager(self.database_env).execute_query_and_close(query, testcase_payload.get_params()) + DatabaseManager(self.database_env).execute_query_and_close( + query, testcase_payload.get_params()) class ExecutionQueryPayload: @@ -90,7 +89,6 @@ def __init__(self): self.username = "Default" self.guid = None - def get_params(self): """ Returns a params object for use with the pool """ return { @@ -117,7 +115,6 @@ def __init__(self): self.message = None self.logURL = None - def get_params(self): """ Returns a params object for use with the pool """ return { diff --git a/seleniumbase/core/web_driver_retry.py b/seleniumbase/core/web_driver_retry.py deleted file mode 100755 index d2927018a3e0..000000000000 --- a/seleniumbase/core/web_driver_retry.py +++ /dev/null @@ -1,28 +0,0 @@ -from selenium.common.exceptions import WebDriverException -import time - - -def web_driver_retry(in_func, timeout=30): - tic = time.time() - end_time = tic + timeout - timeout = 5 - while time.time() < end_time: - try: - return in_func() - except (WebDriverException, AssertionError), e: - if (time.time() - tic) < 5: - sleep_for = 0.25 - else: - sleep_for = 1 - time.sleep(sleep_for) - raise e - - -def asserts_visible(in_func): - def out_func(): - element = in_func() - if len(element) and element[0].is_displayed(): - return element[0] - raise WebDriverException - return out_func - From 8eca1a418d6d2aba10bfbed86b7b0a6d62e98700 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 2 Dec 2015 23:53:35 -0500 Subject: [PATCH 205/219] Continue the flake8 crusade --- docker/docker_setup.py | 27 ++++++++++++++----------- examples/my_first_test.py | 3 ++- examples/rate_limiting_test.py | 2 +- examples/test_fail.py | 1 + seleniumbase/config/settings.py | 36 +++++++++++++++++++++------------ 5 files changed, 42 insertions(+), 27 deletions(-) diff --git a/docker/docker_setup.py b/docker/docker_setup.py index e34486b54e81..06206c2b86a9 100755 --- a/docker/docker_setup.py +++ b/docker/docker_setup.py @@ -4,31 +4,34 @@ a MySQL DB during test runs. """ -from setuptools import setup, find_packages +from setuptools import setup, find_packages # noqa setup( - name = 'seleniumbase', - version = '1.1.7', - author = 'Michael Mintz', - author_email = '@mintzworld', - maintainer = 'Michael Mintz', - description = 'The SeleniumBase Test Framework. (Powered by Python, WebDriver, and more...)', - license = 'The MIT License', - packages = ['seleniumbase', + name='seleniumbase', + version='1.1.7', + author='Michael Mintz', + author_email='@mintzworld', + maintainer='Michael Mintz', + description='''The SeleniumBase Test Framework. + (Powered by Python, WebDriver, and more...)''', + license='The MIT License', + packages=['seleniumbase', 'seleniumbase.core', 'seleniumbase.plugins', 'seleniumbase.fixtures', 'seleniumbase.common', 'seleniumbase.config'], - entry_points = { + entry_points={ 'nose.plugins': [ 'base_plugin = seleniumbase.plugins.base_plugin:Base', - 'selenium_docker = seleniumbase.plugins.docker_selenium_plugin:SeleniumBrowser', + 'selenium_docker = ' + 'seleniumbase.plugins.docker_selenium_plugin:SeleniumBrowser', 'page_source = seleniumbase.plugins.page_source:PageSource', 'screen_shots = seleniumbase.plugins.screen_shots:ScreenShots', 'test_info = seleniumbase.plugins.basic_test_info:BasicTestInfo', 's3_logging = seleniumbase.plugins.s3_logging_plugin:S3Logging', - 'hipchat_reporting = seleniumbase.plugins.hipchat_reporting_plugin:HipchatReporting', + 'hipchat_reporting = seleniumbase.plugins' + '.hipchat_reporting_plugin:HipchatReporting', ] } ) diff --git a/examples/my_first_test.py b/examples/my_first_test.py index 9a34d0384eaa..13e21ad2404f 100755 --- a/examples/my_first_test.py +++ b/examples/my_first_test.py @@ -1,5 +1,6 @@ from seleniumbase import BaseCase + class MyTestClass(BaseCase): def test_basic(self): @@ -11,7 +12,7 @@ def test_basic(self): self.assertTrue("You can use them freely" in text) self.open("http://xkcd.com/1481/") self.click_link_text('Blag') - self.wait_for_text_visible("The blag of the webcomic", "#site-description") + self.wait_for_text_visible("blag of the webcomic", "#site-description") self.update_text_value("input#s", "Robots!\n") self.wait_for_text_visible("Hooray robots!", "#content") self.open("http://xkcd.com/1319/") diff --git a/examples/rate_limiting_test.py b/examples/rate_limiting_test.py index 30eb7637ba37..6917f854284c 100755 --- a/examples/rate_limiting_test.py +++ b/examples/rate_limiting_test.py @@ -9,6 +9,6 @@ def print_item(self, item): print item def test_rate_limited_printing(self): - print "\nRunning rate-limited print test:" + print "\nRunning rate-limited print test:" for item in xrange(10): self.print_item(item) diff --git a/examples/test_fail.py b/examples/test_fail.py index d680d3580cd1..eb0395a7c114 100755 --- a/examples/test_fail.py +++ b/examples/test_fail.py @@ -1,6 +1,7 @@ """ test_fail.py """ from seleniumbase import BaseCase + class MyTestClass(BaseCase): def test_find_army_of_robots_on_xkcd_desert_island(self): diff --git a/seleniumbase/config/settings.py b/seleniumbase/config/settings.py index 35f58384cf4e..fe4d98f2751e 100755 --- a/seleniumbase/config/settings.py +++ b/seleniumbase/config/settings.py @@ -3,25 +3,32 @@ """ -#####>>>>>----- REQUIRED SETTINGS -----<<<<<##### +# #####>>>>>----- REQUIRED SETTINGS -----<<<<<##### # Default times to wait for page elements to appear before performing actions SMALL_TIMEOUT = 5 LARGE_TIMEOUT = 10 EXTREME_TIMEOUT = 30 -# The option of adding wait_for_ready_state_complete() after various actions. -# By default, Selenium waits for the 'interactive' state, which might not be enough. -# Setting these values to True might make your tests more reliable, but it also might slightly slow them down. -WAIT_FOR_RSC_ON_PAGE_LOADS = False # When using self.open(url) or self.open_url(url), NOT self.driver.open(url) -WAIT_FOR_RSC_ON_CLICKS = False # When using self.click(selector), NOT element.click() +''' The option of adding wait_for_ready_state_complete() + after various actions. + By default, Selenium waits for the 'interactive' state, + which might not be enough. + Setting these values to True might make your tests more reliable, + but it also might slightly slow them down. ''' +# When using self.open(url) or self.open_url(url), NOT self.driver.open(url) +WAIT_FOR_RSC_ON_PAGE_LOADS = False +# When using self.click(selector), NOT element.click() +WAIT_FOR_RSC_ON_CLICKS = False -#####>>>>>----- RECOMMENDED SETTINGS -----<<<<<##### +# #####>>>>>----- RECOMMENDED SETTINGS -----<<<<<##### -# Amazon S3 Bucket Credentials (where screenshots and other log files get saved) +# Amazon S3 Bucket Credentials +# (where screenshots and other log files get saved) S3_LOG_BUCKET = "[ENTER LOG BUCKET FOLDER NAME HERE]" -S3_BUCKET_URL = "http://[ENTER SUBDOMAIN OF AMAZON BUCKET URL HERE].s3-[ENTER S3 REGION HERE].amazonaws.com/" +S3_BUCKET_URL = ("http://[ENTER SUBDOMAIN OF AMAZON BUCKET URL HERE]" + ".s3-[ENTER S3 REGION HERE].amazonaws.com/") S3_SELENIUM_ACCESS_KEY = "[ENTER YOUR S3 ACCESS KEY FOR SELENIUM HERE]" S3_SELENIUM_SECRET_KEY = "[ENTER YOUR S3 SECRET KEY FOR SELENIUM HERE]" @@ -32,14 +39,17 @@ DB_SCHEMA = "[TEST DB SCHEMA]" -#####>>>>>----- OPTIONAL SETTINGS -----<<<<<##### +# #####>>>>>----- OPTIONAL SETTINGS -----<<<<<##### -# Default Email Credentials (if tests send out emails, you can scan through and verify them by using IMAP) +# Default Email Credentials +# (If tests send out emails, you can scan and verify them by using IMAP) EMAIL_USERNAME = "[TEST ACCOUNT GMAIL USERNAME]@gmail.com" EMAIL_PASSWORD = "[TEST ACCOUNT GMAIL PASSWORD]" EMAIL_IMAP_STRING = "imap.gmail.com" EMAIL_IMAP_PORT = 993 -# HipChat Reporting Credentials (for HipChat notifications if your team uses HipChat) -# Other info such as room id and owner to mention get entered during nosetest options +# HipChat Reporting Credentials +# (for HipChat notifications if your team uses HipChat) +# Other info such as room id and owner to mention +# get entered during nosetest options HIPCHAT_AUTH_TOKEN = "[ENTER YOUR HIPCHAT AUTH TOKEN HERE]" From 7796be8d12ae810a23df40a859484c4c7d7ff9b1 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 3 Dec 2015 01:31:44 -0500 Subject: [PATCH 206/219] Make the failing test example fail faster --- examples/test_fail.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/test_fail.py b/examples/test_fail.py index eb0395a7c114..228009ae662a 100755 --- a/examples/test_fail.py +++ b/examples/test_fail.py @@ -1,4 +1,6 @@ -""" test_fail.py """ +""" This test was made to fail on purpose to demonstrate the + logging capabilities of the SeleniumBase Test Framework """ + from seleniumbase import BaseCase @@ -6,4 +8,4 @@ class MyTestClass(BaseCase): def test_find_army_of_robots_on_xkcd_desert_island(self): self.driver.get("http://xkcd.com/731/") - self.wait_for_element_visible("div#ARMY_OF_ROBOTS", timeout=3) + self.wait_for_element_visible("div#ARMY_OF_ROBOTS", timeout=0.5) From 9579324973ad22ca5dd90540b82bc9a350258020 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 3 Dec 2015 02:04:00 -0500 Subject: [PATCH 207/219] Renaming a file and a method --- seleniumbase/core/{pytest_helper.py => log_helper.py} | 2 +- seleniumbase/fixtures/base_case.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename seleniumbase/core/{pytest_helper.py => log_helper.py} (91%) diff --git a/seleniumbase/core/pytest_helper.py b/seleniumbase/core/log_helper.py similarity index 91% rename from seleniumbase/core/pytest_helper.py rename to seleniumbase/core/log_helper.py index bdaf93ebe02e..b100e3187579 100755 --- a/seleniumbase/core/pytest_helper.py +++ b/seleniumbase/core/log_helper.py @@ -2,7 +2,7 @@ import traceback -def log_test_error_data(log_file, driver, browser): +def log_test_failure_data(log_file, driver, browser): try: last_page = driver.current_url except Exception: diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 70c86f43e904..cf62da68b538 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -13,7 +13,7 @@ import unittest from seleniumbase.config import settings from seleniumbase.core import browser_launcher -from seleniumbase.core import pytest_helper +from seleniumbase.core import log_helper from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.common.by import By import page_actions @@ -83,7 +83,7 @@ def tearDown(self): basic_file_path = "%s/%s" % (test_logpath, basic_info_name) basic_info_file = codecs.open( basic_file_path, "w+", "utf-8") - pytest_helper.log_test_error_data( + log_helper.log_test_failure_data( basic_info_file, self.driver, self.browser) basic_info_file.close() From be674a561e85ad9c8d56a19412db0f8440bcf3f5 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 3 Dec 2015 04:07:27 -0500 Subject: [PATCH 208/219] Clean up the pytest logging system --- seleniumbase/core/log_helper.py | 69 +++++++++++++++++++++++++++--- seleniumbase/fixtures/base_case.py | 14 ++---- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/seleniumbase/core/log_helper.py b/seleniumbase/core/log_helper.py index b100e3187579..3bbb2ff9b41b 100755 --- a/seleniumbase/core/log_helper.py +++ b/seleniumbase/core/log_helper.py @@ -1,14 +1,19 @@ +import codecs import sys import traceback -def log_test_failure_data(log_file, driver, browser): - try: - last_page = driver.current_url - except Exception: - last_page = '[WARNING! Browser Not Open!]' - if len(last_page) < 5: - last_page = '[WARNING! Browser Not Open!]' +def log_screenshot(test_logpath, driver): + screenshot_name = "screenshot.jpg" + screenshot_path = "%s/%s" % (test_logpath, screenshot_name) + driver.get_screenshot_as_file(screenshot_path) + + +def log_test_failure_data(test_logpath, driver, browser): + basic_info_name = "basic_test_info.txt" + basic_file_path = "%s/%s" % (test_logpath, basic_info_name) + log_file = codecs.open(basic_file_path, "w+", "utf-8") + last_page = get_last_page(driver) data_to_save = [] data_to_save.append("Last_Page: %s" % last_page) data_to_save.append("Browser: %s " % browser) @@ -17,3 +22,53 @@ def log_test_failure_data(log_file, driver, browser): sys.exc_info()[1], sys.exc_info()[2]))) log_file.writelines("\r\n".join(data_to_save)) + log_file.close() + + +def log_page_source(test_logpath, driver): + html_file_name = "page_source.html" + try: + page_source = driver.page_source + except Exception: + # Since we can't get the page source from here, skip saving it + return + html_file_path = "%s/%s" % (test_logpath, html_file_name) + html_file = codecs.open(html_file_path, "w+", "utf-8") + rendered_source = get_html_source_with_base_href(driver, page_source) + html_file.write(rendered_source) + html_file.close() + + +def get_last_page(driver): + try: + last_page = driver.current_url + except Exception: + last_page = '[WARNING! Browser Not Open!]' + if len(last_page) < 5: + last_page = '[WARNING! Browser Not Open!]' + return last_page + + +def get_base_url(full_url): + protocol = full_url.split('://')[0] + simple_url = full_url.split('://')[1] + base_url = simple_url.split('/')[0] + full_base_url = "%s://%s" % (protocol, base_url) + return full_base_url + + +def get_base_href_html(full_url): + ''' The base href line tells the html what the base page really is. + This is important when trying to open the page outside it's home. ''' + base_url = get_base_url(full_url) + return '' % base_url + + +def get_html_source_with_base_href(driver, page_source): + ''' Combines the domain base href with the html source. + This is needed for the page html to render correctly. ''' + last_page = get_last_page(driver) + if '://' in last_page: + base_href_html = get_base_href_html(last_page) + return '%s\n%s' % (base_href_html, page_source) + return '' diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index cf62da68b538..9fbac1784930 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -4,7 +4,6 @@ by giving page elements enough time to load before taking action on them. """ -import codecs import json import logging import os @@ -75,17 +74,12 @@ def tearDown(self): if not os.path.exists(test_logpath): os.makedirs(test_logpath) # Handle screenshot logging - screenshot_name = "screenshot.jpg" - screenshot_path = "%s/%s" % (test_logpath, screenshot_name) - self.driver.get_screenshot_as_file(screenshot_path) + log_helper.log_screenshot(test_logpath, self.driver) # Handle basic test info logging - basic_info_name = "basic_test_info.txt" - basic_file_path = "%s/%s" % (test_logpath, basic_info_name) - basic_info_file = codecs.open( - basic_file_path, "w+", "utf-8") log_helper.log_test_failure_data( - basic_info_file, self.driver, self.browser) - basic_info_file.close() + test_logpath, self.driver, self.browser) + # Handle page source logging + log_helper.log_page_source(test_logpath, self.driver) # Finally close the browser self.driver.quit() From 7e99bcf1b3143d887501a0e8ef02f992acdeb65c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 3 Dec 2015 04:14:36 -0500 Subject: [PATCH 209/219] Maybe I don't need all that code right now --- seleniumbase/plugins/screen_shots.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/seleniumbase/plugins/screen_shots.py b/seleniumbase/plugins/screen_shots.py index 9a4f11d2823b..beb7be10d23f 100755 --- a/seleniumbase/plugins/screen_shots.py +++ b/seleniumbase/plugins/screen_shots.py @@ -3,7 +3,6 @@ """ import os -import time from nose.plugins import Plugin @@ -34,15 +33,16 @@ def add_screenshot(self, test, err, capt=None, tbinfo=None): os.makedirs(test_logpath) screenshot_file = "%s/%s" % (test_logpath, self.logfile_name) test.driver.get_screenshot_as_file(screenshot_file) - try: + '''try: # Let humans see any errors on screen before closing the window test.driver.maximize_window() + import time time.sleep(0.2) # Make sure the screen is ready except Exception: pass # Second screenshot at fullscreen might not be necessary # import base64 - '''screen_b64 = test.driver.get_screenshot_as_base64() + screen_b64 = test.driver.get_screenshot_as_base64() screen = base64.decodestring(screen_b64) screenshot_file_2 = "%s/%s" % (test_logpath, self.logfile_name_2) f1 = open(screenshot_file_2, 'w+') From cde66adeca3b599a3b3933221134bb3caec44a48 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 3 Dec 2015 19:08:06 -0500 Subject: [PATCH 210/219] Improve the logging system --- conftest.py | 12 +++++--- seleniumbase/config/settings.py | 29 +++++++++++++------ seleniumbase/core/log_helper.py | 7 +++-- seleniumbase/plugins/base_plugin.py | 38 ++++++++++++++----------- seleniumbase/plugins/basic_test_info.py | 3 +- seleniumbase/plugins/page_source.py | 22 ++++++++++++-- seleniumbase/plugins/screen_shots.py | 3 +- setup.cfg | 6 ++++ 8 files changed, 83 insertions(+), 37 deletions(-) create mode 100755 setup.cfg diff --git a/conftest.py b/conftest.py index 4699cf101664..8d0a0a39727c 100755 --- a/conftest.py +++ b/conftest.py @@ -4,6 +4,7 @@ import shutil import pytest import time +from seleniumbase.config import settings from seleniumbase.fixtures import constants @@ -71,11 +72,14 @@ def pytest_runtest_setup(): if not os.path.exists(log_path): os.makedirs(log_path) else: - if not os.path.exists("%s/../archived_logs/" % log_path): - os.makedirs("%s/../archived_logs/" % log_path) - shutil.move(log_path, "%s/../archived_logs/logs_%s" % ( - log_path, int(time.time()))) + archived_folder = "%s/../archived_logs/" % log_path + if not os.path.exists(archived_folder): + os.makedirs(archived_folder) + archived_logs = "%slogs_%s" % (archived_folder, int(time.time())) + shutil.move(log_path, archived_logs) os.makedirs(log_path) + if not settings.ARCHIVE_EXISTING_LOGS: + shutil.rmtree(archived_logs) def pytest_runtest_teardown(): diff --git a/seleniumbase/config/settings.py b/seleniumbase/config/settings.py index fe4d98f2751e..254bd4a8b8b7 100755 --- a/seleniumbase/config/settings.py +++ b/seleniumbase/config/settings.py @@ -3,26 +3,36 @@ """ -# #####>>>>>----- REQUIRED SETTINGS -----<<<<<##### +# #####>>>>>----- REQUIRED/IMPORTANT SETTINGS -----<<<<<##### # Default times to wait for page elements to appear before performing actions SMALL_TIMEOUT = 5 LARGE_TIMEOUT = 10 EXTREME_TIMEOUT = 30 -''' The option of adding wait_for_ready_state_complete() - after various actions. - By default, Selenium waits for the 'interactive' state, - which might not be enough. - Setting these values to True might make your tests more reliable, - but it also might slightly slow them down. ''' -# When using self.open(url) or self.open_url(url), NOT self.driver.open(url) +# If True, existing logs from past test runs will be saved and take up space. +# If False, only the logs from the most recent test run will be saved locally. +# This has no effect on Jenkins/S3/MySQL, which may still be saving test logs. +ARCHIVE_EXISTING_LOGS = False + +# Default names for files saved during test failures when logging is turned on +SCREENSHOT_NAME = "screenshot.jpg" +BASIC_INFO_NAME = "basic_test_info.txt" +PAGE_SOURCE_NAME = "page_source.html" + +''' This adds wait_for_ready_state_complete() after various browser actions. + By default, Selenium waits for the 'interactive' state before continuing. + Setting this to True may improve reliability at the cost of speed. + WARNING: Some websites are in a perpetual "interactive" state due to + dynamic content that never fully finishes loading (Use "False" there). ''' +# Called after self.open(url) or self.open_url(url), NOT self.driver.open(url) WAIT_FOR_RSC_ON_PAGE_LOADS = False -# When using self.click(selector), NOT element.click() +# Called after self.click(selector), NOT element.click() WAIT_FOR_RSC_ON_CLICKS = False # #####>>>>>----- RECOMMENDED SETTINGS -----<<<<<##### +# ##### (For test logging and database reporting) # Amazon S3 Bucket Credentials # (where screenshots and other log files get saved) @@ -40,6 +50,7 @@ # #####>>>>>----- OPTIONAL SETTINGS -----<<<<<##### +# ##### (For reading emails, notifying people via chat apps, etc.) # Default Email Credentials # (If tests send out emails, you can scan and verify them by using IMAP) diff --git a/seleniumbase/core/log_helper.py b/seleniumbase/core/log_helper.py index 3bbb2ff9b41b..b3c8a5376905 100755 --- a/seleniumbase/core/log_helper.py +++ b/seleniumbase/core/log_helper.py @@ -1,16 +1,17 @@ import codecs import sys import traceback +from seleniumbase.config import settings def log_screenshot(test_logpath, driver): - screenshot_name = "screenshot.jpg" + screenshot_name = settings.SCREENSHOT_NAME screenshot_path = "%s/%s" % (test_logpath, screenshot_name) driver.get_screenshot_as_file(screenshot_path) def log_test_failure_data(test_logpath, driver, browser): - basic_info_name = "basic_test_info.txt" + basic_info_name = settings.BASIC_INFO_NAME basic_file_path = "%s/%s" % (test_logpath, basic_info_name) log_file = codecs.open(basic_file_path, "w+", "utf-8") last_page = get_last_page(driver) @@ -26,7 +27,7 @@ def log_test_failure_data(test_logpath, driver, browser): def log_page_source(test_logpath, driver): - html_file_name = "page_source.html" + html_file_name = settings.PAGE_SOURCE_NAME try: page_source = driver.page_source except Exception: diff --git a/seleniumbase/plugins/base_plugin.py b/seleniumbase/plugins/base_plugin.py index d531655d9f9b..7ebcaa77207f 100755 --- a/seleniumbase/plugins/base_plugin.py +++ b/seleniumbase/plugins/base_plugin.py @@ -10,6 +10,7 @@ import time from nose.plugins import Plugin from nose.exc import SkipTest +from seleniumbase.config import settings from seleniumbase.fixtures import constants, errors @@ -19,7 +20,7 @@ class Base(Plugin): self.options.env -- the environment for the tests to use (--env=ENV) self.options.data -- any extra data to pass to the tests (--data=DATA) self.options.log_path -- the directory in which the log files - are saved (--log_path=LOG_PATH) + are saved (--log_path=LOG_PATH) """ name = 'testing_base' # Usage: --with-testing_base @@ -27,12 +28,13 @@ def options(self, parser, env): super(Base, self).options(parser, env=env) parser.add_option('--env', action='store', dest='environment', - choices=(constants.Environment.QA, - constants.Environment.STAGING, - constants.Environment.PRODUCTION, - constants.Environment.MASTER, - constants.Environment.LOCAL, - constants.Environment.TEST), + choices=( + constants.Environment.QA, + constants.Environment.STAGING, + constants.Environment.PRODUCTION, + constants.Environment.MASTER, + constants.Environment.LOCAL, + constants.Environment.TEST), default=constants.Environment.TEST, help="The environment to run the tests in.") parser.add_option('--data', dest='data', @@ -47,16 +49,20 @@ def configure(self, options, conf): if not self.enabled: return self.options = options - if options.log_path.endswith("/"): - options.log_path = options.log_path[:-1] - if not os.path.exists(options.log_path): - os.makedirs(options.log_path) + log_path = options.log_path + if log_path.endswith("/"): + log_path = log_path[:-1] + if not os.path.exists(log_path): + os.makedirs(log_path) else: - if not os.path.exists("%s/../archived_logs/" % options.log_path): - os.makedirs("%s/../archived_logs/" % options.log_path) - shutil.move(options.log_path, "%s/../archived_logs/logs_%s" % ( - options.log_path, int(time.time()))) - os.makedirs(options.log_path) + archived_folder = "%s/../archived_logs/" % log_path + if not os.path.exists(archived_folder): + os.makedirs(archived_folder) + archived_logs = "%slogs_%s" % (archived_folder, int(time.time())) + shutil.move(log_path, archived_logs) + os.makedirs(log_path) + if not settings.ARCHIVE_EXISTING_LOGS: + shutil.rmtree(archived_logs) def beforeTest(self, test): test_logpath = self.options.log_path + "/" + test.id() diff --git a/seleniumbase/plugins/basic_test_info.py b/seleniumbase/plugins/basic_test_info.py index d8dc523e47a7..569a4f25b0d3 100755 --- a/seleniumbase/plugins/basic_test_info.py +++ b/seleniumbase/plugins/basic_test_info.py @@ -13,6 +13,7 @@ import codecs import traceback from nose.plugins import Plugin +from seleniumbase.config import settings class BasicTestInfo(Plugin): @@ -23,7 +24,7 @@ class BasicTestInfo(Plugin): """ name = "basic_test_info" # Usage: --with-basic_test_info - logfile_name = "basic_test_info.txt" + logfile_name = settings.BASIC_INFO_NAME def options(self, parser, env): super(BasicTestInfo, self).options(parser, env=env) diff --git a/seleniumbase/plugins/page_source.py b/seleniumbase/plugins/page_source.py index fcc151e6a8c2..292eb59a5931 100755 --- a/seleniumbase/plugins/page_source.py +++ b/seleniumbase/plugins/page_source.py @@ -5,6 +5,8 @@ import os import codecs from nose.plugins import Plugin +from seleniumbase.config import settings +from seleniumbase.core import log_helper class PageSource(Plugin): @@ -14,7 +16,7 @@ class PageSource(Plugin): logs file specified, along with default test information. """ name = "page_source" # Usage: --with-page_source - logfile_name = "page_source.html" + logfile_name = settings.PAGE_SOURCE_NAME def options(self, parser, env): super(PageSource, self).options(parser, env=env) @@ -26,19 +28,33 @@ def configure(self, options, conf): self.options = options def addError(self, test, err, capt=None): + try: + page_source = test.driver.page_source + except Exception: + # Since we can't get the page source from here, skip saving it + return test_logpath = self.options.log_path + "/" + test.id() if not os.path.exists(test_logpath): os.makedirs(test_logpath) html_file_name = "%s/%s" % (test_logpath, self.logfile_name) html_file = codecs.open(html_file_name, "w+", "utf-8") - html_file.write(test.driver.page_source) + rendered_source = log_helper.get_html_source_with_base_href( + test.driver, page_source) + html_file.write(rendered_source) html_file.close() def addFailure(self, test, err, capt=None, tbinfo=None): + try: + page_source = test.driver.page_source + except Exception: + # Since we can't get the page source from here, skip saving it + return test_logpath = self.options.log_path + "/" + test.id() if not os.path.exists(test_logpath): os.makedirs(test_logpath) html_file_name = "%s/%s" % (test_logpath, self.logfile_name) html_file = codecs.open(html_file_name, "w+", "utf-8") - html_file.write(test.driver.page_source) + rendered_source = log_helper.get_html_source_with_base_href( + test.driver, page_source) + html_file.write(rendered_source) html_file.close() diff --git a/seleniumbase/plugins/screen_shots.py b/seleniumbase/plugins/screen_shots.py index beb7be10d23f..7074305ede04 100755 --- a/seleniumbase/plugins/screen_shots.py +++ b/seleniumbase/plugins/screen_shots.py @@ -4,6 +4,7 @@ import os from nose.plugins import Plugin +from seleniumbase.config import settings class ScreenShots(Plugin): @@ -14,7 +15,7 @@ class ScreenShots(Plugin): """ name = "screen_shots" - logfile_name = "screenshot.jpg" + logfile_name = settings.SCREENSHOT_NAME # Browser windows aren't always maximized. This may display more details. logfile_name_2 = "full_screenshot.jpg" diff --git a/setup.cfg b/setup.cfg new file mode 100755 index 000000000000..061dbdd10827 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,6 @@ +[nosetests] + +; This is the config file for default values used during nosetest runs + +nocapture=1 ; Displays print statements from output. Undo this by using: --nologcapture +logging-level=INFO ; INFO keeps the logs much cleaner than using DEBUG From e293f4bb03180a17e8702a5cbdd261af0211b88e Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 3 Dec 2015 20:54:34 -0500 Subject: [PATCH 211/219] Update the ReadMe --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f3d0f78746e..6cfb1a264e03 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SeleniumBase Test Framework -## **Web Automation For Everything** +## **Intelligent Web Automation** #### Features include: * ALL the power of Python, WebDriver, and Nosetests (pytest also supported) From 7bb0342ae9b602dadaa923ab63bd3ebee8abe937 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 3 Dec 2015 21:03:39 -0500 Subject: [PATCH 212/219] Better marketing slogan --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6cfb1a264e03..a1f0f8bd38b6 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SeleniumBase Test Framework -## **Intelligent Web Automation** +## **Intelligent, Reliable Web Automation** #### Features include: * ALL the power of Python, WebDriver, and Nosetests (pytest also supported) From 588b59be3de7a3411923c8adb05000d448807652 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 3 Dec 2015 21:56:44 -0500 Subject: [PATCH 213/219] Update settings descriptions and example values --- seleniumbase/config/settings.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/seleniumbase/config/settings.py b/seleniumbase/config/settings.py index 254bd4a8b8b7..0af7b9dd401b 100755 --- a/seleniumbase/config/settings.py +++ b/seleniumbase/config/settings.py @@ -35,18 +35,19 @@ # ##### (For test logging and database reporting) # Amazon S3 Bucket Credentials -# (where screenshots and other log files get saved) +# (For saving screenshots and other log files from tests) S3_LOG_BUCKET = "[ENTER LOG BUCKET FOLDER NAME HERE]" S3_BUCKET_URL = ("http://[ENTER SUBDOMAIN OF AMAZON BUCKET URL HERE]" ".s3-[ENTER S3 REGION HERE].amazonaws.com/") S3_SELENIUM_ACCESS_KEY = "[ENTER YOUR S3 ACCESS KEY FOR SELENIUM HERE]" S3_SELENIUM_SECRET_KEY = "[ENTER YOUR S3 SECRET KEY FOR SELENIUM HERE]" -# MySQL DB Credentials (where data from tests gets saved) -DB_HOST = "[TEST DB HOST]" -DB_USERNAME = "[TEST DB USERNAME]" -DB_PASSWORD = "[TEST DB PASSWORD]" -DB_SCHEMA = "[TEST DB SCHEMA]" +# MySQL DB Credentials +# (For saving data from tests) +DB_HOST = "[TEST DB HOST]" # Ex: "127.0.0.1" +DB_USERNAME = "[TEST DB USERNAME]" # Ex: "root" +DB_PASSWORD = "[TEST DB PASSWORD]" # Ex: "test" +DB_SCHEMA = "[TEST DB SCHEMA]" # Ex: "test" # #####>>>>>----- OPTIONAL SETTINGS -----<<<<<##### @@ -60,7 +61,6 @@ EMAIL_IMAP_PORT = 993 # HipChat Reporting Credentials -# (for HipChat notifications if your team uses HipChat) -# Other info such as room id and owner to mention -# get entered during nosetest options +# (For HipChat notifications if your team uses HipChat) +# (room_id and owner_to_mention get entered during nosetest options) HIPCHAT_AUTH_TOKEN = "[ENTER YOUR HIPCHAT AUTH TOKEN HERE]" From 6aad3052ed5ec5dd398b760be977200e1412488e Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 4 Dec 2015 02:10:00 -0500 Subject: [PATCH 214/219] Update the spacing --- seleniumbase/plugins/base_plugin.py | 37 ++++++++++++++++------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/seleniumbase/plugins/base_plugin.py b/seleniumbase/plugins/base_plugin.py index 7ebcaa77207f..823fbc605379 100755 --- a/seleniumbase/plugins/base_plugin.py +++ b/seleniumbase/plugins/base_plugin.py @@ -26,23 +26,26 @@ class Base(Plugin): def options(self, parser, env): super(Base, self).options(parser, env=env) - parser.add_option('--env', action='store', - dest='environment', - choices=( - constants.Environment.QA, - constants.Environment.STAGING, - constants.Environment.PRODUCTION, - constants.Environment.MASTER, - constants.Environment.LOCAL, - constants.Environment.TEST), - default=constants.Environment.TEST, - help="The environment to run the tests in.") - parser.add_option('--data', dest='data', - default=None, - help='Extra data to pass from the command line.') - parser.add_option('--log_path', dest='log_path', - default='logs/', - help='Where the log files are saved.') + parser.add_option( + '--env', action='store', + dest='environment', + choices=( + constants.Environment.QA, + constants.Environment.STAGING, + constants.Environment.PRODUCTION, + constants.Environment.MASTER, + constants.Environment.LOCAL, + constants.Environment.TEST), + default=constants.Environment.TEST, + help="The environment to run the tests in.") + parser.add_option( + '--data', dest='data', + default=None, + help='Extra data to pass from the command line.') + parser.add_option( + '--log_path', dest='log_path', + default='logs/', + help='Where the log files are saved.') def configure(self, options, conf): super(Base, self).configure(options, conf) From 913a5fc69ce96c0e356e1492cb75aa786ca988ee Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 4 Dec 2015 02:10:58 -0500 Subject: [PATCH 215/219] Add to the settings description --- seleniumbase/config/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/seleniumbase/config/settings.py b/seleniumbase/config/settings.py index 0af7b9dd401b..6ae63fc1784a 100755 --- a/seleniumbase/config/settings.py +++ b/seleniumbase/config/settings.py @@ -15,7 +15,8 @@ # This has no effect on Jenkins/S3/MySQL, which may still be saving test logs. ARCHIVE_EXISTING_LOGS = False -# Default names for files saved during test failures when logging is turned on +# Default names for files saved during test failures when logging is turned on. +# (These files will get saved to the "logs/" folder) SCREENSHOT_NAME = "screenshot.jpg" BASIC_INFO_NAME = "basic_test_info.txt" PAGE_SOURCE_NAME = "page_source.html" From 6cd7e7a6d82fcb217da26c948059b149f1e4a16f Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 4 Dec 2015 02:14:31 -0500 Subject: [PATCH 216/219] Update the ReadMe --- README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a1f0f8bd38b6..9600bee46ebf 100755 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ -# SeleniumBase Test Framework +# SeleniumBase Automation Framework -## **Intelligent, Reliable Web Automation** +## **An open-source solution for reliable testing & business automation** + +### Trusted by some of Boston's most promising companies, including HubSpot, Jana, and Veracode. #### Features include: -* ALL the power of Python, WebDriver, and Nosetests (pytest also supported) -* Easy integration with [Jenkins](http://jenkins-ci.org/), [Selenium Grid](http://docs.seleniumhq.org/projects/grid/), [Docker](https://www.docker.com/), and [AWS](http://aws.amazon.com/) -* A flexible logging system for storing test data, results, and screenshots -* Libraries for code-simplification, time-saving, and reusability -* Fully customizable (see [settings.py](https://github.com/mdmintz/SeleniumBase/blob/master/seleniumbase/config/settings.py) in the seleniumbase/config folder) +* Python libraries for quickly making reliable WebDriver scripts that run fast. +* Built-in Nosetest & Pytest plugins for logging test data, results, and screenshots. +* Simple integration with [Jenkins](http://jenkins-ci.org/), [Selenium Grid](http://docs.seleniumhq.org/projects/grid/), [Docker](https://www.docker.com/), and [AWS](http://aws.amazon.com/). +* Customizable with command-line options and a global config file: [settings.py](https://github.com/mdmintz/SeleniumBase/blob/master/seleniumbase/config/settings.py). -*Looking for a real-world business example? Check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot) to learn how all the pieces of web automation come together. For more, read [The Classic "QA Team" is Obsolete](http://product.hubspot.com/blog/the-classic-qa-team-is-obsolete).* +*Learn how the pros handle test automation: Check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot), and read [The Classic "QA Team" is Obsolete](http://product.hubspot.com/blog/the-classic-qa-team-is-obsolete) for more.* ## Part I: MAC SETUP INSTRUCTIONS @@ -230,14 +231,14 @@ Here are some other useful nosetest arguments that you may want to append to you Due to high demand, pytest support has been added. You can run the above sample script in pytest like this: ```bash -py.test my_first_test.py --with-selenium --browser=chrome -s +py.test my_first_test.py --with-selenium --with-testing_base --browser=chrome -s -py.test my_first_test.py --with-selenium --browser=phantomjs -s +py.test my_first_test.py --with-selenium --with-testing_base --browser=phantomjs -s -py.test my_first_test.py --with-selenium --browser=firefox -s +py.test my_first_test.py --with-selenium --with-testing_base --browser=firefox -s ``` -(NOTE: If you want to run tests using pytest, you won't have access to the nosetest plugins that are specially defined in SeleniumBase. The nosetest plugins all start with "--with-" and are appended to the test selection in the run commands.) +(NOTE: I'm currently adding more pytest plugins to catch up with nosetests. The latest one added is "--with-testing_base", which gives you full logging on test failures for screenshots, page source, and basic test info. Coming soon: The DB and S3 plugins, which are already available with nosetests.) #### **Step 6:** Complete the setup From 831acb25b0ba85b21758cbadc6a3f90d8eeb551a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 4 Dec 2015 02:43:54 -0500 Subject: [PATCH 217/219] ReadMe perfection-seeking --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 9600bee46ebf..6d1407b203aa 100755 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # SeleniumBase Automation Framework -## **An open-source solution for reliable testing & business automation** - -### Trusted by some of Boston's most promising companies, including HubSpot, Jana, and Veracode. +### An open-source solution for reliable testing and business automation. Trusted by Boston's most promising companies, including HubSpot, Jana, and Veracode. #### Features include: * Python libraries for quickly making reliable WebDriver scripts that run fast. From f13c64652e2b3a9ee8946690a338da36a988fe07 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 4 Dec 2015 02:50:10 -0500 Subject: [PATCH 218/219] ReadMe spacing --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d1407b203aa..2c336d1346aa 100755 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # SeleniumBase Automation Framework -### An open-source solution for reliable testing and business automation. Trusted by Boston's most promising companies, including HubSpot, Jana, and Veracode. +### An open-source solution for reliable testing and business automation. + +Trusted by Boston's most promising companies, including HubSpot, Jana, and Veracode. #### Features include: * Python libraries for quickly making reliable WebDriver scripts that run fast. From 58aa4d899e1b1c947f7eb86ecbf1912645b55520 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 4 Dec 2015 11:10:54 -0500 Subject: [PATCH 219/219] The MySQL part is important to mention. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c336d1346aa..73a18e732f30 100755 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Trusted by Boston's most promising companies, including HubSpot, Jana, and Verac #### Features include: * Python libraries for quickly making reliable WebDriver scripts that run fast. * Built-in Nosetest & Pytest plugins for logging test data, results, and screenshots. -* Simple integration with [Jenkins](http://jenkins-ci.org/), [Selenium Grid](http://docs.seleniumhq.org/projects/grid/), [Docker](https://www.docker.com/), and [AWS](http://aws.amazon.com/). +* Simple integration with [Jenkins](http://jenkins-ci.org/), [Selenium Grid](http://docs.seleniumhq.org/projects/grid/), [MySQL](http://www.mysql.com/), [Docker](https://www.docker.com/), and [AWS](http://aws.amazon.com/). * Customizable with command-line options and a global config file: [settings.py](https://github.com/mdmintz/SeleniumBase/blob/master/seleniumbase/config/settings.py). *Learn how the pros handle test automation: Check out HubSpot's blog article on [Automated Testing with Selenium](http://dev.hubspot.com/blog/bid/88880/Automated-Integration-Testing-with-Selenium-at-HubSpot), and read [The Classic "QA Team" is Obsolete](http://product.hubspot.com/blog/the-classic-qa-team-is-obsolete) for more.*