diff --git a/BUILD.bazel b/BUILD.bazel
new file mode 100644
index 00000000..86d2cef6
--- /dev/null
+++ b/BUILD.bazel
@@ -0,0 +1,37 @@
+genrule(
+    name = "get-attribute",
+    srcs = ["//javascript/webdriver/atoms:get-attribute.js"],
+    outs = ["selenium/webdriver/remote/getAttribute.js"],
+    cmd = "cp $< $@",
+)
+
+genrule(
+    name = "is-displayed",
+    srcs = ["//javascript/atoms/fragments:is-displayed.js"],
+    outs = ["selenium/webdriver/remote/isDisplayed.js"],
+    cmd = "cp $< $@",
+)
+
+py_library(
+    name = "main",
+    srcs = glob(["selenium/**/*.py"]),
+    data = [
+        ":get-attribute",
+        ":is-displayed",
+    ],
+    imports = ["."],
+    visibility = ["//visibility:public"],
+)
+
+py_test(
+    name = "unit",
+    size = "small",
+    srcs = glob([
+        "test/unit/**/*.py",
+    ]) + [ "test/run_pytest.py" ],
+    main = "test/run_pytest.py",
+    deps = [
+        ":main",
+    ],
+    legacy_create_init = False,
+)
\ No newline at end of file
diff --git a/CHANGES b/CHANGES
new file mode 100644
index 00000000..1ea15ac8
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,701 @@
+Selenium 4.0 Alpha 1
+* Update driver initialisation to use service and option objects
+* turn on keep-alive by default for remote connections (#7072)
+* Fix ConnectionResetError
+* Add new Cast commands
+* Suggest download Microsoft Webdriver over HTTPS
+* Clear PoolManager in ‘remote_connection’ to ensure sockets are closed
+* remove --disable-gpu option for headless Chrome
+* Add support for the New Window command (#6873)
+* Update docstrings in Options classes to allow documentation to highlight Return values
+* Fix typos in select.py (#6925)
+* Remove native events handling code
+* Deleting unused imports, fixing flake8 issues
+* Remove unused port selection in IE Driver
+* Enabling tests xpassed in Chrome
+* Pretty-printing code samples
+* remove all deprecated methods and args from Python bindings
+* Fix DeprecationWarning: invalid escape sequence
+* Don't override browser options with desired capabilities by default in WebKitGTK (#6814)
+* Add WebKitGTK to API docs (#6815)
+* Subclass options classes from a common base class (#6522)
+* Update Sphinx (#6728)
+* WebDriverWait: update documentation for until and until_not (#6711)
+* Fix typo in description of WebDriver class (#6724)
+* add strictFileInteractability to acceptable W3C capabilities
+* Deprecate Blackberry Driver support
+* Fixing/tidying docstring.
+
+
+Selenium 3.141.0
+* Bump version to a better approximation of Π
+* Improved Test build targets
+* fix os path in test for Windows
+* use 'NUL' for /dev/null on Windows
+* Update ctor docstrings to explain that a directory passed in is cloned. Fixes #6542
+* Allow passing of service_args to Safari. Fixes #6459
+* Remove element equals url
+* Improved WebExtension support
+
+Selenium 3.14.1
+* Fix ability to set timeout for urllib3 (#6286)
+* get_cookie uses w3c endpoint when compliant
+* Remove body from GET requests (#6250)
+* Fix actions pause for fraction of a second (#6300)
+* Fixed input pausing for some actions methods
+* Capabilities can be set on Options classes
+* WebElement rect method is now forward compatible for OSS endpoints (#6355)
+* Deprecation warnings now have a stacklevel of 2
+* keep_alive can now be set on Webdriver init (#6316)
+* isDisplayed atom is now used for all w3c compliant browser, fixing issue with Safari 12
+
+Selenium 3.14.0
+* Fix doc of URL-related ExpectedCondition (#6236)
+* Added ExpectedCondition invisibility_of_element
+* Swap out httplib for urllib3
+* Be consistent with webdriver init kwarg service_log_path (#5725)
+
+Selenium 3.13.0
+
+* Add executing Chrome devtools command (#5989)
+* fix incorrect w3c action encoding in python client (#6014)
+* Implement context manager for WebDriver
+* Stop sending "windowHandle" param in maximize_window command for w3c
+
+Selenium 3.12.0
+
+* Add desired_capabilities keyword to IE and Firefox drivers for driver consitency
+* Fix bug with creating Safari webdriver instance (#5578)
+* Add support for Safari extension command
+* Deprecate Options `set_headless` methods in favor of property setter
+* Only set --disable-gpu for Chrome headless when on Windows
+* Add selenium User-Agent header (#5696)
+* Remote webdriver can now be started when passing options
+* All Options.to_capabilities now start with default DesiredCapabilities
+* Improve the error message that is raised when safaridriver cannot be found (#5739)
+* IeOptions class is now imported to selenium.webdriver
+* Remove the beta `authenticate` methods from `Alert`
+
+Selenium 3.11.0
+
+No changes just keeping python version in step with the rest of the project.
+
+Selenium 3.10.0
+
+* make tests to check clicking on disabled element work for w3c compliant drivers (#5561)
+* add docstring for InvalidElementStateException. Fixes #5520
+* Deleting unused imports
+* Making python specification in IDEA project more generic
+* It should be possible to use a custom safaridriver executable to run Selenium's test suite.
+
+Selenium 3.9.0
+
+* Add docstrings to WebElement find methods (#5384)
+* Additional data in unexpected alert error is now handled for w3c drivers (#5416)
+* Allow service_args to be passed into Firefox WebDriver (#5421)
+* Fix bug introduced with response logging in 3.8.1 (#5362)
+
+Selenium 3.8.1
+
+* Fix bug when creating an Opera driver (#5266)
+* Stop sending sessionId in w3c payload. (#4620)
+* Fix issue with w3c actions releasing on element (#5180)
+* A more descriptive log message is displayed if the port cannot be connected (#2913)
+* Initialize Alert object by calling alert.text (#1863)
+* PhantomJS is now deprecated, please use either Chrome or Firefox in headless mode
+* Legacy Firefox driver: ensuring copy of profile dir, its 'extensions' subdir and 'user.js' file are writable. (#1466)
+
+Selenium 3.8.0
+
+* Firefox options can now be imported from selenium.webdriver as FirefoxOptions (#5120)
+* Headless mode can now be set in Chrome Options using `set_headless`
+* Headless mode can now be set in Firefox Options using `set_headless`
+* Add the WebKitGTK WebDriver and options class (#4635)
+* Browser options can now be passed to remote WebDriver via the `options` parameter
+* Browser option parameters are now standardized across drivers as `options`. `firefox_options`,
+  `chrome_options`, and `ie_options` are now deprecated
+* Added missing W3C Error Codes (#4556)
+* Support has been removed for Python versions 2.6 and 3.3
+
+Selenium 3.7.0
+
+* need to pass applicable environment variables to tox
+* Fix active_element for w3c drivers (#3979)
+* add support for minimize command
+* add support for fullscreen command
+* window rect commands should fail on firefox and remote (legacy)
+* Fix python backward compatibility for window commands (#4937)
+* Update docstrings to specify the type of path needed to install firefox addons. (#4881)
+* Update python chromeOptions key for capabilities (#4622)
+* Fix python pause action implementation (#4795)
+
+Selenium 3.6.0
+
+* Fix package name in python webelement module (#4670)
+* Fix python driver examples (#3872)
+* No need to multiply pause by 1000
+* Add pause to action chains
+* only check for proxyType once
+* lowercase proxy type for w3c payload in python #4574
+* guarding against null return value from find_elements in python #4555
+* remove unnecessary pytest marking, address flake8 issues
+* allow IE WebDriver to accept IE Options
+* add IE Options class
+* convert OSS capabilities to W3C equivalent for W3C payload
+* Add Safari to API docs
+
+Selenium 3.5.0
+
+* Numerous test fixes
+*Iterate over capabilities in a way to support py2.7 and py3
+* Fix W3C switching to window by name.
+* Support GeckoDriver addon install/uninstall commands  #4215.
+* Move firefox_profile into moz:firefoxOptions.
+* Filter non-W3C capability names out of alwaysMatch.
+* Honor cmd line args passed to Service ctor (#4167)
+* Add expected conditions based on URL to Python Expected Conditions #4160
+* Add network emulation to Chrome Python bindings (#4011)
+* add warning when saving incorrectly named screenshot (#4141)
+
+Selenium 3.4.3
+* Fix EventFiringWebdriver and WebElement to raise AttributeError on missing attributes. (#4107)
+* unwrap WebElements inside dicts
+
+Selenium 3.4.2
+
+* translate move_by_offset command to w3c
+* Update capabilities properly instead of assuming dict structure. Fixes #3927
+* Add missing file for Chrome options to API docs.
+* Add Chrome options module to API docs.
+
+Selenium 3.4.1
+* Add back the ability to set profile when using Firefox 45ESR. Fixes #3897
+
+Selenium 3.4.0
+* Correct usage of newSession around `firstMatch` and `alwaysMatch`
+* Remove superfluous capabilities that are not needed
+* Add expected condition that waits for all found elements to be visible (#3532)
+* Allow methods wrapped by EventFiringWebDriver and EventFiringWebElement (#806)
+* Dropping `javascriptEnabled` capability for real browsers
+* Use W3C check from parent object instead of assuming from capabilities
+* Bump example source distribution to match latest release.
+* Replace TypeError with KeyError in remote webdriver error handler code (#3826)
+* When testing Marionette use default capabilities in testing
+* Conform to the api of urllib2 for adding header for a request (#3803)
+* Add `text` key to alert#sendKeys parameters for W3C Endpoint
+* Location once scrolled into view should use W3C executeScript endpoint not JSONWP
+* Fixed the usage information in documentation of "save_screenshot". (#3804)
+* Add Element Not Interactable exception
+* Clean up imports in error handler
+* flake8 cleanup
+
+Selenium 3.3.3
+
+* make w3c execute_script commands unique
+
+Selenium 3.3.2
+
+* Update window commands to use W3C End points
+* Update Alert when in W3C mode to use W3C Endpoints
+* Update to new W3C Execute Script end points
+* Add setting/getting proxy details to Firefox Options
+* Deprecate the use of browser profile when instantiating a session
+* Update start session to handle the W3C New Session
+* Add get/set window rect commands
+* Add InvalidArgumentException
+* When passing in `text` to send_keys, make sure we send a string not array
+* Fix string decoding in remote connection (#3663)
+* Fix indentation to satisfy PEP8
+* Try use old way of setting page load timeout if new way fails. Fixes #3654
+* fix file uploads for Firefox
+* Run unit tests on Python 3.3, 3.4, and 3.5 (#3638)
+* Fix indentation in double_click.
+* Fix non-W3C page load timeout setting.
+
+Selenium 3.3.1
+* Fix encoding of basic auth header when using Python 3 Fixes #3622
+* Add initial unit test suite
+* Update W3C Timeout setting to be in line with the specification
+* support.ui.Select class inherits from object (#3067)
+* fix bug in proxy constructor that some properties are not proper set (#3459)
+* Fix flake8 issues (#3628)
+
+Selenium 3.3.0
+** Note ** If you are updating to this version, please also update GeckoDriver to v0.15.0
+* Fix python HTTPS encoding for python driver (#3379)
+* Allow Firefox preferences to be set directly in Options
+* Fix shutdown and process termination (#3263)
+* Preventing exception if log_path is none or empty. Fixes #3128
+* Add the W3C capability to Firefox for accepting insecure certificates
+* Initial implementation of Pointer Actions
+* Only skip tests if driver name matches a directory name.
+* Update calls that return a pure object with keys to look for 'value' key
+* Initial W3C Actions support
+* fix docs output directory
+
+Selenium 3.0.2
+* Add support for W3C Get Active Element
+* Return when we use executeScript for faking WebElement.get_property
+* Expand user paths and resolve absolute path for Chrome extensions
+* Add support for verbose logging and custom path to EdgeDriver
+* Update TakeElementScreenshot to match WebDriver specification
+* Raise WebDriverException when FirefoxBinary fails to locate binary
+* Fix getting IP for python 3
+* Write Service log to DEVNULL by default
+* Only attempt to remove the Firefox profile path if one was specified
+* Added context manager for chrome/content in Firefox
+
+Selenium 3.0.1
+* Fix regressions with python 3
+* Add support for Safari Technology Preview
+
+Selenium 3.0.0
+* new FirefoxDriver ctor precedence logic and moz:firefoxOptions support (#2882)
+* Add W3C Set Window Position and W3C Get Window Position
+* enable log path setting from firefox webdriver (#2700)
+* Correct encoding of getAttribute.js and only load it once. Fixes #2785
+* Encode the isDisplayed atom and only load it once
+
+Selenium 3.0.0.b3
+* Use atoms for get_attribute and is_displayed when communicating with
+  a w3c compliant remote end.
+* Make it possible to specialise web element
+
+Selenium 3.0.0.b2
+* Updated Marionette port argument to match other drivers.
+
+Selenium 3.0.0.b1
+* Fix basestring reference to work with python 3. Fixes #1820
+* Correct Length conditional when filtering in PhantomJS. Fixes #1817
+* Fix send keys when using PUA keys e.g. Keys.RIGHT #1839
+* Fix cookie file leak in PhantomJS #1854
+* Use the correct binary path when using Marionette
+* Fixed: Unhelpful error message when PhantomJS exits. (#2173 #2168)
+* Fix broken link to python documentation (#2159)
+* Attempt to remove Firefox profile when using Marionette
+* Ensure all capabilities are either within desiredCapabilities or requiredCapabilities
+* Correct the expected capability name for the Firefox profile
+* Add Firefox options to capabilities
+* Visibility_of_all implies it only returns elements if all visible (#2052)
+* Find visible elements (#2041)
+*  Pass the firefox_profile as a desired capability in the Python client when using a remote server
+* Avoid checking exception details for invalid locators due to differences in server implementations
+* Handle capabilities better with Marionette and GeckoDriver
+* Updated the maxVersion of FirefoxDriver xpi maxVersion to work with Firefox 47.0.1
+* Remove Selenium RC support
+
+Selenium 2.53.0
+* Adding Options object for use with Python FirefoxDriver
+* Fixed improper usage of super in exceptions module
+* create a temp file for cookies in phantomjs if not specified
+* Pass in the executable that FirefoxBinary finds to the service if there isnt one passed in as a kwarg or capability
+* Applied some DRY and extracted out the keys_to_typing()
+* Fix deselecting options in <select>
+
+
+Selenium 2.52.0
+* Fixing case where UnexpectedAlertException doesn't get the alert_text in the error object
+* Firefox: Actually use launch_browser timeout Fixes #1300
+
+Selenium 2.51.1
+* correcting bundling issue missing README.rst file
+
+Selenium 2.51.0
+* Firefox updates (see java changelog)
+
+Selenium 2.50.1
+* Fixing error message handling. Fixes issue #1497
+* Fixing error message handling. Fixes issue #1507
+* Update webelement to handle W3C commands for size/location and rect
+* rewrite click scrolling tests to match the Java ones
+
+Selenium 2.50.0
+* handle potential URLError from sending shutdown, set self.process to None after it's already been quit
+* Add support for submit() with W3C compliant endpoint
+
+Selenium 2.49.1
+* Ensure you can close stream before attempting to close it.
+* message response may cause json loads ValueError when it's not actually json
+  and just a string (like the message that occurs when firefox driver thinks
+  another element will receive the click)
+* Cleanup some error handling when sniffing what protocol you are speaking
+
+Selenium 2.49.0
+* Have Firefox service write to a file instead of PIPE
+* on osx for firefox, fallback to checking homebrew install, if the default isn't there
+* Added Firefox path variable for string placeholder
+* Update README to show Python 3.2+
+* refactoring all the service classes to use a common one.
+* Add Firefox specific command to switch context between Browser content and Browser chrome
+* updating files after go copyright:update
+* Use specificationLevel to know that we are speaking GeckoDriver
+* Bug fixes: #1294, #1186
+
+Selenium 2.48.0
+* Update error pulling to match spec when we encounter a spec compliant browser.
+* Disable tests that are not working with Marionette when running Marionette tests
+* Add the ability to run python marionette tests
+* Python 3 compatibility for remote Authorization
+* changing casing of children finding tests
+
+Selenium 2.47.3
+* Bring back py 3 support
+
+Selenium 2.47.2
+* Fix running Edge driver locally on win10
+* adding repr to WebDriver and WebElement
+
+Selenium 2.47.1
+* Fix the issue of deleting the profile when shutting down Firefox
+* WebElement __eq__ compares against more types
+* Issues fixed: 850
+
+Selenium 2.47.0
+* Add in support for when communicating with a Spec compliant browsers
+* Initial support for Edge using EdgeDriver
+* Issues fixed: 818
+
+Selenium 2.46.1
+* Adding ability to make remote call for webelement screenshots in accordance to the W3C spec
+* Adding api to authenticate HTTP Auth modal dialogs via driver.switch_to.alert (beta)
+* Add rebeccapurple to Color Object
+* Add element screenshot
+* Add service handler and minimal update to driver to use service for Marionette
+* Add the ability to start FirefoxDriver backed with Marionette via a capability
+* support socket timeout for connections
+* free_port checks if port is available on all interfaces
+* Allow error handling to handle both current errors and w3c errors
+* Update find_elements to match spec
+* phantomjs: service: remove unused import of signal
+* phantomjs: add port information to WebDriverException
+* Issues fixed (Github): 478, 612, 734, 780
+
+Selenium 2.46.0
+* Firefox support up to 38
+* BlackBerry browser support
+* remove Presto-Opera support
+* firefox extension extraction fixes
+* process management fixes with phantomjs
+* Comparing remote web element for equality does not require a remote command
+* Issues Fixed: (gcode) 8493, 8521, 8498, 8274, 8497, 5923
+* Issues Fixed: (github) 401
+
+Selenium 2.45.0
+* Firefox support up to 35, support for native events up to 34.
+* Make Opera driver support also the new Blink based Opera
+* README: Fix the Yahoo example
+* WebElement docstring fixes
+* Add debugger_address option to the ChromeDriver options list to optionally instruct ChromeDriver to wait for the target devtools instance to be started at a given host:ip
+* Set default value for PhantomJS process reference
+* Allow setting of FileDetector for send_keys
+* Pass info to TimeoutException in WebDriverWait
+* Issues Fixed: 8065, 8310, 8539
+
+Selenium 2.44.0
+* (previous release person forgot to add release notes! DAVID!)
+
+Selenium 2.43.0
+* Expand WebElement.get_attribute API docs
+* firefox may be installed without admininstrator privileges
+    and therefore there may be no HKEY_LOCAL_MACHINE entry. Issue #7784
+* UnexpectedAlertPresentException should contain the alert text in python too. Issue #7745
+* don't mutate the global 'ignored exceptions', take a copy of the globally specified ones, change the
+  global to be a tuple instead. Issue #7725
+* raise exception when the firefox binary isn't actually found, which usually implies the upgrade failed (on windows) Issue #6092 ,#6847
+* Fixing NameError: global name 'options' is not defined.
+* Remove unused import subprocess.PIPE
+* Redirect Firefox output to /dev/null by default Fixes Issue #7677
+* More flexible management of http timeouts in Selenium RC python client
+* Generate Python API docs for selenium.webdriver.chrome.options. Fixes issue #7471
+* Use 127.0.0.1 as localhost name resolving might fail on some systems
+
+Selenium 2.42.1
+* Fixed Py3 issues
+* Make firefox_binary.py and firefox_profile.py not executable
+* Make exceptions Python 3 compatible
+
+Selenium 2.42
+* Support for Firefox 29 Native Events
+* "remote_url" and "remote_browser" parameters for "./go test_remote".
+* missing __init__ in new android module
+* issue #7304 Fix memory leak caused by __del__ in PhantomJS
+* File upload using remotedriver on python3
+* Updating xpi install to align with mozprofile
+* command_executor should also support unicode strings as well.
+
+Selenium 2.41
+* Support for Firefox 28
+* deprecating switch_to_* in favour of driver.switch_to.*
+
+Selenium 2.40
+* Support for Firefox 27
+* Fixes related to http connection
+* Fix for phantomjs running on windows #6736
+
+Selenium 2.39
+* Support for Firefox 26
+
+Selenium 2.38.4
+* keep-alive can't be used for phantomjs / IE, fix for that and tested for py3 :)
+
+Selenium 2.38.3
+* really supporting py3 :)
+
+Selenium 2.38.2
+* py3 support (once again)
+
+Selenium 2.38.1
+* fix packaging problem where firefox/webdriver_prefs.json was missing
+
+Selenium 2.38
+* Support for Firefox 25
+* FirefoxProfile now using common webdriver.json instead of having our own copy in py
+  - behavior change to the preferences is that they now should be treated
+    like raw types rather than strings and allow the json library to translate
+    the types appropriated (e.g. True => true)
+
+* Set proper 'Accept' request header so that Python bindings work with some old WebDriver implementations that reply 404 to requests with no 'Accept' set.
+* handle redirect response explicitly (since switching to using keep-alive)
+* phantomjs service needs to really kill the spawned process Issue #5921
+* removing old api endpoints from command listing
+* using keep-alive for remote connection
+* adjusting phantomjs subprocess.Popen
+* ActionsChains.send_keys should use <session>/keys endpoint Issue #6348
+* fix TypeError in chrome_options.extensions for Python3.x
+
+* Other Bugs Fixed: #6531, #6513, #4569, #6454
+
+
+Selenium 2.37.2
+* fix regression added with unicode fix
+* Bug fix #6360
+
+Selenium 2.37.1
+* fix find_elements on webelement using unicode locators and py 2.7
+
+Selenium 2.37
+* repackage with fix for Firefox native events on Linux
+* fix issue with unicode By locators w/ python 2.7 #6430
+
+Selenium 2.36
+* Added Safari WebDriver. Fixes issue 5352.
+* fix platform for safari caps
+* Convert all offsets/coordinates/speeds into integers
+* Fix drag and drop by offset behaviour
+* Fix initialization of Proxy by capabilities when proxyType is set
+* Enable SOCKS proxy support
+* Validation of passed locator for find_element(s) methods #5690
+* Adding support for /status, /sessions commands
+* Doc fixes
+* ability to set Chrome extensions by using base64 encoded strings #4013
+* fix logic regarding Select.select_by_visible_text #3910
+* Bugs fixed: #6165, #6231
+
+Selenium 2.35
+* Remove duplicate 'get screenshot as file' methods.  Add method 'get_screenshot_as_png'
+* fixing UnicodeEncodeError on get attribute of webelement
+
+Selenium 2.34
+* Corrected webdriverbackedselenium session handling. Fixes issue 4283
+* Corrected use of basestring for python 3. Fixes issue 5924
+* Support for Firefox 22
+* Added support for logging from the browser
+* corrected proxy handling on FirefoxProfile
+* Corrected handling of chrome extensions. Fixes issue 5762
+
+Selenium 2.33
+* getText() ignores elements in the <head>
+* Adding both official and informal string representations to Color object.
+* Replace distutils.dir_util by shutil
+* Allow finding firefox binary at ProgramFiles(x86) on windows(64 bit)
+* Py3 compatible winreg import and content-type access
+
+Selenium 2.32
+* Support for FF20 Native Events
+* Python 3 support
+* Misc Python 3 patches
+* Allow easy FirefoxBinary subclassing
+
+Selenium 2.31
+* Support for FF19 native events
+* web element equality is now in conformance with other language bindings
+
+Selenium 2.30
+* Allow env to be specified for the chromedriver service
+* Allow log path to be specified for phantomjs driver service.
+* Bug Fixes: 4608 4940 4974 5034 5075
+
+Selenium 2.29
+* Allow subclassing of driver and have the ability to send_keys Issue 4877, 5017
+* Simplifying save_screenshot and allow phantomjs to take screenshots
+
+Selenium 2.28
+* "null" can now be passed to executeScript
+* Add transparent and extended colour keywords to color support module. Fixes issue 4866
+
+Selenium 2.27
+* Added support for phantomjs / ghostdriver
+* Fix python client, avoid duplicate chrome option items after reusing options class. Fixes Issue 4744.
+* adding colour support to Python. fixes issue 4623
+* Adding log_path/service_log_path as named kwargs for chrome
+
+Selenium 2.26
+* Added location_when_scrolled_into_view - Bug 4357
+* Added new expected_conditions support module to be used with WebDriverWait
+
+Selenium 2.25
+* Jython 2.7 Support - Bug 3988
+* EventFiringWebDriver added to Support module - Bug 2267
+* Added IEDriverServer logging that can be accessed via desired capabilities
+* Fixed by data being passed into find_elements - bug 3735
+* Removed deprecated ChromeDriver items around desiredcapabilites in favour of chrome options
+* Added default values for a number of action_chains calls
+
+Selenium 2.24
+* Removing the ctypes approach of invoking IEDriver, you will need to download the IEDriverServer from
+  https://code.google.com/p/selenium/downloads/list
+
+Selenium 2.23
+* Support for FF13 native events
+
+Selenium 2.22
+* Moving IEDriver to be able to use IEDriverServer
+
+Selenium 2.21.3
+* Fix for File Upload to remote servers
+* Better handling of typing in input=file. Bug 3831, 3736
+* Better handling of unicode URLS Bug 3740
+
+Selenium 2.21.2
+* Fix typing to file input when not using Selenium Server. Bug 3736
+
+Selenium 2.21.1
+* focusmanager.testmode messes with native events, removing it.
+
+Selenium 2.21
+* Local File upload capabilities for non-remote browser
+* Adding maximize_window api call
+* Updating default firefox profile to set focusmanager.testmode to true
+    see https://bugzilla.mozilla.org/show_bug.cgi?id=704583
+* bugs fixed: 3506, 3528, 3607
+
+Selenium 2.20
+* disable native events for FF on Mac by default
+* fix webdriverwait to execute at least once when using 0 timeout
+* Fixed Issue 3438
+
+Selenium 2.19
+* WebDriverBackedSelenium is now avalaible to all languages
+* Addon installation fixes
+
+Selenium 2.18
+* Proxy capabilities passing
+
+Selenium 2.17
+* OperaDriver can now be invoked by webdriver.Opera()
+* Support has been added for ChomeOptions. This deprecates support passing in DesiredCapabilities
+* Proxy class to tell the browser a proxy is in use. Currently only for Firefox
+
+Selenium 2.16
+* bug fixes
+
+Selenium 2.15
+* bug fixes
+
+Selenium 2.14
+* Fix for LD_PRELOAD being polluted by WebDriver
+* Added Orientation API
+* A fix for Error Handling
+
+Selenium 2.13
+* Fixed switch_to_window so that it didnt crash Firefox Bug 2633
+* Fixed Screenshot handling to work in all browsers. Bug 2829
+* Force Firefox to the Foreground
+
+Selenium 2.12
+* Added Select as a support pacakge
+* Added Beta window size / position api's
+* Bug Fixes
+
+Selenium 2.11.0 2.11.1
+* no changes just packaging
+
+Selenum 2.10
+* "Choose which apps" dialog has been disabled
+* Bug Fixes
+
+Selenium 2.9
+* Bug Fixes
+* Documentation
+
+Selenium 2.8
+* Actions updates
+* Bug Fixes
+
+Selenium 2.6
+* Documentation fixes
+
+Selenium 2.5
+* Fixed x64 IE Support
+* Bug Fixes
+
+Selenium 2.4
+* Bug Fixes
+* x64 IE Support
+* Added WebDriverWait as a support package
+
+Selenium 2.3
+* Bug Fixes
+
+Selenium 2.2
+* Ability to get screenshots from Exceptions if they are given
+* Access to Remote StackTrace on error
+
+Selenium 2.1
+* Bug Fixes
+
+Selenium 2
+* Removed toggle() and select()
+
+Selenium 2 RC 3
+* Added Opera to Desired Capabilities
+* Removed deprecrated methods
+* Deprecated toggle() and select() methods. This will be removed in the next release
+
+Selenium 2 Beta 4
+* Fix for using existing Firefox Profiles
+* Alerts Support in IE
+* Fix to dictionary returned from size
+* Deprecated value property. Use the get_attribute("value") method
+* Deprecated get_page_source method. Use page_source property
+* Deprecated get_current_window_handle. Use current_window_handle property
+* Deprecated get_window_handles. Use window_handles property
+* Ability to install extensions into profiles
+* Added Location to the WebElement
+* ChromeDriver rewritten to use new built in mechanism
+* Added Advanced User Interaction API. Only Available for HTMLUnit at the moment
+* Profiles now delete their temp folders when driver.quit() is called
+
+Selenium 2 Beta 3
+* Accept Untrusted Certificates in Firefox
+* Fixed Screenshots
+* Added DesiredCapabilities to simplify choosing Drivers
+* Fixed Firefox Profile creation
+* Added Firefox 4 support
+* DocStrings Improvements
+
+Selenium 2 Beta 2
+
+* New bindings landed. Change webdriver namespace to "selenium.webdriver"
+* Ability to move to default content
+* Implicit Waits
+* Change the API to use properties instead of get_x
+* Changed the Element Finding to match other languages
+* Added ability to execute asynchronous scripts from the driver
+* Ability to get rendered element size
+* Ability to get CSS Value on a webelement
+* Corrected Element finding from the element
+* Alert and Prompt handling
+* Improved IEDriver
+* Basic Authentication support for Selenium 2
+* Ability to have multiple Firefox instances
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 00000000..a9114391
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,20 @@
+prune *
+recursive-include selenium/webdriver *.py
+recursive-include selenium/webdriver/common *.py
+recursive-include selenium/webdriver/common/actions *.py
+recursive-include selenium/webdriver/common/html5 *.py
+recursive-include selenium/common *.py
+recursive-include selenium/webdriver/chrome *.py
+recursive-include selenium/webdriver/opera *.py
+recursive-include selenium/webdriver/phantomjs *.py
+recursive-include selenium/webdriver/firefox *.py *.xpi *.json
+recursive-include selenium/webdriver/ie *.py
+recursive-include selenium/webdriver/edge *.py
+recursive-include selenium/webdriver/remote *.py *.js
+recursive-include selenium/webdriver/support *.py
+include selenium/selenium.py
+include selenium/__init__.py
+include CHANGES
+include README.rst
+include LICENSE
+recursive-include selenium.egg-info *
diff --git a/README.rst b/README.rst
new file mode 120000
index 00000000..57b939dd
--- /dev/null
+++ b/README.rst
@@ -0,0 +1 @@
+docs/source/index.rst
\ No newline at end of file
diff --git a/build.desc b/build.desc
new file mode 100644
index 00000000..78e85636
--- /dev/null
+++ b/build.desc
@@ -0,0 +1,85 @@
+# py_test targets emplicitly get the following extra targets added:
+# for each browser in browsers:
+#   :name_B to gather the sources of the tests for browser B
+#   :name_B:run to run the tests for browser B
+# Also, if only one browser, B, is listed in browsers:
+#   :name:run is created as a synonym for :name_B:run
+#
+# Currently, pulling in other tests through deps
+# is limited to deps declared in the same build.desc file
+
+py_test(
+  name = "firefox_test",
+  deps = [ ":test_ff" ],
+  resources = [
+    { "//third_party/js/selenium:webdriver_prefs" : "selenium/webdriver/firefox/webdriver_prefs.json" },
+    { "//third_party/js/selenium:webdriver" : "selenium/webdriver/firefox/" }
+  ],
+  browsers = [ "ff" ])
+
+py_test(
+  name = "marionette_test",
+  deps = [ ":test_marionette" ],
+  browsers = [ "marionette" ])
+
+py_test(
+  name = "blackberry_test",
+  deps = [ ":test_blackberry" ],
+  browsers = [ "blackberry" ])
+
+py_test(
+  name = "chrome_test",
+  deps = [ ":test_chrome" ],
+  browsers = [ "chrome" ])
+
+py_test(
+  name = "ie_test",
+  deps = [ ":test_ie" ],
+  browsers = [ "ie" ])
+
+py_test(
+  name = "edge_test",
+  deps = [":test_edge"],
+  browsers = [ "edge"])
+
+py_test(
+  name = "remote_firefox_test",
+  deps = [ ":test_remote_firefox" ],
+  browsers = [ "remote_firefox" ])
+
+py_test(
+  name = "safari_test",
+  deps = [ ":test_safari" ],
+  browsers = [ "safari" ])
+
+py_test(
+  name = "test",
+  browsers = [
+    "chrome",
+    "ff",
+    "marionette",
+    "ie",
+    "edge",
+    "blackberry",
+    "remote_firefox",
+    "safari",
+  ])
+
+py_docs(
+  name = "docs"
+)
+
+py_install(
+  name = "install"
+)
+
+py_prep(
+  name = "prep",
+  deps = [
+    "//cpp:noblur",
+    "//cpp:noblur64",
+    "//javascript/atoms/fragments:is-displayed",
+    "//third_party/js/selenium:webdriver",
+    "//third_party/js/selenium:webdriver_prefs",
+    "//javascript/webdriver/atoms:get-attribute"
+  ])
diff --git a/conftest.py b/conftest.py
new file mode 100644
index 00000000..7ba634a0
--- /dev/null
+++ b/conftest.py
@@ -0,0 +1,219 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import os
+import socket
+import subprocess
+import sys
+import time
+
+import pytest
+from _pytest.skipping import MarkEvaluator
+
+from selenium import webdriver
+from selenium.webdriver import DesiredCapabilities
+from test.selenium.webdriver.common.webserver import SimpleWebServer
+from test.selenium.webdriver.common.network import get_lan_ip
+
+if sys.version_info[0] == 3:
+    from urllib.request import urlopen
+else:
+    from urllib import urlopen
+
+drivers = (
+    'BlackBerry',
+    'Chrome',
+    'Edge',
+    'Firefox',
+    'Ie',
+    'Marionette',
+    'Remote',
+    'Safari',
+    'WebKitGTK',
+)
+
+
+def pytest_addoption(parser):
+    parser.addoption('--driver', action='append', choices=drivers, dest='drivers',
+                     metavar='DRIVER',
+                     help='driver to run tests against ({})'.format(', '.join(drivers)))
+    parser.addoption('--browser-binary', action='store', dest='binary',
+                     help='location of the browser binary')
+    parser.addoption('--driver-binary', action='store', dest='executable',
+                     help='location of the service executable binary')
+    parser.addoption('--browser-args', action='store', dest='args',
+                     help='arguments to start the browser with')
+
+
+def pytest_ignore_collect(path, config):
+    drivers_opt = config.getoption('drivers')
+    _drivers = set(drivers).difference(drivers_opt or drivers)
+    if drivers_opt:
+        _drivers.add('unit')
+    parts = path.dirname.split(os.path.sep)
+    return len([d for d in _drivers if d.lower() in parts]) > 0
+
+
+driver_instance = None
+
+
+@pytest.fixture(scope='function')
+def driver(request):
+    kwargs = {}
+
+    try:
+        driver_class = request.param
+    except AttributeError:
+        raise Exception('This test requires a --driver to be specified.')
+
+    # conditionally mark tests as expected to fail based on driver
+    request.node._evalxfail = request.node._evalxfail or MarkEvaluator(
+        request.node, 'xfail_{0}'.format(driver_class.lower()))
+    if request.node._evalxfail.istrue():
+        def fin():
+            global driver_instance
+            if driver_instance is not None:
+                driver_instance.quit()
+            driver_instance = None
+        request.addfinalizer(fin)
+
+    # skip driver instantiation if xfail(run=False)
+    if not request.config.getoption('runxfail'):
+        if request.node._evalxfail.istrue():
+            if request.node._evalxfail.get('run') is False:
+                yield
+                return
+
+    driver_path = request.config.option.executable
+    options = None
+
+    global driver_instance
+    if driver_instance is None:
+        if driver_class == 'BlackBerry':
+            kwargs.update({'device_password': 'password'})
+        if driver_class == 'Firefox':
+            kwargs.update({'capabilities': {'marionette': False}})
+            options = get_options(driver_class, request.config)
+        if driver_class == 'Marionette':
+            driver_class = 'Firefox'
+            options = get_options(driver_class, request.config)
+        if driver_class == 'Remote':
+            capabilities = DesiredCapabilities.FIREFOX.copy()
+            kwargs.update({'desired_capabilities': capabilities})
+            options = get_options('Firefox', request.config)
+        if driver_class == 'WebKitGTK':
+            options = get_options(driver_class, request.config)
+        if driver_path is not None:
+            kwargs['executable_path'] = driver_path
+        if options is not None:
+            kwargs['options'] = options
+        driver_instance = getattr(webdriver, driver_class)(**kwargs)
+    yield driver_instance
+    if MarkEvaluator(request.node, 'no_driver_after_test').istrue():
+        driver_instance = None
+
+
+def get_options(driver_class, config):
+    browser_path = config.option.binary
+    browser_args = config.option.args
+    options = None
+    if browser_path or browser_args:
+        options = getattr(webdriver, '{}Options'.format(driver_class))()
+        if driver_class == 'WebKitGTK':
+            options.overlay_scrollbars_enabled = False
+        if browser_path is not None:
+            options.binary_location = browser_path
+        if browser_args is not None:
+            for arg in browser_args.split():
+                options.add_argument(arg)
+    return options
+
+
+@pytest.fixture(scope='session', autouse=True)
+def stop_driver(request):
+    def fin():
+        global driver_instance
+        if driver_instance is not None:
+            driver_instance.quit()
+        driver_instance = None
+    request.addfinalizer(fin)
+
+
+def pytest_exception_interact(node, call, report):
+    if report.failed:
+        global driver_instance
+        if driver_instance is not None:
+            driver_instance.quit()
+        driver_instance = None
+
+
+@pytest.fixture
+def pages(driver, webserver):
+    class Pages(object):
+        def url(self, name):
+            return webserver.where_is(name)
+
+        def load(self, name):
+            driver.get(self.url(name))
+    return Pages()
+
+
+@pytest.fixture(autouse=True, scope='session')
+def server(request):
+    drivers = request.config.getoption('drivers')
+    if drivers is None or 'Remote' not in drivers:
+        yield None
+        return
+
+    _host = 'localhost'
+    _port = 4444
+    _path = '../buck-out/gen/java/server/src/org/openqa/grid/selenium/selenium.jar'
+
+    def wait_for_server(url, timeout):
+        start = time.time()
+        while time.time() - start < timeout:
+            try:
+                urlopen(url)
+                return 1
+            except IOError:
+                time.sleep(0.2)
+        return 0
+
+    _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    url = 'http://{}:{}/wd/hub'.format(_host, _port)
+    try:
+        _socket.connect((_host, _port))
+        print('The remote driver server is already running or something else'
+              'is using port {}, continuing...'.format(_port))
+    except Exception:
+        print('Starting the Selenium server')
+        process = subprocess.Popen(['java', '-jar', _path])
+        print('Selenium server running as process: {}'.format(process.pid))
+        assert wait_for_server(url, 10), 'Timed out waiting for Selenium server at {}'.format(url)
+        print('Selenium server is ready')
+        yield process
+        process.terminate()
+        process.wait()
+        print('Selenium server has been terminated')
+
+
+@pytest.fixture(autouse=True, scope='session')
+def webserver():
+    webserver = SimpleWebServer(host=get_lan_ip())
+    webserver.start()
+    yield webserver
+    webserver.stop()
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 00000000..2d483706
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,131 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+HTML_DESTINATION     = ../../docs/api/py
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html       to make standalone HTML files"
+	@echo "  dirhtml    to make HTML files named index.html in directories"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  pickle     to make pickle files"
+	@echo "  json       to make JSON files"
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
+	@echo "  qthelp     to make HTML files and a qthelp project"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
+	@echo "  epub       to make an epub"
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+	@echo "  text       to make text files"
+	@echo "  man        to make manual pages"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck  to check all external links for integrity"
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html: clean
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(HTML_DESTINATION)
+	@echo
+	@echo "Build finished. The HTML pages are in $(HTML_DESTINATION)."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+	@echo
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Selenium.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Selenium.qhc"
+
+devhelp:
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+	@echo
+	@echo "Build finished."
+	@echo "To view the help file:"
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/Selenium"
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Selenium"
+	@echo "# devhelp"
+
+epub:
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
+	      "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through pdflatex..."
+	make -C $(BUILDDIR)/latex all-pdf
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+	@echo
+	@echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+	@echo
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/source/api.rst b/docs/source/api.rst
new file mode 100644
index 00000000..82f128e2
--- /dev/null
+++ b/docs/source/api.rst
@@ -0,0 +1,149 @@
+:orphan:
+
+======================
+Selenium Documentation
+======================
+
+Common
+------
+
+.. currentmodule:: selenium.common
+.. autosummary::
+   :toctree: common
+
+   selenium.common.exceptions
+
+Webdriver.common
+----------------
+
+.. currentmodule:: selenium.webdriver.common
+.. autosummary::
+   :toctree: webdriver
+
+   selenium.webdriver.common.action_chains
+   selenium.webdriver.common.alert
+   selenium.webdriver.common.by
+   selenium.webdriver.common.desired_capabilities
+   selenium.webdriver.common.keys
+   selenium.webdriver.common.touch_actions
+   selenium.webdriver.common.utils
+   selenium.webdriver.common.proxy
+   selenium.webdriver.common.service
+   selenium.webdriver.common.html5.application_cache
+
+Webdriver.support
+-----------------
+
+.. currentmodule:: selenium.webdriver.support
+.. autosummary::
+   :toctree: webdriver_support
+
+   selenium.webdriver.support.abstract_event_listener
+   selenium.webdriver.support.color
+   selenium.webdriver.support.event_firing_webdriver
+   selenium.webdriver.support.expected_conditions
+   selenium.webdriver.support.select
+   selenium.webdriver.support.wait
+
+Webdriver.android
+-----------------
+
+.. currentmodule:: selenium.webdriver.android
+.. autosummary::
+   :toctree: webdriver_android
+
+   selenium.webdriver.android.webdriver
+
+Webdriver.chrome
+----------------
+
+.. currentmodule:: selenium.webdriver.chrome
+.. autosummary::
+   :toctree: webdriver_chrome
+
+   selenium.webdriver.chrome.options
+   selenium.webdriver.chrome.service
+   selenium.webdriver.chrome.webdriver
+
+Webdriver.firefox
+-----------------
+
+.. currentmodule:: selenium.webdriver.firefox
+.. autosummary::
+   :toctree: webdriver_firefox
+
+   selenium.webdriver.firefox.extension_connection
+   selenium.webdriver.firefox.firefox_binary
+   selenium.webdriver.firefox.options
+   selenium.webdriver.firefox.firefox_profile
+   selenium.webdriver.firefox.webdriver
+
+Webdriver.ie
+------------
+
+.. currentmodule:: selenium.webdriver.ie
+.. autosummary::
+   :toctree: webdriver_ie
+
+   selenium.webdriver.ie.webdriver
+
+Webdriver.opera
+---------------
+
+.. currentmodule:: selenium.webdriver.opera
+.. autosummary::
+   :toctree: webdriver_opera
+
+   selenium.webdriver.opera.webdriver
+
+Webdriver.phantomjs
+-------------------
+
+.. currentmodule:: selenium.webdriver.phantomjs
+.. autosummary::
+   :toctree: webdriver_phantomjs
+
+   selenium.webdriver.phantomjs.service
+   selenium.webdriver.phantomjs.webdriver
+
+Webdriver.remote
+----------------
+
+.. currentmodule:: selenium.webdriver.remote
+.. autosummary::
+   :toctree: webdriver_remote
+
+   selenium.webdriver.remote.command
+   selenium.webdriver.remote.errorhandler
+   selenium.webdriver.remote.mobile
+   selenium.webdriver.remote.remote_connection
+   selenium.webdriver.remote.utils
+   selenium.webdriver.remote.webdriver
+   selenium.webdriver.remote.webelement
+
+Webdriver.safari
+----------------
+
+.. currentmodule:: selenium.webdriver.safari
+.. autosummary::
+  :toctree: webdriver_safari
+
+  selenium.webdriver.safari.service
+  selenium.webdriver.safari.webdriver
+
+Webdriver.webkitgtk
+-------------------
+
+.. currentmodule:: selenium.webdriver.webkitgtk
+.. autosummary::
+  :toctree: webdriver_webkitgtk
+
+  selenium.webdriver.webkitgtk.options
+  selenium.webdriver.webkitgtk.service
+  selenium.webdriver.webkitgtk.webdriver
+
+Indices and tables
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/docs/source/common/selenium.common.exceptions.rst b/docs/source/common/selenium.common.exceptions.rst
new file mode 100644
index 00000000..356d256c
--- /dev/null
+++ b/docs/source/common/selenium.common.exceptions.rst
@@ -0,0 +1,4 @@
+selenium.common.exceptions
+==========================
+
+.. automodule:: selenium.common.exceptions
diff --git a/docs/source/conf.py b/docs/source/conf.py
new file mode 100644
index 00000000..992ea3b0
--- /dev/null
+++ b/docs/source/conf.py
@@ -0,0 +1,274 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import unicode_literals
+
+import sys, os, os.path
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+# sys.path.insert(0, os.path.abspath('.'))
+sys.path.insert(0, os.path.join(os.getcwd(), "..", ".."))
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.imgmath', 'sphinx.ext.viewcode']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'Selenium'
+copyright = '2011, plightbo, simon.m.stewart, hbchai, jrhuggins, et al.'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '3.141'
+# The full version, including alpha/beta/rc tags.
+release = version
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = []
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+html_show_sphinx = False
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Seleniumdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'Selenium.tex', 'Selenium Documentation',
+   'plightbo, simon.m.stewart, hbchai, jrhuggins, et al.', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    ('index', 'selenium', 'Selenium Documentation',
+     ['plightbo, simon.m.stewart, hbchai, jrhuggins, et al.'], 1)
+]
+
+
+# -- Options for Epub output ---------------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = 'Selenium'
+epub_author = 'plightbo, simon.m.stewart, hbchai, jrhuggins, et al.'
+epub_publisher = 'plightbo, simon.m.stewart, hbchai, jrhuggins, et al.'
+epub_copyright = '2011, plightbo, simon.m.stewart, hbchai, jrhuggins, et al.'
+
+# The language of the text. It defaults to the language option
+# or en if the language is not set.
+#epub_language = ''
+
+# The scheme of the identifier. Typical schemes are ISBN or URL.
+#epub_scheme = ''
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+#epub_identifier = ''
+
+# A unique identification for the text.
+#epub_uid = ''
+
+# HTML files that should be inserted before the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+#epub_pre_files = []
+
+# HTML files shat should be inserted after the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+#epub_post_files = []
+
+# A list of files that should not be packed into the epub file.
+#epub_exclude_files = []
+
+# The depth of the table of contents in toc.ncx.
+#epub_tocdepth = 3
+
+# Allow duplicate toc entries.
+#epub_tocdup = True
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'http://docs.python.org/': None}
+
+# 'members' includes anything that has a docstring, 'undoc-members' includes
+# functions without docstrings.
+autodoc_default_flags = ['members', 'undoc-members']
+
+# Include __init__ comments
+autoclass_content = "both"
diff --git a/docs/source/index.rst b/docs/source/index.rst
new file mode 100644
index 00000000..8ef805d2
--- /dev/null
+++ b/docs/source/index.rst
@@ -0,0 +1,145 @@
+======================
+Selenium Client Driver
+======================
+
+Introduction
+============
+
+Python language bindings for Selenium WebDriver.
+
+The `selenium` package is used to automate web browser interaction from Python.
+
++-----------+--------------------------------------------------------------------------------------+
+| **Home**: | http://www.seleniumhq.org                                                            |
++-----------+--------------------------------------------------------------------------------------+
+| **Docs**: | `selenium package API <https://seleniumhq.github.io/selenium/docs/api/py/api.html>`_ |
++-----------+--------------------------------------------------------------------------------------+
+| **Dev**:  | https://github.com/SeleniumHQ/Selenium                                               |
++-----------+--------------------------------------------------------------------------------------+
+| **PyPI**: | https://pypi.org/project/selenium/                                                   |
++-----------+--------------------------------------------------------------------------------------+
+| **IRC**:  | **#selenium** channel on freenode                                                    |
++-----------+--------------------------------------------------------------------------------------+
+
+Several browsers/drivers are supported (Firefox, Chrome, Internet Explorer), as well as the Remote protocol.
+
+Supported Python Versions
+=========================
+
+* Python 2.7, 3.4+
+
+Installing
+==========
+
+If you have `pip <https://pip.pypa.io/>`_ on your system, you can simply install or upgrade the Python bindings::
+
+    pip install -U selenium
+
+Alternately, you can download the source distribution from `PyPI <https://pypi.org/project/selenium/#files>`_ (e.g. selenium-4.0.0a1.tar.gz), unarchive it, and run::
+
+    python setup.py install
+
+Note: You may want to consider using `virtualenv <http://www.virtualenv.org/>`_ to create isolated Python environments.
+
+Drivers
+=======
+
+Selenium requires a driver to interface with the chosen browser. Firefox,
+for example, requires `geckodriver <https://github.com/mozilla/geckodriver/releases>`_, which needs to be installed before the below examples can be run. Make sure it's in your `PATH`, e. g., place it in `/usr/bin` or `/usr/local/bin`.
+
+Failure to observe this step will give you an error `selenium.common.exceptions.WebDriverException: Message: 'geckodriver' executable needs to be in PATH.`
+
+Other supported browsers will have their own drivers available. Links to some of the more popular browser drivers follow.
+
++--------------+-----------------------------------------------------------------------+
+| **Chrome**:  | https://sites.google.com/a/chromium.org/chromedriver/downloads        |
++--------------+-----------------------------------------------------------------------+
+| **Edge**:    | https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ |
++--------------+-----------------------------------------------------------------------+
+| **Firefox**: | https://github.com/mozilla/geckodriver/releases                       |
++--------------+-----------------------------------------------------------------------+
+| **Safari**:  | https://webkit.org/blog/6900/webdriver-support-in-safari-10/          |
++--------------+-----------------------------------------------------------------------+
+
+Example 0:
+==========
+
+* open a new Firefox browser
+* load the page at the given URL
+
+.. code-block:: python
+
+    from selenium import webdriver
+
+    browser = webdriver.Firefox()
+    browser.get('http://seleniumhq.org/')
+
+Example 1:
+==========
+
+* open a new Firefox browser
+* load the Yahoo homepage
+* search for "seleniumhq"
+* close the browser
+
+.. code-block:: python
+
+    from selenium import webdriver
+    from selenium.webdriver.common.keys import Keys
+
+    browser = webdriver.Firefox()
+
+    browser.get('http://www.yahoo.com')
+    assert 'Yahoo' in browser.title
+
+    elem = browser.find_element_by_name('p')  # Find the search box
+    elem.send_keys('seleniumhq' + Keys.RETURN)
+
+    browser.quit()
+
+Example 2:
+==========
+
+Selenium WebDriver is often used as a basis for testing web applications.  Here is a simple example using Python's standard `unittest <http://docs.python.org/3/library/unittest.html>`_ library:
+
+.. code-block:: python
+
+    import unittest
+    from selenium import webdriver
+
+    class GoogleTestCase(unittest.TestCase):
+
+        def setUp(self):
+            self.browser = webdriver.Firefox()
+            self.addCleanup(self.browser.quit)
+
+        def testPageTitle(self):
+            self.browser.get('http://www.google.com')
+            self.assertIn('Google', self.browser.title)
+
+    if __name__ == '__main__':
+        unittest.main(verbosity=2)
+
+Selenium Server (optional)
+==========================
+
+For normal WebDriver scripts (non-Remote), the Java server is not needed.
+
+However, to use Selenium Webdriver Remote or the legacy Selenium API (Selenium-RC), you need to also run the Selenium server.  The server requires a Java Runtime Environment (JRE).
+
+Download the server separately, from: http://selenium-release.storage.googleapis.com/4.0/selenium-server-standalone-4.0.0.jar
+
+Run the server from the command line::
+
+    java -jar selenium-server-standalone-3.141.0.jar
+
+Then run your Python client scripts.
+
+Use The Source Luke!
+====================
+
+View source code online:
+
++-----------+-------------------------------------------------------+
+| official: | https://github.com/SeleniumHQ/selenium/tree/master/py |
++-----------+-------------------------------------------------------+
diff --git a/docs/source/webdriver/selenium.webdriver.common.action_chains.rst b/docs/source/webdriver/selenium.webdriver.common.action_chains.rst
new file mode 100644
index 00000000..07fff7b0
--- /dev/null
+++ b/docs/source/webdriver/selenium.webdriver.common.action_chains.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.common.action_chains
+=======================================
+
+.. automodule:: selenium.webdriver.common.action_chains
diff --git a/docs/source/webdriver/selenium.webdriver.common.alert.rst b/docs/source/webdriver/selenium.webdriver.common.alert.rst
new file mode 100644
index 00000000..ac884381
--- /dev/null
+++ b/docs/source/webdriver/selenium.webdriver.common.alert.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.common.alert
+===============================
+
+.. automodule:: selenium.webdriver.common.alert
diff --git a/docs/source/webdriver/selenium.webdriver.common.by.rst b/docs/source/webdriver/selenium.webdriver.common.by.rst
new file mode 100644
index 00000000..cdf69417
--- /dev/null
+++ b/docs/source/webdriver/selenium.webdriver.common.by.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.common.by
+============================
+
+.. automodule:: selenium.webdriver.common.by
diff --git a/docs/source/webdriver/selenium.webdriver.common.desired_capabilities.rst b/docs/source/webdriver/selenium.webdriver.common.desired_capabilities.rst
new file mode 100644
index 00000000..30fdf17c
--- /dev/null
+++ b/docs/source/webdriver/selenium.webdriver.common.desired_capabilities.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.common.desired_capabilities
+==============================================
+
+.. automodule:: selenium.webdriver.common.desired_capabilities
diff --git a/docs/source/webdriver/selenium.webdriver.common.html5.application_cache.rst b/docs/source/webdriver/selenium.webdriver.common.html5.application_cache.rst
new file mode 100644
index 00000000..e2e06394
--- /dev/null
+++ b/docs/source/webdriver/selenium.webdriver.common.html5.application_cache.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.common.html5.application_cache
+====================================================
+
+.. automodule:: selenium.webdriver.common.html5.application_cache
diff --git a/docs/source/webdriver/selenium.webdriver.common.keys.rst b/docs/source/webdriver/selenium.webdriver.common.keys.rst
new file mode 100644
index 00000000..1704a17c
--- /dev/null
+++ b/docs/source/webdriver/selenium.webdriver.common.keys.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.common.keys
+==============================
+
+.. automodule:: selenium.webdriver.common.keys
diff --git a/docs/source/webdriver/selenium.webdriver.common.proxy.rst b/docs/source/webdriver/selenium.webdriver.common.proxy.rst
new file mode 100644
index 00000000..06a77f1b
--- /dev/null
+++ b/docs/source/webdriver/selenium.webdriver.common.proxy.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.common.proxy
+===============================
+
+.. automodule:: selenium.webdriver.common.proxy
diff --git a/docs/source/webdriver/selenium.webdriver.common.service.rst b/docs/source/webdriver/selenium.webdriver.common.service.rst
new file mode 100644
index 00000000..e313ced3
--- /dev/null
+++ b/docs/source/webdriver/selenium.webdriver.common.service.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.common.service
+=================================
+
+.. automodule:: selenium.webdriver.common.service
diff --git a/docs/source/webdriver/selenium.webdriver.common.touch_actions.rst b/docs/source/webdriver/selenium.webdriver.common.touch_actions.rst
new file mode 100644
index 00000000..7bc9aa64
--- /dev/null
+++ b/docs/source/webdriver/selenium.webdriver.common.touch_actions.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.common.touch_actions
+=======================================
+
+.. automodule:: selenium.webdriver.common.touch_actions
diff --git a/docs/source/webdriver/selenium.webdriver.common.utils.rst b/docs/source/webdriver/selenium.webdriver.common.utils.rst
new file mode 100644
index 00000000..f30250a6
--- /dev/null
+++ b/docs/source/webdriver/selenium.webdriver.common.utils.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.common.utils
+===============================
+
+.. automodule:: selenium.webdriver.common.utils
diff --git a/docs/source/webdriver_android/selenium.webdriver.android.webdriver.rst b/docs/source/webdriver_android/selenium.webdriver.android.webdriver.rst
new file mode 100644
index 00000000..a7693d9d
--- /dev/null
+++ b/docs/source/webdriver_android/selenium.webdriver.android.webdriver.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.android.webdriver
+====================================
+
+.. automodule:: selenium.webdriver.android.webdriver
diff --git a/docs/source/webdriver_chrome/selenium.webdriver.chrome.options.rst b/docs/source/webdriver_chrome/selenium.webdriver.chrome.options.rst
new file mode 100644
index 00000000..9a6e42a9
--- /dev/null
+++ b/docs/source/webdriver_chrome/selenium.webdriver.chrome.options.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.chrome.options
+=================================
+
+.. automodule:: selenium.webdriver.chrome.options
diff --git a/docs/source/webdriver_chrome/selenium.webdriver.chrome.service.rst b/docs/source/webdriver_chrome/selenium.webdriver.chrome.service.rst
new file mode 100644
index 00000000..ffe8b20f
--- /dev/null
+++ b/docs/source/webdriver_chrome/selenium.webdriver.chrome.service.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.chrome.service
+=================================
+
+.. automodule:: selenium.webdriver.chrome.service
diff --git a/docs/source/webdriver_chrome/selenium.webdriver.chrome.webdriver.rst b/docs/source/webdriver_chrome/selenium.webdriver.chrome.webdriver.rst
new file mode 100644
index 00000000..7f542536
--- /dev/null
+++ b/docs/source/webdriver_chrome/selenium.webdriver.chrome.webdriver.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.chrome.webdriver
+===================================
+
+.. automodule:: selenium.webdriver.chrome.webdriver
diff --git a/docs/source/webdriver_firefox/selenium.webdriver.firefox.extension_connection.rst b/docs/source/webdriver_firefox/selenium.webdriver.firefox.extension_connection.rst
new file mode 100644
index 00000000..413badac
--- /dev/null
+++ b/docs/source/webdriver_firefox/selenium.webdriver.firefox.extension_connection.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.firefox.extension_connection
+===============================================
+
+.. automodule:: selenium.webdriver.firefox.extension_connection
diff --git a/docs/source/webdriver_firefox/selenium.webdriver.firefox.firefox_binary.rst b/docs/source/webdriver_firefox/selenium.webdriver.firefox.firefox_binary.rst
new file mode 100644
index 00000000..e0b04b18
--- /dev/null
+++ b/docs/source/webdriver_firefox/selenium.webdriver.firefox.firefox_binary.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.firefox.firefox_binary
+=========================================
+
+.. automodule:: selenium.webdriver.firefox.firefox_binary
diff --git a/docs/source/webdriver_firefox/selenium.webdriver.firefox.firefox_profile.rst b/docs/source/webdriver_firefox/selenium.webdriver.firefox.firefox_profile.rst
new file mode 100644
index 00000000..78ba9499
--- /dev/null
+++ b/docs/source/webdriver_firefox/selenium.webdriver.firefox.firefox_profile.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.firefox.firefox_profile
+==========================================
+
+.. automodule:: selenium.webdriver.firefox.firefox_profile
diff --git a/docs/source/webdriver_firefox/selenium.webdriver.firefox.options.rst b/docs/source/webdriver_firefox/selenium.webdriver.firefox.options.rst
new file mode 100644
index 00000000..a64b990a
--- /dev/null
+++ b/docs/source/webdriver_firefox/selenium.webdriver.firefox.options.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.firefox.options
+==================================
+
+.. automodule:: selenium.webdriver.firefox.options
diff --git a/docs/source/webdriver_firefox/selenium.webdriver.firefox.webdriver.rst b/docs/source/webdriver_firefox/selenium.webdriver.firefox.webdriver.rst
new file mode 100644
index 00000000..e9bfe53e
--- /dev/null
+++ b/docs/source/webdriver_firefox/selenium.webdriver.firefox.webdriver.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.firefox.webdriver
+====================================
+
+.. automodule:: selenium.webdriver.firefox.webdriver
diff --git a/docs/source/webdriver_ie/selenium.webdriver.ie.webdriver.rst b/docs/source/webdriver_ie/selenium.webdriver.ie.webdriver.rst
new file mode 100644
index 00000000..3623813c
--- /dev/null
+++ b/docs/source/webdriver_ie/selenium.webdriver.ie.webdriver.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.ie.webdriver
+===============================
+
+.. automodule:: selenium.webdriver.ie.webdriver
diff --git a/docs/source/webdriver_opera/selenium.webdriver.opera.webdriver.rst b/docs/source/webdriver_opera/selenium.webdriver.opera.webdriver.rst
new file mode 100644
index 00000000..ddabbc36
--- /dev/null
+++ b/docs/source/webdriver_opera/selenium.webdriver.opera.webdriver.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.opera.webdriver
+==================================
+
+.. automodule:: selenium.webdriver.opera.webdriver
diff --git a/docs/source/webdriver_phantomjs/selenium.webdriver.phantomjs.service.rst b/docs/source/webdriver_phantomjs/selenium.webdriver.phantomjs.service.rst
new file mode 100644
index 00000000..8d6e5ff7
--- /dev/null
+++ b/docs/source/webdriver_phantomjs/selenium.webdriver.phantomjs.service.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.phantomjs.service
+====================================
+
+.. automodule:: selenium.webdriver.phantomjs.service
diff --git a/docs/source/webdriver_phantomjs/selenium.webdriver.phantomjs.webdriver.rst b/docs/source/webdriver_phantomjs/selenium.webdriver.phantomjs.webdriver.rst
new file mode 100644
index 00000000..5b3ab482
--- /dev/null
+++ b/docs/source/webdriver_phantomjs/selenium.webdriver.phantomjs.webdriver.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.phantomjs.webdriver
+======================================
+
+.. automodule:: selenium.webdriver.phantomjs.webdriver
diff --git a/docs/source/webdriver_remote/selenium.webdriver.remote.command.rst b/docs/source/webdriver_remote/selenium.webdriver.remote.command.rst
new file mode 100644
index 00000000..ff977b74
--- /dev/null
+++ b/docs/source/webdriver_remote/selenium.webdriver.remote.command.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.remote.command
+=================================
+
+.. automodule:: selenium.webdriver.remote.command
diff --git a/docs/source/webdriver_remote/selenium.webdriver.remote.errorhandler.rst b/docs/source/webdriver_remote/selenium.webdriver.remote.errorhandler.rst
new file mode 100644
index 00000000..50028874
--- /dev/null
+++ b/docs/source/webdriver_remote/selenium.webdriver.remote.errorhandler.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.remote.errorhandler
+======================================
+
+.. automodule:: selenium.webdriver.remote.errorhandler
diff --git a/docs/source/webdriver_remote/selenium.webdriver.remote.mobile.rst b/docs/source/webdriver_remote/selenium.webdriver.remote.mobile.rst
new file mode 100644
index 00000000..dc6ab4ab
--- /dev/null
+++ b/docs/source/webdriver_remote/selenium.webdriver.remote.mobile.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.remote.mobile
+================================
+
+.. automodule:: selenium.webdriver.remote.mobile
diff --git a/docs/source/webdriver_remote/selenium.webdriver.remote.remote_connection.rst b/docs/source/webdriver_remote/selenium.webdriver.remote.remote_connection.rst
new file mode 100644
index 00000000..88d13894
--- /dev/null
+++ b/docs/source/webdriver_remote/selenium.webdriver.remote.remote_connection.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.remote.remote_connection
+===========================================
+
+.. automodule:: selenium.webdriver.remote.remote_connection
diff --git a/docs/source/webdriver_remote/selenium.webdriver.remote.utils.rst b/docs/source/webdriver_remote/selenium.webdriver.remote.utils.rst
new file mode 100644
index 00000000..e035f43a
--- /dev/null
+++ b/docs/source/webdriver_remote/selenium.webdriver.remote.utils.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.remote.utils
+===============================
+
+.. automodule:: selenium.webdriver.remote.utils
diff --git a/docs/source/webdriver_remote/selenium.webdriver.remote.webdriver.rst b/docs/source/webdriver_remote/selenium.webdriver.remote.webdriver.rst
new file mode 100644
index 00000000..b390c196
--- /dev/null
+++ b/docs/source/webdriver_remote/selenium.webdriver.remote.webdriver.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.remote.webdriver
+===================================
+
+.. automodule:: selenium.webdriver.remote.webdriver
diff --git a/docs/source/webdriver_remote/selenium.webdriver.remote.webelement.rst b/docs/source/webdriver_remote/selenium.webdriver.remote.webelement.rst
new file mode 100644
index 00000000..7b6ab4de
--- /dev/null
+++ b/docs/source/webdriver_remote/selenium.webdriver.remote.webelement.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.remote.webelement
+====================================
+
+.. automodule:: selenium.webdriver.remote.webelement
diff --git a/docs/source/webdriver_safari/selenium.webdriver.safari.service.rst b/docs/source/webdriver_safari/selenium.webdriver.safari.service.rst
new file mode 100644
index 00000000..de44909b
--- /dev/null
+++ b/docs/source/webdriver_safari/selenium.webdriver.safari.service.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.safari.service
+=================================
+
+.. automodule:: selenium.webdriver.safari.service
diff --git a/docs/source/webdriver_safari/selenium.webdriver.safari.webdriver.rst b/docs/source/webdriver_safari/selenium.webdriver.safari.webdriver.rst
new file mode 100644
index 00000000..f3b649ed
--- /dev/null
+++ b/docs/source/webdriver_safari/selenium.webdriver.safari.webdriver.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.safari.webdriver
+===================================
+
+.. automodule:: selenium.webdriver.safari.webdriver
diff --git a/docs/source/webdriver_support/selenium.webdriver.support.abstract_event_listener.rst b/docs/source/webdriver_support/selenium.webdriver.support.abstract_event_listener.rst
new file mode 100644
index 00000000..53828ad5
--- /dev/null
+++ b/docs/source/webdriver_support/selenium.webdriver.support.abstract_event_listener.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.support.abstract_event_listener
+==================================================
+
+.. automodule:: selenium.webdriver.support.abstract_event_listener
diff --git a/docs/source/webdriver_support/selenium.webdriver.support.color.rst b/docs/source/webdriver_support/selenium.webdriver.support.color.rst
new file mode 100644
index 00000000..44aa61cf
--- /dev/null
+++ b/docs/source/webdriver_support/selenium.webdriver.support.color.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.support.color
+================================
+
+.. automodule:: selenium.webdriver.support.color
diff --git a/docs/source/webdriver_support/selenium.webdriver.support.event_firing_webdriver.rst b/docs/source/webdriver_support/selenium.webdriver.support.event_firing_webdriver.rst
new file mode 100644
index 00000000..abbeacd0
--- /dev/null
+++ b/docs/source/webdriver_support/selenium.webdriver.support.event_firing_webdriver.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.support.event_firing_webdriver
+=================================================
+
+.. automodule:: selenium.webdriver.support.event_firing_webdriver
diff --git a/docs/source/webdriver_support/selenium.webdriver.support.expected_conditions.rst b/docs/source/webdriver_support/selenium.webdriver.support.expected_conditions.rst
new file mode 100644
index 00000000..46417a9b
--- /dev/null
+++ b/docs/source/webdriver_support/selenium.webdriver.support.expected_conditions.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.support.expected_conditions
+==============================================
+
+.. automodule:: selenium.webdriver.support.expected_conditions
diff --git a/docs/source/webdriver_support/selenium.webdriver.support.select.rst b/docs/source/webdriver_support/selenium.webdriver.support.select.rst
new file mode 100644
index 00000000..02b0eb0b
--- /dev/null
+++ b/docs/source/webdriver_support/selenium.webdriver.support.select.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.support.select
+=================================
+
+.. automodule:: selenium.webdriver.support.select
diff --git a/docs/source/webdriver_support/selenium.webdriver.support.wait.rst b/docs/source/webdriver_support/selenium.webdriver.support.wait.rst
new file mode 100644
index 00000000..f42089be
--- /dev/null
+++ b/docs/source/webdriver_support/selenium.webdriver.support.wait.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.support.wait
+===============================
+
+.. automodule:: selenium.webdriver.support.wait
diff --git a/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.options.rst b/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.options.rst
new file mode 100644
index 00000000..192eb79d
--- /dev/null
+++ b/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.options.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.webkitgtk.options
+====================================
+
+.. automodule:: selenium.webdriver.webkitgtk.options
diff --git a/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.service.rst b/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.service.rst
new file mode 100644
index 00000000..81497e01
--- /dev/null
+++ b/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.service.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.webkitgtk.service
+====================================
+
+.. automodule:: selenium.webdriver.webkitgtk.service
diff --git a/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.webdriver.rst b/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.webdriver.rst
new file mode 100644
index 00000000..c819a4a9
--- /dev/null
+++ b/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.webdriver.rst
@@ -0,0 +1,4 @@
+selenium.webdriver.webkitgtk.webdriver
+======================================
+
+.. automodule:: selenium.webdriver.webkitgtk.webdriver
diff --git a/python.iml b/python.iml
new file mode 100644
index 00000000..3c8cbb09
--- /dev/null
+++ b/python.iml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+  <component name="FacetManager">
+    <facet type="Python" name="Python">
+      <configuration sdkName="" />
+    </facet>
+  </component>
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/selenium" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
+    </content>
+    <orderEntry type="jdk" jdkName="Python 2.7" jdkType="Python SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+  <component name="sonarModuleSettings">
+    <option name="alternativeWorkingDirPath" value="" />
+    <option name="localAnalysisScripName" value="&lt;PROJECT&gt;" />
+    <option name="serverName" value="&lt;PROJECT&gt;" />
+    <option name="useAlternativeWorkingDir" value="false" />
+    <option name="workingDirSelection" value="&lt;MODULE&gt;" />
+  </component>
+</module>
\ No newline at end of file
diff --git a/selenium/__init__.py b/selenium/__init__.py
new file mode 100644
index 00000000..c4a089da
--- /dev/null
+++ b/selenium/__init__.py
@@ -0,0 +1,19 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+__version__ = "4.0.0a1"
diff --git a/selenium/common/__init__.py b/selenium/common/__init__.py
new file mode 100644
index 00000000..ea71319a
--- /dev/null
+++ b/selenium/common/__init__.py
@@ -0,0 +1,18 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from . import exceptions  # noqa
diff --git a/selenium/common/exceptions.py b/selenium/common/exceptions.py
new file mode 100644
index 00000000..1b151216
--- /dev/null
+++ b/selenium/common/exceptions.py
@@ -0,0 +1,315 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Exceptions that may happen in all the webdriver code.
+"""
+
+
+class WebDriverException(Exception):
+    """
+    Base webdriver exception.
+    """
+
+    def __init__(self, msg=None, screen=None, stacktrace=None):
+        self.msg = msg
+        self.screen = screen
+        self.stacktrace = stacktrace
+
+    def __str__(self):
+        exception_msg = "Message: %s\n" % self.msg
+        if self.screen is not None:
+            exception_msg += "Screenshot: available via screen\n"
+        if self.stacktrace is not None:
+            stacktrace = "\n".join(self.stacktrace)
+            exception_msg += "Stacktrace:\n%s" % stacktrace
+        return exception_msg
+
+
+class InvalidSwitchToTargetException(WebDriverException):
+    """
+    Thrown when frame or window target to be switched doesn't exist.
+    """
+    pass
+
+
+class NoSuchFrameException(InvalidSwitchToTargetException):
+    """
+    Thrown when frame target to be switched doesn't exist.
+    """
+    pass
+
+
+class NoSuchWindowException(InvalidSwitchToTargetException):
+    """
+    Thrown when window target to be switched doesn't exist.
+
+    To find the current set of active window handles, you can get a list
+    of the active window handles in the following way::
+
+        print driver.window_handles
+
+    """
+    pass
+
+
+class NoSuchElementException(WebDriverException):
+    """
+    Thrown when element could not be found.
+
+    If you encounter this exception, you may want to check the following:
+        * Check your selector used in your find_by...
+        * Element may not yet be on the screen at the time of the find operation,
+          (webpage is still loading) see selenium.webdriver.support.wait.WebDriverWait()
+          for how to write a wait wrapper to wait for an element to appear.
+    """
+    pass
+
+
+class NoSuchAttributeException(WebDriverException):
+    """
+    Thrown when the attribute of element could not be found.
+
+    You may want to check if the attribute exists in the particular browser you are
+    testing against.  Some browsers may have different property names for the same
+    property.  (IE8's .innerText vs. Firefox .textContent)
+    """
+    pass
+
+
+class StaleElementReferenceException(WebDriverException):
+    """
+    Thrown when a reference to an element is now "stale".
+
+    Stale means the element no longer appears on the DOM of the page.
+
+
+    Possible causes of StaleElementReferenceException include, but not limited to:
+        * You are no longer on the same page, or the page may have refreshed since the element
+          was located.
+        * The element may have been removed and re-added to the screen, since it was located.
+          Such as an element being relocated.
+          This can happen typically with a javascript framework when values are updated and the
+          node is rebuilt.
+        * Element may have been inside an iframe or another context which was refreshed.
+    """
+    pass
+
+
+class InvalidElementStateException(WebDriverException):
+    """
+    Thrown when a command could not be completed because the element is in an invalid state.
+
+    This can be caused by attempting to clear an element that isn't both editable and resettable.
+    """
+    pass
+
+
+class UnexpectedAlertPresentException(WebDriverException):
+    """
+    Thrown when an unexpected alert has appeared.
+
+    Usually raised when  an unexpected modal is blocking the webdriver from executing
+    commands.
+    """
+    def __init__(self, msg=None, screen=None, stacktrace=None, alert_text=None):
+        super(UnexpectedAlertPresentException, self).__init__(msg, screen, stacktrace)
+        self.alert_text = alert_text
+
+    def __str__(self):
+        return "Alert Text: %s\n%s" % (self.alert_text, super(UnexpectedAlertPresentException, self).__str__())
+
+
+class NoAlertPresentException(WebDriverException):
+    """
+    Thrown when switching to no presented alert.
+
+    This can be caused by calling an operation on the Alert() class when an alert is
+    not yet on the screen.
+    """
+    pass
+
+
+class ElementNotVisibleException(InvalidElementStateException):
+    """
+    Thrown when an element is present on the DOM, but
+    it is not visible, and so is not able to be interacted with.
+
+    Most commonly encountered when trying to click or read text
+    of an element that is hidden from view.
+    """
+    pass
+
+
+class ElementNotInteractableException(InvalidElementStateException):
+    """
+    Thrown when an element is present in the DOM but interactions
+    with that element will hit another element do to paint order
+    """
+    pass
+
+
+class ElementNotSelectableException(InvalidElementStateException):
+    """
+    Thrown when trying to select an unselectable element.
+
+    For example, selecting a 'script' element.
+    """
+    pass
+
+
+class InvalidCookieDomainException(WebDriverException):
+    """
+    Thrown when attempting to add a cookie under a different domain
+    than the current URL.
+    """
+    pass
+
+
+class UnableToSetCookieException(WebDriverException):
+    """
+    Thrown when a driver fails to set a cookie.
+    """
+    pass
+
+
+class RemoteDriverServerException(WebDriverException):
+    """
+    """
+    pass
+
+
+class TimeoutException(WebDriverException):
+    """
+    Thrown when a command does not complete in enough time.
+    """
+    pass
+
+
+class MoveTargetOutOfBoundsException(WebDriverException):
+    """
+    Thrown when the target provided to the `ActionsChains` move()
+    method is invalid, i.e. out of document.
+    """
+    pass
+
+
+class UnexpectedTagNameException(WebDriverException):
+    """
+    Thrown when a support class did not get an expected web element.
+    """
+    pass
+
+
+class InvalidSelectorException(NoSuchElementException):
+    """
+    Thrown when the selector which is used to find an element does not return
+    a WebElement. Currently this only happens when the selector is an xpath
+    expression and it is either syntactically invalid (i.e. it is not a
+    xpath expression) or the expression does not select WebElements
+    (e.g. "count(//input)").
+    """
+    pass
+
+
+class ImeNotAvailableException(WebDriverException):
+    """
+    Thrown when IME support is not available. This exception is thrown for every IME-related
+    method call if IME support is not available on the machine.
+    """
+    pass
+
+
+class ImeActivationFailedException(WebDriverException):
+    """
+    Thrown when activating an IME engine has failed.
+    """
+    pass
+
+
+class InvalidArgumentException(WebDriverException):
+    """
+    The arguments passed to a command are either invalid or malformed.
+    """
+    pass
+
+
+class JavascriptException(WebDriverException):
+    """
+    An error occurred while executing JavaScript supplied by the user.
+    """
+    pass
+
+
+class NoSuchCookieException(WebDriverException):
+    """
+    No cookie matching the given path name was found amongst the associated cookies of the
+    current browsing context's active document.
+    """
+    pass
+
+
+class ScreenshotException(WebDriverException):
+    """
+    A screen capture was made impossible.
+    """
+    pass
+
+
+class ElementClickInterceptedException(WebDriverException):
+    """
+    The Element Click command could not be completed because the element receiving the events
+    is obscuring the element that was requested clicked.
+    """
+    pass
+
+
+class InsecureCertificateException(WebDriverException):
+    """
+    Navigation caused the user agent to hit a certificate warning, which is usually the result
+    of an expired or invalid TLS certificate.
+    """
+    pass
+
+
+class InvalidCoordinatesException(WebDriverException):
+    """
+    The coordinates provided to an interactions operation are invalid.
+    """
+    pass
+
+
+class InvalidSessionIdException(WebDriverException):
+    """
+    Occurs if the given session id is not in the list of active sessions, meaning the session
+    either does not exist or that it's not active.
+    """
+    pass
+
+
+class SessionNotCreatedException(WebDriverException):
+    """
+    A new session could not be created.
+    """
+    pass
+
+
+class UnknownMethodException(WebDriverException):
+    """
+    The requested command matched a known URL but did not match an method for that URL.
+    """
+    pass
diff --git a/selenium/webdriver/__init__.py b/selenium/webdriver/__init__.py
new file mode 100644
index 00000000..b0f17911
--- /dev/null
+++ b/selenium/webdriver/__init__.py
@@ -0,0 +1,39 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from .firefox.webdriver import WebDriver as Firefox  # noqa
+from .firefox.firefox_profile import FirefoxProfile  # noqa
+from .firefox.options import Options as FirefoxOptions  # noqa
+from .chrome.webdriver import WebDriver as Chrome  # noqa
+from .chrome.options import Options as ChromeOptions  # noqa
+from .ie.webdriver import WebDriver as Ie  # noqa
+from .ie.options import Options as IeOptions  # noqa
+from .edge.webdriver import WebDriver as Edge  # noqa
+from .opera.webdriver import WebDriver as Opera  # noqa
+from .safari.webdriver import WebDriver as Safari  # noqa
+from .blackberry.webdriver import WebDriver as BlackBerry  # noqa
+from .phantomjs.webdriver import WebDriver as PhantomJS  # noqa
+from .android.webdriver import WebDriver as Android  # noqa
+from .webkitgtk.webdriver import WebDriver as WebKitGTK # noqa
+from .webkitgtk.options import Options as WebKitGTKOptions # noqa
+from .remote.webdriver import WebDriver as Remote  # noqa
+from .common.desired_capabilities import DesiredCapabilities  # noqa
+from .common.action_chains import ActionChains  # noqa
+from .common.touch_actions import TouchActions  # noqa
+from .common.proxy import Proxy  # noqa
+
+__version__ = '4.0.0a1'
diff --git a/selenium/webdriver/android/__init__.py b/selenium/webdriver/android/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/android/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/android/webdriver.py b/selenium/webdriver/android/webdriver.py
new file mode 100644
index 00000000..68a55c27
--- /dev/null
+++ b/selenium/webdriver/android/webdriver.py
@@ -0,0 +1,42 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+
+
+class WebDriver(RemoteWebDriver):
+    """
+    Simple RemoteWebDriver wrapper to start connect to Selendroid's WebView app
+
+    For more info on getting started with Selendroid
+    http://selendroid.io/mobileWeb.html
+    """
+
+    def __init__(self, host="localhost", port=4444, desired_capabilities=DesiredCapabilities.ANDROID):
+        """
+        Creates a new instance of Selendroid using the WebView app
+
+        :Args:
+         - host - location of where selendroid is running
+         - port - port that selendroid is running on
+         - desired_capabilities: Dictionary object with capabilities
+        """
+        RemoteWebDriver.__init__(
+            self,
+            command_executor="http://%s:%d/wd/hub" % (host, port),
+            desired_capabilities=desired_capabilities)
diff --git a/selenium/webdriver/blackberry/__init__.py b/selenium/webdriver/blackberry/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/blackberry/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/blackberry/webdriver.py b/selenium/webdriver/blackberry/webdriver.py
new file mode 100644
index 00000000..cded6e7d
--- /dev/null
+++ b/selenium/webdriver/blackberry/webdriver.py
@@ -0,0 +1,121 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import os
+import platform
+import subprocess
+
+try:
+    import http.client as http_client
+except ImportError:
+    import httplib as http_client
+
+from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.support.ui import WebDriverWait
+
+LOAD_TIMEOUT = 5
+
+
+class WebDriver(RemoteWebDriver):
+    """
+    Controls the BlackBerry Browser and allows you to drive it.
+
+    :Args:
+     - device_password - password for the BlackBerry device or emulator you are
+       trying to drive
+     - bb_tools_dir path to the blackberry-deploy executable. If the default
+       is used it assumes it is in the $PATH
+     - hostip - the ip for the device you are trying to drive. Falls back to
+       169.254.0.1 which is the default ip used
+     - port - the port being used for WebDriver on device. defaults to 1338
+     - desired_capabilities: Dictionary object with non-browser specific
+       capabilities only, such as "proxy" or "loggingPref".
+
+    Note: To get blackberry-deploy you will need to install the BlackBerry
+          WebWorks SDK - the default install will put it in the $PATH for you.
+          Download at https://developer.blackberry.com/html5/downloads/
+    """
+    def __init__(self, device_password, bb_tools_dir=None,
+                 hostip='169.254.0.1', port=1338, desired_capabilities={}):
+        import warnings
+        warnings.warn('BlackBerry Driver is no longer supported and will be '
+                      'removed in future versions',
+                      DeprecationWarning, stacklevel=2)
+
+        remote_addr = 'http://{}:{}'.format(hostip, port)
+
+        filename = 'blackberry-deploy'
+        if platform.system() == "Windows":
+            filename += '.bat'
+
+        if bb_tools_dir is not None:
+            if os.path.isdir(bb_tools_dir):
+                bb_deploy_location = os.path.join(bb_tools_dir, filename)
+                if not os.path.isfile(bb_deploy_location):
+                    raise WebDriverException('Invalid blackberry-deploy location: {}'.format(bb_deploy_location))
+            else:
+                raise WebDriverException('Invalid blackberry tools location, must be a directory: {}'.format(bb_tools_dir))
+        else:
+            bb_deploy_location = filename
+
+        """
+        Now launch the BlackBerry browser before allowing anything else to run.
+        """
+        try:
+            launch_args = [bb_deploy_location,
+                           '-launchApp',
+                           str(hostip),
+                           '-package-name', 'sys.browser',
+                           '-package-id', 'gYABgJYFHAzbeFMPCCpYWBtHAm0',
+                           '-password', str(device_password)]
+
+            with open(os.devnull, 'w') as fp:
+                p = subprocess.Popen(launch_args, stdout=fp)
+
+            returncode = p.wait()
+
+            if returncode == 0:
+                # wait for the BlackBerry10 browser to load.
+                is_running_args = [bb_deploy_location,
+                                   '-isAppRunning',
+                                   str(hostip),
+                                   '-package-name', 'sys.browser',
+                                   '-package-id', 'gYABgJYFHAzbeFMPCCpYWBtHAm0',
+                                   '-password', str(device_password)]
+
+                WebDriverWait(None, LOAD_TIMEOUT)\
+                    .until(lambda x: subprocess.check_output(is_running_args)
+                           .find('result::true'),
+                           message='waiting for BlackBerry10 browser to load')
+
+                RemoteWebDriver.__init__(self,
+                                         command_executor=remote_addr,
+                                         desired_capabilities=desired_capabilities)
+            else:
+                raise WebDriverException('blackberry-deploy failed to launch browser')
+        except Exception as e:
+            raise WebDriverException('Something went wrong launching blackberry-deploy', stacktrace=getattr(e, 'stacktrace', None))
+
+    def quit(self):
+        """
+        Closes the browser and shuts down the
+        """
+        try:
+            RemoteWebDriver.quit(self)
+        except http_client.BadStatusLine:
+            pass
diff --git a/selenium/webdriver/chrome/__init__.py b/selenium/webdriver/chrome/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/chrome/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/chrome/options.py b/selenium/webdriver/chrome/options.py
new file mode 100644
index 00000000..c5ec4180
--- /dev/null
+++ b/selenium/webdriver/chrome/options.py
@@ -0,0 +1,177 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import base64
+import os
+
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+from selenium.webdriver.common.options import ArgOptions
+
+
+class Options(ArgOptions):
+    KEY = "goog:chromeOptions"
+
+    def __init__(self):
+        super(Options, self).__init__()
+        self._binary_location = ''
+        self._extension_files = []
+        self._extensions = []
+        self._experimental_options = {}
+        self._debugger_address = None
+
+    @property
+    def binary_location(self):
+        """
+        :Returns: The location of the binary, otherwise an empty string
+        """
+        return self._binary_location
+
+    @binary_location.setter
+    def binary_location(self, value):
+        """
+        Allows you to set where the chromium binary lives
+
+        :Args:
+         - value: path to the Chromium binary
+        """
+        self._binary_location = value
+
+    @property
+    def debugger_address(self):
+        """
+        :Returns: The address of the remote devtools instance
+        """
+        return self._debugger_address
+
+    @debugger_address.setter
+    def debugger_address(self, value):
+        """
+        Allows you to set the address of the remote devtools instance
+        that the ChromeDriver instance will try to connect to during an
+        active wait.
+
+        :Args:
+         - value: address of remote devtools instance if any (hostname[:port])
+        """
+        self._debugger_address = value
+
+    @property
+    def extensions(self):
+        """
+        :Returns: A list of encoded extensions that will be loaded into chrome
+        """
+        encoded_extensions = []
+        for ext in self._extension_files:
+            file_ = open(ext, 'rb')
+            # Should not use base64.encodestring() which inserts newlines every
+            # 76 characters (per RFC 1521).  Chromedriver has to remove those
+            # unnecessary newlines before decoding, causing performance hit.
+            encoded_extensions.append(base64.b64encode(file_.read()).decode('UTF-8'))
+
+            file_.close()
+        return encoded_extensions + self._extensions
+
+    def add_extension(self, extension):
+        """
+        Adds the path to the extension to a list that will be used to extract it
+        to the ChromeDriver
+
+        :Args:
+         - extension: path to the \\*.crx file
+        """
+        if extension:
+            extension_to_add = os.path.abspath(os.path.expanduser(extension))
+            if os.path.exists(extension_to_add):
+                self._extension_files.append(extension_to_add)
+            else:
+                raise IOError("Path to the extension doesn't exist")
+        else:
+            raise ValueError("argument can not be null")
+
+    def add_encoded_extension(self, extension):
+        """
+        Adds Base64 encoded string with extension data to a list that will be used to extract it
+        to the ChromeDriver
+
+        :Args:
+         - extension: Base64 encoded string with extension data
+        """
+        if extension:
+            self._extensions.append(extension)
+        else:
+            raise ValueError("argument can not be null")
+
+    @property
+    def experimental_options(self):
+        """
+        :Returns: A dictionary of experimental options for chrome
+        """
+        return self._experimental_options
+
+    def add_experimental_option(self, name, value):
+        """
+        Adds an experimental option which is passed to chrome.
+
+        :Args:
+          name: The experimental option name.
+          value: The option value.
+        """
+        self._experimental_options[name] = value
+
+    @property
+    def headless(self):
+        """
+        :Returns: True if the headless argument is set, else False
+        """
+        return '--headless' in self._arguments
+
+    @headless.setter
+    def headless(self, value):
+        """
+        Sets the headless argument
+
+        :Args:
+          value: boolean value indicating to set the headless option
+        """
+        args = {'--headless'}
+        if value is True:
+            self._arguments.extend(args)
+        else:
+            self._arguments = list(set(self._arguments) - args)
+
+    def to_capabilities(self):
+        """
+        Creates a capabilities with all the options that have been set
+
+        :Returns: A dictionary with everything
+        """
+        caps = self._caps
+        chrome_options = self.experimental_options.copy()
+        chrome_options["extensions"] = self.extensions
+        if self.binary_location:
+            chrome_options["binary"] = self.binary_location
+        chrome_options["args"] = self.arguments
+        if self.debugger_address:
+            chrome_options["debuggerAddress"] = self.debugger_address
+
+        caps[self.KEY] = chrome_options
+
+        return caps
+
+    @property
+    def default_capabilities(self):
+        return DesiredCapabilities.CHROME.copy()
diff --git a/selenium/webdriver/chrome/remote_connection.py b/selenium/webdriver/chrome/remote_connection.py
new file mode 100644
index 00000000..d8408a08
--- /dev/null
+++ b/selenium/webdriver/chrome/remote_connection.py
@@ -0,0 +1,33 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.remote.remote_connection import RemoteConnection
+
+
+class ChromeRemoteConnection(RemoteConnection):
+
+    def __init__(self, remote_server_addr, keep_alive=True):
+        RemoteConnection.__init__(self, remote_server_addr, keep_alive)
+        self._commands["launchApp"] = ('POST', '/session/$sessionId/chromium/launch_app')
+        self._commands["setNetworkConditions"] = ('POST', '/session/$sessionId/chromium/network_conditions')
+        self._commands["getNetworkConditions"] = ('GET', '/session/$sessionId/chromium/network_conditions')
+        self._commands['executeCdpCommand'] = ('POST', '/session/$sessionId/goog/cdp/execute')
+        self._commands['getSinks'] = ('GET', '/session/$sessionId/goog/cast/get_sinks')
+        self._commands['getIssueMessage'] = ('GET', '/session/$sessionId/goog/cast/get_issue_message')
+        self._commands['setSinkToUse'] = ('POST', '/session/$sessionId/goog/cast/set_sink_to_use')
+        self._commands['startTabMirroring'] = ('POST', '/session/$sessionId/goog/cast/start_tab_mirroring')
+        self._commands['stopCasting'] = ('POST', '/session/$sessionId/goog/cast/stop_casting')
diff --git a/selenium/webdriver/chrome/service.py b/selenium/webdriver/chrome/service.py
new file mode 100644
index 00000000..5a67b2cb
--- /dev/null
+++ b/selenium/webdriver/chrome/service.py
@@ -0,0 +1,45 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.common import service
+
+
+class Service(service.Service):
+    """
+    Object that manages the starting and stopping of the ChromeDriver
+    """
+
+    def __init__(self, executable_path, port=0, service_args=None,
+                 log_path=None, env=None):
+        """
+        Creates a new instance of the Service
+
+        :Args:
+         - executable_path : Path to the ChromeDriver
+         - port : Port the service is running on
+         - service_args : List of args to pass to the chromedriver service
+         - log_path : Path for the chromedriver service to log to"""
+
+        self.service_args = service_args or []
+        if log_path:
+            self.service_args.append('--log-path=%s' % log_path)
+
+        service.Service.__init__(self, executable_path, port=port, env=env,
+                                 start_error_message="Please see https://sites.google.com/a/chromium.org/chromedriver/home")
+
+    def command_line_args(self):
+        return ["--port=%d" % self.port] + self.service_args
diff --git a/selenium/webdriver/chrome/webdriver.py b/selenium/webdriver/chrome/webdriver.py
new file mode 100644
index 00000000..97d12d13
--- /dev/null
+++ b/selenium/webdriver/chrome/webdriver.py
@@ -0,0 +1,223 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import warnings
+from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
+from .remote_connection import ChromeRemoteConnection
+from .service import Service
+from .options import Options
+
+
+DEFAULT_PORT = 0
+DEFAULT_SERVICE_LOG_PATH = None
+
+
+class WebDriver(RemoteWebDriver):
+    """
+    Controls the ChromeDriver and allows you to drive the browser.
+
+    You will need to download the ChromeDriver executable from
+    http://chromedriver.storage.googleapis.com/index.html
+    """
+
+    def __init__(self, executable_path="chromedriver", port=DEFAULT_PORT,
+                 options=None, service_args=None,
+                 desired_capabilities=None, service_log_path=DEFAULT_SERVICE_LOG_PATH,
+                 chrome_options=None, service=None, keep_alive=True):
+        """
+        Creates a new instance of the chrome driver.
+
+        Starts the service and then creates new instance of chrome driver.
+
+        :Args:
+         - executable_path - Deprecated: path to the executable. If the default is used it assumes the executable is in the $PATH
+         - port - Deprecated: port you would like the service to run, if left as 0, a free port will be found.
+         - options - this takes an instance of ChromeOptions
+         - service_args - Deprecated: List of args to pass to the driver service
+         - desired_capabilities - Deprecated: Dictionary object with non-browser specific
+           capabilities only, such as "proxy" or "loggingPref".
+         - service_log_path - Deprecated: Where to log information from the driver.
+         - keep_alive - Whether to configure ChromeRemoteConnection to use HTTP keep-alive.
+        """
+        if executable_path != 'chromedriver':
+            warnings.warn('executable_path has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        if desired_capabilities is not None:
+            warnings.warn('desired_capabilities has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        if port != DEFAULT_PORT:
+            warnings.warn('port has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        self.port = port
+        if service_log_path != DEFAULT_SERVICE_LOG_PATH:
+            warnings.warn('service_log_path has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+
+        if chrome_options:
+            warnings.warn('use options instead of chrome_options',
+                          DeprecationWarning, stacklevel=2)
+            options = chrome_options
+
+        if options is None:
+            # desired_capabilities stays as passed in
+            if desired_capabilities is None:
+                desired_capabilities = self.create_options().to_capabilities()
+        else:
+            if desired_capabilities is None:
+                desired_capabilities = options.to_capabilities()
+            else:
+                desired_capabilities.update(options.to_capabilities())
+
+        if service:
+            self.service = service
+        else:
+            self.service = Service(
+                executable_path,
+                port=port,
+                service_args=service_args,
+                log_path=service_log_path)
+        self.service.start()
+
+        try:
+            RemoteWebDriver.__init__(
+                self,
+                command_executor=ChromeRemoteConnection(
+                    remote_server_addr=self.service.service_url,
+                    keep_alive=keep_alive),
+                desired_capabilities=desired_capabilities)
+        except Exception:
+            self.quit()
+            raise
+        self._is_remote = False
+
+    def launch_app(self, id):
+        """Launches Chrome app specified by id."""
+        return self.execute("launchApp", {'id': id})
+
+    def get_network_conditions(self):
+        """
+        Gets Chrome network emulation settings.
+
+        :Returns:
+            A dict. For example:
+
+            {'latency': 4, 'download_throughput': 2, 'upload_throughput': 2,
+            'offline': False}
+
+        """
+        return self.execute("getNetworkConditions")['value']
+
+    def set_network_conditions(self, **network_conditions):
+        """
+        Sets Chrome network emulation settings.
+
+        :Args:
+         - network_conditions: A dict with conditions specification.
+
+        :Usage:
+            ::
+
+                driver.set_network_conditions(
+                    offline=False,
+                    latency=5,  # additional latency (ms)
+                    download_throughput=500 * 1024,  # maximal throughput
+                    upload_throughput=500 * 1024)  # maximal throughput
+
+            Note: 'throughput' can be used to set both (for download and upload).
+        """
+        self.execute("setNetworkConditions", {
+            'network_conditions': network_conditions
+        })
+
+    def execute_cdp_cmd(self, cmd, cmd_args):
+        """
+        Execute Chrome Devtools Protocol command and get returned result
+
+        The command and command args should follow chrome devtools protocol domains/commands, refer to link
+        https://chromedevtools.github.io/devtools-protocol/
+
+        :Args:
+         - cmd: A str, command name
+         - cmd_args: A dict, command args. empty dict {} if there is no command args
+
+        :Usage:
+            ::
+
+                driver.execute_cdp_cmd('Network.getResponseBody', {'requestId': requestId})
+
+        :Returns:
+            A dict, empty dict {} if there is no result to return.
+            For example to getResponseBody:
+
+            {'base64Encoded': False, 'body': 'response body string'}
+
+        """
+        return self.execute("executeCdpCommand", {'cmd': cmd, 'params': cmd_args})['value']
+
+    def get_sinks(self):
+        """
+        :Returns: A list of sinks avaliable for Cast.
+        """
+        return self.execute('getSinks')['value']
+
+    def get_issue_message(self):
+        """
+        :Returns: An error message when there is any issue in a Cast session.
+        """
+        return self.execute('getIssueMessage')['value']
+
+    def set_sink_to_use(self, sink_name):
+        """
+        Sets a specific sink, using its name, as a Cast session receiver target.
+
+        :Args:
+         - sink_name: Name of the sink to use as the target.
+        """
+        return self.execute('setSinkToUse', {'sinkName': sink_name})
+
+    def start_tab_mirroring(self, sink_name):
+        """
+        Starts a tab mirroring session on a specific receiver target.
+
+        :Args:
+         - sink_name: Name of the sink to use as the target.
+        """
+        return self.execute('startTabMirroring', {'sinkName': sink_name})
+
+    def stop_casting(self, sink_name):
+        """
+        Stops the existing Cast session on a specific receiver target.
+
+        :Args:
+         - sink_name: Name of the sink to stop the Cast session.
+        """
+        return self.execute('stopCasting', {'sinkName': sink_name})
+
+    def quit(self):
+        """
+        Closes the browser and shuts down the ChromeDriver executable
+        that is started when starting the ChromeDriver
+        """
+        try:
+            RemoteWebDriver.quit(self)
+        except Exception:
+            # We don't care about the message because something probably has gone wrong
+            pass
+        finally:
+            self.service.stop()
+
+    def create_options(self):
+        return Options()
diff --git a/selenium/webdriver/common/__init__.py b/selenium/webdriver/common/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/common/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/common/action_chains.py b/selenium/webdriver/common/action_chains.py
new file mode 100644
index 00000000..704124bd
--- /dev/null
+++ b/selenium/webdriver/common/action_chains.py
@@ -0,0 +1,365 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+The ActionChains implementation,
+"""
+
+import time
+
+from selenium.webdriver.remote.command import Command
+
+from .utils import keys_to_typing
+from .actions.action_builder import ActionBuilder
+
+
+class ActionChains(object):
+    """
+    ActionChains are a way to automate low level interactions such as
+    mouse movements, mouse button actions, key press, and context menu interactions.
+    This is useful for doing more complex actions like hover over and drag and drop.
+
+    Generate user actions.
+       When you call methods for actions on the ActionChains object,
+       the actions are stored in a queue in the ActionChains object.
+       When you call perform(), the events are fired in the order they
+       are queued up.
+
+    ActionChains can be used in a chain pattern::
+
+        menu = driver.find_element_by_css_selector(".nav")
+        hidden_submenu = driver.find_element_by_css_selector(".nav #submenu1")
+
+        ActionChains(driver).move_to_element(menu).click(hidden_submenu).perform()
+
+    Or actions can be queued up one by one, then performed.::
+
+        menu = driver.find_element_by_css_selector(".nav")
+        hidden_submenu = driver.find_element_by_css_selector(".nav #submenu1")
+
+        actions = ActionChains(driver)
+        actions.move_to_element(menu)
+        actions.click(hidden_submenu)
+        actions.perform()
+
+    Either way, the actions are performed in the order they are called, one after
+    another.
+    """
+
+    def __init__(self, driver):
+        """
+        Creates a new ActionChains.
+
+        :Args:
+         - driver: The WebDriver instance which performs user actions.
+        """
+        self._driver = driver
+        self._actions = []
+        if self._driver.w3c:
+            self.w3c_actions = ActionBuilder(driver)
+
+    def perform(self):
+        """
+        Performs all stored actions.
+        """
+        if self._driver.w3c:
+            self.w3c_actions.perform()
+        else:
+            for action in self._actions:
+                action()
+
+    def reset_actions(self):
+        """
+        Clears actions that are already stored locally and on the remote end
+        """
+        if self._driver.w3c:
+            self.w3c_actions.clear_actions()
+            for device in self.w3c_actions.devices:
+                device.clear_actions()
+        self._actions = []
+
+    def click(self, on_element=None):
+        """
+        Clicks an element.
+
+        :Args:
+         - on_element: The element to click.
+           If None, clicks on current mouse position.
+        """
+        if on_element:
+            self.move_to_element(on_element)
+        if self._driver.w3c:
+            self.w3c_actions.pointer_action.click()
+            self.w3c_actions.key_action.pause()
+            self.w3c_actions.key_action.pause()
+        else:
+            self._actions.append(lambda: self._driver.execute(
+                                 Command.CLICK, {'button': 0}))
+        return self
+
+    def click_and_hold(self, on_element=None):
+        """
+        Holds down the left mouse button on an element.
+
+        :Args:
+         - on_element: The element to mouse down.
+           If None, clicks on current mouse position.
+        """
+        if on_element:
+            self.move_to_element(on_element)
+        if self._driver.w3c:
+            self.w3c_actions.pointer_action.click_and_hold()
+            self.w3c_actions.key_action.pause()
+        else:
+            self._actions.append(lambda: self._driver.execute(
+                                 Command.MOUSE_DOWN, {}))
+        return self
+
+    def context_click(self, on_element=None):
+        """
+        Performs a context-click (right click) on an element.
+
+        :Args:
+         - on_element: The element to context-click.
+           If None, clicks on current mouse position.
+        """
+        if on_element:
+            self.move_to_element(on_element)
+        if self._driver.w3c:
+            self.w3c_actions.pointer_action.context_click()
+            self.w3c_actions.key_action.pause()
+            self.w3c_actions.key_action.pause()
+        else:
+            self._actions.append(lambda: self._driver.execute(
+                                 Command.CLICK, {'button': 2}))
+        return self
+
+    def double_click(self, on_element=None):
+        """
+        Double-clicks an element.
+
+        :Args:
+         - on_element: The element to double-click.
+           If None, clicks on current mouse position.
+        """
+        if on_element:
+            self.move_to_element(on_element)
+        if self._driver.w3c:
+            self.w3c_actions.pointer_action.double_click()
+            for _ in range(4):
+                self.w3c_actions.key_action.pause()
+        else:
+            self._actions.append(lambda: self._driver.execute(
+                                 Command.DOUBLE_CLICK, {}))
+        return self
+
+    def drag_and_drop(self, source, target):
+        """
+        Holds down the left mouse button on the source element,
+           then moves to the target element and releases the mouse button.
+
+        :Args:
+         - source: The element to mouse down.
+         - target: The element to mouse up.
+        """
+        self.click_and_hold(source)
+        self.release(target)
+        return self
+
+    def drag_and_drop_by_offset(self, source, xoffset, yoffset):
+        """
+        Holds down the left mouse button on the source element,
+           then moves to the target offset and releases the mouse button.
+
+        :Args:
+         - source: The element to mouse down.
+         - xoffset: X offset to move to.
+         - yoffset: Y offset to move to.
+        """
+        self.click_and_hold(source)
+        self.move_by_offset(xoffset, yoffset)
+        self.release()
+        return self
+
+    def key_down(self, value, element=None):
+        """
+        Sends a key press only, without releasing it.
+           Should only be used with modifier keys (Control, Alt and Shift).
+
+        :Args:
+         - value: The modifier key to send. Values are defined in `Keys` class.
+         - element: The element to send keys.
+           If None, sends a key to current focused element.
+
+        Example, pressing ctrl+c::
+
+            ActionChains(driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform()
+
+        """
+        if element:
+            self.click(element)
+        if self._driver.w3c:
+            self.w3c_actions.key_action.key_down(value)
+            self.w3c_actions.pointer_action.pause()
+        else:
+            self._actions.append(lambda: self._driver.execute(
+                Command.SEND_KEYS_TO_ACTIVE_ELEMENT,
+                {"value": keys_to_typing(value)}))
+        return self
+
+    def key_up(self, value, element=None):
+        """
+        Releases a modifier key.
+
+        :Args:
+         - value: The modifier key to send. Values are defined in Keys class.
+         - element: The element to send keys.
+           If None, sends a key to current focused element.
+
+        Example, pressing ctrl+c::
+
+            ActionChains(driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform()
+
+        """
+        if element:
+            self.click(element)
+        if self._driver.w3c:
+            self.w3c_actions.key_action.key_up(value)
+            self.w3c_actions.pointer_action.pause()
+        else:
+            self._actions.append(lambda: self._driver.execute(
+                Command.SEND_KEYS_TO_ACTIVE_ELEMENT,
+                {"value": keys_to_typing(value)}))
+        return self
+
+    def move_by_offset(self, xoffset, yoffset):
+        """
+        Moving the mouse to an offset from current mouse position.
+
+        :Args:
+         - xoffset: X offset to move to, as a positive or negative integer.
+         - yoffset: Y offset to move to, as a positive or negative integer.
+        """
+        if self._driver.w3c:
+            self.w3c_actions.pointer_action.move_by(xoffset, yoffset)
+            self.w3c_actions.key_action.pause()
+        else:
+            self._actions.append(lambda: self._driver.execute(
+                Command.MOVE_TO, {
+                    'xoffset': int(xoffset),
+                    'yoffset': int(yoffset)}))
+        return self
+
+    def move_to_element(self, to_element):
+        """
+        Moving the mouse to the middle of an element.
+
+        :Args:
+         - to_element: The WebElement to move to.
+        """
+        if self._driver.w3c:
+            self.w3c_actions.pointer_action.move_to(to_element)
+            self.w3c_actions.key_action.pause()
+        else:
+            self._actions.append(lambda: self._driver.execute(
+                                 Command.MOVE_TO, {'element': to_element.id}))
+        return self
+
+    def move_to_element_with_offset(self, to_element, xoffset, yoffset):
+        """
+        Move the mouse by an offset of the specified element.
+           Offsets are relative to the top-left corner of the element.
+
+        :Args:
+         - to_element: The WebElement to move to.
+         - xoffset: X offset to move to.
+         - yoffset: Y offset to move to.
+        """
+        if self._driver.w3c:
+            self.w3c_actions.pointer_action.move_to(to_element, xoffset, yoffset)
+            self.w3c_actions.key_action.pause()
+        else:
+            self._actions.append(
+                lambda: self._driver.execute(Command.MOVE_TO, {
+                    'element': to_element.id,
+                    'xoffset': int(xoffset),
+                    'yoffset': int(yoffset)}))
+        return self
+
+    def pause(self, seconds):
+        """ Pause all inputs for the specified duration in seconds """
+        if self._driver.w3c:
+            self.w3c_actions.pointer_action.pause(seconds)
+            self.w3c_actions.key_action.pause(seconds)
+        else:
+            self._actions.append(lambda: time.sleep(seconds))
+        return self
+
+    def release(self, on_element=None):
+        """
+        Releasing a held mouse button on an element.
+
+        :Args:
+         - on_element: The element to mouse up.
+           If None, releases on current mouse position.
+        """
+        if on_element:
+            self.move_to_element(on_element)
+        if self._driver.w3c:
+            self.w3c_actions.pointer_action.release()
+            self.w3c_actions.key_action.pause()
+        else:
+            self._actions.append(lambda: self._driver.execute(Command.MOUSE_UP, {}))
+        return self
+
+    def send_keys(self, *keys_to_send):
+        """
+        Sends keys to current focused element.
+
+        :Args:
+         - keys_to_send: The keys to send.  Modifier keys constants can be found in the
+           'Keys' class.
+        """
+        typing = keys_to_typing(keys_to_send)
+        if self._driver.w3c:
+            for key in typing:
+                self.key_down(key)
+                self.key_up(key)
+        else:
+            self._actions.append(lambda: self._driver.execute(
+                Command.SEND_KEYS_TO_ACTIVE_ELEMENT, {'value': typing}))
+        return self
+
+    def send_keys_to_element(self, element, *keys_to_send):
+        """
+        Sends keys to an element.
+
+        :Args:
+         - element: The element to send keys.
+         - keys_to_send: The keys to send.  Modifier keys constants can be found in the
+           'Keys' class.
+        """
+        self.click(element)
+        self.send_keys(*keys_to_send)
+        return self
+
+    # Context manager so ActionChains can be used in a 'with .. as' statements.
+    def __enter__(self):
+        return self  # Return created instance of self.
+
+    def __exit__(self, _type, _value, _traceback):
+        pass  # Do nothing, does not require additional cleanup.
diff --git a/selenium/webdriver/common/actions/__init__.py b/selenium/webdriver/common/actions/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/common/actions/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/common/actions/action_builder.py b/selenium/webdriver/common/actions/action_builder.py
new file mode 100644
index 00000000..4b43ccc5
--- /dev/null
+++ b/selenium/webdriver/common/actions/action_builder.py
@@ -0,0 +1,85 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.remote.command import Command
+from . import interaction
+from .key_actions import KeyActions
+from .key_input import KeyInput
+from .pointer_actions import PointerActions
+from .pointer_input import PointerInput
+
+
+class ActionBuilder(object):
+    def __init__(self, driver, mouse=None, keyboard=None):
+        if mouse is None:
+            mouse = PointerInput(interaction.POINTER_MOUSE, "mouse")
+        if keyboard is None:
+            keyboard = KeyInput(interaction.KEY)
+        self.devices = [mouse, keyboard]
+        self._key_action = KeyActions(keyboard)
+        self._pointer_action = PointerActions(mouse)
+        self.driver = driver
+
+    def get_device_with(self, name):
+        try:
+            idx = self.devices.index(name)
+            return self.devices[idx]
+        except:
+            pass
+
+    @property
+    def pointer_inputs(self):
+        return [device for device in self.devices if device.type == interaction.POINTER]
+
+    @property
+    def key_inputs(self):
+        return [device for device in self.devices if device.type == interaction.KEY]
+
+    @property
+    def key_action(self):
+        return self._key_action
+
+    @property
+    def pointer_action(self):
+        return self._pointer_action
+
+    def add_key_input(self, name):
+        new_input = KeyInput(name)
+        self._add_input(new_input)
+        return new_input
+
+    def add_pointer_input(self, kind, name):
+        new_input = PointerInput(kind, name)
+        self._add_input(new_input)
+        return new_input
+
+    def perform(self):
+        enc = {"actions": []}
+        for device in self.devices:
+            encoded = device.encode()
+            if encoded['actions']:
+                enc["actions"].append(encoded)
+        self.driver.execute(Command.W3C_ACTIONS, enc)
+
+    def clear_actions(self):
+        """
+            Clears actions that are already stored on the remote end
+        """
+        self.driver.execute(Command.W3C_CLEAR_ACTIONS)
+
+    def _add_input(self, input):
+        self.devices.append(input)
diff --git a/selenium/webdriver/common/actions/input_device.py b/selenium/webdriver/common/actions/input_device.py
new file mode 100644
index 00000000..984ef310
--- /dev/null
+++ b/selenium/webdriver/common/actions/input_device.py
@@ -0,0 +1,43 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import uuid
+
+
+class InputDevice(object):
+    """
+        Describes the input device being used for the action.
+    """
+    def __init__(self, name=None):
+        if name is None:
+            self.name = uuid.uuid4()
+        else:
+            self.name = name
+
+        self.actions = []
+
+    def add_action(self, action):
+        """
+
+        """
+        self.actions.append(action)
+
+    def clear_actions(self):
+        self.actions = []
+
+    def create_pause(self, duraton=0):
+        pass
diff --git a/selenium/webdriver/common/actions/interaction.py b/selenium/webdriver/common/actions/interaction.py
new file mode 100644
index 00000000..68047fc7
--- /dev/null
+++ b/selenium/webdriver/common/actions/interaction.py
@@ -0,0 +1,50 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+KEY = "key"
+POINTER = "pointer"
+NONE = "none"
+SOURCE_TYPES = set([KEY, POINTER, NONE])
+
+POINTER_MOUSE = "mouse"
+POINTER_TOUCH = "touch"
+POINTER_PEN = "pen"
+
+POINTER_KINDS = set([POINTER_MOUSE, POINTER_TOUCH, POINTER_PEN])
+
+
+class Interaction(object):
+
+    PAUSE = "pause"
+
+    def __init__(self, source):
+        self.source = source
+
+
+class Pause(Interaction):
+
+    def __init__(self, source, duration=0):
+        super(Interaction, self).__init__()
+        self.source = source
+        self.duration = duration
+
+    def encode(self):
+        return {
+            "type": self.PAUSE,
+            "duration": int(self.duration * 1000)
+        }
diff --git a/selenium/webdriver/common/actions/key_actions.py b/selenium/webdriver/common/actions/key_actions.py
new file mode 100644
index 00000000..c4d07299
--- /dev/null
+++ b/selenium/webdriver/common/actions/key_actions.py
@@ -0,0 +1,50 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from .interaction import Interaction, KEY
+from .key_input import KeyInput
+from ..utils import keys_to_typing
+
+
+class KeyActions(Interaction):
+
+    def __init__(self, source=None):
+        if source is None:
+            source = KeyInput(KEY)
+        self.source = source
+        super(KeyActions, self).__init__(source)
+
+    def key_down(self, letter):
+        return self._key_action("create_key_down", letter)
+
+    def key_up(self, letter):
+        return self._key_action("create_key_up", letter)
+
+    def pause(self, duration=0):
+        return self._key_action("create_pause", duration)
+
+    def send_keys(self, text):
+        if not isinstance(text, list):
+            text = keys_to_typing(text)
+        for letter in text:
+            self.key_down(letter)
+            self.key_up(letter)
+        return self
+
+    def _key_action(self, action, letter):
+        meth = getattr(self.source, action)
+        meth(letter)
+        return self
diff --git a/selenium/webdriver/common/actions/key_input.py b/selenium/webdriver/common/actions/key_input.py
new file mode 100644
index 00000000..f392c6bc
--- /dev/null
+++ b/selenium/webdriver/common/actions/key_input.py
@@ -0,0 +1,51 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from . import interaction
+
+from .input_device import InputDevice
+from .interaction import (Interaction,
+                          Pause)
+
+
+class KeyInput(InputDevice):
+    def __init__(self, name):
+        super(KeyInput, self).__init__()
+        self.name = name
+        self.type = interaction.KEY
+
+    def encode(self):
+        return {"type": self.type, "id": self.name, "actions": [acts.encode() for acts in self.actions]}
+
+    def create_key_down(self, key):
+        self.add_action(TypingInteraction(self, "keyDown", key))
+
+    def create_key_up(self, key):
+        self.add_action(TypingInteraction(self, "keyUp", key))
+
+    def create_pause(self, pause_duration=0):
+        self.add_action(Pause(self, pause_duration))
+
+
+class TypingInteraction(Interaction):
+
+    def __init__(self, source, type_, key):
+        super(TypingInteraction, self).__init__(source)
+        self.type = type_
+        self.key = key
+
+    def encode(self):
+        return {"type": self.type, "value": self.key}
diff --git a/selenium/webdriver/common/actions/mouse_button.py b/selenium/webdriver/common/actions/mouse_button.py
new file mode 100644
index 00000000..5fe1b38b
--- /dev/null
+++ b/selenium/webdriver/common/actions/mouse_button.py
@@ -0,0 +1,23 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+class MouseButton(object):
+
+    LEFT = 0
+    MIDDLE = 1
+    RIGHT = 2
diff --git a/selenium/webdriver/common/actions/pointer_actions.py b/selenium/webdriver/common/actions/pointer_actions.py
new file mode 100644
index 00000000..f9368574
--- /dev/null
+++ b/selenium/webdriver/common/actions/pointer_actions.py
@@ -0,0 +1,101 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from . import interaction
+
+from .interaction import Interaction
+from .mouse_button import MouseButton
+from .pointer_input import PointerInput
+
+from selenium.webdriver.remote.webelement import WebElement
+from selenium.webdriver.support.event_firing_webdriver import EventFiringWebElement
+
+
+class PointerActions(Interaction):
+
+    def __init__(self, source=None):
+        if source is None:
+            source = PointerInput(interaction.POINTER_MOUSE, "mouse")
+        self.source = source
+        super(PointerActions, self).__init__(source)
+
+    def pointer_down(self, button=MouseButton.LEFT):
+        self._button_action("create_pointer_down", button=button)
+
+    def pointer_up(self, button=MouseButton.LEFT):
+        self._button_action("create_pointer_up", button=button)
+
+    def move_to(self, element, x=None, y=None):
+        if not isinstance(element, (WebElement, EventFiringWebElement)):
+            raise AttributeError("move_to requires a WebElement")
+        if x is not None or y is not None:
+            el_rect = element.rect
+            left_offset = el_rect['width'] / 2
+            top_offset = el_rect['height'] / 2
+            left = -left_offset + (x or 0)
+            top = -top_offset + (y or 0)
+        else:
+            left = 0
+            top = 0
+        self.source.create_pointer_move(origin=element, x=int(left), y=int(top))
+        return self
+
+    def move_by(self, x, y):
+        self.source.create_pointer_move(origin=interaction.POINTER, x=int(x), y=int(y))
+        return self
+
+    def move_to_location(self, x, y):
+        self.source.create_pointer_move(origin='viewport', x=int(x), y=int(y))
+        return self
+
+    def click(self, element=None):
+        if element:
+            self.move_to(element)
+        self.pointer_down(MouseButton.LEFT)
+        self.pointer_up(MouseButton.LEFT)
+        return self
+
+    def context_click(self, element=None):
+        if element:
+            self.move_to(element)
+        self.pointer_down(MouseButton.RIGHT)
+        self.pointer_up(MouseButton.RIGHT)
+        return self
+
+    def click_and_hold(self, element=None):
+        if element:
+            self.move_to(element)
+        self.pointer_down()
+        return self
+
+    def release(self):
+        self.pointer_up()
+        return self
+
+    def double_click(self, element=None):
+        if element:
+            self.move_to(element)
+        self.click()
+        self.click()
+
+    def pause(self, duration=0):
+        self.source.create_pause(duration)
+        return self
+
+    def _button_action(self, action, button=MouseButton.LEFT):
+        meth = getattr(self.source, action)
+        meth(button)
+        return self
diff --git a/selenium/webdriver/common/actions/pointer_input.py b/selenium/webdriver/common/actions/pointer_input.py
new file mode 100644
index 00000000..2216a5a2
--- /dev/null
+++ b/selenium/webdriver/common/actions/pointer_input.py
@@ -0,0 +1,64 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from .input_device import InputDevice
+from .interaction import POINTER, POINTER_KINDS
+
+from selenium.common.exceptions import InvalidArgumentException
+from selenium.webdriver.remote.webelement import WebElement
+from selenium.webdriver.support.event_firing_webdriver import EventFiringWebElement
+
+
+class PointerInput(InputDevice):
+
+    DEFAULT_MOVE_DURATION = 250
+
+    def __init__(self, kind, name):
+        super(PointerInput, self).__init__()
+        if (kind not in POINTER_KINDS):
+            raise InvalidArgumentException("Invalid PointerInput kind '%s'" % kind)
+        self.type = POINTER
+        self.kind = kind
+        self.name = name
+
+    def create_pointer_move(self, duration=DEFAULT_MOVE_DURATION, x=None, y=None, origin=None):
+        action = dict(type="pointerMove", duration=duration)
+        action["x"] = x
+        action["y"] = y
+        if isinstance(origin, (WebElement, EventFiringWebElement)):
+            action["origin"] = {"element-6066-11e4-a52e-4f735466cecf": origin.id}
+        elif origin is not None:
+            action["origin"] = origin
+
+        self.add_action(action)
+
+    def create_pointer_down(self, button):
+        self.add_action({"type": "pointerDown", "duration": 0, "button": button})
+
+    def create_pointer_up(self, button):
+        self.add_action({"type": "pointerUp", "duration": 0, "button": button})
+
+    def create_pointer_cancel(self):
+        self.add_action({"type": "pointerCancel"})
+
+    def create_pause(self, pause_duration):
+        self.add_action({"type": "pause", "duration": int(pause_duration * 1000)})
+
+    def encode(self):
+        return {"type": self.type,
+                "parameters": {"pointerType": self.kind},
+                "id": self.name,
+                "actions": [acts for acts in self.actions]}
diff --git a/selenium/webdriver/common/alert.py b/selenium/webdriver/common/alert.py
new file mode 100644
index 00000000..65e97fc8
--- /dev/null
+++ b/selenium/webdriver/common/alert.py
@@ -0,0 +1,105 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+The Alert implementation.
+"""
+
+from selenium.webdriver.common.utils import keys_to_typing
+from selenium.webdriver.remote.command import Command
+
+
+class Alert(object):
+    """
+    Allows to work with alerts.
+
+    Use this class to interact with alert prompts.  It contains methods for dismissing,
+    accepting, inputting, and getting text from alert prompts.
+
+    Accepting / Dismissing alert prompts::
+
+        Alert(driver).accept()
+        Alert(driver).dismiss()
+
+    Inputting a value into an alert prompt:
+
+        name_prompt = Alert(driver)
+        name_prompt.send_keys("Willian Shakesphere")
+        name_prompt.accept()
+
+
+    Reading a the text of a prompt for verification:
+
+        alert_text = Alert(driver).text
+        self.assertEqual("Do you wish to quit?", alert_text)
+
+    """
+
+    def __init__(self, driver):
+        """
+        Creates a new Alert.
+
+        :Args:
+         - driver: The WebDriver instance which performs user actions.
+        """
+        self.driver = driver
+
+    @property
+    def text(self):
+        """
+        Gets the text of the Alert.
+        """
+        if self.driver.w3c:
+            return self.driver.execute(Command.W3C_GET_ALERT_TEXT)["value"]
+        else:
+            return self.driver.execute(Command.GET_ALERT_TEXT)["value"]
+
+    def dismiss(self):
+        """
+        Dismisses the alert available.
+        """
+        if self.driver.w3c:
+            self.driver.execute(Command.W3C_DISMISS_ALERT)
+        else:
+            self.driver.execute(Command.DISMISS_ALERT)
+
+    def accept(self):
+        """
+        Accepts the alert available.
+
+        Usage::
+        Alert(driver).accept() # Confirm a alert dialog.
+        """
+        if self.driver.w3c:
+            self.driver.execute(Command.W3C_ACCEPT_ALERT)
+        else:
+            self.driver.execute(Command.ACCEPT_ALERT)
+
+    def send_keys(self, keysToSend):
+        """
+        Send Keys to the Alert.
+
+        :Args:
+         - keysToSend: The text to be sent to Alert.
+
+
+        """
+        if self.driver.w3c:
+            self.driver.execute(Command.W3C_SET_ALERT_VALUE, {'value': keys_to_typing(keysToSend),
+                                                              'text': keysToSend})
+        else:
+            self.driver.execute(Command.SET_ALERT_VALUE, {'text': keysToSend})
diff --git a/selenium/webdriver/common/by.py b/selenium/webdriver/common/by.py
new file mode 100644
index 00000000..7b1228d2
--- /dev/null
+++ b/selenium/webdriver/common/by.py
@@ -0,0 +1,35 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+The By implementation.
+"""
+
+
+class By(object):
+    """
+    Set of supported locator strategies.
+    """
+
+    ID = "id"
+    XPATH = "xpath"
+    LINK_TEXT = "link text"
+    PARTIAL_LINK_TEXT = "partial link text"
+    NAME = "name"
+    TAG_NAME = "tag name"
+    CLASS_NAME = "class name"
+    CSS_SELECTOR = "css selector"
diff --git a/selenium/webdriver/common/desired_capabilities.py b/selenium/webdriver/common/desired_capabilities.py
new file mode 100644
index 00000000..07c7cacc
--- /dev/null
+++ b/selenium/webdriver/common/desired_capabilities.py
@@ -0,0 +1,127 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+The Desired Capabilities implementation.
+"""
+
+
+class DesiredCapabilities(object):
+    """
+    Set of default supported desired capabilities.
+
+    Use this as a starting point for creating a desired capabilities object for
+    requesting remote webdrivers for connecting to selenium server or selenium grid.
+
+    Usage Example::
+
+        from selenium import webdriver
+
+        selenium_grid_url = "http://198.0.0.1:4444/wd/hub"
+
+        # Create a desired capabilities object as a starting point.
+        capabilities = DesiredCapabilities.FIREFOX.copy()
+        capabilities['platform'] = "WINDOWS"
+        capabilities['version'] = "10"
+
+        # Instantiate an instance of Remote WebDriver with the desired capabilities.
+        driver = webdriver.Remote(desired_capabilities=capabilities,
+                                  command_executor=selenium_grid_url)
+
+    Note: Always use '.copy()' on the DesiredCapabilities object to avoid the side
+    effects of altering the Global class instance.
+
+    """
+
+    FIREFOX = {
+        "browserName": "firefox",
+        "acceptInsecureCerts": True,
+    }
+
+    INTERNETEXPLORER = {
+        "browserName": "internet explorer",
+        "version": "",
+        "platform": "WINDOWS",
+    }
+
+    EDGE = {
+        "browserName": "MicrosoftEdge",
+        "version": "",
+        "platform": "WINDOWS"
+    }
+
+    CHROME = {
+        "browserName": "chrome",
+        "version": "",
+        "platform": "ANY",
+    }
+
+    OPERA = {
+        "browserName": "opera",
+        "version": "",
+        "platform": "ANY",
+    }
+
+    SAFARI = {
+        "browserName": "safari",
+        "version": "",
+        "platform": "MAC",
+    }
+
+    HTMLUNIT = {
+        "browserName": "htmlunit",
+        "version": "",
+        "platform": "ANY",
+    }
+
+    HTMLUNITWITHJS = {
+        "browserName": "htmlunit",
+        "version": "firefox",
+        "platform": "ANY",
+        "javascriptEnabled": True,
+    }
+
+    IPHONE = {
+        "browserName": "iPhone",
+        "version": "",
+        "platform": "MAC",
+    }
+
+    IPAD = {
+        "browserName": "iPad",
+        "version": "",
+        "platform": "MAC",
+    }
+
+    ANDROID = {
+        "browserName": "android",
+        "version": "",
+        "platform": "ANDROID",
+    }
+
+    PHANTOMJS = {
+        "browserName": "phantomjs",
+        "version": "",
+        "platform": "ANY",
+        "javascriptEnabled": True,
+    }
+
+    WEBKITGTK = {
+        "browserName": "MiniBrowser",
+        "version": "",
+        "platform": "ANY",
+    }
diff --git a/selenium/webdriver/common/html5/__init__.py b/selenium/webdriver/common/html5/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/common/html5/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/common/html5/application_cache.py b/selenium/webdriver/common/html5/application_cache.py
new file mode 100644
index 00000000..ecfe5e10
--- /dev/null
+++ b/selenium/webdriver/common/html5/application_cache.py
@@ -0,0 +1,48 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+The ApplicationCache implementaion.
+"""
+
+from selenium.webdriver.remote.command import Command
+
+
+class ApplicationCache(object):
+
+    UNCACHED = 0
+    IDLE = 1
+    CHECKING = 2
+    DOWNLOADING = 3
+    UPDATE_READY = 4
+    OBSOLETE = 5
+
+    def __init__(self, driver):
+        """
+        Creates a new Aplication Cache.
+
+        :Args:
+         - driver: The WebDriver instance which performs user actions.
+        """
+        self.driver = driver
+
+    @property
+    def status(self):
+        """
+        Returns a current status of application cache.
+        """
+        return self.driver.execute(Command.GET_APP_CACHE_STATUS)['value']
diff --git a/selenium/webdriver/common/keys.py b/selenium/webdriver/common/keys.py
new file mode 100644
index 00000000..cd3bb76c
--- /dev/null
+++ b/selenium/webdriver/common/keys.py
@@ -0,0 +1,96 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+The Keys implementation.
+"""
+
+from __future__ import unicode_literals
+
+
+class Keys(object):
+    """
+    Set of special keys codes.
+    """
+
+    NULL = '\ue000'
+    CANCEL = '\ue001'  # ^break
+    HELP = '\ue002'
+    BACKSPACE = '\ue003'
+    BACK_SPACE = BACKSPACE
+    TAB = '\ue004'
+    CLEAR = '\ue005'
+    RETURN = '\ue006'
+    ENTER = '\ue007'
+    SHIFT = '\ue008'
+    LEFT_SHIFT = SHIFT
+    CONTROL = '\ue009'
+    LEFT_CONTROL = CONTROL
+    ALT = '\ue00a'
+    LEFT_ALT = ALT
+    PAUSE = '\ue00b'
+    ESCAPE = '\ue00c'
+    SPACE = '\ue00d'
+    PAGE_UP = '\ue00e'
+    PAGE_DOWN = '\ue00f'
+    END = '\ue010'
+    HOME = '\ue011'
+    LEFT = '\ue012'
+    ARROW_LEFT = LEFT
+    UP = '\ue013'
+    ARROW_UP = UP
+    RIGHT = '\ue014'
+    ARROW_RIGHT = RIGHT
+    DOWN = '\ue015'
+    ARROW_DOWN = DOWN
+    INSERT = '\ue016'
+    DELETE = '\ue017'
+    SEMICOLON = '\ue018'
+    EQUALS = '\ue019'
+
+    NUMPAD0 = '\ue01a'  # number pad keys
+    NUMPAD1 = '\ue01b'
+    NUMPAD2 = '\ue01c'
+    NUMPAD3 = '\ue01d'
+    NUMPAD4 = '\ue01e'
+    NUMPAD5 = '\ue01f'
+    NUMPAD6 = '\ue020'
+    NUMPAD7 = '\ue021'
+    NUMPAD8 = '\ue022'
+    NUMPAD9 = '\ue023'
+    MULTIPLY = '\ue024'
+    ADD = '\ue025'
+    SEPARATOR = '\ue026'
+    SUBTRACT = '\ue027'
+    DECIMAL = '\ue028'
+    DIVIDE = '\ue029'
+
+    F1 = '\ue031'  # function  keys
+    F2 = '\ue032'
+    F3 = '\ue033'
+    F4 = '\ue034'
+    F5 = '\ue035'
+    F6 = '\ue036'
+    F7 = '\ue037'
+    F8 = '\ue038'
+    F9 = '\ue039'
+    F10 = '\ue03a'
+    F11 = '\ue03b'
+    F12 = '\ue03c'
+
+    META = '\ue03d'
+    COMMAND = '\ue03d'
diff --git a/selenium/webdriver/common/options.py b/selenium/webdriver/common/options.py
new file mode 100644
index 00000000..06f74b54
--- /dev/null
+++ b/selenium/webdriver/common/options.py
@@ -0,0 +1,74 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from abc import ABCMeta, abstractmethod
+
+
+class BaseOptions(object):
+    """
+    Base class for individual browser options
+    """
+    __metaclass__ = ABCMeta
+
+    def __init__(self):
+        self._caps = self.default_capabilities
+
+    @property
+    def capabilities(self):
+        return self._caps
+
+    def set_capability(self, name, value):
+        """ Sets a capability """
+        self._caps[name] = value
+
+    @abstractmethod
+    def to_capabilities(self):
+        return
+
+    @property
+    @abstractmethod
+    def default_capabilities(self):
+        return {}
+
+
+class ArgOptions(BaseOptions):
+
+    def __init__(self):
+        super(ArgOptions, self).__init__()
+        self._arguments = []
+
+    @property
+    def arguments(self):
+        """
+        :Returns: A list of arguments needed for the browser
+        """
+        return self._arguments
+
+    def add_argument(self, argument):
+        """
+        Adds an argument to the list
+
+        :Args:
+         - Sets the arguments
+        """
+        if argument:
+            self._arguments.append(argument)
+        else:
+            raise ValueError('argument can not be null')
+
+    def to_capabilities(self):
+        return self._caps
diff --git a/selenium/webdriver/common/proxy.py b/selenium/webdriver/common/proxy.py
new file mode 100644
index 00000000..2dd11a7b
--- /dev/null
+++ b/selenium/webdriver/common/proxy.py
@@ -0,0 +1,358 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+The Proxy implementation.
+"""
+
+
+class ProxyTypeFactory:
+    """
+    Factory for proxy types.
+    """
+
+    @staticmethod
+    def make(ff_value, string):
+        return {'ff_value': ff_value, 'string': string}
+
+
+class ProxyType:
+    """
+    Set of possible types of proxy.
+
+    Each proxy type has 2 properties:
+       'ff_value' is value of Firefox profile preference,
+       'string' is id of proxy type.
+    """
+
+    DIRECT = ProxyTypeFactory.make(0, 'DIRECT')  # Direct connection, no proxy (default on Windows).
+    MANUAL = ProxyTypeFactory.make(1, 'MANUAL')  # Manual proxy settings (e.g., for httpProxy).
+    PAC = ProxyTypeFactory.make(2, 'PAC')  # Proxy autoconfiguration from URL.
+    RESERVED_1 = ProxyTypeFactory.make(3, 'RESERVED1')  # Never used.
+    AUTODETECT = ProxyTypeFactory.make(4, 'AUTODETECT')  # Proxy autodetection (presumably with WPAD).
+    SYSTEM = ProxyTypeFactory.make(5, 'SYSTEM')  # Use system settings (default on Linux).
+    UNSPECIFIED = ProxyTypeFactory.make(6, 'UNSPECIFIED')  # Not initialized (for internal use).
+
+    @classmethod
+    def load(cls, value):
+        if isinstance(value, dict) and 'string' in value:
+            value = value['string']
+        value = str(value).upper()
+        for attr in dir(cls):
+            attr_value = getattr(cls, attr)
+            if isinstance(attr_value, dict) and \
+                    'string' in attr_value and \
+                    attr_value['string'] is not None and \
+                    attr_value['string'] == value:
+                return attr_value
+        raise Exception("No proxy type is found for %s" % (value))
+
+
+class Proxy(object):
+    """
+    Proxy contains information about proxy type and necessary proxy settings.
+    """
+
+    proxyType = ProxyType.UNSPECIFIED
+    autodetect = False
+    ftpProxy = ''
+    httpProxy = ''
+    noProxy = ''
+    proxyAutoconfigUrl = ''
+    sslProxy = ''
+    socksProxy = ''
+    socksUsername = ''
+    socksPassword = ''
+    socksVersion = None
+
+    def __init__(self, raw=None):
+        """
+        Creates a new Proxy.
+
+        :Args:
+         - raw: raw proxy data. If None, default class values are used.
+        """
+        if raw is not None:
+            if 'proxyType' in raw and raw['proxyType'] is not None:
+                self.proxy_type = ProxyType.load(raw['proxyType'])
+            if 'ftpProxy' in raw and raw['ftpProxy'] is not None:
+                self.ftp_proxy = raw['ftpProxy']
+            if 'httpProxy' in raw and raw['httpProxy'] is not None:
+                self.http_proxy = raw['httpProxy']
+            if 'noProxy' in raw and raw['noProxy'] is not None:
+                self.no_proxy = raw['noProxy']
+            if 'proxyAutoconfigUrl' in raw and raw['proxyAutoconfigUrl'] is not None:
+                self.proxy_autoconfig_url = raw['proxyAutoconfigUrl']
+            if 'sslProxy' in raw and raw['sslProxy'] is not None:
+                self.sslProxy = raw['sslProxy']
+            if 'autodetect' in raw and raw['autodetect'] is not None:
+                self.auto_detect = raw['autodetect']
+            if 'socksProxy' in raw and raw['socksProxy'] is not None:
+                self.socks_proxy = raw['socksProxy']
+            if 'socksUsername' in raw and raw['socksUsername'] is not None:
+                self.socks_username = raw['socksUsername']
+            if 'socksPassword' in raw and raw['socksPassword'] is not None:
+                self.socks_password = raw['socksPassword']
+            if 'socksVersion' in raw and raw['socksVersion'] is not None:
+                self.socks_version = raw['socksVersion']
+
+    @property
+    def proxy_type(self):
+        """
+        Returns proxy type as `ProxyType`.
+        """
+        return self.proxyType
+
+    @proxy_type.setter
+    def proxy_type(self, value):
+        """
+        Sets proxy type.
+
+        :Args:
+         - value: The proxy type.
+        """
+        self._verify_proxy_type_compatibility(value)
+        self.proxyType = value
+
+    @property
+    def auto_detect(self):
+        """
+        Returns autodetect setting.
+        """
+        return self.autodetect
+
+    @auto_detect.setter
+    def auto_detect(self, value):
+        """
+        Sets autodetect setting.
+
+        :Args:
+         - value: The autodetect value.
+        """
+        if isinstance(value, bool):
+            if self.autodetect is not value:
+                self._verify_proxy_type_compatibility(ProxyType.AUTODETECT)
+                self.proxyType = ProxyType.AUTODETECT
+                self.autodetect = value
+        else:
+            raise ValueError("Autodetect proxy value needs to be a boolean")
+
+    @property
+    def ftp_proxy(self):
+        """
+        Returns ftp proxy setting.
+        """
+        return self.ftpProxy
+
+    @ftp_proxy.setter
+    def ftp_proxy(self, value):
+        """
+        Sets ftp proxy setting.
+
+        :Args:
+         - value: The ftp proxy value.
+        """
+        self._verify_proxy_type_compatibility(ProxyType.MANUAL)
+        self.proxyType = ProxyType.MANUAL
+        self.ftpProxy = value
+
+    @property
+    def http_proxy(self):
+        """
+        Returns http proxy setting.
+        """
+        return self.httpProxy
+
+    @http_proxy.setter
+    def http_proxy(self, value):
+        """
+        Sets http proxy setting.
+
+        :Args:
+         - value: The http proxy value.
+        """
+        self._verify_proxy_type_compatibility(ProxyType.MANUAL)
+        self.proxyType = ProxyType.MANUAL
+        self.httpProxy = value
+
+    @property
+    def no_proxy(self):
+        """
+        Returns noproxy setting.
+        """
+        return self.noProxy
+
+    @no_proxy.setter
+    def no_proxy(self, value):
+        """
+        Sets noproxy setting.
+
+        :Args:
+         - value: The noproxy value.
+        """
+        self._verify_proxy_type_compatibility(ProxyType.MANUAL)
+        self.proxyType = ProxyType.MANUAL
+        self.noProxy = value
+
+    @property
+    def proxy_autoconfig_url(self):
+        """
+        Returns proxy autoconfig url setting.
+        """
+        return self.proxyAutoconfigUrl
+
+    @proxy_autoconfig_url.setter
+    def proxy_autoconfig_url(self, value):
+        """
+        Sets proxy autoconfig url setting.
+
+        :Args:
+         - value: The proxy autoconfig url value.
+        """
+        self._verify_proxy_type_compatibility(ProxyType.PAC)
+        self.proxyType = ProxyType.PAC
+        self.proxyAutoconfigUrl = value
+
+    @property
+    def ssl_proxy(self):
+        """
+        Returns https proxy setting.
+        """
+        return self.sslProxy
+
+    @ssl_proxy.setter
+    def ssl_proxy(self, value):
+        """
+        Sets https proxy setting.
+
+        :Args:
+         - value: The https proxy value.
+        """
+        self._verify_proxy_type_compatibility(ProxyType.MANUAL)
+        self.proxyType = ProxyType.MANUAL
+        self.sslProxy = value
+
+    @property
+    def socks_proxy(self):
+        """
+        Returns socks proxy setting.
+        """
+        return self.socksProxy
+
+    @socks_proxy.setter
+    def socks_proxy(self, value):
+        """
+        Sets socks proxy setting.
+
+        :Args:
+         - value: The socks proxy value.
+        """
+        self._verify_proxy_type_compatibility(ProxyType.MANUAL)
+        self.proxyType = ProxyType.MANUAL
+        self.socksProxy = value
+
+    @property
+    def socks_username(self):
+        """
+        Returns socks proxy username setting.
+        """
+        return self.socksUsername
+
+    @socks_username.setter
+    def socks_username(self, value):
+        """
+        Sets socks proxy username setting.
+
+        :Args:
+         - value: The socks proxy username value.
+        """
+        self._verify_proxy_type_compatibility(ProxyType.MANUAL)
+        self.proxyType = ProxyType.MANUAL
+        self.socksUsername = value
+
+    @property
+    def socks_password(self):
+        """
+        Returns socks proxy password setting.
+        """
+        return self.socksPassword
+
+    @socks_password.setter
+    def socks_password(self, value):
+        """
+        Sets socks proxy password setting.
+
+        :Args:
+         - value: The socks proxy password value.
+        """
+        self._verify_proxy_type_compatibility(ProxyType.MANUAL)
+        self.proxyType = ProxyType.MANUAL
+        self.socksPassword = value
+
+    @property
+    def socks_version(self):
+        """
+        Returns socks proxy version setting.
+        """
+        return self.socksVersion
+
+    @socks_version.setter
+    def socks_version(self, value):
+        """
+        Sets socks proxy version setting.
+
+        :Args:
+         - value: The socks proxy version value.
+        """
+        self._verify_proxy_type_compatibility(ProxyType.MANUAL)
+        self.proxyType = ProxyType.MANUAL
+        self.socksVersion = value
+
+    def _verify_proxy_type_compatibility(self, compatibleProxy):
+        if self.proxyType != ProxyType.UNSPECIFIED and self.proxyType != compatibleProxy:
+            raise Exception(" Specified proxy type (%s) not compatible with current setting (%s)" % (compatibleProxy, self.proxyType))
+
+    def add_to_capabilities(self, capabilities):
+        """
+        Adds proxy information as capability in specified capabilities.
+
+        :Args:
+         - capabilities: The capabilities to which proxy will be added.
+        """
+        proxy_caps = {}
+        proxy_caps['proxyType'] = self.proxyType['string']
+        if self.autodetect:
+            proxy_caps['autodetect'] = self.autodetect
+        if self.ftpProxy:
+            proxy_caps['ftpProxy'] = self.ftpProxy
+        if self.httpProxy:
+            proxy_caps['httpProxy'] = self.httpProxy
+        if self.proxyAutoconfigUrl:
+            proxy_caps['proxyAutoconfigUrl'] = self.proxyAutoconfigUrl
+        if self.sslProxy:
+            proxy_caps['sslProxy'] = self.sslProxy
+        if self.noProxy:
+            proxy_caps['noProxy'] = self.noProxy
+        if self.socksProxy:
+            proxy_caps['socksProxy'] = self.socksProxy
+        if self.socksUsername:
+            proxy_caps['socksUsername'] = self.socksUsername
+        if self.socksPassword:
+            proxy_caps['socksPassword'] = self.socksPassword
+        if self.socksVersion:
+            proxy_caps['socksVersion'] = self.socksVersion
+        capabilities['proxy'] = proxy_caps
diff --git a/selenium/webdriver/common/service.py b/selenium/webdriver/common/service.py
new file mode 100644
index 00000000..af04a9f2
--- /dev/null
+++ b/selenium/webdriver/common/service.py
@@ -0,0 +1,178 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import errno
+import os
+import platform
+import subprocess
+from subprocess import PIPE
+import time
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.common import utils
+
+try:
+    from subprocess import DEVNULL
+    _HAS_NATIVE_DEVNULL = True
+except ImportError:
+    DEVNULL = -3
+    _HAS_NATIVE_DEVNULL = False
+
+
+class Service(object):
+
+    def __init__(self, executable, port=0, log_file=DEVNULL, env=None, start_error_message=""):
+        self.path = executable
+
+        self.port = port
+        if self.port == 0:
+            self.port = utils.free_port()
+
+        if not _HAS_NATIVE_DEVNULL and log_file == DEVNULL:
+            log_file = open(os.devnull, 'wb')
+
+        self.start_error_message = start_error_message
+        self.log_file = log_file
+        self.env = env or os.environ
+
+    @property
+    def service_url(self):
+        """
+        Gets the url of the Service
+        """
+        return "http://%s" % utils.join_host_port('localhost', self.port)
+
+    def command_line_args(self):
+        raise NotImplemented("This method needs to be implemented in a sub class")
+
+    def start(self):
+        """
+        Starts the Service.
+
+        :Exceptions:
+         - WebDriverException : Raised either when it can't start the service
+           or when it can't connect to the service
+        """
+        try:
+            cmd = [self.path]
+            cmd.extend(self.command_line_args())
+            self.process = subprocess.Popen(cmd, env=self.env,
+                                            close_fds=platform.system() != 'Windows',
+                                            stdout=self.log_file,
+                                            stderr=self.log_file,
+                                            stdin=PIPE)
+        except TypeError:
+            raise
+        except OSError as err:
+            if err.errno == errno.ENOENT:
+                raise WebDriverException(
+                    "'%s' executable needs to be in PATH. %s" % (
+                        os.path.basename(self.path), self.start_error_message)
+                )
+            elif err.errno == errno.EACCES:
+                raise WebDriverException(
+                    "'%s' executable may have wrong permissions. %s" % (
+                        os.path.basename(self.path), self.start_error_message)
+                )
+            else:
+                raise
+        except Exception as e:
+            raise WebDriverException(
+                "The executable %s needs to be available in the path. %s\n%s" %
+                (os.path.basename(self.path), self.start_error_message, str(e)))
+        count = 0
+        while True:
+            self.assert_process_still_running()
+            if self.is_connectable():
+                break
+            count += 1
+            time.sleep(1)
+            if count == 30:
+                raise WebDriverException("Can not connect to the Service %s" % self.path)
+
+    def assert_process_still_running(self):
+        return_code = self.process.poll()
+        if return_code is not None:
+            raise WebDriverException(
+                'Service %s unexpectedly exited. Status code was: %s'
+                % (self.path, return_code)
+            )
+
+    def is_connectable(self):
+        return utils.is_connectable(self.port)
+
+    def send_remote_shutdown_command(self):
+        try:
+            from urllib import request as url_request
+            URLError = url_request.URLError
+        except ImportError:
+            import urllib2 as url_request
+            import urllib2
+            URLError = urllib2.URLError
+
+        try:
+            url_request.urlopen("%s/shutdown" % self.service_url)
+        except URLError:
+            return
+
+        for x in range(30):
+            if not self.is_connectable():
+                break
+            else:
+                time.sleep(1)
+
+    def stop(self):
+        """
+        Stops the service.
+        """
+        if self.log_file != PIPE and not (self.log_file == DEVNULL and _HAS_NATIVE_DEVNULL):
+            try:
+                self.log_file.close()
+            except Exception:
+                pass
+
+        if self.process is None:
+            return
+
+        try:
+            self.send_remote_shutdown_command()
+        except TypeError:
+            pass
+
+        try:
+            if self.process:
+                for stream in [self.process.stdin,
+                               self.process.stdout,
+                               self.process.stderr]:
+                    try:
+                        stream.close()
+                    except AttributeError:
+                        pass
+                self.process.terminate()
+                self.process.wait()
+                self.process.kill()
+                self.process = None
+        except OSError:
+            pass
+
+    def __del__(self):
+        # `subprocess.Popen` doesn't send signal on `__del__`;
+        # so we attempt to close the launched process when `__del__`
+        # is triggered.
+        try:
+            self.stop()
+        except Exception:
+            pass
diff --git a/selenium/webdriver/common/touch_actions.py b/selenium/webdriver/common/touch_actions.py
new file mode 100644
index 00000000..89b25290
--- /dev/null
+++ b/selenium/webdriver/common/touch_actions.py
@@ -0,0 +1,192 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+The Touch Actions implementation
+"""
+
+from selenium.webdriver.remote.command import Command
+
+
+class TouchActions(object):
+    """
+    Generate touch actions. Works like ActionChains; actions are stored in the
+    TouchActions object and are fired with perform().
+    """
+
+    def __init__(self, driver):
+        """
+        Creates a new TouchActions object.
+
+        :Args:
+         - driver: The WebDriver instance which performs user actions.
+           It should be with touchscreen enabled.
+        """
+        self._driver = driver
+        self._actions = []
+
+    def perform(self):
+        """
+        Performs all stored actions.
+        """
+        for action in self._actions:
+            action()
+
+    def tap(self, on_element):
+        """
+        Taps on a given element.
+
+        :Args:
+         - on_element: The element to tap.
+        """
+        self._actions.append(lambda: self._driver.execute(
+            Command.SINGLE_TAP, {'element': on_element.id}))
+        return self
+
+    def double_tap(self, on_element):
+        """
+        Double taps on a given element.
+
+        :Args:
+         - on_element: The element to tap.
+        """
+        self._actions.append(lambda: self._driver.execute(
+            Command.DOUBLE_TAP, {'element': on_element.id}))
+        return self
+
+    def tap_and_hold(self, xcoord, ycoord):
+        """
+        Touch down at given coordinates.
+
+        :Args:
+         - xcoord: X Coordinate to touch down.
+         - ycoord: Y Coordinate to touch down.
+        """
+        self._actions.append(lambda: self._driver.execute(
+            Command.TOUCH_DOWN, {
+                'x': int(xcoord),
+                'y': int(ycoord)}))
+        return self
+
+    def move(self, xcoord, ycoord):
+        """
+        Move held tap to specified location.
+
+        :Args:
+         - xcoord: X Coordinate to move.
+         - ycoord: Y Coordinate to move.
+        """
+        self._actions.append(lambda: self._driver.execute(
+            Command.TOUCH_MOVE, {
+                'x': int(xcoord),
+                'y': int(ycoord)}))
+        return self
+
+    def release(self, xcoord, ycoord):
+        """
+        Release previously issued tap 'and hold' command at specified location.
+
+        :Args:
+         - xcoord: X Coordinate to release.
+         - ycoord: Y Coordinate to release.
+        """
+        self._actions.append(lambda: self._driver.execute(
+            Command.TOUCH_UP, {
+                'x': int(xcoord),
+                'y': int(ycoord)}))
+        return self
+
+    def scroll(self, xoffset, yoffset):
+        """
+        Touch and scroll, moving by xoffset and yoffset.
+
+        :Args:
+         - xoffset: X offset to scroll to.
+         - yoffset: Y offset to scroll to.
+        """
+        self._actions.append(lambda: self._driver.execute(
+            Command.TOUCH_SCROLL, {
+                'xoffset': int(xoffset),
+                'yoffset': int(yoffset)}))
+        return self
+
+    def scroll_from_element(self, on_element, xoffset, yoffset):
+        """
+        Touch and scroll starting at on_element, moving by xoffset and yoffset.
+
+        :Args:
+         - on_element: The element where scroll starts.
+         - xoffset: X offset to scroll to.
+         - yoffset: Y offset to scroll to.
+        """
+        self._actions.append(lambda: self._driver.execute(
+            Command.TOUCH_SCROLL, {
+                'element': on_element.id,
+                'xoffset': int(xoffset),
+                'yoffset': int(yoffset)}))
+        return self
+
+    def long_press(self, on_element):
+        """
+        Long press on an element.
+
+        :Args:
+         - on_element: The element to long press.
+        """
+        self._actions.append(lambda: self._driver.execute(
+            Command.LONG_PRESS, {'element': on_element.id}))
+        return self
+
+    def flick(self, xspeed, yspeed):
+        """
+        Flicks, starting anywhere on the screen.
+
+        :Args:
+         - xspeed: The X speed in pixels per second.
+         - yspeed: The Y speed in pixels per second.
+        """
+        self._actions.append(lambda: self._driver.execute(
+            Command.FLICK, {
+                'xspeed': int(xspeed),
+                'yspeed': int(yspeed)}))
+        return self
+
+    def flick_element(self, on_element, xoffset, yoffset, speed):
+        """
+        Flick starting at on_element, and moving by the xoffset and yoffset
+        with specified speed.
+
+        :Args:
+         - on_element: Flick will start at center of element.
+         - xoffset: X offset to flick to.
+         - yoffset: Y offset to flick to.
+         - speed: Pixels per second to flick.
+        """
+        self._actions.append(lambda: self._driver.execute(
+            Command.FLICK, {
+                'element': on_element.id,
+                'xoffset': int(xoffset),
+                'yoffset': int(yoffset),
+                'speed': int(speed)}))
+        return self
+
+    # Context manager so TouchActions can be used in a 'with .. as' statements.
+    def __enter__(self):
+        return self  # Return created instance of self.
+
+    def __exit__(self, _type, _value, _traceback):
+        pass  # Do nothing, does not require additional cleanup.
diff --git a/selenium/webdriver/common/utils.py b/selenium/webdriver/common/utils.py
new file mode 100644
index 00000000..957f2fd2
--- /dev/null
+++ b/selenium/webdriver/common/utils.py
@@ -0,0 +1,155 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+The Utils methods.
+"""
+import socket
+from selenium.webdriver.common.keys import Keys
+
+try:
+    # Python 2
+    basestring
+    _is_connectable_exceptions = (socket.error,)
+except NameError:
+    # Python 3
+    basestring = str
+    _is_connectable_exceptions = (socket.error, ConnectionResetError)
+
+
+def free_port():
+    """
+    Determines a free port using sockets.
+    """
+    free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    free_socket.bind(('0.0.0.0', 0))
+    free_socket.listen(5)
+    port = free_socket.getsockname()[1]
+    free_socket.close()
+    return port
+
+
+def find_connectable_ip(host, port=None):
+    """Resolve a hostname to an IP, preferring IPv4 addresses.
+
+    We prefer IPv4 so that we don't change behavior from previous IPv4-only
+    implementations, and because some drivers (e.g., FirefoxDriver) do not
+    support IPv6 connections.
+
+    If the optional port number is provided, only IPs that listen on the given
+    port are considered.
+
+    :Args:
+        - host - A hostname.
+        - port - Optional port number.
+
+    :Returns:
+        A single IP address, as a string. If any IPv4 address is found, one is
+        returned. Otherwise, if any IPv6 address is found, one is returned. If
+        neither, then None is returned.
+
+    """
+    try:
+        addrinfos = socket.getaddrinfo(host, None)
+    except socket.gaierror:
+        return None
+
+    ip = None
+    for family, _, _, _, sockaddr in addrinfos:
+        connectable = True
+        if port:
+            connectable = is_connectable(port, sockaddr[0])
+
+        if connectable and family == socket.AF_INET:
+            return sockaddr[0]
+        if connectable and not ip and family == socket.AF_INET6:
+            ip = sockaddr[0]
+    return ip
+
+
+def join_host_port(host, port):
+    """Joins a hostname and port together.
+
+    This is a minimal implementation intended to cope with IPv6 literals. For
+    example, _join_host_port('::1', 80) == '[::1]:80'.
+
+    :Args:
+        - host - A hostname.
+        - port - An integer port.
+
+    """
+    if ':' in host and not host.startswith('['):
+        return '[%s]:%d' % (host, port)
+    return '%s:%d' % (host, port)
+
+
+def is_connectable(port, host="localhost"):
+    """
+    Tries to connect to the server at port to see if it is running.
+
+    :Args:
+     - port - The port to connect.
+    """
+    socket_ = None
+    try:
+        socket_ = socket.create_connection((host, port), 1)
+        result = True
+    except _is_connectable_exceptions:
+        result = False
+    finally:
+        if socket_:
+            socket_.close()
+    return result
+
+
+def is_url_connectable(port):
+    """
+    Tries to connect to the HTTP server at /status path
+    and specified port to see if it responds successfully.
+
+    :Args:
+     - port - The port to connect.
+    """
+    try:
+        from urllib import request as url_request
+    except ImportError:
+        import urllib2 as url_request
+
+    try:
+        res = url_request.urlopen("http://127.0.0.1:%s/status" % port)
+        if res.getcode() == 200:
+            return True
+        else:
+            return False
+    except Exception:
+        return False
+
+
+def keys_to_typing(value):
+    """Processes the values that will be typed in the element."""
+    typing = []
+    for val in value:
+        if isinstance(val, Keys):
+            typing.append(val)
+        elif isinstance(val, int):
+            val = str(val)
+            for i in range(len(val)):
+                typing.append(val[i])
+        else:
+            for i in range(len(val)):
+                typing.append(val[i])
+    return typing
diff --git a/selenium/webdriver/common/window.py b/selenium/webdriver/common/window.py
new file mode 100644
index 00000000..73f5998c
--- /dev/null
+++ b/selenium/webdriver/common/window.py
@@ -0,0 +1,29 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+The WindowTypes implementation.
+"""
+
+from __future__ import unicode_literals
+
+
+class WindowTypes(object):
+    """Set of supported window types."""
+
+    TAB = 'tab'
+    WINDOW = 'window'
diff --git a/selenium/webdriver/edge/__init__.py b/selenium/webdriver/edge/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/edge/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/edge/options.py b/selenium/webdriver/edge/options.py
new file mode 100644
index 00000000..2fa5b038
--- /dev/null
+++ b/selenium/webdriver/edge/options.py
@@ -0,0 +1,51 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+from selenium.webdriver.common.options import BaseOptions
+
+
+class Options(BaseOptions):
+
+    def __init__(self):
+        super(Options, self).__init__()
+        self._page_load_strategy = "normal"
+
+    @property
+    def page_load_strategy(self):
+        return self._page_load_strategy
+
+    @page_load_strategy.setter
+    def page_load_strategy(self, value):
+        if value not in ['normal', 'eager', 'none']:
+            raise ValueError("Page Load Strategy should be 'normal', 'eager' or 'none'.")
+        self._page_load_strategy = value
+
+    def to_capabilities(self):
+        """
+        Creates a capabilities with all the options that have been set and
+
+        :Returns: A dictionary with everything
+        """
+        caps = self._caps
+        caps['pageLoadStrategy'] = self._page_load_strategy
+
+        return caps
+
+    @property
+    def default_capabilities(self):
+        return DesiredCapabilities.EDGE.copy()
diff --git a/selenium/webdriver/edge/service.py b/selenium/webdriver/edge/service.py
new file mode 100644
index 00000000..4f2bf879
--- /dev/null
+++ b/selenium/webdriver/edge/service.py
@@ -0,0 +1,57 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.common import service
+
+
+class Service(service.Service):
+
+    def __init__(self, executable_path, port=0, verbose=False, log_path=None):
+        """
+        Creates a new instance of the EdgeDriver service.
+
+        EdgeDriver provides an interface for Microsoft WebDriver to use
+        with Microsoft Edge.
+
+        :param executable_path: Path to the Microsoft WebDriver binary.
+        :param port: Run the remote service on a specified port.
+            Defaults to 0, which binds to a random open port of the
+            system's choosing.
+        :verbose: Whether to make the webdriver more verbose (passes the
+            --verbose option to the binary). Defaults to False.
+        :param log_path: Optional path for the webdriver binary to log to.
+            Defaults to None which disables logging.
+
+        """
+
+        self.service_args = []
+        if verbose:
+            self.service_args.append("--verbose")
+
+        params = {
+            "executable": executable_path,
+            "port": port,
+            "start_error_message": "Please download from https://go.microsoft.com/fwlink/?LinkId=619687"
+        }
+
+        if log_path:
+            params["log_file"] = open(log_path, "a+")
+
+        service.Service.__init__(self, **params)
+
+    def command_line_args(self):
+        return ["--port=%d" % self.port] + self.service_args
diff --git a/selenium/webdriver/edge/webdriver.py b/selenium/webdriver/edge/webdriver.py
new file mode 100644
index 00000000..22aae8e6
--- /dev/null
+++ b/selenium/webdriver/edge/webdriver.py
@@ -0,0 +1,92 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import warnings
+
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+from selenium.webdriver.edge.service import Service
+from selenium.webdriver.remote.remote_connection import RemoteConnection
+from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
+
+DEFAULT_PORT = 0
+DEFAULT_SERVICE_LOG_PATH = None
+
+
+class WebDriver(RemoteWebDriver):
+
+    def __init__(self, executable_path='MicrosoftWebDriver.exe',
+                 capabilities=None, port=DEFAULT_PORT, verbose=False,
+                 service_log_path=None, log_path=DEFAULT_SERVICE_LOG_PATH,
+                 service=None, options=None, keep_alive=False):
+        """
+        Creates a new instance of the chrome driver.
+
+        Starts the service and then creates new instance of chrome driver.
+
+        :Args:
+         - executable_path - path to the executable. If the default is used it assumes the executable is in the $PATH
+         - capabilities - Dictionary object with non-browser specific
+           capabilities only, such as "proxy" or "loggingPref".
+         - port - port you would like the service to run, if left as 0, a free port will be found.
+         - verbose - whether to set verbose logging in the service
+         - service_log_path - Where to log information from the driver.
+         - keep_alive - Whether to configure EdgeRemoteConnection to use HTTP keep-alive.
+         """
+        if port != DEFAULT_PORT:
+            warnings.warn('port has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        self.port = port
+
+        if service_log_path != DEFAULT_SERVICE_LOG_PATH:
+            warnings.warn('service_log_path has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        if capabilities is not None:
+            warnings.warn('capabilities has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        if service_log_path != DEFAULT_SERVICE_LOG_PATH:
+            warnings.warn('service_log_path has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        if verbose:
+            warnings.warn('verbose has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+
+        if service:
+            self.service = service
+        else:
+            self.service = Service(executable_path, port=self.port, verbose=verbose,
+                                   log_path=service_log_path)
+        self.service.start()
+
+        if capabilities is None:
+            capabilities = DesiredCapabilities.EDGE
+
+        RemoteWebDriver.__init__(
+            self,
+            command_executor=RemoteConnection(self.service.service_url,
+                                              resolve_ip=False,
+                                              keep_alive=keep_alive),
+            desired_capabilities=capabilities)
+        self._is_remote = False
+
+    @property
+    def edge_service(self):
+        warnings.warn("'edge_service' has been deprecated, please use 'service'",
+                      DeprecationWarning, stacklevel=2)
+        return self.service
+
+    def quit(self):
+        RemoteWebDriver.quit(self)
+        self.service.stop()
diff --git a/selenium/webdriver/firefox/__init__.py b/selenium/webdriver/firefox/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/firefox/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/firefox/extension_connection.py b/selenium/webdriver/firefox/extension_connection.py
new file mode 100644
index 00000000..ca715108
--- /dev/null
+++ b/selenium/webdriver/firefox/extension_connection.py
@@ -0,0 +1,84 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import logging
+import time
+
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+from selenium.webdriver.common import utils
+from selenium.webdriver.remote.command import Command
+from selenium.webdriver.remote.remote_connection import RemoteConnection
+from selenium.webdriver.firefox.firefox_binary import FirefoxBinary
+
+LOGGER = logging.getLogger(__name__)
+PORT = 0
+HOST = None
+_URL = ""
+
+
+class ExtensionConnection(RemoteConnection):
+    def __init__(self, host, firefox_profile, firefox_binary=None, timeout=30):
+        self.profile = firefox_profile
+        self.binary = firefox_binary
+        HOST = host
+        timeout = int(timeout)
+
+        if self.binary is None:
+            self.binary = FirefoxBinary()
+
+        if HOST is None:
+            HOST = "127.0.0.1"
+
+        PORT = utils.free_port()
+        self.profile.port = PORT
+        self.profile.update_preferences()
+
+        self.profile.add_extension()
+
+        self.binary.launch_browser(self.profile, timeout=timeout)
+        _URL = "http://%s:%d/hub" % (HOST, PORT)
+        RemoteConnection.__init__(
+            self, _URL, keep_alive=True)
+
+    def quit(self, sessionId=None):
+        self.execute(Command.QUIT, {'sessionId': sessionId})
+        while self.is_connectable():
+            LOGGER.info("waiting to quit")
+            time.sleep(1)
+
+    def connect(self):
+        """Connects to the extension and retrieves the session id."""
+        return self.execute(Command.NEW_SESSION,
+                            {'desiredCapabilities': DesiredCapabilities.FIREFOX})
+
+    @classmethod
+    def connect_and_quit(self):
+        """Connects to an running browser and quit immediately."""
+        self._request('%s/extensions/firefox/quit' % _URL)
+
+    @classmethod
+    def is_connectable(self):
+        """Trys to connect to the extension but do not retrieve context."""
+        utils.is_connectable(self.profile.port)
+
+
+class ExtensionConnectionError(Exception):
+    """An internal error occurred int the extension.
+
+    Might be caused by bad input or bugs in webdriver
+    """
+    pass
diff --git a/selenium/webdriver/firefox/firefox_binary.py b/selenium/webdriver/firefox/firefox_binary.py
new file mode 100644
index 00000000..f619f1e1
--- /dev/null
+++ b/selenium/webdriver/firefox/firefox_binary.py
@@ -0,0 +1,217 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+import os
+import platform
+from subprocess import Popen, STDOUT
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.common import utils
+import time
+
+
+class FirefoxBinary(object):
+
+    NO_FOCUS_LIBRARY_NAME = "x_ignore_nofocus.so"
+
+    def __init__(self, firefox_path=None, log_file=None):
+        """
+        Creates a new instance of Firefox binary.
+
+        :Args:
+         - firefox_path - Path to the Firefox executable. By default, it will be detected from the standard locations.
+         - log_file - A file object to redirect the firefox process output to. It can be sys.stdout.
+                      Please note that with parallel run the output won't be synchronous.
+                      By default, it will be redirected to /dev/null.
+        """
+        self._start_cmd = firefox_path
+        # We used to default to subprocess.PIPE instead of /dev/null, but after
+        # a while the pipe would fill up and Firefox would freeze.
+        self._log_file = log_file or open(os.devnull, "wb")
+        self.command_line = None
+        if self._start_cmd is None:
+            self._start_cmd = self._get_firefox_start_cmd()
+        if not self._start_cmd.strip():
+            raise WebDriverException(
+                "Failed to find firefox binary. You can set it by specifying "
+                "the path to 'firefox_binary':\n\nfrom "
+                "selenium.webdriver.firefox.firefox_binary import "
+                "FirefoxBinary\n\nbinary = "
+                "FirefoxBinary('/path/to/binary')\ndriver = "
+                "webdriver.Firefox(firefox_binary=binary)")
+        # Rather than modifying the environment of the calling Python process
+        # copy it and modify as needed.
+        self._firefox_env = os.environ.copy()
+        self._firefox_env["MOZ_CRASHREPORTER_DISABLE"] = "1"
+        self._firefox_env["MOZ_NO_REMOTE"] = "1"
+        self._firefox_env["NO_EM_RESTART"] = "1"
+
+    def add_command_line_options(self, *args):
+        self.command_line = args
+
+    def launch_browser(self, profile, timeout=30):
+        """Launches the browser for the given profile name.
+        It is assumed the profile already exists.
+        """
+        self.profile = profile
+
+        self._start_from_profile_path(self.profile.path)
+        self._wait_until_connectable(timeout=timeout)
+
+    def kill(self):
+        """Kill the browser.
+
+        This is useful when the browser is stuck.
+        """
+        if self.process:
+            self.process.kill()
+            self.process.wait()
+
+    def _start_from_profile_path(self, path):
+        self._firefox_env["XRE_PROFILE_PATH"] = path
+
+        if platform.system().lower() == 'linux':
+            self._modify_link_library_path()
+        command = [self._start_cmd, "-foreground"]
+        if self.command_line is not None:
+            for cli in self.command_line:
+                command.append(cli)
+        self.process = Popen(
+            command, stdout=self._log_file, stderr=STDOUT,
+            env=self._firefox_env)
+
+    def _wait_until_connectable(self, timeout=30):
+        """Blocks until the extension is connectable in the firefox."""
+        count = 0
+        while not utils.is_connectable(self.profile.port):
+            if self.process.poll() is not None:
+                # Browser has exited
+                raise WebDriverException(
+                    "The browser appears to have exited "
+                    "before we could connect. If you specified a log_file in "
+                    "the FirefoxBinary constructor, check it for details.")
+            if count >= timeout:
+                self.kill()
+                raise WebDriverException(
+                    "Can't load the profile. Possible firefox version mismatch. "
+                    "You must use GeckoDriver instead for Firefox 48+. Profile "
+                    "Dir: %s If you specified a log_file in the "
+                    "FirefoxBinary constructor, check it for details."
+                    % (self.profile.path))
+            count += 1
+            time.sleep(1)
+        return True
+
+    def _find_exe_in_registry(self):
+        try:
+            from _winreg import OpenKey, QueryValue, HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER
+        except ImportError:
+            from winreg import OpenKey, QueryValue, HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER
+        import shlex
+        keys = (r"SOFTWARE\Classes\FirefoxHTML\shell\open\command",
+                r"SOFTWARE\Classes\Applications\firefox.exe\shell\open\command")
+        command = ""
+        for path in keys:
+            try:
+                key = OpenKey(HKEY_LOCAL_MACHINE, path)
+                command = QueryValue(key, "")
+                break
+            except OSError:
+                try:
+                    key = OpenKey(HKEY_CURRENT_USER, path)
+                    command = QueryValue(key, "")
+                    break
+                except OSError:
+                    pass
+        else:
+            return ""
+
+        if not command:
+            return ""
+
+        return shlex.split(command)[0]
+
+    def _get_firefox_start_cmd(self):
+        """Return the command to start firefox."""
+        start_cmd = ""
+        if platform.system() == "Darwin":
+            start_cmd = "/Applications/Firefox.app/Contents/MacOS/firefox-bin"
+            # fallback to homebrew installation for mac users
+            if not os.path.exists(start_cmd):
+                start_cmd = os.path.expanduser("~") + start_cmd
+        elif platform.system() == "Windows":
+            start_cmd = (self._find_exe_in_registry() or self._default_windows_location())
+        elif platform.system() == 'Java' and os._name == 'nt':
+            start_cmd = self._default_windows_location()
+        else:
+            for ffname in ["firefox", "iceweasel"]:
+                start_cmd = self.which(ffname)
+                if start_cmd is not None:
+                    break
+            else:
+                # couldn't find firefox on the system path
+                raise RuntimeError(
+                    "Could not find firefox in your system PATH." +
+                    " Please specify the firefox binary location or install firefox")
+        return start_cmd
+
+    def _default_windows_location(self):
+        program_files = [os.getenv("PROGRAMFILES", r"C:\Program Files"),
+                         os.getenv("PROGRAMFILES(X86)", r"C:\Program Files (x86)")]
+        for path in program_files:
+            binary_path = os.path.join(path, r"Mozilla Firefox\firefox.exe")
+            if os.access(binary_path, os.X_OK):
+                return binary_path
+        return ""
+
+    def _modify_link_library_path(self):
+        existing_ld_lib_path = os.environ.get('LD_LIBRARY_PATH', '')
+
+        new_ld_lib_path = self._extract_and_check(
+            self.profile, self.NO_FOCUS_LIBRARY_NAME, "x86", "amd64")
+
+        new_ld_lib_path += existing_ld_lib_path
+
+        self._firefox_env["LD_LIBRARY_PATH"] = new_ld_lib_path
+        self._firefox_env['LD_PRELOAD'] = self.NO_FOCUS_LIBRARY_NAME
+
+    def _extract_and_check(self, profile, no_focus_so_name, x86, amd64):
+
+        paths = [x86, amd64]
+        built_path = ""
+        for path in paths:
+            library_path = os.path.join(profile.path, path)
+            if not os.path.exists(library_path):
+                os.makedirs(library_path)
+            import shutil
+            shutil.copy(os.path.join(
+                os.path.dirname(__file__),
+                path,
+                self.NO_FOCUS_LIBRARY_NAME),
+                library_path)
+            built_path += library_path + ":"
+
+        return built_path
+
+    def which(self, fname):
+        """Returns the fully qualified path by searching Path of the given
+        name"""
+        for pe in os.environ['PATH'].split(os.pathsep):
+            checkname = os.path.join(pe, fname)
+            if os.access(checkname, os.X_OK) and not os.path.isdir(checkname):
+                return checkname
+        return None
diff --git a/selenium/webdriver/firefox/firefox_profile.py b/selenium/webdriver/firefox/firefox_profile.py
new file mode 100644
index 00000000..f5b529ed
--- /dev/null
+++ b/selenium/webdriver/firefox/firefox_profile.py
@@ -0,0 +1,364 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import with_statement
+
+import base64
+import copy
+import json
+import os
+import re
+import shutil
+import sys
+import tempfile
+import zipfile
+
+try:
+    from cStringIO import StringIO as BytesIO
+except ImportError:
+    from io import BytesIO
+
+from xml.dom import minidom
+from selenium.common.exceptions import WebDriverException
+
+
+WEBDRIVER_EXT = "webdriver.xpi"
+WEBDRIVER_PREFERENCES = "webdriver_prefs.json"
+EXTENSION_NAME = "fxdriver@googlecode.com"
+
+
+class AddonFormatError(Exception):
+    """Exception for not well-formed add-on manifest files"""
+
+
+class FirefoxProfile(object):
+    ANONYMOUS_PROFILE_NAME = "WEBDRIVER_ANONYMOUS_PROFILE"
+    DEFAULT_PREFERENCES = None
+
+    def __init__(self, profile_directory=None):
+        """
+        Initialises a new instance of a Firefox Profile
+
+        :args:
+         - profile_directory: Directory of profile that you want to use. If a
+           directory is passed in it will be cloned and the cloned directory
+           will be used by the driver when instantiated.
+           This defaults to None and will create a new
+           directory when object is created.
+        """
+        if not FirefoxProfile.DEFAULT_PREFERENCES:
+            with open(os.path.join(os.path.dirname(__file__),
+                                   WEBDRIVER_PREFERENCES)) as default_prefs:
+                FirefoxProfile.DEFAULT_PREFERENCES = json.load(default_prefs)
+
+        self.default_preferences = copy.deepcopy(
+            FirefoxProfile.DEFAULT_PREFERENCES['mutable'])
+        self.profile_dir = profile_directory
+        self.tempfolder = None
+        if self.profile_dir is None:
+            self.profile_dir = self._create_tempfolder()
+        else:
+            self.tempfolder = tempfile.mkdtemp()
+            newprof = os.path.join(self.tempfolder, "webdriver-py-profilecopy")
+            shutil.copytree(self.profile_dir, newprof,
+                            ignore=shutil.ignore_patterns("parent.lock", "lock", ".parentlock"))
+            self.profile_dir = newprof
+            os.chmod(self.profile_dir, 0o755)
+            self._read_existing_userjs(os.path.join(self.profile_dir, "user.js"))
+        self.extensionsDir = os.path.join(self.profile_dir, "extensions")
+        self.userPrefs = os.path.join(self.profile_dir, "user.js")
+        if os.path.isfile(self.userPrefs):
+            os.chmod(self.userPrefs, 0o644)
+
+    # Public Methods
+    def set_preference(self, key, value):
+        """
+        sets the preference that we want in the profile.
+        """
+        self.default_preferences[key] = value
+
+    def add_extension(self, extension=WEBDRIVER_EXT):
+        self._install_extension(extension)
+
+    def update_preferences(self):
+        for key, value in FirefoxProfile.DEFAULT_PREFERENCES['frozen'].items():
+            self.default_preferences[key] = value
+        self._write_user_prefs(self.default_preferences)
+
+    # Properties
+
+    @property
+    def path(self):
+        """
+        Gets the profile directory that is currently being used
+        """
+        return self.profile_dir
+
+    @property
+    def port(self):
+        """
+        Gets the port that WebDriver is working on
+        """
+        return self._port
+
+    @port.setter
+    def port(self, port):
+        """
+        Sets the port that WebDriver will be running on
+        """
+        if not isinstance(port, int):
+            raise WebDriverException("Port needs to be an integer")
+        try:
+            port = int(port)
+            if port < 1 or port > 65535:
+                raise WebDriverException("Port number must be in the range 1..65535")
+        except (ValueError, TypeError):
+            raise WebDriverException("Port needs to be an integer")
+        self._port = port
+        self.set_preference("webdriver_firefox_port", self._port)
+
+    @property
+    def accept_untrusted_certs(self):
+        return self.default_preferences["webdriver_accept_untrusted_certs"]
+
+    @accept_untrusted_certs.setter
+    def accept_untrusted_certs(self, value):
+        if value not in [True, False]:
+            raise WebDriverException("Please pass in a Boolean to this call")
+        self.set_preference("webdriver_accept_untrusted_certs", value)
+
+    @property
+    def assume_untrusted_cert_issuer(self):
+        return self.default_preferences["webdriver_assume_untrusted_issuer"]
+
+    @assume_untrusted_cert_issuer.setter
+    def assume_untrusted_cert_issuer(self, value):
+        if value not in [True, False]:
+            raise WebDriverException("Please pass in a Boolean to this call")
+
+        self.set_preference("webdriver_assume_untrusted_issuer", value)
+
+    @property
+    def encoded(self):
+        """
+        A zipped, base64 encoded string of profile directory
+        for use with remote WebDriver JSON wire protocol
+        """
+        self.update_preferences()
+        fp = BytesIO()
+        zipped = zipfile.ZipFile(fp, 'w', zipfile.ZIP_DEFLATED)
+        path_root = len(self.path) + 1  # account for trailing slash
+        for base, dirs, files in os.walk(self.path):
+            for fyle in files:
+                filename = os.path.join(base, fyle)
+                zipped.write(filename, filename[path_root:])
+        zipped.close()
+        return base64.b64encode(fp.getvalue()).decode('UTF-8')
+
+    def _create_tempfolder(self):
+        """
+        Creates a temp folder to store User.js and the extension
+        """
+        return tempfile.mkdtemp()
+
+    def _write_user_prefs(self, user_prefs):
+        """
+        writes the current user prefs dictionary to disk
+        """
+        with open(self.userPrefs, "w") as f:
+            for key, value in user_prefs.items():
+                f.write('user_pref("%s", %s);\n' % (key, json.dumps(value)))
+
+    def _read_existing_userjs(self, userjs):
+        import warnings
+
+        PREF_RE = re.compile(r'user_pref\("(.*)",\s(.*)\)')
+        try:
+            with open(userjs) as f:
+                for usr in f:
+                    matches = re.search(PREF_RE, usr)
+                    try:
+                        self.default_preferences[matches.group(1)] = json.loads(matches.group(2))
+                    except Exception:
+                        warnings.warn("(skipping) failed to json.loads existing preference: " +
+                                      matches.group(1) + matches.group(2))
+        except Exception:
+            # The profile given hasn't had any changes made, i.e no users.js
+            pass
+
+    def _install_extension(self, addon, unpack=True):
+        """
+            Installs addon from a filepath, url
+            or directory of addons in the profile.
+            - path: url, absolute path to .xpi, or directory of addons
+            - unpack: whether to unpack unless specified otherwise in the install.rdf
+        """
+        if addon == WEBDRIVER_EXT:
+            addon = os.path.join(os.path.dirname(__file__), WEBDRIVER_EXT)
+
+        tmpdir = None
+        xpifile = None
+        if addon.endswith('.xpi'):
+            tmpdir = tempfile.mkdtemp(suffix='.' + os.path.split(addon)[-1])
+            compressed_file = zipfile.ZipFile(addon, 'r')
+            for name in compressed_file.namelist():
+                if name.endswith('/'):
+                    if not os.path.isdir(os.path.join(tmpdir, name)):
+                        os.makedirs(os.path.join(tmpdir, name))
+                else:
+                    if not os.path.isdir(os.path.dirname(os.path.join(tmpdir, name))):
+                        os.makedirs(os.path.dirname(os.path.join(tmpdir, name)))
+                    data = compressed_file.read(name)
+                    with open(os.path.join(tmpdir, name), 'wb') as f:
+                        f.write(data)
+            xpifile = addon
+            addon = tmpdir
+
+        # determine the addon id
+        addon_details = self._addon_details(addon)
+        addon_id = addon_details.get('id')
+        assert addon_id, 'The addon id could not be found: %s' % addon
+
+        # copy the addon to the profile
+        addon_path = os.path.join(self.extensionsDir, addon_id)
+        if not unpack and not addon_details['unpack'] and xpifile:
+            if not os.path.exists(self.extensionsDir):
+                os.makedirs(self.extensionsDir)
+                os.chmod(self.extensionsDir, 0o755)
+            shutil.copy(xpifile, addon_path + '.xpi')
+        else:
+            if not os.path.exists(addon_path):
+                shutil.copytree(addon, addon_path, symlinks=True)
+
+        # remove the temporary directory, if any
+        if tmpdir:
+            shutil.rmtree(tmpdir)
+
+    def _addon_details(self, addon_path):
+        """
+        Returns a dictionary of details about the addon.
+
+        :param addon_path: path to the add-on directory or XPI
+
+        Returns::
+
+            {'id':      u'rainbow@colors.org', # id of the addon
+             'version': u'1.4',                # version of the addon
+             'name':    u'Rainbow',            # name of the addon
+             'unpack':  False }                # whether to unpack the addon
+        """
+
+        details = {
+            'id': None,
+            'unpack': False,
+            'name': None,
+            'version': None
+        }
+
+        def get_namespace_id(doc, url):
+            attributes = doc.documentElement.attributes
+            namespace = ""
+            for i in range(attributes.length):
+                if attributes.item(i).value == url:
+                    if ":" in attributes.item(i).name:
+                        # If the namespace is not the default one remove 'xlmns:'
+                        namespace = attributes.item(i).name.split(':')[1] + ":"
+                        break
+            return namespace
+
+        def get_text(element):
+            """Retrieve the text value of a given node"""
+            rc = []
+            for node in element.childNodes:
+                if node.nodeType == node.TEXT_NODE:
+                    rc.append(node.data)
+            return ''.join(rc).strip()
+
+        def parse_manifest_json(content):
+            """Extracts the details from the contents of a WebExtensions `manifest.json` file."""
+            manifest = json.loads(content)
+            try:
+                id = manifest['applications']['gecko']['id']
+            except KeyError:
+                id = manifest['name'].replace(" ", "") + "@" + manifest['version']
+            return {
+                'id': id,
+                'version': manifest['version'],
+                'name': manifest['version'],
+                'unpack': False,
+            }
+
+        if not os.path.exists(addon_path):
+            raise IOError('Add-on path does not exist: %s' % addon_path)
+
+        try:
+            if zipfile.is_zipfile(addon_path):
+                # Bug 944361 - We cannot use 'with' together with zipFile because
+                # it will cause an exception thrown in Python 2.6.
+                try:
+                    compressed_file = zipfile.ZipFile(addon_path, 'r')
+                    if 'manifest.json' in compressed_file.namelist():
+                        return parse_manifest_json(compressed_file.read('manifest.json'))
+
+                    manifest = compressed_file.read('install.rdf')
+                finally:
+                    compressed_file.close()
+            elif os.path.isdir(addon_path):
+                manifest_json_filename = os.path.join(addon_path, 'manifest.json')
+                if os.path.exists(manifest_json_filename):
+                    with open(manifest_json_filename, 'r') as f:
+                        return parse_manifest_json(f.read())
+
+                with open(os.path.join(addon_path, 'install.rdf'), 'r') as f:
+                    manifest = f.read()
+            else:
+                raise IOError('Add-on path is neither an XPI nor a directory: %s' % addon_path)
+        except (IOError, KeyError) as e:
+            raise AddonFormatError(str(e), sys.exc_info()[2])
+
+        try:
+            doc = minidom.parseString(manifest)
+
+            # Get the namespaces abbreviations
+            em = get_namespace_id(doc, 'http://www.mozilla.org/2004/em-rdf#')
+            rdf = get_namespace_id(doc, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#')
+
+            description = doc.getElementsByTagName(rdf + 'Description').item(0)
+            if description is None:
+                description = doc.getElementsByTagName('Description').item(0)
+            for node in description.childNodes:
+                # Remove the namespace prefix from the tag for comparison
+                entry = node.nodeName.replace(em, "")
+                if entry in details.keys():
+                    details.update({entry: get_text(node)})
+            if details.get('id') is None:
+                for i in range(description.attributes.length):
+                    attribute = description.attributes.item(i)
+                    if attribute.name == em + 'id':
+                        details.update({'id': attribute.value})
+        except Exception as e:
+            raise AddonFormatError(str(e), sys.exc_info()[2])
+
+        # turn unpack into a true/false value
+        if isinstance(details['unpack'], str):
+            details['unpack'] = details['unpack'].lower() == 'true'
+
+        # If no ID is set, the add-on is invalid
+        if details.get('id') is None:
+            raise AddonFormatError('Add-on id could not be found.')
+
+        return details
diff --git a/selenium/webdriver/firefox/options.py b/selenium/webdriver/firefox/options.py
new file mode 100644
index 00000000..913dcfe1
--- /dev/null
+++ b/selenium/webdriver/firefox/options.py
@@ -0,0 +1,171 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from selenium.common.exceptions import InvalidArgumentException
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+from selenium.webdriver.common.proxy import Proxy
+from selenium.webdriver.firefox.firefox_binary import FirefoxBinary
+from selenium.webdriver.firefox.firefox_profile import FirefoxProfile
+from selenium.webdriver.common.options import ArgOptions
+
+
+class Log(object):
+    def __init__(self):
+        self.level = None
+
+    def to_capabilities(self):
+        if self.level is not None:
+            return {"log": {"level": self.level}}
+        return {}
+
+
+class Options(ArgOptions):
+    KEY = "moz:firefoxOptions"
+
+    def __init__(self):
+        super(Options, self).__init__()
+        self._binary = None
+        self._preferences = {}
+        self._profile = None
+        self._proxy = None
+        self.log = Log()
+
+    @property
+    def binary(self):
+        """Returns the FirefoxBinary instance"""
+        return self._binary
+
+    @binary.setter
+    def binary(self, new_binary):
+        """Sets location of the browser binary, either by string or
+        ``FirefoxBinary`` instance.
+
+        """
+        if not isinstance(new_binary, FirefoxBinary):
+            new_binary = FirefoxBinary(new_binary)
+        self._binary = new_binary
+
+    @property
+    def binary_location(self):
+        """
+        :Returns: The location of the binary.
+        """
+        return self.binary._start_cmd
+
+    @binary_location.setter  # noqa
+    def binary_location(self, value):
+        """ Sets the location of the browser binary by string """
+        self.binary = value
+
+    @property
+    def accept_insecure_certs(self):
+        return self._caps.get('acceptInsecureCerts')
+
+    @accept_insecure_certs.setter
+    def accept_insecure_certs(self, value):
+        self._caps['acceptInsecureCerts'] = value
+
+    @property
+    def preferences(self):
+        """:Returns: A dict of preferences."""
+        return self._preferences
+
+    def set_preference(self, name, value):
+        """Sets a preference."""
+        self._preferences[name] = value
+
+    @property
+    def proxy(self):
+        """
+        :Returns: Proxy if set, otherwise None.
+        """
+        return self._proxy
+
+    @proxy.setter
+    def proxy(self, value):
+        if not isinstance(value, Proxy):
+            raise InvalidArgumentException("Only Proxy objects can be passed in.")
+        self._proxy = value
+
+    @property
+    def profile(self):
+        """
+        :Returns: The Firefox profile to use.
+        """
+        return self._profile
+
+    @profile.setter
+    def profile(self, new_profile):
+        """Sets location of the browser profile to use, either by string
+        or ``FirefoxProfile``.
+
+        """
+        if not isinstance(new_profile, FirefoxProfile):
+            new_profile = FirefoxProfile(new_profile)
+        self._profile = new_profile
+
+    @property
+    def headless(self):
+        """
+        :Returns: True if the headless argument is set, else False
+        """
+        return '-headless' in self._arguments
+
+    @headless.setter
+    def headless(self, value):
+        """
+        Sets the headless argument
+
+        Args:
+          value: boolean value indicating to set the headless option
+        """
+        if value is True:
+            self._arguments.append('-headless')
+        elif '-headless' in self._arguments:
+            self._arguments.remove('-headless')
+
+    def to_capabilities(self):
+        """Marshals the Firefox options to a `moz:firefoxOptions`
+        object.
+        """
+        # This intentionally looks at the internal properties
+        # so if a binary or profile has _not_ been set,
+        # it will defer to geckodriver to find the system Firefox
+        # and generate a fresh profile.
+        caps = self._caps
+        opts = {}
+
+        if self._binary is not None:
+            opts["binary"] = self._binary._start_cmd
+        if len(self._preferences) > 0:
+            opts["prefs"] = self._preferences
+        if self._proxy is not None:
+            self._proxy.add_to_capabilities(caps)
+        if self._profile is not None:
+            opts["profile"] = self._profile.encoded
+        if len(self._arguments) > 0:
+            opts["args"] = self._arguments
+
+        opts.update(self.log.to_capabilities())
+
+        if len(opts) > 0:
+            caps[Options.KEY] = opts
+
+        return caps
+
+    @property
+    def default_capabilities(self):
+        return DesiredCapabilities.FIREFOX.copy()
diff --git a/selenium/webdriver/firefox/remote_connection.py b/selenium/webdriver/firefox/remote_connection.py
new file mode 100644
index 00000000..5b350008
--- /dev/null
+++ b/selenium/webdriver/firefox/remote_connection.py
@@ -0,0 +1,36 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.remote.remote_connection import RemoteConnection
+
+
+class FirefoxRemoteConnection(RemoteConnection):
+    def __init__(self, remote_server_addr, keep_alive=True):
+        RemoteConnection.__init__(self, remote_server_addr, keep_alive)
+
+        self._commands["GET_CONTEXT"] = ('GET', '/session/$sessionId/moz/context')
+        self._commands["SET_CONTEXT"] = ("POST", "/session/$sessionId/moz/context")
+        self._commands["ELEMENT_GET_ANONYMOUS_CHILDREN"] = \
+            ("POST", "/session/$sessionId/moz/xbl/$id/anonymous_children")
+        self._commands["ELEMENT_FIND_ANONYMOUS_ELEMENTS_BY_ATTRIBUTE"] = \
+            ("POST", "/session/$sessionId/moz/xbl/$id/anonymous_by_attribute")
+        self._commands["INSTALL_ADDON"] = \
+            ("POST", "/session/$sessionId/moz/addon/install")
+        self._commands["UNINSTALL_ADDON"] = \
+            ("POST", "/session/$sessionId/moz/addon/uninstall")
+        self._commands["FULL_PAGE_SCREENSHOT"] = \
+            ("GET", "/session/$sessionId/moz/screenshot/full")
diff --git a/selenium/webdriver/firefox/service.py b/selenium/webdriver/firefox/service.py
new file mode 100644
index 00000000..f762eb7c
--- /dev/null
+++ b/selenium/webdriver/firefox/service.py
@@ -0,0 +1,54 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.common import service
+
+
+class Service(service.Service):
+    """Object that manages the starting and stopping of the
+    GeckoDriver."""
+
+    def __init__(self, executable_path, port=0, service_args=None,
+                 log_path="geckodriver.log", env=None):
+        """Creates a new instance of the GeckoDriver remote service proxy.
+
+        GeckoDriver provides a HTTP interface speaking the W3C WebDriver
+        protocol to Marionette.
+
+        :param executable_path: Path to the GeckoDriver binary.
+        :param port: Run the remote service on a specified port.
+            Defaults to 0, which binds to a random open port of the
+            system's choosing.
+        :param service_args: Optional list of arguments to pass to the
+            GeckoDriver binary.
+        :param log_path: Optional path for the GeckoDriver to log to.
+            Defaults to _geckodriver.log_ in the current working directory.
+        :param env: Optional dictionary of output variables to expose
+            in the services' environment.
+
+        """
+        log_file = open(log_path, "a+") if log_path is not None and log_path != "" else None
+
+        service.Service.__init__(
+            self, executable_path, port=port, log_file=log_file, env=env)
+        self.service_args = service_args or []
+
+    def command_line_args(self):
+        return ["--port", "%d" % self.port] + self.service_args
+
+    def send_remote_shutdown_command(self):
+        pass
diff --git a/selenium/webdriver/firefox/webdriver.py b/selenium/webdriver/firefox/webdriver.py
new file mode 100644
index 00000000..119ba817
--- /dev/null
+++ b/selenium/webdriver/firefox/webdriver.py
@@ -0,0 +1,328 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+try:
+    basestring
+except NameError:  # Python 3.x
+    basestring = str
+
+import base64
+import shutil
+import warnings
+from contextlib import contextmanager
+
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
+
+from .firefox_binary import FirefoxBinary
+from .firefox_profile import FirefoxProfile
+from .options import Options
+from .remote_connection import FirefoxRemoteConnection
+from .service import Service
+from .webelement import FirefoxWebElement
+
+
+DEFAULT_SERVICE_LOG_PATH = None
+
+
+class WebDriver(RemoteWebDriver):
+
+    CONTEXT_CHROME = "chrome"
+    CONTEXT_CONTENT = "content"
+
+    _web_element_cls = FirefoxWebElement
+
+    def __init__(self, firefox_profile=None, firefox_binary=None,
+                 timeout=30, capabilities=None, proxy=None,
+                 executable_path="geckodriver", options=None,
+                 service_log_path="geckodriver.log", firefox_options=None,
+                 service_args=None, service=None, desired_capabilities=None, log_path=None,
+                 keep_alive=True):
+        """Starts a new local session of Firefox.
+
+        Based on the combination and specificity of the various keyword
+        arguments, a capabilities dictionary will be constructed that
+        is passed to the remote end.
+
+        The keyword arguments given to this constructor are helpers to
+        more easily allow Firefox WebDriver sessions to be customised
+        with different options.  They are mapped on to a capabilities
+        dictionary that is passed on to the remote end.
+
+        As some of the options, such as `firefox_profile` and
+        `options.profile` are mutually exclusive, precedence is
+        given from how specific the setting is.  `capabilities` is the
+        least specific keyword argument, followed by `options`,
+        followed by `firefox_binary` and `firefox_profile`.
+
+        In practice this means that if `firefox_profile` and
+        `options.profile` are both set, the selected profile
+        instance will always come from the most specific variable.
+        In this case that would be `firefox_profile`.  This will result in
+        `options.profile` to be ignored because it is considered
+        a less specific setting than the top-level `firefox_profile`
+        keyword argument.  Similarly, if you had specified a
+        `capabilities["moz:firefoxOptions"]["profile"]` Base64 string,
+        this would rank below `options.profile`.
+
+        :param firefox_profile: Instance of ``FirefoxProfile`` object
+            or a string.  If undefined, a fresh profile will be created
+            in a temporary location on the system.
+        :param firefox_binary: Instance of ``FirefoxBinary`` or full
+            path to the Firefox binary.  If undefined, the system default
+            Firefox installation will  be used.
+        :param timeout: Time to wait for Firefox to launch when using
+            the extension connection.
+        :param capabilities: Dictionary of desired capabilities.
+        :param proxy: The proxy settings to use when communicating with
+            Firefox via the extension connection.
+        :param executable_path: Full path to override which geckodriver
+            binary to use for Firefox 47.0.1 and greater, which
+            defaults to picking up the binary from the system path.
+        :param options: Instance of ``options.Options``.
+        :param service_log_path: Where to log information from the driver.
+        :param service_args: List of args to pass to the driver service
+        :param desired_capabilities: alias of capabilities. In future
+            versions of this library, this will replace 'capabilities'.
+            This will make the signature consistent with RemoteWebDriver.
+        :param keep_alive: Whether to configure remote_connection.RemoteConnection to use
+             HTTP keep-alive.
+        """
+
+        if executable_path != 'geckodriver':
+            warnings.warn('executable_path has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        if capabilities is not None:
+            warnings.warn('capabilities has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        if firefox_binary is not None:
+            warnings.warn('firefox_binary has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        self.binary = None
+        if firefox_profile is not None:
+            warnings.warn('firefox_profile has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        self.profile = None
+
+        if log_path != DEFAULT_SERVICE_LOG_PATH:
+            warnings.warn('log_path has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+
+        self.service = service
+
+        # If desired capabilities is set, alias it to capabilities.
+        # If both are set ignore desired capabilities.
+        if capabilities is None and desired_capabilities:
+            capabilities = desired_capabilities
+
+        if capabilities is None:
+            capabilities = DesiredCapabilities.FIREFOX.copy()
+        if options is None:
+            options = Options()
+
+        capabilities = dict(capabilities)
+
+        if capabilities.get("binary"):
+            self.binary = capabilities["binary"]
+
+        # options overrides capabilities
+        if options is not None:
+            if options.binary is not None:
+                self.binary = options.binary
+            if options.profile is not None:
+                self.profile = options.profile
+
+        # firefox_binary and firefox_profile
+        # override options
+        if firefox_binary is not None:
+            if isinstance(firefox_binary, basestring):
+                firefox_binary = FirefoxBinary(firefox_binary)
+            self.binary = firefox_binary
+            options.binary = firefox_binary
+        if firefox_profile is not None:
+            if isinstance(firefox_profile, basestring):
+                firefox_profile = FirefoxProfile(firefox_profile)
+            self.profile = firefox_profile
+            options.profile = firefox_profile
+
+        if self.service is None:
+            self.service = Service(
+                executable_path,
+                service_args=service_args,
+                log_path=service_log_path)
+        self.service.start()
+
+        capabilities.update(options.to_capabilities())
+
+        executor = FirefoxRemoteConnection(
+            remote_server_addr=self.service.service_url)
+        RemoteWebDriver.__init__(
+            self,
+            command_executor=executor,
+            desired_capabilities=capabilities,
+            keep_alive=True)
+
+        self._is_remote = False
+
+    def quit(self):
+        """Quits the driver and close every associated window."""
+        try:
+            RemoteWebDriver.quit(self)
+        except Exception:
+            # We don't care about the message because something probably has gone wrong
+            pass
+
+        if self.w3c:
+            self.service.stop()
+        else:
+            self.binary.kill()
+
+        if self.profile is not None:
+            try:
+                shutil.rmtree(self.profile.path)
+                if self.profile.tempfolder is not None:
+                    shutil.rmtree(self.profile.tempfolder)
+            except Exception as e:
+                print(str(e))
+
+    @property
+    def firefox_profile(self):
+        return self.profile
+
+    # Extension commands:
+
+    def set_context(self, context):
+        self.execute("SET_CONTEXT", {"context": context})
+
+    @contextmanager
+    def context(self, context):
+        """Sets the context that Selenium commands are running in using
+        a `with` statement. The state of the context on the server is
+        saved before entering the block, and restored upon exiting it.
+
+        :param context: Context, may be one of the class properties
+            `CONTEXT_CHROME` or `CONTEXT_CONTENT`.
+
+        Usage example::
+
+            with selenium.context(selenium.CONTEXT_CHROME):
+                # chrome scope
+                ... do stuff ...
+        """
+        initial_context = self.execute('GET_CONTEXT').pop('value')
+        self.set_context(context)
+        try:
+            yield
+        finally:
+            self.set_context(initial_context)
+
+    def install_addon(self, path, temporary=None):
+        """
+        Installs Firefox addon.
+
+        Returns identifier of installed addon. This identifier can later
+        be used to uninstall addon.
+
+        :param path: Absolute path to the addon that will be installed.
+
+        :Usage:
+            ::
+
+                driver.install_addon('/path/to/firebug.xpi')
+        """
+        payload = {"path": path}
+        if temporary is not None:
+            payload["temporary"] = temporary
+        return self.execute("INSTALL_ADDON", payload)["value"]
+
+    def uninstall_addon(self, identifier):
+        """
+        Uninstalls Firefox addon using its identifier.
+
+        :Usage:
+            ::
+
+                driver.uninstall_addon('addon@foo.com')
+        """
+        self.execute("UNINSTALL_ADDON", {"id": identifier})
+
+    def get_full_page_screenshot_as_file(self, filename):
+        """
+        Saves a full document screenshot of the current window to a PNG image file. Returns
+           False if there is any IOError, else returns True. Use full paths in
+           your filename.
+
+        :Args:
+         - filename: The full path you wish to save your screenshot to. This
+           should end with a `.png` extension.
+
+        :Usage:
+            ::
+
+                driver.get_full_page_screenshot_as_file('/Screenshots/foo.png')
+        """
+        if not filename.lower().endswith('.png'):
+            warnings.warn("name used for saved screenshot does not match file "
+                          "type. It should end with a `.png` extension", UserWarning)
+        png = self.get_full_page_screenshot_as_png()
+        try:
+            with open(filename, 'wb') as f:
+                f.write(png)
+        except IOError:
+            return False
+        finally:
+            del png
+        return True
+
+    def save_full_page_screenshot(self, filename):
+        """
+        Saves a full document screenshot of the current window to a PNG image file. Returns
+           False if there is any IOError, else returns True. Use full paths in
+           your filename.
+
+        :Args:
+         - filename: The full path you wish to save your screenshot to. This
+           should end with a `.png` extension.
+
+        :Usage:
+            ::
+
+                driver.save_full_page_screenshot('/Screenshots/foo.png')
+        """
+        return self.get_full_page_screenshot_as_file(filename)
+
+    def get_full_page_screenshot_as_png(self):
+        """
+        Gets the full document screenshot of the current window as a binary data.
+
+        :Usage:
+            ::
+
+                driver.get_full_page_screenshot_as_png()
+        """
+        return base64.b64decode(self.get_full_page_screenshot_as_base64().encode('ascii'))
+
+    def get_full_page_screenshot_as_base64(self):
+        """
+        Gets the full document screenshot of the current window as a base64 encoded string
+           which is useful in embedded images in HTML.
+
+        :Usage:
+            ::
+
+                driver.get_full_page_screenshot_as_base64()
+        """
+        return self.execute("FULL_PAGE_SCREENSHOT")['value']
diff --git a/selenium/webdriver/firefox/webelement.py b/selenium/webdriver/firefox/webelement.py
new file mode 100644
index 00000000..4117ec06
--- /dev/null
+++ b/selenium/webdriver/firefox/webelement.py
@@ -0,0 +1,49 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.remote.webelement import WebElement as RemoteWebElement
+
+
+class FirefoxWebElement(RemoteWebElement):
+
+    @property
+    def anonymous_children(self):
+        """Retrieve the anonymous children of this element in an XBL
+        context.  This is only available in chrome context.
+
+        See the  `anonymous content documentation
+        <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XBL/XBL_1.0_Reference/Anonymous_Content>`_
+        on MDN for more information.
+
+        """
+        return self._execute(
+            "ELEMENT_GET_ANONYMOUS_CHILDREN",
+            {"value": None})
+
+    def find_anonymous_element_by_attribute(self, name, value):
+        """Retrieve an anonymous descendant with a specified attribute
+        value.  Typically used with an (arbitrary) anonid attribute to
+        retrieve a specific anonymous child in an XBL binding.
+
+        See the  `anonymous content documentation
+        <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XBL/XBL_1.0_Reference/Anonymous_Content>`_
+        on MDN for more information.
+
+        """
+        return self._execute(
+            "ELEMENT_FIND_ANONYMOUS_ELEMENTS_BY_ATTRIBUTE",
+            {"name": name, "value": value})["value"]
diff --git a/selenium/webdriver/ie/__init__.py b/selenium/webdriver/ie/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/ie/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/ie/options.py b/selenium/webdriver/ie/options.py
new file mode 100644
index 00000000..672b8f6c
--- /dev/null
+++ b/selenium/webdriver/ie/options.py
@@ -0,0 +1,338 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+from selenium.webdriver.common.options import ArgOptions
+
+
+class ElementScrollBehavior(object):
+    TOP = 0
+    BOTTOM = 1
+
+
+class Options(ArgOptions):
+
+    KEY = 'se:ieOptions'
+    SWITCHES = 'ie.browserCommandLineSwitches'
+
+    BROWSER_ATTACH_TIMEOUT = 'browserAttachTimeout'
+    ELEMENT_SCROLL_BEHAVIOR = 'elementScrollBehavior'
+    ENSURE_CLEAN_SESSION = 'ie.ensureCleanSession'
+    FILE_UPLOAD_DIALOG_TIMEOUT = 'ie.fileUploadDialogTimeout'
+    FORCE_CREATE_PROCESS_API = 'ie.forceCreateProcessApi'
+    FORCE_SHELL_WINDOWS_API = 'ie.forceShellWindowsApi'
+    FULL_PAGE_SCREENSHOT = 'ie.enableFullPageScreenshot'
+    IGNORE_PROTECTED_MODE_SETTINGS = 'ignoreProtectedModeSettings'
+    IGNORE_ZOOM_LEVEL = 'ignoreZoomSetting'
+    INITIAL_BROWSER_URL = 'initialBrowserUrl'
+    NATIVE_EVENTS = 'nativeEvents'
+    PERSISTENT_HOVER = 'enablePersistentHover'
+    REQUIRE_WINDOW_FOCUS = 'requireWindowFocus'
+    USE_PER_PROCESS_PROXY = 'ie.usePerProcessProxy'
+    VALIDATE_COOKIE_DOCUMENT_TYPE = 'ie.validateCookieDocumentType'
+
+    def __init__(self):
+        super(Options, self).__init__()
+        self._options = {}
+        self._additional = {}
+
+    @property
+    def options(self):
+        """:Returns: A dictionary of browser options """
+        return self._options
+
+    @property
+    def browser_attach_timeout(self):
+        """
+        :Returns: The options Browser Attach Timeout in milliseconds
+        """
+        return self._options.get(self.BROWSER_ATTACH_TIMEOUT)
+
+    @browser_attach_timeout.setter
+    def browser_attach_timeout(self, value):
+        """
+        Sets the options Browser Attach Timeout
+
+        :Args:
+         - value: Timeout in milliseconds
+
+        """
+        if not isinstance(value, int):
+            raise ValueError('Browser Attach Timeout must be an integer.')
+        self._options[self.BROWSER_ATTACH_TIMEOUT] = value
+
+    @property
+    def element_scroll_behavior(self):
+        """:Returns: The options Element Scroll Behavior in milliseconds """
+        return self._options.get(self.ELEMENT_SCROLL_BEHAVIOR)
+
+    @element_scroll_behavior.setter
+    def element_scroll_behavior(self, value):
+        """
+        Sets the options Element Scroll Behavior
+
+        :Args:
+         - value: 0 - Top, 1 - Bottom
+
+        """
+        if value not in [ElementScrollBehavior.TOP, ElementScrollBehavior.BOTTOM]:
+            raise ValueError('Element Scroll Behavior out of range.')
+        self._options[self.ELEMENT_SCROLL_BEHAVIOR] = value
+
+    @property
+    def ensure_clean_session(self):
+        """:Returns: The options Ensure Clean Session value """
+        return self._options.get(self.ENSURE_CLEAN_SESSION)
+
+    @ensure_clean_session.setter
+    def ensure_clean_session(self, value):
+        """
+        Sets the options Ensure Clean Session value
+
+        :Args:
+         - value: boolean value
+
+        """
+        self._options[self.ENSURE_CLEAN_SESSION] = value
+
+    @property
+    def file_upload_dialog_timeout(self):
+        """:Returns: The options File Upload Dialog Timeout in milliseconds """
+        return self._options.get(self.FILE_UPLOAD_DIALOG_TIMEOUT)
+
+    @file_upload_dialog_timeout.setter
+    def file_upload_dialog_timeout(self, value):
+        """
+        Sets the options File Upload Dialog Timeout value
+
+        :Args:
+         - value: Timeout in milliseconds
+
+        """
+        if not isinstance(value, int):
+            raise ValueError('File Upload Dialog Timeout must be an integer.')
+        self._options[self.FILE_UPLOAD_DIALOG_TIMEOUT] = value
+
+    @property
+    def force_create_process_api(self):
+        """:Returns: The options Force Create Process Api value """
+        return self._options.get(self.FORCE_CREATE_PROCESS_API)
+
+    @force_create_process_api.setter
+    def force_create_process_api(self, value):
+        """
+        Sets the options Force Create Process Api value
+
+        :Args:
+         - value: boolean value
+
+        """
+        self._options[self.FORCE_CREATE_PROCESS_API] = value
+
+    @property
+    def force_shell_windows_api(self):
+        """:Returns: The options Force Shell Windows Api value """
+        return self._options.get(self.FORCE_SHELL_WINDOWS_API)
+
+    @force_shell_windows_api.setter
+    def force_shell_windows_api(self, value):
+        """
+        Sets the options Force Shell Windows Api value
+
+        :Args:
+         - value: boolean value
+
+        """
+        self._options[self.FORCE_SHELL_WINDOWS_API] = value
+
+    @property
+    def full_page_screenshot(self):
+        """:Returns: The options Full Page Screenshot value """
+        return self._options.get(self.FULL_PAGE_SCREENSHOT)
+
+    @full_page_screenshot.setter
+    def full_page_screenshot(self, value):
+        """
+        Sets the options Full Page Screenshot value
+
+        :Args:
+         - value: boolean value
+
+        """
+        self._options[self.FULL_PAGE_SCREENSHOT] = value
+
+    @property
+    def ignore_protected_mode_settings(self):
+        """:Returns: The options Ignore Protected Mode Settings value """
+        return self._options.get(self.IGNORE_PROTECTED_MODE_SETTINGS)
+
+    @ignore_protected_mode_settings.setter
+    def ignore_protected_mode_settings(self, value):
+        """
+        Sets the options Ignore Protected Mode Settings value
+
+        :Args:
+         - value: boolean value
+
+        """
+        self._options[self.IGNORE_PROTECTED_MODE_SETTINGS] = value
+
+    @property
+    def ignore_zoom_level(self):
+        """:Returns: The options Ignore Zoom Level value """
+        return self._options.get(self.IGNORE_ZOOM_LEVEL)
+
+    @ignore_zoom_level.setter
+    def ignore_zoom_level(self, value):
+        """
+        Sets the options Ignore Zoom Level value
+
+        :Args:
+         - value: boolean value
+
+        """
+        self._options[self.IGNORE_ZOOM_LEVEL] = value
+
+    @property
+    def initial_browser_url(self):
+        """:Returns: The options Initial Browser Url value """
+        return self._options.get(self.INITIAL_BROWSER_URL)
+
+    @initial_browser_url.setter
+    def initial_browser_url(self, value):
+        """
+        Sets the options Initial Browser Url value
+
+        :Args:
+         - value: URL string
+
+        """
+        self._options[self.INITIAL_BROWSER_URL] = value
+
+    @property
+    def native_events(self):
+        """:Returns: The options Native Events value """
+        return self._options.get(self.NATIVE_EVENTS)
+
+    @native_events.setter
+    def native_events(self, value):
+        """
+        Sets the options Native Events value
+
+        :Args:
+         - value: boolean value
+
+        """
+        self._options[self.NATIVE_EVENTS] = value
+
+    @property
+    def persistent_hover(self):
+        """:Returns: The options Persistent Hover value """
+        return self._options.get(self.PERSISTENT_HOVER)
+
+    @persistent_hover.setter
+    def persistent_hover(self, value):
+        """
+        Sets the options Persistent Hover value
+
+        :Args:
+         - value: boolean value
+
+        """
+        self._options[self.PERSISTENT_HOVER] = value
+
+    @property
+    def require_window_focus(self):
+        """:Returns: The options Require Window Focus value """
+        return self._options.get(self.REQUIRE_WINDOW_FOCUS)
+
+    @require_window_focus.setter
+    def require_window_focus(self, value):
+        """
+        Sets the options Require Window Focus value
+
+        :Args:
+         - value: boolean value
+
+        """
+        self._options[self.REQUIRE_WINDOW_FOCUS] = value
+
+    @property
+    def use_per_process_proxy(self):
+        """:Returns: The options User Per Process Proxy value """
+        return self._options.get(self.USE_PER_PROCESS_PROXY)
+
+    @use_per_process_proxy.setter
+    def use_per_process_proxy(self, value):
+        """
+        Sets the options User Per Process Proxy value
+
+        :Args:
+         - value: boolean value
+
+        """
+        self._options[self.USE_PER_PROCESS_PROXY] = value
+
+    @property
+    def validate_cookie_document_type(self):
+        """:Returns: The options Validate Cookie Document Type value """
+        return self._options.get(self.VALIDATE_COOKIE_DOCUMENT_TYPE)
+
+    @validate_cookie_document_type.setter
+    def validate_cookie_document_type(self, value):
+        """
+        Sets the options Validate Cookie Document Type value
+
+        :Args:
+         - value: boolean value
+
+        """
+        self._options[self.VALIDATE_COOKIE_DOCUMENT_TYPE] = value
+
+    @property
+    def additional_options(self):
+        """:Returns: The additional options """
+        return self._additional
+
+    def add_additional_option(self, name, value):
+        """
+        Adds an additional option not yet added as a safe option for IE
+
+        :Args:
+         - name: name of the option to add
+         - value: value of the option to add
+
+        """
+        self._additional[name] = value
+
+    def to_capabilities(self):
+        """Marshals the IE options to the correct object."""
+        caps = self._caps
+
+        opts = self._options.copy()
+        if len(self._arguments) > 0:
+            opts[self.SWITCHES] = ' '.join(self._arguments)
+
+        if len(self._additional) > 0:
+            opts.update(self._additional)
+
+        if len(opts) > 0:
+            caps[Options.KEY] = opts
+        return caps
+
+    @property
+    def default_capabilities(self):
+        return DesiredCapabilities.INTERNETEXPLORER.copy()
diff --git a/selenium/webdriver/ie/service.py b/selenium/webdriver/ie/service.py
new file mode 100644
index 00000000..c9083e16
--- /dev/null
+++ b/selenium/webdriver/ie/service.py
@@ -0,0 +1,50 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.common import service
+
+
+class Service(service.Service):
+    """
+    Object that manages the starting and stopping of the IEDriver
+    """
+
+    def __init__(self, executable_path, port=0, host=None, log_level=None, log_file=None):
+        """
+        Creates a new instance of the Service
+
+        :Args:
+         - executable_path : Path to the IEDriver
+         - port : Port the service is running on
+         - host : IP address the service port is bound
+         - log_level : Level of logging of service, may be "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE".
+           Default is "FATAL".
+         - log_file : Target of logging of service, may be "stdout", "stderr" or file path.
+           Default is "stdout"."""
+        self.service_args = []
+        if host is not None:
+            self.service_args.append("--host=%s" % host)
+        if log_level is not None:
+            self.service_args.append("--log-level=%s" % log_level)
+        if log_file is not None:
+            self.service_args.append("--log-file=%s" % log_file)
+
+        service.Service.__init__(self, executable_path, port=port,
+                                 start_error_message="Please download from http://selenium-release.storage.googleapis.com/index.html and read up at https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver")
+
+    def command_line_args(self):
+        return ["--port=%d" % self.port] + self.service_args
diff --git a/selenium/webdriver/ie/webdriver.py b/selenium/webdriver/ie/webdriver.py
new file mode 100644
index 00000000..c15fc0cc
--- /dev/null
+++ b/selenium/webdriver/ie/webdriver.py
@@ -0,0 +1,115 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import warnings
+
+from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
+from .service import Service
+from .options import Options
+
+DEFAULT_TIMEOUT = 30
+DEFAULT_PORT = 0
+DEFAULT_HOST = None
+DEFAULT_LOG_LEVEL = None
+DEFAULT_SERVICE_LOG_PATH = None
+
+
+class WebDriver(RemoteWebDriver):
+    """ Controls the IEServerDriver and allows you to drive Internet Explorer """
+
+    def __init__(self, executable_path='IEDriverServer.exe', capabilities=None,
+                 port=DEFAULT_PORT, timeout=DEFAULT_TIMEOUT, host=DEFAULT_HOST,
+                 log_level=DEFAULT_LOG_LEVEL, service_log_path=DEFAULT_SERVICE_LOG_PATH,
+                 options=None, service=None,
+                 desired_capabilities=None, keep_alive=False):
+        """
+        Creates a new instance of the chrome driver.
+
+        Starts the service and then creates new instance of chrome driver.
+
+        :Args:
+         - executable_path - Deprecated: path to the executable. If the default is used it assumes the executable is in the $PATH
+         - capabilities - Deprecated: capabilities Dictionary object
+         - port - Deprecated: port you would like the service to run, if left as 0, a free port will be found.
+         - timeout - Deprecated: no longer used, kept for backward compatibility
+         - host - Deprecated: IP address for the service
+         - log_level - Deprecated: log level you would like the service to run.
+         - service_log_path - Deprecated: target of logging of service, may be "stdout", "stderr" or file path.
+         - options - IE Options instance, providing additional IE options
+         - desired_capabilities - Deprecated: alias of capabilities; this will make the signature consistent with RemoteWebDriver.
+         - keep_alive - Whether to configure RemoteConnection to use HTTP keep-alive.
+        """
+        if executable_path != 'IEDriverServer.exe':
+            warnings.warn('executable_path has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        if capabilities is not None:
+            warnings.warn('capabilities has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        if port != DEFAULT_PORT:
+            warnings.warn('port has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        self.port = port
+        if timeout != DEFAULT_TIMEOUT:
+            warnings.warn('timeout has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        if host != DEFAULT_HOST:
+            warnings.warn('host has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        self.host = host
+        if log_level != DEFAULT_LOG_LEVEL:
+            warnings.warn('log_level has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+        if service_log_path != DEFAULT_SERVICE_LOG_PATH:
+            warnings.warn('service_log_path has been deprecated, please pass in a Service object',
+                          DeprecationWarning, stacklevel=2)
+
+        # If both capabilities and desired capabilities are set, ignore desired capabilities.
+        if capabilities is None and desired_capabilities:
+            capabilities = desired_capabilities
+
+        if options is None:
+            if capabilities is None:
+                capabilities = self.create_options().to_capabilities()
+        else:
+            if capabilities is None:
+                capabilities = options.to_capabilities()
+            else:
+                # desired_capabilities stays as passed in
+                capabilities.update(options.to_capabilities())
+        if service is None:
+            service = Service()
+        self.iedriver = Service(
+            executable_path,
+            port=self.port,
+            host=self.host,
+            log_level=log_level,
+            log_file=service_log_path)
+
+        self.iedriver.start()
+
+        RemoteWebDriver.__init__(
+            self,
+            command_executor='http://localhost:%d' % self.port,
+            desired_capabilities=capabilities,
+            keep_alive=keep_alive)
+        self._is_remote = False
+
+    def quit(self):
+        RemoteWebDriver.quit(self)
+        self.iedriver.stop()
+
+    def create_options(self):
+        return Options()
diff --git a/selenium/webdriver/opera/__init__.py b/selenium/webdriver/opera/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/opera/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/opera/options.py b/selenium/webdriver/opera/options.py
new file mode 100644
index 00000000..4fc29c17
--- /dev/null
+++ b/selenium/webdriver/opera/options.py
@@ -0,0 +1,109 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.chrome.options import Options as ChromeOptions
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+
+
+class Options(ChromeOptions):
+    KEY = "operaOptions"
+
+    def __init__(self):
+        ChromeOptions.__init__(self)
+        self._android_package_name = ''
+        self._android_device_socket = ''
+        self._android_command_line_file = ''
+
+    @property
+    def android_package_name(self):
+        """
+        :Returns: The name of the Opera package
+        """
+        return self._android_package_name
+
+    @android_package_name.setter
+    def android_package_name(self, value):
+        """
+        Allows you to set the package name
+
+        :Args:
+         - value: devtools socket name
+        """
+        self._android_package_name = value
+
+    @property
+    def android_device_socket(self):
+        """
+        :Returns: The name of the devtools socket
+        """
+        return self._android_device_socket
+
+    @android_device_socket.setter
+    def android_device_socket(self, value):
+        """
+        Allows you to set the devtools socket name
+
+        :Args:
+         - value: devtools socket name
+        """
+        self._android_device_socket = value
+
+    @property
+    def android_command_line_file(self):
+        """
+        :Returns: The path of the command line file
+        """
+        return self._android_command_line_file
+
+    @android_command_line_file.setter
+    def android_command_line_file(self, value):
+        """
+        Allows you to set where the command line file lives
+
+        :Args:
+         - value: command line file path
+        """
+        self._android_command_line_file = value
+
+    def to_capabilities(self):
+        """
+        Creates a capabilities with all the options that have been set and
+        returns a dictionary with everything
+        """
+        capabilities = ChromeOptions.to_capabilities(self)
+        capabilities.update(self._caps)
+        opera_options = capabilities[self.KEY]
+
+        if self.android_package_name:
+            opera_options["androidPackage"] = self.android_package_name
+        if self.android_device_socket:
+            opera_options["androidDeviceSocket"] = self.android_device_socket
+        if self.android_command_line_file:
+            opera_options["androidCommandLineFile"] = \
+                self.android_command_line_file
+        return capabilities
+
+    @property
+    def default_capabilities(self):
+        return DesiredCapabilities.OPERA.copy()
+
+
+class AndroidOptions(Options):
+
+    def __init__(self):
+        Options.__init__(self)
+        self.android_package_name = 'com.opera.browser'
diff --git a/selenium/webdriver/opera/webdriver.py b/selenium/webdriver/opera/webdriver.py
new file mode 100644
index 00000000..291e603a
--- /dev/null
+++ b/selenium/webdriver/opera/webdriver.py
@@ -0,0 +1,75 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from selenium.webdriver.chrome.webdriver import WebDriver as ChromiumDriver
+from .options import Options
+
+
+class OperaDriver(ChromiumDriver):
+    """Controls the new OperaDriver and allows you
+    to drive the Opera browser based on Chromium."""
+
+    def __init__(self, executable_path=None, port=0,
+                 options=None, service_args=None,
+                 desired_capabilities=None, service_log_path=None,
+                 opera_options=None, keep_alive=True):
+        """
+        Creates a new instance of the operadriver.
+
+        Starts the service and then creates new instance of operadriver.
+
+        :Args:
+         - executable_path - path to the executable. If the default is used
+                             it assumes the executable is in the $PATH
+         - port - port you would like the service to run, if left as 0,
+                  a free port will be found.
+         - options: this takes an instance of OperaOptions
+         - service_args - List of args to pass to the driver service
+         - desired_capabilities: Dictionary object with non-browser specific
+         - service_log_path - Where to log information from the driver.
+           capabilities only, such as "proxy" or "loggingPref".
+        """
+        executable_path = (executable_path if executable_path is not None
+                           else "operadriver")
+        ChromiumDriver.__init__(self,
+                                executable_path=executable_path,
+                                port=port,
+                                options=options,
+                                service_args=service_args,
+                                desired_capabilities=desired_capabilities,
+                                service_log_path=service_log_path,
+                                keep_alive=keep_alive)
+
+    def create_options(self):
+        return Options()
+
+
+class WebDriver(OperaDriver):
+    class ServiceType:
+        CHROMIUM = 2
+
+    def __init__(self,
+                 desired_capabilities=None,
+                 executable_path=None,
+                 port=0,
+                 service_log_path=None,
+                 service_args=None,
+                 options=None):
+        OperaDriver.__init__(self, executable_path=executable_path,
+                             port=port, options=options,
+                             service_args=service_args,
+                             desired_capabilities=desired_capabilities,
+                             service_log_path=service_log_path)
diff --git a/selenium/webdriver/phantomjs/__init__.py b/selenium/webdriver/phantomjs/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/phantomjs/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/phantomjs/service.py b/selenium/webdriver/phantomjs/service.py
new file mode 100644
index 00000000..37b4e9f8
--- /dev/null
+++ b/selenium/webdriver/phantomjs/service.py
@@ -0,0 +1,68 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import os
+import tempfile
+from selenium.webdriver.common import service
+
+
+class Service(service.Service):
+    """
+    Object that manages the starting and stopping of PhantomJS / Ghostdriver
+    """
+
+    def __init__(self, executable_path, port=0, service_args=None, log_path=None):
+        """
+        Creates a new instance of the Service
+
+        :Args:
+         - executable_path : Path to PhantomJS binary
+         - port : Port the service is running on
+         - service_args : A List of other command line options to pass to PhantomJS
+         - log_path: Path for PhantomJS service to log to
+        """
+        self.service_args = service_args
+        if self.service_args is None:
+            self.service_args = []
+        else:
+            self.service_args = service_args[:]
+        if not log_path:
+            log_path = "ghostdriver.log"
+        if not self._args_contain("--cookies-file="):
+            self._cookie_temp_file_handle, self._cookie_temp_file = tempfile.mkstemp()
+            self.service_args.append("--cookies-file=" + self._cookie_temp_file)
+        else:
+            self._cookie_temp_file = None
+
+        service.Service.__init__(self, executable_path, port=port, log_file=open(log_path, 'w'))
+
+    def _args_contain(self, arg):
+        return len(list(filter(lambda x: x.startswith(arg), self.service_args))) > 0
+
+    def command_line_args(self):
+        return self.service_args + ["--webdriver=%d" % self.port]
+
+    @property
+    def service_url(self):
+        """
+        Gets the url of the GhostDriver Service
+        """
+        return "http://localhost:%d/wd/hub" % self.port
+
+    def send_remote_shutdown_command(self):
+        if self._cookie_temp_file:
+            os.close(self._cookie_temp_file_handle)
+            os.remove(self._cookie_temp_file)
diff --git a/selenium/webdriver/phantomjs/webdriver.py b/selenium/webdriver/phantomjs/webdriver.py
new file mode 100644
index 00000000..e07024f7
--- /dev/null
+++ b/selenium/webdriver/phantomjs/webdriver.py
@@ -0,0 +1,80 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import warnings
+
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
+from .service import Service
+
+
+class WebDriver(RemoteWebDriver):
+    """
+    Wrapper to communicate with PhantomJS through Ghostdriver.
+
+    You will need to follow all the directions here:
+    https://github.com/detro/ghostdriver
+    """
+
+    def __init__(self, executable_path="phantomjs",
+                 port=0, desired_capabilities=DesiredCapabilities.PHANTOMJS,
+                 service_args=None, service_log_path=None):
+        """
+        Creates a new instance of the PhantomJS / Ghostdriver.
+
+        Starts the service and then creates new instance of the driver.
+
+        :Args:
+         - executable_path - path to the executable. If the default is used it assumes the executable is in the $PATH
+         - port - port you would like the service to run, if left as 0, a free port will be found.
+         - desired_capabilities: Dictionary object with non-browser specific
+           capabilities only, such as "proxy" or "loggingPref".
+         - service_args : A List of command line arguments to pass to PhantomJS
+         - service_log_path: Path for phantomjs service to log to.
+        """
+        warnings.warn('Selenium support for PhantomJS has been deprecated, please use headless '
+                      'versions of Chrome or Firefox instead')
+        self.service = Service(
+            executable_path,
+            port=port,
+            service_args=service_args,
+            log_path=service_log_path)
+        self.service.start()
+
+        try:
+            RemoteWebDriver.__init__(
+                self,
+                command_executor=self.service.service_url,
+                desired_capabilities=desired_capabilities)
+        except Exception:
+            self.quit()
+            raise
+
+        self._is_remote = False
+
+    def quit(self):
+        """
+        Closes the browser and shuts down the PhantomJS executable
+        that is started when starting the PhantomJS
+        """
+        try:
+            RemoteWebDriver.quit(self)
+        except Exception:
+            # We don't care about the message because something probably has gone wrong
+            pass
+        finally:
+            self.service.stop()
diff --git a/selenium/webdriver/remote/__init__.py b/selenium/webdriver/remote/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/remote/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/remote/command.py b/selenium/webdriver/remote/command.py
new file mode 100644
index 00000000..053e47b7
--- /dev/null
+++ b/selenium/webdriver/remote/command.py
@@ -0,0 +1,174 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+class Command(object):
+    """
+    Defines constants for the standard WebDriver commands.
+
+    While these constants have no meaning in and of themselves, they are
+    used to marshal commands through a service that implements WebDriver's
+    remote wire protocol:
+
+        https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
+
+    """
+
+    # Keep in sync with org.openqa.selenium.remote.DriverCommand
+
+    STATUS = "status"
+    NEW_SESSION = "newSession"
+    GET_ALL_SESSIONS = "getAllSessions"
+    DELETE_SESSION = "deleteSession"
+    NEW_WINDOW = "newWindow"
+    CLOSE = "close"
+    QUIT = "quit"
+    GET = "get"
+    GO_BACK = "goBack"
+    GO_FORWARD = "goForward"
+    REFRESH = "refresh"
+    ADD_COOKIE = "addCookie"
+    GET_COOKIE = "getCookie"
+    GET_ALL_COOKIES = "getCookies"
+    DELETE_COOKIE = "deleteCookie"
+    DELETE_ALL_COOKIES = "deleteAllCookies"
+    FIND_ELEMENT = "findElement"
+    FIND_ELEMENTS = "findElements"
+    FIND_CHILD_ELEMENT = "findChildElement"
+    FIND_CHILD_ELEMENTS = "findChildElements"
+    CLEAR_ELEMENT = "clearElement"
+    CLICK_ELEMENT = "clickElement"
+    SEND_KEYS_TO_ELEMENT = "sendKeysToElement"
+    SEND_KEYS_TO_ACTIVE_ELEMENT = "sendKeysToActiveElement"
+    SUBMIT_ELEMENT = "submitElement"
+    UPLOAD_FILE = "uploadFile"
+    GET_CURRENT_WINDOW_HANDLE = "getCurrentWindowHandle"
+    W3C_GET_CURRENT_WINDOW_HANDLE = "w3cGetCurrentWindowHandle"
+    GET_WINDOW_HANDLES = "getWindowHandles"
+    W3C_GET_WINDOW_HANDLES = "w3cGetWindowHandles"
+    GET_WINDOW_SIZE = "getWindowSize"
+    W3C_GET_WINDOW_SIZE = "w3cGetWindowSize"
+    W3C_GET_WINDOW_POSITION = "w3cGetWindowPosition"
+    GET_WINDOW_POSITION = "getWindowPosition"
+    SET_WINDOW_SIZE = "setWindowSize"
+    W3C_SET_WINDOW_SIZE = "w3cSetWindowSize"
+    SET_WINDOW_RECT = "setWindowRect"
+    GET_WINDOW_RECT = "getWindowRect"
+    SET_WINDOW_POSITION = "setWindowPosition"
+    W3C_SET_WINDOW_POSITION = "w3cSetWindowPosition"
+    SWITCH_TO_WINDOW = "switchToWindow"
+    SWITCH_TO_FRAME = "switchToFrame"
+    SWITCH_TO_PARENT_FRAME = "switchToParentFrame"
+    GET_ACTIVE_ELEMENT = "getActiveElement"
+    W3C_GET_ACTIVE_ELEMENT = "w3cGetActiveElement"
+    GET_CURRENT_URL = "getCurrentUrl"
+    GET_PAGE_SOURCE = "getPageSource"
+    GET_TITLE = "getTitle"
+    EXECUTE_SCRIPT = "executeScript"
+    W3C_EXECUTE_SCRIPT = "w3cExecuteScript"
+    W3C_EXECUTE_SCRIPT_ASYNC = "w3cExecuteScriptAsync"
+    GET_ELEMENT_TEXT = "getElementText"
+    GET_ELEMENT_VALUE = "getElementValue"
+    GET_ELEMENT_TAG_NAME = "getElementTagName"
+    SET_ELEMENT_SELECTED = "setElementSelected"
+    IS_ELEMENT_SELECTED = "isElementSelected"
+    IS_ELEMENT_ENABLED = "isElementEnabled"
+    IS_ELEMENT_DISPLAYED = "isElementDisplayed"
+    GET_ELEMENT_LOCATION = "getElementLocation"
+    GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW = "getElementLocationOnceScrolledIntoView"
+    GET_ELEMENT_SIZE = "getElementSize"
+    GET_ELEMENT_RECT = "getElementRect"
+    GET_ELEMENT_ATTRIBUTE = "getElementAttribute"
+    GET_ELEMENT_PROPERTY = "getElementProperty"
+    GET_ELEMENT_VALUE_OF_CSS_PROPERTY = "getElementValueOfCssProperty"
+    SCREENSHOT = "screenshot"
+    ELEMENT_SCREENSHOT = "elementScreenshot"
+    IMPLICIT_WAIT = "implicitlyWait"
+    EXECUTE_ASYNC_SCRIPT = "executeAsyncScript"
+    SET_SCRIPT_TIMEOUT = "setScriptTimeout"
+    SET_TIMEOUTS = "setTimeouts"
+    MAXIMIZE_WINDOW = "windowMaximize"
+    W3C_MAXIMIZE_WINDOW = "w3cMaximizeWindow"
+    GET_LOG = "getLog"
+    GET_AVAILABLE_LOG_TYPES = "getAvailableLogTypes"
+    FULLSCREEN_WINDOW = "fullscreenWindow"
+    MINIMIZE_WINDOW = "minimizeWindow"
+
+    # Alerts
+    DISMISS_ALERT = "dismissAlert"
+    W3C_DISMISS_ALERT = "w3cDismissAlert"
+    ACCEPT_ALERT = "acceptAlert"
+    W3C_ACCEPT_ALERT = "w3cAcceptAlert"
+    SET_ALERT_VALUE = "setAlertValue"
+    W3C_SET_ALERT_VALUE = "w3cSetAlertValue"
+    GET_ALERT_TEXT = "getAlertText"
+    W3C_GET_ALERT_TEXT = "w3cGetAlertText"
+    SET_ALERT_CREDENTIALS = "setAlertCredentials"
+
+    # Advanced user interactions
+    W3C_ACTIONS = "actions"
+    W3C_CLEAR_ACTIONS = "clearActionState"
+    CLICK = "mouseClick"
+    DOUBLE_CLICK = "mouseDoubleClick"
+    MOUSE_DOWN = "mouseButtonDown"
+    MOUSE_UP = "mouseButtonUp"
+    MOVE_TO = "mouseMoveTo"
+
+    # Screen Orientation
+    SET_SCREEN_ORIENTATION = "setScreenOrientation"
+    GET_SCREEN_ORIENTATION = "getScreenOrientation"
+
+    # Touch Actions
+    SINGLE_TAP = "touchSingleTap"
+    TOUCH_DOWN = "touchDown"
+    TOUCH_UP = "touchUp"
+    TOUCH_MOVE = "touchMove"
+    TOUCH_SCROLL = "touchScroll"
+    DOUBLE_TAP = "touchDoubleTap"
+    LONG_PRESS = "touchLongPress"
+    FLICK = "touchFlick"
+
+    # HTML 5
+    EXECUTE_SQL = "executeSql"
+
+    GET_LOCATION = "getLocation"
+    SET_LOCATION = "setLocation"
+
+    GET_APP_CACHE = "getAppCache"
+    GET_APP_CACHE_STATUS = "getAppCacheStatus"
+    CLEAR_APP_CACHE = "clearAppCache"
+
+    GET_LOCAL_STORAGE_ITEM = "getLocalStorageItem"
+    REMOVE_LOCAL_STORAGE_ITEM = "removeLocalStorageItem"
+    GET_LOCAL_STORAGE_KEYS = "getLocalStorageKeys"
+    SET_LOCAL_STORAGE_ITEM = "setLocalStorageItem"
+    CLEAR_LOCAL_STORAGE = "clearLocalStorage"
+    GET_LOCAL_STORAGE_SIZE = "getLocalStorageSize"
+
+    GET_SESSION_STORAGE_ITEM = "getSessionStorageItem"
+    REMOVE_SESSION_STORAGE_ITEM = "removeSessionStorageItem"
+    GET_SESSION_STORAGE_KEYS = "getSessionStorageKeys"
+    SET_SESSION_STORAGE_ITEM = "setSessionStorageItem"
+    CLEAR_SESSION_STORAGE = "clearSessionStorage"
+    GET_SESSION_STORAGE_SIZE = "getSessionStorageSize"
+
+    # Mobile
+    GET_NETWORK_CONNECTION = "getNetworkConnection"
+    SET_NETWORK_CONNECTION = "setNetworkConnection"
+    CURRENT_CONTEXT_HANDLE = "getCurrentContextHandle"
+    CONTEXT_HANDLES = "getContextHandles"
+    SWITCH_TO_CONTEXT = "switchToContext"
diff --git a/selenium/webdriver/remote/errorhandler.py b/selenium/webdriver/remote/errorhandler.py
new file mode 100644
index 00000000..b74da3e0
--- /dev/null
+++ b/selenium/webdriver/remote/errorhandler.py
@@ -0,0 +1,242 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.common.exceptions import (ElementClickInterceptedException,
+                                        ElementNotInteractableException,
+                                        ElementNotSelectableException,
+                                        ElementNotVisibleException,
+                                        InsecureCertificateException,
+                                        InvalidCoordinatesException,
+                                        InvalidElementStateException,
+                                        InvalidSessionIdException,
+                                        InvalidSelectorException,
+                                        ImeNotAvailableException,
+                                        ImeActivationFailedException,
+                                        InvalidArgumentException,
+                                        InvalidCookieDomainException,
+                                        JavascriptException,
+                                        MoveTargetOutOfBoundsException,
+                                        NoSuchCookieException,
+                                        NoSuchElementException,
+                                        NoSuchFrameException,
+                                        NoSuchWindowException,
+                                        NoAlertPresentException,
+                                        ScreenshotException,
+                                        SessionNotCreatedException,
+                                        StaleElementReferenceException,
+                                        TimeoutException,
+                                        UnableToSetCookieException,
+                                        UnexpectedAlertPresentException,
+                                        UnknownMethodException,
+                                        WebDriverException)
+
+try:
+    basestring
+except NameError:  # Python 3.x
+    basestring = str
+
+
+class ErrorCode(object):
+    """
+    Error codes defined in the WebDriver wire protocol.
+    """
+    # Keep in sync with org.openqa.selenium.remote.ErrorCodes and errorcodes.h
+    SUCCESS = 0
+    NO_SUCH_ELEMENT = [7, 'no such element']
+    NO_SUCH_FRAME = [8, 'no such frame']
+    UNKNOWN_COMMAND = [9, 'unknown command']
+    STALE_ELEMENT_REFERENCE = [10, 'stale element reference']
+    ELEMENT_NOT_VISIBLE = [11, 'element not visible']
+    INVALID_ELEMENT_STATE = [12, 'invalid element state']
+    UNKNOWN_ERROR = [13, 'unknown error']
+    ELEMENT_IS_NOT_SELECTABLE = [15, 'element not selectable']
+    JAVASCRIPT_ERROR = [17, 'javascript error']
+    XPATH_LOOKUP_ERROR = [19, 'invalid selector']
+    TIMEOUT = [21, 'timeout']
+    NO_SUCH_WINDOW = [23, 'no such window']
+    INVALID_COOKIE_DOMAIN = [24, 'invalid cookie domain']
+    UNABLE_TO_SET_COOKIE = [25, 'unable to set cookie']
+    UNEXPECTED_ALERT_OPEN = [26, 'unexpected alert open']
+    NO_ALERT_OPEN = [27, 'no such alert']
+    SCRIPT_TIMEOUT = [28, 'script timeout']
+    INVALID_ELEMENT_COORDINATES = [29, 'invalid element coordinates']
+    IME_NOT_AVAILABLE = [30, 'ime not available']
+    IME_ENGINE_ACTIVATION_FAILED = [31, 'ime engine activation failed']
+    INVALID_SELECTOR = [32, 'invalid selector']
+    SESSION_NOT_CREATED = [33, 'session not created']
+    MOVE_TARGET_OUT_OF_BOUNDS = [34, 'move target out of bounds']
+    INVALID_XPATH_SELECTOR = [51, 'invalid selector']
+    INVALID_XPATH_SELECTOR_RETURN_TYPER = [52, 'invalid selector']
+
+    ELEMENT_NOT_INTERACTABLE = [60, 'element not interactable']
+    INSECURE_CERTIFICATE = ['insecure certificate']
+    INVALID_ARGUMENT = [61, 'invalid argument']
+    INVALID_COORDINATES = ['invalid coordinates']
+    INVALID_SESSION_ID = ['invalid session id']
+    NO_SUCH_COOKIE = [62, 'no such cookie']
+    UNABLE_TO_CAPTURE_SCREEN = [63, 'unable to capture screen']
+    ELEMENT_CLICK_INTERCEPTED = [64, 'element click intercepted']
+    UNKNOWN_METHOD = ['unknown method exception']
+
+    METHOD_NOT_ALLOWED = [405, 'unsupported operation']
+
+
+class ErrorHandler(object):
+    """
+    Handles errors returned by the WebDriver server.
+    """
+    def check_response(self, response):
+        """
+        Checks that a JSON response from the WebDriver does not have an error.
+
+        :Args:
+         - response - The JSON response from the WebDriver server as a dictionary
+           object.
+
+        :Raises: If the response contains an error message.
+        """
+        status = response.get('status', None)
+        if status is None or status == ErrorCode.SUCCESS:
+            return
+        value = None
+        message = response.get("message", "")
+        screen = response.get("screen", "")
+        stacktrace = None
+        if isinstance(status, int):
+            value_json = response.get('value', None)
+            if value_json and isinstance(value_json, basestring):
+                import json
+                try:
+                    value = json.loads(value_json)
+                    if len(value.keys()) == 1:
+                        value = value['value']
+                    status = value.get('error', None)
+                    if status is None:
+                        status = value["status"]
+                        message = value["value"]
+                        if not isinstance(message, basestring):
+                            value = message
+                            message = message.get('message')
+                    else:
+                        message = value.get('message', None)
+                except ValueError:
+                    pass
+
+        if status in ErrorCode.NO_SUCH_ELEMENT:
+            exception_class = NoSuchElementException
+        elif status in ErrorCode.NO_SUCH_FRAME:
+            exception_class = NoSuchFrameException
+        elif status in ErrorCode.NO_SUCH_WINDOW:
+            exception_class = NoSuchWindowException
+        elif status in ErrorCode.STALE_ELEMENT_REFERENCE:
+            exception_class = StaleElementReferenceException
+        elif status in ErrorCode.ELEMENT_NOT_VISIBLE:
+            exception_class = ElementNotVisibleException
+        elif status in ErrorCode.INVALID_ELEMENT_STATE:
+            exception_class = InvalidElementStateException
+        elif status in ErrorCode.INVALID_SELECTOR \
+                or status in ErrorCode.INVALID_XPATH_SELECTOR \
+                or status in ErrorCode.INVALID_XPATH_SELECTOR_RETURN_TYPER:
+            exception_class = InvalidSelectorException
+        elif status in ErrorCode.ELEMENT_IS_NOT_SELECTABLE:
+            exception_class = ElementNotSelectableException
+        elif status in ErrorCode.ELEMENT_NOT_INTERACTABLE:
+            exception_class = ElementNotInteractableException
+        elif status in ErrorCode.INVALID_COOKIE_DOMAIN:
+            exception_class = InvalidCookieDomainException
+        elif status in ErrorCode.UNABLE_TO_SET_COOKIE:
+            exception_class = UnableToSetCookieException
+        elif status in ErrorCode.TIMEOUT:
+            exception_class = TimeoutException
+        elif status in ErrorCode.SCRIPT_TIMEOUT:
+            exception_class = TimeoutException
+        elif status in ErrorCode.UNKNOWN_ERROR:
+            exception_class = WebDriverException
+        elif status in ErrorCode.UNEXPECTED_ALERT_OPEN:
+            exception_class = UnexpectedAlertPresentException
+        elif status in ErrorCode.NO_ALERT_OPEN:
+            exception_class = NoAlertPresentException
+        elif status in ErrorCode.IME_NOT_AVAILABLE:
+            exception_class = ImeNotAvailableException
+        elif status in ErrorCode.IME_ENGINE_ACTIVATION_FAILED:
+            exception_class = ImeActivationFailedException
+        elif status in ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS:
+            exception_class = MoveTargetOutOfBoundsException
+        elif status in ErrorCode.JAVASCRIPT_ERROR:
+            exception_class = JavascriptException
+        elif status in ErrorCode.SESSION_NOT_CREATED:
+            exception_class = SessionNotCreatedException
+        elif status in ErrorCode.INVALID_ARGUMENT:
+            exception_class = InvalidArgumentException
+        elif status in ErrorCode.NO_SUCH_COOKIE:
+            exception_class = NoSuchCookieException
+        elif status in ErrorCode.UNABLE_TO_CAPTURE_SCREEN:
+            exception_class = ScreenshotException
+        elif status in ErrorCode.ELEMENT_CLICK_INTERCEPTED:
+            exception_class = ElementClickInterceptedException
+        elif status in ErrorCode.INSECURE_CERTIFICATE:
+            exception_class = InsecureCertificateException
+        elif status in ErrorCode.INVALID_COORDINATES:
+            exception_class = InvalidCoordinatesException
+        elif status in ErrorCode.INVALID_SESSION_ID:
+            exception_class = InvalidSessionIdException
+        elif status in ErrorCode.UNKNOWN_METHOD:
+            exception_class = UnknownMethodException
+        else:
+            exception_class = WebDriverException
+        if value == '' or value is None:
+            value = response['value']
+        if isinstance(value, basestring):
+            raise exception_class(value)
+        if message == "" and 'message' in value:
+            message = value['message']
+
+        screen = None
+        if 'screen' in value:
+            screen = value['screen']
+
+        stacktrace = None
+        st_value = value.get('stackTrace') or value.get('stacktrace')
+        if st_value:
+            if isinstance(st_value, basestring):
+                st_value = st_value.split('\n')
+            stacktrace = []
+            try:
+                for frame in st_value:
+                    line = self._value_or_default(frame, 'lineNumber', '')
+                    file = self._value_or_default(frame, 'fileName', '<anonymous>')
+                    if line:
+                        file = "%s:%s" % (file, line)
+                    meth = self._value_or_default(frame, 'methodName', '<anonymous>')
+                    if 'className' in frame:
+                        meth = "%s.%s" % (frame['className'], meth)
+                    msg = "    at %s (%s)"
+                    msg = msg % (meth, file)
+                    stacktrace.append(msg)
+            except TypeError:
+                pass
+        if exception_class == UnexpectedAlertPresentException:
+            alert_text = None
+            if 'data' in value:
+                alert_text = value['data'].get('text')
+            elif 'alert' in value:
+                alert_text = value['alert'].get('text')
+            raise exception_class(message, screen, stacktrace, alert_text)
+        raise exception_class(message, screen, stacktrace)
+
+    def _value_or_default(self, obj, key, default):
+        return obj[key] if key in obj else default
diff --git a/selenium/webdriver/remote/file_detector.py b/selenium/webdriver/remote/file_detector.py
new file mode 100644
index 00000000..caec9c30
--- /dev/null
+++ b/selenium/webdriver/remote/file_detector.py
@@ -0,0 +1,58 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import abc
+import os
+from selenium.webdriver.common.utils import keys_to_typing
+
+
+class FileDetector(object):
+    """
+    Used for identifying whether a sequence of chars represents the path to a
+    file.
+    """
+    __metaclass__ = abc.ABCMeta
+
+    @abc.abstractmethod
+    def is_local_file(self, *keys):
+        return
+
+
+class UselessFileDetector(FileDetector):
+    """
+    A file detector that never finds anything.
+    """
+    def is_local_file(self, *keys):
+        return None
+
+
+class LocalFileDetector(FileDetector):
+    """
+    Detects files on the local disk.
+    """
+    def is_local_file(self, *keys):
+        file_path = ''.join(keys_to_typing(keys))
+
+        if not file_path:
+            return None
+
+        try:
+            if os.path.isfile(file_path):
+                return file_path
+        except Exception:
+            pass
+        return None
diff --git a/selenium/webdriver/remote/mobile.py b/selenium/webdriver/remote/mobile.py
new file mode 100644
index 00000000..a0da2498
--- /dev/null
+++ b/selenium/webdriver/remote/mobile.py
@@ -0,0 +1,85 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from .command import Command
+
+
+class Mobile(object):
+
+    class ConnectionType(object):
+
+        def __init__(self, mask):
+            self.mask = mask
+
+        @property
+        def airplane_mode(self):
+            return self.mask % 2 == 1
+
+        @property
+        def wifi(self):
+            return (self.mask / 2) % 2 == 1
+
+        @property
+        def data(self):
+            return (self.mask / 4) > 0
+
+    ALL_NETWORK = ConnectionType(6)
+    WIFI_NETWORK = ConnectionType(2)
+    DATA_NETWORK = ConnectionType(4)
+    AIRPLANE_MODE = ConnectionType(1)
+
+    def __init__(self, driver):
+        self._driver = driver
+
+    @property
+    def network_connection(self):
+        return self.ConnectionType(self._driver.execute(Command.GET_NETWORK_CONNECTION)['value'])
+
+    def set_network_connection(self, network):
+        """
+        Set the network connection for the remote device.
+
+        Example of setting airplane mode::
+
+            driver.mobile.set_network_connection(driver.mobile.AIRPLANE_MODE)
+        """
+        mode = network.mask if isinstance(network, self.ConnectionType) else network
+        return self.ConnectionType(self._driver.execute(
+            Command.SET_NETWORK_CONNECTION, {
+                'name': 'network_connection',
+                'parameters': {'type': mode}})['value'])
+
+    @property
+    def context(self):
+        """
+        returns the current context (Native or WebView).
+        """
+        return self._driver.execute(Command.CURRENT_CONTEXT_HANDLE)
+
+    @property
+    def contexts(self):
+        """
+        returns a list of available contexts
+        """
+        return self._driver.execute(Command.CONTEXT_HANDLES)
+
+    @context.setter
+    def context(self, new_context):
+        """
+        sets the current context
+        """
+        self._driver.execute(Command.SWITCH_TO_CONTEXT, {"name": new_context})
diff --git a/selenium/webdriver/remote/remote_connection.py b/selenium/webdriver/remote/remote_connection.py
new file mode 100644
index 00000000..9098bb5f
--- /dev/null
+++ b/selenium/webdriver/remote/remote_connection.py
@@ -0,0 +1,449 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import base64
+import logging
+import platform
+import socket
+import string
+
+import urllib3
+
+try:
+    from urllib import parse
+except ImportError:  # above is available in py3+, below is py2.7
+    import urlparse as parse
+
+from selenium.webdriver.common import utils as common_utils
+from selenium import __version__
+from .command import Command
+from .errorhandler import ErrorCode
+from . import utils
+
+LOGGER = logging.getLogger(__name__)
+
+
+class RemoteConnection(object):
+    """A connection with the Remote WebDriver server.
+
+    Communicates with the server using the WebDriver wire protocol:
+    https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol"""
+
+    _timeout = socket._GLOBAL_DEFAULT_TIMEOUT
+
+    @classmethod
+    def get_timeout(cls):
+        """
+        :Returns:
+            Timeout value in seconds for all http requests made to the Remote Connection
+        """
+        return None if cls._timeout == socket._GLOBAL_DEFAULT_TIMEOUT else cls._timeout
+
+    @classmethod
+    def set_timeout(cls, timeout):
+        """
+        Override the default timeout
+
+        :Args:
+            - timeout - timeout value for http requests in seconds
+        """
+        cls._timeout = timeout
+
+    @classmethod
+    def reset_timeout(cls):
+        """
+        Reset the http request timeout to socket._GLOBAL_DEFAULT_TIMEOUT
+        """
+        cls._timeout = socket._GLOBAL_DEFAULT_TIMEOUT
+
+    @classmethod
+    def get_remote_connection_headers(cls, parsed_url, keep_alive=False):
+        """
+        Get headers for remote request.
+
+        :Args:
+         - parsed_url - The parsed url
+         - keep_alive (Boolean) - Is this a keep-alive connection (default: False)
+        """
+
+        system = platform.system().lower()
+        if system == "darwin":
+            system = "mac"
+
+        headers = {
+            'Accept': 'application/json',
+            'Content-Type': 'application/json;charset=UTF-8',
+            'User-Agent': 'selenium/{} (python {})'.format(__version__, system)
+        }
+
+        if parsed_url.username:
+            base64string = base64.b64encode('{0.username}:{0.password}'.format(parsed_url).encode())
+            headers.update({
+                'Authorization': 'Basic {}'.format(base64string.decode())
+            })
+
+        if keep_alive:
+            headers.update({
+                'Connection': 'keep-alive'
+            })
+
+        return headers
+
+    def __init__(self, remote_server_addr, keep_alive=False, resolve_ip=True):
+        # Attempt to resolve the hostname and get an IP address.
+        self.keep_alive = keep_alive
+        parsed_url = parse.urlparse(remote_server_addr)
+        if parsed_url.hostname and resolve_ip:
+            port = parsed_url.port or None
+            if parsed_url.scheme == "https":
+                ip = parsed_url.hostname
+            elif port and not common_utils.is_connectable(port, parsed_url.hostname):
+                ip = None
+                LOGGER.info('Could not connect to port {} on host '
+                            '{}'.format(port, parsed_url.hostname))
+            else:
+                ip = common_utils.find_connectable_ip(parsed_url.hostname,
+                                                      port=port)
+            if ip:
+                netloc = ip
+                if parsed_url.port:
+                    netloc = common_utils.join_host_port(netloc,
+                                                         parsed_url.port)
+                if parsed_url.username:
+                    auth = parsed_url.username
+                    if parsed_url.password:
+                        auth += ':%s' % parsed_url.password
+                    netloc = '%s@%s' % (auth, netloc)
+                remote_server_addr = parse.urlunparse(
+                    (parsed_url.scheme, netloc, parsed_url.path,
+                     parsed_url.params, parsed_url.query, parsed_url.fragment))
+            else:
+                LOGGER.info('Could not get IP address for host: %s' %
+                            parsed_url.hostname)
+
+        self._url = remote_server_addr
+        if keep_alive:
+            self._conn = urllib3.PoolManager(timeout=self._timeout)
+
+        self._commands = {
+            Command.STATUS: ('GET', '/status'),
+            Command.NEW_SESSION: ('POST', '/session'),
+            Command.GET_ALL_SESSIONS: ('GET', '/sessions'),
+            Command.QUIT: ('DELETE', '/session/$sessionId'),
+            Command.GET_CURRENT_WINDOW_HANDLE:
+                ('GET', '/session/$sessionId/window_handle'),
+            Command.W3C_GET_CURRENT_WINDOW_HANDLE:
+                ('GET', '/session/$sessionId/window'),
+            Command.GET_WINDOW_HANDLES:
+                ('GET', '/session/$sessionId/window_handles'),
+            Command.W3C_GET_WINDOW_HANDLES:
+                ('GET', '/session/$sessionId/window/handles'),
+            Command.GET: ('POST', '/session/$sessionId/url'),
+            Command.GO_FORWARD: ('POST', '/session/$sessionId/forward'),
+            Command.GO_BACK: ('POST', '/session/$sessionId/back'),
+            Command.REFRESH: ('POST', '/session/$sessionId/refresh'),
+            Command.EXECUTE_SCRIPT: ('POST', '/session/$sessionId/execute'),
+            Command.W3C_EXECUTE_SCRIPT:
+                ('POST', '/session/$sessionId/execute/sync'),
+            Command.W3C_EXECUTE_SCRIPT_ASYNC:
+                ('POST', '/session/$sessionId/execute/async'),
+            Command.GET_CURRENT_URL: ('GET', '/session/$sessionId/url'),
+            Command.GET_TITLE: ('GET', '/session/$sessionId/title'),
+            Command.GET_PAGE_SOURCE: ('GET', '/session/$sessionId/source'),
+            Command.SCREENSHOT: ('GET', '/session/$sessionId/screenshot'),
+            Command.ELEMENT_SCREENSHOT: ('GET', '/session/$sessionId/element/$id/screenshot'),
+            Command.FIND_ELEMENT: ('POST', '/session/$sessionId/element'),
+            Command.FIND_ELEMENTS: ('POST', '/session/$sessionId/elements'),
+            Command.W3C_GET_ACTIVE_ELEMENT: ('GET', '/session/$sessionId/element/active'),
+            Command.GET_ACTIVE_ELEMENT:
+                ('POST', '/session/$sessionId/element/active'),
+            Command.FIND_CHILD_ELEMENT:
+                ('POST', '/session/$sessionId/element/$id/element'),
+            Command.FIND_CHILD_ELEMENTS:
+                ('POST', '/session/$sessionId/element/$id/elements'),
+            Command.CLICK_ELEMENT: ('POST', '/session/$sessionId/element/$id/click'),
+            Command.CLEAR_ELEMENT: ('POST', '/session/$sessionId/element/$id/clear'),
+            Command.SUBMIT_ELEMENT: ('POST', '/session/$sessionId/element/$id/submit'),
+            Command.GET_ELEMENT_TEXT: ('GET', '/session/$sessionId/element/$id/text'),
+            Command.SEND_KEYS_TO_ELEMENT:
+                ('POST', '/session/$sessionId/element/$id/value'),
+            Command.SEND_KEYS_TO_ACTIVE_ELEMENT:
+                ('POST', '/session/$sessionId/keys'),
+            Command.UPLOAD_FILE: ('POST', "/session/$sessionId/file"),
+            Command.GET_ELEMENT_VALUE:
+                ('GET', '/session/$sessionId/element/$id/value'),
+            Command.GET_ELEMENT_TAG_NAME:
+                ('GET', '/session/$sessionId/element/$id/name'),
+            Command.IS_ELEMENT_SELECTED:
+                ('GET', '/session/$sessionId/element/$id/selected'),
+            Command.SET_ELEMENT_SELECTED:
+                ('POST', '/session/$sessionId/element/$id/selected'),
+            Command.IS_ELEMENT_ENABLED:
+                ('GET', '/session/$sessionId/element/$id/enabled'),
+            Command.IS_ELEMENT_DISPLAYED:
+                ('GET', '/session/$sessionId/element/$id/displayed'),
+            Command.GET_ELEMENT_LOCATION:
+                ('GET', '/session/$sessionId/element/$id/location'),
+            Command.GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW:
+                ('GET', '/session/$sessionId/element/$id/location_in_view'),
+            Command.GET_ELEMENT_SIZE:
+                ('GET', '/session/$sessionId/element/$id/size'),
+            Command.GET_ELEMENT_RECT:
+                ('GET', '/session/$sessionId/element/$id/rect'),
+            Command.GET_ELEMENT_ATTRIBUTE:
+                ('GET', '/session/$sessionId/element/$id/attribute/$name'),
+            Command.GET_ELEMENT_PROPERTY:
+                ('GET', '/session/$sessionId/element/$id/property/$name'),
+            Command.GET_ALL_COOKIES: ('GET', '/session/$sessionId/cookie'),
+            Command.ADD_COOKIE: ('POST', '/session/$sessionId/cookie'),
+            Command.GET_COOKIE: ('GET', '/session/$sessionId/cookie/$name'),
+            Command.DELETE_ALL_COOKIES:
+                ('DELETE', '/session/$sessionId/cookie'),
+            Command.DELETE_COOKIE:
+                ('DELETE', '/session/$sessionId/cookie/$name'),
+            Command.SWITCH_TO_FRAME: ('POST', '/session/$sessionId/frame'),
+            Command.SWITCH_TO_PARENT_FRAME: ('POST', '/session/$sessionId/frame/parent'),
+            Command.SWITCH_TO_WINDOW: ('POST', '/session/$sessionId/window'),
+            Command.NEW_WINDOW: ('POST', '/session/$sessionId/window/new'),
+            Command.CLOSE: ('DELETE', '/session/$sessionId/window'),
+            Command.GET_ELEMENT_VALUE_OF_CSS_PROPERTY:
+                ('GET', '/session/$sessionId/element/$id/css/$propertyName'),
+            Command.IMPLICIT_WAIT:
+                ('POST', '/session/$sessionId/timeouts/implicit_wait'),
+            Command.EXECUTE_ASYNC_SCRIPT: ('POST', '/session/$sessionId/execute_async'),
+            Command.SET_SCRIPT_TIMEOUT:
+                ('POST', '/session/$sessionId/timeouts/async_script'),
+            Command.SET_TIMEOUTS:
+                ('POST', '/session/$sessionId/timeouts'),
+            Command.DISMISS_ALERT:
+                ('POST', '/session/$sessionId/dismiss_alert'),
+            Command.W3C_DISMISS_ALERT:
+                ('POST', '/session/$sessionId/alert/dismiss'),
+            Command.ACCEPT_ALERT:
+                ('POST', '/session/$sessionId/accept_alert'),
+            Command.W3C_ACCEPT_ALERT:
+                ('POST', '/session/$sessionId/alert/accept'),
+            Command.SET_ALERT_VALUE:
+                ('POST', '/session/$sessionId/alert_text'),
+            Command.W3C_SET_ALERT_VALUE:
+                ('POST', '/session/$sessionId/alert/text'),
+            Command.GET_ALERT_TEXT:
+                ('GET', '/session/$sessionId/alert_text'),
+            Command.W3C_GET_ALERT_TEXT:
+                ('GET', '/session/$sessionId/alert/text'),
+            Command.SET_ALERT_CREDENTIALS:
+                ('POST', '/session/$sessionId/alert/credentials'),
+            Command.CLICK:
+                ('POST', '/session/$sessionId/click'),
+            Command.W3C_ACTIONS:
+                ('POST', '/session/$sessionId/actions'),
+            Command.W3C_CLEAR_ACTIONS:
+                ('DELETE', '/session/$sessionId/actions'),
+            Command.DOUBLE_CLICK:
+                ('POST', '/session/$sessionId/doubleclick'),
+            Command.MOUSE_DOWN:
+                ('POST', '/session/$sessionId/buttondown'),
+            Command.MOUSE_UP:
+                ('POST', '/session/$sessionId/buttonup'),
+            Command.MOVE_TO:
+                ('POST', '/session/$sessionId/moveto'),
+            Command.GET_WINDOW_SIZE:
+                ('GET', '/session/$sessionId/window/$windowHandle/size'),
+            Command.SET_WINDOW_SIZE:
+                ('POST', '/session/$sessionId/window/$windowHandle/size'),
+            Command.GET_WINDOW_POSITION:
+                ('GET', '/session/$sessionId/window/$windowHandle/position'),
+            Command.SET_WINDOW_POSITION:
+                ('POST', '/session/$sessionId/window/$windowHandle/position'),
+            Command.SET_WINDOW_RECT:
+                ('POST', '/session/$sessionId/window/rect'),
+            Command.GET_WINDOW_RECT:
+                ('GET', '/session/$sessionId/window/rect'),
+            Command.MAXIMIZE_WINDOW:
+                ('POST', '/session/$sessionId/window/$windowHandle/maximize'),
+            Command.W3C_MAXIMIZE_WINDOW:
+                ('POST', '/session/$sessionId/window/maximize'),
+            Command.SET_SCREEN_ORIENTATION:
+                ('POST', '/session/$sessionId/orientation'),
+            Command.GET_SCREEN_ORIENTATION:
+                ('GET', '/session/$sessionId/orientation'),
+            Command.SINGLE_TAP:
+                ('POST', '/session/$sessionId/touch/click'),
+            Command.TOUCH_DOWN:
+                ('POST', '/session/$sessionId/touch/down'),
+            Command.TOUCH_UP:
+                ('POST', '/session/$sessionId/touch/up'),
+            Command.TOUCH_MOVE:
+                ('POST', '/session/$sessionId/touch/move'),
+            Command.TOUCH_SCROLL:
+                ('POST', '/session/$sessionId/touch/scroll'),
+            Command.DOUBLE_TAP:
+                ('POST', '/session/$sessionId/touch/doubleclick'),
+            Command.LONG_PRESS:
+                ('POST', '/session/$sessionId/touch/longclick'),
+            Command.FLICK:
+                ('POST', '/session/$sessionId/touch/flick'),
+            Command.EXECUTE_SQL:
+                ('POST', '/session/$sessionId/execute_sql'),
+            Command.GET_LOCATION:
+                ('GET', '/session/$sessionId/location'),
+            Command.SET_LOCATION:
+                ('POST', '/session/$sessionId/location'),
+            Command.GET_APP_CACHE:
+                ('GET', '/session/$sessionId/application_cache'),
+            Command.GET_APP_CACHE_STATUS:
+                ('GET', '/session/$sessionId/application_cache/status'),
+            Command.CLEAR_APP_CACHE:
+                ('DELETE', '/session/$sessionId/application_cache/clear'),
+            Command.GET_NETWORK_CONNECTION:
+                ('GET', '/session/$sessionId/network_connection'),
+            Command.SET_NETWORK_CONNECTION:
+                ('POST', '/session/$sessionId/network_connection'),
+            Command.GET_LOCAL_STORAGE_ITEM:
+                ('GET', '/session/$sessionId/local_storage/key/$key'),
+            Command.REMOVE_LOCAL_STORAGE_ITEM:
+                ('DELETE', '/session/$sessionId/local_storage/key/$key'),
+            Command.GET_LOCAL_STORAGE_KEYS:
+                ('GET', '/session/$sessionId/local_storage'),
+            Command.SET_LOCAL_STORAGE_ITEM:
+                ('POST', '/session/$sessionId/local_storage'),
+            Command.CLEAR_LOCAL_STORAGE:
+                ('DELETE', '/session/$sessionId/local_storage'),
+            Command.GET_LOCAL_STORAGE_SIZE:
+                ('GET', '/session/$sessionId/local_storage/size'),
+            Command.GET_SESSION_STORAGE_ITEM:
+                ('GET', '/session/$sessionId/session_storage/key/$key'),
+            Command.REMOVE_SESSION_STORAGE_ITEM:
+                ('DELETE', '/session/$sessionId/session_storage/key/$key'),
+            Command.GET_SESSION_STORAGE_KEYS:
+                ('GET', '/session/$sessionId/session_storage'),
+            Command.SET_SESSION_STORAGE_ITEM:
+                ('POST', '/session/$sessionId/session_storage'),
+            Command.CLEAR_SESSION_STORAGE:
+                ('DELETE', '/session/$sessionId/session_storage'),
+            Command.GET_SESSION_STORAGE_SIZE:
+                ('GET', '/session/$sessionId/session_storage/size'),
+            Command.GET_LOG:
+                ('POST', '/session/$sessionId/log'),
+            Command.GET_AVAILABLE_LOG_TYPES:
+                ('GET', '/session/$sessionId/log/types'),
+            Command.CURRENT_CONTEXT_HANDLE:
+                ('GET', '/session/$sessionId/context'),
+            Command.CONTEXT_HANDLES:
+                ('GET', '/session/$sessionId/contexts'),
+            Command.SWITCH_TO_CONTEXT:
+                ('POST', '/session/$sessionId/context'),
+            Command.FULLSCREEN_WINDOW:
+                ('POST', '/session/$sessionId/window/fullscreen'),
+            Command.MINIMIZE_WINDOW:
+                ('POST', '/session/$sessionId/window/minimize')
+        }
+
+    def execute(self, command, params):
+        """
+        Send a command to the remote server.
+
+        Any path subtitutions required for the URL mapped to the command should be
+        included in the command parameters.
+
+        :Args:
+         - command - A string specifying the command to execute.
+         - params - A dictionary of named parameters to send with the command as
+           its JSON payload.
+        """
+        command_info = self._commands[command]
+        assert command_info is not None, 'Unrecognised command %s' % command
+        path = string.Template(command_info[1]).substitute(params)
+        if hasattr(self, 'w3c') and self.w3c and isinstance(params, dict) and 'sessionId' in params:
+            del params['sessionId']
+        data = utils.dump_json(params)
+        url = '%s%s' % (self._url, path)
+        return self._request(command_info[0], url, body=data)
+
+    def _request(self, method, url, body=None):
+        """
+        Send an HTTP request to the remote server.
+
+        :Args:
+         - method - A string for the HTTP method to send the request with.
+         - url - A string for the URL to send the request to.
+         - body - A string for request body. Ignored unless method is POST or PUT.
+
+        :Returns:
+          A dictionary with the server's parsed JSON response.
+        """
+        LOGGER.debug('%s %s %s' % (method, url, body))
+
+        parsed_url = parse.urlparse(url)
+        headers = self.get_remote_connection_headers(parsed_url, self.keep_alive)
+        resp = None
+        if body and method != 'POST' and method != 'PUT':
+            body = None
+
+        if self.keep_alive:
+            resp = self._conn.request(method, url, body=body, headers=headers)
+
+            statuscode = resp.status
+        else:
+            http = urllib3.PoolManager(timeout=self._timeout)
+            resp = http.request(method, url, body=body, headers=headers)
+
+            statuscode = resp.status
+            if not hasattr(resp, 'getheader'):
+                if hasattr(resp.headers, 'getheader'):
+                    resp.getheader = lambda x: resp.headers.getheader(x)
+                elif hasattr(resp.headers, 'get'):
+                    resp.getheader = lambda x: resp.headers.get(x)
+
+        data = resp.data.decode('UTF-8')
+        try:
+            if 300 <= statuscode < 304:
+                return self._request('GET', resp.getheader('location'))
+            if 399 < statuscode <= 500:
+                return {'status': statuscode, 'value': data}
+            content_type = []
+            if resp.getheader('Content-Type') is not None:
+                content_type = resp.getheader('Content-Type').split(';')
+            if not any([x.startswith('image/png') for x in content_type]):
+
+                try:
+                    data = utils.load_json(data.strip())
+                except ValueError:
+                    if 199 < statuscode < 300:
+                        status = ErrorCode.SUCCESS
+                    else:
+                        status = ErrorCode.UNKNOWN_ERROR
+                    return {'status': status, 'value': data.strip()}
+
+                # Some of the drivers incorrectly return a response
+                # with no 'value' field when they should return null.
+                if 'value' not in data:
+                    data['value'] = None
+                return data
+            else:
+                data = {'status': 0, 'value': data}
+                return data
+        finally:
+            LOGGER.debug("Finished Request")
+            resp.close()
+
+    def close(self):
+        """
+        Clean up resources when finished with the remote_connection
+        """
+        if hasattr(self, '_conn'):
+            self._conn.clear()
diff --git a/selenium/webdriver/remote/switch_to.py b/selenium/webdriver/remote/switch_to.py
new file mode 100644
index 00000000..a1aaacbb
--- /dev/null
+++ b/selenium/webdriver/remote/switch_to.py
@@ -0,0 +1,160 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from .command import Command
+from selenium.webdriver.common.alert import Alert
+from selenium.webdriver.common.by import By
+from selenium.common.exceptions import NoSuchElementException, NoSuchFrameException, NoSuchWindowException
+
+try:
+    basestring
+except NameError:
+    basestring = str
+
+
+class SwitchTo:
+    def __init__(self, driver):
+        self._driver = driver
+
+    @property
+    def active_element(self):
+        """
+        Returns the element with focus, or BODY if nothing has focus.
+
+        :Usage:
+            ::
+
+                element = driver.switch_to.active_element
+        """
+        if self._driver.w3c:
+            return self._driver.execute(Command.W3C_GET_ACTIVE_ELEMENT)['value']
+        else:
+            return self._driver.execute(Command.GET_ACTIVE_ELEMENT)['value']
+
+    @property
+    def alert(self):
+        """
+        Switches focus to an alert on the page.
+
+        :Usage:
+            ::
+
+                alert = driver.switch_to.alert
+        """
+        alert = Alert(self._driver)
+        alert.text
+        return alert
+
+    def default_content(self):
+        """
+        Switch focus to the default frame.
+
+        :Usage:
+            ::
+
+                driver.switch_to.default_content()
+        """
+        self._driver.execute(Command.SWITCH_TO_FRAME, {'id': None})
+
+    def frame(self, frame_reference):
+        """
+        Switches focus to the specified frame, by index, name, or webelement.
+
+        :Args:
+         - frame_reference: The name of the window to switch to, an integer representing the index,
+                            or a webelement that is an (i)frame to switch to.
+
+        :Usage:
+            ::
+
+                driver.switch_to.frame('frame_name')
+                driver.switch_to.frame(1)
+                driver.switch_to.frame(driver.find_elements_by_tag_name("iframe")[0])
+        """
+        if isinstance(frame_reference, basestring) and self._driver.w3c:
+            try:
+                frame_reference = self._driver.find_element(By.ID, frame_reference)
+            except NoSuchElementException:
+                try:
+                    frame_reference = self._driver.find_element(By.NAME, frame_reference)
+                except NoSuchElementException:
+                    raise NoSuchFrameException(frame_reference)
+
+        self._driver.execute(Command.SWITCH_TO_FRAME, {'id': frame_reference})
+
+    def new_window(self, type_hint=None):
+        """Switches to a new top-level browsing context.
+
+        The type hint can be one of "tab" or "window". If not specified the
+        browser will automatically select it.
+
+        :Usage:
+            ::
+
+                driver.switch_to.new_window('tab')
+        """
+        value = self._driver.execute(Command.NEW_WINDOW, {'type': type_hint})['value']
+        self._w3c_window(value['handle'])
+
+    def parent_frame(self):
+        """
+        Switches focus to the parent context. If the current context is the top
+        level browsing context, the context remains unchanged.
+
+        :Usage:
+            ::
+
+                driver.switch_to.parent_frame()
+        """
+        self._driver.execute(Command.SWITCH_TO_PARENT_FRAME)
+
+    def window(self, window_name):
+        """
+        Switches focus to the specified window.
+
+        :Args:
+         - window_name: The name or window handle of the window to switch to.
+
+        :Usage:
+            ::
+
+                driver.switch_to.window('main')
+        """
+        if self._driver.w3c:
+            self._w3c_window(window_name)
+            return
+        data = {'name': window_name}
+        self._driver.execute(Command.SWITCH_TO_WINDOW, data)
+
+    def _w3c_window(self, window_name):
+        def send_handle(h):
+            self._driver.execute(Command.SWITCH_TO_WINDOW, {'handle': h})
+
+        try:
+            # Try using it as a handle first.
+            send_handle(window_name)
+        except NoSuchWindowException as e:
+            # Check every window to try to find the given window name.
+            original_handle = self._driver.current_window_handle
+            handles = self._driver.window_handles
+            for handle in handles:
+                send_handle(handle)
+                current_name = self._driver.execute_script('return window.name')
+                if window_name == current_name:
+                    return
+            send_handle(original_handle)
+            raise e
diff --git a/selenium/webdriver/remote/utils.py b/selenium/webdriver/remote/utils.py
new file mode 100644
index 00000000..0599dc8f
--- /dev/null
+++ b/selenium/webdriver/remote/utils.py
@@ -0,0 +1,90 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import json
+import logging
+import os
+import tempfile
+import zipfile
+
+
+LOGGER = logging.getLogger(__name__)
+
+
+def format_json(json_struct):
+    return json.dumps(json_struct, indent=4)
+
+
+def dump_json(json_struct):
+    return json.dumps(json_struct)
+
+
+def load_json(s):
+    return json.loads(s)
+
+
+def unzip_to_temp_dir(zip_file_name):
+    """Unzip zipfile to a temporary directory.
+
+    The directory of the unzipped files is returned if success,
+    otherwise None is returned. """
+    if not zip_file_name or not os.path.exists(zip_file_name):
+        return None
+
+    zf = zipfile.ZipFile(zip_file_name)
+
+    if zf.testzip() is not None:
+        return None
+
+    # Unzip the files into a temporary directory
+    LOGGER.info("Extracting zipped file: %s" % zip_file_name)
+    tempdir = tempfile.mkdtemp()
+
+    try:
+        # Create directories that don't exist
+        for zip_name in zf.namelist():
+            # We have no knowledge on the os where the zipped file was
+            # created, so we restrict to zip files with paths without
+            # charactor "\" and "/".
+            name = (zip_name.replace("\\", os.path.sep).
+                    replace("/", os.path.sep))
+            dest = os.path.join(tempdir, name)
+            if (name.endswith(os.path.sep) and not os.path.exists(dest)):
+                os.mkdir(dest)
+                LOGGER.debug("Directory %s created." % dest)
+
+        # Copy files
+        for zip_name in zf.namelist():
+            # We have no knowledge on the os where the zipped file was
+            # created, so we restrict to zip files with paths without
+            # charactor "\" and "/".
+            name = (zip_name.replace("\\", os.path.sep).
+                    replace("/", os.path.sep))
+            dest = os.path.join(tempdir, name)
+            if not (name.endswith(os.path.sep)):
+                LOGGER.debug("Copying file %s......" % dest)
+                outfile = open(dest, 'wb')
+                outfile.write(zf.read(zip_name))
+                outfile.close()
+                LOGGER.debug("File %s copied." % dest)
+
+        LOGGER.info("Unzipped file can be found at %s" % tempdir)
+        return tempdir
+
+    except IOError as err:
+        LOGGER.error("Error in extracting webdriver.xpi: %s" % err)
+        return None
diff --git a/selenium/webdriver/remote/webdriver.py b/selenium/webdriver/remote/webdriver.py
new file mode 100644
index 00000000..5cace703
--- /dev/null
+++ b/selenium/webdriver/remote/webdriver.py
@@ -0,0 +1,1333 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""The WebDriver implementation."""
+
+import base64
+import copy
+from contextlib import contextmanager
+import warnings
+
+from .command import Command
+from .webelement import WebElement
+from .remote_connection import RemoteConnection
+from .errorhandler import ErrorHandler
+from .switch_to import SwitchTo
+from .mobile import Mobile
+from .file_detector import FileDetector, LocalFileDetector
+from selenium.common.exceptions import (InvalidArgumentException,
+                                        WebDriverException,
+                                        NoSuchCookieException,
+                                        UnknownMethodException)
+from selenium.webdriver.common.by import By
+from selenium.webdriver.common.html5.application_cache import ApplicationCache
+
+try:
+    str = basestring
+except NameError:
+    pass
+
+
+_W3C_CAPABILITY_NAMES = frozenset([
+    'acceptInsecureCerts',
+    'browserName',
+    'browserVersion',
+    'platformName',
+    'pageLoadStrategy',
+    'proxy',
+    'setWindowRect',
+    'timeouts',
+    'unhandledPromptBehavior',
+    'strictFileInteractability'
+])
+
+_OSS_W3C_CONVERSION = {
+    'acceptSslCerts': 'acceptInsecureCerts',
+    'version': 'browserVersion',
+    'platform': 'platformName'
+}
+
+
+def _make_w3c_caps(caps):
+    """Makes a W3C alwaysMatch capabilities object.
+
+    Filters out capability names that are not in the W3C spec. Spec-compliant
+    drivers will reject requests containing unknown capability names.
+
+    Moves the Firefox profile, if present, from the old location to the new Firefox
+    options object.
+
+    :Args:
+     - caps - A dictionary of capabilities requested by the caller.
+    """
+    caps = copy.deepcopy(caps)
+    profile = caps.get('firefox_profile')
+    always_match = {}
+    if caps.get('proxy') and caps['proxy'].get('proxyType'):
+        caps['proxy']['proxyType'] = caps['proxy']['proxyType'].lower()
+    for k, v in caps.items():
+        if v and k in _OSS_W3C_CONVERSION:
+            always_match[_OSS_W3C_CONVERSION[k]] = v.lower() if k == 'platform' else v
+        if k in _W3C_CAPABILITY_NAMES or ':' in k:
+            always_match[k] = v
+    if profile:
+        moz_opts = always_match.get('moz:firefoxOptions', {})
+        # If it's already present, assume the caller did that intentionally.
+        if 'profile' not in moz_opts:
+            # Don't mutate the original capabilities.
+            new_opts = copy.deepcopy(moz_opts)
+            new_opts['profile'] = profile
+            always_match['moz:firefoxOptions'] = new_opts
+    return {"firstMatch": [{}], "alwaysMatch": always_match}
+
+
+class WebDriver(object):
+    """
+    Controls a browser by sending commands to a remote server.
+    This server is expected to be running the WebDriver wire protocol
+    as defined at
+    https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
+
+    :Attributes:
+     - session_id - String ID of the browser session started and controlled by this WebDriver.
+     - capabilities - Dictionary of effective capabilities of this browser session as returned
+         by the remote server. See https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities
+     - command_executor - remote_connection.RemoteConnection object used to execute commands.
+     - error_handler - errorhandler.ErrorHandler object used to handle errors.
+    """
+
+    _web_element_cls = WebElement
+
+    def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',
+                 desired_capabilities=None, browser_profile=None, proxy=None,
+                 keep_alive=True, file_detector=None, options=None):
+        """
+        Create a new driver that will issue commands using the wire protocol.
+
+        :Args:
+         - command_executor - Either a string representing URL of the remote server or a custom
+             remote_connection.RemoteConnection object. Defaults to 'http://127.0.0.1:4444/wd/hub'.
+         - desired_capabilities - A dictionary of capabilities to request when
+             starting the browser session. Required parameter.
+         - browser_profile - A selenium.webdriver.firefox.firefox_profile.FirefoxProfile object.
+             Only used if Firefox is requested. Optional.
+         - proxy - A selenium.webdriver.common.proxy.Proxy object. The browser session will
+             be started with given proxy settings, if possible. Optional.
+         - keep_alive - Whether to configure remote_connection.RemoteConnection to use
+             HTTP keep-alive. Defaults to True.
+         - file_detector - Pass custom file detector object during instantiation. If None,
+             then default LocalFileDetector() will be used.
+         - options - instance of a driver options.Options class
+        """
+        capabilities = {}
+        if options is not None:
+            capabilities = options.to_capabilities()
+        if desired_capabilities is not None:
+            if not isinstance(desired_capabilities, dict):
+                raise WebDriverException("Desired Capabilities must be a dictionary")
+            else:
+                capabilities.update(desired_capabilities)
+        self.command_executor = command_executor
+        if type(self.command_executor) is bytes or isinstance(self.command_executor, str):
+            self.command_executor = RemoteConnection(command_executor, keep_alive=keep_alive)
+        self._is_remote = True
+        self.session_id = None
+        self.capabilities = {}
+        self.error_handler = ErrorHandler()
+        self.start_client()
+        self.start_session(capabilities, browser_profile)
+        self._switch_to = SwitchTo(self)
+        self._mobile = Mobile(self)
+        self.file_detector = file_detector or LocalFileDetector()
+
+    def __repr__(self):
+        return '<{0.__module__}.{0.__name__} (session="{1}")>'.format(
+            type(self), self.session_id)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args):
+        self.quit()
+
+    @contextmanager
+    def file_detector_context(self, file_detector_class, *args, **kwargs):
+        """
+        Overrides the current file detector (if necessary) in limited context.
+        Ensures the original file detector is set afterwards.
+
+        Example:
+
+        with webdriver.file_detector_context(UselessFileDetector):
+            someinput.send_keys('/etc/hosts')
+
+        :Args:
+         - file_detector_class - Class of the desired file detector. If the class is different
+             from the current file_detector, then the class is instantiated with args and kwargs
+             and used as a file detector during the duration of the context manager.
+         - args - Optional arguments that get passed to the file detector class during
+             instantiation.
+         - kwargs - Keyword arguments, passed the same way as args.
+        """
+        last_detector = None
+        if not isinstance(self.file_detector, file_detector_class):
+            last_detector = self.file_detector
+            self.file_detector = file_detector_class(*args, **kwargs)
+        try:
+            yield
+        finally:
+            if last_detector is not None:
+                self.file_detector = last_detector
+
+    @property
+    def mobile(self):
+        return self._mobile
+
+    @property
+    def name(self):
+        """Returns the name of the underlying browser for this instance.
+
+        :Usage:
+            ::
+
+                name = driver.name
+        """
+        if 'browserName' in self.capabilities:
+            return self.capabilities['browserName']
+        else:
+            raise KeyError('browserName not specified in session capabilities')
+
+    def start_client(self):
+        """
+        Called before starting a new session. This method may be overridden
+        to define custom startup behavior.
+        """
+        pass
+
+    def stop_client(self):
+        """
+        Called after executing a quit command. This method may be overridden
+        to define custom shutdown behavior.
+        """
+        pass
+
+    def start_session(self, capabilities, browser_profile=None):
+        """
+        Creates a new session with the desired capabilities.
+
+        :Args:
+         - browser_name - The name of the browser to request.
+         - version - Which browser version to request.
+         - platform - Which platform to request the browser on.
+         - javascript_enabled - Whether the new session should support JavaScript.
+         - browser_profile - A selenium.webdriver.firefox.firefox_profile.FirefoxProfile object. Only used if Firefox is requested.
+        """
+        if not isinstance(capabilities, dict):
+            raise InvalidArgumentException("Capabilities must be a dictionary")
+        if browser_profile:
+            if "moz:firefoxOptions" in capabilities:
+                capabilities["moz:firefoxOptions"]["profile"] = browser_profile.encoded
+            else:
+                capabilities.update({'firefox_profile': browser_profile.encoded})
+        w3c_caps = _make_w3c_caps(capabilities)
+        parameters = {"capabilities": w3c_caps,
+                      "desiredCapabilities": capabilities}
+        response = self.execute(Command.NEW_SESSION, parameters)
+        if 'sessionId' not in response:
+            response = response['value']
+        self.session_id = response['sessionId']
+        self.capabilities = response.get('value')
+
+        # if capabilities is none we are probably speaking to
+        # a W3C endpoint
+        if self.capabilities is None:
+            self.capabilities = response.get('capabilities')
+
+        # Double check to see if we have a W3C Compliant browser
+        self.w3c = response.get('status') is None
+        self.command_executor.w3c = self.w3c
+
+    def _wrap_value(self, value):
+        if isinstance(value, dict):
+            converted = {}
+            for key, val in value.items():
+                converted[key] = self._wrap_value(val)
+            return converted
+        elif isinstance(value, self._web_element_cls):
+            return {'ELEMENT': value.id, 'element-6066-11e4-a52e-4f735466cecf': value.id}
+        elif isinstance(value, list):
+            return list(self._wrap_value(item) for item in value)
+        else:
+            return value
+
+    def create_web_element(self, element_id):
+        """Creates a web element with the specified `element_id`."""
+        return self._web_element_cls(self, element_id, w3c=self.w3c)
+
+    def _unwrap_value(self, value):
+        if isinstance(value, dict):
+            if 'ELEMENT' in value or 'element-6066-11e4-a52e-4f735466cecf' in value:
+                wrapped_id = value.get('ELEMENT', None)
+                if wrapped_id:
+                    return self.create_web_element(value['ELEMENT'])
+                else:
+                    return self.create_web_element(value['element-6066-11e4-a52e-4f735466cecf'])
+            else:
+                for key, val in value.items():
+                    value[key] = self._unwrap_value(val)
+                return value
+        elif isinstance(value, list):
+            return list(self._unwrap_value(item) for item in value)
+        else:
+            return value
+
+    def execute(self, driver_command, params=None):
+        """
+        Sends a command to be executed by a command.CommandExecutor.
+
+        :Args:
+         - driver_command: The name of the command to execute as a string.
+         - params: A dictionary of named parameters to send with the command.
+
+        :Returns:
+          The command's JSON response loaded into a dictionary object.
+        """
+        if self.session_id is not None:
+            if not params:
+                params = {'sessionId': self.session_id}
+            elif 'sessionId' not in params:
+                params['sessionId'] = self.session_id
+
+        params = self._wrap_value(params)
+        response = self.command_executor.execute(driver_command, params)
+        if response:
+            self.error_handler.check_response(response)
+            response['value'] = self._unwrap_value(
+                response.get('value', None))
+            return response
+        # If the server doesn't send a response, assume the command was
+        # a success
+        return {'success': 0, 'value': None, 'sessionId': self.session_id}
+
+    def get(self, url):
+        """
+        Loads a web page in the current browser session.
+        """
+        self.execute(Command.GET, {'url': url})
+
+    @property
+    def title(self):
+        """Returns the title of the current page.
+
+        :Usage:
+            ::
+
+                title = driver.title
+        """
+        resp = self.execute(Command.GET_TITLE)
+        return resp['value'] if resp['value'] is not None else ""
+
+    def find_element_by_id(self, id_):
+        """Finds an element by id.
+
+        :Args:
+         - id\\_ - The id of the element to be found.
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = driver.find_element_by_id('foo')
+        """
+        return self.find_element(by=By.ID, value=id_)
+
+    def find_elements_by_id(self, id_):
+        """
+        Finds multiple elements by id.
+
+        :Args:
+         - id\\_ - The id of the elements to be found.
+
+        :Returns:
+         - list of WebElement - a list with elements if any was found.  An
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = driver.find_elements_by_id('foo')
+        """
+        return self.find_elements(by=By.ID, value=id_)
+
+    def find_element_by_xpath(self, xpath):
+        """
+        Finds an element by xpath.
+
+        :Args:
+         - xpath - The xpath locator of the element to find.
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = driver.find_element_by_xpath('//div/td[1]')
+        """
+        return self.find_element(by=By.XPATH, value=xpath)
+
+    def find_elements_by_xpath(self, xpath):
+        """
+        Finds multiple elements by xpath.
+
+        :Args:
+         - xpath - The xpath locator of the elements to be found.
+
+        :Returns:
+         - list of WebElement - a list with elements if any was found.  An
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = driver.find_elements_by_xpath("//div[contains(@class, 'foo')]")
+        """
+        return self.find_elements(by=By.XPATH, value=xpath)
+
+    def find_element_by_link_text(self, link_text):
+        """
+        Finds an element by link text.
+
+        :Args:
+         - link_text: The text of the element to be found.
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = driver.find_element_by_link_text('Sign In')
+        """
+        return self.find_element(by=By.LINK_TEXT, value=link_text)
+
+    def find_elements_by_link_text(self, text):
+        """
+        Finds elements by link text.
+
+        :Args:
+         - link_text: The text of the elements to be found.
+
+        :Returns:
+         - list of webelement - a list with elements if any was found.  an
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = driver.find_elements_by_link_text('Sign In')
+        """
+        return self.find_elements(by=By.LINK_TEXT, value=text)
+
+    def find_element_by_partial_link_text(self, link_text):
+        """
+        Finds an element by a partial match of its link text.
+
+        :Args:
+         - link_text: The text of the element to partially match on.
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = driver.find_element_by_partial_link_text('Sign')
+        """
+        return self.find_element(by=By.PARTIAL_LINK_TEXT, value=link_text)
+
+    def find_elements_by_partial_link_text(self, link_text):
+        """
+        Finds elements by a partial match of their link text.
+
+        :Args:
+         - link_text: The text of the element to partial match on.
+
+        :Returns:
+         - list of webelement - a list with elements if any was found.  an
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = driver.find_elements_by_partial_link_text('Sign')
+        """
+        return self.find_elements(by=By.PARTIAL_LINK_TEXT, value=link_text)
+
+    def find_element_by_name(self, name):
+        """
+        Finds an element by name.
+
+        :Args:
+         - name: The name of the element to find.
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = driver.find_element_by_name('foo')
+        """
+        return self.find_element(by=By.NAME, value=name)
+
+    def find_elements_by_name(self, name):
+        """
+        Finds elements by name.
+
+        :Args:
+         - name: The name of the elements to find.
+
+        :Returns:
+         - list of webelement - a list with elements if any was found.  an
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = driver.find_elements_by_name('foo')
+        """
+        return self.find_elements(by=By.NAME, value=name)
+
+    def find_element_by_tag_name(self, name):
+        """
+        Finds an element by tag name.
+
+        :Args:
+         - name - name of html tag (eg: h1, a, span)
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = driver.find_element_by_tag_name('h1')
+        """
+        return self.find_element(by=By.TAG_NAME, value=name)
+
+    def find_elements_by_tag_name(self, name):
+        """
+        Finds elements by tag name.
+
+        :Args:
+         - name - name of html tag (eg: h1, a, span)
+
+        :Returns:
+         - list of WebElement - a list with elements if any was found.  An
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = driver.find_elements_by_tag_name('h1')
+        """
+        return self.find_elements(by=By.TAG_NAME, value=name)
+
+    def find_element_by_class_name(self, name):
+        """
+        Finds an element by class name.
+
+        :Args:
+         - name: The class name of the element to find.
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = driver.find_element_by_class_name('foo')
+        """
+        return self.find_element(by=By.CLASS_NAME, value=name)
+
+    def find_elements_by_class_name(self, name):
+        """
+        Finds elements by class name.
+
+        :Args:
+         - name: The class name of the elements to find.
+
+        :Returns:
+         - list of WebElement - a list with elements if any was found.  An
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = driver.find_elements_by_class_name('foo')
+        """
+        return self.find_elements(by=By.CLASS_NAME, value=name)
+
+    def find_element_by_css_selector(self, css_selector):
+        """
+        Finds an element by css selector.
+
+        :Args:
+         - css_selector - CSS selector string, ex: 'a.nav#home'
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = driver.find_element_by_css_selector('#foo')
+        """
+        return self.find_element(by=By.CSS_SELECTOR, value=css_selector)
+
+    def find_elements_by_css_selector(self, css_selector):
+        """
+        Finds elements by css selector.
+
+        :Args:
+         - css_selector - CSS selector string, ex: 'a.nav#home'
+
+        :Returns:
+         - list of WebElement - a list with elements if any was found.  An
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = driver.find_elements_by_css_selector('.foo')
+        """
+        return self.find_elements(by=By.CSS_SELECTOR, value=css_selector)
+
+    def execute_script(self, script, *args):
+        """
+        Synchronously Executes JavaScript in the current window/frame.
+
+        :Args:
+         - script: The JavaScript to execute.
+         - \\*args: Any applicable arguments for your JavaScript.
+
+        :Usage:
+            ::
+
+                driver.execute_script('return document.title;')
+        """
+        converted_args = list(args)
+        command = None
+        if self.w3c:
+            command = Command.W3C_EXECUTE_SCRIPT
+        else:
+            command = Command.EXECUTE_SCRIPT
+
+        return self.execute(command, {
+            'script': script,
+            'args': converted_args})['value']
+
+    def execute_async_script(self, script, *args):
+        """
+        Asynchronously Executes JavaScript in the current window/frame.
+
+        :Args:
+         - script: The JavaScript to execute.
+         - \\*args: Any applicable arguments for your JavaScript.
+
+        :Usage:
+            ::
+
+                script = "var callback = arguments[arguments.length - 1]; " \\
+                         "window.setTimeout(function(){ callback('timeout') }, 3000);"
+                driver.execute_async_script(script)
+        """
+        converted_args = list(args)
+        if self.w3c:
+            command = Command.W3C_EXECUTE_SCRIPT_ASYNC
+        else:
+            command = Command.EXECUTE_ASYNC_SCRIPT
+
+        return self.execute(command, {
+            'script': script,
+            'args': converted_args})['value']
+
+    @property
+    def current_url(self):
+        """
+        Gets the URL of the current page.
+
+        :Usage:
+            ::
+
+                driver.current_url
+        """
+        return self.execute(Command.GET_CURRENT_URL)['value']
+
+    @property
+    def page_source(self):
+        """
+        Gets the source of the current page.
+
+        :Usage:
+            ::
+
+                driver.page_source
+        """
+        return self.execute(Command.GET_PAGE_SOURCE)['value']
+
+    def close(self):
+        """
+        Closes the current window.
+
+        :Usage:
+            ::
+
+                driver.close()
+        """
+        self.execute(Command.CLOSE)
+
+    def quit(self):
+        """
+        Quits the driver and closes every associated window.
+
+        :Usage:
+            ::
+
+                driver.quit()
+        """
+        try:
+            self.execute(Command.QUIT)
+        finally:
+            self.stop_client()
+            self.command_executor.close()
+
+    @property
+    def current_window_handle(self):
+        """
+        Returns the handle of the current window.
+
+        :Usage:
+            ::
+
+                driver.current_window_handle
+        """
+        if self.w3c:
+            return self.execute(Command.W3C_GET_CURRENT_WINDOW_HANDLE)['value']
+        else:
+            return self.execute(Command.GET_CURRENT_WINDOW_HANDLE)['value']
+
+    @property
+    def window_handles(self):
+        """
+        Returns the handles of all windows within the current session.
+
+        :Usage:
+            ::
+
+                driver.window_handles
+        """
+        if self.w3c:
+            return self.execute(Command.W3C_GET_WINDOW_HANDLES)['value']
+        else:
+            return self.execute(Command.GET_WINDOW_HANDLES)['value']
+
+    def maximize_window(self):
+        """
+        Maximizes the current window that webdriver is using
+        """
+        params = None
+        command = Command.W3C_MAXIMIZE_WINDOW
+        if not self.w3c:
+            command = Command.MAXIMIZE_WINDOW
+            params = {'windowHandle': 'current'}
+        self.execute(command, params)
+
+    def fullscreen_window(self):
+        """
+        Invokes the window manager-specific 'full screen' operation
+        """
+        self.execute(Command.FULLSCREEN_WINDOW)
+
+    def minimize_window(self):
+        """
+        Invokes the window manager-specific 'minimize' operation
+        """
+        self.execute(Command.MINIMIZE_WINDOW)
+
+    @property
+    def switch_to(self):
+        """
+        :Returns:
+            - SwitchTo: an object containing all options to switch focus into
+
+        :Usage:
+            ::
+
+                element = driver.switch_to.active_element
+                alert = driver.switch_to.alert
+                driver.switch_to.default_content()
+                driver.switch_to.frame('frame_name')
+                driver.switch_to.frame(1)
+                driver.switch_to.frame(driver.find_elements_by_tag_name("iframe")[0])
+                driver.switch_to.parent_frame()
+                driver.switch_to.window('main')
+        """
+        return self._switch_to
+
+    # Navigation
+    def back(self):
+        """
+        Goes one step backward in the browser history.
+
+        :Usage:
+            ::
+
+                driver.back()
+        """
+        self.execute(Command.GO_BACK)
+
+    def forward(self):
+        """
+        Goes one step forward in the browser history.
+
+        :Usage:
+            ::
+
+                driver.forward()
+        """
+        self.execute(Command.GO_FORWARD)
+
+    def refresh(self):
+        """
+        Refreshes the current page.
+
+        :Usage:
+            ::
+
+                driver.refresh()
+        """
+        self.execute(Command.REFRESH)
+
+    # Options
+    def get_cookies(self):
+        """
+        Returns a set of dictionaries, corresponding to cookies visible in the current session.
+
+        :Usage:
+            ::
+
+                driver.get_cookies()
+        """
+        return self.execute(Command.GET_ALL_COOKIES)['value']
+
+    def get_cookie(self, name):
+        """
+        Get a single cookie by name. Returns the cookie if found, None if not.
+
+        :Usage:
+            ::
+
+                driver.get_cookie('my_cookie')
+        """
+        if self.w3c:
+            try:
+                return self.execute(Command.GET_COOKIE, {'name': name})['value']
+            except NoSuchCookieException:
+                return None
+        else:
+            cookies = self.get_cookies()
+            for cookie in cookies:
+                if cookie['name'] == name:
+                    return cookie
+            return None
+
+    def delete_cookie(self, name):
+        """
+        Deletes a single cookie with the given name.
+
+        :Usage:
+            ::
+
+                driver.delete_cookie('my_cookie')
+        """
+        self.execute(Command.DELETE_COOKIE, {'name': name})
+
+    def delete_all_cookies(self):
+        """
+        Delete all cookies in the scope of the session.
+
+        :Usage:
+            ::
+
+                driver.delete_all_cookies()
+        """
+        self.execute(Command.DELETE_ALL_COOKIES)
+
+    def add_cookie(self, cookie_dict):
+        """
+        Adds a cookie to your current session.
+
+        :Args:
+         - cookie_dict: A dictionary object, with required keys - "name" and "value";
+            optional keys - "path", "domain", "secure", "expiry"
+
+        Usage:
+            driver.add_cookie({'name' : 'foo', 'value' : 'bar'})
+            driver.add_cookie({'name' : 'foo', 'value' : 'bar', 'path' : '/'})
+            driver.add_cookie({'name' : 'foo', 'value' : 'bar', 'path' : '/', 'secure':True})
+
+        """
+        self.execute(Command.ADD_COOKIE, {'cookie': cookie_dict})
+
+    # Timeouts
+    def implicitly_wait(self, time_to_wait):
+        """
+        Sets a sticky timeout to implicitly wait for an element to be found,
+           or a command to complete. This method only needs to be called one
+           time per session. To set the timeout for calls to
+           execute_async_script, see set_script_timeout.
+
+        :Args:
+         - time_to_wait: Amount of time to wait (in seconds)
+
+        :Usage:
+            ::
+
+                driver.implicitly_wait(30)
+        """
+        if self.w3c:
+            self.execute(Command.SET_TIMEOUTS, {
+                'implicit': int(float(time_to_wait) * 1000)})
+        else:
+            self.execute(Command.IMPLICIT_WAIT, {
+                'ms': float(time_to_wait) * 1000})
+
+    def set_script_timeout(self, time_to_wait):
+        """
+        Set the amount of time that the script should wait during an
+           execute_async_script call before throwing an error.
+
+        :Args:
+         - time_to_wait: The amount of time to wait (in seconds)
+
+        :Usage:
+            ::
+
+                driver.set_script_timeout(30)
+        """
+        if self.w3c:
+            self.execute(Command.SET_TIMEOUTS, {
+                'script': int(float(time_to_wait) * 1000)})
+        else:
+            self.execute(Command.SET_SCRIPT_TIMEOUT, {
+                'ms': float(time_to_wait) * 1000})
+
+    def set_page_load_timeout(self, time_to_wait):
+        """
+        Set the amount of time to wait for a page load to complete
+           before throwing an error.
+
+        :Args:
+         - time_to_wait: The amount of time to wait
+
+        :Usage:
+            ::
+
+                driver.set_page_load_timeout(30)
+        """
+        try:
+            self.execute(Command.SET_TIMEOUTS, {
+                'pageLoad': int(float(time_to_wait) * 1000)})
+        except WebDriverException:
+            self.execute(Command.SET_TIMEOUTS, {
+                'ms': float(time_to_wait) * 1000,
+                'type': 'page load'})
+
+    def find_element(self, by=By.ID, value=None):
+        """
+        Find an element given a By strategy and locator. Prefer the find_element_by_* methods when
+        possible.
+
+        :Usage:
+            ::
+
+                element = driver.find_element(By.ID, 'foo')
+
+        :rtype: WebElement
+        """
+        if self.w3c:
+            if by == By.ID:
+                by = By.CSS_SELECTOR
+                value = '[id="%s"]' % value
+            elif by == By.TAG_NAME:
+                by = By.CSS_SELECTOR
+            elif by == By.CLASS_NAME:
+                by = By.CSS_SELECTOR
+                value = ".%s" % value
+            elif by == By.NAME:
+                by = By.CSS_SELECTOR
+                value = '[name="%s"]' % value
+        return self.execute(Command.FIND_ELEMENT, {
+            'using': by,
+            'value': value})['value']
+
+    def find_elements(self, by=By.ID, value=None):
+        """
+        Find elements given a By strategy and locator. Prefer the find_elements_by_* methods when
+        possible.
+
+        :Usage:
+            ::
+
+                elements = driver.find_elements(By.CLASS_NAME, 'foo')
+
+        :rtype: list of WebElement
+        """
+        if self.w3c:
+            if by == By.ID:
+                by = By.CSS_SELECTOR
+                value = '[id="%s"]' % value
+            elif by == By.TAG_NAME:
+                by = By.CSS_SELECTOR
+            elif by == By.CLASS_NAME:
+                by = By.CSS_SELECTOR
+                value = ".%s" % value
+            elif by == By.NAME:
+                by = By.CSS_SELECTOR
+                value = '[name="%s"]' % value
+
+        # Return empty list if driver returns null
+        # See https://github.com/SeleniumHQ/selenium/issues/4555
+        return self.execute(Command.FIND_ELEMENTS, {
+            'using': by,
+            'value': value})['value'] or []
+
+    @property
+    def desired_capabilities(self):
+        """
+        returns the drivers current desired capabilities being used
+        """
+        return self.capabilities
+
+    def get_screenshot_as_file(self, filename):
+        """
+        Saves a screenshot of the current window to a PNG image file. Returns
+           False if there is any IOError, else returns True. Use full paths in
+           your filename.
+
+        :Args:
+         - filename: The full path you wish to save your screenshot to. This
+           should end with a `.png` extension.
+
+        :Usage:
+            ::
+
+                driver.get_screenshot_as_file('/Screenshots/foo.png')
+        """
+        if not filename.lower().endswith('.png'):
+            warnings.warn("name used for saved screenshot does not match file "
+                          "type. It should end with a `.png` extension", UserWarning)
+        png = self.get_screenshot_as_png()
+        try:
+            with open(filename, 'wb') as f:
+                f.write(png)
+        except IOError:
+            return False
+        finally:
+            del png
+        return True
+
+    def save_screenshot(self, filename):
+        """
+        Saves a screenshot of the current window to a PNG image file. Returns
+           False if there is any IOError, else returns True. Use full paths in
+           your filename.
+
+        :Args:
+         - filename: The full path you wish to save your screenshot to. This
+           should end with a `.png` extension.
+
+        :Usage:
+            ::
+
+                driver.save_screenshot('/Screenshots/foo.png')
+        """
+        return self.get_screenshot_as_file(filename)
+
+    def get_screenshot_as_png(self):
+        """
+        Gets the screenshot of the current window as a binary data.
+
+        :Usage:
+            ::
+
+                driver.get_screenshot_as_png()
+        """
+        return base64.b64decode(self.get_screenshot_as_base64().encode('ascii'))
+
+    def get_screenshot_as_base64(self):
+        """
+        Gets the screenshot of the current window as a base64 encoded string
+           which is useful in embedded images in HTML.
+
+        :Usage:
+            ::
+
+                driver.get_screenshot_as_base64()
+        """
+        return self.execute(Command.SCREENSHOT)['value']
+
+    def set_window_size(self, width, height, windowHandle='current'):
+        """
+        Sets the width and height of the current window. (window.resizeTo)
+
+        :Args:
+         - width: the width in pixels to set the window to
+         - height: the height in pixels to set the window to
+
+        :Usage:
+            ::
+
+                driver.set_window_size(800,600)
+        """
+        if self.w3c:
+            if windowHandle != 'current':
+                warnings.warn("Only 'current' window is supported for W3C compatibile browsers.")
+            self.set_window_rect(width=int(width), height=int(height))
+        else:
+            self.execute(Command.SET_WINDOW_SIZE, {
+                'width': int(width),
+                'height': int(height),
+                'windowHandle': windowHandle})
+
+    def get_window_size(self, windowHandle='current'):
+        """
+        Gets the width and height of the current window.
+
+        :Usage:
+            ::
+
+                driver.get_window_size()
+        """
+        command = Command.GET_WINDOW_SIZE
+        if self.w3c:
+            if windowHandle != 'current':
+                warnings.warn("Only 'current' window is supported for W3C compatibile browsers.")
+            size = self.get_window_rect()
+        else:
+            size = self.execute(command, {'windowHandle': windowHandle})
+
+        if size.get('value', None) is not None:
+            size = size['value']
+
+        return {k: size[k] for k in ('width', 'height')}
+
+    def set_window_position(self, x, y, windowHandle='current'):
+        """
+        Sets the x,y position of the current window. (window.moveTo)
+
+        :Args:
+         - x: the x-coordinate in pixels to set the window position
+         - y: the y-coordinate in pixels to set the window position
+
+        :Usage:
+            ::
+
+                driver.set_window_position(0,0)
+        """
+        if self.w3c:
+            if windowHandle != 'current':
+                warnings.warn("Only 'current' window is supported for W3C compatibile browsers.")
+            return self.set_window_rect(x=int(x), y=int(y))
+        else:
+            self.execute(Command.SET_WINDOW_POSITION,
+                         {
+                             'x': int(x),
+                             'y': int(y),
+                             'windowHandle': windowHandle
+                         })
+
+    def get_window_position(self, windowHandle='current'):
+        """
+        Gets the x,y position of the current window.
+
+        :Usage:
+            ::
+
+                driver.get_window_position()
+        """
+        if self.w3c:
+            if windowHandle != 'current':
+                warnings.warn("Only 'current' window is supported for W3C compatibile browsers.")
+            position = self.get_window_rect()
+        else:
+            position = self.execute(Command.GET_WINDOW_POSITION,
+                                    {'windowHandle': windowHandle})['value']
+
+        return {k: position[k] for k in ('x', 'y')}
+
+    def get_window_rect(self):
+        """
+        Gets the x, y coordinates of the window as well as height and width of
+        the current window.
+
+        :Usage:
+            ::
+
+                driver.get_window_rect()
+        """
+        return self.execute(Command.GET_WINDOW_RECT)['value']
+
+    def set_window_rect(self, x=None, y=None, width=None, height=None):
+        """
+        Sets the x, y coordinates of the window as well as height and width of
+        the current window. This method is only supported for W3C compatible
+        browsers; other browsers should use `set_window_position` and
+        `set_window_size`.
+
+        :Usage:
+            ::
+
+                driver.set_window_rect(x=10, y=10)
+                driver.set_window_rect(width=100, height=200)
+                driver.set_window_rect(x=10, y=10, width=100, height=200)
+        """
+        if not self.w3c:
+            raise UnknownMethodException("set_window_rect is only supported for W3C compatible browsers")
+
+        if (x is None and y is None) and (height is None and width is None):
+            raise InvalidArgumentException("x and y or height and width need values")
+
+        return self.execute(Command.SET_WINDOW_RECT, {"x": x, "y": y,
+                                                      "width": width,
+                                                      "height": height})['value']
+
+    @property
+    def file_detector(self):
+        return self._file_detector
+
+    @file_detector.setter
+    def file_detector(self, detector):
+        """
+        Set the file detector to be used when sending keyboard input.
+        By default, this is set to a file detector that does nothing.
+
+        see FileDetector
+        see LocalFileDetector
+        see UselessFileDetector
+
+        :Args:
+         - detector: The detector to use. Must not be None.
+        """
+        if detector is None:
+            raise WebDriverException("You may not set a file detector that is null")
+        if not isinstance(detector, FileDetector):
+            raise WebDriverException("Detector has to be instance of FileDetector")
+        self._file_detector = detector
+
+    @property
+    def orientation(self):
+        """
+        Gets the current orientation of the device
+
+        :Usage:
+            ::
+
+                orientation = driver.orientation
+        """
+        return self.execute(Command.GET_SCREEN_ORIENTATION)['value']
+
+    @orientation.setter
+    def orientation(self, value):
+        """
+        Sets the current orientation of the device
+
+        :Args:
+         - value: orientation to set it to.
+
+        :Usage:
+            ::
+
+                driver.orientation = 'landscape'
+        """
+        allowed_values = ['LANDSCAPE', 'PORTRAIT']
+        if value.upper() in allowed_values:
+            self.execute(Command.SET_SCREEN_ORIENTATION, {'orientation': value})
+        else:
+            raise WebDriverException("You can only set the orientation to 'LANDSCAPE' and 'PORTRAIT'")
+
+    @property
+    def application_cache(self):
+        """ Returns a ApplicationCache Object to interact with the browser app cache"""
+        return ApplicationCache(self)
+
+    @property
+    def log_types(self):
+        """
+        Gets a list of the available log types
+
+        :Usage:
+            ::
+
+                driver.log_types
+        """
+        return self.execute(Command.GET_AVAILABLE_LOG_TYPES)['value']
+
+    def get_log(self, log_type):
+        """
+        Gets the log for a given log type
+
+        :Args:
+         - log_type: type of log that which will be returned
+
+        :Usage:
+            ::
+
+                driver.get_log('browser')
+                driver.get_log('driver')
+                driver.get_log('client')
+                driver.get_log('server')
+        """
+        return self.execute(Command.GET_LOG, {'type': log_type})['value']
diff --git a/selenium/webdriver/remote/webelement.py b/selenium/webdriver/remote/webelement.py
new file mode 100644
index 00000000..475b74aa
--- /dev/null
+++ b/selenium/webdriver/remote/webelement.py
@@ -0,0 +1,751 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import base64
+import hashlib
+import os
+import pkgutil
+import warnings
+import zipfile
+
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.common.by import By
+from selenium.webdriver.common.utils import keys_to_typing
+from .command import Command
+
+# Python 3 imports
+try:
+    str = basestring
+except NameError:
+    pass
+
+try:
+    from StringIO import StringIO as IOStream
+except ImportError:  # 3+
+    from io import BytesIO as IOStream
+
+# not relying on __package__ here as it can be `None` in some situations (see #4558)
+_pkg = '.'.join(__name__.split('.')[:-1])
+getAttribute_js = pkgutil.get_data(_pkg, 'getAttribute.js').decode('utf8')
+isDisplayed_js = pkgutil.get_data(_pkg, 'isDisplayed.js').decode('utf8')
+
+
+class WebElement(object):
+    """Represents a DOM element.
+
+    Generally, all interesting operations that interact with a document will be
+    performed through this interface.
+
+    All method calls will do a freshness check to ensure that the element
+    reference is still valid.  This essentially determines whether or not the
+    element is still attached to the DOM.  If this test fails, then an
+    ``StaleElementReferenceException`` is thrown, and all future calls to this
+    instance will fail."""
+
+    def __init__(self, parent, id_, w3c=False):
+        self._parent = parent
+        self._id = id_
+        self._w3c = w3c
+
+    def __repr__(self):
+        return '<{0.__module__}.{0.__name__} (session="{1}", element="{2}")>'.format(
+            type(self), self._parent.session_id, self._id)
+
+    @property
+    def tag_name(self):
+        """This element's ``tagName`` property."""
+        return self._execute(Command.GET_ELEMENT_TAG_NAME)['value']
+
+    @property
+    def text(self):
+        """The text of the element."""
+        return self._execute(Command.GET_ELEMENT_TEXT)['value']
+
+    def click(self):
+        """Clicks the element."""
+        self._execute(Command.CLICK_ELEMENT)
+
+    def submit(self):
+        """Submits a form."""
+        if self._w3c:
+            form = self.find_element(By.XPATH, "./ancestor-or-self::form")
+            self._parent.execute_script(
+                "var e = arguments[0].ownerDocument.createEvent('Event');"
+                "e.initEvent('submit', true, true);"
+                "if (arguments[0].dispatchEvent(e)) { arguments[0].submit() }", form)
+        else:
+            self._execute(Command.SUBMIT_ELEMENT)
+
+    def clear(self):
+        """Clears the text if it's a text entry element."""
+        self._execute(Command.CLEAR_ELEMENT)
+
+    def get_property(self, name):
+        """
+        Gets the given property of the element.
+
+        :Args:
+            - name - Name of the property to retrieve.
+
+        :Usage:
+            ::
+
+                text_length = target_element.get_property("text_length")
+        """
+        try:
+            return self._execute(Command.GET_ELEMENT_PROPERTY, {"name": name})["value"]
+        except WebDriverException:
+            # if we hit an end point that doesnt understand getElementProperty lets fake it
+            return self.parent.execute_script('return arguments[0][arguments[1]]', self, name)
+
+    def get_attribute(self, name):
+        """Gets the given attribute or property of the element.
+
+        This method will first try to return the value of a property with the
+        given name. If a property with that name doesn't exist, it returns the
+        value of the attribute with the same name. If there's no attribute with
+        that name, ``None`` is returned.
+
+        Values which are considered truthy, that is equals "true" or "false",
+        are returned as booleans.  All other non-``None`` values are returned
+        as strings.  For attributes or properties which do not exist, ``None``
+        is returned.
+
+        :Args:
+            - name - Name of the attribute/property to retrieve.
+
+        Example::
+
+            # Check if the "active" CSS class is applied to an element.
+            is_active = "active" in target_element.get_attribute("class")
+
+        """
+
+        attributeValue = ''
+        if self._w3c:
+            attributeValue = self.parent.execute_script(
+                "return (%s).apply(null, arguments);" % getAttribute_js,
+                self, name)
+        else:
+            resp = self._execute(Command.GET_ELEMENT_ATTRIBUTE, {'name': name})
+            attributeValue = resp.get('value')
+            if attributeValue is not None:
+                if name != 'value' and attributeValue.lower() in ('true', 'false'):
+                    attributeValue = attributeValue.lower()
+        return attributeValue
+
+    def is_selected(self):
+        """Returns whether the element is selected.
+
+        Can be used to check if a checkbox or radio button is selected.
+        """
+        return self._execute(Command.IS_ELEMENT_SELECTED)['value']
+
+    def is_enabled(self):
+        """Returns whether the element is enabled."""
+        return self._execute(Command.IS_ELEMENT_ENABLED)['value']
+
+    def find_element_by_id(self, id_):
+        """Finds element within this element's children by ID.
+
+        :Args:
+         - id\\_ - ID of child element to locate.
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                foo_element = element.find_element_by_id('foo')
+        """
+        return self.find_element(by=By.ID, value=id_)
+
+    def find_elements_by_id(self, id_):
+        """Finds a list of elements within this element's children by ID.
+        Will return a list of webelements if found, or an empty list if not.
+
+        :Args:
+         - id\\_ - Id of child element to find.
+
+        :Returns:
+         - list of WebElement - a list with elements if any was found.  An
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = element.find_elements_by_id('foo')
+        """
+        return self.find_elements(by=By.ID, value=id_)
+
+    def find_element_by_name(self, name):
+        """Finds element within this element's children by name.
+
+        :Args:
+         - name - name property of the element to find.
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = element.find_element_by_name('foo')
+        """
+        return self.find_element(by=By.NAME, value=name)
+
+    def find_elements_by_name(self, name):
+        """Finds a list of elements within this element's children by name.
+
+        :Args:
+         - name - name property to search for.
+
+        :Returns:
+         - list of webelement - a list with elements if any was found.  an
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = element.find_elements_by_name('foo')
+        """
+        return self.find_elements(by=By.NAME, value=name)
+
+    def find_element_by_link_text(self, link_text):
+        """Finds element within this element's children by visible link text.
+
+        :Args:
+         - link_text - Link text string to search for.
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = element.find_element_by_link_text('Sign In')
+        """
+        return self.find_element(by=By.LINK_TEXT, value=link_text)
+
+    def find_elements_by_link_text(self, link_text):
+        """Finds a list of elements within this element's children by visible link text.
+
+        :Args:
+         - link_text - Link text string to search for.
+
+        :Returns:
+         - list of webelement - a list with elements if any was found.  an
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = element.find_elements_by_link_text('Sign In')
+        """
+        return self.find_elements(by=By.LINK_TEXT, value=link_text)
+
+    def find_element_by_partial_link_text(self, link_text):
+        """Finds element within this element's children by partially visible link text.
+
+        :Args:
+         - link_text: The text of the element to partially match on.
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = element.find_element_by_partial_link_text('Sign')
+        """
+        return self.find_element(by=By.PARTIAL_LINK_TEXT, value=link_text)
+
+    def find_elements_by_partial_link_text(self, link_text):
+        """Finds a list of elements within this element's children by link text.
+
+        :Args:
+         - link_text: The text of the element to partial match on.
+
+        :Returns:
+         - list of webelement - a list with elements if any was found.  an
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = element.find_elements_by_partial_link_text('Sign')
+        """
+        return self.find_elements(by=By.PARTIAL_LINK_TEXT, value=link_text)
+
+    def find_element_by_tag_name(self, name):
+        """Finds element within this element's children by tag name.
+
+        :Args:
+         - name - name of html tag (eg: h1, a, span)
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = element.find_element_by_tag_name('h1')
+        """
+        return self.find_element(by=By.TAG_NAME, value=name)
+
+    def find_elements_by_tag_name(self, name):
+        """Finds a list of elements within this element's children by tag name.
+
+        :Args:
+         - name - name of html tag (eg: h1, a, span)
+
+        :Returns:
+         - list of WebElement - a list with elements if any was found.  An
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = element.find_elements_by_tag_name('h1')
+        """
+        return self.find_elements(by=By.TAG_NAME, value=name)
+
+    def find_element_by_xpath(self, xpath):
+        """Finds element by xpath.
+
+        :Args:
+         - xpath - xpath of element to locate.  "//input[@class='myelement']"
+
+        Note: The base path will be relative to this element's location.
+
+        This will select the first link under this element.
+
+        ::
+
+            myelement.find_element_by_xpath(".//a")
+
+        However, this will select the first link on the page.
+
+        ::
+
+            myelement.find_element_by_xpath("//a")
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = element.find_element_by_xpath('//div/td[1]')
+        """
+        return self.find_element(by=By.XPATH, value=xpath)
+
+    def find_elements_by_xpath(self, xpath):
+        """Finds elements within the element by xpath.
+
+        :Args:
+         - xpath - xpath locator string.
+
+        Note: The base path will be relative to this element's location.
+
+        This will select all links under this element.
+
+        ::
+
+            myelement.find_elements_by_xpath(".//a")
+
+        However, this will select all links in the page itself.
+
+        ::
+
+            myelement.find_elements_by_xpath("//a")
+
+        :Returns:
+         - list of WebElement - a list with elements if any was found.  An
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = element.find_elements_by_xpath("//div[contains(@class, 'foo')]")
+
+        """
+        return self.find_elements(by=By.XPATH, value=xpath)
+
+    def find_element_by_class_name(self, name):
+        """Finds element within this element's children by class name.
+
+        :Args:
+         - name: The class name of the element to find.
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = element.find_element_by_class_name('foo')
+        """
+        return self.find_element(by=By.CLASS_NAME, value=name)
+
+    def find_elements_by_class_name(self, name):
+        """Finds a list of elements within this element's children by class name.
+
+        :Args:
+         - name: The class name of the elements to find.
+
+        :Returns:
+         - list of WebElement - a list with elements if any was found.  An
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = element.find_elements_by_class_name('foo')
+        """
+        return self.find_elements(by=By.CLASS_NAME, value=name)
+
+    def find_element_by_css_selector(self, css_selector):
+        """Finds element within this element's children by CSS selector.
+
+        :Args:
+         - css_selector - CSS selector string, ex: 'a.nav#home'
+
+        :Returns:
+         - WebElement - the element if it was found
+
+        :Raises:
+         - NoSuchElementException - if the element wasn't found
+
+        :Usage:
+            ::
+
+                element = element.find_element_by_css_selector('#foo')
+        """
+        return self.find_element(by=By.CSS_SELECTOR, value=css_selector)
+
+    def find_elements_by_css_selector(self, css_selector):
+        """Finds a list of elements within this element's children by CSS selector.
+
+        :Args:
+         - css_selector - CSS selector string, ex: 'a.nav#home'
+
+        :Returns:
+         - list of WebElement - a list with elements if any was found.  An
+           empty list if not
+
+        :Usage:
+            ::
+
+                elements = element.find_elements_by_css_selector('.foo')
+        """
+        return self.find_elements(by=By.CSS_SELECTOR, value=css_selector)
+
+    def send_keys(self, *value):
+        """Simulates typing into the element.
+
+        :Args:
+            - value - A string for typing, or setting form fields.  For setting
+              file inputs, this could be a local file path.
+
+        Use this to send simple key events or to fill out form fields::
+
+            form_textfield = driver.find_element_by_name('username')
+            form_textfield.send_keys("admin")
+
+        This can also be used to set file inputs.
+
+        ::
+
+            file_input = driver.find_element_by_name('profilePic')
+            file_input.send_keys("path/to/profilepic.gif")
+            # Generally it's better to wrap the file path in one of the methods
+            # in os.path to return the actual path to support cross OS testing.
+            # file_input.send_keys(os.path.abspath("path/to/profilepic.gif"))
+
+        """
+        # transfer file to another machine only if remote driver is used
+        # the same behaviour as for java binding
+        if self.parent._is_remote:
+            local_file = self.parent.file_detector.is_local_file(*value)
+            if local_file is not None:
+                value = self._upload(local_file)
+
+        self._execute(Command.SEND_KEYS_TO_ELEMENT,
+                      {'text': "".join(keys_to_typing(value)),
+                       'value': keys_to_typing(value)})
+
+    # RenderedWebElement Items
+    def is_displayed(self):
+        """Whether the element is visible to a user."""
+        # Only go into this conditional for browsers that don't use the atom themselves
+        if self._w3c:
+            return self.parent.execute_script(
+                "return (%s).apply(null, arguments);" % isDisplayed_js,
+                self)
+        else:
+            return self._execute(Command.IS_ELEMENT_DISPLAYED)['value']
+
+    @property
+    def location_once_scrolled_into_view(self):
+        """THIS PROPERTY MAY CHANGE WITHOUT WARNING. Use this to discover
+        where on the screen an element is so that we can click it. This method
+        should cause the element to be scrolled into view.
+
+        Returns the top lefthand corner location on the screen, or ``None`` if
+        the element is not visible.
+
+        """
+        if self._w3c:
+            old_loc = self._execute(Command.W3C_EXECUTE_SCRIPT, {
+                'script': "arguments[0].scrollIntoView(true); return arguments[0].getBoundingClientRect()",
+                'args': [self]})['value']
+            return {"x": round(old_loc['x']),
+                    "y": round(old_loc['y'])}
+        else:
+            return self._execute(Command.GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW)['value']
+
+    @property
+    def size(self):
+        """The size of the element."""
+        size = {}
+        if self._w3c:
+            size = self._execute(Command.GET_ELEMENT_RECT)['value']
+        else:
+            size = self._execute(Command.GET_ELEMENT_SIZE)['value']
+        new_size = {"height": size["height"],
+                    "width": size["width"]}
+        return new_size
+
+    def value_of_css_property(self, property_name):
+        """The value of a CSS property."""
+        return self._execute(Command.GET_ELEMENT_VALUE_OF_CSS_PROPERTY, {
+            'propertyName': property_name})['value']
+
+    @property
+    def location(self):
+        """The location of the element in the renderable canvas."""
+        if self._w3c:
+            old_loc = self._execute(Command.GET_ELEMENT_RECT)['value']
+        else:
+            old_loc = self._execute(Command.GET_ELEMENT_LOCATION)['value']
+        new_loc = {"x": round(old_loc['x']),
+                   "y": round(old_loc['y'])}
+        return new_loc
+
+    @property
+    def rect(self):
+        """A dictionary with the size and location of the element."""
+        if self._w3c:
+            return self._execute(Command.GET_ELEMENT_RECT)['value']
+        else:
+            rect = self.size.copy()
+            rect.update(self.location)
+            return rect
+
+    @property
+    def screenshot_as_base64(self):
+        """
+        Gets the screenshot of the current element as a base64 encoded string.
+
+        :Usage:
+            ::
+
+                img_b64 = element.screenshot_as_base64
+        """
+        return self._execute(Command.ELEMENT_SCREENSHOT)['value']
+
+    @property
+    def screenshot_as_png(self):
+        """
+        Gets the screenshot of the current element as a binary data.
+
+        :Usage:
+            ::
+
+                element_png = element.screenshot_as_png
+        """
+        return base64.b64decode(self.screenshot_as_base64.encode('ascii'))
+
+    def screenshot(self, filename):
+        """
+        Saves a screenshot of the current element to a PNG image file. Returns
+           False if there is any IOError, else returns True. Use full paths in
+           your filename.
+
+        :Args:
+         - filename: The full path you wish to save your screenshot to. This
+           should end with a `.png` extension.
+
+        :Usage:
+            ::
+
+                element.screenshot('/Screenshots/foo.png')
+        """
+        if not filename.lower().endswith('.png'):
+            warnings.warn("name used for saved screenshot does not match file "
+                          "type. It should end with a `.png` extension", UserWarning)
+        png = self.screenshot_as_png
+        try:
+            with open(filename, 'wb') as f:
+                f.write(png)
+        except IOError:
+            return False
+        finally:
+            del png
+        return True
+
+    @property
+    def parent(self):
+        """Internal reference to the WebDriver instance this element was found from."""
+        return self._parent
+
+    @property
+    def id(self):
+        """Internal ID used by selenium.
+
+        This is mainly for internal use. Simple use cases such as checking if 2
+        webelements refer to the same element, can be done using ``==``::
+
+            if element1 == element2:
+                print("These 2 are equal")
+
+        """
+        return self._id
+
+    def __eq__(self, element):
+        return hasattr(element, 'id') and self._id == element.id
+
+    def __ne__(self, element):
+        return not self.__eq__(element)
+
+    # Private Methods
+    def _execute(self, command, params=None):
+        """Executes a command against the underlying HTML element.
+
+        Args:
+          command: The name of the command to _execute as a string.
+          params: A dictionary of named parameters to send with the command.
+
+        Returns:
+          The command's JSON response loaded into a dictionary object.
+        """
+        if not params:
+            params = {}
+        params['id'] = self._id
+        return self._parent.execute(command, params)
+
+    def find_element(self, by=By.ID, value=None):
+        """
+        Find an element given a By strategy and locator. Prefer the find_element_by_* methods when
+        possible.
+
+        :Usage:
+            ::
+
+                element = element.find_element(By.ID, 'foo')
+
+        :rtype: WebElement
+        """
+        if self._w3c:
+            if by == By.ID:
+                by = By.CSS_SELECTOR
+                value = '[id="%s"]' % value
+            elif by == By.TAG_NAME:
+                by = By.CSS_SELECTOR
+            elif by == By.CLASS_NAME:
+                by = By.CSS_SELECTOR
+                value = ".%s" % value
+            elif by == By.NAME:
+                by = By.CSS_SELECTOR
+                value = '[name="%s"]' % value
+
+        return self._execute(Command.FIND_CHILD_ELEMENT,
+                             {"using": by, "value": value})['value']
+
+    def find_elements(self, by=By.ID, value=None):
+        """
+        Find elements given a By strategy and locator. Prefer the find_elements_by_* methods when
+        possible.
+
+        :Usage:
+            ::
+
+                element = element.find_elements(By.CLASS_NAME, 'foo')
+
+        :rtype: list of WebElement
+        """
+        if self._w3c:
+            if by == By.ID:
+                by = By.CSS_SELECTOR
+                value = '[id="%s"]' % value
+            elif by == By.TAG_NAME:
+                by = By.CSS_SELECTOR
+            elif by == By.CLASS_NAME:
+                by = By.CSS_SELECTOR
+                value = ".%s" % value
+            elif by == By.NAME:
+                by = By.CSS_SELECTOR
+                value = '[name="%s"]' % value
+
+        return self._execute(Command.FIND_CHILD_ELEMENTS,
+                             {"using": by, "value": value})['value']
+
+    def __hash__(self):
+        return int(hashlib.md5(self._id.encode('utf-8')).hexdigest(), 16)
+
+    def _upload(self, filename):
+        fp = IOStream()
+        zipped = zipfile.ZipFile(fp, 'w', zipfile.ZIP_DEFLATED)
+        zipped.write(filename, os.path.split(filename)[1])
+        zipped.close()
+        content = base64.encodestring(fp.getvalue())
+        if not isinstance(content, str):
+            content = content.decode('utf-8')
+        try:
+            return self._execute(Command.UPLOAD_FILE, {'file': content})['value']
+        except WebDriverException as e:
+            if "Unrecognized command: POST" in e.__str__():
+                return filename
+            elif "Command not found: POST " in e.__str__():
+                return filename
+            elif '{"status":405,"value":["GET","HEAD","DELETE"]}' in e.__str__():
+                return filename
+            else:
+                raise e
diff --git a/selenium/webdriver/safari/__init__.py b/selenium/webdriver/safari/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/safari/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/safari/permissions.py b/selenium/webdriver/safari/permissions.py
new file mode 100644
index 00000000..e95f2bc9
--- /dev/null
+++ b/selenium/webdriver/safari/permissions.py
@@ -0,0 +1,28 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+The Permission implementation.
+"""
+
+
+class Permission(object):
+    """
+    Set of supported permissions.
+    """
+
+    GET_USER_MEDIA = "getUserMedia"
diff --git a/selenium/webdriver/safari/remote_connection.py b/selenium/webdriver/safari/remote_connection.py
new file mode 100644
index 00000000..ee3bdc66
--- /dev/null
+++ b/selenium/webdriver/safari/remote_connection.py
@@ -0,0 +1,27 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.remote.remote_connection import RemoteConnection
+
+
+class SafariRemoteConnection(RemoteConnection):
+    def __init__(self, remote_server_addr, keep_alive=True):
+        RemoteConnection.__init__(self, remote_server_addr, keep_alive)
+
+        self._commands["GET_PERMISSIONS"] = ('GET', '/session/$sessionId/apple/permissions')
+        self._commands["SET_PERMISSIONS"] = ('POST', '/session/$sessionId/apple/permissions')
+        self._commands["ATTACH_DEBUGGER"] = ('POST', '/session/$sessionId/apple/attach_debugger')
diff --git a/selenium/webdriver/safari/service.py b/selenium/webdriver/safari/service.py
new file mode 100644
index 00000000..bdbae56c
--- /dev/null
+++ b/selenium/webdriver/safari/service.py
@@ -0,0 +1,64 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import os
+from selenium.webdriver.common import service, utils
+from subprocess import PIPE
+
+
+class Service(service.Service):
+    """
+    Object that manages the starting and stopping of the SafariDriver
+    """
+
+    def __init__(self, executable_path, port=0, quiet=False, service_args=None):
+        """
+        Creates a new instance of the Service
+
+        :Args:
+         - executable_path : Path to the SafariDriver
+         - port : Port the service is running on
+         - quiet : Suppress driver stdout and stderr
+         - service_args : List of args to pass to the safaridriver service """
+
+        if not os.path.exists(executable_path):
+            if "Safari Technology Preview" in executable_path:
+                message = "Safari Technology Preview does not seem to be installed. You can download it at https://developer.apple.com/safari/download/."
+            else:
+                message = "SafariDriver was not found; are you running Safari 10 or later? You can download Safari at https://developer.apple.com/safari/download/."
+            raise Exception(message)
+
+        if port == 0:
+            port = utils.free_port()
+
+        self.service_args = service_args or []
+
+        self.quiet = quiet
+        log = PIPE
+        if quiet:
+            log = open(os.devnull, 'w')
+        service.Service.__init__(self, executable_path, port, log)
+
+    def command_line_args(self):
+        return ["-p", "%s" % self.port] + self.service_args
+
+    @property
+    def service_url(self):
+        """
+        Gets the url of the SafariDriver Service
+        """
+        return "http://localhost:%d" % self.port
diff --git a/selenium/webdriver/safari/webdriver.py b/selenium/webdriver/safari/webdriver.py
new file mode 100644
index 00000000..547e60a1
--- /dev/null
+++ b/selenium/webdriver/safari/webdriver.py
@@ -0,0 +1,113 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from selenium.common.exceptions import WebDriverException
+
+try:
+    import http.client as http_client
+except ImportError:
+    import httplib as http_client
+
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
+from .service import Service
+from .remote_connection import SafariRemoteConnection
+
+
+class WebDriver(RemoteWebDriver):
+    """
+    Controls the SafariDriver and allows you to drive the browser.
+
+    """
+
+    def __init__(self, port=0, executable_path="/usr/bin/safaridriver", reuse_service=False,
+                 desired_capabilities=DesiredCapabilities.SAFARI, quiet=False,
+                 keep_alive=True, service_args=None):
+        """
+
+        Creates a new Safari driver instance and launches or finds a running safaridriver service.
+
+        :Args:
+         - port - The port on which the safaridriver service should listen for new connections. If zero, a free port will be found.
+         - executable_path - Path to a custom safaridriver executable to be used. If absent, /usr/bin/safaridriver is used.
+         - reuse_service - If True, do not spawn a safaridriver instance; instead, connect to an already-running service that was launched externally.
+         - desired_capabilities: Dictionary object with desired capabilities (Can be used to provide various Safari switches).
+         - quiet - If True, the driver's stdout and stderr is suppressed.
+         - keep_alive - Whether to configure SafariRemoteConnection to use
+             HTTP keep-alive. Defaults to False.
+         - service_args : List of args to pass to the safaridriver service
+        """
+
+        self._reuse_service = reuse_service
+        self.service = Service(executable_path, port=port, quiet=quiet, service_args=service_args)
+        if not reuse_service:
+            self.service.start()
+
+        executor = SafariRemoteConnection(remote_server_addr=self.service.service_url,
+                                          keep_alive=keep_alive)
+
+        RemoteWebDriver.__init__(
+            self,
+            command_executor=executor,
+            desired_capabilities=desired_capabilities)
+
+        self._is_remote = False
+
+    def quit(self):
+        """
+        Closes the browser and shuts down the SafariDriver executable
+        that is started when starting the SafariDriver
+        """
+        try:
+            RemoteWebDriver.quit(self)
+        except http_client.BadStatusLine:
+            pass
+        finally:
+            if not self._reuse_service:
+                self.service.stop()
+
+    # safaridriver extension commands. The canonical command support matrix is here:
+    # https://developer.apple.com/library/content/documentation/NetworkingInternetWeb/Conceptual/WebDriverEndpointDoc/Commands/Commands.html
+
+    # First available in Safari 11.1 and Safari Technology Preview 41.
+    def set_permission(self, permission, value):
+        if not isinstance(value, bool):
+            raise WebDriverException("Value of a session permission must be set to True or False.")
+
+        payload = {}
+        payload[permission] = value
+        self.execute("SET_PERMISSIONS", {"permissions": payload})
+
+    # First available in Safari 11.1 and Safari Technology Preview 41.
+    def get_permission(self, permission):
+        payload = self.execute("GET_PERMISSIONS")["value"]
+        permissions = payload["permissions"]
+        if not permissions:
+            return None
+
+        if permission not in permissions:
+            return None
+
+        value = permissions[permission]
+        if not isinstance(value, bool):
+            return None
+
+        return value
+
+    # First available in Safari 11.1 and Safari Technology Preview 42.
+    def debug(self):
+        self.execute("ATTACH_DEBUGGER")
+        self.execute_script("debugger;")
diff --git a/selenium/webdriver/support/__init__.py b/selenium/webdriver/support/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/support/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/support/abstract_event_listener.py b/selenium/webdriver/support/abstract_event_listener.py
new file mode 100644
index 00000000..7dd166b7
--- /dev/null
+++ b/selenium/webdriver/support/abstract_event_listener.py
@@ -0,0 +1,79 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+class AbstractEventListener(object):
+    """
+    Event listener must subclass and implement this fully or partially
+    """
+
+    def before_navigate_to(self, url, driver):
+        pass
+
+    def after_navigate_to(self, url, driver):
+        pass
+
+    def before_navigate_back(self, driver):
+        pass
+
+    def after_navigate_back(self, driver):
+        pass
+
+    def before_navigate_forward(self, driver):
+        pass
+
+    def after_navigate_forward(self, driver):
+        pass
+
+    def before_find(self, by, value, driver):
+        pass
+
+    def after_find(self, by, value, driver):
+        pass
+
+    def before_click(self, element, driver):
+        pass
+
+    def after_click(self, element, driver):
+        pass
+
+    def before_change_value_of(self, element, driver):
+        pass
+
+    def after_change_value_of(self, element, driver):
+        pass
+
+    def before_execute_script(self, script, driver):
+        pass
+
+    def after_execute_script(self, script, driver):
+        pass
+
+    def before_close(self, driver):
+        pass
+
+    def after_close(self, driver):
+        pass
+
+    def before_quit(self, driver):
+        pass
+
+    def after_quit(self, driver):
+        pass
+
+    def on_exception(self, exception, driver):
+        pass
diff --git a/selenium/webdriver/support/color.py b/selenium/webdriver/support/color.py
new file mode 100644
index 00000000..b94f7e51
--- /dev/null
+++ b/selenium/webdriver/support/color.py
@@ -0,0 +1,310 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+RGB_PATTERN = r"^\s*rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)\s*$"
+RGB_PCT_PATTERN = r"^\s*rgb\(\s*(\d{1,3}|\d{1,2}\.\d+)%\s*,\s*(\d{1,3}|\d{1,2}\.\d+)%\s*,\s*(\d{1,3}|\d{1,2}\.\d+)%\s*\)\s*$"
+RGBA_PATTERN = r"^\s*rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0|1|0\.\d+)\s*\)\s*$"
+RGBA_PCT_PATTERN = r"^\s*rgba\(\s*(\d{1,3}|\d{1,2}\.\d+)%\s*,\s*(\d{1,3}|\d{1,2}\.\d+)%\s*,\s*(\d{1,3}|\d{1,2}\.\d+)%\s*,\s*(0|1|0\.\d+)\s*\)\s*$"
+HEX_PATTERN = r"#([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})"
+HEX3_PATTERN = r"#([A-Fa-f0-9])([A-Fa-f0-9])([A-Fa-f0-9])"
+HSL_PATTERN = r"^\s*hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)\s*$"
+HSLA_PATTERN = r"^\s*hsla\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*,\s*(0|1|0\.\d+)\s*\)\s*$"
+
+
+class Color(object):
+    """
+    Color conversion support class
+
+    Example:
+
+    ::
+
+        from selenium.webdriver.support.color import Color
+
+        print(Color.from_string('#00ff33').rgba)
+        print(Color.from_string('rgb(1, 255, 3)').hex)
+        print(Color.from_string('blue').rgba)
+    """
+
+    @staticmethod
+    def from_string(str_):
+        import re
+
+        class Matcher(object):
+            def __init__(self):
+                self.match_obj = None
+
+            def match(self, pattern, str_):
+                self.match_obj = re.match(pattern, str_)
+                return self.match_obj
+
+            @property
+            def groups(self):
+                return () if self.match_obj is None else self.match_obj.groups()
+
+        m = Matcher()
+
+        if m.match(RGB_PATTERN, str_):
+            return Color(*m.groups)
+        elif m.match(RGB_PCT_PATTERN, str_):
+            rgb = tuple([float(each) / 100 * 255 for each in m.groups])
+            return Color(*rgb)
+        elif m.match(RGBA_PATTERN, str_):
+            return Color(*m.groups)
+        elif m.match(RGBA_PCT_PATTERN, str_):
+            rgba = tuple([float(each) / 100 * 255 for each in m.groups[:3]] + [m.groups[3]])
+            return Color(*rgba)
+        elif m.match(HEX_PATTERN, str_):
+            rgb = tuple([int(each, 16) for each in m.groups])
+            return Color(*rgb)
+        elif m.match(HEX3_PATTERN, str_):
+            rgb = tuple([int(each * 2, 16) for each in m.groups])
+            return Color(*rgb)
+        elif m.match(HSL_PATTERN, str_) or m.match(HSLA_PATTERN, str_):
+            return Color._from_hsl(*m.groups)
+        elif str_.upper() in Colors.keys():
+            return Colors[str_.upper()]
+        else:
+            raise ValueError("Could not convert %s into color" % str_)
+
+    @staticmethod
+    def _from_hsl(h, s, l, a=1):
+        h = float(h) / 360
+        s = float(s) / 100
+        l = float(l) / 100
+
+        if s == 0:
+            r = l
+            g = r
+            b = r
+        else:
+            luminocity2 = l * (1 + s) if l < 0.5 else l + s - l * s
+            luminocity1 = 2 * l - luminocity2
+
+            def hue_to_rgb(lum1, lum2, hue):
+                if hue < 0.0:
+                    hue += 1
+                if hue > 1.0:
+                    hue -= 1
+
+                if hue < 1.0 / 6.0:
+                    return (lum1 + (lum2 - lum1) * 6.0 * hue)
+                elif hue < 1.0 / 2.0:
+                    return lum2
+                elif hue < 2.0 / 3.0:
+                    return lum1 + (lum2 - lum1) * ((2.0 / 3.0) - hue) * 6.0
+                else:
+                    return lum1
+
+            r = hue_to_rgb(luminocity1, luminocity2, h + 1.0 / 3.0)
+            g = hue_to_rgb(luminocity1, luminocity2, h)
+            b = hue_to_rgb(luminocity1, luminocity2, h - 1.0 / 3.0)
+
+        return Color(round(r * 255), round(g * 255), round(b * 255), a)
+
+    def __init__(self, red, green, blue, alpha=1):
+        self.red = int(red)
+        self.green = int(green)
+        self.blue = int(blue)
+        self.alpha = "1" if float(alpha) == 1 else str(float(alpha) or 0)
+
+    @property
+    def rgb(self):
+        return "rgb(%d, %d, %d)" % (self.red, self.green, self.blue)
+
+    @property
+    def rgba(self):
+        return "rgba(%d, %d, %d, %s)" % (self.red, self.green, self.blue, self.alpha)
+
+    @property
+    def hex(self):
+        return "#%02x%02x%02x" % (self.red, self.green, self.blue)
+
+    def __eq__(self, other):
+        if isinstance(other, Color):
+            return self.rgba == other.rgba
+        return NotImplemented
+
+    def __ne__(self, other):
+        result = self.__eq__(other)
+        if result is NotImplemented:
+            return result
+        return not result
+
+    def __hash__(self):
+        return hash((self.red, self.green, self.blue, self.alpha))
+
+    def __repr__(self):
+        return "Color(red=%d, green=%d, blue=%d, alpha=%s)" % (self.red, self.green, self.blue, self.alpha)
+
+    def __str__(self):
+        return "Color: %s" % self.rgba
+
+
+# Basic, extended and transparent colour keywords as defined by the W3C HTML4 spec
+# See http://www.w3.org/TR/css3-color/#html4
+Colors = {
+    "TRANSPARENT": Color(0, 0, 0, 0),
+    "ALICEBLUE": Color(240, 248, 255),
+    "ANTIQUEWHITE": Color(250, 235, 215),
+    "AQUA": Color(0, 255, 255),
+    "AQUAMARINE": Color(127, 255, 212),
+    "AZURE": Color(240, 255, 255),
+    "BEIGE": Color(245, 245, 220),
+    "BISQUE": Color(255, 228, 196),
+    "BLACK": Color(0, 0, 0),
+    "BLANCHEDALMOND": Color(255, 235, 205),
+    "BLUE": Color(0, 0, 255),
+    "BLUEVIOLET": Color(138, 43, 226),
+    "BROWN": Color(165, 42, 42),
+    "BURLYWOOD": Color(222, 184, 135),
+    "CADETBLUE": Color(95, 158, 160),
+    "CHARTREUSE": Color(127, 255, 0),
+    "CHOCOLATE": Color(210, 105, 30),
+    "CORAL": Color(255, 127, 80),
+    "CORNFLOWERBLUE": Color(100, 149, 237),
+    "CORNSILK": Color(255, 248, 220),
+    "CRIMSON": Color(220, 20, 60),
+    "CYAN": Color(0, 255, 255),
+    "DARKBLUE": Color(0, 0, 139),
+    "DARKCYAN": Color(0, 139, 139),
+    "DARKGOLDENROD": Color(184, 134, 11),
+    "DARKGRAY": Color(169, 169, 169),
+    "DARKGREEN": Color(0, 100, 0),
+    "DARKGREY": Color(169, 169, 169),
+    "DARKKHAKI": Color(189, 183, 107),
+    "DARKMAGENTA": Color(139, 0, 139),
+    "DARKOLIVEGREEN": Color(85, 107, 47),
+    "DARKORANGE": Color(255, 140, 0),
+    "DARKORCHID": Color(153, 50, 204),
+    "DARKRED": Color(139, 0, 0),
+    "DARKSALMON": Color(233, 150, 122),
+    "DARKSEAGREEN": Color(143, 188, 143),
+    "DARKSLATEBLUE": Color(72, 61, 139),
+    "DARKSLATEGRAY": Color(47, 79, 79),
+    "DARKSLATEGREY": Color(47, 79, 79),
+    "DARKTURQUOISE": Color(0, 206, 209),
+    "DARKVIOLET": Color(148, 0, 211),
+    "DEEPPINK": Color(255, 20, 147),
+    "DEEPSKYBLUE": Color(0, 191, 255),
+    "DIMGRAY": Color(105, 105, 105),
+    "DIMGREY": Color(105, 105, 105),
+    "DODGERBLUE": Color(30, 144, 255),
+    "FIREBRICK": Color(178, 34, 34),
+    "FLORALWHITE": Color(255, 250, 240),
+    "FORESTGREEN": Color(34, 139, 34),
+    "FUCHSIA": Color(255, 0, 255),
+    "GAINSBORO": Color(220, 220, 220),
+    "GHOSTWHITE": Color(248, 248, 255),
+    "GOLD": Color(255, 215, 0),
+    "GOLDENROD": Color(218, 165, 32),
+    "GRAY": Color(128, 128, 128),
+    "GREY": Color(128, 128, 128),
+    "GREEN": Color(0, 128, 0),
+    "GREENYELLOW": Color(173, 255, 47),
+    "HONEYDEW": Color(240, 255, 240),
+    "HOTPINK": Color(255, 105, 180),
+    "INDIANRED": Color(205, 92, 92),
+    "INDIGO": Color(75, 0, 130),
+    "IVORY": Color(255, 255, 240),
+    "KHAKI": Color(240, 230, 140),
+    "LAVENDER": Color(230, 230, 250),
+    "LAVENDERBLUSH": Color(255, 240, 245),
+    "LAWNGREEN": Color(124, 252, 0),
+    "LEMONCHIFFON": Color(255, 250, 205),
+    "LIGHTBLUE": Color(173, 216, 230),
+    "LIGHTCORAL": Color(240, 128, 128),
+    "LIGHTCYAN": Color(224, 255, 255),
+    "LIGHTGOLDENRODYELLOW": Color(250, 250, 210),
+    "LIGHTGRAY": Color(211, 211, 211),
+    "LIGHTGREEN": Color(144, 238, 144),
+    "LIGHTGREY": Color(211, 211, 211),
+    "LIGHTPINK": Color(255, 182, 193),
+    "LIGHTSALMON": Color(255, 160, 122),
+    "LIGHTSEAGREEN": Color(32, 178, 170),
+    "LIGHTSKYBLUE": Color(135, 206, 250),
+    "LIGHTSLATEGRAY": Color(119, 136, 153),
+    "LIGHTSLATEGREY": Color(119, 136, 153),
+    "LIGHTSTEELBLUE": Color(176, 196, 222),
+    "LIGHTYELLOW": Color(255, 255, 224),
+    "LIME": Color(0, 255, 0),
+    "LIMEGREEN": Color(50, 205, 50),
+    "LINEN": Color(250, 240, 230),
+    "MAGENTA": Color(255, 0, 255),
+    "MAROON": Color(128, 0, 0),
+    "MEDIUMAQUAMARINE": Color(102, 205, 170),
+    "MEDIUMBLUE": Color(0, 0, 205),
+    "MEDIUMORCHID": Color(186, 85, 211),
+    "MEDIUMPURPLE": Color(147, 112, 219),
+    "MEDIUMSEAGREEN": Color(60, 179, 113),
+    "MEDIUMSLATEBLUE": Color(123, 104, 238),
+    "MEDIUMSPRINGGREEN": Color(0, 250, 154),
+    "MEDIUMTURQUOISE": Color(72, 209, 204),
+    "MEDIUMVIOLETRED": Color(199, 21, 133),
+    "MIDNIGHTBLUE": Color(25, 25, 112),
+    "MINTCREAM": Color(245, 255, 250),
+    "MISTYROSE": Color(255, 228, 225),
+    "MOCCASIN": Color(255, 228, 181),
+    "NAVAJOWHITE": Color(255, 222, 173),
+    "NAVY": Color(0, 0, 128),
+    "OLDLACE": Color(253, 245, 230),
+    "OLIVE": Color(128, 128, 0),
+    "OLIVEDRAB": Color(107, 142, 35),
+    "ORANGE": Color(255, 165, 0),
+    "ORANGERED": Color(255, 69, 0),
+    "ORCHID": Color(218, 112, 214),
+    "PALEGOLDENROD": Color(238, 232, 170),
+    "PALEGREEN": Color(152, 251, 152),
+    "PALETURQUOISE": Color(175, 238, 238),
+    "PALEVIOLETRED": Color(219, 112, 147),
+    "PAPAYAWHIP": Color(255, 239, 213),
+    "PEACHPUFF": Color(255, 218, 185),
+    "PERU": Color(205, 133, 63),
+    "PINK": Color(255, 192, 203),
+    "PLUM": Color(221, 160, 221),
+    "POWDERBLUE": Color(176, 224, 230),
+    "PURPLE": Color(128, 0, 128),
+    "REBECCAPURPLE": Color(128, 51, 153),
+    "RED": Color(255, 0, 0),
+    "ROSYBROWN": Color(188, 143, 143),
+    "ROYALBLUE": Color(65, 105, 225),
+    "SADDLEBROWN": Color(139, 69, 19),
+    "SALMON": Color(250, 128, 114),
+    "SANDYBROWN": Color(244, 164, 96),
+    "SEAGREEN": Color(46, 139, 87),
+    "SEASHELL": Color(255, 245, 238),
+    "SIENNA": Color(160, 82, 45),
+    "SILVER": Color(192, 192, 192),
+    "SKYBLUE": Color(135, 206, 235),
+    "SLATEBLUE": Color(106, 90, 205),
+    "SLATEGRAY": Color(112, 128, 144),
+    "SLATEGREY": Color(112, 128, 144),
+    "SNOW": Color(255, 250, 250),
+    "SPRINGGREEN": Color(0, 255, 127),
+    "STEELBLUE": Color(70, 130, 180),
+    "TAN": Color(210, 180, 140),
+    "TEAL": Color(0, 128, 128),
+    "THISTLE": Color(216, 191, 216),
+    "TOMATO": Color(255, 99, 71),
+    "TURQUOISE": Color(64, 224, 208),
+    "VIOLET": Color(238, 130, 238),
+    "WHEAT": Color(245, 222, 179),
+    "WHITE": Color(255, 255, 255),
+    "WHITESMOKE": Color(245, 245, 245),
+    "YELLOW": Color(255, 255, 0),
+    "YELLOWGREEN": Color(154, 205, 50)
+}
diff --git a/selenium/webdriver/support/event_firing_webdriver.py b/selenium/webdriver/support/event_firing_webdriver.py
new file mode 100644
index 00000000..318dd85b
--- /dev/null
+++ b/selenium/webdriver/support/event_firing_webdriver.py
@@ -0,0 +1,322 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.common.by import By
+from selenium.webdriver.remote.webdriver import WebDriver
+from selenium.webdriver.remote.webelement import WebElement
+from .abstract_event_listener import AbstractEventListener
+
+
+def _wrap_elements(result, ef_driver):
+    if isinstance(result, WebElement):
+        return EventFiringWebElement(result, ef_driver)
+    elif isinstance(result, list):
+        return [_wrap_elements(item, ef_driver) for item in result]
+    else:
+        return result
+
+
+class EventFiringWebDriver(object):
+    """
+    A wrapper around an arbitrary WebDriver instance which supports firing events
+    """
+
+    def __init__(self, driver, event_listener):
+        """
+        Creates a new instance of the EventFiringWebDriver
+
+        :Args:
+         - driver : A WebDriver instance
+         - event_listener : Instance of a class that subclasses AbstractEventListener and implements it fully or partially
+
+        Example:
+
+        ::
+
+            from selenium.webdriver import Firefox
+            from selenium.webdriver.support.events import EventFiringWebDriver, AbstractEventListener
+
+            class MyListener(AbstractEventListener):
+                def before_navigate_to(self, url, driver):
+                    print("Before navigate to %s" % url)
+                def after_navigate_to(self, url, driver):
+                    print("After navigate to %s" % url)
+
+            driver = Firefox()
+            ef_driver = EventFiringWebDriver(driver, MyListener())
+            ef_driver.get("http://www.google.co.in/")
+        """
+        if not isinstance(driver, WebDriver):
+            raise WebDriverException("A WebDriver instance must be supplied")
+        if not isinstance(event_listener, AbstractEventListener):
+            raise WebDriverException("Event listener must be a subclass of AbstractEventListener")
+        self._driver = driver
+        self._driver._wrap_value = self._wrap_value
+        self._listener = event_listener
+
+    @property
+    def wrapped_driver(self):
+        """Returns the WebDriver instance wrapped by this EventsFiringWebDriver"""
+        return self._driver
+
+    def get(self, url):
+        self._dispatch("navigate_to", (url, self._driver), "get", (url, ))
+
+    def back(self):
+        self._dispatch("navigate_back", (self._driver,), "back", ())
+
+    def forward(self):
+        self._dispatch("navigate_forward", (self._driver,), "forward", ())
+
+    def execute_script(self, script, *args):
+        unwrapped_args = (script,) + self._unwrap_element_args(args)
+        return self._dispatch("execute_script", (script, self._driver), "execute_script", unwrapped_args)
+
+    def execute_async_script(self, script, *args):
+        unwrapped_args = (script,) + self._unwrap_element_args(args)
+        return self._dispatch("execute_script", (script, self._driver), "execute_async_script", unwrapped_args)
+
+    def close(self):
+        self._dispatch("close", (self._driver,), "close", ())
+
+    def quit(self):
+        self._dispatch("quit", (self._driver,), "quit", ())
+
+    def find_element(self, by=By.ID, value=None):
+        return self._dispatch("find", (by, value, self._driver), "find_element", (by, value))
+
+    def find_elements(self, by=By.ID, value=None):
+        return self._dispatch("find", (by, value, self._driver), "find_elements", (by, value))
+
+    def find_element_by_id(self, id_):
+        return self.find_element(by=By.ID, value=id_)
+
+    def find_elements_by_id(self, id_):
+        return self.find_elements(by=By.ID, value=id_)
+
+    def find_element_by_xpath(self, xpath):
+        return self.find_element(by=By.XPATH, value=xpath)
+
+    def find_elements_by_xpath(self, xpath):
+        return self.find_elements(by=By.XPATH, value=xpath)
+
+    def find_element_by_link_text(self, link_text):
+        return self.find_element(by=By.LINK_TEXT, value=link_text)
+
+    def find_elements_by_link_text(self, text):
+        return self.find_elements(by=By.LINK_TEXT, value=text)
+
+    def find_element_by_partial_link_text(self, link_text):
+        return self.find_element(by=By.PARTIAL_LINK_TEXT, value=link_text)
+
+    def find_elements_by_partial_link_text(self, link_text):
+        return self.find_elements(by=By.PARTIAL_LINK_TEXT, value=link_text)
+
+    def find_element_by_name(self, name):
+        return self.find_element(by=By.NAME, value=name)
+
+    def find_elements_by_name(self, name):
+        return self.find_elements(by=By.NAME, value=name)
+
+    def find_element_by_tag_name(self, name):
+        return self.find_element(by=By.TAG_NAME, value=name)
+
+    def find_elements_by_tag_name(self, name):
+        return self.find_elements(by=By.TAG_NAME, value=name)
+
+    def find_element_by_class_name(self, name):
+        return self.find_element(by=By.CLASS_NAME, value=name)
+
+    def find_elements_by_class_name(self, name):
+        return self.find_elements(by=By.CLASS_NAME, value=name)
+
+    def find_element_by_css_selector(self, css_selector):
+        return self.find_element(by=By.CSS_SELECTOR, value=css_selector)
+
+    def find_elements_by_css_selector(self, css_selector):
+        return self.find_elements(by=By.CSS_SELECTOR, value=css_selector)
+
+    def _dispatch(self, l_call, l_args, d_call, d_args):
+        getattr(self._listener, "before_%s" % l_call)(*l_args)
+        try:
+            result = getattr(self._driver, d_call)(*d_args)
+        except Exception as e:
+            self._listener.on_exception(e, self._driver)
+            raise e
+        getattr(self._listener, "after_%s" % l_call)(*l_args)
+        return _wrap_elements(result, self)
+
+    def _unwrap_element_args(self, args):
+        if isinstance(args, EventFiringWebElement):
+            return args.wrapped_element
+        elif isinstance(args, tuple):
+            return tuple([self._unwrap_element_args(item) for item in args])
+        elif isinstance(args, list):
+            return [self._unwrap_element_args(item) for item in args]
+        else:
+            return args
+
+    def _wrap_value(self, value):
+        if isinstance(value, EventFiringWebElement):
+            return WebDriver._wrap_value(self._driver, value.wrapped_element)
+        return WebDriver._wrap_value(self._driver, value)
+
+    def __setattr__(self, item, value):
+        if item.startswith("_") or not hasattr(self._driver, item):
+            object.__setattr__(self, item, value)
+        else:
+            try:
+                object.__setattr__(self._driver, item, value)
+            except Exception as e:
+                self._listener.on_exception(e, self._driver)
+                raise e
+
+    def __getattr__(self, name):
+        def _wrap(*args, **kwargs):
+            try:
+                result = attrib(*args, **kwargs)
+                return _wrap_elements(result, self)
+            except Exception as e:
+                self._listener.on_exception(e, self._driver)
+                raise
+
+        try:
+            attrib = getattr(self._driver, name)
+            return _wrap if callable(attrib) else attrib
+        except Exception as e:
+            self._listener.on_exception(e, self._driver)
+            raise
+
+
+class EventFiringWebElement(object):
+    """"
+    A wrapper around WebElement instance which supports firing events
+    """
+
+    def __init__(self, webelement, ef_driver):
+        """
+        Creates a new instance of the EventFiringWebElement
+        """
+        self._webelement = webelement
+        self._ef_driver = ef_driver
+        self._driver = ef_driver.wrapped_driver
+        self._listener = ef_driver._listener
+
+    @property
+    def wrapped_element(self):
+        """Returns the WebElement wrapped by this EventFiringWebElement instance"""
+        return self._webelement
+
+    def click(self):
+        self._dispatch("click", (self._webelement, self._driver), "click", ())
+
+    def clear(self):
+        self._dispatch("change_value_of", (self._webelement, self._driver), "clear", ())
+
+    def send_keys(self, *value):
+        self._dispatch("change_value_of", (self._webelement, self._driver), "send_keys", value)
+
+    def find_element(self, by=By.ID, value=None):
+        return self._dispatch("find", (by, value, self._driver), "find_element", (by, value))
+
+    def find_elements(self, by=By.ID, value=None):
+        return self._dispatch("find", (by, value, self._driver), "find_elements", (by, value))
+
+    def find_element_by_id(self, id_):
+        return self.find_element(by=By.ID, value=id_)
+
+    def find_elements_by_id(self, id_):
+        return self.find_elements(by=By.ID, value=id_)
+
+    def find_element_by_name(self, name):
+        return self.find_element(by=By.NAME, value=name)
+
+    def find_elements_by_name(self, name):
+        return self.find_elements(by=By.NAME, value=name)
+
+    def find_element_by_link_text(self, link_text):
+        return self.find_element(by=By.LINK_TEXT, value=link_text)
+
+    def find_elements_by_link_text(self, link_text):
+        return self.find_elements(by=By.LINK_TEXT, value=link_text)
+
+    def find_element_by_partial_link_text(self, link_text):
+        return self.find_element(by=By.PARTIAL_LINK_TEXT, value=link_text)
+
+    def find_elements_by_partial_link_text(self, link_text):
+        return self.find_elements(by=By.PARTIAL_LINK_TEXT, value=link_text)
+
+    def find_element_by_tag_name(self, name):
+        return self.find_element(by=By.TAG_NAME, value=name)
+
+    def find_elements_by_tag_name(self, name):
+        return self.find_elements(by=By.TAG_NAME, value=name)
+
+    def find_element_by_xpath(self, xpath):
+        return self.find_element(by=By.XPATH, value=xpath)
+
+    def find_elements_by_xpath(self, xpath):
+        return self.find_elements(by=By.XPATH, value=xpath)
+
+    def find_element_by_class_name(self, name):
+        return self.find_element(by=By.CLASS_NAME, value=name)
+
+    def find_elements_by_class_name(self, name):
+        return self.find_elements(by=By.CLASS_NAME, value=name)
+
+    def find_element_by_css_selector(self, css_selector):
+        return self.find_element(by=By.CSS_SELECTOR, value=css_selector)
+
+    def find_elements_by_css_selector(self, css_selector):
+        return self.find_elements(by=By.CSS_SELECTOR, value=css_selector)
+
+    def _dispatch(self, l_call, l_args, d_call, d_args):
+        getattr(self._listener, "before_%s" % l_call)(*l_args)
+        try:
+            result = getattr(self._webelement, d_call)(*d_args)
+        except Exception as e:
+            self._listener.on_exception(e, self._driver)
+            raise e
+        getattr(self._listener, "after_%s" % l_call)(*l_args)
+        return _wrap_elements(result, self._ef_driver)
+
+    def __setattr__(self, item, value):
+        if item.startswith("_") or not hasattr(self._webelement, item):
+            object.__setattr__(self, item, value)
+        else:
+            try:
+                object.__setattr__(self._webelement, item, value)
+            except Exception as e:
+                self._listener.on_exception(e, self._driver)
+                raise e
+
+    def __getattr__(self, name):
+        def _wrap(*args, **kwargs):
+            try:
+                result = attrib(*args, **kwargs)
+                return _wrap_elements(result, self._ef_driver)
+            except Exception as e:
+                self._listener.on_exception(e, self._driver)
+                raise
+
+        try:
+            attrib = getattr(self._webelement, name)
+            return _wrap if callable(attrib) else attrib
+        except Exception as e:
+            self._listener.on_exception(e, self._driver)
+            raise
diff --git a/selenium/webdriver/support/events.py b/selenium/webdriver/support/events.py
new file mode 100644
index 00000000..775d00a6
--- /dev/null
+++ b/selenium/webdriver/support/events.py
@@ -0,0 +1,19 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from .abstract_event_listener import AbstractEventListener  # noqa
+from .event_firing_webdriver import EventFiringWebDriver  # noqa
diff --git a/selenium/webdriver/support/expected_conditions.py b/selenium/webdriver/support/expected_conditions.py
new file mode 100644
index 00000000..2a64920d
--- /dev/null
+++ b/selenium/webdriver/support/expected_conditions.py
@@ -0,0 +1,422 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.common.exceptions import NoSuchElementException
+from selenium.common.exceptions import NoSuchFrameException
+from selenium.common.exceptions import StaleElementReferenceException
+from selenium.common.exceptions import WebDriverException
+from selenium.common.exceptions import NoAlertPresentException
+from selenium.webdriver.remote.webdriver import WebElement
+
+"""
+ * Canned "Expected Conditions" which are generally useful within webdriver
+ * tests.
+"""
+
+
+class title_is(object):
+    """An expectation for checking the title of a page.
+    title is the expected title, which must be an exact match
+    returns True if the title matches, false otherwise."""
+    def __init__(self, title):
+        self.title = title
+
+    def __call__(self, driver):
+        return self.title == driver.title
+
+
+class title_contains(object):
+    """ An expectation for checking that the title contains a case-sensitive
+    substring. title is the fragment of title expected
+    returns True when the title matches, False otherwise
+    """
+    def __init__(self, title):
+        self.title = title
+
+    def __call__(self, driver):
+        return self.title in driver.title
+
+
+class presence_of_element_located(object):
+    """ An expectation for checking that an element is present on the DOM
+    of a page. This does not necessarily mean that the element is visible.
+    locator - used to find the element
+    returns the WebElement once it is located
+    """
+    def __init__(self, locator):
+        self.locator = locator
+
+    def __call__(self, driver):
+        return _find_element(driver, self.locator)
+
+
+class url_contains(object):
+    """ An expectation for checking that the current url contains a
+    case-sensitive substring.
+    url is the fragment of url expected,
+    returns True when the url matches, False otherwise
+    """
+    def __init__(self, url):
+        self.url = url
+
+    def __call__(self, driver):
+        return self.url in driver.current_url
+
+
+class url_matches(object):
+    """An expectation for checking the current url.
+    pattern is the expected pattern, which must be an exact match
+    returns True if the url matches, false otherwise."""
+    def __init__(self, pattern):
+        self.pattern = pattern
+
+    def __call__(self, driver):
+        import re
+        match = re.search(self.pattern, driver.current_url)
+
+        return match is not None
+
+
+class url_to_be(object):
+    """An expectation for checking the current url.
+    url is the expected url, which must be an exact match
+    returns True if the url matches, false otherwise."""
+    def __init__(self, url):
+        self.url = url
+
+    def __call__(self, driver):
+        return self.url == driver.current_url
+
+
+class url_changes(object):
+    """An expectation for checking the current url.
+    url is the expected url, which must not be an exact match
+    returns True if the url is different, false otherwise."""
+    def __init__(self, url):
+        self.url = url
+
+    def __call__(self, driver):
+        return self.url != driver.current_url
+
+
+class visibility_of_element_located(object):
+    """ An expectation for checking that an element is present on the DOM of a
+    page and visible. Visibility means that the element is not only displayed
+    but also has a height and width that is greater than 0.
+    locator - used to find the element
+    returns the WebElement once it is located and visible
+    """
+    def __init__(self, locator):
+        self.locator = locator
+
+    def __call__(self, driver):
+        try:
+            return _element_if_visible(_find_element(driver, self.locator))
+        except StaleElementReferenceException:
+            return False
+
+
+class visibility_of(object):
+    """ An expectation for checking that an element, known to be present on the
+    DOM of a page, is visible. Visibility means that the element is not only
+    displayed but also has a height and width that is greater than 0.
+    element is the WebElement
+    returns the (same) WebElement once it is visible
+    """
+    def __init__(self, element):
+        self.element = element
+
+    def __call__(self, ignored):
+        return _element_if_visible(self.element)
+
+
+def _element_if_visible(element, visibility=True):
+    return element if element.is_displayed() == visibility else False
+
+
+class presence_of_all_elements_located(object):
+    """ An expectation for checking that there is at least one element present
+    on a web page.
+    locator is used to find the element
+    returns the list of WebElements once they are located
+    """
+    def __init__(self, locator):
+        self.locator = locator
+
+    def __call__(self, driver):
+        return _find_elements(driver, self.locator)
+
+
+class visibility_of_any_elements_located(object):
+    """ An expectation for checking that there is at least one element visible
+    on a web page.
+    locator is used to find the element
+    returns the list of WebElements once they are located
+    """
+    def __init__(self, locator):
+        self.locator = locator
+
+    def __call__(self, driver):
+        return [element for element in _find_elements(driver, self.locator) if _element_if_visible(element)]
+
+
+class visibility_of_all_elements_located(object):
+    """ An expectation for checking that all elements are present on the DOM of a
+    page and visible. Visibility means that the elements are not only displayed
+    but also has a height and width that is greater than 0.
+    locator - used to find the elements
+    returns the list of WebElements once they are located and visible
+    """
+    def __init__(self, locator):
+        self.locator = locator
+
+    def __call__(self, driver):
+        try:
+            elements = _find_elements(driver, self.locator)
+            for element in elements:
+                if _element_if_visible(element, visibility=False):
+                    return False
+            return elements
+        except StaleElementReferenceException:
+            return False
+
+
+class text_to_be_present_in_element(object):
+    """ An expectation for checking if the given text is present in the
+    specified element.
+    locator, text
+    """
+    def __init__(self, locator, text_):
+        self.locator = locator
+        self.text = text_
+
+    def __call__(self, driver):
+        try:
+            element_text = _find_element(driver, self.locator).text
+            return self.text in element_text
+        except StaleElementReferenceException:
+            return False
+
+
+class text_to_be_present_in_element_value(object):
+    """
+    An expectation for checking if the given text is present in the element's
+    locator, text
+    """
+    def __init__(self, locator, text_):
+        self.locator = locator
+        self.text = text_
+
+    def __call__(self, driver):
+        try:
+            element_text = _find_element(driver,
+                                         self.locator).get_attribute("value")
+            if element_text:
+                return self.text in element_text
+            else:
+                return False
+        except StaleElementReferenceException:
+                return False
+
+
+class frame_to_be_available_and_switch_to_it(object):
+    """ An expectation for checking whether the given frame is available to
+    switch to.  If the frame is available it switches the given driver to the
+    specified frame.
+    """
+    def __init__(self, locator):
+        self.frame_locator = locator
+
+    def __call__(self, driver):
+        try:
+            if isinstance(self.frame_locator, tuple):
+                driver.switch_to.frame(_find_element(driver,
+                                                     self.frame_locator))
+            else:
+                driver.switch_to.frame(self.frame_locator)
+            return True
+        except NoSuchFrameException:
+            return False
+
+
+class invisibility_of_element_located(object):
+    """ An Expectation for checking that an element is either invisible or not
+    present on the DOM.
+
+    locator used to find the element
+    """
+    def __init__(self, locator):
+        self.target = locator
+
+    def __call__(self, driver):
+        try:
+            target = self.target
+            if not isinstance(target, WebElement):
+                target = _find_element(driver, target)
+            return _element_if_visible(target, False)
+        except (NoSuchElementException, StaleElementReferenceException):
+            # In the case of NoSuchElement, returns true because the element is
+            # not present in DOM. The try block checks if the element is present
+            # but is invisible.
+            # In the case of StaleElementReference, returns true because stale
+            # element reference implies that element is no longer visible.
+            return True
+
+
+class invisibility_of_element(invisibility_of_element_located):
+    """ An Expectation for checking that an element is either invisible or not
+    present on the DOM.
+
+    element is either a locator (text) or an WebElement
+    """
+    def __init(self, element):
+        self.target = element
+
+
+class element_to_be_clickable(object):
+    """ An Expectation for checking an element is visible and enabled such that
+    you can click it."""
+    def __init__(self, locator):
+        self.locator = locator
+
+    def __call__(self, driver):
+        element = visibility_of_element_located(self.locator)(driver)
+        if element and element.is_enabled():
+            return element
+        else:
+            return False
+
+
+class staleness_of(object):
+    """ Wait until an element is no longer attached to the DOM.
+    element is the element to wait for.
+    returns False if the element is still attached to the DOM, true otherwise.
+    """
+    def __init__(self, element):
+        self.element = element
+
+    def __call__(self, ignored):
+        try:
+            # Calling any method forces a staleness check
+            self.element.is_enabled()
+            return False
+        except StaleElementReferenceException:
+            return True
+
+
+class element_to_be_selected(object):
+    """ An expectation for checking the selection is selected.
+    element is WebElement object
+    """
+    def __init__(self, element):
+        self.element = element
+
+    def __call__(self, ignored):
+        return self.element.is_selected()
+
+
+class element_located_to_be_selected(object):
+    """An expectation for the element to be located is selected.
+    locator is a tuple of (by, path)"""
+    def __init__(self, locator):
+        self.locator = locator
+
+    def __call__(self, driver):
+        return _find_element(driver, self.locator).is_selected()
+
+
+class element_selection_state_to_be(object):
+    """ An expectation for checking if the given element is selected.
+    element is WebElement object
+    is_selected is a Boolean."
+    """
+    def __init__(self, element, is_selected):
+        self.element = element
+        self.is_selected = is_selected
+
+    def __call__(self, ignored):
+        return self.element.is_selected() == self.is_selected
+
+
+class element_located_selection_state_to_be(object):
+    """ An expectation to locate an element and check if the selection state
+    specified is in that state.
+    locator is a tuple of (by, path)
+    is_selected is a boolean
+    """
+    def __init__(self, locator, is_selected):
+        self.locator = locator
+        self.is_selected = is_selected
+
+    def __call__(self, driver):
+        try:
+            element = _find_element(driver, self.locator)
+            return element.is_selected() == self.is_selected
+        except StaleElementReferenceException:
+            return False
+
+
+class number_of_windows_to_be(object):
+    """ An expectation for the number of windows to be a certain value."""
+
+    def __init__(self, num_windows):
+        self.num_windows = num_windows
+
+    def __call__(self, driver):
+        return len(driver.window_handles) == self.num_windows
+
+
+class new_window_is_opened(object):
+    """ An expectation that a new window will be opened and have the number of
+    windows handles increase"""
+
+    def __init__(self, current_handles):
+        self.current_handles = current_handles
+
+    def __call__(self, driver):
+        return len(driver.window_handles) > len(self.current_handles)
+
+
+class alert_is_present(object):
+    """ Expect an alert to be present."""
+    def __init__(self):
+        pass
+
+    def __call__(self, driver):
+        try:
+            alert = driver.switch_to.alert
+            return alert
+        except NoAlertPresentException:
+            return False
+
+
+def _find_element(driver, by):
+    """Looks up an element. Logs and re-raises ``WebDriverException``
+    if thrown."""
+    try:
+        return driver.find_element(*by)
+    except NoSuchElementException as e:
+        raise e
+    except WebDriverException as e:
+        raise e
+
+
+def _find_elements(driver, by):
+    try:
+        return driver.find_elements(*by)
+    except WebDriverException as e:
+        raise e
diff --git a/selenium/webdriver/support/select.py b/selenium/webdriver/support/select.py
new file mode 100644
index 00000000..71675cad
--- /dev/null
+++ b/selenium/webdriver/support/select.py
@@ -0,0 +1,241 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.common.by import By
+from selenium.common.exceptions import NoSuchElementException, UnexpectedTagNameException
+
+
+class Select(object):
+
+    def __init__(self, webelement):
+        """
+        Constructor. A check is made that the given element is, indeed, a SELECT tag. If it is not,
+        then an UnexpectedTagNameException is thrown.
+
+        :Args:
+         - webelement - element SELECT element to wrap
+
+        Example:
+            from selenium.webdriver.support.ui import Select \n
+            Select(driver.find_element_by_tag_name("select")).select_by_index(2)
+        """
+        if webelement.tag_name.lower() != "select":
+            raise UnexpectedTagNameException(
+                "Select only works on <select> elements, not on <%s>" %
+                webelement.tag_name)
+        self._el = webelement
+        multi = self._el.get_attribute("multiple")
+        self.is_multiple = multi and multi != "false"
+
+    @property
+    def options(self):
+        """Returns a list of all options belonging to this select tag"""
+        return self._el.find_elements(By.TAG_NAME, 'option')
+
+    @property
+    def all_selected_options(self):
+        """Returns a list of all selected options belonging to this select tag"""
+        ret = []
+        for opt in self.options:
+            if opt.is_selected():
+                ret.append(opt)
+        return ret
+
+    @property
+    def first_selected_option(self):
+        """The first selected option in this select tag (or the currently selected option in a
+        normal select)"""
+        for opt in self.options:
+            if opt.is_selected():
+                return opt
+        raise NoSuchElementException("No options are selected")
+
+    def select_by_value(self, value):
+        """Select all options that have a value matching the argument. That is, when given "foo" this
+           would select an option like:
+
+           <option value="foo">Bar</option>
+
+           :Args:
+            - value - The value to match against
+
+           throws NoSuchElementException If there is no option with specified value in SELECT
+           """
+        css = "option[value =%s]" % self._escapeString(value)
+        opts = self._el.find_elements(By.CSS_SELECTOR, css)
+        matched = False
+        for opt in opts:
+            self._setSelected(opt)
+            if not self.is_multiple:
+                return
+            matched = True
+        if not matched:
+            raise NoSuchElementException("Cannot locate option with value: %s" % value)
+
+    def select_by_index(self, index):
+        """Select the option at the given index. This is done by examing the "index" attribute of an
+           element, and not merely by counting.
+
+           :Args:
+            - index - The option at this index will be selected
+
+           throws NoSuchElementException If there is no option with specified index in SELECT
+           """
+        match = str(index)
+        for opt in self.options:
+            if opt.get_attribute("index") == match:
+                self._setSelected(opt)
+                return
+        raise NoSuchElementException("Could not locate element with index %d" % index)
+
+    def select_by_visible_text(self, text):
+        """Select all options that display text matching the argument. That is, when given "Bar" this
+           would select an option like:
+
+            <option value="foo">Bar</option>
+
+           :Args:
+            - text - The visible text to match against
+
+            throws NoSuchElementException If there is no option with specified text in SELECT
+           """
+        xpath = ".//option[normalize-space(.) = %s]" % self._escapeString(text)
+        opts = self._el.find_elements(By.XPATH, xpath)
+        matched = False
+        for opt in opts:
+            self._setSelected(opt)
+            if not self.is_multiple:
+                return
+            matched = True
+
+        if len(opts) == 0 and " " in text:
+            subStringWithoutSpace = self._get_longest_token(text)
+            if subStringWithoutSpace == "":
+                candidates = self.options
+            else:
+                xpath = ".//option[contains(.,%s)]" % self._escapeString(subStringWithoutSpace)
+                candidates = self._el.find_elements(By.XPATH, xpath)
+            for candidate in candidates:
+                if text == candidate.text:
+                    self._setSelected(candidate)
+                    if not self.is_multiple:
+                        return
+                    matched = True
+
+        if not matched:
+            raise NoSuchElementException("Could not locate element with visible text: %s" % text)
+
+    def deselect_all(self):
+        """Clear all selected entries. This is only valid when the SELECT supports multiple selections.
+           throws NotImplementedError If the SELECT does not support multiple selections
+        """
+        if not self.is_multiple:
+            raise NotImplementedError("You may only deselect all options of a multi-select")
+        for opt in self.options:
+            self._unsetSelected(opt)
+
+    def deselect_by_value(self, value):
+        """Deselect all options that have a value matching the argument. That is, when given "foo" this
+           would deselect an option like:
+
+            <option value="foo">Bar</option>
+
+           :Args:
+            - value - The value to match against
+
+            throws NoSuchElementException If there is no option with specified value in SELECT
+        """
+        if not self.is_multiple:
+            raise NotImplementedError("You may only deselect options of a multi-select")
+        matched = False
+        css = "option[value = %s]" % self._escapeString(value)
+        opts = self._el.find_elements(By.CSS_SELECTOR, css)
+        for opt in opts:
+            self._unsetSelected(opt)
+            matched = True
+        if not matched:
+            raise NoSuchElementException("Could not locate element with value: %s" % value)
+
+    def deselect_by_index(self, index):
+        """Deselect the option at the given index. This is done by examing the "index" attribute of an
+           element, and not merely by counting.
+
+           :Args:
+            - index - The option at this index will be deselected
+
+            throws NoSuchElementException If there is no option with specified index in SELECT
+        """
+        if not self.is_multiple:
+            raise NotImplementedError("You may only deselect options of a multi-select")
+        for opt in self.options:
+            if opt.get_attribute("index") == str(index):
+                self._unsetSelected(opt)
+                return
+        raise NoSuchElementException("Could not locate element with index %d" % index)
+
+    def deselect_by_visible_text(self, text):
+        """Deselect all options that display text matching the argument. That is, when given "Bar" this
+           would deselect an option like:
+
+           <option value="foo">Bar</option>
+
+           :Args:
+            - text - The visible text to match against
+        """
+        if not self.is_multiple:
+            raise NotImplementedError("You may only deselect options of a multi-select")
+        matched = False
+        xpath = ".//option[normalize-space(.) = %s]" % self._escapeString(text)
+        opts = self._el.find_elements(By.XPATH, xpath)
+        for opt in opts:
+            self._unsetSelected(opt)
+            matched = True
+        if not matched:
+            raise NoSuchElementException("Could not locate element with visible text: %s" % text)
+
+    def _setSelected(self, option):
+        if not option.is_selected():
+            option.click()
+
+    def _unsetSelected(self, option):
+        if option.is_selected():
+            option.click()
+
+    def _escapeString(self, value):
+        if '"' in value and "'" in value:
+            substrings = value.split("\"")
+            result = ["concat("]
+            for substring in substrings:
+                result.append("\"%s\"" % substring)
+                result.append(", '\"', ")
+            result = result[0:-1]
+            if value.endswith('"'):
+                result.append(", '\"'")
+            return "".join(result) + ")"
+
+        if '"' in value:
+            return "'%s'" % value
+
+        return "\"%s\"" % value
+
+    def _get_longest_token(self, value):
+        items = value.split(" ")
+        longest = ""
+        for item in items:
+            if len(item) > len(longest):
+                longest = item
+        return longest
diff --git a/selenium/webdriver/support/ui.py b/selenium/webdriver/support/ui.py
new file mode 100644
index 00000000..ee3b2b65
--- /dev/null
+++ b/selenium/webdriver/support/ui.py
@@ -0,0 +1,19 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from .select import Select  # noqa
+from .wait import WebDriverWait  # noqa
diff --git a/selenium/webdriver/support/wait.py b/selenium/webdriver/support/wait.py
new file mode 100644
index 00000000..eb50ec6e
--- /dev/null
+++ b/selenium/webdriver/support/wait.py
@@ -0,0 +1,109 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import time
+from selenium.common.exceptions import NoSuchElementException
+from selenium.common.exceptions import TimeoutException
+
+POLL_FREQUENCY = 0.5  # How long to sleep inbetween calls to the method
+IGNORED_EXCEPTIONS = (NoSuchElementException,)  # exceptions ignored during calls to the method
+
+
+class WebDriverWait(object):
+    def __init__(self, driver, timeout, poll_frequency=POLL_FREQUENCY, ignored_exceptions=None):
+        """Constructor, takes a WebDriver instance and timeout in seconds.
+
+           :Args:
+            - driver - Instance of WebDriver (Ie, Firefox, Chrome or Remote)
+            - timeout - Number of seconds before timing out
+            - poll_frequency - sleep interval between calls
+              By default, it is 0.5 second.
+            - ignored_exceptions - iterable structure of exception classes ignored during calls.
+              By default, it contains NoSuchElementException only.
+
+           Example:
+            from selenium.webdriver.support.ui import WebDriverWait \n
+            element = WebDriverWait(driver, 10).until(lambda x: x.find_element_by_id("someId")) \n
+            is_disappeared = WebDriverWait(driver, 30, 1, (ElementNotVisibleException)).\\ \n
+                        until_not(lambda x: x.find_element_by_id("someId").is_displayed())
+        """
+        self._driver = driver
+        self._timeout = timeout
+        self._poll = poll_frequency
+        # avoid the divide by zero
+        if self._poll == 0:
+            self._poll = POLL_FREQUENCY
+        exceptions = list(IGNORED_EXCEPTIONS)
+        if ignored_exceptions is not None:
+            try:
+                exceptions.extend(iter(ignored_exceptions))
+            except TypeError:  # ignored_exceptions is not iterable
+                exceptions.append(ignored_exceptions)
+        self._ignored_exceptions = tuple(exceptions)
+
+    def __repr__(self):
+        return '<{0.__module__}.{0.__name__} (session="{1}")>'.format(
+            type(self), self._driver.session_id)
+
+    def until(self, method, message=''):
+        """Calls the method provided with the driver as an argument until the \
+        return value does not evaluate to ``False``.
+
+        :param method: callable(WebDriver)
+        :param message: optional message for :exc:`TimeoutException`
+        :returns: the result of the last call to `method`
+        :raises: :exc:`selenium.common.exceptions.TimeoutException` if timeout occurs
+        """
+        screen = None
+        stacktrace = None
+
+        end_time = time.time() + self._timeout
+        while True:
+            try:
+                value = method(self._driver)
+                if value:
+                    return value
+            except self._ignored_exceptions as exc:
+                screen = getattr(exc, 'screen', None)
+                stacktrace = getattr(exc, 'stacktrace', None)
+            time.sleep(self._poll)
+            if time.time() > end_time:
+                break
+        raise TimeoutException(message, screen, stacktrace)
+
+    def until_not(self, method, message=''):
+        """Calls the method provided with the driver as an argument until the \
+        return value evaluates to ``False``.
+
+        :param method: callable(WebDriver)
+        :param message: optional message for :exc:`TimeoutException`
+        :returns: the result of the last call to `method`, or
+                  ``True`` if `method` has raised one of the ignored exceptions
+        :raises: :exc:`selenium.common.exceptions.TimeoutException` if timeout occurs
+        """
+        end_time = time.time() + self._timeout
+        while True:
+            try:
+                value = method(self._driver)
+                if not value:
+                    return value
+            except self._ignored_exceptions:
+                return True
+            time.sleep(self._poll)
+            if time.time() > end_time:
+                break
+        raise TimeoutException(message)
diff --git a/selenium/webdriver/webkitgtk/__init__.py b/selenium/webdriver/webkitgtk/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/selenium/webdriver/webkitgtk/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/selenium/webdriver/webkitgtk/options.py b/selenium/webdriver/webkitgtk/options.py
new file mode 100644
index 00000000..ad84c468
--- /dev/null
+++ b/selenium/webdriver/webkitgtk/options.py
@@ -0,0 +1,84 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+from selenium.webdriver.common.options import ArgOptions
+
+
+class Options(ArgOptions):
+    KEY = 'webkitgtk:browserOptions'
+
+    def __init__(self):
+        super(Options, self).__init__()
+        self._binary_location = ''
+        self._overlay_scrollbars_enabled = True
+
+    @property
+    def binary_location(self):
+        """
+        :Returns: The location of the browser binary otherwise an empty string
+        """
+        return self._binary_location
+
+    @binary_location.setter
+    def binary_location(self, value):
+        """
+        Allows you to set the browser binary to launch
+
+        :Args:
+         - value : path to the browser binary
+        """
+        self._binary_location = value
+
+    @property
+    def overlay_scrollbars_enabled(self):
+        """
+        :Returns: Whether overlay scrollbars should be enabled
+        """
+        return self._overlay_scrollbars_enabled
+
+    @overlay_scrollbars_enabled.setter
+    def overlay_scrollbars_enabled(self, value):
+        """
+        Allows you to enable or disable overlay scrollbars
+
+        :Args:
+         - value : True or False
+        """
+        self._overlay_scrollbars_enabled = value
+
+    def to_capabilities(self):
+        """
+        Creates a capabilities with all the options that have been set and
+        returns a dictionary with everything
+        """
+        caps = self._caps
+
+        browser_options = {}
+        if self.binary_location:
+            browser_options["binary"] = self.binary_location
+        if self.arguments:
+            browser_options["args"] = self.arguments
+        browser_options["useOverlayScrollbars"] = self.overlay_scrollbars_enabled
+
+        caps[Options.KEY] = browser_options
+
+        return caps
+
+    @property
+    def default_capabilities(self):
+        return DesiredCapabilities.WEBKITGTK.copy()
diff --git a/selenium/webdriver/webkitgtk/service.py b/selenium/webdriver/webkitgtk/service.py
new file mode 100644
index 00000000..26dad741
--- /dev/null
+++ b/selenium/webdriver/webkitgtk/service.py
@@ -0,0 +1,42 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.common import service
+
+
+class Service(service.Service):
+    """
+    Object that manages the starting and stopping of the WebKitGTKDriver
+    """
+
+    def __init__(self, executable_path, port=0, log_path=None):
+        """
+        Creates a new instance of the Service
+
+        :Args:
+         - executable_path : Path to the WebKitGTKDriver
+         - port : Port the service is running on
+         - log_path : Path for the WebKitGTKDriver service to log to
+        """
+        log_file = open(log_path, "wb") if log_path is not None and log_path != "" else None
+        service.Service.__init__(self, executable_path, port, log_file)
+
+    def command_line_args(self):
+        return ["-p", "%d" % self.port]
+
+    def send_remote_shutdown_command(self):
+        pass
diff --git a/selenium/webdriver/webkitgtk/webdriver.py b/selenium/webdriver/webkitgtk/webdriver.py
new file mode 100644
index 00000000..22a257cb
--- /dev/null
+++ b/selenium/webdriver/webkitgtk/webdriver.py
@@ -0,0 +1,78 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+try:
+    import http.client as http_client
+except ImportError:
+    import httplib as http_client
+
+from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
+from .service import Service
+from .options import Options
+
+
+class WebDriver(RemoteWebDriver):
+    """
+    Controls the WebKitGTKDriver and allows you to drive the browser.
+    """
+
+    def __init__(self, executable_path="WebKitWebDriver", port=0, options=None,
+                 desired_capabilities=None,
+                 service_log_path=None, keep_alive=False):
+        """
+        Creates a new instance of the WebKitGTK driver.
+
+        Starts the service and then creates new instance of WebKitGTK Driver.
+
+        :Args:
+         - executable_path : path to the executable. If the default is used it assumes the executable is in the $PATH.
+         - port : port you would like the service to run, if left as 0, a free port will be found.
+         - options : an instance of WebKitGTKOptions
+         - desired_capabilities : Dictionary object with desired capabilities
+         - service_log_path : Path to write service stdout and stderr output.
+         - keep_alive : Whether to configure RemoteConnection to use HTTP keep-alive.
+        """
+        if options is None:
+            if desired_capabilities is None:
+                desired_capabilities = Options().to_capabilities()
+        else:
+            capabilities = options.to_capabilities()
+            if desired_capabilities is not None:
+                capabilities.update(desired_capabilities)
+            desired_capabilities = capabilities
+
+        self.service = Service(executable_path, port=port, log_path=service_log_path)
+        self.service.start()
+
+        RemoteWebDriver.__init__(
+            self,
+            command_executor=self.service.service_url,
+            desired_capabilities=desired_capabilities,
+            keep_alive=keep_alive)
+        self._is_remote = False
+
+    def quit(self):
+        """
+        Closes the browser and shuts down the WebKitGTKDriver executable
+        that is started when starting the WebKitGTKDriver
+        """
+        try:
+            RemoteWebDriver.quit(self)
+        except http_client.BadStatusLine:
+            pass
+        finally:
+            self.service.stop()
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..1385258a
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,11 @@
+[bdist_wheel]
+universal = 1
+
+[flake8]
+exclude = .tox,docs/source/conf.py
+ignore = E501
+
+[tool:pytest]
+addopts = -r=a
+python_files = test_*.py *_tests.py
+testpaths = test
diff --git a/setup.py b/setup.py
new file mode 100755
index 00000000..12a3850e
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,73 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from distutils.command.install import INSTALL_SCHEMES
+from os.path import dirname, join, abspath
+from setuptools import setup
+from setuptools.command.install import install
+
+
+for scheme in INSTALL_SCHEMES.values():
+    scheme['data'] = scheme['purelib']
+
+setup_args = {
+    'cmdclass': {'install': install},
+    'name': 'selenium',
+    'version': "4.0.0a1",
+    'license': 'Apache 2.0',
+    'description': 'Python bindings for Selenium',
+    'long_description': open(join(abspath(dirname(__file__)), "README.rst")).read(),
+    'url': 'https://github.com/SeleniumHQ/selenium/',
+    'classifiers': ['Development Status :: 5 - Production/Stable',
+                    'Intended Audience :: Developers',
+                    'License :: OSI Approved :: Apache Software License',
+                    'Operating System :: POSIX',
+                    'Operating System :: Microsoft :: Windows',
+                    'Operating System :: MacOS :: MacOS X',
+                    'Topic :: Software Development :: Testing',
+                    'Topic :: Software Development :: Libraries',
+                    'Programming Language :: Python',
+                    'Programming Language :: Python :: 2.7',
+                    'Programming Language :: Python :: 3.4',
+                    'Programming Language :: Python :: 3.5',
+                    'Programming Language :: Python :: 3.6'],
+    'package_dir': {
+        'selenium': 'selenium',
+        'selenium.common': 'selenium/common',
+        'selenium.webdriver': 'selenium/webdriver',
+    },
+    'packages': ['selenium',
+                 'selenium.common',
+                 'selenium.webdriver',
+                 'selenium.webdriver.android',
+                 'selenium.webdriver.chrome',
+                 'selenium.webdriver.common',
+                 'selenium.webdriver.common.html5',
+                 'selenium.webdriver.support',
+                 'selenium.webdriver.firefox',
+                 'selenium.webdriver.ie',
+                 'selenium.webdriver.edge',
+                 'selenium.webdriver.opera',
+                 'selenium.webdriver.phantomjs',
+                 'selenium.webdriver.remote',
+                 'selenium.webdriver.support', ],
+    'include_package_data': True,
+    'install_requires': ['urllib3'],
+    'zip_safe': False
+}
+
+setup(**setup_args)
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/run_pytest.py b/test/run_pytest.py
new file mode 100644
index 00000000..f6557514
--- /dev/null
+++ b/test/run_pytest.py
@@ -0,0 +1,2 @@
+import pytest
+raise SystemExit(pytest.main())
\ No newline at end of file
diff --git a/test/selenium/__init__.py b/test/selenium/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/selenium/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/selenium/webdriver/__init__.py b/test/selenium/webdriver/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/selenium/webdriver/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/selenium/webdriver/chrome/__init__.py b/test/selenium/webdriver/chrome/__init__.py
new file mode 100755
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/selenium/webdriver/chrome/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/selenium/webdriver/chrome/chrome_execute_cdp_cmd_tests.py b/test/selenium/webdriver/chrome/chrome_execute_cdp_cmd_tests.py
new file mode 100644
index 00000000..e6bd5f26
--- /dev/null
+++ b/test/selenium/webdriver/chrome/chrome_execute_cdp_cmd_tests.py
@@ -0,0 +1,25 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver import Chrome
+
+
+def test_execute_cdp_cmd():
+    driver = Chrome()
+    version_info = driver.execute_cdp_cmd('Browser.getVersion', {})
+    assert isinstance(version_info, dict)
+    assert 'userAgent' in version_info
diff --git a/test/selenium/webdriver/chrome/chrome_launcher_tests.py b/test/selenium/webdriver/chrome/chrome_launcher_tests.py
new file mode 100755
index 00000000..b9470d1f
--- /dev/null
+++ b/test/selenium/webdriver/chrome/chrome_launcher_tests.py
@@ -0,0 +1,41 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver import Chrome
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+
+
+def test_launch_and_close_browser():
+    driver = Chrome()
+    driver.quit()
+
+
+def test_we_can_launch_multiple_chrome_instances():
+    driver1 = Chrome()
+    driver2 = Chrome()
+    driver3 = Chrome()
+    driver1.quit()
+    driver2.quit()
+    driver3.quit()
+
+
+def test_launch_chrome_do_not_affect_default_capabilities():
+    expected = DesiredCapabilities.CHROME.copy()
+    driver = Chrome()
+    actual = DesiredCapabilities.CHROME.copy()
+    driver.quit()
+    assert actual == expected
diff --git a/test/selenium/webdriver/chrome/chrome_network_emulation_tests.py b/test/selenium/webdriver/chrome/chrome_network_emulation_tests.py
new file mode 100755
index 00000000..342bc76b
--- /dev/null
+++ b/test/selenium/webdriver/chrome/chrome_network_emulation_tests.py
@@ -0,0 +1,31 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver import Chrome
+
+
+def test_network_conditions_emulation():
+    driver = Chrome()
+    driver.set_network_conditions(
+        offline=False,
+        latency=56,  # additional latency (ms)
+        throughput=789)
+    conditions = driver.get_network_conditions()
+    assert conditions['offline'] is False
+    assert conditions['latency'] == 56
+    assert conditions['download_throughput'] == 789
+    assert conditions['upload_throughput'] == 789
diff --git a/test/selenium/webdriver/common/__init__.py b/test/selenium/webdriver/common/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/selenium/webdriver/common/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/selenium/webdriver/common/alerts_tests.py b/test/selenium/webdriver/common/alerts_tests.py
new file mode 100644
index 00000000..7ed1a12b
--- /dev/null
+++ b/test/selenium/webdriver/common/alerts_tests.py
@@ -0,0 +1,306 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import sys
+
+import pytest
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.support.wait import WebDriverWait
+from selenium.common.exceptions import (
+    InvalidElementStateException,
+    NoAlertPresentException,
+    UnexpectedAlertPresentException)
+
+
+@pytest.fixture(autouse=True)
+def close_alert(driver):
+    yield
+    try:
+        driver.switch_to.alert.dismiss()
+    except Exception:
+        pass
+
+
+def testShouldBeAbleToOverrideTheWindowAlertMethod(driver, pages):
+    pages.load("alerts.html")
+    driver.execute_script(
+        "window.alert = function(msg) { document.getElementById('text').innerHTML = msg; }")
+    driver.find_element(by=By.ID, value="alert").click()
+    try:
+        assert driver.find_element_by_id('text').text == "cheese"
+    except Exception as e:
+        # if we're here, likely the alert is displayed
+        # not dismissing it will affect other tests
+        try:
+            _waitForAlert(driver).dismiss()
+        except Exception:
+            pass
+        raise e
+
+
+def testShouldAllowUsersToAcceptAnAlertManually(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(by=By.ID, value="alert").click()
+    alert = _waitForAlert(driver)
+    alert.accept()
+    #  If we can perform any action, we're good to go
+    assert "Testing Alerts" == driver.title
+
+
+def testShouldAllowUsersToAcceptAnAlertWithNoTextManually(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(By.ID, "empty-alert").click()
+    alert = _waitForAlert(driver)
+    alert.accept()
+
+    #  If we can perform any action, we're good to go
+    assert "Testing Alerts" == driver.title
+
+
+def testShouldGetTextOfAlertOpenedInSetTimeout(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element_by_id("slow-alert").click()
+
+    # DO NOT WAIT OR SLEEP HERE
+    # This is a regression test for a bug where only the first switchTo call would throw,
+    # and only if it happens before the alert actually loads.
+
+    alert = _waitForAlert(driver)
+    try:
+        assert "Slow" == alert.text
+    finally:
+        alert.accept()
+
+
+def testShouldAllowUsersToDismissAnAlertManually(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(by=By.ID, value="alert").click()
+    alert = _waitForAlert(driver)
+    alert.dismiss()
+    #  If we can perform any action, we're good to go
+    assert "Testing Alerts" == driver.title
+
+
+def testShouldAllowAUserToAcceptAPrompt(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(by=By.ID, value="prompt").click()
+    alert = _waitForAlert(driver)
+    alert.accept()
+
+    #  If we can perform any action, we're good to go
+    assert "Testing Alerts" == driver.title
+
+
+def testShouldAllowAUserToDismissAPrompt(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(by=By.ID, value="prompt").click()
+    alert = _waitForAlert(driver)
+    alert.dismiss()
+
+    #  If we can perform any action, we're good to go
+    assert "Testing Alerts" == driver.title
+
+
+def testShouldAllowAUserToSetTheValueOfAPrompt(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(by=By.ID, value="prompt").click()
+    alert = _waitForAlert(driver)
+    alert.send_keys("cheese")
+    alert.accept()
+
+    result = driver.find_element(by=By.ID, value="text").text
+    assert "cheese" == result
+
+
+def testSettingTheValueOfAnAlertThrows(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(By.ID, "alert").click()
+
+    alert = _waitForAlert(driver)
+    with pytest.raises(InvalidElementStateException):
+        alert.send_keys("cheese")
+    alert.accept()
+
+
+@pytest.mark.xfail_chrome(
+    condition=sys.platform == 'darwin',
+    reason='https://bugs.chromium.org/p/chromedriver/issues/detail?id=26',
+    run=False)
+def testAlertShouldNotAllowAdditionalCommandsIfDimissed(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(By.ID, "alert").click()
+
+    alert = _waitForAlert(driver)
+    alert.dismiss()
+
+    with pytest.raises(NoAlertPresentException):
+        alert.text
+
+
+@pytest.mark.xfail_marionette(reason='Fails on travis')
+@pytest.mark.xfail_remote(reason='Fails on travis')
+def testShouldAllowUsersToAcceptAnAlertInAFrame(driver, pages):
+    pages.load("alerts.html")
+    driver.switch_to.frame(driver.find_element(By.NAME, "iframeWithAlert"))
+    driver.find_element_by_id("alertInFrame").click()
+
+    alert = _waitForAlert(driver)
+    alert.accept()
+
+    assert "Testing Alerts" == driver.title
+
+
+@pytest.mark.xfail_marionette(reason='Fails on travis')
+@pytest.mark.xfail_remote(reason='Fails on travis')
+def testShouldAllowUsersToAcceptAnAlertInANestedFrame(driver, pages):
+    pages.load("alerts.html")
+    driver.switch_to.frame(driver.find_element(By.NAME, "iframeWithIframe"))
+    driver.switch_to.frame(driver.find_element(By.NAME, "iframeWithAlert"))
+
+    driver.find_element_by_id("alertInFrame").click()
+
+    alert = _waitForAlert(driver)
+    alert.accept()
+
+    assert "Testing Alerts" == driver.title
+
+
+def testShouldThrowAnExceptionIfAnAlertHasNotBeenDealtWithAndDismissTheAlert():
+    pass
+    # //TODO(David) Complete this test
+
+
+def testPromptShouldUseDefaultValueIfNoKeysSent(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(By.ID, "prompt-with-default").click()
+
+    alert = _waitForAlert(driver)
+    alert.accept()
+
+    txt = driver.find_element(By.ID, "text").text
+    assert "This is a default value" == txt
+
+
+def testPromptShouldHaveNullValueIfDismissed(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(By.ID, "prompt-with-default").click()
+    alert = _waitForAlert(driver)
+    alert.dismiss()
+
+    assert "null" == driver.find_element(By.ID, "text").text
+
+
+@pytest.mark.xfail_marionette(reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1477977')
+@pytest.mark.xfail_remote(reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1477977')
+def testHandlesTwoAlertsFromOneInteraction(driver, pages):
+    pages.load("alerts.html")
+
+    driver.find_element(By.ID, "double-prompt").click()
+
+    alert1 = _waitForAlert(driver)
+    alert1.send_keys("brie")
+    alert1.accept()
+
+    alert2 = _waitForAlert(driver)
+    alert2.send_keys("cheddar")
+    alert2.accept()
+
+    assert driver.find_element(By.ID, "text1").text == "brie"
+    assert driver.find_element(By.ID, "text2").text == "cheddar"
+
+
+def testShouldHandleAlertOnPageLoad(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(By.ID, "open-page-with-onload-alert").click()
+    alert = _waitForAlert(driver)
+    value = alert.text
+    alert.accept()
+    assert "onload" == value
+
+
+def testShouldHandleAlertOnPageLoadUsingGet(driver, pages):
+    pages.load("pageWithOnLoad.html")
+    alert = _waitForAlert(driver)
+    value = alert.text
+    alert.accept()
+
+    assert "onload" == value
+    WebDriverWait(driver, 3).until(EC.text_to_be_present_in_element((By.TAG_NAME, "p"), "Page with onload event handler"))
+
+
+@pytest.mark.xfail_firefox(reason='Non W3C conformant')
+@pytest.mark.xfail_chrome(reason='Non W3C conformant')
+def testShouldHandleAlertOnPageBeforeUnload(driver, pages):
+    pages.load("pageWithOnBeforeUnloadMessage.html")
+
+    element = driver.find_element(By.ID, "navigate")
+    element.click()
+    WebDriverWait(driver, 3).until(EC.title_is("Testing Alerts"))
+
+
+def testShouldAllowTheUserToGetTheTextOfAnAlert(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(by=By.ID, value="alert").click()
+    alert = _waitForAlert(driver)
+    value = alert.text
+    alert.accept()
+    assert "cheese" == value
+
+
+def testShouldAllowTheUserToGetTheTextOfAPrompt(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(By.ID, "prompt").click()
+
+    alert = _waitForAlert(driver)
+    value = alert.text
+    alert.accept()
+
+    assert "Enter something" == value
+
+
+def testAlertShouldNotAllowAdditionalCommandsIfDismissed(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(By.ID, "alert").click()
+
+    alert = _waitForAlert(driver)
+    alert.accept()
+
+    with pytest.raises(NoAlertPresentException):
+        alert.text
+
+
+@pytest.mark.xfail_chrome(
+    reason='https://bugs.chromium.org/p/chromedriver/issues/detail?id=1537')
+@pytest.mark.xfail_marionette(
+    reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1279211')
+@pytest.mark.xfail_remote(
+    reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1279211')
+def testUnexpectedAlertPresentExceptionContainsAlertText(driver, pages):
+    pages.load("alerts.html")
+    driver.find_element(by=By.ID, value="alert").click()
+    alert = _waitForAlert(driver)
+    value = alert.text
+    with pytest.raises(UnexpectedAlertPresentException) as e:
+        pages.load("simpleTest.html")
+    assert value == e.value.alert_text
+    assert "Alert Text: {}".format(value) in str(e)
+
+
+def _waitForAlert(driver):
+    return WebDriverWait(driver, 3).until(EC.alert_is_present())
diff --git a/test/selenium/webdriver/common/api_example_tests.py b/test/selenium/webdriver/common/api_example_tests.py
new file mode 100644
index 00000000..679c9be2
--- /dev/null
+++ b/test/selenium/webdriver/common/api_example_tests.py
@@ -0,0 +1,301 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Copyright 2008-2009 WebDriver committers
+# Copyright 2008-2009 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+from selenium.common.exceptions import (
+    NoSuchElementException,
+    NoSuchWindowException,
+    TimeoutException,
+    WebDriverException)
+from selenium.webdriver.support.wait import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+
+
+def testGetTitle(driver, pages):
+    pages.load("simpleTest.html")
+    title = driver.title
+    assert "Hello WebDriver" == title
+
+
+def testGetCurrentUrl(driver, pages, webserver):
+    pages.load("simpleTest.html")
+    url = driver.current_url
+    assert webserver.where_is('simpleTest.html') == url
+
+
+def testFindElementsByXPath(driver, pages):
+    pages.load("simpleTest.html")
+    elem = driver.find_element_by_xpath("//h1")
+    assert "Heading" == elem.text
+
+
+def testFindElementByXpathThrowNoSuchElementException(driver, pages):
+    pages.load("simpleTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element_by_xpath("//h4")
+
+
+def testFindElementsByXpath(driver, pages):
+    pages.load("nestedElements.html")
+    elems = driver.find_elements_by_xpath("//option")
+    assert 48 == len(elems)
+    assert "One" == elems[0].get_attribute("value")
+
+
+def testFindElementsByName(driver, pages):
+    pages.load("xhtmlTest.html")
+    elem = driver.find_element_by_name("windowOne")
+    assert "Open new window" == elem.text
+
+
+def testFindElementsByNameInElementContext(driver, pages):
+    pages.load("nestedElements.html")
+    elem = driver.find_element_by_name("form2")
+    sub_elem = elem.find_element_by_name("selectomatic")
+    assert "2" == sub_elem.get_attribute("id")
+
+
+def testFindElementsByLinkTextInElementContext(driver, pages):
+    pages.load("nestedElements.html")
+    elem = driver.find_element_by_name("div1")
+    sub_elem = elem.find_element_by_link_text("hello world")
+    assert "link1" == sub_elem.get_attribute("name")
+
+
+def testFindElementByIdInElementContext(driver, pages):
+    pages.load("nestedElements.html")
+    elem = driver.find_element_by_name("form2")
+    sub_elem = elem.find_element_by_id("2")
+    assert "selectomatic" == sub_elem.get_attribute("name")
+
+
+def testFindElementByXpathInElementContext(driver, pages):
+    pages.load("nestedElements.html")
+    elem = driver.find_element_by_name("form2")
+    sub_elem = elem.find_element_by_xpath("select")
+    assert "2" == sub_elem.get_attribute("id")
+
+
+def testFindElementByXpathInElementContextNotFound(driver, pages):
+    pages.load("nestedElements.html")
+    elem = driver.find_element_by_name("form2")
+    with pytest.raises(NoSuchElementException):
+        elem.find_element_by_xpath("div")
+
+
+def testShouldBeAbleToEnterDataIntoFormFields(driver, pages):
+    pages.load("xhtmlTest.html")
+    elem = driver.find_element_by_xpath("//form[@name='someForm']/input[@id='username']")
+    elem.clear()
+    elem.send_keys("some text")
+    elem = driver.find_element_by_xpath("//form[@name='someForm']/input[@id='username']")
+    assert "some text" == elem.get_attribute("value")
+
+
+def testFindElementByTagName(driver, pages):
+    pages.load("simpleTest.html")
+    elems = driver.find_elements_by_tag_name("div")
+    num_by_xpath = len(driver.find_elements_by_xpath("//div"))
+    assert num_by_xpath == len(elems)
+    elems = driver.find_elements_by_tag_name("iframe")
+    assert 0 == len(elems)
+
+
+def testFindElementByTagNameWithinElement(driver, pages):
+    pages.load("simpleTest.html")
+    div = driver.find_element_by_id("multiline")
+    elems = div.find_elements_by_tag_name("p")
+    assert len(elems) == 1
+
+
+@pytest.mark.xfail_marionette(
+    reason="W3C implementations can't switch to a window by name",
+    raises=TimeoutException,
+    run=False)
+def testSwitchToWindow(driver, pages):
+    title_1 = "XHTML Test Page"
+    title_2 = "We Arrive Here"
+    switch_to_window_timeout = 5
+    wait = WebDriverWait(driver, switch_to_window_timeout, ignored_exceptions=[NoSuchWindowException])
+    pages.load("xhtmlTest.html")
+    driver.find_element_by_link_text("Open new window").click()
+    assert title_1 == driver.title
+    wait.until(lambda dr: dr.switch_to.window("result") is None)
+    assert title_2 == driver.title
+
+
+def testSwitchFrameByName(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame(driver.find_element_by_name("third"))
+    checkbox = driver.find_element_by_id("checky")
+    checkbox.click()
+    checkbox.submit()
+
+
+def testIsEnabled(driver, pages):
+    pages.load("formPage.html")
+    elem = driver.find_element_by_xpath("//input[@id='working']")
+    assert elem.is_enabled()
+    elem = driver.find_element_by_xpath("//input[@id='notWorking']")
+    assert not elem.is_enabled()
+
+
+def testIsSelectedAndToggle(driver, pages):
+    pages.load("formPage.html")
+    elem = driver.find_element_by_id("multi")
+    option_elems = elem.find_elements_by_xpath("option")
+    assert option_elems[0].is_selected()
+    option_elems[0].click()
+    assert not option_elems[0].is_selected()
+    option_elems[0].click()
+    assert option_elems[0].is_selected()
+    assert option_elems[2].is_selected()
+
+
+def testNavigate(driver, pages):
+    pages.load("formPage.html")
+    driver.find_element_by_id("imageButton").submit()
+    WebDriverWait(driver, 3).until(EC.title_is("We Arrive Here"))
+    driver.back()
+    assert "We Leave From Here" == driver.title
+    driver.forward()
+    assert "We Arrive Here" == driver.title
+
+
+def testGetAttribute(driver, pages):
+    url = pages.url('xhtmlTest.html')
+    driver.get(url)
+    elem = driver.find_element_by_id("id1")
+    attr = elem.get_attribute("href")
+    assert '{0}#'.format(url) == attr
+
+
+def testGetImplicitAttribute(driver, pages):
+    pages.load("nestedElements.html")
+    elems = driver.find_elements_by_xpath("//option")
+    assert len(elems) >= 3
+    for i, elem in enumerate(elems[:3]):
+        assert i == int(elem.get_attribute("index"))
+
+
+def testExecuteSimpleScript(driver, pages):
+    pages.load("xhtmlTest.html")
+    title = driver.execute_script("return document.title;")
+    assert "XHTML Test Page" == title
+
+
+def testExecuteScriptAndReturnElement(driver, pages):
+    pages.load("xhtmlTest.html")
+    elem = driver.execute_script("return document.getElementById('id1');")
+    assert "WebElement" in str(type(elem))
+
+
+def testExecuteScriptWithArgs(driver, pages):
+    pages.load("xhtmlTest.html")
+    result = driver.execute_script("return arguments[0] == 'fish' ? 'fish' : 'not fish';", "fish")
+    assert "fish" == result
+
+
+def testExecuteScriptWithMultipleArgs(driver, pages):
+    pages.load("xhtmlTest.html")
+    result = driver.execute_script(
+        "return arguments[0] + arguments[1]", 1, 2)
+    assert 3 == result
+
+
+def testExecuteScriptWithElementArgs(driver, pages):
+    pages.load("javascriptPage.html")
+    button = driver.find_element_by_id("plainButton")
+    result = driver.execute_script("arguments[0]['flibble'] = arguments[0].getAttribute('id'); return arguments[0]['flibble'];", button)
+    assert "plainButton" == result
+
+
+def testFindElementsByPartialLinkText(driver, pages):
+    pages.load("xhtmlTest.html")
+    elem = driver.find_element_by_partial_link_text("new window")
+    elem.click()
+
+
+def testIsElementDisplayed(driver, pages):
+    pages.load("javascriptPage.html")
+    visible = driver.find_element_by_id("displayed").is_displayed()
+    not_visible = driver.find_element_by_id("hidden").is_displayed()
+    assert visible
+    assert not not_visible
+
+
+def testMoveWindowPosition(driver, pages):
+    pages.load("blank.html")
+    loc = driver.get_window_position()
+    # note can't test 0,0 since some OS's dont allow that location
+    # because of system toolbars
+    new_x = 50
+    new_y = 50
+    if loc['x'] == new_x:
+        new_x += 10
+    if loc['y'] == new_y:
+        new_y += 10
+    driver.set_window_position(new_x, new_y)
+    loc = driver.get_window_position()
+    assert loc['x'] == new_x
+    assert loc['y'] == new_y
+
+
+def testChangeWindowSize(driver, pages):
+    pages.load("blank.html")
+    size = driver.get_window_size()
+    newSize = [600, 600]
+    if size['width'] == 600:
+        newSize[0] = 500
+    if size['height'] == 600:
+        newSize[1] = 500
+    driver.set_window_size(newSize[0], newSize[1])
+    size = driver.get_window_size()
+    assert size['width'] == newSize[0]
+    assert size['height'] == newSize[1]
+
+
+@pytest.mark.xfail_chrome(raises=WebDriverException)
+@pytest.mark.xfail_marionette(raises=WebDriverException)
+def testGetLogTypes(driver, pages):
+    pages.load("blank.html")
+    assert isinstance(driver.log_types, list)
+
+
+@pytest.mark.xfail_chrome(raises=WebDriverException)
+@pytest.mark.xfail_marionette(raises=WebDriverException)
+def testGetLog(driver, pages):
+    pages.load("blank.html")
+    for log_type in driver.log_types:
+        log = driver.get_log(log_type)
+        assert isinstance(log, list)
diff --git a/test/selenium/webdriver/common/appcache_tests.py b/test/selenium/webdriver/common/appcache_tests.py
new file mode 100644
index 00000000..946a33b6
--- /dev/null
+++ b/test/selenium/webdriver/common/appcache_tests.py
@@ -0,0 +1,36 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.common.html5.application_cache import ApplicationCache
+
+
+@pytest.mark.xfail_chrome
+@pytest.mark.xfail_marionette(raises=WebDriverException)
+@pytest.mark.xfail_remote
+def testWeCanGetTheStatusOfTheAppCache(driver, pages):
+    pages.load('html5Page')
+    driver.implicitly_wait(2)
+    app_cache = driver.application_cache
+
+    status = app_cache.status
+    while status == ApplicationCache.DOWNLOADING:
+        status = app_cache.status
+
+    assert ApplicationCache.UNCACHED == app_cache.status
diff --git a/test/selenium/webdriver/common/children_finding_tests.py b/test/selenium/webdriver/common/children_finding_tests.py
new file mode 100755
index 00000000..47a244bf
--- /dev/null
+++ b/test/selenium/webdriver/common/children_finding_tests.py
@@ -0,0 +1,183 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import (
+    WebDriverException,
+    NoSuchElementException)
+
+
+def test_should_find_element_by_xpath(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element_by_name("form2")
+    child = element.find_element_by_xpath("select")
+    assert child.get_attribute("id") == "2"
+
+
+def test_should_not_find_element_by_xpath(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element_by_name("form2")
+    with pytest.raises(NoSuchElementException):
+        element.find_element_by_xpath("select/x")
+
+
+def test_finding_dot_slash_elements_on_element_by_xpath_should_find_not_top_level_elements(driver, pages):
+    pages.load("simpleTest.html")
+    parent = driver.find_element_by_id("multiline")
+    children = parent.find_elements_by_xpath("./p")
+    assert 1 == len(children)
+    assert "A div containing" == children[0].text
+
+
+def test_should_find_elements_by_xpath(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element_by_name("form2")
+    children = element.find_elements_by_xpath("select/option")
+    assert len(children) == 8
+    assert children[0].text == "One"
+    assert children[1].text == "Two"
+
+
+def test_should_not_find_elements_by_xpath(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element_by_name("form2")
+    children = element.find_elements_by_xpath("select/x")
+    assert len(children) == 0
+
+
+def test_finding_elements_on_element_by_xpath_should_find_top_level_elements(driver, pages):
+    pages.load("simpleTest.html")
+    parent = driver.find_element_by_id("multiline")
+    all_para_elements = driver.find_elements_by_xpath("//p")
+    children = parent.find_elements_by_xpath("//p")
+    assert len(all_para_elements) == len(children)
+
+
+def test_should_find_element_by_name(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element_by_name("form2")
+    child = element.find_element_by_name("selectomatic")
+    assert child.get_attribute("id") == "2"
+
+
+def test_should_find_elements_by_name(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element_by_name("form2")
+    children = element.find_elements_by_name("selectomatic")
+    assert len(children) == 2
+
+
+def test_should_find_element_by_id(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element_by_name("form2")
+    child = element.find_element_by_id("2")
+    assert child.get_attribute("name") == "selectomatic"
+
+
+def test_should_find_elements_by_id(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element_by_name("form2")
+    child = element.find_elements_by_id("2")
+    assert len(child) == 2
+
+
+def test_should_find_element_by_id_when_multiple_matches_exist(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element_by_id("test_id_div")
+    child = element.find_element_by_id("test_id")
+    assert child.text == "inside"
+
+
+def test_should_find_element_by_id_when_no_match_in_context(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element_by_id("test_id_div")
+    with pytest.raises(NoSuchElementException):
+        element.find_element_by_id("test_id_out")
+
+
+def test_should_find_element_by_link_text(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element_by_name("div1")
+    child = element.find_element_by_link_text("hello world")
+    assert child.get_attribute("name") == "link1"
+
+
+def test_should_find_elements_by_link_text(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element_by_name("div1")
+    children = element.find_elements_by_link_text("hello world")
+    assert len(children) == 2
+    assert "link1" == children[0].get_attribute("name")
+    assert "link2" == children[1].get_attribute("name")
+
+
+def test_should_find_element_by_class_name(driver, pages):
+    pages.load("nestedElements.html")
+    parent = driver.find_element_by_name("classes")
+    element = parent.find_element_by_class_name("one")
+    assert "Find me" == element.text
+
+
+def test_should_find_elements_by_class_name(driver, pages):
+    pages.load("nestedElements.html")
+    parent = driver.find_element_by_name("classes")
+    elements = parent.find_elements_by_class_name("one")
+    assert 2 == len(elements)
+
+
+def test_should_find_element_by_tag_name(driver, pages):
+    pages.load("nestedElements.html")
+    parent = driver.find_element_by_name("div1")
+    element = parent.find_element_by_tag_name("a")
+    assert "link1" == element.get_attribute("name")
+
+
+def test_should_find_elements_by_tag_name(driver, pages):
+    pages.load("nestedElements.html")
+    parent = driver.find_element_by_name("div1")
+    elements = parent.find_elements_by_tag_name("a")
+    assert 2 == len(elements)
+
+
+def test_should_be_able_to_find_an_element_by_css_selector(driver, pages):
+    pages.load("nestedElements.html")
+    parent = driver.find_element_by_name("form2")
+    element = parent.find_element_by_css_selector('*[name="selectomatic"]')
+    assert "2" == element.get_attribute("id")
+
+
+def test_should_be_able_to_find_multiple_elements_by_css_selector(driver, pages):
+    pages.load("nestedElements.html")
+    parent = driver.find_element_by_name("form2")
+    elements = parent.find_elements_by_css_selector(
+        '*[name="selectomatic"]')
+    assert 2 == len(elements)
+
+
+def test_should_throw_an_error_if_user_passes_in_invalid_by(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element_by_name("form2")
+    with pytest.raises(WebDriverException):
+        element.find_element("foo", "bar")
+
+
+def test_should_throw_an_error_if_user_passes_in_invalid_by_when_find_elements(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element_by_name("form2")
+    with pytest.raises(WebDriverException):
+        element.find_elements("foo", "bar")
diff --git a/test/selenium/webdriver/common/clear_tests.py b/test/selenium/webdriver/common/clear_tests.py
new file mode 100644
index 00000000..b650f17b
--- /dev/null
+++ b/test/selenium/webdriver/common/clear_tests.py
@@ -0,0 +1,70 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import InvalidElementStateException
+
+
+def testWritableTextInputShouldClear(driver, pages):
+    pages.load("readOnlyPage.html")
+    element = driver.find_element_by_id("writableTextInput")
+    element.clear()
+    assert "" == element.get_attribute("value")
+
+
+def testTextInputShouldNotClearWhenDisabled(driver, pages):
+    pages.load("readOnlyPage.html")
+    element = driver.find_element_by_id("textInputnotenabled")
+    assert not element.is_enabled()
+    with pytest.raises(InvalidElementStateException):
+        element.clear()
+
+
+def testTextInputShouldNotClearWhenReadOnly(driver, pages):
+    pages.load("readOnlyPage.html")
+    element = driver.find_element_by_id("readOnlyTextInput")
+    with pytest.raises(InvalidElementStateException):
+        element.clear()
+
+
+def testWritableTextAreaShouldClear(driver, pages):
+    pages.load("readOnlyPage.html")
+    element = driver.find_element_by_id("writableTextArea")
+    element.clear()
+    assert "" == element.get_attribute("value")
+
+
+def testTextAreaShouldNotClearWhenDisabled(driver, pages):
+    pages.load("readOnlyPage.html")
+    element = driver.find_element_by_id("textAreaNotenabled")
+    with pytest.raises(InvalidElementStateException):
+        element.clear()
+
+
+def testTextAreaShouldNotClearWhenReadOnly(driver, pages):
+    pages.load("readOnlyPage.html")
+    element = driver.find_element_by_id("textAreaReadOnly")
+    with pytest.raises(InvalidElementStateException):
+        element.clear()
+
+
+def testContentEditableAreaShouldClear(driver, pages):
+    pages.load("readOnlyPage.html")
+    element = driver.find_element_by_id("content-editable")
+    element.clear()
+    assert "" == element.text
diff --git a/test/selenium/webdriver/common/click_scrolling_tests.py b/test/selenium/webdriver/common/click_scrolling_tests.py
new file mode 100644
index 00000000..bc8adb8f
--- /dev/null
+++ b/test/selenium/webdriver/common/click_scrolling_tests.py
@@ -0,0 +1,186 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import MoveTargetOutOfBoundsException
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def testClickingOnAnchorScrollsPage(driver, pages):
+    scrollScript = """var pageY;
+    if (typeof(window.pageYOffset) == 'number') {
+      pageY = window.pageYOffset;
+    } else {
+      pageY = document.documentElement.scrollTop;
+    }
+    return pageY;"""
+
+    pages.load("macbeth.html")
+
+    driver.find_element(By.PARTIAL_LINK_TEXT, "last speech").click()
+
+    yOffset = driver.execute_script(scrollScript)
+
+    # Focusing on to click, but not actually following,
+    # the link will scroll it in to view, which is a few pixels further than 0
+    assert yOffset > 300
+
+
+def testShouldScrollToClickOnAnElementHiddenByOverflow(driver, pages):
+    pages.load("click_out_of_bounds_overflow.html")
+
+    link = driver.find_element(By.ID, "link")
+    try:
+        link.click()
+    except MoveTargetOutOfBoundsException as e:
+        AssertionError("Should not be out of bounds: %s" % e.msg)
+
+
+def testShouldBeAbleToClickOnAnElementHiddenByOverflow(driver, pages):
+    pages.load("scroll.html")
+
+    link = driver.find_element(By.ID, "line8")
+    # This used to throw a MoveTargetOutOfBoundsException - we don't expect it to
+    link.click()
+    assert "line8" == driver.find_element(By.ID, "clicked").text
+
+
+def testShouldBeAbleToClickOnAnElementHiddenByDoubleOverflow(driver, pages):
+    pages.load("scrolling_tests/page_with_double_overflow_auto.html")
+
+    driver.find_element(By.ID, "link").click()
+    WebDriverWait(driver, 3).until(EC.title_is("Clicked Successfully!"))
+
+
+def testShouldBeAbleToClickOnAnElementHiddenByYOverflow(driver, pages):
+    pages.load("scrolling_tests/page_with_y_overflow_auto.html")
+
+    driver.find_element(By.ID, "link").click()
+    WebDriverWait(driver, 3).until(EC.title_is("Clicked Successfully!"))
+
+
+def testShouldNotScrollOverflowElementsWhichAreVisible(driver, pages):
+    pages.load("scroll2.html")
+    list = driver.find_element(By.TAG_NAME, "ul")
+    item = list.find_element(By.ID, "desired")
+    item.click()
+    yOffset = driver.execute_script("return arguments[0].scrollTop", list)
+    assert 0 == yOffset, "Should not have scrolled"
+
+
+@pytest.mark.xfail_chrome(
+    reason='https://bugs.chromium.org/p/chromedriver/issues/detail?id=1542')
+@pytest.mark.xfail_marionette
+@pytest.mark.xfail_remote
+def testShouldNotScrollIfAlreadyScrolledAndElementIsInView(driver, pages):
+    pages.load("scroll3.html")
+    driver.find_element(By.ID, "button1").click()
+    scrollTop = getScrollTop(driver)
+    driver.find_element(By.ID, "button2").click()
+    assert scrollTop == getScrollTop(driver)
+
+
+def testShouldBeAbleToClickRadioButtonScrolledIntoView(driver, pages):
+    pages.load("scroll4.html")
+    driver.find_element(By.ID, "radio").click()
+    # If we don't throw, we're good
+
+
+def testShouldScrollOverflowElementsIfClickPointIsOutOfViewButElementIsInView(driver, pages):
+    pages.load("scroll5.html")
+    driver.find_element(By.ID, "inner").click()
+    assert "clicked" == driver.find_element(By.ID, "clicked").text
+
+
+@pytest.mark.xfail_marionette(
+    reason='https://github.com/w3c/webdriver/issues/408')
+@pytest.mark.xfail_remote(
+    reason='https://github.com/w3c/webdriver/issues/408')
+def testShouldBeAbleToClickElementInAFrameThatIsOutOfView(driver, pages):
+    pages.load("scrolling_tests/page_with_frame_out_of_view.html")
+    driver.switch_to.frame(driver.find_element_by_name("frame"))
+    element = driver.find_element(By.NAME, "checkbox")
+    element.click()
+    assert element.is_selected()
+
+
+def testShouldBeAbleToClickElementThatIsOutOfViewInAFrame(driver, pages):
+    pages.load("scrolling_tests/page_with_scrolling_frame.html")
+    driver.switch_to.frame(driver.find_element_by_name("scrolling_frame"))
+    element = driver.find_element(By.NAME, "scroll_checkbox")
+    element.click()
+    assert element.is_selected()
+
+
+def testShouldNotBeAbleToClickElementThatIsOutOfViewInANonScrollableFrame(driver, pages):
+    pages.load("scrolling_tests/page_with_non_scrolling_frame.html")
+    driver.switch_to.frame("scrolling_frame")
+    element = driver.find_element(By.NAME, "scroll_checkbox")
+    element.click()
+    # TODO we should assert that the click was unsuccessful
+
+
+def testShouldBeAbleToClickElementThatIsOutOfViewInAFrameThatIsOutOfView(driver, pages):
+    pages.load("scrolling_tests/page_with_scrolling_frame_out_of_view.html")
+    driver.switch_to.frame(driver.find_element_by_name("scrolling_frame"))
+    element = driver.find_element(By.NAME, "scroll_checkbox")
+    element.click()
+    assert element.is_selected()
+
+
+def testShouldBeAbleToClickElementThatIsOutOfViewInANestedFrame(driver, pages):
+    pages.load("scrolling_tests/page_with_nested_scrolling_frames.html")
+    driver.switch_to.frame(driver.find_element_by_name("scrolling_frame"))
+    driver.switch_to.frame(driver.find_element_by_name("nested_scrolling_frame"))
+    element = driver.find_element(By.NAME, "scroll_checkbox")
+    element.click()
+    assert element.is_selected()
+
+
+def testShouldBeAbleToClickElementThatIsOutOfViewInANestedFrameThatIsOutOfView(driver, pages):
+    pages.load("scrolling_tests/page_with_nested_scrolling_frames_out_of_view.html")
+    driver.switch_to.frame(driver.find_element_by_name("scrolling_frame"))
+    driver.switch_to.frame(driver.find_element_by_name("nested_scrolling_frame"))
+    element = driver.find_element(By.NAME, "scroll_checkbox")
+    element.click()
+    assert element.is_selected()
+
+
+def testShouldNotScrollWhenGettingElementSize(driver, pages):
+    pages.load("scroll3.html")
+    scrollTop = getScrollTop(driver)
+    driver.find_element(By.ID, "button1").size
+    assert scrollTop == getScrollTop(driver)
+
+
+def getScrollTop(driver):
+    return driver.execute_script("return document.body.scrollTop")
+
+
+@pytest.mark.xfail_marionette(
+    reason='https://github.com/w3c/webdriver/issues/408')
+@pytest.mark.xfail_remote(
+    reason='https://github.com/w3c/webdriver/issues/408')
+def testShouldBeAbleToClickElementInATallFrame(driver, pages):
+    pages.load("scrolling_tests/page_with_tall_frame.html")
+    driver.switch_to.frame(driver.find_element_by_name("tall_frame"))
+    element = driver.find_element(By.NAME, "checkbox")
+    element.click()
+    assert element.is_selected()
diff --git a/test/selenium/webdriver/common/click_tests.py b/test/selenium/webdriver/common/click_tests.py
new file mode 100644
index 00000000..5f4458fd
--- /dev/null
+++ b/test/selenium/webdriver/common/click_tests.py
@@ -0,0 +1,37 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.support.wait import WebDriverWait
+
+
+@pytest.fixture(autouse=True)
+def loadPage(pages):
+    pages.load("clicks.html")
+
+
+def testCanClickOnALinkThatOverflowsAndFollowIt(driver):
+    driver.find_element(By.ID, "overflowLink").click()
+    WebDriverWait(driver, 3).until(EC.title_is("XHTML Test Page"))
+
+
+def testClickingALinkMadeUpOfNumbersIsHandledCorrectly(driver):
+    driver.find_element(By.LINK_TEXT, "333333").click()
+    WebDriverWait(driver, 3).until(EC.title_is("XHTML Test Page"))
diff --git a/test/selenium/webdriver/common/conftest.py b/test/selenium/webdriver/common/conftest.py
new file mode 100644
index 00000000..75670927
--- /dev/null
+++ b/test/selenium/webdriver/common/conftest.py
@@ -0,0 +1,21 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+def pytest_generate_tests(metafunc):
+    if 'driver' in metafunc.fixturenames and metafunc.config.option.drivers:
+        metafunc.parametrize('driver', metafunc.config.option.drivers, indirect=True)
diff --git a/test/selenium/webdriver/common/cookie_tests.py b/test/selenium/webdriver/common/cookie_tests.py
new file mode 100644
index 00000000..a1c0970f
--- /dev/null
+++ b/test/selenium/webdriver/common/cookie_tests.py
@@ -0,0 +1,102 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import calendar
+import time
+import random
+
+import pytest
+
+
+@pytest.fixture
+def cookie(webserver):
+    cookie = {
+        'name': 'foo',
+        'value': 'bar',
+        'domain': webserver.host,
+        'path': '/',
+        'secure': False}
+    return cookie
+
+
+@pytest.fixture(autouse=True)
+def pages(request, driver, pages):
+    pages.load('simpleTest.html')
+    yield pages
+    driver.delete_all_cookies()
+
+
+def testAddCookie(cookie, driver):
+    driver.add_cookie(cookie)
+    returned = driver.execute_script('return document.cookie')
+    assert cookie['name'] in returned
+
+
+@pytest.mark.xfail_ie
+def testAddingACookieThatExpiredInThePast(cookie, driver):
+    expired = cookie.copy()
+    expired['expiry'] = calendar.timegm(time.gmtime()) - 1
+    driver.add_cookie(expired)
+    assert 0 == len(driver.get_cookies())
+
+
+def testDeleteAllCookie(cookie, driver):
+    driver.add_cookie(cookie)
+    driver.delete_all_cookies()
+    assert not driver.get_cookies()
+
+
+def testDeleteCookie(cookie, driver):
+    driver.add_cookie(cookie)
+    driver.delete_cookie('foo')
+    assert not driver.get_cookies()
+
+
+def testShouldGetCookieByName(driver):
+    key = 'key_{}'.format(int(random.random() * 10000000))
+    driver.execute_script("document.cookie = arguments[0] + '=set';", key)
+    cookie = driver.get_cookie(key)
+    assert 'set' == cookie['value']
+
+
+def testShouldReturnNoneWhenCookieDoesNotExist(driver):
+    key = 'key_{}'.format(int(random.random() * 10000000))
+    cookie = driver.get_cookie(key)
+    assert cookie is None
+
+
+def testGetAllCookies(cookie, driver, pages, webserver):
+    cookies = driver.get_cookies()
+    count = len(cookies)
+
+    for i in range(2):
+        cookie['name'] = 'key_{}'.format(int(random.random() * 10000000))
+        driver.add_cookie(cookie)
+
+    pages.load('simpleTest.html')
+    assert count + 2 == len(driver.get_cookies())
+
+
+def testShouldNotDeleteCookiesWithASimilarName(cookie, driver, webserver):
+    cookie2 = cookie.copy()
+    cookie2['name'] = '{}x'.format(cookie['name'])
+    driver.add_cookie(cookie)
+    driver.add_cookie(cookie2)
+    driver.delete_cookie(cookie['name'])
+    cookies = driver.get_cookies()
+    assert cookie['name'] != cookies[0]['name']
+    assert cookie2['name'] == cookies[0]['name']
diff --git a/test/selenium/webdriver/common/correct_event_firing_tests.py b/test/selenium/webdriver/common/correct_event_firing_tests.py
new file mode 100644
index 00000000..1a6af378
--- /dev/null
+++ b/test/selenium/webdriver/common/correct_event_firing_tests.py
@@ -0,0 +1,123 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+def testShouldFireClickEventWhenClicking(driver, pages):
+    pages.load("javascriptPage.html")
+    _clickOnElementWhichRecordsEvents(driver)
+    _assertEventFired(driver, "click")
+
+
+def testShouldFireMouseDownEventWhenClicking(driver, pages):
+    pages.load("javascriptPage.html")
+    _clickOnElementWhichRecordsEvents(driver)
+    _assertEventFired(driver, "mousedown")
+
+
+def testShouldFireMouseUpEventWhenClicking(driver, pages):
+    pages.load("javascriptPage.html")
+    _clickOnElementWhichRecordsEvents(driver)
+    _assertEventFired(driver, "mouseup")
+
+
+def testShouldIssueMouseDownEvents(driver, pages):
+    pages.load("javascriptPage.html")
+    driver.find_element_by_id("mousedown").click()
+    result = driver.find_element_by_id("result").text
+    assert result == "mouse down"
+
+
+def testShouldIssueClickEvents(driver, pages):
+    pages.load("javascriptPage.html")
+    driver.find_element_by_id("mouseclick").click()
+    result = driver.find_element_by_id("result").text
+    assert result == "mouse click"
+
+
+def testShouldIssueMouseUpEvents(driver, pages):
+    pages.load("javascriptPage.html")
+    driver.find_element_by_id("mouseup").click()
+    result = driver.find_element_by_id("result").text
+    assert result == "mouse up"
+
+
+def testMouseEventsShouldBubbleUpToContainingElements(driver, pages):
+    pages.load("javascriptPage.html")
+    driver.find_element_by_id("child").click()
+    result = driver.find_element_by_id("result").text
+    assert result == "mouse down"
+
+
+def testShouldEmitOnChangeEventsWhenSelectingElements(driver, pages):
+    pages.load("javascriptPage.html")
+    select = driver.find_element_by_id('selector')
+    options = select.find_elements_by_tag_name('option')
+    initialTextValue = driver.find_element_by_id("result").text
+
+    select.click()
+    assert driver.find_element_by_id("result").text == initialTextValue
+    options[1].click()
+    assert driver.find_element_by_id("result").text == "bar"
+
+
+def testShouldEmitOnChangeEventsWhenChangingTheStateOfACheckbox(driver, pages):
+    pages.load("javascriptPage.html")
+    checkbox = driver.find_element_by_id("checkbox")
+    checkbox.click()
+    assert driver.find_element_by_id("result").text == "checkbox thing"
+
+
+def testShouldEmitClickEventWhenClickingOnATextInputElement(driver, pages):
+    pages.load("javascriptPage.html")
+    clicker = driver.find_element_by_id("clickField")
+    clicker.click()
+
+    assert clicker.get_attribute("value") == "Clicked"
+
+
+def testClearingAnElementShouldCauseTheOnChangeHandlerToFire(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element_by_id("clearMe")
+    element.clear()
+    result = driver.find_element_by_id("result")
+    assert result.text == "Cleared"
+
+# TODO Currently Failing and needs fixing
+# def testSendingKeysToAnotherElementShouldCauseTheBlurEventToFire(driver, pages):
+#    pages.load("javascriptPage.html")
+#    element = driver.find_element_by_id("theworks")
+#    element.send_keys("foo")
+#    element2 = driver.find_element_by_id("changeable")
+#    element2.send_keys("bar")
+#    _assertEventFired(driver, "blur")
+
+# TODO Currently Failing and needs fixing
+# def testSendingKeysToAnElementShouldCauseTheFocusEventToFire(driver, pages):
+#    pages.load("javascriptPage.html")
+#    element = driver.find_element_by_id("theworks")
+#    element.send_keys("foo")
+#    _assertEventFired(driver, "focus")
+
+
+def _clickOnElementWhichRecordsEvents(driver):
+    driver.find_element_by_id("plainButton").click()
+
+
+def _assertEventFired(driver, eventName):
+    result = driver.find_element_by_id("result")
+    text = result.text
+    assert eventName in text, "No " + eventName + " fired: " + text
diff --git a/test/selenium/webdriver/common/driver_element_finding_tests.py b/test/selenium/webdriver/common/driver_element_finding_tests.py
new file mode 100755
index 00000000..a1402f9e
--- /dev/null
+++ b/test/selenium/webdriver/common/driver_element_finding_tests.py
@@ -0,0 +1,629 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver.common.by import By
+from selenium.common.exceptions import (
+    InvalidSelectorException,
+    NoSuchElementException,
+    WebDriverException)
+
+# By.id positive
+
+
+def test_Should_Be_Able_To_Find_ASingle_Element_By_Id(driver, pages):
+    pages.load("xhtmlTest.html")
+    element = driver.find_element(By.ID, "linkId")
+    assert element.get_attribute("id") == "linkId"
+
+
+def test_Should_Be_Able_To_Find_ASingle_Element_By_Numeric_Id(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element(By.ID, "2")
+    assert element.get_attribute("id") == "2"
+
+
+def test_should_be_able_to_find_an_element_with_css_escape(driver, pages):
+    pages.load("idElements.html")
+    element = driver.find_element(By.ID, "with.dots")
+    assert element.get_attribute("id") == "with.dots"
+
+
+def test_Should_Be_Able_To_Find_Multiple_Elements_By_Id(driver, pages):
+    pages.load("nestedElements.html")
+    elements = driver.find_elements(By.ID, "test_id")
+    assert len(elements) == 2
+
+
+def test_Should_Be_Able_To_Find_Multiple_Elements_By_Numeric_Id(driver, pages):
+    pages.load("nestedElements.html")
+    elements = driver.find_elements(By.ID, "2")
+    assert len(elements) == 8
+
+# By.id negative
+
+
+def test_Should_Not_Be_Able_To_Locate_By_Id_ASingle_Element_That_Does_Not_Exist(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.ID, "non_Existent_Button")
+
+
+def test_Should_Not_Be_Able_To_Locate_By_Id_Multiple_Elements_That_Do_Not_Exist(driver, pages):
+    pages.load("formPage.html")
+    elements = driver.find_elements(By.ID, "non_Existent_Button")
+    assert len(elements) == 0
+
+
+def test_Finding_ASingle_Element_By_Empty_Id_Should_Throw(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.ID, "")
+
+
+def test_Finding_Multiple_Elements_By_Empty_Id_Should_Return_Empty_List(driver, pages):
+    pages.load("formPage.html")
+    elements = driver.find_elements(By.ID, "")
+    assert len(elements) == 0
+
+
+def test_Finding_ASingle_Element_By_Id_With_Space_Should_Throw(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.ID, "nonexistent button")
+
+
+def test_Finding_Multiple_Elements_By_Id_With_Space_Should_Return_Empty_List(driver, pages):
+    pages.load("formPage.html")
+    elements = driver.find_elements(By.ID, "nonexistent button")
+    assert len(elements) == 0
+
+# By.name positive
+
+
+def test_Should_Be_Able_To_Find_ASingle_Element_By_Name(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element(By.NAME, "checky")
+    assert element.get_attribute("value") == "furrfu"
+
+
+def test_Should_Be_Able_To_Find_Multiple_Elements_By_Name(driver, pages):
+    pages.load("nestedElements.html")
+    elements = driver.find_elements(By.NAME, "checky")
+    assert len(elements) > 1
+
+
+def test_Should_Be_Able_To_Find_An_Element_That_Does_Not_Support_The_Name_Property(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element(By.NAME, "div1")
+    assert element.get_attribute("name") == "div1"
+
+# By.name negative
+
+
+def test_Should_Not_Be_Able_To_Locate_By_Name_ASingle_Element_That_Does_Not_Exist(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.NAME, "non_Existent_Button")
+
+
+def test_Should_Not_Be_Able_To_Locate_By_Name_Multiple_Elements_That_Do_Not_Exist(driver, pages):
+    pages.load("formPage.html")
+    elements = driver.find_elements(By.NAME, "non_Existent_Button")
+    assert len(elements) == 0
+
+
+def test_Finding_ASingle_Element_By_Empty_Name_Should_Throw(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.NAME, "")
+
+
+def test_Finding_Multiple_Elements_By_Empty_Name_Should_Return_Empty_List(driver, pages):
+    pages.load("formPage.html")
+    elements = driver.find_elements(By.NAME, "")
+    assert len(elements) == 0
+
+
+def test_Finding_ASingle_Element_By_Name_With_Space_Should_Throw(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.NAME, "nonexistent button")
+
+
+def test_Finding_Multiple_Elements_By_Name_With_Space_Should_Return_Empty_List(driver, pages):
+    pages.load("formPage.html")
+    elements = driver.find_elements(By.NAME, "nonexistent button")
+    assert len(elements) == 0
+
+# By.tag_Name positive
+
+
+def test_Should_Be_Able_To_Find_ASingle_Element_By_Tag_Name(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element(By.TAG_NAME, "input")
+    assert element.tag_name.lower() == "input"
+
+
+def test_Should_Be_Able_To_Find_Multiple_Elements_By_Tag_Name(driver, pages):
+    pages.load("formPage.html")
+    elements = driver.find_elements(By.TAG_NAME, "input")
+    assert len(elements) > 1
+
+# By.tag_Name negative
+
+
+def test_Should_Not_Be_Able_To_Locate_By_Tag_Name_ASingle_Element_That_Does_Not_Exist(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.TAG_NAME, "non_Existent_Button")
+
+
+def test_Should_Not_Be_Able_To_Locate_By_Tag_Name_Multiple_Elements_That_Do_Not_Exist(driver, pages):
+    pages.load("formPage.html")
+    elements = driver.find_elements(By.TAG_NAME, "non_Existent_Button")
+    assert len(elements) == 0
+
+
+def test_Finding_ASingle_Element_By_Empty_Tag_Name_Should_Throw(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(InvalidSelectorException):
+        driver.find_element(By.TAG_NAME, "")
+
+
+def test_Finding_Multiple_Elements_By_Empty_Tag_Name_Should_Throw(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(InvalidSelectorException):
+        driver.find_elements(By.TAG_NAME, "")
+
+
+def test_Finding_ASingle_Element_By_Tag_Name_With_Space_Should_Throw(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.TAG_NAME, "nonexistent button")
+
+
+def test_Finding_Multiple_Elements_By_Tag_Name_With_Space_Should_Return_Empty_List(driver, pages):
+    pages.load("formPage.html")
+    elements = driver.find_elements(By.TAG_NAME, "nonexistent button")
+    assert len(elements) == 0
+
+# By.class_Name positive
+
+
+def test_Should_Be_Able_To_Find_ASingle_Element_By_Class(driver, pages):
+    pages.load("xhtmlTest.html")
+    element = driver.find_element(By.CLASS_NAME, "extraDiv")
+    assert "Another div starts here." in element.text
+
+
+def test_Should_Be_Able_To_Find_Multiple_Elements_By_Class_Name(driver, pages):
+    pages.load("xhtmlTest.html")
+    elements = driver.find_elements(By.CLASS_NAME, "nameC")
+    assert len(elements) > 1
+
+
+def test_Should_Find_Element_By_Class_When_It_Is_The_First_Name_Among_Many(driver, pages):
+    pages.load("xhtmlTest.html")
+    element = driver.find_element(By.CLASS_NAME, "nameA")
+    assert element.text == "An H2 title"
+
+
+def test_Should_Find_Element_By_Class_When_It_Is_The_Last_Name_Among_Many(driver, pages):
+    pages.load("xhtmlTest.html")
+    element = driver.find_element(By.CLASS_NAME, "nameC")
+    assert element.text == "An H2 title"
+
+
+def test_Should_Find_Element_By_Class_When_It_Is_In_The_Middle_Among_Many(driver, pages):
+    pages.load("xhtmlTest.html")
+    element = driver.find_element(By.CLASS_NAME, "nameBnoise")
+    assert element.text == "An H2 title"
+
+
+def test_Should_Find_Element_By_Class_When_Its_Name_Is_Surrounded_By_Whitespace(driver, pages):
+    pages.load("xhtmlTest.html")
+    element = driver.find_element(By.CLASS_NAME, "spaceAround")
+    assert element.text == "Spaced out"
+
+
+def test_Should_Find_Elements_By_Class_When_Its_Name_Is_Surrounded_By_Whitespace(driver, pages):
+    pages.load("xhtmlTest.html")
+    elements = driver.find_elements(By.CLASS_NAME, "spaceAround")
+    assert len(elements) == 1
+    assert elements[0].text == "Spaced out"
+
+# By.class_Name negative
+
+
+def test_Should_Not_Find_Element_By_Class_When_The_Name_Queried_Is_Shorter_Than_Candidate_Name(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.CLASS_NAME, "name_B")
+
+
+def test_Finding_ASingle_Element_By_Empty_Class_Name_Should_Throw(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.CLASS_NAME, "")
+
+
+def test_Finding_Multiple_Elements_By_Empty_Class_Name_Should_Throw(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_elements(By.CLASS_NAME, "")
+
+
+def test_Finding_ASingle_Element_By_Compound_Class_Name_Should_Throw(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.CLASS_NAME, "a b")
+
+
+def test_Finding_ASingle_Element_By_Invalid_Class_Name_Should_Throw(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.CLASS_NAME, "!@#$%^&*")
+
+
+def test_Finding_Multiple_Elements_By_Invalid_Class_Name_Should_Throw(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_elements(By.CLASS_NAME, "!@#$%^&*")
+
+# By.xpath positive
+
+
+def test_Should_Be_Able_To_Find_ASingle_Element_By_XPath(driver, pages):
+    pages.load("xhtmlTest.html")
+    element = driver.find_element(By.XPATH, "//h1")
+    assert element.text == "XHTML Might Be The Future"
+
+
+def test_Should_Be_Able_To_Find_Multiple_Elements_By_XPath(driver, pages):
+    pages.load("xhtmlTest.html")
+    elements = driver.find_elements(By.XPATH, "//div")
+    assert len(elements) == 13
+
+
+def test_Should_Be_Able_To_Find_Many_Elements_Repeatedly_By_XPath(driver, pages):
+    pages.load("xhtmlTest.html")
+    xpath = "//node()[contains(@id,'id')]"
+    assert len(driver.find_elements(By.XPATH, xpath)) == 3
+
+    xpath = "//node()[contains(@id,'nope')]"
+    assert len(driver.find_elements(By.XPATH, xpath)) == 0
+
+
+def test_Should_Be_Able_To_Identify_Elements_By_Class(driver, pages):
+    pages.load("xhtmlTest.html")
+    header = driver.find_element(By.XPATH, "//h1[@class='header']")
+    assert header.text == "XHTML Might Be The Future"
+
+
+def test_Should_Be_Able_To_Find_An_Element_By_XPath_With_Multiple_Attributes(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element(
+        By.XPATH, "//form[@name='optional']/input[@type='submit' and @value='Click!']")
+    assert element.tag_name.lower() == "input"
+    assert element.get_attribute("value") == "Click!"
+
+
+def test_Finding_ALink_By_Xpath_Should_Locate_An_Element_With_The_Given_Text(driver, pages):
+    pages.load("xhtmlTest.html")
+    element = driver.find_element(By.XPATH, "//a[text()='click me']")
+    assert element.text == "click me"
+
+
+def test_Finding_ALink_By_Xpath_Using_Contains_Keyword_Should_Work(driver, pages):
+    pages.load("nestedElements.html")
+    element = driver.find_element(By.XPATH, "//a[contains(.,'hello world')]")
+    assert "hello world" in element.text
+
+
+@pytest.mark.xfail_chrome(raises=InvalidSelectorException)
+@pytest.mark.xfail_firefox(raises=InvalidSelectorException)
+@pytest.mark.xfail_remote(raises=InvalidSelectorException)
+@pytest.mark.xfail_marionette(raises=WebDriverException)
+@pytest.mark.xfail_safari(raises=NoSuchElementException)
+@pytest.mark.xfail_webkitgtk(raises=InvalidSelectorException)
+def test_Should_Be_Able_To_Find_Element_By_XPath_With_Namespace(driver, pages):
+    pages.load("svgPage.html")
+    element = driver.find_element(By.XPATH, "//svg:svg//svg:text")
+    assert element.text == "Test Chart"
+
+
+def test_Should_Be_Able_To_Find_Element_By_XPath_In_Xml_Document(driver, pages):
+    pages.load("simple.xml")
+    element = driver.find_element(By.XPATH, "//foo")
+    assert "baz" in element.text
+
+# By.xpath negative
+
+
+def test_Should_Throw_An_Exception_When_There_Is_No_Link_To_Click(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.XPATH, "//a[@id='Not here']")
+
+
+def test_Should_Throw_InvalidSelectorException_When_XPath_Is_Syntactically_Invalid_In_Driver_Find_Element(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(InvalidSelectorException):
+        driver.find_element(By.XPATH, "this][isnot][valid")
+
+
+def test_Should_Throw_InvalidSelectorException_When_XPath_Is_Syntactically_Invalid_In_Driver_Find_Elements(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(InvalidSelectorException):
+        driver.find_elements(By.XPATH, "this][isnot][valid")
+
+
+def test_Should_Throw_InvalidSelectorException_When_XPath_Is_Syntactically_Invalid_In_Element_Find_Element(driver, pages):
+    pages.load("formPage.html")
+    body = driver.find_element(By.TAG_NAME, "body")
+    with pytest.raises(InvalidSelectorException):
+        body.find_element(By.XPATH, "this][isnot][valid")
+
+
+def test_Should_Throw_InvalidSelectorException_When_XPath_Is_Syntactically_Invalid_In_Element_Find_Elements(driver, pages):
+    pages.load("formPage.html")
+    body = driver.find_element(By.TAG_NAME, "body")
+    with pytest.raises(InvalidSelectorException):
+        body.find_elements(By.XPATH, "this][isnot][valid")
+
+
+def test_Should_Throw_InvalidSelectorException_When_XPath_Returns_Wrong_Type_In_Driver_Find_Element(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(InvalidSelectorException):
+        driver.find_element(By.XPATH, "count(//input)")
+
+
+def test_Should_Throw_InvalidSelectorException_When_XPath_Returns_Wrong_Type_In_Driver_Find_Elements(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(InvalidSelectorException):
+        driver.find_elements(By.XPATH, "count(//input)")
+
+
+def test_Should_Throw_InvalidSelectorException_When_XPath_Returns_Wrong_Type_In_Element_Find_Element(driver, pages):
+    pages.load("formPage.html")
+    body = driver.find_element(By.TAG_NAME, "body")
+    with pytest.raises(InvalidSelectorException):
+        body.find_element(By.XPATH, "count(//input)")
+
+
+def test_Should_Throw_InvalidSelectorException_When_XPath_Returns_Wrong_Type_In_Element_Find_Elements(driver, pages):
+    pages.load("formPage.html")
+    body = driver.find_element(By.TAG_NAME, "body")
+    with pytest.raises(InvalidSelectorException):
+        body.find_elements(By.XPATH, "count(//input)")
+
+# By.css_Selector positive
+
+
+def test_Should_Be_Able_To_Find_ASingle_Element_By_Css_Selector(driver, pages):
+    pages.load("xhtmlTest.html")
+    element = driver.find_element(By.CSS_SELECTOR, "div.content")
+    assert element.tag_name.lower() == "div"
+    assert element.get_attribute("class") == "content"
+
+
+def test_Should_Be_Able_To_Find_Multiple_Elements_By_Css_Selector(driver, pages):
+    pages.load("xhtmlTest.html")
+    elements = driver.find_elements(By.CSS_SELECTOR, "p")
+    assert len(elements) > 1
+
+
+def test_Should_Be_Able_To_Find_ASingle_Element_By_Compound_Css_Selector(driver, pages):
+    pages.load("xhtmlTest.html")
+    element = driver.find_element(By.CSS_SELECTOR, "div.extraDiv, div.content")
+    assert element.tag_name.lower() == "div"
+    assert element.get_attribute("class") == "content"
+
+
+def test_Should_Be_Able_To_Find_Multiple_Elements_By_Compound_Css_Selector(driver, pages):
+    pages.load("xhtmlTest.html")
+    elements = driver.find_elements(By.CSS_SELECTOR, "div.extraDiv, div.content")
+    assert len(elements) > 1
+    assert elements[0].get_attribute("class") == "content"
+    assert elements[1].get_attribute("class") == "extraDiv"
+
+
+def test_Should_Be_Able_To_Find_An_Element_By_Boolean_Attribute_Using_Css_Selector(driver, pages):
+    pages.load("locators_tests/boolean_attribute_selected.html")
+    element = driver.find_element(By.CSS_SELECTOR, "option[selected='selected']")
+    assert element.get_attribute("value") == "two"
+
+
+def test_Should_Be_Able_To_Find_An_Element_By_Boolean_Attribute_Using_Short_Css_Selector(driver, pages):
+    pages.load("locators_tests/boolean_attribute_selected.html")
+    element = driver.find_element(By.CSS_SELECTOR, "option[selected]")
+    assert element.get_attribute("value") == "two"
+
+
+def test_Should_Be_Able_To_Find_An_Element_By_Boolean_Attribute_Using_Short_Css_Selector_On_Html4Page(driver, pages):
+    pages.load("locators_tests/boolean_attribute_selected_html4.html")
+    element = driver.find_element(By.CSS_SELECTOR, "option[selected]")
+    assert element.get_attribute("value") == "two"
+
+# By.css_Selector negative
+
+
+def test_Should_Not_Find_Element_By_Css_Selector_When_There_Is_No_Such_Element(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.CSS_SELECTOR, ".there-is-no-such-class")
+
+
+def test_Should_Not_Find_Elements_By_Css_Selector_When_There_Is_No_Such_Element(driver, pages):
+    pages.load("xhtmlTest.html")
+    elements = driver.find_elements(By.CSS_SELECTOR, ".there-is-no-such-class")
+    assert len(elements) == 0
+
+
+def test_Finding_ASingle_Element_By_Empty_Css_Selector_Should_Throw(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.CSS_SELECTOR, "")
+
+
+def test_Finding_Multiple_Elements_By_Empty_Css_Selector_Should_Throw(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_elements(By.CSS_SELECTOR, "")
+
+
+def test_Finding_ASingle_Element_By_Invalid_Css_Selector_Should_Throw(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.CSS_SELECTOR, "//a/b/c[@id='1']")
+
+
+def test_Finding_Multiple_Elements_By_Invalid_Css_Selector_Should_Throw(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_elements(By.CSS_SELECTOR, "//a/b/c[@id='1']")
+
+# By.link_Text positive
+
+
+def test_Should_Be_Able_To_Find_ALink_By_Text(driver, pages):
+    pages.load("xhtmlTest.html")
+    link = driver.find_element(By.LINK_TEXT, "click me")
+    assert link.text == "click me"
+
+
+def test_Should_Be_Able_To_Find_Multiple_Links_By_Text(driver, pages):
+    pages.load("xhtmlTest.html")
+    elements = driver.find_elements(By.LINK_TEXT, "click me")
+    assert len(elements) == 2
+
+
+def test_Should_Find_Element_By_Link_Text_Containing_Equals_Sign(driver, pages):
+    pages.load("xhtmlTest.html")
+    element = driver.find_element(By.LINK_TEXT, "Link=equalssign")
+    assert element.get_attribute("id") == "linkWithEqualsSign"
+
+
+def test_Should_Find_Multiple_Elements_By_Link_Text_Containing_Equals_Sign(driver, pages):
+    pages.load("xhtmlTest.html")
+    elements = driver.find_elements(By.LINK_TEXT, "Link=equalssign")
+    assert 1 == len(elements)
+    assert elements[0].get_attribute("id") == "linkWithEqualsSign"
+
+
+def test_finds_By_Link_Text_On_Xhtml_Page(driver, pages):
+    pages.load("actualXhtmlPage.xhtml")
+    link_Text = "Foo"
+    element = driver.find_element(By.LINK_TEXT, link_Text)
+    assert element.text == link_Text
+
+
+def test_Link_With_Formatting_Tags(driver, pages):
+    pages.load("simpleTest.html")
+    elem = driver.find_element(By.ID, "links")
+
+    res = elem.find_element(By.PARTIAL_LINK_TEXT, "link with formatting tags")
+    assert res.text == "link with formatting tags"
+
+
+def test_Driver_Can_Get_Link_By_Link_Test_Ignoring_Trailing_Whitespace(driver, pages):
+    pages.load("simpleTest.html")
+    link = driver.find_element(By.LINK_TEXT, "link with trailing space")
+    assert link.get_attribute("id") == "linkWithTrailingSpace"
+    assert link.text == "link with trailing space"
+
+# By.link_Text negative
+
+
+def test_Should_Not_Be_Able_To_Locate_By_Link_Text_ASingle_Element_That_Does_Not_Exist(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.LINK_TEXT, "Not here either")
+
+
+def test_Should_Not_Be_Able_To_Locate_By_Link_Text_Multiple_Elements_That_Do_Not_Exist(driver, pages):
+    pages.load("xhtmlTest.html")
+    elements = driver.find_elements(By.LINK_TEXT, "Not here either")
+    assert len(elements) == 0
+
+# By.partial_Link_Text positive
+
+
+def test_Should_Be_Able_To_Find_Multiple_Elements_By_Partial_Link_Text(driver, pages):
+    pages.load("xhtmlTest.html")
+    elements = driver.find_elements(By.PARTIAL_LINK_TEXT, "ick me")
+    assert len(elements) == 2
+
+
+def test_Should_Be_Able_To_Find_ASingle_Element_By_Partial_Link_Text(driver, pages):
+    pages.load("xhtmlTest.html")
+    element = driver.find_element(By.PARTIAL_LINK_TEXT, "anon")
+    assert "anon" in element.text
+
+
+def test_Should_Find_Element_By_Partial_Link_Text_Containing_Equals_Sign(driver, pages):
+    pages.load("xhtmlTest.html")
+    element = driver.find_element(By.PARTIAL_LINK_TEXT, "Link=")
+    assert element.get_attribute("id") == "linkWithEqualsSign"
+
+
+def test_Should_Find_Multiple_Elements_By_Partial_Link_Text_Containing_Equals_Sign(driver, pages):
+    pages.load("xhtmlTest.html")
+    elements = driver.find_elements(By.PARTIAL_LINK_TEXT, "Link=")
+    assert len(elements) == 1
+    assert elements[0].get_attribute("id") == "linkWithEqualsSign"
+
+# Misc tests
+
+
+def test_Driver_Should_Be_Able_To_Find_Elements_After_Loading_More_Than_One_Page_At_ATime(driver, pages):
+    pages.load("formPage.html")
+    pages.load("xhtmlTest.html")
+    link = driver.find_element(By.LINK_TEXT, "click me")
+    assert link.text == "click me"
+
+# You don't want to ask why this is here
+
+
+def test_When_Finding_By_Name_Should_Not_Return_By_Id(driver, pages):
+    pages.load("formPage.html")
+
+    element = driver.find_element(By.NAME, "id-name1")
+    assert element.get_attribute("value") == "name"
+
+    element = driver.find_element(By.ID, "id-name1")
+    assert element.get_attribute("value") == "id"
+
+    element = driver.find_element(By.NAME, "id-name2")
+    assert element.get_attribute("value") == "name"
+
+    element = driver.find_element(By.ID, "id-name2")
+    assert element.get_attribute("value") == "id"
+
+
+def test_Should_Be_Able_To_Find_AHidden_Elements_By_Name(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element(By.NAME, "hidden")
+    assert element.get_attribute("name") == "hidden"
+
+
+def test_Should_Not_Be_Able_To_Find_An_Element_On_ABlank_Page(driver, pages):
+    driver.get("about:blank")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element(By.TAG_NAME, "a")
diff --git a/test/selenium/webdriver/common/element_attribute_tests.py b/test/selenium/webdriver/common/element_attribute_tests.py
new file mode 100644
index 00000000..bc6dd8ab
--- /dev/null
+++ b/test/selenium/webdriver/common/element_attribute_tests.py
@@ -0,0 +1,272 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import WebDriverException
+
+
+def testShouldReturnNullWhenGettingTheValueOfAnAttributeThatIsNotListed(driver, pages):
+    pages.load("simpleTest.html")
+    head = driver.find_element_by_xpath("/html")
+    attribute = head.get_attribute("cheese")
+    assert attribute is None
+
+
+def testShouldReturnNullWhenGettingSrcAttributeOfInvalidImgTag(driver, pages):
+    pages.load("simpleTest.html")
+    img = driver.find_element_by_id("invalidImgTag")
+    img_attr = img.get_attribute("src")
+    assert img_attr is None
+
+
+def testShouldReturnAnAbsoluteUrlWhenGettingSrcAttributeOfAValidImgTag(driver, pages):
+    pages.load("simpleTest.html")
+    img = driver.find_element_by_id("validImgTag")
+    img_attr = img.get_attribute("src")
+    assert "icon.gif" in img_attr
+
+
+def testShouldReturnAnAbsoluteUrlWhenGettingHrefAttributeOfAValidAnchorTag(driver, pages):
+    pages.load("simpleTest.html")
+    img = driver.find_element_by_id("validAnchorTag")
+    img_attr = img.get_attribute("href")
+    assert "icon.gif" in img_attr
+
+
+def testShouldReturnEmptyAttributeValuesWhenPresentAndTheValueIsActuallyEmpty(driver, pages):
+    pages.load("simpleTest.html")
+    body = driver.find_element_by_xpath("//body")
+    assert "" == body.get_attribute("style")
+
+
+def testShouldReturnTheValueOfTheDisabledAttributeAsFalseIfNotSet(driver, pages):
+    pages.load("formPage.html")
+    inputElement = driver.find_element_by_xpath("//input[@id='working']")
+    assert inputElement.get_attribute("disabled") is None
+    assert inputElement.is_enabled()
+
+    pElement = driver.find_element_by_id("peas")
+    assert pElement.get_attribute("disabled") is None
+    assert pElement.is_enabled()
+
+
+def testShouldReturnTheValueOfTheIndexAttrbuteEvenIfItIsMissing(driver, pages):
+    pages.load("formPage.html")
+    multiSelect = driver.find_element_by_id("multi")
+    options = multiSelect.find_elements_by_tag_name("option")
+    assert "1" == options[1].get_attribute("index")
+
+
+def testShouldIndicateTheElementsThatAreDisabledAreNotis_enabled(driver, pages):
+    pages.load("formPage.html")
+    inputElement = driver.find_element_by_xpath("//input[@id='notWorking']")
+    assert not inputElement.is_enabled()
+
+    inputElement = driver.find_element_by_xpath("//input[@id='working']")
+    assert inputElement.is_enabled()
+
+
+def testElementsShouldBeDisabledIfTheyAreDisabledUsingRandomDisabledStrings(driver, pages):
+    pages.load("formPage.html")
+    disabledTextElement1 = driver.find_element_by_id("disabledTextElement1")
+    assert not disabledTextElement1.is_enabled()
+
+    disabledTextElement2 = driver.find_element_by_id("disabledTextElement2")
+    assert not disabledTextElement2.is_enabled()
+
+    disabledSubmitElement = driver.find_element_by_id("disabledSubmitElement")
+    assert not disabledSubmitElement.is_enabled()
+
+
+def testShouldIndicateWhenATextAreaIsDisabled(driver, pages):
+    pages.load("formPage.html")
+    textArea = driver.find_element_by_xpath("//textarea[@id='notWorkingArea']")
+    assert not textArea.is_enabled()
+
+
+def testShouldThrowExceptionIfSendingKeysToElementDisabledUsingRandomDisabledStrings(driver, pages):
+    pages.load("formPage.html")
+    disabledTextElement1 = driver.find_element_by_id("disabledTextElement1")
+    with pytest.raises(WebDriverException):
+        disabledTextElement1.send_keys("foo")
+    assert "" == disabledTextElement1.text
+
+    disabledTextElement2 = driver.find_element_by_id("disabledTextElement2")
+    with pytest.raises(WebDriverException):
+        disabledTextElement2.send_keys("bar")
+    assert "" == disabledTextElement2.text
+
+
+def testShouldIndicateWhenASelectIsDisabled(driver, pages):
+    pages.load("formPage.html")
+    enabled = driver.find_element_by_name("selectomatic")
+    disabled = driver.find_element_by_name("no-select")
+
+    assert enabled.is_enabled()
+    assert not disabled.is_enabled()
+
+
+def testShouldReturnTheValueOfCheckedForACheckboxEvenIfItLacksThatAttribute(driver, pages):
+    pages.load("formPage.html")
+    checkbox = driver.find_element_by_xpath("//input[@id='checky']")
+    assert checkbox.get_attribute("checked") is None
+    checkbox.click()
+    assert "true" == checkbox.get_attribute("checked")
+
+
+def testShouldReturnTheValueOfSelectedForRadioButtonsEvenIfTheyLackThatAttribute(driver, pages):
+    pages.load("formPage.html")
+    neverSelected = driver.find_element_by_id("cheese")
+    initiallyNotSelected = driver.find_element_by_id("peas")
+    initiallySelected = driver.find_element_by_id("cheese_and_peas")
+
+    assert neverSelected.get_attribute("checked") is None
+    assert initiallyNotSelected.get_attribute("checked") is None
+    assert "true" == initiallySelected.get_attribute("checked")
+
+    initiallyNotSelected.click()
+    assert neverSelected.get_attribute("selected") is None
+    assert "true" == initiallyNotSelected.get_attribute("checked")
+    assert initiallySelected.get_attribute("checked") is None
+
+
+def testShouldReturnTheValueOfSelectedForOptionsInSelectsEvenIfTheyLackThatAttribute(driver, pages):
+    pages.load("formPage.html")
+    selectBox = driver.find_element_by_xpath("//select[@name='selectomatic']")
+    options = selectBox.find_elements_by_tag_name("option")
+    one = options[0]
+    two = options[1]
+    assert one.is_selected()
+    assert not two.is_selected()
+    assert "true" == one.get_attribute("selected")
+    assert two.get_attribute("selected") is None
+
+
+def testShouldReturnValueOfClassAttributeOfAnElement(driver, pages):
+    pages.load("xhtmlTest.html")
+    heading = driver.find_element_by_xpath("//h1")
+    classname = heading.get_attribute("class")
+    assert "header" == classname
+
+# Disabled due to issues with Frames
+# def testShouldReturnValueOfClassAttributeOfAnElementAfterSwitchingIFrame(driver, pages):
+#    pages.load("iframes.html")
+#    driver.switch_to.frame("iframe1")
+#
+#    wallace = driver.find_element_by_xpath("//div[@id='wallace']")
+#    classname = wallace.get_attribute("class")
+#    assert "gromit" == classname
+
+
+def testShouldReturnTheContentsOfATextAreaAsItsValue(driver, pages):
+    pages.load("formPage.html")
+    value = driver.find_element_by_id("withText").get_attribute("value")
+    assert "Example text" == value
+
+
+def testShouldReturnTheContentsOfATextAreaAsItsValueWhenSetToNonNorminalTrue(driver, pages):
+    pages.load("formPage.html")
+    e = driver.find_element_by_id("withText")
+    driver.execute_script("arguments[0].value = 'tRuE'", e)
+    value = e.get_attribute("value")
+    assert "tRuE" == value
+
+
+def testShouldTreatReadonlyAsAValue(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element_by_name("readonly")
+    readOnlyAttribute = element.get_attribute("readonly")
+
+    textInput = driver.find_element_by_name("x")
+    notReadOnly = textInput.get_attribute("readonly")
+
+    assert readOnlyAttribute != notReadOnly
+
+
+def testShouldGetNumericAtribute(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element_by_id("withText")
+    assert "5" == element.get_attribute("rows")
+
+
+def testCanReturnATextApproximationOfTheStyleAttribute(driver, pages):
+    pages.load("javascriptPage.html")
+    style = driver.find_element_by_id("red-item").get_attribute("style")
+    assert "background-color" in style.lower()
+
+
+def testShouldCorrectlyReportValueOfColspan(driver, pages):
+    pages.load("tables.html")
+
+    th1 = driver.find_element_by_id("th1")
+    td2 = driver.find_element_by_id("td2")
+
+    assert "th1" == th1.get_attribute("id")
+    assert "3" == th1.get_attribute("colspan")
+
+    assert "td2" == td2.get_attribute("id")
+    assert "2" == td2.get_attribute("colspan")
+
+
+def testCanRetrieveTheCurrentValueOfATextFormField_textInput(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element_by_id("working")
+    assert "" == element.get_attribute("value")
+    element.send_keys("hello world")
+    assert "hello world" == element.get_attribute("value")
+
+
+def testCanRetrieveTheCurrentValueOfATextFormField_emailInput(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element_by_id("email")
+    assert "" == element.get_attribute("value")
+    element.send_keys("hello@example.com")
+    assert "hello@example.com" == element.get_attribute("value")
+
+
+def testCanRetrieveTheCurrentValueOfATextFormField_textArea(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element_by_id("emptyTextArea")
+    assert "" == element.get_attribute("value")
+    element.send_keys("hello world")
+    assert "hello world" == element.get_attribute("value")
+
+
+def testShouldReturnNullForNonPresentBooleanAttributes(driver, pages):
+    pages.load("booleanAttributes.html")
+    element1 = driver.find_element_by_id("working")
+    assert element1.get_attribute("required") is None
+
+
+@pytest.mark.xfail_ie
+def testShouldReturnTrueForPresentBooleanAttributes(driver, pages):
+    pages.load("booleanAttributes.html")
+    element1 = driver.find_element_by_id("emailRequired")
+    assert "true" == element1.get_attribute("required")
+    element2 = driver.find_element_by_id("emptyTextAreaRequired")
+    assert "true" == element2.get_attribute("required")
+    element3 = driver.find_element_by_id("inputRequired")
+    assert "true" == element3.get_attribute("required")
+    element4 = driver.find_element_by_id("textAreaRequired")
+    assert "true" == element4.get_attribute("required")
+
+
+def tesShouldGetUnicodeCharsFromAttribute(driver, pages):
+    pages.load("formPage.html")
+    title = driver.find_element_by_id("vsearchGadget").get_attribute("title")
+    assert 'Hvad s\xf8ger du?' == title
diff --git a/test/selenium/webdriver/common/element_equality_tests.py b/test/selenium/webdriver/common/element_equality_tests.py
new file mode 100644
index 00000000..da8b3343
--- /dev/null
+++ b/test/selenium/webdriver/common/element_equality_tests.py
@@ -0,0 +1,43 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.common.by import By
+
+
+def testSameElementLookedUpDifferentWaysShouldBeEqual(driver, pages):
+    pages.load("simpleTest.html")
+    body = driver.find_element(By.TAG_NAME, "body")
+    xbody = driver.find_elements(By.XPATH, "//body")[0]
+
+    assert body == xbody
+
+
+def testDifferentElementsAreNotEqual(driver, pages):
+    pages.load("simpleTest.html")
+    body = driver.find_element(By.TAG_NAME, "body")
+    div = driver.find_element(By.TAG_NAME, "div")
+
+    assert body != div
+
+
+def testSameElementsFoundDifferentWaysShouldNotBeDuplicatedInASet(driver, pages):
+    pages.load("simpleTest.html")
+    body = driver.find_element(By.TAG_NAME, "body")
+    xbody = driver.find_elements(By.XPATH, "//body")
+    s = set(xbody)
+    s.add(body)
+    assert 1 == len(s)
diff --git a/test/selenium/webdriver/common/example2.py b/test/selenium/webdriver/common/example2.py
new file mode 100644
index 00000000..33b53dd3
--- /dev/null
+++ b/test/selenium/webdriver/common/example2.py
@@ -0,0 +1,28 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from google_one_box import GoogleOneBox
+
+
+def testSearch(driver):
+    """This example shows how to use the page object pattern.
+
+    For more information about this pattern, see:
+    https://github.com/SeleniumHQ/selenium/wiki/PageObjects"""
+    google = GoogleOneBox(driver, "http://www.google.com")
+    res = google.search_for("cheese")
+    assert res.link_contains_match_for("Wikipedia")
diff --git a/test/selenium/webdriver/common/executing_async_javascript_tests.py b/test/selenium/webdriver/common/executing_async_javascript_tests.py
new file mode 100644
index 00000000..5d960834
--- /dev/null
+++ b/test/selenium/webdriver/common/executing_async_javascript_tests.py
@@ -0,0 +1,213 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver.common.by import By
+from selenium.common.exceptions import WebDriverException
+from selenium.common.exceptions import TimeoutException
+from selenium.webdriver.remote.webelement import WebElement
+
+
+@pytest.fixture(autouse=True)
+def reset_timeouts(driver):
+    driver.set_script_timeout(0)
+    yield
+    driver.set_script_timeout(30)
+
+
+def testShouldNotTimeoutIfCallbackInvokedImmediately(driver, pages):
+    pages.load("ajaxy_page.html")
+    result = driver.execute_async_script("arguments[arguments.length - 1](123);")
+    assert type(result) == int
+    assert 123 == result
+
+
+def testShouldBeAbleToReturnJavascriptPrimitivesFromAsyncScripts_NeitherNoneNorUndefined(driver, pages):
+    pages.load("ajaxy_page.html")
+    assert 123 == driver.execute_async_script("arguments[arguments.length - 1](123);")
+    assert "abc" == driver.execute_async_script("arguments[arguments.length - 1]('abc');")
+    assert not bool(driver.execute_async_script("arguments[arguments.length - 1](false);"))
+    assert bool(driver.execute_async_script("arguments[arguments.length - 1](true);"))
+
+
+def testShouldBeAbleToReturnJavascriptPrimitivesFromAsyncScripts_NullAndUndefined(driver, pages):
+    pages.load("ajaxy_page.html")
+    assert driver.execute_async_script("arguments[arguments.length - 1](null)") is None
+    assert driver.execute_async_script("arguments[arguments.length - 1]()") is None
+
+
+def testShouldBeAbleToReturnAnArrayLiteralFromAnAsyncScript(driver, pages):
+    pages.load("ajaxy_page.html")
+    result = driver.execute_async_script("arguments[arguments.length - 1]([]);")
+    assert "Expected not to be null!", result is not None
+    assert type(result) == list
+    assert len(result) == 0
+
+
+def testShouldBeAbleToReturnAnArrayObjectFromAnAsyncScript(driver, pages):
+    pages.load("ajaxy_page.html")
+    result = driver.execute_async_script("arguments[arguments.length - 1](new Array());")
+    assert "Expected not to be null!", result is not None
+    assert type(result) == list
+    assert len(result) == 0
+
+
+def testShouldBeAbleToReturnArraysOfPrimitivesFromAsyncScripts(driver, pages):
+    pages.load("ajaxy_page.html")
+
+    result = driver.execute_async_script(
+        "arguments[arguments.length - 1]([null, 123, 'abc', true, false]);")
+
+    assert result is not None
+    assert type(result) == list
+    assert not bool(result.pop())
+    assert bool(result.pop())
+    assert "abc" == result.pop()
+    assert 123 == result.pop()
+    assert result.pop() is None
+    assert len(result) == 0
+
+
+def testShouldBeAbleToReturnWebElementsFromAsyncScripts(driver, pages):
+    pages.load("ajaxy_page.html")
+
+    result = driver.execute_async_script("arguments[arguments.length - 1](document.body);")
+    assert isinstance(result, WebElement)
+    assert "body" == result.tag_name.lower()
+
+
+def testShouldBeAbleToReturnArraysOfWebElementsFromAsyncScripts(driver, pages):
+    pages.load("ajaxy_page.html")
+
+    result = driver.execute_async_script(
+        "arguments[arguments.length - 1]([document.body, document.body]);")
+    assert result is not None
+    assert type(result) == list
+
+    list_ = result
+    assert 2 == len(list_)
+    assert isinstance(list_[0], WebElement)
+    assert isinstance(list_[1], WebElement)
+    assert "body" == list_[0].tag_name
+    # assert list_[0] == list_[1]
+
+
+def testShouldTimeoutIfScriptDoesNotInvokeCallback(driver, pages):
+    pages.load("ajaxy_page.html")
+    with pytest.raises(TimeoutException):
+        # Script is expected to be async and explicitly callback, so this should timeout.
+        driver.execute_async_script("return 1 + 2;")
+
+
+def testShouldTimeoutIfScriptDoesNotInvokeCallbackWithAZeroTimeout(driver, pages):
+    pages.load("ajaxy_page.html")
+    with pytest.raises(TimeoutException):
+        driver.execute_async_script("window.setTimeout(function() {}, 0);")
+
+
+@pytest.mark.xfail_marionette
+@pytest.mark.xfail_remote
+def testShouldNotTimeoutIfScriptCallsbackInsideAZeroTimeout(driver, pages):
+    pages.load("ajaxy_page.html")
+    driver.execute_async_script(
+        """var callback = arguments[arguments.length - 1];
+        window.setTimeout(function() { callback(123); }, 0)""")
+
+
+def testShouldTimeoutIfScriptDoesNotInvokeCallbackWithLongTimeout(driver, pages):
+    driver.set_script_timeout(0.5)
+    pages.load("ajaxy_page.html")
+    with pytest.raises(TimeoutException):
+        driver.execute_async_script(
+            """var callback = arguments[arguments.length - 1];
+            window.setTimeout(callback, 1500);""")
+
+
+def testShouldDetectPageLoadsWhileWaitingOnAnAsyncScriptAndReturnAnError(driver, pages):
+    pages.load("ajaxy_page.html")
+    driver.set_script_timeout(0.1)
+    with pytest.raises(WebDriverException):
+        url = pages.url("dynamic.html")
+        driver.execute_async_script("window.location = '{0}';".format(url))
+
+
+def testShouldCatchErrorsWhenExecutingInitialScript(driver, pages):
+    pages.load("ajaxy_page.html")
+    with pytest.raises(WebDriverException):
+        driver.execute_async_script("throw Error('you should catch this!');")
+
+
+def testShouldBeAbleToExecuteAsynchronousScripts(driver, pages):
+    pages.load("ajaxy_page.html")
+
+    typer = driver.find_element(by=By.NAME, value="typer")
+    typer.send_keys("bob")
+    assert "bob" == typer.get_attribute("value")
+
+    driver.find_element(by=By.ID, value="red").click()
+    driver.find_element(by=By.NAME, value="submit").click()
+
+    assert 1 == len(driver.find_elements(by=By.TAG_NAME, value='div')), \
+        "There should only be 1 DIV at this point, which is used for the butter message"
+    driver.set_script_timeout(10)
+    text = driver.execute_async_script(
+        """var callback = arguments[arguments.length - 1];
+        window.registerListener(arguments[arguments.length - 1]);""")
+    assert "bob" == text
+    assert "" == typer.get_attribute("value")
+
+    assert 2 == len(driver.find_elements(by=By.TAG_NAME, value='div')), \
+        "There should be 1 DIV (for the butter message) + 1 DIV (for the new label)"
+
+
+def testShouldBeAbleToPassMultipleArgumentsToAsyncScripts(driver, pages):
+    pages.load("ajaxy_page.html")
+    result = driver.execute_async_script("""
+        arguments[arguments.length - 1](arguments[0] + arguments[1]);""", 1, 2)
+    assert 3 == result
+
+# TODO DavidBurns Disabled till Java WebServer is used
+# def testShouldBeAbleToMakeXMLHttpRequestsAndWaitForTheResponse(driver, pages):
+#    script = """
+#        var url = arguments[0];
+#        var callback = arguments[arguments.length - 1];
+#        // Adapted from http://www.quirksmode.org/js/xmlhttp.html
+#        var XMLHttpFactories = [
+#          function () return new XMLHttpRequest(),
+#          function () return new ActiveXObject('Msxml2.XMLHTTP'),
+#          function () return new ActiveXObject('Msxml3.XMLHTTP'),
+#          function () return new ActiveXObject('Microsoft.XMLHTTP')
+#        ];
+#        var xhr = false;
+#        while (!xhr && XMLHttpFactories.length)
+#          try{
+#            xhr = XMLHttpFactories.shift().call();
+#           }catch (e)
+#
+#        if (!xhr) throw Error('unable to create XHR object');
+#        xhr.open('GET', url, true);
+#        xhr.onreadystatechange = function()
+#          if (xhr.readyState == 4) callback(xhr.responseText);
+#
+#        xhr.send('');""" # empty string to stop firefox 3 from choking
+#
+#    pages.load("ajaxy_page.html")
+#    driver.set_script_timeout(3)
+#    response = driver.execute_async_script(script, pages.sleepingPage + "?time=2")
+#    htm = "<html><head><title>Done</title></head><body>Slept for 2s</body></html>"
+#    assert response.strip() == htm
diff --git a/test/selenium/webdriver/common/executing_javascript_tests.py b/test/selenium/webdriver/common/executing_javascript_tests.py
new file mode 100644
index 00000000..aa19183d
--- /dev/null
+++ b/test/selenium/webdriver/common/executing_javascript_tests.py
@@ -0,0 +1,282 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.remote.webelement import WebElement
+
+try:
+    str = unicode
+except NameError:
+    pass
+
+
+def testShouldBeAbleToExecuteSimpleJavascriptAndReturnAString(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    result = driver.execute_script("return document.title")
+
+    assert type(result) == str, "The type of the result is %s" % type(result)
+    assert "XHTML Test Page" == result
+
+
+def testShouldBeAbleToExecuteSimpleJavascriptAndReturnAnInteger(driver, pages):
+    pages.load("nestedElements.html")
+    result = driver.execute_script("return document.getElementsByName('checky').length")
+
+    assert type(result) == int
+    assert int(result) > 1
+
+
+def testShouldBeAbleToExecuteSimpleJavascriptAndReturnAWebElement(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    result = driver.execute_script("return document.getElementById('id1')")
+
+    assert result is not None
+    assert isinstance(result, WebElement)
+    assert "a" == result.tag_name.lower()
+
+
+def testShouldBeAbleToExecuteSimpleJavascriptAndReturnAListOfWebElements(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    result = driver.execute_script("return document.querySelectorAll('div.navigation a')")
+
+    assert result is not None
+    assert isinstance(result, list)
+    assert all(isinstance(item, WebElement) for item in result)
+    assert all('a' == item.tag_name.lower() for item in result)
+
+
+def testShouldBeAbleToExecuteSimpleJavascriptAndReturnWebElementsInsideAList(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    result = driver.execute_script("return [document.body]")
+
+    assert result is not None
+    assert isinstance(result, list)
+    assert isinstance(result[0], WebElement)
+
+
+def testShouldBeAbleToExecuteSimpleJavascriptAndReturnWebElementsInsideANestedList(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    result = driver.execute_script("return [document.body, [document.getElementById('id1')]]")
+
+    assert result is not None
+    assert isinstance(result, list)
+    assert isinstance(result[0], WebElement)
+    assert isinstance(result[1][0], WebElement)
+
+
+def testShouldBeAbleToExecuteSimpleJavascriptAndReturnWebElementsInsideADict(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    result = driver.execute_script("return {el1: document.body}")
+
+    assert result is not None
+    assert isinstance(result, dict)
+    assert isinstance(result.get('el1'), WebElement)
+
+
+def testShouldBeAbleToExecuteSimpleJavascriptAndReturnWebElementsInsideANestedDict(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    result = driver.execute_script("return {el1: document.body, "
+                                   "nested: {el2: document.getElementById('id1')}}")
+
+    assert result is not None
+    assert isinstance(result, dict)
+    assert isinstance(result.get('el1'), WebElement)
+    assert isinstance(result.get('nested').get('el2'), WebElement)
+
+
+def testShouldBeAbleToExecuteSimpleJavascriptAndReturnWebElementsInsideAListInsideADict(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    result = driver.execute_script("return {el1: [document.body]}")
+
+    assert result is not None
+    assert isinstance(result, dict)
+    assert isinstance(result.get('el1'), list)
+    assert isinstance(result.get('el1')[0], WebElement)
+
+
+def testShouldBeAbleToExecuteSimpleJavascriptAndReturnABoolean(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    result = driver.execute_script("return true")
+
+    assert result is not None
+    assert type(result) == bool
+    assert bool(result)
+
+
+def testShouldBeAbleToExecuteSimpleJavascriptAndAStringsArray(driver, pages):
+    pages.load("javascriptPage.html")
+    expectedResult = []
+    expectedResult.append("zero")
+    expectedResult.append("one")
+    expectedResult.append("two")
+    result = driver.execute_script(
+        "return ['zero', 'one', 'two']")
+
+    assert expectedResult == result
+
+
+def testShouldBeAbleToExecuteSimpleJavascriptAndReturnAnArray(driver, pages):
+    pages.load("javascriptPage.html")
+    expectedResult = []
+    expectedResult.append("zero")
+    subList = []
+    subList.append(True)
+    subList.append(False)
+    expectedResult.append(subList)
+    result = driver.execute_script("return ['zero', [true, false]]")
+    assert result is not None
+    assert type(result) == list
+    assert expectedResult == result
+
+
+def testPassingAndReturningAnIntShouldReturnAWholeNumber(driver, pages):
+    pages.load("javascriptPage.html")
+    expectedResult = 1
+    result = driver.execute_script("return arguments[0]", expectedResult)
+    assert type(result) == int
+    assert expectedResult == result
+
+
+def testPassingAndReturningADoubleShouldReturnADecimal(driver, pages):
+    pages.load("javascriptPage.html")
+    expectedResult = 1.2
+    result = driver.execute_script("return arguments[0]", expectedResult)
+    assert type(result) == float
+    assert expectedResult == result
+
+
+def testShouldThrowAnExceptionWhenTheJavascriptIsBad(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(WebDriverException):
+        driver.execute_script("return squiggle()")
+
+
+def testShouldBeAbleToCallFunctionsDefinedOnThePage(driver, pages):
+    pages.load("javascriptPage.html")
+    driver.execute_script("displayMessage('I like cheese')")
+    text = driver.find_element_by_id("result").text
+    assert "I like cheese" == text.strip()
+
+
+def testShouldBeAbleToPassAStringAnAsArgument(driver, pages):
+    pages.load("javascriptPage.html")
+    value = driver.execute_script(
+        "return arguments[0] == 'fish' ? 'fish' : 'not fish'", "fish")
+    assert "fish" == value
+
+
+def testShouldBeAbleToPassABooleanAnAsArgument(driver, pages):
+    pages.load("javascriptPage.html")
+    value = bool(driver.execute_script("return arguments[0] == true", True))
+    assert value
+
+
+def testShouldBeAbleToPassANumberAnAsArgument(driver, pages):
+    pages.load("javascriptPage.html")
+    value = bool(driver.execute_script("return arguments[0] == 1 ? true : false", 1))
+    assert value
+
+
+def testShouldBeAbleToPassAWebElementAsArgument(driver, pages):
+    pages.load("javascriptPage.html")
+    button = driver.find_element_by_id("plainButton")
+    value = driver.execute_script(
+        "arguments[0]['flibble'] = arguments[0].getAttribute('id'); return arguments[0]['flibble']",
+        button)
+    assert "plainButton" == value
+
+
+def testShouldBeAbleToPassAnArrayAsArgument(driver, pages):
+    pages.load("javascriptPage.html")
+    array = ["zero", 1, True, 3.14159]
+    length = int(driver.execute_script("return arguments[0].length", array))
+    assert len(array) == length
+
+
+def testShouldBeAbleToPassACollectionAsArgument(driver, pages):
+    pages.load("javascriptPage.html")
+    collection = []
+    collection.append("Cheddar")
+    collection.append("Brie")
+    collection.append(7)
+    length = int(driver.execute_script("return arguments[0].length", collection))
+    assert len(collection) == length
+
+    collection = []
+    collection.append("Gouda")
+    collection.append("Stilton")
+    collection.append("Stilton")
+    collection.append(True)
+    length = int(driver.execute_script("return arguments[0].length", collection))
+    assert len(collection) == length
+
+
+def testShouldThrowAnExceptionIfAnArgumentIsNotValid(driver, pages):
+    pages.load("javascriptPage.html")
+    with pytest.raises(Exception):
+        driver.execute_script("return arguments[0]", driver)
+
+
+def testShouldBeAbleToPassInMoreThanOneArgument(driver, pages):
+    pages.load("javascriptPage.html")
+    result = driver.execute_script("return arguments[0] + arguments[1]", "one", "two")
+    assert "onetwo" == result
+
+
+def testJavascriptStringHandlingShouldWorkAsExpected(driver, pages):
+    pages.load("javascriptPage.html")
+    value = driver.execute_script("return ''")
+    assert "" == value
+
+    value = driver.execute_script("return undefined")
+    assert value is None
+
+    value = driver.execute_script("return ' '")
+    assert " " == value
+
+
+def testShouldBeAbleToCreateAPersistentValue(driver, pages):
+    pages.load("formPage.html")
+    driver.execute_script("document.alerts = []")
+    driver.execute_script("document.alerts.push('hello world')")
+    text = driver.execute_script("return document.alerts.shift()")
+    assert "hello world" == text
+
+
+def testCanPassADictionaryAsAParameter(driver, pages):
+    pages.load("simpleTest.html")
+    nums = [1, 2]
+    args = {"bar": "test", "foo": nums}
+    res = driver.execute_script("return arguments[0]['foo'][1]", args)
+    assert 2 == res
+
+
+def testCanPassANone(driver, pages):
+    pages.load("simpleTest.html")
+    res = driver.execute_script("return arguments[0] === null", None)
+    assert res
diff --git a/test/selenium/webdriver/common/form_handling_tests.py b/test/selenium/webdriver/common/form_handling_tests.py
new file mode 100644
index 00000000..deeacb9c
--- /dev/null
+++ b/test/selenium/webdriver/common/form_handling_tests.py
@@ -0,0 +1,235 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import NoSuchElementException
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def testShouldClickOnSubmitInputElements(driver, pages):
+    pages.load("formPage.html")
+    driver.find_element_by_id("submitButton").click()
+    WebDriverWait(driver, 3).until(EC.title_is("We Arrive Here"))
+
+
+def testClickingOnUnclickableElementsDoesNothing(driver, pages):
+    pages.load("formPage.html")
+    driver.find_element_by_xpath("//body").click()
+
+
+def testShouldBeAbleToClickImageButtons(driver, pages):
+    pages.load("formPage.html")
+    driver.find_element_by_id("imageButton").click()
+    WebDriverWait(driver, 3).until(EC.title_is("We Arrive Here"))
+
+
+def testShouldBeAbleToSubmitForms(driver, pages):
+    pages.load("formPage.html")
+    driver.find_element_by_name("login").submit()
+    WebDriverWait(driver, 3).until(EC.title_is("We Arrive Here"))
+
+
+def testShouldSubmitAFormWhenAnyInputElementWithinThatFormIsSubmitted(driver, pages):
+    pages.load("formPage.html")
+    driver.find_element_by_id("checky").submit()
+    WebDriverWait(driver, 3).until(EC.title_is("We Arrive Here"))
+
+
+def testShouldSubmitAFormWhenAnyElementWihinThatFormIsSubmitted(driver, pages):
+    pages.load("formPage.html")
+    driver.find_element_by_xpath("//form/p").submit()
+    WebDriverWait(driver, 5).until(EC.title_is("We Arrive Here"))
+
+
+def testShouldNotBeAbleToSubmitAFormThatDoesNotExist(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(NoSuchElementException):
+        driver.find_element_by_name("there is no spoon").submit()
+
+
+def testShouldBeAbleToEnterTextIntoATextAreaBySettingItsValue(driver, pages):
+    pages.load("javascriptPage.html")
+    textarea = driver.find_element_by_id("keyUpArea")
+    cheesey = "Brie and cheddar"
+    textarea.send_keys(cheesey)
+    assert textarea.get_attribute("value") == cheesey
+
+
+def testShouldEnterDataIntoFormFields(driver, pages):
+    pages.load("xhtmlTest.html")
+    element = driver.find_element_by_xpath("//form[@name='someForm']/input[@id='username']")
+    originalValue = element.get_attribute("value")
+    assert originalValue == "change"
+
+    element.clear()
+    element.send_keys("some text")
+
+    element = driver.find_element_by_xpath("//form[@name='someForm']/input[@id='username']")
+    newFormValue = element.get_attribute("value")
+    assert newFormValue == "some text"
+
+
+def testShouldBeAbleToSelectACheckBox(driver, pages):
+    pages.load("formPage.html")
+    checkbox = driver.find_element_by_id("checky")
+    assert checkbox.is_selected() is False
+    checkbox.click()
+    assert checkbox.is_selected() is True
+    checkbox.click()
+    assert checkbox.is_selected() is False
+
+
+def testShouldToggleTheCheckedStateOfACheckbox(driver, pages):
+    pages.load("formPage.html")
+    checkbox = driver.find_element_by_id("checky")
+    assert checkbox.is_selected() is False
+    checkbox.click()
+    assert checkbox.is_selected() is True
+    checkbox.click()
+    assert checkbox.is_selected() is False
+
+
+def testTogglingACheckboxShouldReturnItsCurrentState(driver, pages):
+    pages.load("formPage.html")
+    checkbox = driver.find_element_by_id("checky")
+    assert checkbox.is_selected() is False
+    checkbox.click()
+    assert checkbox.is_selected() is True
+    checkbox.click()
+    assert checkbox.is_selected() is False
+
+
+def testShouldBeAbleToSelectARadioButton(driver, pages):
+    pages.load("formPage.html")
+    radioButton = driver.find_element_by_id("peas")
+    assert radioButton.is_selected() is False
+    radioButton.click()
+    assert radioButton.is_selected() is True
+
+
+def testShouldBeAbleToSelectARadioButtonByClickingOnIt(driver, pages):
+    pages.load("formPage.html")
+    radioButton = driver.find_element_by_id("peas")
+    assert radioButton.is_selected() is False
+    radioButton.click()
+    assert radioButton.is_selected() is True
+
+
+def testShouldReturnStateOfRadioButtonsBeforeInteration(driver, pages):
+    pages.load("formPage.html")
+    radioButton = driver.find_element_by_id("cheese_and_peas")
+    assert radioButton.is_selected() is True
+
+    radioButton = driver.find_element_by_id("cheese")
+    assert radioButton.is_selected() is False
+
+# [ExpectedException(typeof(NotImplementedException))]
+# def testShouldThrowAnExceptionWhenTogglingTheStateOfARadioButton(driver, pages):
+#    pages.load("formPage.html")
+#    radioButton = driver.find_element_by_id("cheese"))
+#    radioButton.click()
+
+# [IgnoreBrowser(Browser.IE, "IE allows toggling of an option not in a multiselect")]
+# [ExpectedException(typeof(NotImplementedException))]
+# def testTogglingAnOptionShouldThrowAnExceptionIfTheOptionIsNotInAMultiSelect(driver, pages):
+#    pages.load("formPage.html")
+#    select = driver.find_element_by_name("selectomatic"))
+#    option = select.find_elements_by_tag_name("option"))[0]
+#    option.click()
+
+
+def testTogglingAnOptionShouldToggleOptionsInAMultiSelect(driver, pages):
+    pages.load("formPage.html")
+
+    select = driver.find_element_by_name("multi")
+    option = select.find_elements_by_tag_name("option")[0]
+
+    selected = option.is_selected()
+    option.click()
+    assert not selected == option.is_selected()
+
+    option.click()
+    assert selected == option.is_selected()
+
+
+def testShouldThrowAnExceptionWhenSelectingAnUnselectableElement(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element_by_xpath("//title")
+    with pytest.raises(WebDriverException):
+        element.click()
+
+
+def testSendingKeyboardEventsShouldAppendTextInInputs(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element_by_id("working")
+    element.send_keys("Some")
+    value = element.get_attribute("value")
+    assert value == "Some"
+
+    element.send_keys(" text")
+    value = element.get_attribute("value")
+    assert value == "Some text"
+
+
+def testShouldBeAbleToClearTextFromInputElements(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element_by_id("working")
+    element.send_keys("Some text")
+    value = element.get_attribute("value")
+    assert len(value) > 0
+
+    element.clear()
+    value = element.get_attribute("value")
+    assert len(value) == 0
+
+
+def testEmptyTextBoxesShouldReturnAnEmptyStringNotNull(driver, pages):
+    pages.load("formPage.html")
+    emptyTextBox = driver.find_element_by_id("working")
+    assert emptyTextBox.get_attribute("value") == ""
+
+    emptyTextArea = driver.find_element_by_id("emptyTextArea")
+    assert emptyTextArea.get_attribute("value") == ""
+
+
+def testShouldBeAbleToClearTextFromTextAreas(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element_by_id("withText")
+    element.send_keys("Some text")
+    value = element.get_attribute("value")
+    assert len(value) > 0
+
+    element.clear()
+    value = element.get_attribute("value")
+    assert len(value) == 0
+
+
+def testRadioShouldNotBeSelectedAfterSelectingSibling(driver, pages):
+    pages.load("formPage.html")
+    cheese = driver.find_element_by_id("cheese")
+    peas = driver.find_element_by_id("peas")
+
+    cheese.click()
+    assert cheese.is_selected() is True
+    assert peas.is_selected() is False
+
+    peas.click()
+    assert cheese.is_selected() is False
+    assert peas.is_selected() is True
diff --git a/test/selenium/webdriver/common/frame_switching_tests.py b/test/selenium/webdriver/common/frame_switching_tests.py
new file mode 100644
index 00000000..4c7c9a62
--- /dev/null
+++ b/test/selenium/webdriver/common/frame_switching_tests.py
@@ -0,0 +1,427 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import (
+    NoSuchElementException,
+    NoSuchFrameException,
+    WebDriverException)
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+
+
+# ----------------------------------------------------------------------------------------------
+#
+# Tests that WebDriver doesn't do anything fishy when it navigates to a page with frames.
+#
+# ----------------------------------------------------------------------------------------------
+
+
+@pytest.fixture(autouse=True)
+def restore_default_context(driver):
+    yield
+    driver.switch_to.default_content()
+
+
+def testShouldAlwaysFocusOnTheTopMostFrameAfterANavigationEvent(driver, pages):
+    pages.load("frameset.html")
+    driver.find_element(By.TAG_NAME, "frameset")  # Test passes if this does not throw.
+
+
+def testShouldNotAutomaticallySwitchFocusToAnIFrameWhenAPageContainingThemIsLoaded(driver, pages):
+    pages.load("iframes.html")
+    driver.find_element(By.ID, "iframe_page_heading")
+
+
+def testShouldOpenPageWithBrokenFrameset(driver, pages):
+    pages.load("framesetPage3.html")
+
+    frame1 = driver.find_element(By.ID, "first")
+    driver.switch_to.frame(frame1)
+    driver.switch_to.default_content()
+
+    frame2 = driver.find_element(By.ID, "second")
+    driver.switch_to.frame(frame2)  # IE9 can not switch to this broken frame - it has no window.
+
+# ----------------------------------------------------------------------------------------------
+#
+# Tests that WebDriver can switch to frames as expected.
+#
+# ----------------------------------------------------------------------------------------------
+
+
+def testShouldBeAbleToSwitchToAFrameByItsIndex(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame(1)
+    assert driver.find_element(By.ID, "pageNumber").text == "2"
+
+
+def testShouldBeAbleToSwitchToAnIframeByItsIndex(driver, pages):
+    pages.load("iframes.html")
+    driver.switch_to.frame(0)
+    assert driver.find_element(By.NAME, "id-name1").get_attribute("value") == "name"
+
+
+def testShouldBeAbleToSwitchToAFrameByItsName(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame("fourth")
+    assert driver.find_element(By.TAG_NAME, "frame").get_attribute("name") == "child1"
+
+
+def testShouldBeAbleToSwitchToAnIframeByItsName(driver, pages):
+    pages.load("iframes.html")
+    driver.switch_to.frame("iframe1-name")
+    assert driver.find_element(By.NAME, "id-name1").get_attribute("value") == "name"
+
+
+def testShouldBeAbleToSwitchToAFrameByItsID(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame("fifth")
+    assert driver.find_element(By.NAME, "windowOne").text == "Open new window"
+
+
+def testShouldBeAbleToSwitchToAnIframeByItsID(driver, pages):
+    pages.load("iframes.html")
+    driver.switch_to.frame("iframe1")
+    assert driver.find_element(By.NAME, "id-name1").get_attribute("value") == "name"
+
+
+def testShouldBeAbleToSwitchToFrameWithNameContainingDot(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame("sixth.iframe1")
+    assert "Page number 3" in driver.find_element(By.TAG_NAME, "body").text
+
+
+def testShouldBeAbleToSwitchToAFrameUsingAPreviouslyLocatedWebElement(driver, pages):
+    pages.load("frameset.html")
+    frame = driver.find_element(By.TAG_NAME, "frame")
+    driver.switch_to.frame(frame)
+    assert driver.find_element(By.ID, "pageNumber").text == "1"
+
+
+def testShouldBeAbleToSwitchToAnIFrameUsingAPreviouslyLocatedWebElement(driver, pages):
+    pages.load("iframes.html")
+    frame = driver.find_element(By.TAG_NAME, "iframe")
+    driver.switch_to.frame(frame)
+
+    element = driver.find_element(By.NAME, "id-name1")
+    assert element.get_attribute("value") == "name"
+
+
+def testShouldEnsureElementIsAFrameBeforeSwitching(driver, pages):
+    pages.load("frameset.html")
+    frame = driver.find_element(By.TAG_NAME, "frameset")
+    with pytest.raises(NoSuchFrameException):
+        driver.switch_to.frame(frame)
+
+
+def testFrameSearchesShouldBeRelativeToTheCurrentlySelectedFrame(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame("second")
+    assert driver.find_element(By.ID, "pageNumber").text == "2"
+
+    with pytest.raises(NoSuchElementException):
+        driver.switch_to.frame(driver.find_element_by_name("third"))
+
+    driver.switch_to.default_content()
+    driver.switch_to.frame(driver.find_element_by_name("third"))
+
+    with pytest.raises(NoSuchFrameException):
+        driver.switch_to.frame("second")
+
+    driver.switch_to.default_content()
+    driver.switch_to.frame(driver.find_element_by_name("second"))
+    assert driver.find_element(By.ID, "pageNumber").text == "2"
+
+
+def testShouldSelectChildFramesByChainedCalls(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame(driver.find_element_by_name("fourth"))
+    driver.switch_to.frame(driver.find_element_by_name("child2"))
+    assert driver.find_element(By.ID, "pageNumber").text == "11"
+
+
+def testShouldThrowFrameNotFoundExceptionLookingUpSubFramesWithSuperFrameNames(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame(driver.find_element_by_name("fourth"))
+    with pytest.raises(NoSuchElementException):
+        driver.switch_to.frame(driver.find_element_by_name("second"))
+
+
+def testShouldThrowAnExceptionWhenAFrameCannotBeFound(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchElementException):
+        driver.switch_to.frame(driver.find_element_by_name("Nothing here"))
+
+
+def testShouldThrowAnExceptionWhenAFrameCannotBeFoundByIndex(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchFrameException):
+        driver.switch_to.frame(27)
+
+
+def testShouldBeAbleToSwitchToParentFrame(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame(driver.find_element_by_name("fourth"))
+    driver.switch_to.parent_frame()
+    driver.switch_to.frame(driver.find_element_by_name("first"))
+    assert driver.find_element(By.ID, "pageNumber").text == "1"
+
+
+def testShouldBeAbleToSwitchToParentFrameFromASecondLevelFrame(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame(driver.find_element_by_name("fourth"))
+    driver.switch_to.frame(driver.find_element_by_name("child1"))
+    driver.switch_to.parent_frame()
+    driver.switch_to.frame(driver.find_element_by_name("child2"))
+    assert driver.find_element(By.ID, "pageNumber").text == "11"
+
+
+def testSwitchingToParentFrameFromDefaultContextIsNoOp(driver, pages):
+    pages.load("xhtmlTest.html")
+    driver.switch_to.parent_frame()
+    assert driver.title == "XHTML Test Page"
+
+
+def testShouldBeAbleToSwitchToParentFromAnIframe(driver, pages):
+    pages.load("iframes.html")
+    driver.switch_to.frame(0)
+    driver.switch_to.parent_frame()
+    driver.find_element(By.ID, "iframe_page_heading")
+
+# ----------------------------------------------------------------------------------------------
+#
+# General frame handling behavior tests
+#
+# ----------------------------------------------------------------------------------------------
+
+
+def testShouldContinueToReferToTheSameFrameOnceItHasBeenSelected(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame(2)
+    checkbox = driver.find_element(By.XPATH, "//input[@name='checky']")
+    checkbox.click()
+    checkbox.submit()
+
+    # TODO(simon): this should not be needed, and is only here because IE's submit returns too
+    # soon.
+
+    WebDriverWait(driver, 3).until(EC.text_to_be_present_in_element((By.XPATH, '//p'), 'Success!'))
+
+
+@pytest.mark.xfail_marionette(raises=WebDriverException,
+                              reason='https://github.com/mozilla/geckodriver/issues/610')
+@pytest.mark.xfail_remote(raises=WebDriverException,
+                          reason='https://github.com/mozilla/geckodriver/issues/610')
+def testShouldFocusOnTheReplacementWhenAFrameFollowsALinkToA_TopTargetedPage(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame(0)
+    driver.find_element(By.LINK_TEXT, "top").click()
+
+    expectedTitle = "XHTML Test Page"
+
+    WebDriverWait(driver, 3).until(EC.title_is(expectedTitle))
+    WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "only-exists-on-xhtmltest")))
+
+
+def testShouldAllowAUserToSwitchFromAnIframeBackToTheMainContentOfThePage(driver, pages):
+    pages.load("iframes.html")
+    driver.switch_to.frame(0)
+    driver.switch_to.default_content()
+    driver.find_element(By.ID, "iframe_page_heading")
+
+
+def testShouldAllowTheUserToSwitchToAnIFrameAndRemainFocusedOnIt(driver, pages):
+    pages.load("iframes.html")
+    driver.switch_to.frame(0)
+    driver.find_element(By.ID, "submitButton").click()
+    assert getTextOfGreetingElement(driver) == "Success!"
+
+
+def getTextOfGreetingElement(driver):
+    return WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "greeting"))).text
+
+
+def testShouldBeAbleToClickInAFrame(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame("third")
+
+    # This should replace frame "third" ...
+    driver.find_element(By.ID, "submitButton").click()
+    # driver should still be focused on frame "third" ...
+    assert getTextOfGreetingElement(driver) == "Success!"
+    # Make sure it was really frame "third" which was replaced ...
+    driver.switch_to.default_content()
+    driver.switch_to.frame("third")
+    assert getTextOfGreetingElement(driver) == "Success!"
+
+
+def testShouldBeAbleToClickInAFrameThatRewritesTopWindowLocation(driver, pages):
+    pages.load("click_tests/issue5237.html")
+    driver.switch_to.frame(driver.find_element_by_id("search"))
+    driver.find_element(By.ID, "submit").click()
+    driver.switch_to.default_content()
+    WebDriverWait(driver, 3).until(EC.title_is("Target page for issue 5237"))
+
+
+def testShouldBeAbleToClickInASubFrame(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame(driver.find_element_by_id("sixth"))
+    driver.switch_to.frame(driver.find_element_by_id("iframe1"))
+
+    # This should replace frame "iframe1" inside frame "sixth" ...
+    driver.find_element(By.ID, "submitButton").click()
+    # driver should still be focused on frame "iframe1" inside frame "sixth" ...
+    assert getTextOfGreetingElement(driver), "Success!"
+    # Make sure it was really frame "iframe1" inside frame "sixth" which was replaced ...
+    driver.switch_to.default_content()
+    driver.switch_to.frame(driver.find_element_by_id("sixth"))
+    driver.switch_to.frame(driver.find_element_by_id("iframe1"))
+    assert driver.find_element(By.ID, "greeting").text == "Success!"
+
+
+def testShouldBeAbleToFindElementsInIframesByXPath(driver, pages):
+    pages.load("iframes.html")
+    driver.switch_to.frame(driver.find_element_by_id("iframe1"))
+    element = driver.find_element(By.XPATH, "//*[@id = 'changeme']")
+    assert element is not None
+
+
+def testGetCurrentUrlReturnsTopLevelBrowsingContextUrl(driver, pages):
+    pages.load("frameset.html")
+    assert "frameset.html" in driver.current_url
+    driver.switch_to.frame(driver.find_element_by_name("second"))
+    assert "frameset.html" in driver.current_url
+
+
+def testGetCurrentUrlReturnsTopLevelBrowsingContextUrlForIframes(driver, pages):
+    pages.load("iframes.html")
+    assert "iframes.html" in driver.current_url
+    driver.switch_to.frame(driver.find_element_by_id("iframe1"))
+    assert "iframes.html" in driver.current_url
+
+
+def testShouldBeAbleToSwitchToTheTopIfTheFrameIsDeletedFromUnderUs(driver, pages):
+    pages.load("frame_switching_tests/deletingFrame.html")
+    driver.switch_to.frame(driver.find_element_by_id("iframe1"))
+
+    killIframe = driver.find_element(By.ID, "killIframe")
+    killIframe.click()
+    driver.switch_to.default_content()
+    WebDriverWait(driver, 3).until_not(
+        EC.presence_of_element_located((By.ID, "iframe1")))
+
+    addIFrame = driver.find_element(By.ID, "addBackFrame")
+    addIFrame.click()
+    WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "iframe1")))
+    driver.switch_to.frame(driver.find_element_by_id("iframe1"))
+    WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "success")))
+
+
+def testShouldBeAbleToSwitchToTheTopIfTheFrameIsDeletedFromUnderUsWithFrameIndex(driver, pages):
+    pages.load("frame_switching_tests/deletingFrame.html")
+    iframe = 0
+    WebDriverWait(driver, 3).until(EC.frame_to_be_available_and_switch_to_it(iframe))
+    # we should be in the frame now
+    killIframe = driver.find_element(By.ID, "killIframe")
+    killIframe.click()
+    driver.switch_to.default_content()
+
+    addIFrame = driver.find_element(By.ID, "addBackFrame")
+    addIFrame.click()
+    WebDriverWait(driver, 3).until(EC.frame_to_be_available_and_switch_to_it(iframe))
+    WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "success")))
+
+
+def testShouldBeAbleToSwitchToTheTopIfTheFrameIsDeletedFromUnderUsWithWebelement(driver, pages):
+    pages.load("frame_switching_tests/deletingFrame.html")
+    iframe = driver.find_element(By.ID, "iframe1")
+    WebDriverWait(driver, 3).until(EC.frame_to_be_available_and_switch_to_it(iframe))
+    # we should be in the frame now
+    killIframe = driver.find_element(By.ID, "killIframe")
+    killIframe.click()
+    driver.switch_to.default_content()
+
+    addIFrame = driver.find_element(By.ID, "addBackFrame")
+    addIFrame.click()
+
+    iframe = driver.find_element(By.ID, "iframe1")
+    WebDriverWait(driver, 3).until(EC.frame_to_be_available_and_switch_to_it(iframe))
+    WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "success")))
+
+
+@pytest.mark.xfail_chrome(raises=NoSuchElementException)
+@pytest.mark.xfail_marionette(raises=WebDriverException,
+                              reason='https://github.com/mozilla/geckodriver/issues/614')
+@pytest.mark.xfail_remote(raises=WebDriverException,
+                          reason='https://github.com/mozilla/geckodriver/issues/614')
+@pytest.mark.xfail_webkitgtk(raises=NoSuchElementException)
+def testShouldNotBeAbleToDoAnythingTheFrameIsDeletedFromUnderUs(driver, pages):
+    pages.load("frame_switching_tests/deletingFrame.html")
+    driver.switch_to.frame(driver.find_element_by_id("iframe1"))
+
+    killIframe = driver.find_element(By.ID, "killIframe")
+    killIframe.click()
+
+    with pytest.raises(NoSuchFrameException):
+        driver.find_element(By.ID, "killIframe").click()
+
+
+def testShouldReturnWindowTitleInAFrameset(driver, pages):
+    pages.load("frameset.html")
+    driver.switch_to.frame(driver.find_element_by_name("third"))
+    assert "Unique title" == driver.title
+
+
+def testJavaScriptShouldExecuteInTheContextOfTheCurrentFrame(driver, pages):
+    pages.load("frameset.html")
+    assert driver.execute_script("return window == window.top")
+    driver.switch_to.frame(driver.find_element(By.NAME, "third"))
+    assert driver.execute_script("return window != window.top")
+
+
+def testShouldNotSwitchMagicallyToTheTopWindow(driver, pages):
+    pages.load("frame_switching_tests/bug4876.html")
+    driver.switch_to.frame(0)
+    WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "inputText")))
+
+    for i in range(20):
+        try:
+            input = WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "inputText")))
+            submit = WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "submitButton")))
+            input.clear()
+            import random
+            input.send_keys("rand%s" % int(random.random()))
+            submit.click()
+        finally:
+            url = driver.execute_script("return window.location.href")
+        # IE6 and Chrome add "?"-symbol to the end of the URL
+    if (url.endswith("?")):
+        url = url[:len(url) - 1]
+
+    assert pages.url("frame_switching_tests/bug4876_iframe.html") == url
+
+
+def testGetShouldSwitchToDefaultContext(driver, pages):
+    pages.load("iframes.html")
+    driver.find_element(By.ID, "iframe1")
+    driver.switch_to.frame(driver.find_element(By.ID, "iframe1"))
+    driver.find_element(By.ID, "cheese")  # Found on formPage.html but not on iframes.html.
+    pages.load("iframes.html")  # This must effectively switch_to.default_content(), too.
+    driver.find_element(By.ID, "iframe1")
diff --git a/test/selenium/webdriver/common/google_one_box.py b/test/selenium/webdriver/common/google_one_box.py
new file mode 100644
index 00000000..fe57a184
--- /dev/null
+++ b/test/selenium/webdriver/common/google_one_box.py
@@ -0,0 +1,45 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.common.exceptions import NoSuchElementException
+from results_page import ResultsPage
+from page_loader import require_loaded
+
+
+class GoogleOneBox(object):
+    """This class models a page that has a google search bar."""
+
+    def __init__(self, driver, url):
+        self._driver = driver
+        self._url = url
+
+    def is_loaded(self):
+        try:
+            self._driver.find_element_by_name("q")
+            return True
+        except NoSuchElementException:
+            return False
+
+    def load(self):
+        self._driver.get(self._url)
+
+    @require_loaded
+    def search_for(self, search_term):
+        element = self._driver.find_element_by_name("q")
+        element.send_keys(search_term)
+        element.submit()
+        return ResultsPage(self._driver)
diff --git a/test/selenium/webdriver/common/implicit_waits_tests.py b/test/selenium/webdriver/common/implicit_waits_tests.py
new file mode 100644
index 00000000..2e305252
--- /dev/null
+++ b/test/selenium/webdriver/common/implicit_waits_tests.py
@@ -0,0 +1,73 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import NoSuchElementException
+
+
+def testShouldImplicitlyWaitForASingleElement(driver, pages):
+    pages.load("dynamic.html")
+    add = driver.find_element_by_id("adder")
+    driver.implicitly_wait(3)
+    add.click()
+    driver.find_element_by_id("box0")  # All is well if this doesn't throw.
+
+
+def testShouldStillFailToFindAnElementWhenImplicitWaitsAreEnabled(driver, pages):
+    pages.load("dynamic.html")
+    driver.implicitly_wait(0.5)
+    with pytest.raises(NoSuchElementException):
+        driver.find_element_by_id("box0")
+
+
+def testShouldReturnAfterFirstAttemptToFindOneAfterDisablingImplicitWaits(driver, pages):
+    pages.load("dynamic.html")
+    driver.implicitly_wait(3)
+    driver.implicitly_wait(0)
+    with pytest.raises(NoSuchElementException):
+        driver.find_element_by_id("box0")
+
+
+def testShouldImplicitlyWaitUntilAtLeastOneElementIsFoundWhenSearchingForMany(driver, pages):
+    pages.load("dynamic.html")
+    add = driver.find_element_by_id("adder")
+
+    driver.implicitly_wait(2)
+    add.click()
+    add.click()
+
+    elements = driver.find_elements_by_class_name("redbox")
+    assert len(elements) >= 1
+
+
+def testShouldStillFailToFindAnElemenstWhenImplicitWaitsAreEnabled(driver, pages):
+    pages.load("dynamic.html")
+
+    driver.implicitly_wait(0.5)
+    elements = driver.find_elements_by_class_name("redbox")
+    assert 0 == len(elements)
+
+
+def testShouldReturnAfterFirstAttemptToFindManyAfterDisablingImplicitWaits(driver, pages):
+    pages.load("dynamic.html")
+    add = driver.find_element_by_id("adder")
+    driver.implicitly_wait(1.1)
+    driver.implicitly_wait(0)
+    add.click()
+    elements = driver.find_elements_by_class_name("redbox")
+    assert 0 == len(elements)
diff --git a/test/selenium/webdriver/common/interactions_tests.py b/test/selenium/webdriver/common/interactions_tests.py
new file mode 100644
index 00000000..0a2908b2
--- /dev/null
+++ b/test/selenium/webdriver/common/interactions_tests.py
@@ -0,0 +1,265 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for advanced user interactions."""
+import pytest
+
+from selenium.webdriver.common.keys import Keys
+from selenium.webdriver.common.action_chains import ActionChains
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def performDragAndDropWithMouse(driver, pages):
+    """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface."""
+    pages.load("draggableLists.html")
+    dragReporter = driver.find_element_by_id("dragging_reports")
+    toDrag = driver.find_element_by_id("rightitem-3")
+    dragInto = driver.find_element_by_id("sortable1")
+
+    holdItem = ActionChains(driver).click_and_hold(toDrag)
+    moveToSpecificItem = ActionChains(driver) \
+        .move_to_element(driver.find_element_by_id("leftitem-4"))
+    moveToOtherList = ActionChains(driver).move_to_element(dragInto)
+    drop = ActionChains(driver).release(dragInto)
+    assert "Nothing happened." == dragReporter.text
+
+    holdItem.perform()
+    moveToSpecificItem.perform()
+    moveToOtherList.perform()
+    assert "Nothing happened. DragOut" == dragReporter.text
+
+    drop.perform()
+
+
+def testDraggingElementWithMouseMovesItToAnotherList(driver, pages):
+    """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface."""
+    performDragAndDropWithMouse(driver, pages)
+    dragInto = driver.find_element_by_id("sortable1")
+    assert 6 == len(dragInto.find_elements_by_tag_name("li"))
+
+
+def testDraggingElementWithMouseFiresEvents(driver, pages):
+    """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface."""
+    performDragAndDropWithMouse(driver, pages)
+    dragReporter = driver.find_element_by_id("dragging_reports")
+    assert "Nothing happened. DragOut DropIn RightItem 3" == dragReporter.text
+
+
+def _isElementAvailable(driver, id):
+    """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface."""
+    try:
+        driver.find_element_by_id(id)
+        return True
+    except Exception:
+        return False
+
+
+def testDragAndDrop(driver, pages):
+    """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface."""
+    element_available_timeout = 15
+    wait = WebDriverWait(driver, element_available_timeout)
+    pages.load("droppableItems.html")
+    wait.until(lambda dr: _isElementAvailable(driver, "draggable"))
+
+    if not _isElementAvailable(driver, "draggable"):
+        raise AssertionError("Could not find draggable element after 15 seconds.")
+
+    toDrag = driver.find_element_by_id("draggable")
+    dropInto = driver.find_element_by_id("droppable")
+
+    holdDrag = ActionChains(driver) \
+        .click_and_hold(toDrag)
+    move = ActionChains(driver) \
+        .move_to_element(dropInto)
+    drop = ActionChains(driver).release(dropInto)
+
+    holdDrag.perform()
+    move.perform()
+    drop.perform()
+
+    dropInto = driver.find_element_by_id("droppable")
+    text = dropInto.find_element_by_tag_name("p").text
+    assert "Dropped!" == text
+
+
+def testDoubleClick(driver, pages):
+    """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface."""
+    pages.load("javascriptPage.html")
+    toDoubleClick = driver.find_element_by_id("doubleClickField")
+
+    dblClick = ActionChains(driver) \
+        .double_click(toDoubleClick)
+
+    dblClick.perform()
+    assert "DoubleClicked" == toDoubleClick.get_attribute('value')
+
+
+def testContextClick(driver, pages):
+    """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface."""
+    pages.load("javascriptPage.html")
+    toContextClick = driver.find_element_by_id("doubleClickField")
+
+    contextClick = ActionChains(driver) \
+        .context_click(toContextClick)
+
+    contextClick.perform()
+    assert "ContextClicked" == toContextClick.get_attribute('value')
+
+
+def testMoveAndClick(driver, pages):
+    """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface."""
+    pages.load("javascriptPage.html")
+    toClick = driver.find_element_by_id("clickField")
+
+    click = ActionChains(driver) \
+        .move_to_element(toClick) \
+        .click()
+
+    click.perform()
+    assert "Clicked" == toClick.get_attribute('value')
+
+
+def testCannotMoveToANullLocator(driver, pages):
+    """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface."""
+    pages.load("javascriptPage.html")
+
+    with pytest.raises(AttributeError):
+        move = ActionChains(driver) \
+            .move_to_element(None)
+        move.perform()
+
+
+@pytest.mark.xfail_firefox
+def testClickingOnFormElements(driver, pages):
+    """Copied from org.openqa.selenium.interactions.CombinedInputActionsTest."""
+    pages.load("formSelectionPage.html")
+    options = driver.find_elements_by_tag_name("option")
+    selectThreeOptions = ActionChains(driver) \
+        .click(options[1]) \
+        .key_down(Keys.SHIFT) \
+        .click(options[3]) \
+        .key_up(Keys.SHIFT)
+    selectThreeOptions.perform()
+
+    showButton = driver.find_element_by_name("showselected")
+    showButton.click()
+
+    resultElement = driver.find_element_by_id("result")
+    assert "roquefort parmigiano cheddar" == resultElement.text
+
+
+@pytest.mark.xfail_marionette(
+    reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1292178')
+@pytest.mark.xfail_remote(
+    reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1292178')
+def testSelectingMultipleItems(driver, pages):
+    """Copied from org.openqa.selenium.interactions.CombinedInputActionsTest."""
+    pages.load("selectableItems.html")
+    reportingElement = driver.find_element_by_id("infodiv")
+    assert "no info" == reportingElement.text
+
+    listItems = driver.find_elements_by_tag_name("li")
+    selectThreeItems = ActionChains(driver) \
+        .key_down(Keys.CONTROL) \
+        .click(listItems[1]) \
+        .click(listItems[3]) \
+        .click(listItems[5]) \
+        .key_up(Keys.CONTROL)
+    selectThreeItems.perform()
+
+    assert "#item2 #item4 #item6" == reportingElement.text
+
+    # Now click on another element, make sure that's the only one selected.
+    actionsBuilder = ActionChains(driver)
+    actionsBuilder.click(listItems[6]).perform()
+    assert "#item7" == reportingElement.text
+
+
+@pytest.mark.xfail_marionette(
+    reason='https://github.com/mozilla/geckodriver/issues/646')
+@pytest.mark.xfail_remote(
+    reason='https://github.com/mozilla/geckodriver/issues/646')
+def testSendingKeysToActiveElementWithModifier(driver, pages):
+    pages.load("formPage.html")
+    e = driver.find_element_by_id("working")
+    e.click()
+
+    ActionChains(driver) \
+        .key_down(Keys.SHIFT) \
+        .send_keys("abc") \
+        .key_up(Keys.SHIFT) \
+        .perform()
+
+    assert "ABC" == e.get_attribute('value')
+
+
+def testSendingKeysToElement(driver, pages):
+    pages.load("formPage.html")
+    e = driver.find_element_by_id("working")
+
+    ActionChains(driver).send_keys_to_element(e, 'abc').perform()
+
+    assert "abc" == e.get_attribute('value')
+
+
+def testCanSendKeysBetweenClicks(driver, pages):
+    """
+    For W3C, ensures that the correct number of pauses are given to the other
+    input device.
+    """
+    pages.load('javascriptPage.html')
+    keyup = driver.find_element_by_id("keyUp")
+    keydown = driver.find_element_by_id("keyDown")
+    ActionChains(driver).click(keyup).send_keys('foobar').click(keydown).perform()
+
+    assert 'foobar' == keyup.get_attribute('value')
+
+
+def test_can_reset_interactions(driver, pages):
+    actions = ActionChains(driver)
+    actions.click()
+    actions.key_down('A')
+    if driver.w3c:
+        assert all((len(device.actions) > 0 for device in actions.w3c_actions.devices))
+    else:
+        assert len(actions._actions) > 0
+
+    actions.reset_actions()
+
+    if driver.w3c:
+        assert all((len(device.actions) == 0 for device in actions.w3c_actions.devices))
+    else:
+        assert len(actions._actions) == 0
+
+
+def test_can_pause(driver, pages):
+    from time import time
+    pages.load("javascriptPage.html")
+
+    pause_time = 2
+    toClick = driver.find_element_by_id("clickField")
+    toDoubleClick = driver.find_element_by_id("doubleClickField")
+
+    pause = ActionChains(driver).click(toClick).pause(pause_time).click(toDoubleClick)
+
+    start = time()
+    pause.perform()
+    end = time()
+
+    assert pause_time < end - start
+    assert "Clicked" == toClick.get_attribute('value')
+    assert "Clicked" == toDoubleClick.get_attribute('value')
diff --git a/test/selenium/webdriver/common/network.py b/test/selenium/webdriver/common/network.py
new file mode 100644
index 00000000..b36deb57
--- /dev/null
+++ b/test/selenium/webdriver/common/network.py
@@ -0,0 +1,55 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# module for getting the lan ip address of the computer
+import os
+import socket
+
+if os.name != "nt":
+    import fcntl
+    import struct
+
+    def get_interface_ip(ifname):
+        def _bytes(value, encoding):
+            try:
+                return bytes(value, encoding)  # Python 3
+            except TypeError:
+                return value  # Python 2
+
+        sckt = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        return socket.inet_ntoa(fcntl.ioctl(
+            sckt.fileno(),
+            0x8915,  # SIOCGIFADDR
+            struct.pack('256s', _bytes(ifname[:15], 'utf-8'))
+        )[20:24])
+
+
+def get_lan_ip():
+    try:
+        ip = socket.gethostbyname(socket.gethostname())
+    except Exception:
+        return '0.0.0.0'
+    if ip.startswith("127.") and os.name != "nt":
+        interfaces = ["eth0", "eth1", "eth2", "en0", "en1", "en2", "en3",
+                      "en4", "wlan0", "wlan1", "wifi0", "ath0", "ath1", "ppp0"]
+        for ifname in interfaces:
+            try:
+                ip = get_interface_ip(ifname)
+                break
+            except IOError:
+                pass
+    return ip
diff --git a/test/selenium/webdriver/common/opacity_tests.py b/test/selenium/webdriver/common/opacity_tests.py
new file mode 100644
index 00000000..1593e8d6
--- /dev/null
+++ b/test/selenium/webdriver/common/opacity_tests.py
@@ -0,0 +1,47 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver.common.by import By
+
+
+@pytest.mark.xfail_ie
+def testShouldBeAbleToClickOnElementsWithOpacityZero(driver, pages):
+    pages.load("click_jacker.html")
+    element = driver.find_element(By.ID, "clickJacker")
+    assert '0' == element.value_of_css_property("opacity"), \
+        "Precondition failed: clickJacker should be transparent.\
+        Value was %s" % element.value_of_css_property("opacity")
+    element.click()
+    assert '1' == element.value_of_css_property("opacity")
+
+
+@pytest.mark.xfail_ie
+def testShouldBeAbleToSelectOptionsFromAnInvisibleSelect(driver, pages):
+    pages.load("formPage.html")
+    select = driver.find_element(By.ID, "invisi_select")
+    options = select.find_elements(By.TAG_NAME, "option")
+    apples = options[0]
+    oranges = options[1]
+
+    assert apples.is_selected()
+    assert not oranges.is_selected()
+
+    oranges.click()
+    assert not apples.is_selected()
+    assert oranges.is_selected()
diff --git a/test/selenium/webdriver/common/page_load_timeout_tests.py b/test/selenium/webdriver/common/page_load_timeout_tests.py
new file mode 100644
index 00000000..19c83aaf
--- /dev/null
+++ b/test/selenium/webdriver/common/page_load_timeout_tests.py
@@ -0,0 +1,40 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import TimeoutException
+
+
+@pytest.fixture(autouse=True)
+def reset_timeouts(driver):
+    yield
+    driver.set_page_load_timeout(300)
+
+
+def testShouldTimeoutOnPageLoadTakingTooLong(driver, pages):
+    driver.set_page_load_timeout(0.01)
+    with pytest.raises(TimeoutException):
+        pages.load("simpleTest.html")
+
+
+@pytest.mark.xfail_chrome
+def testClickShouldTimeout(driver, pages):
+    pages.load("simpleTest.html")
+    driver.set_page_load_timeout(0.01)
+    with pytest.raises(TimeoutException):
+        driver.find_element_by_id("multilinelink").click()
diff --git a/test/selenium/webdriver/common/page_loader.py b/test/selenium/webdriver/common/page_loader.py
new file mode 100644
index 00000000..8af8fcac
--- /dev/null
+++ b/test/selenium/webdriver/common/page_loader.py
@@ -0,0 +1,32 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""This module contains some decorators that can be used to support
+the page models. For example for an action that needs a page to be fully
+loaded, the @require_loaded decorator will make sure the page is loaded
+before the call is invoked.
+This pattern is also useful for waiting for certain asynchronous events
+to happen before excuting certain actions."""
+
+
+def require_loaded(func):
+    def load_page(page, *params, **kwds):
+        if not page.is_loaded():
+            page.load()
+        assert page.is_loaded(), "page should be loaded by now"
+        return func(page, *params, **kwds)
+    return load_page
diff --git a/test/selenium/webdriver/common/page_loading_tests.py b/test/selenium/webdriver/common/page_loading_tests.py
new file mode 100644
index 00000000..2f192804
--- /dev/null
+++ b/test/selenium/webdriver/common/page_loading_tests.py
@@ -0,0 +1,132 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.wait import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+
+
+def testShouldWaitForDocumentToBeLoaded(driver, pages):
+    pages.load("simpleTest.html")
+    assert driver.title == "Hello WebDriver"
+
+
+# Disabled till Java WebServer is used
+# def testShouldFollowRedirectsSentInTheHttpResponseHeaders(driver, pages):
+#    pages.load("redirect.html")
+#    assert driver.title == "We Arrive Here"
+
+
+# Disabled till the Java WebServer is used
+# def testShouldFollowMetaRedirects(driver, pages):
+#    pages.load("metaRedirect.html")
+#    assert driver.title == "We Arrive Here"
+
+
+def testShouldBeAbleToGetAFragmentOnTheCurrentPage(driver, pages):
+    pages.load("xhtmlTest.html")
+    location = driver.current_url
+    driver.get(location + "#text")
+    driver.find_element(by=By.ID, value="id1")
+
+
+@pytest.mark.xfail_marionette(raises=WebDriverException)
+@pytest.mark.xfail_remote(raises=WebDriverException)
+def testShouldReturnWhenGettingAUrlThatDoesNotResolve(driver):
+    #  Of course, we're up the creek if this ever does get registered
+    driver.get("http://www.thisurldoesnotexist.comx/")
+
+
+@pytest.mark.xfail_marionette(raises=WebDriverException)
+@pytest.mark.xfail_remote(raises=WebDriverException)
+def testShouldReturnWhenGettingAUrlThatDoesNotConnect(driver):
+    #  Here's hoping that there's nothing here. There shouldn't be
+    driver.get("http://localhost:3001")
+
+# def testShouldBeAbleToLoadAPageWithFramesetsAndWaitUntilAllFramesAreLoaded() {
+#     driver.get(pages.framesetPage)
+#
+#     driver.switchTo().frame(0)
+#     WebElement pageNumber = driver.findElement(By.xpath("#span[@id='pageNumber']"))
+#     self.assertEqual((pageNumber.getText().trim(), equalTo("1"))
+#
+#     driver.switchTo().defaultContent().switchTo().frame(1)
+#     pageNumber = driver.findElement(By.xpath("#span[@id='pageNumber']"))
+#     self.assertEqual((pageNumber.getText().trim(), equalTo("2"))
+
+# Need to implement this decorator
+# @NeedsFreshDriver
+# def testSouldDoNothingIfThereIsNothingToGoBackTo() {
+#     originalTitle = driver.getTitle();
+#     driver.get(pages.formPage);
+#     driver.back();
+#     # We may have returned to the browser's home page
+#     self.assertEqual(driver.title, anyOf(equalTo(originalTitle), equalTo("We Leave From Here")));
+
+
+def testShouldBeAbleToNavigateBackInTheBrowserHistory(driver, pages):
+    pages.load("formPage.html")
+
+    driver.find_element(by=By.ID, value="imageButton").submit()
+    WebDriverWait(driver, 3).until(EC.title_is("We Arrive Here"))
+
+    driver.back()
+    assert driver.title == "We Leave From Here"
+
+
+def testShouldBeAbleToNavigateBackInTheBrowserHistoryInPresenceOfIframes(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    driver.find_element(by=By.NAME, value="sameWindow").click()
+
+    assert driver.title == "This page has iframes"
+
+    driver.back()
+    assert driver.title == "XHTML Test Page"
+
+
+def testShouldBeAbleToNavigateForwardsInTheBrowserHistory(driver, pages):
+    pages.load("formPage.html")
+
+    driver.find_element(by=By.ID, value="imageButton").submit()
+    WebDriverWait(driver, 3).until(EC.title_is("We Arrive Here"))
+
+    driver.back()
+    assert driver.title == "We Leave From Here"
+
+    driver.forward()
+    assert driver.title == "We Arrive Here"
+
+
+@pytest.mark.xfail_ie
+@pytest.mark.xfail_marionette(run=False)
+@pytest.mark.xfail_remote(run=False)
+@pytest.mark.xfail_chrome(run=False)
+def testShouldNotHangifDocumentOpenCallIsNeverFollowedByDocumentCloseCall(driver, pages):
+    pages.load("document_write_in_onload.html")
+    driver.find_element(By.XPATH, "//body")
+
+
+def testShouldBeAbleToRefreshAPage(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    driver.refresh()
+
+    assert driver.title == "XHTML Test Page"
diff --git a/test/selenium/webdriver/common/position_and_size_tests.py b/test/selenium/webdriver/common/position_and_size_tests.py
new file mode 100644
index 00000000..07252414
--- /dev/null
+++ b/test/selenium/webdriver/common/position_and_size_tests.py
@@ -0,0 +1,110 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver.common.by import By
+
+
+def testShouldBeAbleToDetermineTheLocationOfAnElement(driver, pages):
+    pages.load("xhtmlTest.html")
+    location = driver.find_element(By.ID, "username").location_once_scrolled_into_view
+    assert location["x"] > 0
+    assert location["y"] > 0
+
+
+@pytest.mark.parametrize('page', (
+    'coordinates_tests/simple_page.html',
+    'coordinates_tests/page_with_empty_element.html',
+    'coordinates_tests/page_with_transparent_element.html',
+    'coordinates_tests/page_with_hidden_element.html'),
+    ids=('basic', 'empty', 'transparent', 'hidden'))
+def testShouldGetCoordinatesOfAnElement(page, driver, pages):
+    pages.load(page)
+    element = driver.find_element(By.ID, "box")
+    _check_location(element.location_once_scrolled_into_view, x=10, y=10)
+    _check_location(element.location, x=10, y=10)
+
+
+def testShouldGetCoordinatesOfAnInvisibleElement(driver, pages):
+    pages.load("coordinates_tests/page_with_invisible_element.html")
+    element = driver.find_element(By.ID, "box")
+    _check_location(element.location_once_scrolled_into_view, x=0, y=0)
+    _check_location(element.location, x=0, y=0)
+
+
+def testShouldScrollPageAndGetCoordinatesOfAnElementThatIsOutOfViewPort(driver, pages):
+    pages.load("coordinates_tests/page_with_element_out_of_view.html")
+    element = driver.find_element(By.ID, "box")
+    windowHeight = driver.get_window_size()["height"]
+    _check_location(element.location_once_scrolled_into_view, x=10)
+    assert 0 <= element.location_once_scrolled_into_view["y"] <= (windowHeight - 100)
+    _check_location(element.location, x=10, y=5010)
+
+
+@pytest.mark.xfail_chrome
+@pytest.mark.xfail_marionette
+@pytest.mark.xfail_remote
+def testShouldGetCoordinatesOfAnElementInAFrame(driver, pages):
+    pages.load("coordinates_tests/element_in_frame.html")
+    driver.switch_to.frame(driver.find_element(By.NAME, "ifr"))
+    element = driver.find_element(By.ID, "box")
+    _check_location(element.location_once_scrolled_into_view, x=25, y=25)
+    _check_location(element.location, x=10, y=10)
+
+
+@pytest.mark.xfail_chrome
+@pytest.mark.xfail_marionette
+@pytest.mark.xfail_remote
+def testShouldGetCoordinatesOfAnElementInANestedFrame(driver, pages):
+    pages.load("coordinates_tests/element_in_nested_frame.html")
+    driver.switch_to.frame(driver.find_element(By.NAME, "ifr"))
+    driver.switch_to.frame(driver.find_element(By.NAME, "ifr"))
+    element = driver.find_element(By.ID, "box")
+    _check_location(element.location_once_scrolled_into_view, x=40, y=40)
+    _check_location(element.location, x=10, y=10)
+
+
+def testShouldGetCoordinatesOfAnElementWithFixedPosition(driver, pages):
+    pages.load("coordinates_tests/page_with_fixed_element.html")
+    element = driver.find_element(By.ID, "fixed")
+    _check_location(element.location_once_scrolled_into_view, y=0)
+    _check_location(element.location, y=0)
+
+    driver.find_element(By.ID, "bottom").click()
+    _check_location(element.location_once_scrolled_into_view, y=0)
+    assert element.location["y"] > 0
+
+
+def testShouldCorrectlyIdentifyThatAnElementHasWidthAndHeight(driver, pages):
+    pages.load("xhtmlTest.html")
+    shrinko = driver.find_element(By.ID, "linkId")
+    size = shrinko.size
+    assert size["width"] > 0
+    assert size["height"] > 0
+
+
+def _check_location(location, **kwargs):
+    try:
+        # python 2.x
+        expected = kwargs.viewitems()
+        actual = location.viewitems()
+    except AttributeError:
+        # python 3.x
+        expected = kwargs.items()
+        actual = location.items()
+    assert expected <= actual
diff --git a/test/selenium/webdriver/common/proxy_tests.py b/test/selenium/webdriver/common/proxy_tests.py
new file mode 100755
index 00000000..b351f833
--- /dev/null
+++ b/test/selenium/webdriver/common/proxy_tests.py
@@ -0,0 +1,145 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver.common.proxy import Proxy, ProxyType
+
+
+MANUAL_PROXY = {
+    'httpProxy': 'some.url:1234',
+    'ftpProxy': 'ftp.proxy',
+    'noProxy': 'localhost, foo.localhost',
+    'sslProxy': 'ssl.proxy:1234',
+    'socksProxy': 'socks.proxy:65555',
+    'socksUsername': 'test',
+    'socksPassword': 'test',
+    'socksVersion': 5,
+}
+
+PAC_PROXY = {
+    'proxyAutoconfigUrl': 'http://pac.url:1234',
+}
+
+AUTODETECT_PROXY = {
+    'autodetect': True,
+}
+
+
+def testCanAddManualProxyToDesiredCapabilities():
+    proxy = Proxy()
+    proxy.http_proxy = MANUAL_PROXY['httpProxy']
+    proxy.ftp_proxy = MANUAL_PROXY['ftpProxy']
+    proxy.no_proxy = MANUAL_PROXY['noProxy']
+    proxy.sslProxy = MANUAL_PROXY['sslProxy']
+    proxy.socksProxy = MANUAL_PROXY['socksProxy']
+    proxy.socksUsername = MANUAL_PROXY['socksUsername']
+    proxy.socksPassword = MANUAL_PROXY['socksPassword']
+    proxy.socksVersion = MANUAL_PROXY['socksVersion']
+
+    desired_capabilities = {}
+    proxy.add_to_capabilities(desired_capabilities)
+
+    proxy_capabilities = MANUAL_PROXY.copy()
+    proxy_capabilities['proxyType'] = 'MANUAL'
+    expected_capabilities = {'proxy': proxy_capabilities}
+    assert expected_capabilities == desired_capabilities
+
+
+def testCanAddAutodetectProxyToDesiredCapabilities():
+    proxy = Proxy()
+    proxy.auto_detect = AUTODETECT_PROXY['autodetect']
+
+    desired_capabilities = {}
+    proxy.add_to_capabilities(desired_capabilities)
+
+    proxy_capabilities = AUTODETECT_PROXY.copy()
+    proxy_capabilities['proxyType'] = 'AUTODETECT'
+    expected_capabilities = {'proxy': proxy_capabilities}
+    assert expected_capabilities == desired_capabilities
+
+
+def testCanAddPACProxyToDesiredCapabilities():
+    proxy = Proxy()
+    proxy.proxy_autoconfig_url = PAC_PROXY['proxyAutoconfigUrl']
+
+    desired_capabilities = {}
+    proxy.add_to_capabilities(desired_capabilities)
+
+    proxy_capabilities = PAC_PROXY.copy()
+    proxy_capabilities['proxyType'] = 'PAC'
+    expected_capabilities = {'proxy': proxy_capabilities}
+    assert expected_capabilities == desired_capabilities
+
+
+def testCanNotChangeInitializedProxyType():
+    proxy = Proxy(raw={'proxyType': 'direct'})
+    with pytest.raises(Exception):
+        proxy.proxy_type = ProxyType.SYSTEM
+
+    proxy = Proxy(raw={'proxyType': ProxyType.DIRECT})
+    with pytest.raises(Exception):
+        proxy.proxy_type = ProxyType.SYSTEM
+
+
+def testCanInitManualProxy():
+    proxy = Proxy(raw=MANUAL_PROXY)
+
+    assert ProxyType.MANUAL == proxy.proxy_type
+    assert MANUAL_PROXY['httpProxy'] == proxy.http_proxy
+    assert MANUAL_PROXY['ftpProxy'] == proxy.ftp_proxy
+    assert MANUAL_PROXY['noProxy'] == proxy.no_proxy
+    assert MANUAL_PROXY['sslProxy'] == proxy.sslProxy
+    assert MANUAL_PROXY['socksProxy'] == proxy.socksProxy
+    assert MANUAL_PROXY['socksUsername'] == proxy.socksUsername
+    assert MANUAL_PROXY['socksPassword'] == proxy.socksPassword
+    assert MANUAL_PROXY['socksVersion'] == proxy.socksVersion
+
+
+def testCanInitAutodetectProxy():
+    proxy = Proxy(raw=AUTODETECT_PROXY)
+    assert ProxyType.AUTODETECT == proxy.proxy_type
+    assert AUTODETECT_PROXY['autodetect'] == proxy.auto_detect
+
+
+def testCanInitPACProxy():
+    proxy = Proxy(raw=PAC_PROXY)
+    assert ProxyType.PAC == proxy.proxy_type
+    assert PAC_PROXY['proxyAutoconfigUrl'] == proxy.proxy_autoconfig_url
+
+
+def testCanInitEmptyProxy():
+    proxy = Proxy()
+    assert ProxyType.UNSPECIFIED == proxy.proxy_type
+    assert '' == proxy.http_proxy
+    assert '' == proxy.ftp_proxy
+    assert '' == proxy.no_proxy
+    assert '' == proxy.sslProxy
+    assert '' == proxy.socksProxy
+    assert '' == proxy.socksUsername
+    assert '' == proxy.socksPassword
+    assert proxy.auto_detect is False
+    assert '' == proxy.proxy_autoconfig_url
+    assert proxy.socks_version is None
+
+    desired_capabilities = {}
+    proxy.add_to_capabilities(desired_capabilities)
+
+    proxy_capabilities = {}
+    proxy_capabilities['proxyType'] = 'UNSPECIFIED'
+    expected_capabilities = {'proxy': proxy_capabilities}
+    assert expected_capabilities == desired_capabilities
diff --git a/test/selenium/webdriver/common/quit_tests.py b/test/selenium/webdriver/common/quit_tests.py
new file mode 100644
index 00000000..2faac425
--- /dev/null
+++ b/test/selenium/webdriver/common/quit_tests.py
@@ -0,0 +1,25 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+
+@pytest.mark.no_driver_after_test
+def test_quit(driver, pages):
+    driver.quit()
+    with pytest.raises(Exception):
+        pages.load('simpleTest.html')
diff --git a/test/selenium/webdriver/common/rendered_webelement_tests.py b/test/selenium/webdriver/common/rendered_webelement_tests.py
new file mode 100644
index 00000000..9c5aca0a
--- /dev/null
+++ b/test/selenium/webdriver/common/rendered_webelement_tests.py
@@ -0,0 +1,65 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.color import Color
+
+
+def testShouldPickUpStyleOfAnElement(driver, pages):
+    pages.load("javascriptPage.html")
+
+    element = driver.find_element(by=By.ID, value="green-parent")
+    backgroundColour = Color.from_string(element.value_of_css_property("background-color"))
+    assert Color.from_string("rgba(0, 128, 0, 1)") == backgroundColour
+
+    element = driver.find_element(by=By.ID, value="red-item")
+    backgroundColour = Color.from_string(element.value_of_css_property("background-color"))
+    assert Color.from_string("rgba(255, 0, 0, 1)") == backgroundColour
+
+
+def testShouldAllowInheritedStylesToBeUsed(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="green-item")
+    backgroundColour = Color.from_string(element.value_of_css_property("background-color"))
+    assert backgroundColour == Color.from_string("transparent")
+
+
+def testShouldCorrectlyIdentifyThatAnElementHasWidth(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    shrinko = driver.find_element(by=By.ID, value="linkId")
+    size = shrinko.size
+    assert size["width"] > 0
+    assert size["height"] > 0
+
+
+@pytest.mark.xfail_safari(
+    reason='Get Element Rect command not implemented',
+    raises=WebDriverException)
+def testShouldBeAbleToDetermineTheRectOfAnElement(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    element = driver.find_element(By.ID, "username")
+    rect = element.rect
+
+    assert rect["x"] > 0
+    assert rect["y"] > 0
+    assert rect["width"] > 0
+    assert rect["height"] > 0
diff --git a/test/selenium/webdriver/common/repr_tests.py b/test/selenium/webdriver/common/repr_tests.py
new file mode 100644
index 00000000..7c830a86
--- /dev/null
+++ b/test/selenium/webdriver/common/repr_tests.py
@@ -0,0 +1,40 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.support.wait import WebDriverWait
+
+
+def testShouldImplementReprForWebDriver(driver):
+    driver_repr = repr(driver)
+    assert type(driver).__name__ in driver_repr
+    assert driver.session_id in driver_repr
+
+
+def testShouldImplementReprForWebElement(driver, pages):
+    pages.load('simpleTest.html')
+    elem = driver.find_element_by_id("validImgTag")
+    elem_repr = repr(elem)
+    assert type(elem).__name__ in elem_repr
+    assert driver.session_id in elem_repr
+    assert elem._id in elem_repr
+
+
+def testShouldImplementReprForWait(driver):
+    wait = WebDriverWait(driver, 30)
+    wait_repr = repr(wait)
+    assert type(wait).__name__ in wait_repr
+    assert driver.session_id in wait_repr
diff --git a/test/selenium/webdriver/common/results_page.py b/test/selenium/webdriver/common/results_page.py
new file mode 100644
index 00000000..4c4dd0c8
--- /dev/null
+++ b/test/selenium/webdriver/common/results_page.py
@@ -0,0 +1,37 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+class ResultsPage(object):
+    """This class models a google search result page."""
+
+    def __init__(self, driver):
+        self._driver = driver
+
+    def is_loaded(self):
+        return "/search" in self._driver.get_current_url()
+
+    def load(self):
+        raise Exception("This page shouldn't be loaded directly")
+
+    def link_contains_match_for(self, term):
+        result_section = self._driver.find_element_by_id("res")
+        elements = result_section.find_elements_by_xpath(".//*[@class='l']")
+        for e in elements:
+            if term in e.get_text():
+                return True
+        return False
diff --git a/test/selenium/webdriver/common/select_class_tests.py b/test/selenium/webdriver/common/select_class_tests.py
new file mode 100644
index 00000000..99fbca3d
--- /dev/null
+++ b/test/selenium/webdriver/common/select_class_tests.py
@@ -0,0 +1,322 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import (
+    ElementNotSelectableException,
+    NoSuchElementException,
+    UnexpectedTagNameException)
+from selenium.webdriver.support.ui import Select
+from selenium.webdriver.common.by import By
+
+disabledSelect = {'name': 'no-select', 'values': ['Foo']}
+singleSelectValues1 = {'name': 'selectomatic', 'values': ['One', 'Two', 'Four', 'Still learning how to count, apparently']}
+singleSelectValues2 = {'name': 'redirect', 'values': ['One', 'Two']}
+singleSelectValuesWithSpaces = {'name': 'select_with_spaces', 'values': ['One', 'Two', 'Four', 'Still learning how to count, apparently']}
+multiSelectValues1 = {'name': 'multi', 'values': ['Eggs', 'Ham', 'Sausages', 'Onion gravy']}
+multiSelectValues2 = {'name': 'select_empty_multiple', 'values': ['select_1', 'select_2', 'select_3', 'select_4']}
+
+
+def testSelectByIndexSingle(driver, pages):
+    pages.load("formPage.html")
+
+    for select in [singleSelectValues1]:
+        sel = Select(driver.find_element(By.NAME, select['name']))
+        for x in range(len(select['values'])):
+            sel.select_by_index(x)
+            assert sel.first_selected_option.text == select['values'][x]
+
+
+@pytest.mark.xfail_chrome
+@pytest.mark.xfail_firefox
+@pytest.mark.xfail_remote
+@pytest.mark.xfail_marionette(reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1429403')
+def testSelectDisabledByIndex(driver, pages):
+    pages.load("formPage.html")
+    sel = Select(driver.find_element(By.NAME, disabledSelect['name']))
+    if driver.w3c:
+        selected = sel.first_selected_option
+        sel.select_by_index(1)
+        assert selected == sel.first_selected_option
+    else:
+        with pytest.raises(ElementNotSelectableException):
+            sel.select_by_index(1)
+
+
+def testSelectByValueSingle(driver, pages):
+    pages.load("formPage.html")
+
+    for select in [singleSelectValues1]:
+        sel = Select(driver.find_element(By.NAME, select['name']))
+        for x in range(len(select['values'])):
+            sel.select_by_value(select['values'][x].lower())
+            assert sel.first_selected_option.text == select['values'][x]
+
+
+@pytest.mark.xfail_chrome
+@pytest.mark.xfail_firefox
+@pytest.mark.xfail_remote
+@pytest.mark.xfail_marionette(reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1429403')
+def testSelectDisabledByValue(driver, pages):
+    pages.load("formPage.html")
+    sel = Select(driver.find_element(By.NAME, disabledSelect['name']))
+    if driver.w3c:
+        selected = sel.first_selected_option
+        sel.select_by_value('bar')
+        assert selected == sel.first_selected_option
+    else:
+        with pytest.raises(ElementNotSelectableException):
+            sel.select_by_value('bar')
+
+
+def testSelectByVisibleTextSingle(driver, pages):
+    pages.load("formPage.html")
+
+    for select in [singleSelectValues1]:
+        sel = Select(driver.find_element(By.NAME, select['name']))
+        for x in range(len(select['values'])):
+            print(select['values'][x])
+            sel.select_by_visible_text(select['values'][x])
+            assert sel.first_selected_option.text == select['values'][x]
+
+
+@pytest.mark.xfail_chrome(
+    reason='https://bugs.chromium.org/p/chromedriver/issues/detail?id=822')
+def testSelectByVisibleTextShouldNormalizeSpaces(driver, pages):
+    pages.load("formPage.html")
+
+    for select in [singleSelectValuesWithSpaces]:
+        sel = Select(driver.find_element(By.NAME, select['name']))
+        for x in range(len(select['values'])):
+            print(select['values'][x])
+            sel.select_by_visible_text(select['values'][x])
+            assert sel.first_selected_option.text == select['values'][x]
+
+
+@pytest.mark.xfail_chrome
+@pytest.mark.xfail_firefox
+@pytest.mark.xfail_remote
+@pytest.mark.xfail_marionette(reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1429403')
+def testSelectDisabledByVisibleText(driver, pages):
+    pages.load("formPage.html")
+    sel = Select(driver.find_element(By.NAME, disabledSelect['name']))
+    if driver.w3c:
+        selected = sel.first_selected_option
+        sel.select_by_visible_text('Bar')
+        assert selected == sel.first_selected_option
+    else:
+        with pytest.raises(ElementNotSelectableException):
+            sel.select_by_visible_text('Bar')
+
+
+def testSelectByIndexMultiple(driver, pages):
+    pages.load("formPage.html")
+
+    for select in [multiSelectValues1, multiSelectValues2]:
+        sel = Select(driver.find_element(By.NAME, select['name']))
+        sel.deselect_all()
+        for x in range(len(select['values'])):
+            sel.select_by_index(x)
+            selected = sel.all_selected_options
+            assert len(selected) == x + 1
+            for j in range(len(selected)):
+                assert selected[j].text == select['values'][j]
+
+
+def testSelectByValueMultiple(driver, pages):
+    pages.load("formPage.html")
+
+    for select in [multiSelectValues1, multiSelectValues2]:
+        sel = Select(driver.find_element(By.NAME, select['name']))
+        sel.deselect_all()
+        for x in range(len(select['values'])):
+            sel.select_by_value(select['values'][x].lower())
+            selected = sel.all_selected_options
+            assert len(selected) == x + 1
+            for j in range(len(selected)):
+                assert selected[j].text == select['values'][j]
+
+
+def testSelectByVisibleTextMultiple(driver, pages):
+    pages.load("formPage.html")
+
+    for select in [multiSelectValues1, multiSelectValues2]:
+        sel = Select(driver.find_element(By.NAME, select['name']))
+        sel.deselect_all()
+        for x in range(len(select['values'])):
+            sel.select_by_visible_text(select['values'][x])
+            selected = sel.all_selected_options
+            assert len(selected) == x + 1
+            for j in range(len(selected)):
+                assert selected[j].text == select['values'][j]
+
+
+def testDeselectAllSingle(driver, pages):
+    pages.load("formPage.html")
+    for select in [singleSelectValues1, singleSelectValues2]:
+        with pytest.raises(NotImplementedError):
+            Select(driver.find_element(By.NAME, select['name'])).deselect_all()
+
+
+def testDeselectAllMultiple(driver, pages):
+    pages.load("formPage.html")
+    for select in [multiSelectValues1, multiSelectValues2]:
+        sel = Select(driver.find_element(By.NAME, select['name']))
+        sel.deselect_all()
+        assert len(sel.all_selected_options) == 0
+
+
+def testDeselectByIndexSingle(driver, pages):
+    pages.load("formPage.html")
+    for select in [singleSelectValues1, singleSelectValues2]:
+        with pytest.raises(NotImplementedError):
+            Select(driver.find_element(By.NAME, select['name'])).deselect_by_index(0)
+
+
+def testDeselectByValueSingle(driver, pages):
+    pages.load("formPage.html")
+    for select in [singleSelectValues1, singleSelectValues2]:
+        with pytest.raises(NotImplementedError):
+            Select(driver.find_element(By.NAME, select['name'])).deselect_by_value(select['values'][0].lower())
+
+
+def testDeselectByVisibleTextSingle(driver, pages):
+    pages.load("formPage.html")
+    for select in [singleSelectValues1, singleSelectValues2]:
+        with pytest.raises(NotImplementedError):
+            Select(driver.find_element(By.NAME, select['name'])).deselect_by_visible_text(select['values'][0])
+
+
+def testDeselectByIndexMultiple(driver, pages):
+    pages.load("formPage.html")
+    for select in [multiSelectValues1, multiSelectValues2]:
+        sel = Select(driver.find_element(By.NAME, select['name']))
+        sel.deselect_all()
+        sel.select_by_index(0)
+        sel.select_by_index(1)
+        sel.select_by_index(2)
+        sel.select_by_index(3)
+        sel.deselect_by_index(1)
+        sel.deselect_by_index(3)
+        selected = sel.all_selected_options
+        assert len(selected) == 2
+        assert selected[0].text == select['values'][0]
+        assert selected[1].text == select['values'][2]
+
+
+def testDeselectByValueMultiple(driver, pages):
+    pages.load("formPage.html")
+    for select in [multiSelectValues1, multiSelectValues2]:
+        sel = Select(driver.find_element(By.NAME, select['name']))
+        sel.deselect_all()
+        sel.select_by_index(0)
+        sel.select_by_index(1)
+        sel.select_by_index(2)
+        sel.select_by_index(3)
+        sel.deselect_by_value(select['values'][1].lower())
+        sel.deselect_by_value(select['values'][3].lower())
+        selected = sel.all_selected_options
+        assert len(selected) == 2
+        assert selected[0].text == select['values'][0]
+        assert selected[1].text == select['values'][2]
+
+
+def testDeselectByVisibleTextMultiple(driver, pages):
+    pages.load("formPage.html")
+    for select in [multiSelectValues1, multiSelectValues2]:
+        sel = Select(driver.find_element(By.NAME, select['name']))
+        sel.deselect_all()
+        sel.select_by_index(0)
+        sel.select_by_index(1)
+        sel.select_by_index(2)
+        sel.select_by_index(3)
+        sel.deselect_by_visible_text(select['values'][1])
+        sel.deselect_by_visible_text(select['values'][3])
+        selected = sel.all_selected_options
+        assert len(selected) == 2
+        assert selected[0].text == select['values'][0]
+        assert selected[1].text == select['values'][2]
+
+
+def testGetOptions(driver, pages):
+    pages.load("formPage.html")
+    for select in [singleSelectValues1, singleSelectValues2, multiSelectValues1, multiSelectValues2]:
+        opts = Select(driver.find_element(By.NAME, select['name'])).options
+        assert len(opts) == len(select['values'])
+        for i in range(len(opts)):
+            assert opts[i].text == select['values'][i]
+
+
+def testGetAllSelectedOptionsSingle(driver, pages):
+    pages.load("formPage.html")
+    for select in [singleSelectValues1, singleSelectValues2, disabledSelect]:
+        opts = Select(driver.find_element(By.NAME, select['name'])).all_selected_options
+        assert len(opts) == 1
+        assert opts[0].text == select['values'][0]
+
+
+def testGetAllSelectedOptionsMultiple(driver, pages):
+    pages.load("formPage.html")
+    opts = Select(driver.find_element(By.NAME, multiSelectValues1['name'])).all_selected_options
+    assert len(opts) == 2
+    assert opts[0].text, multiSelectValues1['values'][0]
+    assert opts[1].text, multiSelectValues1['values'][2]
+    opts = Select(driver.find_element(By.NAME, multiSelectValues2['name'])).all_selected_options
+    assert len(opts) == 0
+
+
+def testGetFirstSelectedOptionSingle(driver, pages):
+    pages.load("formPage.html")
+    for select in [singleSelectValues1, singleSelectValues2]:
+        opt = Select(driver.find_element(By.NAME, select['name'])).first_selected_option
+        assert opt.text == select['values'][0]
+
+
+def testGetFirstSelectedOptionMultiple(driver, pages):
+    pages.load("formPage.html")
+    opt = Select(driver.find_element(By.NAME, multiSelectValues1['name'])).first_selected_option
+    assert opt.text == multiSelectValues1['values'][0]
+    opt = Select(driver.find_element(By.NAME, multiSelectValues2['name'])).all_selected_options
+    assert len(opt) == 0
+
+
+def testRaisesExceptionForInvalidTagName(driver, pages):
+    pages.load("formPage.html")
+    with pytest.raises(UnexpectedTagNameException):
+        Select(driver.find_element(By.TAG_NAME, "div"))
+
+
+def testDeselectByIndexNonExistent(driver, pages):
+    pages.load("formPage.html")
+    for select in [multiSelectValues1, multiSelectValues2]:
+        with pytest.raises(NoSuchElementException):
+            Select(driver.find_element(By.NAME, select['name'])).deselect_by_index(10)
+
+
+def testDeselectByValueNonExistent(driver, pages):
+    pages.load("formPage.html")
+    for select in [multiSelectValues1, multiSelectValues2]:
+        with pytest.raises(NoSuchElementException):
+            Select(driver.find_element(By.NAME, select['name'])).deselect_by_value('not there')
+
+
+def testDeselectByTextNonExistent(driver, pages):
+    pages.load("formPage.html")
+    for select in [multiSelectValues1, multiSelectValues2]:
+        with pytest.raises(NoSuchElementException):
+            Select(driver.find_element(By.NAME, select['name'])).deselect_by_visible_text('not there')
diff --git a/test/selenium/webdriver/common/select_element_handling_tests.py b/test/selenium/webdriver/common/select_element_handling_tests.py
new file mode 100644
index 00000000..c6bfc7dc
--- /dev/null
+++ b/test/selenium/webdriver/common/select_element_handling_tests.py
@@ -0,0 +1,97 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.common.by import By
+
+
+def testShouldBePossibleToDeselectASingleOptionFromASelectWhichAllowsMultipleChoice(driver, pages):
+    pages.load("formPage.html")
+    multiSelect = driver.find_element(By.ID, "multi")
+    options = multiSelect.find_elements(By.TAG_NAME, "option")
+
+    option = options[0]
+    assert option.is_selected() is True
+    option.click()
+    assert option.is_selected() is False
+    option.click()
+    assert option.is_selected() is True
+
+    option = options[2]
+    assert option.is_selected() is True
+
+
+def testShouldBeAbleToChangeTheSelectedOptionInASelec(driver, pages):
+    pages.load("formPage.html")
+    selectBox = driver.find_element(By.XPATH, "//select[@name='selectomatic']")
+    options = selectBox.find_elements(By.TAG_NAME, "option")
+    one = options[0]
+    two = options[1]
+    assert one.is_selected() is True
+    assert two.is_selected() is False
+
+    two.click()
+    assert one.is_selected() is False
+    assert two.is_selected() is True
+
+
+def testShouldBeAbleToSelectMoreThanOneOptionFromASelectWhichAllowsMultipleChoice(driver, pages):
+    pages.load("formPage.html")
+
+    multiSelect = driver.find_element(By.ID, "multi")
+    options = multiSelect.find_elements(By.TAG_NAME, "option")
+    for option in options:
+        if not option.is_selected():
+            option.click()
+
+    for i in range(len(options)):
+        option = options[i]
+        assert option.is_selected() is True
+
+
+def testShouldSelectFirstOptionaultIfNoneIsSelecte(driver, pages):
+    pages.load("formPage.html")
+    selectBox = driver.find_element(By.XPATH, "//select[@name='select-default']")
+    options = selectBox.find_elements(By.TAG_NAME, "option")
+    one = options[0]
+    two = options[1]
+    assert one.is_selected() is True
+    assert two.is_selected() is False
+
+    two.click()
+    assert one.is_selected() is False
+    assert two.is_selected() is True
+
+
+def testCanSelectElementsInOptGroup(driver, pages):
+    pages.load("selectPage.html")
+    element = driver.find_element(By.ID, "two-in-group")
+    element.click()
+    assert element.is_selected() is True
+
+
+def testCanGetValueFromOptionViaAttributeWhenAttributeDoesntExis(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element(By.CSS_SELECTOR, "select[name='select-default'] option")
+    assert element.get_attribute("value") == "One"
+    element = driver.find_element(By.ID, "blankOption")
+    assert element.get_attribute("value") == ""
+
+
+def testCanGetValueFromOptionViaAttributeWhenAttributeIsEmptyString(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element(By.ID, "optionEmptyValueSet")
+    assert element.get_attribute("value") == ""
diff --git a/test/selenium/webdriver/common/stale_reference_tests.py b/test/selenium/webdriver/common/stale_reference_tests.py
new file mode 100644
index 00000000..bc81b256
--- /dev/null
+++ b/test/selenium/webdriver/common/stale_reference_tests.py
@@ -0,0 +1,45 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver.common.by import By
+from selenium.common.exceptions import StaleElementReferenceException
+
+
+def testOldPage(driver, pages):
+    pages.load("simpleTest.html")
+    elem = driver.find_element(by=By.ID, value="links")
+    pages.load("xhtmlTest.html")
+    with pytest.raises(StaleElementReferenceException):
+        elem.click()
+
+
+def testShouldNotCrashWhenCallingGetSizeOnAnObsoleteElement(driver, pages):
+    pages.load("simpleTest.html")
+    elem = driver.find_element(by=By.ID, value="links")
+    pages.load("xhtmlTest.html")
+    with pytest.raises(StaleElementReferenceException):
+        elem.size
+
+
+def testShouldNotCrashWhenQueryingTheAttributeOfAStaleElement(driver, pages):
+    pages.load("xhtmlTest.html")
+    heading = driver.find_element(by=By.XPATH, value="//h1")
+    pages.load("simpleTest.html")
+    with pytest.raises(StaleElementReferenceException):
+        heading.get_attribute("class")
diff --git a/test/selenium/webdriver/common/takes_screenshots_tests.py b/test/selenium/webdriver/common/takes_screenshots_tests.py
new file mode 100644
index 00000000..0c049cec
--- /dev/null
+++ b/test/selenium/webdriver/common/takes_screenshots_tests.py
@@ -0,0 +1,41 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import base64
+import imghdr
+
+import pytest
+
+
+def test_get_screenshot_as_base64(driver, pages):
+    pages.load("simpleTest.html")
+    result = base64.b64decode(driver.get_screenshot_as_base64())
+    assert imghdr.what('', result) == 'png'
+
+
+def test_get_screenshot_as_png(driver, pages):
+    pages.load("simpleTest.html")
+    result = driver.get_screenshot_as_png()
+    assert imghdr.what('', result) == 'png'
+
+
+@pytest.mark.xfail_firefox
+def test_get_element_screenshot(driver, pages):
+    pages.load("simpleTest.html")
+    element = driver.find_element_by_id("multiline")
+    result = base64.b64decode(element.screenshot_as_base64)
+    assert imghdr.what('', result) == 'png'
diff --git a/test/selenium/webdriver/common/text_handling_tests.py b/test/selenium/webdriver/common/text_handling_tests.py
new file mode 100644
index 00000000..9c970cef
--- /dev/null
+++ b/test/selenium/webdriver/common/text_handling_tests.py
@@ -0,0 +1,203 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver.common.by import By
+
+
+newLine = "\n"
+
+
+def testShouldReturnTheTextContentOfASingleElementWithNoChildren(driver, pages):
+    pages.load("simpleTest.html")
+    selectText = driver.find_element(by=By.ID, value="oneline").text
+    assert selectText == "A single line of text"
+
+    getText = driver.find_element(by=By.ID, value="oneline").text
+    assert getText == "A single line of text"
+
+
+def testShouldReturnTheEntireTextContentOfChildElements(driver, pages):
+    pages.load("simpleTest.html")
+    text = driver.find_element(by=By.ID, value="multiline").text
+
+    assert "A div containing" in text
+    assert "More than one line of text" in text
+    assert "and block level elements" in text
+
+
+def testShouldIgnoreScriptElements(driver, pages):
+    pages.load("javascriptEnhancedForm.html")
+    labelForUsername = driver.find_element(by=By.ID, value="labelforusername")
+    text = labelForUsername.text
+
+    assert len(labelForUsername.find_elements(by=By.TAG_NAME, value="script")) == 1
+    assert "document.getElementById" not in text
+    assert text == "Username:"
+
+
+def testShouldRepresentABlockLevelElementAsANewline(driver, pages):
+    pages.load("simpleTest.html")
+    text = driver.find_element(by=By.ID, value="multiline").text
+
+    assert text.startswith("A div containing" + newLine)
+    assert "More than one line of text" + newLine in text
+    assert text.endswith("and block level elements")
+
+
+def testShouldCollapseMultipleWhitespaceCharactersIntoASingleSpace(driver, pages):
+    pages.load("simpleTest.html")
+    text = driver.find_element(by=By.ID, value="lotsofspaces").text
+
+    assert text == "This line has lots of spaces."
+
+
+def testShouldTrimText(driver, pages):
+    pages.load("simpleTest.html")
+    text = driver.find_element(by=By.ID, value="multiline").text
+
+    assert text.startswith("A div containing")
+    assert text.endswith("block level elements")
+
+
+def testShouldConvertANonBreakingSpaceIntoANormalSpaceCharacter(driver, pages):
+    pages.load("simpleTest.html")
+    text = driver.find_element(by=By.ID, value="nbsp").text
+
+    assert text == "This line has a non-breaking space"
+
+
+def testShouldTreatANonBreakingSpaceAsAnyOtherWhitespaceCharacterWhenCollapsingWhitespace(driver, pages):
+    pages.load("simpleTest.html")
+    element = driver.find_element(by=By.ID, value="nbspandspaces")
+    text = element.text
+
+    assert text == "This line has a   non-breaking space and spaces"
+
+
+def testHavingInlineElementsShouldNotAffectHowTextIsReturned(driver, pages):
+    pages.load("simpleTest.html")
+    text = driver.find_element(by=By.ID, value="inline").text
+
+    assert text == "This line has text within elements that are meant to be displayed inline"
+
+
+def testShouldReturnTheEntireTextOfInlineElements(driver, pages):
+    pages.load("simpleTest.html")
+    text = driver.find_element(by=By.ID, value="span").text
+
+    assert text == "An inline element"
+
+
+def testShouldBeAbleToSetMoreThanOneLineOfTextInATextArea(driver, pages):
+    pages.load("formPage.html")
+    textarea = driver.find_element(by=By.ID, value="withText")
+    textarea.clear()
+
+    expectedText = "I like cheese" + newLine + newLine + "It's really nice"
+
+    textarea.send_keys(expectedText)
+
+    seenText = textarea.get_attribute("value")
+    assert seenText == expectedText
+
+
+def testShouldBeAbleToEnterDatesAfterFillingInOtherValuesFirst(driver, pages):
+    pages.load("formPage.html")
+    input_ = driver.find_element(by=By.ID, value="working")
+    expectedValue = "10/03/2007 to 30/07/1993"
+    input_.send_keys(expectedValue)
+    seenValue = input_.get_attribute("value")
+
+    assert seenValue == expectedValue
+
+
+def testShouldReturnEmptyStringWhenTextIsOnlySpaces(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    text = driver.find_element(by=By.ID, value="spaces").text
+    assert text == ""
+
+
+def testShouldReturnEmptyStringWhenTextIsEmpty(driver, pages):
+    pages.load("xhtmlTest.html")
+
+    text = driver.find_element(by=By.ID, value="empty").text
+    assert text == ""
+
+
+@pytest.mark.xfail
+def testShouldReturnEmptyStringWhenTagIsSelfClosing(driver, pages):
+    pages.load("xhtmlFormPage.xhtml")
+
+    text = driver.find_element(by=By.ID, value="self-closed").text
+    assert text == ""
+
+
+def testShouldHandleSiblingBlockLevelElements(driver, pages):
+    pages.load("simpleTest.html")
+
+    text = driver.find_element(by=By.ID, value="twoblocks").text
+    assert text == "Some text" + newLine + "Some more text"
+
+
+def testShouldHandleWhitespaceInInlineElements(driver, pages):
+    pages.load("simpleTest.html")
+
+    text = driver.find_element(by=By.ID, value="inlinespan").text
+    assert text == "line has text"
+
+
+def testReadALargeAmountOfData(driver, pages):
+    pages.load("macbeth.html")
+    source = driver.page_source.strip().lower()
+
+    assert source.endswith("</html>")
+
+
+def testShouldOnlyIncludeVisibleText(driver, pages):
+    pages.load("javascriptPage.html")
+
+    empty = driver.find_element(by=By.ID, value="suppressedParagraph").text
+    explicit = driver.find_element(by=By.ID, value="outer").text
+
+    assert "" == empty
+    assert "sub-element that is explicitly visible" == explicit
+
+
+def testShouldGetTextFromTableCells(driver, pages):
+    pages.load("tables.html")
+
+    tr = driver.find_element(by=By.ID, value="hidden_text")
+    text = tr.text
+
+    assert "some text" in text
+    assert "some more text" not in text
+
+
+def testShouldGetTextWhichIsAValidJSONObject(driver, pages):
+    pages.load("simpleTest.html")
+    element = driver.find_element(by=By.ID, value="simpleJsonText")
+    assert "{a=\"b\", c=1, d=true}" == element.text
+    # assert "{a=\"b\", \"c\"=d, e=true, f=\\123\\\\g\\\\\"\"\"\\\'}", element.text)
+
+
+def testShouldGetTextWhichIsAValidComplexJSONObject(driver, pages):
+    pages.load("simpleTest.html")
+    element = driver.find_element(by=By.ID, value="complexJsonText")
+    assert """{a=\"\\\\b\\\\\\\"\'\\\'\"}""" == element.text
diff --git a/test/selenium/webdriver/common/typing_tests.py b/test/selenium/webdriver/common/typing_tests.py
new file mode 100644
index 00000000..71230f37
--- /dev/null
+++ b/test/selenium/webdriver/common/typing_tests.py
@@ -0,0 +1,335 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.common.by import By
+from selenium.webdriver.common.keys import Keys
+
+import pytest
+
+
+def testShouldFireKeyPressEvents(driver, pages):
+    pages.load("javascriptPage.html")
+    keyReporter = driver.find_element(by=By.ID, value="keyReporter")
+    keyReporter.send_keys("a")
+    result = driver.find_element(by=By.ID, value="result")
+    assert "press:" in result.text
+
+
+def testShouldFireKeyDownEvents(driver, pages):
+    pages.load("javascriptPage.html")
+    keyReporter = driver.find_element(by=By.ID, value="keyReporter")
+    keyReporter.send_keys("I")
+    result = driver.find_element(by=By.ID, value="result")
+    assert "down" in result.text
+
+
+def testShouldFireKeyUpEvents(driver, pages):
+    pages.load("javascriptPage.html")
+    keyReporter = driver.find_element(by=By.ID, value="keyReporter")
+    keyReporter.send_keys("a")
+    result = driver.find_element(by=By.ID, value="result")
+    assert "up:" in result.text
+
+
+def testShouldTypeLowerCaseLetters(driver, pages):
+    pages.load("javascriptPage.html")
+    keyReporter = driver.find_element(by=By.ID, value="keyReporter")
+    keyReporter.send_keys("abc def")
+    assert keyReporter.get_attribute("value") == "abc def"
+
+
+def testShouldBeAbleToTypeCapitalLetters(driver, pages):
+    pages.load("javascriptPage.html")
+    keyReporter = driver.find_element(by=By.ID, value="keyReporter")
+    keyReporter.send_keys("ABC DEF")
+    assert keyReporter.get_attribute("value") == "ABC DEF"
+
+
+def testShouldBeAbleToTypeQuoteMarks(driver, pages):
+    pages.load("javascriptPage.html")
+    keyReporter = driver.find_element(by=By.ID, value="keyReporter")
+    keyReporter.send_keys("\"")
+    assert keyReporter.get_attribute("value") == "\""
+
+
+def testShouldBeAbleToTypeTheAtCharacter(driver, pages):
+    pages.load("javascriptPage.html")
+    keyReporter = driver.find_element(by=By.ID, value="keyReporter")
+    keyReporter.send_keys("@")
+    assert keyReporter.get_attribute("value") == "@"
+
+
+def testShouldBeAbleToMixUpperAndLowerCaseLetters(driver, pages):
+    pages.load("javascriptPage.html")
+    keyReporter = driver.find_element(by=By.ID, value="keyReporter")
+    keyReporter.send_keys("me@eXample.com")
+    assert keyReporter.get_attribute("value") == "me@eXample.com"
+
+
+def testArrowKeysShouldNotBePrintable(driver, pages):
+    pages.load("javascriptPage.html")
+    keyReporter = driver.find_element(by=By.ID, value="keyReporter")
+    keyReporter.send_keys(Keys.ARROW_LEFT)
+    assert keyReporter.get_attribute("value") == ""
+
+
+def testListOfArrowKeysShouldNotBePrintable(driver, pages):
+    pages.load("javascriptPage.html")
+    keyReporter = driver.find_element(by=By.ID, value="keyReporter")
+    keyReporter.send_keys([Keys.ARROW_LEFT])
+    assert keyReporter.get_attribute("value") == ""
+
+
+def testShouldBeAbleToUseArrowKeys(driver, pages):
+    pages.load("javascriptPage.html")
+    keyReporter = driver.find_element(by=By.ID, value="keyReporter")
+    keyReporter.send_keys("Tet", Keys.ARROW_LEFT, "s")
+    assert keyReporter.get_attribute("value") == "Test"
+
+
+def testWillSimulateAKeyUpWhenEnteringTextIntoInputElements(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="keyUp")
+    element.send_keys("I like cheese")
+    result = driver.find_element(by=By.ID, value="result")
+    assert result.text == "I like cheese"
+
+
+def testWillSimulateAKeyDownWhenEnteringTextIntoInputElements(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="keyDown")
+    element.send_keys("I like cheese")
+    result = driver.find_element(by=By.ID, value="result")
+    #  Because the key down gets the result before the input element is
+    #  filled, we're a letter short here
+    assert result.text == "I like chees"
+
+
+def testWillSimulateAKeyPressWhenEnteringTextIntoInputElements(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="keyPress")
+    element.send_keys("I like cheese")
+    result = driver.find_element(by=By.ID, value="result")
+    #  Because the key down gets the result before the input element is
+    #  filled, we're a letter short here
+    assert result.text == "I like chees"
+
+
+def testWillSimulateAKeyUpWhenEnteringTextIntoTextAreas(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="keyUpArea")
+    element.send_keys("I like cheese")
+    result = driver.find_element(by=By.ID, value="result")
+    assert result.text == "I like cheese"
+
+
+def testWillSimulateAKeyDownWhenEnteringTextIntoTextAreas(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="keyDownArea")
+    element.send_keys("I like cheese")
+    result = driver.find_element(by=By.ID, value="result")
+    #  Because the key down gets the result before the input element is
+    #  filled, we're a letter short here
+    assert result.text == "I like chees"
+
+
+def testWillSimulateAKeyPressWhenEnteringTextIntoTextAreas(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="keyPressArea")
+    element.send_keys("I like cheese")
+    result = driver.find_element(by=By.ID, value="result")
+    #  Because the key down gets the result before the input element is
+    #  filled, we're a letter short here
+    assert result.text == "I like chees"
+
+
+def testShouldReportKeyCodeOfArrowKeysUpDownEvents(driver, pages):
+    pages.load("javascriptPage.html")
+    result = driver.find_element(by=By.ID, value="result")
+    element = driver.find_element(by=By.ID, value="keyReporter")
+    element.send_keys(Keys.ARROW_DOWN)
+    assert "down: 40" in result.text.strip()
+    assert "up: 40" in result.text.strip()
+
+    element.send_keys(Keys.ARROW_UP)
+    assert "down: 38" in result.text.strip()
+    assert "up: 38" in result.text.strip()
+
+    element.send_keys(Keys.ARROW_LEFT)
+    assert "down: 37" in result.text.strip()
+    assert "up: 37" in result.text.strip()
+
+    element.send_keys(Keys.ARROW_RIGHT)
+    assert "down: 39" in result.text.strip()
+    assert "up: 39" in result.text.strip()
+
+    #  And leave no rubbish/printable keys in the "keyReporter"
+    assert element.get_attribute("value") == ""
+
+
+def testNumericNonShiftKeys(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="keyReporter")
+    numericLineCharsNonShifted = "`1234567890-=[]\\,.'/42"
+    element.send_keys(numericLineCharsNonShifted)
+    assert element.get_attribute("value") == numericLineCharsNonShifted
+
+
+@pytest.mark.xfail_marionette(
+    reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258')
+@pytest.mark.xfail_remote(
+    reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258')
+def testNumericShiftKeys(driver, pages):
+    pages.load("javascriptPage.html")
+    result = driver.find_element(by=By.ID, value="result")
+    element = driver.find_element(by=By.ID, value="keyReporter")
+    numericShiftsEtc = "~!@#$%^&*()_+{}:i\"<>?|END~"
+    element.send_keys(numericShiftsEtc)
+    assert element.get_attribute("value") == numericShiftsEtc
+    assert "up: 16" in result.text.strip()
+
+
+def testLowerCaseAlphaKeys(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="keyReporter")
+    lowerAlphas = "abcdefghijklmnopqrstuvwxyz"
+    element.send_keys(lowerAlphas)
+    assert element.get_attribute("value") == lowerAlphas
+
+
+@pytest.mark.xfail_marionette(
+    reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258')
+@pytest.mark.xfail_remote(
+    reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258')
+def testUppercaseAlphaKeys(driver, pages):
+    pages.load("javascriptPage.html")
+    result = driver.find_element(by=By.ID, value="result")
+    element = driver.find_element(by=By.ID, value="keyReporter")
+    upperAlphas = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+    element.send_keys(upperAlphas)
+    assert element.get_attribute("value") == upperAlphas
+    assert "up: 16" in result.text.strip()
+
+
+@pytest.mark.xfail_marionette(
+    reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258')
+@pytest.mark.xfail_remote(
+    reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258')
+def testAllPrintableKeys(driver, pages):
+    pages.load("javascriptPage.html")
+    result = driver.find_element(by=By.ID, value="result")
+    element = driver.find_element(by=By.ID, value="keyReporter")
+    allPrintable = "!\"#$%&'()*+,-./0123456789:<=>?@ ABCDEFGHIJKLMNOPQRSTUVWXYZ [\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
+    element.send_keys(allPrintable)
+
+    assert element.get_attribute("value") == allPrintable
+    assert "up: 16" in result.text.strip()
+
+
+def testArrowKeysAndPageUpAndDown(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="keyReporter")
+    element.send_keys(
+        "a" + Keys.LEFT + "b" + Keys.RIGHT +
+        Keys.UP + Keys.DOWN + Keys.PAGE_UP + Keys.PAGE_DOWN + "1")
+    assert element.get_attribute("value") == "ba1"
+
+
+# def testHomeAndEndAndPageUpAndPageDownKeys(driver, pages):
+#  // FIXME: macs don't have HOME keys, would PGUP work?
+#  if (Platform.getCurrent().is(Platform.MAC)) {
+#    return
+#  }
+
+#  pages.load("javascriptPage.html")
+
+#  element = driver.find_element(by=By.ID, value="keyReporter")
+
+#  element.send_keys("abc" + Keys.HOME + "0" + Keys.LEFT + Keys.RIGHT +
+#                   Keys.PAGE_UP + Keys.PAGE_DOWN + Keys.END + "1" + Keys.HOME +
+#                   "0" + Keys.PAGE_UP + Keys.END + "111" + Keys.HOME + "00")
+#  assert element.get_attribute("value") == "0000abc1111"
+
+
+def testDeleteAndBackspaceKeys(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="keyReporter")
+    element.send_keys("abcdefghi")
+    assert element.get_attribute("value") == "abcdefghi"
+
+    element.send_keys(Keys.LEFT, Keys.LEFT, Keys.DELETE)
+    assert element.get_attribute("value") == "abcdefgi"
+
+    element.send_keys(Keys.LEFT, Keys.LEFT, Keys.BACK_SPACE)
+    assert element.get_attribute("value") == "abcdfgi"
+
+
+@pytest.mark.xfail_marionette(
+    reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258')
+@pytest.mark.xfail_remote(
+    reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258')
+def testSpecialSpaceKeys(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="keyReporter")
+    element.send_keys("abcd" + Keys.SPACE + "fgh" + Keys.SPACE + "ij")
+    assert element.get_attribute("value") == "abcd fgh ij"
+
+
+@pytest.mark.xfail_marionette(
+    reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258')
+@pytest.mark.xfail_remote(
+    reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258')
+def testNumberpadAndFunctionKeys(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="keyReporter")
+    element.send_keys(
+        "abcd" + Keys.MULTIPLY + Keys.SUBTRACT + Keys.ADD +
+        Keys.DECIMAL + Keys.SEPARATOR + Keys.NUMPAD0 + Keys.NUMPAD9 +
+        Keys.ADD + Keys.SEMICOLON + Keys.EQUALS + Keys.DIVIDE +
+        Keys.NUMPAD3 + "abcd")
+    assert element.get_attribute("value") == "abcd*-+.,09+;=/3abcd"
+
+    element.clear()
+    element.send_keys("FUNCTION" + Keys.F2 + "-KEYS" + Keys.F2)
+    element.send_keys("" + Keys.F2 + "-TOO" + Keys.F2)
+    assert element.get_attribute("value") == "FUNCTION-KEYS-TOO"
+
+
+def testShiftSelectionDeletes(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="keyReporter")
+
+    element.send_keys("abcd efgh")
+    assert element.get_attribute("value") == "abcd efgh"
+
+    element.send_keys(Keys.SHIFT, Keys.LEFT, Keys.LEFT, Keys.LEFT)
+    element.send_keys(Keys.DELETE)
+    assert element.get_attribute("value") == "abcd e"
+
+
+def testShouldTypeIntoInputElementsThatHaveNoTypeAttribute(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element(by=By.ID, value="no-type")
+    element.send_keys("Should Say Cheese")
+    assert element.get_attribute("value") == "Should Say Cheese"
+
+
+def testShouldTypeAnInteger(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="keyReporter")
+    element.send_keys(1234)
+    assert element.get_attribute("value") == "1234"
diff --git a/test/selenium/webdriver/common/utils.py b/test/selenium/webdriver/common/utils.py
new file mode 100644
index 00000000..863b4256
--- /dev/null
+++ b/test/selenium/webdriver/common/utils.py
@@ -0,0 +1,26 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+def convert_cookie_to_json(cookie):
+    cookie_dict = {}
+    for key, value in cookie.items():
+        if key == "expires":
+            cookie_dict["expiry"] = int(value) * 1000
+        else:
+            cookie_dict[key] = value
+    return cookie_dict
diff --git a/test/selenium/webdriver/common/visibility_tests.py b/test/selenium/webdriver/common/visibility_tests.py
new file mode 100644
index 00000000..786ba858
--- /dev/null
+++ b/test/selenium/webdriver/common/visibility_tests.py
@@ -0,0 +1,143 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.common.exceptions import (
+    ElementNotVisibleException,
+    ElementNotInteractableException)
+from selenium.webdriver.common.by import By
+
+
+def testShouldAllowTheUserToTellIfAnElementIsDisplayedOrNot(driver, pages):
+    pages.load("javascriptPage.html")
+
+    assert driver.find_element(by=By.ID, value="displayed").is_displayed() is True
+    assert driver.find_element(by=By.ID, value="none").is_displayed() is False
+    assert driver.find_element(by=By.ID, value="suppressedParagraph").is_displayed() is False
+    assert driver.find_element(by=By.ID, value="hidden").is_displayed() is False
+
+
+def testVisibilityShouldTakeIntoAccountParentVisibility(driver, pages):
+    pages.load("javascriptPage.html")
+
+    childDiv = driver.find_element(by=By.ID, value="hiddenchild")
+    hiddenLink = driver.find_element(by=By.ID, value="hiddenlink")
+
+    assert childDiv.is_displayed() is False
+    assert hiddenLink.is_displayed() is False
+
+
+def testShouldCountElementsAsVisibleIfStylePropertyHasBeenSet(driver, pages):
+    pages.load("javascriptPage.html")
+    shown = driver.find_element(by=By.ID, value="visibleSubElement")
+    assert shown.is_displayed() is True
+
+
+def testShouldModifyTheVisibilityOfAnElementDynamically(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="hideMe")
+    assert element.is_displayed() is True
+    element.click()
+    assert element.is_displayed() is False
+
+
+def testHiddenInputElementsAreNeverVisible(driver, pages):
+    pages.load("javascriptPage.html")
+
+    shown = driver.find_element(by=By.NAME, value="hidden")
+
+    assert shown.is_displayed() is False
+
+
+def testShouldNotBeAbleToClickOnAnElementThatIsNotDisplayed(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="unclickable")
+    try:
+        element.click()
+        assert 1 == 0, "should have thrown an exception"
+    except (ElementNotVisibleException, ElementNotInteractableException):
+        pass
+
+
+def testShouldNotBeAbleToToggleAnElementThatIsNotDisplayed(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="untogglable")
+    try:
+        element.click()
+        assert 1 == 0, "should have thrown an exception"
+    except (ElementNotVisibleException, ElementNotInteractableException):
+        pass
+
+
+def testShouldNotBeAbleToSelectAnElementThatIsNotDisplayed(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="untogglable")
+    try:
+        element.click()
+        assert 1 == 0, "should have thrown an exception"
+    except (ElementNotVisibleException, ElementNotInteractableException):
+        pass
+
+
+def testShouldNotBeAbleToTypeAnElementThatIsNotDisplayed(driver, pages):
+    pages.load("javascriptPage.html")
+    element = driver.find_element(by=By.ID, value="unclickable")
+    try:
+        element.send_keys("You don't see me")
+        assert 1 == 0, "should have thrown an exception"
+    except (ElementNotVisibleException, ElementNotInteractableException):
+        pass
+    assert element.get_attribute("value") != "You don't see me"
+
+
+def testShouldSayElementsWithNegativeTransformAreNotDisplayed(driver, pages):
+    pages.load('cssTransform.html')
+    elementX = driver.find_element(By.ID, value='parentX')
+    assert elementX.is_displayed() is False
+    elementY = driver.find_element(By.ID, value='parentY')
+    assert elementY.is_displayed() is False
+
+
+def testShouldSayElementsWithParentWithNegativeTransformAreNotDisplayed(driver, pages):
+    pages.load('cssTransform.html')
+    elementX = driver.find_element(By.ID, value='childX')
+    assert elementX.is_displayed() is False
+    elementY = driver.find_element(By.ID, value='childY')
+    assert elementY.is_displayed() is False
+
+
+def testShouldSayElementWithZeroTransformIsVisible(driver, pages):
+    pages.load('cssTransform.html')
+    zero_tranform = driver.find_element(By.ID, 'zero-tranform')
+    assert zero_tranform.is_displayed() is True
+
+
+def testShouldSayElementIsVisibleWhenItHasNegativeTransformButElementisntInANegativeSpace(driver, pages):
+    pages.load('cssTransform2.html')
+    zero_tranform = driver.find_element(By.ID, 'negative-percentage-transformY')
+    assert zero_tranform.is_displayed() is True
+
+
+def testShouldShowElementNotVisibleWithHiddenAttribute(driver, pages):
+    pages.load('hidden.html')
+    singleHidden = driver.find_element(By.ID, 'singleHidden')
+    assert singleHidden.is_displayed() is False
+
+
+def testShouldShowElementNotVisibleWhenParentElementHasHiddenAttribute(driver, pages):
+    pages.load('hidden.html')
+    child = driver.find_element(By.ID, 'child')
+    assert child.is_displayed() is False
diff --git a/test/selenium/webdriver/common/w3c_interaction_tests.py b/test/selenium/webdriver/common/w3c_interaction_tests.py
new file mode 100644
index 00000000..0673caa6
--- /dev/null
+++ b/test/selenium/webdriver/common/w3c_interaction_tests.py
@@ -0,0 +1,209 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver.common.keys import Keys
+from selenium.webdriver.common.actions.action_builder import ActionBuilder
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def test_should_be_able_to_get_pointer_and_keyboard_inputs(driver, pages):
+    actions = ActionBuilder(driver)
+    pointers = actions.pointer_inputs
+    keyboards = actions.key_inputs
+
+    assert pointers is not None
+    assert keyboards is not None
+
+
+@pytest.mark.xfail_firefox
+@pytest.mark.xfail_remote(
+    reason='https://github.com/mozilla/geckodriver/issues/646')
+@pytest.mark.xfail_marionette(
+    reason='https://github.com/mozilla/geckodriver/issues/646')
+def testSendingKeysToActiveElementWithModifier(driver, pages):
+    pages.load("formPage.html")
+    e = driver.find_element_by_id("working")
+    e.click()
+
+    actions = ActionBuilder(driver)
+    key_action = actions.key_action
+    key_action.key_down(Keys.SHIFT) \
+        .send_keys("abc") \
+        .key_up(Keys.SHIFT)
+
+    actions.perform()
+
+    assert "ABC" == e.get_attribute('value')
+
+
+@pytest.mark.xfail_firefox
+def test_can_create_pause_action_on_keyboard(driver, pages):
+    # If we don't get an error and takes less than 3 seconds to run, we are good
+    import datetime
+    start = datetime.datetime.now()
+    actions1 = ActionBuilder(driver)
+    key_actions = actions1.key_action
+    key_actions.pause(1)
+    actions1.perform()
+    finish = datetime.datetime.now()
+    assert (finish - start).seconds <= 3
+
+    # Add a filler step
+    actions2 = ActionBuilder(driver)
+    key_action = actions2.key_action
+    key_action.pause()
+    actions2.perform()
+
+
+@pytest.mark.xfail_firefox
+def test_can_create_pause_action_on_pointer(driver, pages):
+    # If we don't get an error and takes less than 3 seconds to run, we are good
+    import datetime
+    start = datetime.datetime.now()
+    actions1 = ActionBuilder(driver)
+    key_actions = actions1.pointer_action
+    key_actions.pause(1)
+    actions1.perform()
+    finish = datetime.datetime.now()
+    assert (finish - start).seconds <= 3
+
+    # Add a filler step
+    actions2 = ActionBuilder(driver)
+    key_action = actions2.pointer_action
+    key_action.pause()
+    actions2.perform()
+
+
+@pytest.mark.xfail_firefox
+def test_can_clear_actions(driver, pages):
+    actions = ActionBuilder(driver)
+    actions.clear_actions()
+
+
+@pytest.mark.xfail_firefox
+def test_move_and_click(driver, pages):
+    pages.load("javascriptPage.html")
+    toClick = driver.find_element_by_id("clickField")
+
+    actions = ActionBuilder(driver)
+    pointer = actions.pointer_action
+
+    pointer.move_to(toClick) \
+           .click()
+
+    actions.perform()
+    assert "Clicked" == toClick.get_attribute('value')
+
+
+@pytest.mark.xfail_firefox
+def testDragAndDrop(driver, pages):
+    """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface."""
+    element_available_timeout = 15
+    wait = WebDriverWait(driver, element_available_timeout)
+    pages.load("droppableItems.html")
+    wait.until(lambda dr: _isElementAvailable(driver, "draggable"))
+
+    if not _isElementAvailable(driver, "draggable"):
+        raise AssertionError("Could not find draggable element after 15 seconds.")
+
+    toDrag = driver.find_element_by_id("draggable")
+    dropInto = driver.find_element_by_id("droppable")
+    actions = ActionBuilder(driver)
+    pointer = actions.pointer_action
+    pointer.click_and_hold(toDrag) \
+           .move_to(dropInto)\
+           .release()
+
+    actions.perform()
+
+    dropInto = driver.find_element_by_id("droppable")
+    text = dropInto.find_element_by_tag_name("p").text
+    assert "Dropped!" == text
+
+
+@pytest.mark.xfail_firefox
+def test_context_click(driver, pages):
+
+    pages.load("javascriptPage.html")
+    toContextClick = driver.find_element_by_id("doubleClickField")
+
+    actions = ActionBuilder(driver)
+    pointer = actions.pointer_action
+    pointer.context_click(toContextClick)
+
+    actions.perform()
+    assert "ContextClicked" == toContextClick.get_attribute('value')
+
+
+@pytest.mark.xfail_firefox
+def test_double_click(driver, pages):
+    """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface."""
+    pages.load("javascriptPage.html")
+    toDoubleClick = driver.find_element_by_id("doubleClickField")
+
+    actions = ActionBuilder(driver)
+    pointer = actions.pointer_action
+
+    pointer.double_click(toDoubleClick)
+
+    actions.perform()
+    assert "DoubleClicked" == toDoubleClick.get_attribute('value')
+
+
+@pytest.mark.xfail_firefox
+def test_dragging_element_with_mouse_moves_it_to_another_list(driver, pages):
+    _performDragAndDropWithMouse(driver, pages)
+    dragInto = driver.find_element_by_id("sortable1")
+    assert 6 == len(dragInto.find_elements_by_tag_name("li"))
+
+
+@pytest.mark.xfail_firefox
+def test_dragging_element_with_mouse_fires_events(driver, pages):
+    _performDragAndDropWithMouse(driver, pages)
+    dragReporter = driver.find_element_by_id("dragging_reports")
+    assert "Nothing happened. DragOut DropIn RightItem 3" == dragReporter.text
+
+
+def _performDragAndDropWithMouse(driver, pages):
+    """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface."""
+    pages.load("draggableLists.html")
+    dragReporter = driver.find_element_by_id("dragging_reports")
+    toDrag = driver.find_element_by_id("rightitem-3")
+    dragInto = driver.find_element_by_id("sortable1")
+
+    actions = ActionBuilder(driver)
+    pointer = actions.pointer_action
+    pointer.click_and_hold(toDrag) \
+           .move_to(driver.find_element_by_id("leftitem-4")) \
+           .move_to(dragInto) \
+           .release()
+
+    assert "Nothing happened." == dragReporter.text
+
+    actions.perform()
+    assert "Nothing happened. DragOut" in dragReporter.text
+
+
+def _isElementAvailable(driver, id):
+    """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface."""
+    try:
+        driver.find_element_by_id(id)
+        return True
+    except Exception:
+        return False
diff --git a/test/selenium/webdriver/common/webdriverwait_tests.py b/test/selenium/webdriver/common/webdriverwait_tests.py
new file mode 100644
index 00000000..aab83370
--- /dev/null
+++ b/test/selenium/webdriver/common/webdriverwait_tests.py
@@ -0,0 +1,323 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import time
+
+import pytest
+
+from selenium.common.exceptions import TimeoutException
+from selenium.common.exceptions import StaleElementReferenceException
+from selenium.common.exceptions import WebDriverException
+from selenium.common.exceptions import InvalidElementStateException
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+def throwSERE(driver):
+    raise StaleElementReferenceException("test")
+
+
+def testShouldExplicitlyWaitForASingleElement(driver, pages):
+    pages.load("dynamic.html")
+    add = driver.find_element_by_id("adder")
+    add.click()
+    WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "box0")))  # All is well if this doesn't throw.
+
+
+def testShouldStillFailToFindAnElementWithExplicitWait(driver, pages):
+    pages.load("dynamic.html")
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.presence_of_element_located((By.ID, "box0")))
+
+
+def testShouldExplicitlyWaituntilAtLeastOneElementIsFoundWhenSearchingForMany(driver, pages):
+    pages.load("dynamic.html")
+    add = driver.find_element_by_id("adder")
+
+    add.click()
+    add.click()
+
+    elements = WebDriverWait(driver, 2).until(EC.presence_of_all_elements_located((By.CLASS_NAME, "redbox")))
+    assert len(elements) >= 1
+
+
+def testShouldFailToFindElementsWhenExplicitWaiting(driver, pages):
+    pages.load("dynamic.html")
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.presence_of_all_elements_located((By.CLASS_NAME, "redbox")))
+
+
+def testShouldWaitUntilAtLeastOneVisibleElementsIsFoundWhenSearchingForMany(driver, pages):
+    pages.load("hidden_partially.html")
+    add_visible = driver.find_element_by_id("addVisible")
+    add_hidden = driver.find_element_by_id("addHidden")
+
+    add_visible.click()
+    add_visible.click()
+    add_hidden.click()
+
+    class wait_for_two_elements(object):
+
+        def __init__(self, locator):
+            self.locator = locator
+
+        def __call__(self, driver):
+            elements = [element for element in EC._find_elements(driver, self.locator) if EC._element_if_visible(element)]
+            return elements if len(elements) == 2 else False
+
+    elements = WebDriverWait(driver, 2).until(wait_for_two_elements((By.CLASS_NAME, "redbox")))
+    assert len(elements) == 2
+
+
+def testShouldFailToFindVisibleElementsWhenExplicitWaiting(driver, pages):
+    pages.load("hidden_partially.html")
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.visibility_of_any_elements_located((By.CLASS_NAME, "redbox")))
+
+
+def testShouldWaitUntilAllVisibleElementsAreFoundWhenSearchingForMany(driver, pages):
+    pages.load("hidden_partially.html")
+    add_visible = driver.find_element_by_id("addVisible")
+
+    add_visible.click()
+    add_visible.click()
+
+    elements = WebDriverWait(driver, 2).until(EC.visibility_of_all_elements_located((By.CLASS_NAME, "redbox")))
+    assert len(elements) == 2
+
+
+def testShouldFailIfNotAllElementsAreVisible(driver, pages):
+    pages.load("hidden_partially.html")
+    add_visible = driver.find_element_by_id("addVisible")
+    add_hidden = driver.find_element_by_id("addHidden")
+
+    add_visible.click()
+    add_hidden.click()
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.visibility_of_all_elements_located((By.CLASS_NAME, "redbox")))
+
+
+def testShouldWaitOnlyAsLongAsTimeoutSpecifiedWhenImplicitWaitsAreSet(driver, pages):
+    pages.load("dynamic.html")
+    driver.implicitly_wait(0.5)
+    start = time.time()
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 1).until(EC.presence_of_element_located((By.ID, "box0")))
+        assert time.time() - start < 1.5
+
+
+def testShouldWaitAtLeastOnce(driver, pages):
+    pages.load("simpleTest.html")
+    elements = WebDriverWait(driver, 0).until(lambda d: d.find_elements_by_tag_name('h1'))
+    assert len(elements) >= 1
+
+
+def testWaitUntilNotReturnsIfEvaluatesToFalse(driver, pages):
+    assert WebDriverWait(driver, 1).until_not(lambda d: False) is False
+
+
+def testWaitShouldStillFailIfProduceIgnoredException(driver, pages):
+    ignored = (InvalidElementStateException, StaleElementReferenceException)
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 1, 0.7, ignored_exceptions=ignored).until(throwSERE)
+
+
+def testWaitShouldStillFailIfProduceChildOfIgnoredException(driver, pages):
+    ignored = (WebDriverException)
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 1, 0.7, ignored_exceptions=ignored).until(throwSERE)
+
+
+def testWaitUntilNotShouldNotFailIfProduceIgnoredException(driver, pages):
+    ignored = (InvalidElementStateException, StaleElementReferenceException)
+    assert WebDriverWait(driver, 1, 0.7, ignored_exceptions=ignored).until_not(throwSERE)
+
+
+def testExpectedConditionTitleIs(driver, pages):
+    pages.load("blank.html")
+    WebDriverWait(driver, 1).until(EC.title_is("blank"))
+    driver.execute_script("setTimeout(function(){document.title='not blank'}, 200)")
+    WebDriverWait(driver, 1).until(EC.title_is("not blank"))
+    assert driver.title == 'not blank'
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.title_is("blank"))
+
+
+def testExpectedConditionTitleContains(driver, pages):
+    pages.load("blank.html")
+    driver.execute_script("setTimeout(function(){document.title='not blank'}, 200)")
+    WebDriverWait(driver, 1).until(EC.title_contains("not"))
+    assert driver.title == 'not blank'
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.title_contains("blanket"))
+
+
+def testExpectedConditionVisibilityOfElementLocated(driver, pages):
+    pages.load("javascriptPage.html")
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.visibility_of_element_located((By.ID, 'clickToHide')))
+    driver.find_element_by_id('clickToShow').click()
+    element = WebDriverWait(driver, 5).until(EC.visibility_of_element_located((By.ID, 'clickToHide')))
+    assert element.is_displayed() is True
+
+
+def testExpectedConditionVisibilityOf(driver, pages):
+    pages.load("javascriptPage.html")
+    hidden = driver.find_element_by_id('clickToHide')
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.visibility_of(hidden))
+    driver.find_element_by_id('clickToShow').click()
+    element = WebDriverWait(driver, 5).until(EC.visibility_of(hidden))
+    assert element.is_displayed() is True
+
+
+def testExpectedConditionTextToBePresentInElement(driver, pages):
+    pages.load('booleanAttributes.html')
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.text_to_be_present_in_element((By.ID, 'unwrappable'), 'Expected'))
+    driver.execute_script("setTimeout(function(){var el = document.getElementById('unwrappable'); el.textContent = el.innerText = 'Unwrappable Expected text'}, 200)")
+    WebDriverWait(driver, 1).until(EC.text_to_be_present_in_element((By.ID, 'unwrappable'), 'Expected'))
+    assert 'Unwrappable Expected text' == driver.find_element_by_id('unwrappable').text
+
+
+def testExpectedConditionTextToBePresentInElementValue(driver, pages):
+    pages.load('booleanAttributes.html')
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 1).until(EC.text_to_be_present_in_element_value((By.ID, 'inputRequired'), 'Expected'))
+    driver.execute_script("setTimeout(function(){document.getElementById('inputRequired').value = 'Example Expected text'}, 200)")
+    WebDriverWait(driver, 1).until(EC.text_to_be_present_in_element_value((By.ID, 'inputRequired'), 'Expected'))
+    assert 'Example Expected text' == driver.find_element_by_id('inputRequired').get_attribute('value')
+
+
+def testExpectedConditionFrameToBeAvailableAndSwitchToItByName(driver, pages):
+    pages.load("blank.html")
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 1).until(EC.frame_to_be_available_and_switch_to_it('myFrame'))
+    driver.execute_script("setTimeout(function(){var f = document.createElement('iframe'); f.id='myFrame'; f.src = '" + pages.url('iframeWithAlert.html') + "'; document.body.appendChild(f)}, 200)")
+    WebDriverWait(driver, 1).until(EC.frame_to_be_available_and_switch_to_it('myFrame'))
+    assert 'click me' == driver.find_element_by_id('alertInFrame').text
+
+
+def testExpectedConditionFrameToBeAvailableAndSwitchToItByLocator(driver, pages):
+    pages.load("blank.html")
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 1).until(EC.frame_to_be_available_and_switch_to_it((By.ID, 'myFrame')))
+    driver.execute_script("setTimeout(function(){var f = document.createElement('iframe'); f.id='myFrame'; f.src = '" + pages.url('iframeWithAlert.html') + "'; document.body.appendChild(f)}, 200)")
+    WebDriverWait(driver, 1).until(EC.frame_to_be_available_and_switch_to_it((By.ID, 'myFrame')))
+    assert 'click me' == driver.find_element_by_id('alertInFrame').text
+
+
+def testExpectedConditionInvisiblityOfElement(driver, pages):
+    pages.load("javascriptPage.html")
+    target = driver.find_element_by_id('clickToHide')
+    driver.execute_script("delayedShowHide(0, true)")
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.invisibility_of_element(target))
+    driver.execute_script("delayedShowHide(200, false)")
+    element = WebDriverWait(driver, 0.7).until(EC.invisibility_of_element(target))
+    assert element.is_displayed() is False
+    assert target == element
+
+
+def testExpectedConditionInvisiblityOfElementLocated(driver, pages):
+    pages.load("javascriptPage.html")
+    driver.execute_script("delayedShowHide(0, true)")
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.invisibility_of_element_located((By.ID, 'clickToHide')))
+    driver.execute_script("delayedShowHide(200, false)")
+    element = WebDriverWait(driver, 0.7).until(EC.invisibility_of_element_located((By.ID, 'clickToHide')))
+    assert element.is_displayed() is False
+
+
+def testExpectedConditionElementToBeClickable(driver, pages):
+    pages.load("javascriptPage.html")
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.element_to_be_clickable((By.ID, 'clickToHide')))
+    driver.execute_script("delayedShowHide(200, true)")
+    WebDriverWait(driver, 0.7).until(EC.element_to_be_clickable((By.ID, 'clickToHide')))
+    element = driver.find_element_by_id('clickToHide')
+    element.click()
+    WebDriverWait(driver, 3.5).until(EC.invisibility_of_element_located((By.ID, 'clickToHide')))
+    assert element.is_displayed() is False
+
+
+def testExpectedConditionStalenessOf(driver, pages):
+    pages.load('dynamicallyModifiedPage.html')
+    element = driver.find_element_by_id('element-to-remove')
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.staleness_of(element))
+    driver.find_element_by_id('buttonDelete').click()
+    assert 'element' == element.text
+    WebDriverWait(driver, 0.7).until(EC.staleness_of(element))
+    with pytest.raises(StaleElementReferenceException):
+        element.text
+
+
+def testExpectedConditionElementToBeSelected(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element_by_id('checky')
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.element_to_be_selected(element))
+    driver.execute_script("setTimeout(function(){document.getElementById('checky').checked = true}, 200)")
+    WebDriverWait(driver, 0.7).until(EC.element_to_be_selected(element))
+    assert element.is_selected() is True
+
+
+def testExpectedConditionElementLocatedToBeSelected(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element_by_id('checky')
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.element_located_to_be_selected((By.ID, 'checky')))
+    driver.execute_script("setTimeout(function(){document.getElementById('checky').checked = true}, 200)")
+    WebDriverWait(driver, 0.7).until(EC.element_located_to_be_selected((By.ID, 'checky')))
+    assert element.is_selected() is True
+
+
+def testExpectedConditionElementSelectionStateToBe(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element_by_id('checky')
+    WebDriverWait(driver, 0.7).until(EC.element_selection_state_to_be(element, False))
+    assert element.is_selected() is False
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.element_selection_state_to_be(element, True))
+    driver.execute_script("setTimeout(function(){document.getElementById('checky').checked = true}, 200)")
+    WebDriverWait(driver, 0.7).until(EC.element_selection_state_to_be(element, True))
+    assert element.is_selected() is True
+
+
+def testExpectedConditionElementLocatedSelectionStateToBe(driver, pages):
+    pages.load("formPage.html")
+    element = driver.find_element_by_id('checky')
+    WebDriverWait(driver, 0.7).until(EC.element_located_selection_state_to_be((By.ID, 'checky'), False))
+    assert element.is_selected() is False
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.element_located_selection_state_to_be((By.ID, 'checky'), True))
+    driver.execute_script("setTimeout(function(){document.getElementById('checky').checked = true}, 200)")
+    WebDriverWait(driver, 0.7).until(EC.element_located_selection_state_to_be((By.ID, 'checky'), True))
+    assert element.is_selected() is True
+
+
+def testExpectedConditionAlertIsPresent(driver, pages):
+    pages.load('blank.html')
+    with pytest.raises(TimeoutException):
+        WebDriverWait(driver, 0.7).until(EC.alert_is_present())
+    driver.execute_script("setTimeout(function(){alert('alerty')}, 200)")
+    WebDriverWait(driver, 0.7).until(EC.alert_is_present())
+    alert = driver.switch_to.alert
+    assert 'alerty' == alert.text
+    alert.dismiss()
diff --git a/test/selenium/webdriver/common/webserver.py b/test/selenium/webdriver/common/webserver.py
new file mode 100644
index 00000000..df916399
--- /dev/null
+++ b/test/selenium/webdriver/common/webserver.py
@@ -0,0 +1,159 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""A simple web server for testing purpose.
+It serves the testing html pages that are needed by the webdriver unit tests."""
+
+import logging
+import os
+import socket
+import threading
+from io import open
+try:
+    from urllib import request as urllib_request
+except ImportError:
+    import urllib as urllib_request
+try:
+    from http.server import BaseHTTPRequestHandler, HTTPServer
+    from socketserver import ThreadingMixIn
+except ImportError:
+    from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+    from SocketServer import ThreadingMixIn
+
+
+def updir():
+    dirname = os.path.dirname
+    return dirname(dirname(__file__))
+
+
+LOGGER = logging.getLogger(__name__)
+WEBDRIVER = os.environ.get("WEBDRIVER", updir())
+HTML_ROOT = os.path.join(WEBDRIVER, "../../../../common/src/web")
+if not os.path.isdir(HTML_ROOT):
+    message = ("Can't find 'common_web' directory, try setting WEBDRIVER"
+               " environment variable WEBDRIVER:" + WEBDRIVER + "  HTML_ROOT:" + HTML_ROOT)
+    LOGGER.error(message)
+    assert 0, message
+
+DEFAULT_HOST = "127.0.0.1"
+DEFAULT_PORT = 8000
+
+
+class HtmlOnlyHandler(BaseHTTPRequestHandler):
+    """Http handler."""
+    def do_GET(self):
+        """GET method handler."""
+        try:
+            path = self.path[1:].split('?')[0]
+            if path[:5] == "page/":
+                html = """<html><head><title>Page{page_number}</title></head>
+                <body>Page number <span id=\"pageNumber\">{page_number}</span>
+                <p><a href=\"../xhtmlTest.html\" target=\"_top\">top</a>
+                </body></html>""".format(page_number=path[5:])
+                html = html.encode('utf-8')
+            else:
+                with open(os.path.join(HTML_ROOT, path), 'r', encoding='latin-1') as f:
+                    html = f.read().encode('utf-8')
+            self.send_response(200)
+            self.send_header('Content-type', 'text/html')
+            self.end_headers()
+            self.wfile.write(html)
+        except IOError:
+            self.send_error(404, 'File Not Found: %s' % path)
+
+    def log_message(self, format, *args):
+        """Override default to avoid trashing stderr"""
+        pass
+
+
+class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
+    pass
+
+
+class SimpleWebServer(object):
+    """A very basic web server."""
+    def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT):
+        self.stop_serving = False
+        host = host
+        port = port
+        while True:
+            try:
+                self.server = ThreadedHTTPServer((host, port), HtmlOnlyHandler)
+                self.host = host
+                self.port = port
+                break
+            except socket.error:
+                LOGGER.debug("port %d is in use, trying to next one" % port)
+                port += 1
+
+        self.thread = threading.Thread(target=self._run_web_server)
+
+    def _run_web_server(self):
+        """Runs the server loop."""
+        LOGGER.debug("web server started")
+        while not self.stop_serving:
+            self.server.handle_request()
+        self.server.server_close()
+
+    def start(self):
+        """Starts the server."""
+        self.thread.start()
+
+    def stop(self):
+        """Stops the server."""
+        self.stop_serving = True
+        try:
+            # This is to force stop the server loop
+            urllib_request.URLopener().open("http://%s:%d" % (self.host, self.port))
+        except IOError:
+            pass
+        LOGGER.info("Shutting down the webserver")
+        self.thread.join()
+
+    def where_is(self, path):
+        return "http://%s:%d/%s" % (self.host, self.port, path)
+
+
+def main(argv=None):
+    from optparse import OptionParser
+    from time import sleep
+
+    if argv is None:
+        import sys
+        argv = sys.argv
+
+    parser = OptionParser("%prog [options]")
+    parser.add_option("-p", "--port", dest="port", type="int",
+                      help="port to listen (default: %s)" % DEFAULT_PORT,
+                      default=DEFAULT_PORT)
+
+    opts, args = parser.parse_args(argv[1:])
+    if args:
+        parser.error("wrong number of arguments")  # Will exit
+
+    server = SimpleWebServer(opts.port)
+    server.start()
+    print("Server started on port %s, hit CTRL-C to quit" % opts.port)
+    try:
+        while 1:
+            sleep(0.1)
+    except KeyboardInterrupt:
+        pass
+
+
+if __name__ == "__main__":
+    main()
diff --git a/test/selenium/webdriver/common/window_switching_tests.py b/test/selenium/webdriver/common/window_switching_tests.py
new file mode 100644
index 00000000..8ce87f6e
--- /dev/null
+++ b/test/selenium/webdriver/common/window_switching_tests.py
@@ -0,0 +1,205 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import NoSuchWindowException
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.common.by import By
+from selenium.webdriver.common.window import WindowTypes
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.support.ui import WebDriverWait
+
+
+@pytest.fixture(autouse=True)
+def close_windows(driver):
+    main_windows_handle = driver.current_window_handle
+    yield
+    try:
+        from urllib import request as url_request
+    except ImportError:
+        import urllib2 as url_request
+    URLError = url_request.URLError
+
+    try:
+        window_handles = driver.window_handles
+    except URLError:
+        return
+    for handle in window_handles:
+        if handle != main_windows_handle:
+            driver.switch_to.window(handle)
+            driver.close()
+    driver.switch_to.window(main_windows_handle)
+
+
+def testShouldSwitchFocusToANewWindowWhenItIsOpenedAndNotStopFutureOperations(driver, pages):
+    pages.load("xhtmlTest.html")
+    current = driver.current_window_handle
+
+    driver.find_element_by_link_text("Open new window").click()
+    assert driver.title == "XHTML Test Page"
+    handles = driver.window_handles
+    handles.remove(current)
+    driver.switch_to.window(handles[0])
+    assert driver.title == "We Arrive Here"
+
+    pages.load("iframes.html")
+    handle = driver.current_window_handle
+    driver.find_element_by_id("iframe_page_heading")
+    driver.switch_to.frame(driver.find_element(By.ID, "iframe1"))
+    assert driver.current_window_handle == handle
+
+
+def testCanSwitchToWindowByName(driver, pages):
+    pages.load("xhtmlTest.html")
+    handles = driver.window_handles
+    driver.find_element(By.LINK_TEXT, "Open new window").click()
+    WebDriverWait(driver, 3).until(EC.new_window_is_opened(handles))
+    driver.switch_to.window("result")
+    assert driver.title == "We Arrive Here"
+
+
+def testShouldThrowNoSuchWindowException(driver, pages):
+    pages.load("xhtmlTest.html")
+    with pytest.raises(NoSuchWindowException):
+        driver.switch_to.window("invalid name")
+
+
+def testShouldThrowNoSuchWindowExceptionOnAnAttemptToGetItsHandle(driver, pages):
+    pages.load("xhtmlTest.html")
+    current = driver.current_window_handle
+    handles = driver.window_handles
+    driver.find_element(By.LINK_TEXT, "Open new window").click()
+    WebDriverWait(driver, 3).until(EC.new_window_is_opened(handles))
+    handles = driver.window_handles
+    handles.remove(current)
+    driver.switch_to.window(handles[0])
+    driver.close()
+
+    with pytest.raises(NoSuchWindowException):
+        driver.current_window_handle
+
+
+@pytest.mark.xfail_ie
+def testShouldThrowNoSuchWindowExceptionOnAnyOperationIfAWindowIsClosed(driver, pages):
+    pages.load("xhtmlTest.html")
+    current = driver.current_window_handle
+    handles = driver.window_handles
+    driver.find_element(By.LINK_TEXT, "Open new window").click()
+    WebDriverWait(driver, 3).until(EC.new_window_is_opened(handles))
+    handles = driver.window_handles
+    handles.remove(current)
+    driver.switch_to.window(handles[0])
+    driver.close()
+
+    with pytest.raises(NoSuchWindowException):
+        driver.title
+
+    with pytest.raises(NoSuchWindowException):
+        driver.find_element_by_tag_name("body")
+
+
+@pytest.mark.xfail_ie
+def testShouldThrowNoSuchWindowExceptionOnAnyElementOperationIfAWindowIsClosed(driver, pages):
+    pages.load("xhtmlTest.html")
+    current = driver.current_window_handle
+    handles = driver.window_handles
+    driver.find_element(By.LINK_TEXT, "Open new window").click()
+    WebDriverWait(driver, 3).until(EC.new_window_is_opened(handles))
+    handles = driver.window_handles
+    handles.remove(current)
+    driver.switch_to.window(handles[0])
+    element = driver.find_element_by_tag_name("body")
+    driver.close()
+
+    with pytest.raises(NoSuchWindowException):
+        element.text
+
+
+def testClickingOnAButtonThatClosesAnOpenWindowDoesNotCauseTheBrowserToHang(driver, pages):
+    pages.load("xhtmlTest.html")
+    current = driver.current_window_handle
+    handles = driver.window_handles
+    driver.find_element_by_name("windowThree").click()
+    WebDriverWait(driver, 3).until(EC.new_window_is_opened(handles))
+    handles = driver.window_handles
+    handles.remove(current)
+    driver.switch_to.window(handles[0])
+    driver.find_element_by_id("close").click()
+    driver.switch_to.window(current)
+    driver.find_element_by_id("linkId")
+
+
+def testCanCallGetWindowHandlesAfterClosingAWindow(driver, pages):
+    pages.load("xhtmlTest.html")
+    current = driver.current_window_handle
+    handles = driver.window_handles
+    driver.find_element_by_name("windowThree").click()
+    WebDriverWait(driver, 3).until(EC.new_window_is_opened(handles))
+    handles = driver.window_handles
+    handles.remove(current)
+    driver.switch_to.window(handles[0])
+
+    driver.find_element_by_id("close").click()
+    WebDriverWait(driver, 3).until(EC.number_of_windows_to_be(1))
+
+
+def testCanObtainAWindowHandle(driver, pages):
+    pages.load("xhtmlTest.html")
+    currentHandle = driver.current_window_handle
+    assert currentHandle is not None
+
+
+def testFailingToSwitchToAWindowLeavesTheCurrentWindowAsIs(driver, pages):
+    pages.load("xhtmlTest.html")
+    current = driver.current_window_handle
+    with pytest.raises(NoSuchWindowException):
+        driver.switch_to.window("I will never exist")
+    new_handle = driver.current_window_handle
+    assert current == new_handle
+
+
+def testThatAccessingFindingAnElementAfterWindowIsClosedAndHaventswitchedDoesntCrash(driver, pages):
+    pages.load("xhtmlTest.html")
+    current = driver.current_window_handle
+    handles = driver.window_handles
+    driver.find_element_by_name("windowThree").click()
+    WebDriverWait(driver, 3).until(EC.new_window_is_opened(handles))
+    handles = driver.window_handles
+    handles.remove(current)
+    driver.switch_to.window(handles[0])
+
+    with pytest.raises(WebDriverException):
+        driver.find_element_by_id("close").click()
+        all_handles = driver.window_handles
+        assert 1 == len(all_handles)
+        driver.find_element_by_id("close")
+    driver.switch_to.window(current)
+
+
+@pytest.mark.xfail_ie
+@pytest.mark.xfail_chrome
+def testShouldBeAbleToCreateANewWindow(driver, pages):
+    original_handle = driver.current_window_handle
+
+    driver.switch_to.new_window(WindowTypes.TAB)
+    new_handle = driver.current_window_handle
+
+    driver.close()
+    driver.switch_to.window(original_handle)
+
+    assert new_handle != original_handle
diff --git a/test/selenium/webdriver/common/window_tests.py b/test/selenium/webdriver/common/window_tests.py
new file mode 100644
index 00000000..11eeb2a9
--- /dev/null
+++ b/test/selenium/webdriver/common/window_tests.py
@@ -0,0 +1,160 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import os
+import pytest
+
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.support.wait import WebDriverWait
+
+
+@pytest.mark.xfail_ie
+@pytest.mark.xfail_chrome(reason="Fails on Travis")
+@pytest.mark.xfail_marionette(reason="Fails on Travis")
+@pytest.mark.xfail_firefox(reason="Fails on Travis")
+@pytest.mark.xfail_remote(reason="Fails on Travis")
+def testShouldMaximizeTheWindow(driver):
+    resize_timeout = 5
+    wait = WebDriverWait(driver, resize_timeout)
+    old_size = driver.get_window_size()
+    driver.set_window_size(200, 200)
+    wait.until(
+        lambda dr: dr.get_window_size() != old_size if old_size["width"] != 200 and old_size["height"] != 200 else True)
+    size = driver.get_window_size()
+    driver.maximize_window()
+    wait.until(lambda dr: dr.get_window_size() != size)
+    new_size = driver.get_window_size()
+    assert new_size["width"] > size["width"]
+    assert new_size["height"] > size["height"]
+
+
+def test_should_get_the_size_of_the_current_window(driver):
+    size = driver.get_window_size()
+    assert size.get('width') > 0
+    assert size.get('height') > 0
+
+
+def test_should_set_the_size_of_the_current_window(driver):
+    size = driver.get_window_size()
+
+    target_width = size.get('width') - 20
+    target_height = size.get('height') - 20
+    driver.set_window_size(width=target_width, height=target_height)
+
+    new_size = driver.get_window_size()
+    assert new_size.get('width') == target_width
+    assert new_size.get('height') == target_height
+
+
+def test_should_get_the_position_of_the_current_window(driver):
+    position = driver.get_window_position()
+    assert position.get('x') >= 0
+    assert position.get('y') >= 0
+
+
+def test_should_set_the_position_of_the_current_window(driver):
+    position = driver.get_window_position()
+
+    target_x = position.get('x') + 10
+    target_y = position.get('y') + 10
+    driver.set_window_position(x=target_x, y=target_y)
+
+    WebDriverWait(driver, 2).until(lambda d: d.get_window_position()['x'] != position['x'] and
+                                   d.get_window_position()['y'] != position['y'])
+
+    new_position = driver.get_window_position()
+    assert new_position.get('x') == target_x
+    assert new_position.get('y') == target_y
+
+
+@pytest.mark.xfail_firefox(raises=WebDriverException,
+                           reason='Get Window Rect command not implemented')
+@pytest.mark.xfail_safari(raises=WebDriverException,
+                          reason='Get Window Rect command not implemented')
+def test_should_get_the_rect_of_the_current_window(driver):
+    rect = driver.get_window_rect()
+    assert rect.get('x') >= 0
+    assert rect.get('y') >= 0
+    assert rect.get('width') >= 0
+    assert rect.get('height') >= 0
+
+
+@pytest.mark.xfail_firefox(raises=WebDriverException,
+                           reason='Get Window Rect command not implemented')
+@pytest.mark.xfail_safari(raises=WebDriverException,
+                          reason='Get Window Rect command not implemented')
+def test_should_set_the_rect_of_the_current_window(driver):
+    rect = driver.get_window_rect()
+
+    target_x = rect.get('x') + 10
+    target_y = rect.get('y') + 10
+    target_width = rect.get('width') + 10
+    target_height = rect.get('height') + 10
+
+    driver.set_window_rect(x=target_x, y=target_y, width=target_width, height=target_height)
+
+    WebDriverWait(driver, 2).until(lambda d: d.get_window_position()['x'] != rect['x'] and
+                                   d.get_window_position()['y'] != rect['y'])
+
+    new_rect = driver.get_window_rect()
+
+    assert new_rect.get('x') == target_x
+    assert new_rect.get('y') == target_y
+    assert new_rect.get('width') == target_width
+    assert new_rect.get('height') == target_height
+
+
+@pytest.mark.xfail_chrome(raises=WebDriverException,
+                          reason='Fullscreen command not implemented')
+@pytest.mark.xfail_firefox(raises=WebDriverException,
+                           reason='Fullscreen command not implemented')
+@pytest.mark.xfail_safari(raises=WebDriverException,
+                          reason='Fullscreen command not implemented')
+@pytest.mark.skipif(os.environ.get('CI') == 'true',
+                    reason='Fullscreen command causes Travis to hang')
+def test_should_fullscreen_the_current_window(driver):
+    start_width = driver.execute_script('return window.innerWidth;')
+    start_height = driver.execute_script('return window.innerHeight;')
+
+    driver.fullscreen_window()
+
+    WebDriverWait(driver, 2).until(lambda d: driver.execute_script('return window.innerWidth;') >
+                                   start_width)
+
+    end_width = driver.execute_script('return window.innerWidth;')
+    end_height = driver.execute_script('return window.innerHeight;')
+
+    driver.fullscreen_window()  # Restore to original size
+
+    assert end_width > start_width
+    assert end_height > start_height
+
+
+@pytest.mark.xfail_chrome(raises=WebDriverException,
+                          reason='Minimize command not implemented')
+@pytest.mark.xfail_firefox(raises=WebDriverException,
+                           reason='Minimize command not implemented')
+@pytest.mark.xfail_safari(raises=WebDriverException,
+                          reason='Minimize command not implemented')
+@pytest.mark.skipif(os.environ.get('CI') == 'true',
+                    reason='Minimize command causes Travis to hang')
+@pytest.mark.no_driver_after_test
+def test_should_minimize_the_current_window(driver):
+    driver.minimize_window()
+    minimized = driver.execute_script('return document.hidden;')
+    driver.quit()  # Kill driver so we aren't running minimized after
+
+    assert minimized is True
diff --git a/test/selenium/webdriver/firefox/__init__.py b/test/selenium/webdriver/firefox/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/selenium/webdriver/firefox/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/selenium/webdriver/firefox/conftest.py b/test/selenium/webdriver/firefox/conftest.py
new file mode 100644
index 00000000..2a842147
--- /dev/null
+++ b/test/selenium/webdriver/firefox/conftest.py
@@ -0,0 +1,33 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver import Firefox
+
+
+@pytest.fixture
+def capabilities():
+    capabilities = {'marionette': False}
+    return capabilities
+
+
+@pytest.fixture
+def driver(capabilities):
+    driver = Firefox(capabilities=capabilities)
+    yield driver
+    driver.quit()
diff --git a/test/selenium/webdriver/firefox/ff_launcher_tests.py b/test/selenium/webdriver/firefox/ff_launcher_tests.py
new file mode 100644
index 00000000..23d53894
--- /dev/null
+++ b/test/selenium/webdriver/firefox/ff_launcher_tests.py
@@ -0,0 +1,37 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver import Firefox
+
+
+def test_we_can_launch_multiple_firefox_instances(capabilities):
+    driver1 = Firefox(capabilities=capabilities)
+    driver2 = Firefox(capabilities=capabilities)
+    driver3 = Firefox(capabilities=capabilities)
+    driver1.quit()
+    driver2.quit()
+    driver3.quit()
+
+
+def test_launch_firefox_with_none_service_log_path(capabilities):
+    driver = Firefox(capabilities=capabilities, service_log_path=None)
+    driver.quit()
+
+
+def test_launch_firefox_with_empty_string_service_log_path(capabilities):
+    driver = Firefox(capabilities=capabilities, service_log_path="")
+    driver.quit()
diff --git a/test/selenium/webdriver/firefox/ff_profile_tests.py b/test/selenium/webdriver/firefox/ff_profile_tests.py
new file mode 100755
index 00000000..c88a3029
--- /dev/null
+++ b/test/selenium/webdriver/firefox/ff_profile_tests.py
@@ -0,0 +1,167 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import base64
+import os
+import zipfile
+
+try:
+    from io import BytesIO
+except ImportError:
+    from cStringIO import StringIO as BytesIO
+
+try:
+    unicode
+except NameError:
+    unicode = str
+
+from selenium.webdriver import Firefox, FirefoxProfile
+
+
+def test_that_we_can_accept_a_profile(capabilities, webserver):
+    profile1 = FirefoxProfile()
+    profile1.set_preference("browser.startup.homepage_override.mstone", "")
+    profile1.set_preference("startup.homepage_welcome_url", webserver.where_is('simpleTest.html'))
+    profile1.update_preferences()
+
+    profile2 = FirefoxProfile(profile1.path)
+    driver = Firefox(
+        capabilities=capabilities,
+        firefox_profile=profile2)
+    title = driver.title
+    driver.quit()
+    assert "Hello WebDriver" == title
+
+
+def test_that_prefs_are_written_in_the_correct_format():
+    profile = FirefoxProfile()
+    profile.set_preference("sample.preference", "hi there")
+    profile.update_preferences()
+
+    assert 'hi there' == profile.default_preferences["sample.preference"]
+
+    encoded = profile.encoded
+    decoded = base64.b64decode(encoded)
+    with BytesIO(decoded) as fp:
+        zip = zipfile.ZipFile(fp, "r")
+        for entry in zip.namelist():
+            if entry.endswith("user.js"):
+                user_js = zip.read(entry)
+                for line in user_js.splitlines():
+                    if line.startswith(b'user_pref("sample.preference",'):
+                        assert line.endswith(b'hi there");')
+            # there should be only one user.js
+            break
+
+
+def test_that_unicode_prefs_are_written_in_the_correct_format():
+    profile = FirefoxProfile()
+    profile.set_preference('sample.preference.2', unicode('hi there'))
+    profile.update_preferences()
+
+    assert 'hi there' == profile.default_preferences["sample.preference.2"]
+
+    encoded = profile.encoded
+    decoded = base64.b64decode(encoded)
+    with BytesIO(decoded) as fp:
+        zip = zipfile.ZipFile(fp, "r")
+        for entry in zip.namelist():
+            if entry.endswith('user.js'):
+                user_js = zip.read(entry)
+                for line in user_js.splitlines():
+                    if line.startswith(b'user_pref("sample.preference.2",'):
+                        assert line.endswith(b'hi there");')
+            # there should be only one user.js
+            break
+
+
+def test_that_integer_prefs_are_written_in_the_correct_format():
+    profile = FirefoxProfile()
+    profile.set_preference("sample.int.preference", 12345)
+    profile.update_preferences()
+    assert 12345 == profile.default_preferences["sample.int.preference"]
+
+
+def test_that_boolean_prefs_are_written_in_the_correct_format():
+    profile = FirefoxProfile()
+    profile.set_preference("sample.bool.preference", True)
+    profile.update_preferences()
+    assert profile.default_preferences["sample.bool.preference"] is True
+
+
+def test_that_we_delete_the_profile(capabilities):
+    driver = Firefox(capabilities=capabilities)
+    path = driver.firefox_profile.path
+    driver.quit()
+    assert not os.path.exists(path)
+
+
+def test_profiles_do_not_share_preferences():
+    profile1 = FirefoxProfile()
+    profile1.accept_untrusted_certs = False
+    profile2 = FirefoxProfile()
+    # Default is true. Should remain so.
+    assert profile2.default_preferences["webdriver_accept_untrusted_certs"] is True
+
+
+def test_add_extension_web_extension_with_id(capabilities, webserver):
+    current_directory = os.path.dirname(os.path.realpath(__file__))
+    root_directory = os.path.join(current_directory, '..', '..', '..', '..', '..')
+    # TODO: This file should probably live in a common directory.
+    extension_path = os.path.join(root_directory, 'javascript', 'node', 'selenium-webdriver',
+                                  'lib', 'test', 'data', 'firefox', 'webextension.xpi')
+
+    profile = FirefoxProfile()
+    profile.add_extension(extension_path)
+
+    driver = Firefox(capabilities=capabilities, firefox_profile=profile)
+    profile_path = driver.firefox_profile.path
+    extension_path_in_profile = os.path.join(profile_path, 'extensions', 'webextensions-selenium-example@example.com')
+    assert os.path.exists(extension_path_in_profile)
+    driver.get(webserver.where_is('simpleTest.html'))
+    driver.find_element_by_id('webextensions-selenium-example')
+    driver.quit()
+
+
+def test_add_extension_web_extension_without_id(capabilities, webserver):
+    current_directory = os.path.dirname(os.path.realpath(__file__))
+    root_directory = os.path.join(current_directory, '..', '..', '..', '..', '..')
+    extension_path = os.path.join(root_directory, 'third_party', 'firebug', 'mooltipass-1.1.87.xpi')
+
+    profile = FirefoxProfile()
+    profile.add_extension(extension_path)
+
+    driver = Firefox(capabilities=capabilities, firefox_profile=profile)
+    profile_path = driver.firefox_profile.path
+    extension_path_in_profile = os.path.join(profile_path, 'extensions', 'MooltipassExtension@1.1.87')
+    assert os.path.exists(extension_path_in_profile)
+    driver.quit()
+
+
+def test_add_extension_legacy_extension(capabilities, webserver):
+    current_directory = os.path.dirname(os.path.realpath(__file__))
+    root_directory = os.path.join(current_directory, '..', '..', '..', '..', '..')
+    extension_path = os.path.join(root_directory, 'third_party', 'firebug', 'firebug-1.5.0-fx.xpi')
+
+    profile = FirefoxProfile()
+    profile.add_extension(extension_path)
+
+    driver = Firefox(capabilities=capabilities, firefox_profile=profile)
+    profile_path = driver.firefox_profile.path
+    extension_path_in_profile = os.path.join(profile_path, 'extensions', 'firebug@software.joehewitt.com')
+    assert os.path.exists(extension_path_in_profile)
+    driver.quit()
diff --git a/test/selenium/webdriver/firefox/ff_takes_full_page_screenshots_tests.py b/test/selenium/webdriver/firefox/ff_takes_full_page_screenshots_tests.py
new file mode 100644
index 00000000..01c1d435
--- /dev/null
+++ b/test/selenium/webdriver/firefox/ff_takes_full_page_screenshots_tests.py
@@ -0,0 +1,31 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import base64
+import imghdr
+
+
+def test_get_full_page_screenshot_as_base64(driver, pages):
+    pages.load("simpleTest.html")
+    result = base64.b64decode(driver.get_full_page_screenshot_as_base64())
+    assert imghdr.what('', result) == 'png'
+
+
+def test_get_full_page_screenshot_as_png(driver, pages):
+    pages.load("simpleTest.html")
+    result = driver.get_full_page_screenshot_as_png()
+    assert imghdr.what('', result) == 'png'
diff --git a/test/selenium/webdriver/ie/__init__.py b/test/selenium/webdriver/ie/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/selenium/webdriver/ie/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/selenium/webdriver/ie/ie_launcher_tests.py b/test/selenium/webdriver/ie/ie_launcher_tests.py
new file mode 100755
index 00000000..88d627f0
--- /dev/null
+++ b/test/selenium/webdriver/ie/ie_launcher_tests.py
@@ -0,0 +1,52 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver import Ie
+from selenium.webdriver.ie.options import Options
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+
+
+def test_launch_and_close_browser():
+    driver = Ie()
+    driver.quit()
+
+
+def test_we_can_launch_multiple_ie_instances():
+    driver1 = Ie()
+    driver2 = Ie()
+    driver3 = Ie()
+    driver1.quit()
+    driver2.quit()
+    driver3.quit()
+
+
+def test_launch_ie_do_not_affect_default_capabilities():
+    expected = DesiredCapabilities.INTERNETEXPLORER.copy()
+    driver = Ie()
+    actual = DesiredCapabilities.INTERNETEXPLORER.copy()
+    driver.quit()
+    assert actual == expected
+
+
+def test_launch_ie_with_options(pages):
+    opts = Options()
+    expected = "clicks.html"
+    opts.initial_browser_url = pages.url(expected)
+    driver = Ie(options=opts)
+    actual = driver.current_url
+    driver.quit()
+    assert expected in actual
diff --git a/test/selenium/webdriver/marionette/__init__.py b/test/selenium/webdriver/marionette/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/selenium/webdriver/marionette/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/selenium/webdriver/marionette/conftest.py b/test/selenium/webdriver/marionette/conftest.py
new file mode 100644
index 00000000..77871a7e
--- /dev/null
+++ b/test/selenium/webdriver/marionette/conftest.py
@@ -0,0 +1,43 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver import Firefox
+
+
+@pytest.fixture
+def capabilities():
+    capabilities = {'marionette': True}
+    return capabilities
+
+
+@pytest.fixture
+def driver_class():
+    return Firefox
+
+
+@pytest.fixture
+def driver_kwargs(capabilities):
+    return {'capabilities': capabilities}
+
+
+@pytest.fixture
+def driver(driver_class, driver_kwargs):
+    driver = driver_class(**driver_kwargs)
+    yield driver
+    driver.quit()
diff --git a/test/selenium/webdriver/marionette/mn_binary_tests.py b/test/selenium/webdriver/marionette/mn_binary_tests.py
new file mode 100644
index 00000000..5c8efde1
--- /dev/null
+++ b/test/selenium/webdriver/marionette/mn_binary_tests.py
@@ -0,0 +1,35 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.firefox.firefox_binary import FirefoxBinary
+
+
+@pytest.fixture(params=['foo', FirefoxBinary(firefox_path='foo')],
+                ids=['string', 'binary'])
+def driver_kwargs(request, driver_kwargs):
+    driver_kwargs['firefox_binary'] = request.param
+    return driver_kwargs
+
+
+@pytest.mark.xfail(reason='https://github.com/mozilla/geckodriver/issues/832')
+def test_invalid_binary(driver_class, driver_kwargs):
+    with pytest.raises(WebDriverException) as excinfo:
+        driver_class(**driver_kwargs)
+    assert 'no such file or directory' in str(excinfo.value)
diff --git a/test/selenium/webdriver/marionette/mn_context_tests.py b/test/selenium/webdriver/marionette/mn_context_tests.py
new file mode 100644
index 00000000..8722e98c
--- /dev/null
+++ b/test/selenium/webdriver/marionette/mn_context_tests.py
@@ -0,0 +1,27 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+def test_context_sets_correct_context_and_returns(driver):
+
+    def get_context():
+        return driver.execute('GET_CONTEXT').pop('value')
+
+    assert get_context() == driver.CONTEXT_CONTENT
+    with driver.context(driver.CONTEXT_CHROME):
+        assert get_context() == driver.CONTEXT_CHROME
+    assert get_context() == driver.CONTEXT_CONTENT
diff --git a/test/selenium/webdriver/marionette/mn_launcher_tests.py b/test/selenium/webdriver/marionette/mn_launcher_tests.py
new file mode 100644
index 00000000..e81f0258
--- /dev/null
+++ b/test/selenium/webdriver/marionette/mn_launcher_tests.py
@@ -0,0 +1,41 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver import Firefox
+
+
+def test_launch_and_close_browser(driver):
+    assert 'browserName' in driver.capabilities
+
+
+def test_we_can_launch_multiple_firefox_instances(capabilities):
+    driver1 = Firefox(capabilities=capabilities)
+    driver2 = Firefox(capabilities=capabilities)
+    driver3 = Firefox(capabilities=capabilities)
+    driver1.quit()
+    driver2.quit()
+    driver3.quit()
+
+
+def test_launch_firefox_with_none_service_log_path(capabilities):
+    driver = Firefox(capabilities=capabilities, service_log_path=None)
+    driver.quit()
+
+
+def test_launch_firefox_with_empty_string_service_log_path(capabilities):
+    driver = Firefox(capabilities=capabilities, service_log_path="")
+    driver.quit()
diff --git a/test/selenium/webdriver/marionette/mn_options_tests.py b/test/selenium/webdriver/marionette/mn_options_tests.py
new file mode 100644
index 00000000..33e988b2
--- /dev/null
+++ b/test/selenium/webdriver/marionette/mn_options_tests.py
@@ -0,0 +1,132 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+try:
+    basestring
+except NameError:  # Python 3.x
+    basestring = str
+
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+from selenium.webdriver.firefox.firefox_binary import FirefoxBinary
+from selenium.webdriver.firefox.firefox_profile import FirefoxProfile
+from selenium.webdriver.firefox.options import Log, Options
+
+
+@pytest.fixture
+def driver_kwargs(driver_kwargs):
+    driver_kwargs['options'] = Options()
+    return driver_kwargs
+
+
+class TestIntegration(object):
+    def test_we_can_pass_options(self, driver, pages):
+        pages.load("formPage.html")
+        driver.find_element_by_id("cheese")
+
+
+class TestUnit(object):
+    def test_ctor(self):
+        opts = Options()
+        assert opts._binary is None
+        assert opts._preferences == {}
+        assert opts._profile is None
+        assert opts._arguments == []
+        assert isinstance(opts.log, Log)
+
+    def test_binary(self):
+        opts = Options()
+        assert opts.binary is None
+
+        other_binary = FirefoxBinary()
+        assert other_binary != opts.binary
+        opts.binary = other_binary
+        assert other_binary == opts.binary
+
+        path = "/path/to/binary"
+        opts.binary = path
+        assert isinstance(opts.binary, FirefoxBinary)
+        assert opts.binary._start_cmd == path
+
+    def test_prefs(self):
+        opts = Options()
+        assert len(opts.preferences) == 0
+        assert isinstance(opts.preferences, dict)
+
+        opts.set_preference("spam", "ham")
+        assert len(opts.preferences) == 1
+        opts.set_preference("eggs", True)
+        assert len(opts.preferences) == 2
+        opts.set_preference("spam", "spam")
+        assert len(opts.preferences) == 2
+        assert opts.preferences == {"spam": "spam", "eggs": True}
+
+    def test_profile(self, tmpdir_factory):
+        opts = Options()
+        assert opts.profile is None
+
+        other_profile = FirefoxProfile()
+        assert other_profile != opts.profile
+        opts.profile = other_profile
+        assert other_profile == opts.profile
+
+        opts.profile = str(tmpdir_factory.mktemp("profile"))
+        assert isinstance(opts.profile, FirefoxProfile)
+
+    def test_arguments(self):
+        opts = Options()
+        assert len(opts.arguments) == 0
+
+        opts.add_argument("--foo")
+        assert len(opts.arguments) == 1
+        opts.arguments.append("--bar")
+        assert len(opts.arguments) == 2
+        assert opts.arguments == ["--foo", "--bar"]
+
+    def test_to_capabilities(self):
+        opts = Options()
+        assert opts.to_capabilities() == DesiredCapabilities.FIREFOX
+
+        profile = FirefoxProfile()
+        opts.profile = profile
+        caps = opts.to_capabilities()
+        assert "moz:firefoxOptions" in caps
+        assert "profile" in caps["moz:firefoxOptions"]
+        assert isinstance(caps["moz:firefoxOptions"]["profile"], basestring)
+        assert caps["moz:firefoxOptions"]["profile"] == profile.encoded
+
+        opts.add_argument("--foo")
+        caps = opts.to_capabilities()
+        assert "moz:firefoxOptions" in caps
+        assert "args" in caps["moz:firefoxOptions"]
+        assert caps["moz:firefoxOptions"]["args"] == ["--foo"]
+
+        binary = FirefoxBinary()
+        opts.binary = binary
+        caps = opts.to_capabilities()
+        assert "moz:firefoxOptions" in caps
+        assert "binary" in caps["moz:firefoxOptions"]
+        assert isinstance(caps["moz:firefoxOptions"]["binary"], basestring)
+        assert caps["moz:firefoxOptions"]["binary"] == binary._start_cmd
+
+        opts.set_preference("spam", "ham")
+        caps = opts.to_capabilities()
+        assert "moz:firefoxOptions" in caps
+        assert "prefs" in caps["moz:firefoxOptions"]
+        assert isinstance(caps["moz:firefoxOptions"]["prefs"], dict)
+        assert caps["moz:firefoxOptions"]["prefs"]["spam"] == "ham"
diff --git a/test/selenium/webdriver/marionette/mn_preferences_tests.py b/test/selenium/webdriver/marionette/mn_preferences_tests.py
new file mode 100644
index 00000000..21502263
--- /dev/null
+++ b/test/selenium/webdriver/marionette/mn_preferences_tests.py
@@ -0,0 +1,33 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver.firefox.options import Options
+
+
+@pytest.fixture
+def driver_kwargs(request, driver_kwargs):
+    options = Options()
+    options.set_preference('browser.startup.homepage_override.mstone', '')
+    options.set_preference('startup.homepage_welcome_url', 'about:')
+    driver_kwargs['options'] = options
+    return driver_kwargs
+
+
+def test_preferences_are_used(driver):
+    assert 'about:' == driver.current_url
diff --git a/test/selenium/webdriver/marionette/mn_profile_tests.py b/test/selenium/webdriver/marionette/mn_profile_tests.py
new file mode 100644
index 00000000..4b0895bb
--- /dev/null
+++ b/test/selenium/webdriver/marionette/mn_profile_tests.py
@@ -0,0 +1,57 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import os
+
+import pytest
+
+from selenium.webdriver import FirefoxProfile
+from selenium.webdriver.firefox.options import Options
+
+
+@pytest.fixture(params=['capabilities', 'firefox_profile', 'options'])
+def driver_kwargs(request, driver_kwargs, profile):
+    if request.param == 'capabilities':
+        options = {'profile': profile}
+        driver_kwargs[request.param].setdefault('moz:firefoxOptions', options)
+    elif request.param == 'firefox_profile':
+        driver_kwargs[request.param] = profile
+    elif request.param == 'options':
+        options = Options()
+        options.profile = profile
+        driver_kwargs[request.param] = options
+    driver_kwargs['firefox_profile'] = profile
+    return driver_kwargs
+
+
+@pytest.fixture
+def profile():
+    profile = FirefoxProfile()
+    profile.set_preference('browser.startup.homepage_override.mstone', '')
+    profile.set_preference('startup.homepage_welcome_url', 'about:')
+    profile.update_preferences()
+    return profile
+
+
+def test_profile_is_used(driver):
+    assert 'about:' == driver.current_url
+
+
+def test_profile_is_deleted(driver, profile):
+    assert os.path.exists(profile.path)
+    driver.quit()
+    assert not os.path.exists(profile.path)
diff --git a/test/selenium/webdriver/marionette/mn_service_tests.py b/test/selenium/webdriver/marionette/mn_service_tests.py
new file mode 100644
index 00000000..b3f783b7
--- /dev/null
+++ b/test/selenium/webdriver/marionette/mn_service_tests.py
@@ -0,0 +1,32 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.firefox.service import Service
+
+
+def test_command_line_args():
+    service = Service("geckodriver", service_args=["--log", "trace"])
+    found = False
+
+    args = service.command_line_args()
+
+    for idx in range(len(args) - 1):
+        if args[idx] == "--log" and args[idx + 1] == "trace":
+            found = True
+            break
+
+    assert found, "Provided arguments do not exist in array"
diff --git a/test/selenium/webdriver/marionette/mn_set_context_tests.py b/test/selenium/webdriver/marionette/mn_set_context_tests.py
new file mode 100644
index 00000000..6a64a713
--- /dev/null
+++ b/test/selenium/webdriver/marionette/mn_set_context_tests.py
@@ -0,0 +1,21 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+def test_we_can_switch_context_to_chrome(driver):
+    driver.set_context('chrome')
+    assert 1 == driver.execute_script("var c = Components.classes; return 1;")
diff --git a/test/selenium/webdriver/remote/__init__.py b/test/selenium/webdriver/remote/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/selenium/webdriver/remote/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/selenium/webdriver/remote/remote_firefox_profile_tests.py b/test/selenium/webdriver/remote/remote_firefox_profile_tests.py
new file mode 100644
index 00000000..5e60db95
--- /dev/null
+++ b/test/selenium/webdriver/remote/remote_firefox_profile_tests.py
@@ -0,0 +1,45 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+from selenium import webdriver
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+
+
+@pytest.fixture
+def capabilities():
+    capabilities = DesiredCapabilities.FIREFOX.copy()
+    capabilities['marionette'] = False
+    return capabilities
+
+
+@pytest.fixture
+def driver(options):
+    driver = webdriver.Remote(options=options)
+    yield driver
+    driver.quit()
+
+
+@pytest.fixture
+def options():
+    options = webdriver.FirefoxOptions()
+    options.set_preference('browser.startup.homepage', 'about:')
+    return options
+
+
+def test_profile_is_used(driver):
+    assert 'about:blank' == driver.current_url or 'about:' == driver.current_url
diff --git a/test/selenium/webdriver/safari/conftest.py b/test/selenium/webdriver/safari/conftest.py
new file mode 100644
index 00000000..6aa8968b
--- /dev/null
+++ b/test/selenium/webdriver/safari/conftest.py
@@ -0,0 +1,37 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver import Safari
+
+
+@pytest.fixture
+def driver_class():
+    return Safari
+
+
+@pytest.fixture
+def driver_kwargs():
+    return {}
+
+
+@pytest.fixture
+def driver(driver_class, driver_kwargs):
+    driver = driver_class(**driver_kwargs)
+    yield driver
+    driver.quit()
diff --git a/test/selenium/webdriver/safari/launcher_tests.py b/test/selenium/webdriver/safari/launcher_tests.py
new file mode 100644
index 00000000..26601a6f
--- /dev/null
+++ b/test/selenium/webdriver/safari/launcher_tests.py
@@ -0,0 +1,66 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import os
+
+import pytest
+
+
+def test_launch(driver):
+    assert driver.capabilities['browserName'] == 'safari'
+
+
+def test_launch_with_invalid_executable_path_raises_exception(driver_class):
+    path = '/this/path/should/never/exist'
+    assert not os.path.exists(path)
+    with pytest.raises(Exception) as e:
+        driver_class(executable_path=path)
+    assert 'SafariDriver requires Safari 10 on OSX El Capitan' in str(e)
+
+
+class TestTechnologyPreview(object):
+
+    @pytest.fixture
+    def driver_kwargs(self):
+        path = '/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver'
+        assert os.path.exists(path), 'Safari Technology Preview required! Download it from https://developer.apple.com/safari/technology-preview/'
+        return {'executable_path': path}
+
+    def test_launch(self, driver):
+        assert driver.capabilities['browserName'] == 'safari'
+
+
+def test_launch_safari_with_legacy_flag(mocker, driver_class):
+    import subprocess
+    mocker.patch('subprocess.Popen')
+    try:
+        driver_class(service_args=['--legacy'])
+    except Exception as e:
+        pass
+    args, kwargs = subprocess.Popen.call_args
+    assert '--legacy' in args[0]
+
+
+def test_launch_safari_without_legacy_flag(mocker, driver_class):
+    import subprocess
+    mocker.patch('subprocess.Popen')
+    try:
+        driver_class()
+    except Exception as e:
+        pass
+    args, kwargs = subprocess.Popen.call_args
+    assert '--legacy' not in args[0]
diff --git a/test/selenium/webdriver/support/__init__.py b/test/selenium/webdriver/support/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/selenium/webdriver/support/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/selenium/webdriver/support/color_tests.py b/test/selenium/webdriver/support/color_tests.py
new file mode 100644
index 00000000..1f36aec5
--- /dev/null
+++ b/test/selenium/webdriver/support/color_tests.py
@@ -0,0 +1,121 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from selenium.webdriver.support.color import Color
+
+
+def test_rgb_to_rgb():
+    rgb = "rgb(1, 2, 3)"
+    assert Color.from_string(rgb).rgb == rgb
+
+
+def test_rgb_to_rgba():
+    rgb = "rgb(1, 2, 3)"
+    assert Color.from_string(rgb).rgba == "rgba(1, 2, 3, 1)"
+
+
+def test_rgb_pct_to_rgba():
+    rgb = "rgb(10%, 20%, 30%)"
+    assert Color.from_string(rgb).rgba == "rgba(25, 51, 76, 1)"
+
+
+def test_rgb_allows_whitespace():
+    rgb = "rgb(\t1,   2    , 3)"
+    assert Color.from_string(rgb).rgb == "rgb(1, 2, 3)"
+
+
+def test_rgba_to_rgba():
+    rgba = "rgba(1, 2, 3, 0.5)"
+    assert Color.from_string(rgba).rgba == rgba
+
+
+def test_rgba_pct_to_rgba():
+    rgba = "rgba(10%, 20%, 30%, 0.5)"
+    assert Color.from_string(rgba).rgba == "rgba(25, 51, 76, 0.5)"
+
+
+def test_hex_to_hex():
+    hex_ = "#ff00a0"
+    assert Color.from_string(hex_).hex == hex_
+
+
+def test_hex_to_rgb():
+    hex_ = "#01Ff03"
+    rgb = "rgb(1, 255, 3)"
+    assert Color.from_string(hex_).rgb == rgb
+
+
+def test_hex_to_rgba():
+    hex_ = "#01Ff03"
+    rgba = "rgba(1, 255, 3, 1)"
+    assert Color.from_string(hex_).rgba == rgba
+
+    hex_ = "#00ff33"
+    rgba = "rgba(0, 255, 51, 1)"
+    assert Color.from_string(hex_).rgba == rgba
+
+
+def test_rgb_to_hex():
+    assert Color.from_string("rgb(1, 255, 3)").hex == "#01ff03"
+
+
+def test_hex3_to_rgba():
+    assert Color.from_string("#0f3").rgba == "rgba(0, 255, 51, 1)"
+
+
+def test_hsl_to_rgba():
+    hsl = "hsl(120, 100%, 25%)"
+    rgba = "rgba(0, 128, 0, 1)"
+    assert Color.from_string(hsl).rgba == rgba
+
+    hsl = "hsl(100, 0%, 50%)"
+    rgba = "rgba(128, 128, 128, 1)"
+    assert Color.from_string(hsl).rgba == rgba
+
+
+def test_hsla_to_rgba():
+    hsla = "hsla(120, 100%, 25%, 1)"
+    rgba = "rgba(0, 128, 0, 1)"
+    assert Color.from_string(hsla).rgba == rgba
+
+    hsla = "hsla(100, 0%, 50%, 0.5)"
+    rgba = "rgba(128, 128, 128, 0.5)"
+    assert Color.from_string(hsla).rgba == rgba
+
+
+def test_named_color():
+    assert Color.from_string("green").rgba == "rgba(0, 128, 0, 1)"
+    assert Color.from_string("gray").rgba == "rgba(128, 128, 128, 1)"
+    assert Color.from_string("aqua").hex == "#00ffff"
+    assert Color.from_string("transparent").rgba == "rgba(0, 0, 0, 0)"
+
+
+def test_equals():
+    assert Color.from_string("#f00") == Color.from_string("rgb(255, 0, 0)")
+    assert Color.from_string("rgba(30, 30, 30, 0.2)") != Color.from_string("rgba(30, 30, 30, 1)")
+
+
+def test_hash():
+    hash1 = hash(Color.from_string("#f00"))
+    hash2 = hash(Color.from_string("rgb(255, 0, 0)"))
+    assert hash1 == hash2
+
+
+def test_string_representations():
+    hex_ = "#01Ff03"
+    assert str(Color.from_string(hex_)) == "Color: rgba(1, 255, 3, 1)"
+    assert repr(Color.from_string(hex_)) == "Color(red=1, green=255, blue=3, alpha=1)"
diff --git a/test/selenium/webdriver/support/conftest.py b/test/selenium/webdriver/support/conftest.py
new file mode 100644
index 00000000..75670927
--- /dev/null
+++ b/test/selenium/webdriver/support/conftest.py
@@ -0,0 +1,21 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+def pytest_generate_tests(metafunc):
+    if 'driver' in metafunc.fixturenames and metafunc.config.option.drivers:
+        metafunc.parametrize('driver', metafunc.config.option.drivers, indirect=True)
diff --git a/test/selenium/webdriver/support/event_firing_webdriver_tests.py b/test/selenium/webdriver/support/event_firing_webdriver_tests.py
new file mode 100644
index 00000000..e4ad8c01
--- /dev/null
+++ b/test/selenium/webdriver/support/event_firing_webdriver_tests.py
@@ -0,0 +1,246 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+try:
+    from io import BytesIO
+except ImportError:
+    from cStringIO import StringIO as BytesIO
+
+import pytest
+
+from selenium.common.exceptions import NoSuchElementException
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.events import EventFiringWebDriver, AbstractEventListener
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.common.actions.action_builder import ActionBuilder
+
+
+@pytest.fixture
+def log():
+    log = BytesIO()
+    yield log
+    log.close()
+
+
+def test_should_fire_navigation_events(driver, log, pages):
+
+    class EventListener(AbstractEventListener):
+
+        def before_navigate_to(self, url, driver):
+            log.write(("before_navigate_to %s" % url.split("/")[-1]).encode())
+
+        def after_navigate_to(self, url, driver):
+            log.write(("after_navigate_to %s" % url.split("/")[-1]).encode())
+
+        def before_navigate_back(self, driver):
+            log.write(b"before_navigate_back")
+
+        def after_navigate_back(self, driver):
+            log.write(b"after_navigate_back")
+
+        def before_navigate_forward(self, driver):
+            log.write(b"before_navigate_forward")
+
+        def after_navigate_forward(self, driver):
+            log.write(b"after_navigate_forward")
+
+    ef_driver = EventFiringWebDriver(driver, EventListener())
+    ef_driver.get(pages.url("formPage.html"))
+    ef_driver.find_element(by=By.ID, value="imageButton").submit()
+    WebDriverWait(ef_driver, 5).until(lambda d: d.title == "We Arrive Here")
+    assert ef_driver.title == "We Arrive Here"
+
+    ef_driver.back()
+    assert ef_driver.title == "We Leave From Here"
+
+    ef_driver.forward()
+    assert ef_driver.title == "We Arrive Here"
+
+    assert (b"before_navigate_to formPage.html"
+            b"after_navigate_to formPage.html"
+            b"before_navigate_back"
+            b"after_navigate_back"
+            b"before_navigate_forward"
+            b"after_navigate_forward") == log.getvalue()
+
+
+def test_should_fire_click_event(driver, log, pages):
+
+    class EventListener(AbstractEventListener):
+
+        def before_click(self, element, driver):
+            log.write(b"before_click")
+
+        def after_click(self, element, driver):
+            log.write(b"after_click")
+
+    ef_driver = EventFiringWebDriver(driver, EventListener())
+    ef_driver.get(pages.url("clicks.html"))
+    ef_driver.find_element(By.ID, "overflowLink").click()
+    assert ef_driver.title == "XHTML Test Page"
+
+    assert b"before_click" + b"after_click" == log.getvalue()
+
+
+def test_should_fire_change_value_event(driver, log, pages):
+
+    class EventListener(AbstractEventListener):
+
+        def before_change_value_of(self, element, driver):
+            log.write(b"before_change_value_of")
+
+        def after_change_value_of(self, element, driver):
+            log.write(b"after_change_value_of")
+
+    ef_driver = EventFiringWebDriver(driver, EventListener())
+    ef_driver.get(pages.url("readOnlyPage.html"))
+    element = ef_driver.find_element_by_id("writableTextInput")
+    element.clear()
+    assert "" == element.get_attribute("value")
+
+    ef_driver.get(pages.url("javascriptPage.html"))
+    keyReporter = ef_driver.find_element(by=By.ID, value="keyReporter")
+    keyReporter.send_keys("abc def")
+    assert keyReporter.get_attribute("value") == "abc def"
+
+    assert (b"before_change_value_of"
+            b"after_change_value_of"
+            b"before_change_value_of"
+            b"after_change_value_of") == log.getvalue()
+
+
+def test_should_fire_find_event(driver, log, pages):
+
+    class EventListener(AbstractEventListener):
+
+        def before_find(self, by, value, driver):
+            log.write(("before_find by %s %s" % (by, value)).encode())
+
+        def after_find(self, by, value, driver):
+            log.write(("after_find by %s %s" % (by, value)).encode())
+
+    ef_driver = EventFiringWebDriver(driver, EventListener())
+    ef_driver.get(pages.url("simpleTest.html"))
+    e = ef_driver.find_element_by_id("oneline")
+    assert "A single line of text" == e.text
+
+    e = ef_driver.find_element_by_xpath("/html/body/p[1]")
+    assert "A single line of text" == e.text
+
+    ef_driver.get(pages.url("frameset.html"))
+    elements = ef_driver.find_elements_by_css_selector("frame#sixth")
+    assert 1 == len(elements)
+    assert "frame" == elements[0].tag_name.lower()
+    assert "sixth" == elements[0].get_attribute("id")
+
+    assert (b"before_find by id oneline"
+            b"after_find by id oneline"
+            b"before_find by xpath /html/body/p[1]"
+            b"after_find by xpath /html/body/p[1]"
+            b"before_find by css selector frame#sixth"
+            b"after_find by css selector frame#sixth") == log.getvalue()
+
+
+def test_should_call_listener_when_an_exception_is_thrown(driver, log, pages):
+
+    class EventListener(AbstractEventListener):
+        def on_exception(self, exception, driver):
+            if isinstance(exception, NoSuchElementException):
+                log.write(b"NoSuchElementException is thrown")
+
+    ef_driver = EventFiringWebDriver(driver, EventListener())
+    ef_driver.get(pages.url("simpleTest.html"))
+    with pytest.raises(NoSuchElementException):
+        ef_driver.find_element(By.ID, "foo")
+    assert b"NoSuchElementException is thrown" == log.getvalue()
+
+
+def test_should_unwrap_element_args_when_calling_scripts(driver, log, pages):
+    ef_driver = EventFiringWebDriver(driver, AbstractEventListener())
+    ef_driver.get(pages.url("javascriptPage.html"))
+    button = ef_driver.find_element_by_id("plainButton")
+    value = ef_driver.execute_script(
+        "arguments[0]['flibble'] = arguments[0].getAttribute('id'); return arguments[0]['flibble']",
+        button)
+    assert "plainButton" == value
+
+
+def test_should_unwrap_element_args_when_switching_frames(driver, log, pages):
+    ef_driver = EventFiringWebDriver(driver, AbstractEventListener())
+    ef_driver.get(pages.url("iframes.html"))
+    frame = ef_driver.find_element_by_id("iframe1")
+    ef_driver.switch_to.frame(frame)
+    assert "click me!" == ef_driver.find_element_by_id("imageButton").get_attribute("alt")
+
+
+def test_should_be_able_to_access_wrapped_instance_from_event_calls(driver):
+
+    class EventListener(AbstractEventListener):
+        def before_navigate_to(url, d):
+            assert driver is d
+
+    ef_driver = EventFiringWebDriver(driver, EventListener())
+    wrapped_driver = ef_driver.wrapped_driver
+    assert driver is wrapped_driver
+
+
+def test_using_kwargs(driver, pages):
+    ef_driver = EventFiringWebDriver(driver, AbstractEventListener())
+    ef_driver.get(pages.url("javascriptPage.html"))
+    ef_driver.get_cookie(name="cookie_name")
+    element = ef_driver.find_element_by_id("plainButton")
+    element.get_attribute(name="id")
+
+
+def test_missing_attributes_raise_error(driver, pages):
+    ef_driver = EventFiringWebDriver(driver, AbstractEventListener())
+
+    with pytest.raises(AttributeError):
+        ef_driver.attribute_should_not_exist
+
+    ef_driver.get(pages.url("readOnlyPage.html"))
+    element = ef_driver.find_element_by_id("writableTextInput")
+
+    with pytest.raises(AttributeError):
+        element.attribute_should_not_exist
+
+
+def test_can_use_pointer_input_with_event_firing_webdriver(driver, pages):
+    ef_driver = EventFiringWebDriver(driver, AbstractEventListener())
+    pages.load("javascriptPage.html")
+    to_click = ef_driver.find_element_by_id("clickField")
+
+    actions = ActionBuilder(ef_driver)
+    pointer = actions.pointer_action
+    pointer.move_to(to_click).click()
+    actions.perform()
+
+    assert to_click.get_attribute('value') == 'Clicked'
+
+
+def test_can_use_key_input_with_event_firing_webdriver(driver, pages):
+    ef_driver = EventFiringWebDriver(driver, AbstractEventListener())
+    pages.load("javascriptPage.html")
+    ef_driver.find_element_by_id("keyUp").click()
+
+    actions = ActionBuilder(ef_driver)
+    key = actions.key_action
+    key.send_keys('Success')
+    actions.perform()
+
+    result = ef_driver.find_element_by_id("result")
+    assert result.text == 'Success'
diff --git a/test/unit/__init__.py b/test/unit/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/unit/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/unit/selenium/__init__.py b/test/unit/selenium/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/unit/selenium/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/unit/selenium/webdriver/__init__.py b/test/unit/selenium/webdriver/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/unit/selenium/webdriver/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/unit/selenium/webdriver/chrome/chrome_options_tests.py b/test/unit/selenium/webdriver/chrome/chrome_options_tests.py
new file mode 100644
index 00000000..f14f9968
--- /dev/null
+++ b/test/unit/selenium/webdriver/chrome/chrome_options_tests.py
@@ -0,0 +1,155 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import platform
+from os import path
+
+import pytest
+
+from selenium.webdriver.chrome.options import Options
+
+
+@pytest.fixture
+def options():
+    return Options()
+
+
+def test_set_binary_location(options):
+    options.binary_location = '/foo/bar'
+    assert options._binary_location == '/foo/bar'
+
+
+def test_get_binary_location(options):
+    options._binary_location = '/foo/bar'
+    assert options.binary_location == '/foo/bar'
+
+
+def test_set_debugger_address(options):
+    options.debugger_address = '/foo/bar'
+    assert options._debugger_address == '/foo/bar'
+
+
+def test_get_debugger_address(options):
+    options._debugger_address = '/foo/bar'
+    assert options.debugger_address == '/foo/bar'
+
+
+def test_add_arguments(options):
+    options.add_argument('foo')
+    assert 'foo' in options._arguments
+
+
+def test_get_arguments(options):
+    options._arguments = ['foo']
+    assert 'foo' in options.arguments
+
+
+def test_raises_exception_if_argument_is_falsy(options):
+    with pytest.raises(ValueError):
+        options.add_argument(None)
+
+
+def test_raises_exception_if_extension_is_falsy(options):
+    with pytest.raises(ValueError):
+        options.add_extension(None)
+
+
+def test_raises_exception_if_extension_does_not_exist(options):
+    with pytest.raises(IOError):
+        options.add_extension(path.join(path.abspath(path.curdir), 'fakepath'))
+
+
+def test_add_extension(options, mocker):
+    pth = path.abspath(path.expanduser('/foo/bar'))
+    mocker.patch('os.path.exists').return_value = True
+    options.add_extension(pth)
+    assert pth in options._extension_files
+
+
+def test_raises_exception_if_encoded_extension_is_falsy(options):
+    with pytest.raises(ValueError):
+        options.add_encoded_extension(None)
+
+
+def test_add_encoded_extension(options):
+    options.add_encoded_extension('/foo/bar')
+    assert '/foo/bar' in options._extensions
+
+
+def test_get_extensions_from_extension_files(options, mocker):
+    null = 'NUL' if platform.system().lower() == 'windows' else '/dev/null'
+    mocker.patch(
+        'selenium.webdriver.chrome.options.open'.format(__name__)).return_value = open(null)
+    mocker.patch('base64.b64encode').return_value = 'foo'.encode()
+    options._extension_files = ['foo']
+    assert 'foo' in options.extensions
+
+
+def test_get_extensions_from_encoded_extensions(options, mocker):
+    options._extensions = ['foo']
+    assert 'foo' in options.extensions
+
+
+def test_add_experimental_options(options):
+    options.add_experimental_option('foo', 'bar')
+    assert options._experimental_options['foo'] == 'bar'
+
+
+def test_get_experimental_options(options):
+    options._experimental_options = {'foo': 'bar'}
+    assert options.experimental_options['foo'] == 'bar'
+
+
+def test_set_headless(options):
+    options.headless = True
+    assert '--headless' in options._arguments
+
+
+def test_unset_headless(options):
+    options._arguments = ['--headless']
+    options.headless = False
+    assert '--headless' not in options._arguments
+
+
+def test_get_headless(options):
+    options._arguments = ['--headless']
+    assert options.headless is True
+
+
+def test_creates_capabilities(options):
+    options._arguments = ['foo']
+    options._binary_location = '/bar'
+    options._extensions = ['baz']
+    options._debugger_address = '/foo/bar'
+    options._experimental_options = {'foo': 'bar'}
+    caps = options.to_capabilities()
+    opts = caps.get(Options.KEY)
+    assert opts
+    assert 'foo' in opts['args']
+    assert opts['binary'] == '/bar'
+    assert 'baz' in opts['extensions']
+    assert opts['debuggerAddress'] == '/foo/bar'
+    assert opts['foo'] == 'bar'
+
+
+def test_starts_with_default_capabilities(options):
+    from selenium.webdriver import DesiredCapabilities
+    assert options._caps == DesiredCapabilities.CHROME
+
+
+def test_is_a_baseoptions(options):
+    from selenium.webdriver.common.options import BaseOptions
+    assert isinstance(options, BaseOptions)
diff --git a/test/unit/selenium/webdriver/common/common_options_tests.py b/test/unit/selenium/webdriver/common/common_options_tests.py
new file mode 100644
index 00000000..e921eef6
--- /dev/null
+++ b/test/unit/selenium/webdriver/common/common_options_tests.py
@@ -0,0 +1,35 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver.common.options import ArgOptions
+
+
+@pytest.fixture
+def options():
+    return ArgOptions()
+
+
+def test_add_arguments(options):
+    options.add_argument('foo')
+    assert 'foo' in options._arguments
+
+
+def test_get_arguments(options):
+    options._arguments = ['foo']
+    assert 'foo' in options.arguments
diff --git a/test/unit/selenium/webdriver/edge/edge_options_tests.py b/test/unit/selenium/webdriver/edge/edge_options_tests.py
new file mode 100644
index 00000000..3d465779
--- /dev/null
+++ b/test/unit/selenium/webdriver/edge/edge_options_tests.py
@@ -0,0 +1,56 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver.edge.options import Options
+
+
+@pytest.fixture
+def options():
+    return Options()
+
+
+def test_raises_exception_with_invalid_page_load_strategy(options):
+    with pytest.raises(ValueError):
+        options.page_load_strategy = 'never'
+
+
+def test_set_page_load_strategy(options):
+    options.page_load_strategy = 'normal'
+    assert options._page_load_strategy == 'normal'
+
+
+def test_get_page_load_strategy(options):
+    options._page_load_strategy = 'normal'
+    assert options.page_load_strategy == 'normal'
+
+
+def test_creates_capabilities(options):
+    options.page_load_strategy = 'eager'
+    caps = options.to_capabilities()
+    assert caps['pageLoadStrategy'] == 'eager'
+
+
+def test_starts_with_default_capabilities(options):
+    from selenium.webdriver import DesiredCapabilities
+    assert options._caps == DesiredCapabilities.EDGE
+
+
+def test_is_a_baseoptions(options):
+    from selenium.webdriver.common.options import BaseOptions
+    assert isinstance(options, BaseOptions)
diff --git a/test/unit/selenium/webdriver/firefox/firefox_options_tests.py b/test/unit/selenium/webdriver/firefox/firefox_options_tests.py
new file mode 100644
index 00000000..ba7c276a
--- /dev/null
+++ b/test/unit/selenium/webdriver/firefox/firefox_options_tests.py
@@ -0,0 +1,162 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common.exceptions import InvalidArgumentException
+from selenium.webdriver.common.proxy import Proxy, ProxyType
+from selenium.webdriver.firefox.firefox_binary import FirefoxBinary
+from selenium.webdriver.firefox.firefox_profile import FirefoxProfile
+from selenium.webdriver.firefox.options import Options
+
+
+@pytest.fixture
+def options():
+    return Options()
+
+
+def test_set_binary_with_firefox_binary(options):
+    binary = FirefoxBinary('foo')
+    options.binary = binary
+    assert options._binary == binary
+
+
+def test_set_binary_with_path(options):
+    options.binary = '/foo'
+    assert options._binary._start_cmd == '/foo'
+
+
+def test_get_binary(options):
+    options.binary = '/foo'
+    assert options.binary._start_cmd == '/foo'
+
+
+def test_set_binary_location(options):
+    options.binary_location = '/foo'
+    assert options._binary._start_cmd == '/foo'
+
+
+def test_get_binary_location(options):
+    options._binary = FirefoxBinary('/foo')
+    assert options.binary_location == '/foo'
+
+
+def test_set_preference(options):
+    options.set_preference('foo', 'bar')
+    assert options._preferences['foo'] == 'bar'
+
+
+def test_get_preferences(options):
+    options._preferences = {'foo': 'bar'}
+    assert options.preferences['foo'] == 'bar'
+
+
+def test_set_proxy(options):
+    proxy = Proxy({'proxyType': ProxyType.MANUAL})
+    options.proxy = proxy
+    assert options._proxy == proxy
+
+
+def test_raises_exception_if_proxy_is_not_proxy_object(options):
+    with pytest.raises(InvalidArgumentException):
+        options.proxy = 'foo'
+
+
+def test_get_proxy(options):
+    options._proxy = 'foo'
+    assert options.proxy == 'foo'
+
+
+def test_set_profile_with_firefox_profile(options):
+    profile = FirefoxProfile()
+    options.profile = profile
+    assert options._profile == profile
+
+
+def test_set_profile_with_path(options):
+    options.profile = None
+    assert isinstance(options._profile, FirefoxProfile)
+
+
+def test_get_profile(options):
+    options._profile = 'foo'
+    assert options.profile == 'foo'
+
+
+def test_add_arguments(options):
+    options.add_argument('foo')
+    assert 'foo' in options._arguments
+
+
+def test_get_arguments(options):
+    options._arguments = ['foo']
+    assert 'foo' in options.arguments
+
+
+def test_raises_exception_if_argument_is_falsy(options):
+    with pytest.raises(ValueError):
+        options.add_argument(None)
+
+
+def test_set_log_level(options):
+    options.log.level = 'debug'
+    assert options.log.level == 'debug'
+
+
+def test_set_headless(options):
+    options.headless = True
+    assert '-headless' in options._arguments
+
+
+def test_unset_headless(options):
+    options._arguments = ['-headless']
+    options.headless = False
+    assert '-headless' not in options._arguments
+
+
+def test_get_headless(options):
+    options._arguments = ['-headless']
+    assert options.headless
+
+
+def test_creates_capabilities(options):
+    profile = FirefoxProfile()
+    options._arguments = ['foo']
+    options._binary = FirefoxBinary('/bar')
+    options._preferences = {'foo': 'bar'}
+    options._proxy = Proxy({'proxyType': ProxyType.MANUAL})
+    options._profile = profile
+    options.log.level = 'debug'
+    caps = options.to_capabilities()
+    opts = caps.get(Options.KEY)
+    assert opts
+    assert 'foo' in opts['args']
+    assert opts['binary'] == '/bar'
+    assert opts['prefs']['foo'] == 'bar'
+    assert opts['profile'] == profile.encoded
+    assert opts['proxy']['proxyType'] == ProxyType.MANUAL['string']
+    assert opts['log']['level'] == 'debug'
+
+
+def test_starts_with_default_capabilities(options):
+    from selenium.webdriver import DesiredCapabilities
+    assert options._caps == DesiredCapabilities.FIREFOX
+
+
+def test_is_a_baseoptions(options):
+    from selenium.webdriver.common.options import BaseOptions
+    assert isinstance(options, BaseOptions)
diff --git a/test/unit/selenium/webdriver/ie/__init__.py b/test/unit/selenium/webdriver/ie/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/unit/selenium/webdriver/ie/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/unit/selenium/webdriver/ie/test_ie_options.py b/test/unit/selenium/webdriver/ie/test_ie_options.py
new file mode 100644
index 00000000..dc2c7b6a
--- /dev/null
+++ b/test/unit/selenium/webdriver/ie/test_ie_options.py
@@ -0,0 +1,184 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+from selenium.webdriver.ie.options import Options, ElementScrollBehavior
+import pytest
+
+TIMEOUT = 30
+
+
+@pytest.fixture
+def opts():
+    yield Options()
+
+
+def test_arguments(opts):
+    arg1 = '-k'
+    arg2 = '-private'
+    opts.add_argument(arg1)
+    opts.add_argument(arg2)
+    assert arg1 in opts.arguments
+    assert arg2 in opts.arguments
+
+
+def test_browser_attach_timeout(opts):
+    opts.browser_attach_timeout = TIMEOUT
+    assert opts.browser_attach_timeout == TIMEOUT
+    assert opts.options.get(Options.BROWSER_ATTACH_TIMEOUT) == TIMEOUT
+
+
+def test_raises_exception_for_invalid_browser_attach_timeout(opts):
+    with pytest.raises(ValueError):
+        opts.browser_attach_timeout = 'foo'
+
+
+def test_element_scroll_behavior(opts):
+    opts.element_scroll_behavior = ElementScrollBehavior.BOTTOM
+    assert opts.element_scroll_behavior == ElementScrollBehavior.BOTTOM
+    assert opts.options.get(Options.ELEMENT_SCROLL_BEHAVIOR) == ElementScrollBehavior.BOTTOM
+
+
+def test_ensure_clean_session(opts):
+    opts.ensure_clean_session = True
+    assert opts.ensure_clean_session is True
+    assert opts.options.get(Options.ENSURE_CLEAN_SESSION) is True
+
+
+def test_file_upload_dialog_timeout(opts):
+    opts.file_upload_dialog_timeout = TIMEOUT
+    assert opts.file_upload_dialog_timeout is TIMEOUT
+    assert opts.options.get(Options.FILE_UPLOAD_DIALOG_TIMEOUT) is TIMEOUT
+
+
+def test_raises_exception_for_file_upload_dialog_timeout(opts):
+    with pytest.raises(ValueError):
+        opts.file_upload_dialog_timeout = 'foo'
+
+
+def test_force_create_process_api(opts):
+    opts.force_create_process_api = True
+    assert opts.force_create_process_api is True
+    assert opts.options.get(Options.FORCE_CREATE_PROCESS_API) is True
+
+
+def test_force_shell_windows_api(opts):
+    opts.force_shell_windows_api = True
+    assert opts.force_shell_windows_api is True
+    assert opts.options.get(Options.FORCE_SHELL_WINDOWS_API) is True
+
+
+def test_full_page_screenshot(opts):
+    opts.full_page_screenshot = True
+    assert opts.full_page_screenshot is True
+    assert opts.options.get(Options.FULL_PAGE_SCREENSHOT) is True
+
+
+def test_ignore_protected_mode_settings(opts):
+    opts.ignore_protected_mode_settings = True
+    assert opts.ignore_protected_mode_settings is True
+    assert opts.options.get(Options.IGNORE_PROTECTED_MODE_SETTINGS) is True
+
+
+def test_ignore_zoom_level(opts):
+    opts.ignore_zoom_level = True
+    assert opts.ignore_zoom_level is True
+    assert opts.options.get(Options.IGNORE_ZOOM_LEVEL) is True
+
+
+def test_initial_browser_url(opts):
+    url = 'http://www.seleniumhq.org'
+    opts.initial_browser_url = url
+    assert opts.initial_browser_url == url
+    assert opts.options.get(Options.INITIAL_BROWSER_URL) == url
+
+
+def test_native_events(opts):
+    opts.native_events = True
+    assert opts.native_events is True
+    assert opts.options.get(Options.NATIVE_EVENTS) is True
+
+
+def test_persistent_hover(opts):
+    opts.persistent_hover = True
+    assert opts.persistent_hover is True
+    assert opts.options.get(Options.PERSISTENT_HOVER) is True
+
+
+def test_require_window_focus(opts):
+    opts.require_window_focus = True
+    assert opts.require_window_focus is True
+    assert opts.options.get(Options.REQUIRE_WINDOW_FOCUS) is True
+
+
+def test_use_per_process_proxy(opts):
+    opts.use_per_process_proxy = True
+    assert opts.use_per_process_proxy is True
+    assert opts.options.get(Options.USE_PER_PROCESS_PROXY) is True
+
+
+def test_validate_cookie_document_type(opts):
+    opts.validate_cookie_document_type = True
+    assert opts.validate_cookie_document_type is True
+    assert opts.options.get(Options.VALIDATE_COOKIE_DOCUMENT_TYPE) is True
+
+
+def test_additional_options(opts):
+    opts.add_additional_option('foo', 'bar')
+    assert opts.additional_options.get('foo') == 'bar'
+
+
+def test_to_capabilities(opts):
+    opts._options['foo'] = 'bar'
+    assert Options.KEY in opts.to_capabilities()
+    assert opts.to_capabilities().get(Options.KEY) == opts._options
+
+
+def test_to_capabilities_arguments(opts):
+    arg = '-k'
+    opts.add_argument(arg)
+    caps_opts = opts.to_capabilities().get(Options.KEY)
+    assert caps_opts.get(Options.SWITCHES) == arg
+
+
+def test_to_capabilities_additional_options(opts):
+    name = 'foo'
+    value = 'bar'
+    opts.add_additional_option(name, value)
+    caps_opts = opts.to_capabilities().get(Options.KEY)
+    assert caps_opts.get(name) == value
+
+
+def test_to_capabilities_should_not_modify_set_options(opts):
+    opts._options['foo'] = 'bar'
+    arg = '-k'
+    opts.add_argument(arg)
+    opts.add_additional_option('baz', 'qux')
+    opts.to_capabilities().get(Options.KEY)
+    assert opts.options.get('foo') == 'bar'
+    assert opts.arguments[0] == arg
+    assert opts.additional_options.get('baz') == 'qux'
+
+
+def test_starts_with_default_capabilities(opts):
+    from selenium.webdriver import DesiredCapabilities
+    assert opts._caps == DesiredCapabilities.INTERNETEXPLORER
+
+
+def test_is_a_baseoptions(opts):
+    from selenium.webdriver.common.options import BaseOptions
+    assert isinstance(opts, BaseOptions)
diff --git a/test/unit/selenium/webdriver/opera/opera_options_tests.py b/test/unit/selenium/webdriver/opera/opera_options_tests.py
new file mode 100644
index 00000000..523b8600
--- /dev/null
+++ b/test/unit/selenium/webdriver/opera/opera_options_tests.py
@@ -0,0 +1,87 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver.opera.options import Options
+
+
+@pytest.fixture
+def options():
+    return Options()
+
+
+def test_set_android_package_name(options):
+    options.android_package_name = 'bar'
+    assert options._android_package_name == 'bar'
+
+
+def test_get_android_package_name(options):
+    options._android_package_name = 'bar'
+    assert options.android_package_name == 'bar'
+
+
+def test_set_android_device_socket(options):
+    options.android_device_socket = 'bar'
+    assert options._android_device_socket == 'bar'
+
+
+def test_get_android_device_socket(options):
+    options._android_device_socket = 'bar'
+    assert options.android_device_socket == 'bar'
+
+
+def test_set_android_command_line_file(options):
+    options.android_command_line_file = 'bar'
+    assert options._android_command_line_file == 'bar'
+
+
+def test_get_android_command_line_file(options):
+    options._android_command_line_file = 'bar'
+    assert options.android_command_line_file == 'bar'
+
+
+def test_creates_capabilities(options):
+    options._arguments = ['foo']
+    options._binary_location = '/bar'
+    options._extensions = ['baz']
+    options._debugger_address = '/foo/bar'
+    options._experimental_options = {'foo': 'bar'}
+    options._android_package_name = 'bar'
+    options._android_command_line_file = 'foo'
+    options._android_device_socket = 'spam'
+    caps = options.to_capabilities()
+    opts = caps.get(Options.KEY)
+    assert opts
+    assert 'foo' in opts['args']
+    assert opts['binary'] == '/bar'
+    assert 'baz' in opts['extensions']
+    assert opts['debuggerAddress'] == '/foo/bar'
+    assert opts['foo'] == 'bar'
+    assert opts['androidPackage'] == 'bar'
+    assert opts['androidCommandLineFile'] == 'foo'
+    assert opts['androidDeviceSocket'] == 'spam'
+
+
+def test_starts_with_default_capabilities(options):
+    from selenium.webdriver import DesiredCapabilities
+    assert options._caps == DesiredCapabilities.OPERA
+
+
+def test_is_a_baseoptions(options):
+    from selenium.webdriver.common.options import BaseOptions
+    assert isinstance(options, BaseOptions)
diff --git a/test/unit/selenium/webdriver/remote/__init__.py b/test/unit/selenium/webdriver/remote/__init__.py
new file mode 100644
index 00000000..a5b1e6f8
--- /dev/null
+++ b/test/unit/selenium/webdriver/remote/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/test/unit/selenium/webdriver/remote/test_error_handler.py b/test/unit/selenium/webdriver/remote/test_error_handler.py
new file mode 100644
index 00000000..8c851caa
--- /dev/null
+++ b/test/unit/selenium/webdriver/remote/test_error_handler.py
@@ -0,0 +1,255 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.common import exceptions
+from selenium.webdriver.remote.errorhandler import ErrorCode, ErrorHandler
+
+
+@pytest.fixture
+def handler():
+    yield ErrorHandler()
+
+
+def test_does_not_raise_exception_on_success(handler):
+    assert handler.check_response({'status': ErrorCode.SUCCESS}) is None
+    assert handler.check_response({}) is None
+
+
+@pytest.mark.parametrize('code', ErrorCode.NO_SUCH_ELEMENT)
+def test_raises_exception_for_no_such_element(handler, code):
+    with pytest.raises(exceptions.NoSuchElementException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.NO_SUCH_FRAME)
+def test_raises_exception_for_no_such_frame(handler, code):
+    with pytest.raises(exceptions.NoSuchFrameException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.UNKNOWN_COMMAND)
+def test_raises_exception_for_unknown_command(handler, code):
+    with pytest.raises(exceptions.WebDriverException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.STALE_ELEMENT_REFERENCE)
+def test_raises_exception_for_stale_element_reference(handler, code):
+    with pytest.raises(exceptions.StaleElementReferenceException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.ELEMENT_NOT_VISIBLE)
+def test_raises_exception_for_element_not_visible(handler, code):
+    with pytest.raises(exceptions.ElementNotVisibleException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.INVALID_ELEMENT_STATE)
+def test_raises_exception_for_invalid_element_state(handler, code):
+    with pytest.raises(exceptions.InvalidElementStateException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.UNKNOWN_ERROR)
+def test_raises_exception_for_unknown_error(handler, code):
+    with pytest.raises(exceptions.WebDriverException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.ELEMENT_IS_NOT_SELECTABLE)
+def test_raises_exception_for_element_not_selectable(handler, code):
+    with pytest.raises(exceptions.ElementNotSelectableException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.JAVASCRIPT_ERROR)
+def test_raises_exception_for_javascript_error(handler, code):
+    with pytest.raises(exceptions.JavascriptException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.XPATH_LOOKUP_ERROR)
+def test_raises_exception_for_xpath_lookup_error(handler, code):
+    with pytest.raises(exceptions.WebDriverException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.TIMEOUT)
+def test_raises_exception_for_timeout(handler, code):
+    with pytest.raises(exceptions.TimeoutException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.NO_SUCH_WINDOW)
+def test_raises_exception_for_no_such_window(handler, code):
+    with pytest.raises(exceptions.NoSuchWindowException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.INVALID_COOKIE_DOMAIN)
+def test_raises_exception_for_invalid_cookie_domain(handler, code):
+    with pytest.raises(exceptions.InvalidCookieDomainException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.UNABLE_TO_SET_COOKIE)
+def test_raises_exception_for_unable_to_set_cookie(handler, code):
+    with pytest.raises(exceptions.UnableToSetCookieException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.UNEXPECTED_ALERT_OPEN)
+def test_raises_exception_for_unexpected_alert_open(handler, code):
+    with pytest.raises(exceptions.UnexpectedAlertPresentException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.NO_ALERT_OPEN)
+def test_raises_exception_for_no_alert_open(handler, code):
+    with pytest.raises(exceptions.NoAlertPresentException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.SCRIPT_TIMEOUT)
+def test_raises_exception_for_script_timeout(handler, code):
+    with pytest.raises(exceptions.TimeoutException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.INVALID_ELEMENT_COORDINATES)
+def test_raises_exception_for_invalid_element_coordinates(handler, code):
+    with pytest.raises(exceptions.WebDriverException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.IME_NOT_AVAILABLE)
+def test_raises_exception_for_ime_not_available(handler, code):
+    with pytest.raises(exceptions.ImeNotAvailableException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.IME_ENGINE_ACTIVATION_FAILED)
+def test_raises_exception_for_ime_activation_failed(handler, code):
+    with pytest.raises(exceptions.ImeActivationFailedException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.INVALID_SELECTOR)
+def test_raises_exception_for_invalid_selector(handler, code):
+    with pytest.raises(exceptions.InvalidSelectorException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.SESSION_NOT_CREATED)
+def test_raises_exception_for_session_not_created(handler, code):
+    with pytest.raises(exceptions.SessionNotCreatedException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS)
+def test_raises_exception_for_move_target_out_of_bounds(handler, code):
+    with pytest.raises(exceptions.MoveTargetOutOfBoundsException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.INVALID_XPATH_SELECTOR)
+def test_raises_exception_for_invalid_xpath_selector(handler, code):
+    with pytest.raises(exceptions.InvalidSelectorException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.INVALID_XPATH_SELECTOR_RETURN_TYPER)
+def test_raises_exception_for_invalid_xpath_selector_return_typer(handler, code):
+    with pytest.raises(exceptions.InvalidSelectorException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.ELEMENT_NOT_INTERACTABLE)
+def test_raises_exception_for_element_not_interactable(handler, code):
+    with pytest.raises(exceptions.ElementNotInteractableException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.INSECURE_CERTIFICATE)
+def test_raises_exception_for_insecure_certificate(handler, code):
+    with pytest.raises(exceptions.InsecureCertificateException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.INVALID_ARGUMENT)
+def test_raises_exception_for_invalid_argument(handler, code):
+    with pytest.raises(exceptions.InvalidArgumentException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.INVALID_COORDINATES)
+def test_raises_exception_for_invalid_coordinates(handler, code):
+    with pytest.raises(exceptions.InvalidCoordinatesException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.INVALID_SESSION_ID)
+def test_raises_exception_for_invalid_session_id(handler, code):
+    with pytest.raises(exceptions.InvalidSessionIdException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.NO_SUCH_COOKIE)
+def test_raises_exception_for_no_such_cookie(handler, code):
+    with pytest.raises(exceptions.NoSuchCookieException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.UNABLE_TO_CAPTURE_SCREEN)
+def test_raises_exception_for_unable_to_capture_screen_exception(handler, code):
+    with pytest.raises(exceptions.ScreenshotException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.ELEMENT_CLICK_INTERCEPTED)
+def test_raises_exception_for_element_click_intercepted(handler, code):
+    with pytest.raises(exceptions.ElementClickInterceptedException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.UNKNOWN_METHOD)
+def test_raises_exception_for_unknown_method(handler, code):
+    with pytest.raises(exceptions.UnknownMethodException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('code', ErrorCode.METHOD_NOT_ALLOWED)
+def test_raises_exception_for_method_not_allowed(handler, code):
+    with pytest.raises(exceptions.WebDriverException):
+        handler.check_response({'status': code, 'value': 'foo'})
+
+
+@pytest.mark.parametrize('key', ['stackTrace', 'stacktrace'])
+def test_relays_exception_stacktrace(handler, key):
+    import json
+    stacktrace = {'lineNumber': 100, 'fileName': 'egg', 'methodName': 'ham', 'className': 'Spam'}
+    value = {key: [stacktrace],
+             'message': 'very bad',
+             'error': ErrorCode.UNKNOWN_METHOD[0]}
+    response = {'status': 400, 'value': json.dumps({'value': value})}
+    with pytest.raises(exceptions.UnknownMethodException) as e:
+        handler.check_response(response)
+
+    assert 'Spam.ham' in e.value.stacktrace[0]
diff --git a/test/unit/selenium/webdriver/remote/test_new_session.py b/test/unit/selenium/webdriver/remote/test_new_session.py
new file mode 100644
index 00000000..c5bf6b0c
--- /dev/null
+++ b/test/unit/selenium/webdriver/remote/test_new_session.py
@@ -0,0 +1,72 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+from copy import deepcopy
+from importlib import import_module
+
+import pytest
+
+from selenium.webdriver import DesiredCapabilities
+from selenium.webdriver.remote.command import Command
+from selenium.webdriver.remote.webdriver import WebDriver
+
+
+def test_converts_oss_capabilities_to_w3c(mocker):
+    mock = mocker.patch('selenium.webdriver.remote.webdriver.WebDriver.execute')
+    oss_caps = {'platform': 'WINDOWS', 'version': '11', 'acceptSslCerts': True}
+    w3c_caps = {'platformName': 'windows', 'browserVersion': '11', 'acceptInsecureCerts': True}
+    WebDriver(desired_capabilities=deepcopy(oss_caps))
+    expected_params = {'capabilities': {'firstMatch': [{}], 'alwaysMatch': w3c_caps},
+                       'desiredCapabilities': oss_caps}
+    mock.assert_called_with(Command.NEW_SESSION, expected_params)
+
+
+def test_converts_proxy_type_value_to_lowercase_for_w3c(mocker):
+    mock = mocker.patch('selenium.webdriver.remote.webdriver.WebDriver.execute')
+    oss_caps = {'proxy': {'proxyType': 'MANUAL', 'httpProxy': 'foo'}}
+    w3c_caps = {'proxy': {'proxyType': 'manual', 'httpProxy': 'foo'}}
+    WebDriver(desired_capabilities=deepcopy(oss_caps))
+    expected_params = {'capabilities': {'firstMatch': [{}], 'alwaysMatch': w3c_caps},
+                       'desiredCapabilities': oss_caps}
+    mock.assert_called_with(Command.NEW_SESSION, expected_params)
+
+
+def test_works_as_context_manager(mocker):
+    mocker.patch('selenium.webdriver.remote.webdriver.WebDriver.execute')
+    quit_ = mocker.patch('selenium.webdriver.remote.webdriver.WebDriver.quit')
+
+    with WebDriver() as driver:
+        assert isinstance(driver, WebDriver)
+
+    assert quit_.call_count == 1
+
+
+@pytest.mark.parametrize('browser_name', ['firefox', 'chrome', 'ie', 'opera'])
+def test_accepts_firefox_options_to_remote_driver(mocker, browser_name):
+    options = import_module('selenium.webdriver.{}.options'.format(browser_name))
+    caps_name = browser_name.upper() if browser_name != 'ie' else 'INTERNETEXPLORER'
+    mock = mocker.patch('selenium.webdriver.remote.webdriver.WebDriver.start_session')
+
+    opts = options.Options()
+    opts.add_argument('foo')
+    expected_caps = getattr(DesiredCapabilities, caps_name)
+    caps = expected_caps.copy()
+    expected_caps.update(opts.to_capabilities())
+
+    WebDriver(desired_capabilities=caps, options=opts)
+    mock.assert_called_with(expected_caps, None)
diff --git a/test/unit/selenium/webdriver/remote/test_remote_connection.py b/test/unit/selenium/webdriver/remote/test_remote_connection.py
new file mode 100644
index 00000000..4441982f
--- /dev/null
+++ b/test/unit/selenium/webdriver/remote/test_remote_connection.py
@@ -0,0 +1,64 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+try:
+    from urllib import parse
+except ImportError:  # above is available in py3+, below is py2.7
+    import urlparse as parse
+
+from selenium import __version__
+from selenium.webdriver.remote.remote_connection import (
+    RemoteConnection,
+)
+
+
+def test_get_remote_connection_headers_defaults():
+    url = 'http://remote'
+    headers = RemoteConnection.get_remote_connection_headers(parse.urlparse(url))
+    assert 'Authorization' not in headers.keys()
+    assert 'Connection' not in headers.keys()
+    assert headers.get('Accept') == 'application/json'
+    assert headers.get('Content-Type') == 'application/json;charset=UTF-8'
+    assert headers.get('User-Agent').startswith("selenium/%s (python " % __version__)
+    assert headers.get('User-Agent').split(' ')[-1] in {'windows)', 'mac)', 'linux)'}
+
+
+def test_get_remote_connection_headers_adds_auth_header_if_pass():
+    url = 'http://user:pass@remote'
+    headers = RemoteConnection.get_remote_connection_headers(parse.urlparse(url))
+    assert headers.get('Authorization') == 'Basic dXNlcjpwYXNz'
+
+
+def test_get_remote_connection_headers_adds_keep_alive_if_requested():
+    url = 'http://remote'
+    headers = RemoteConnection.get_remote_connection_headers(parse.urlparse(url), keep_alive=True)
+    assert headers.get('Connection') == 'keep-alive'
+
+
+class MockResponse:
+    code = 200
+    headers = []
+
+    def read(self):
+        return b"{}"
+
+    def close(self):
+        pass
+
+    def getheader(self, *args, **kwargs):
+        pass
diff --git a/test/unit/selenium/webdriver/webkitgtk/webkitgtk_options_tests.py b/test/unit/selenium/webdriver/webkitgtk/webkitgtk_options_tests.py
new file mode 100644
index 00000000..508bd90f
--- /dev/null
+++ b/test/unit/selenium/webdriver/webkitgtk/webkitgtk_options_tests.py
@@ -0,0 +1,67 @@
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from selenium.webdriver.webkitgtk.options import Options
+
+
+@pytest.fixture
+def options():
+    return Options()
+
+
+def test_set_binary_location(options):
+    options.binary_location = '/foo/bar'
+    assert options._binary_location == '/foo/bar'
+
+
+def test_get_binary_location(options):
+    options._binary_location = '/foo/bar'
+    assert options.binary_location == '/foo/bar'
+
+
+def test_set_overlay_scrollbars_enabled(options):
+    options.overlay_scrollbars_enabled = False
+    assert options._overlay_scrollbars_enabled is False
+
+
+def test_get_overlay_scrollbars_enabled(options):
+    options._overlay_scrollbars_enabled = True
+    assert options.overlay_scrollbars_enabled is True
+
+
+def test_creates_capabilities(options):
+    options._arguments = ['foo']
+    options._binary_location = '/bar'
+    options._overlay_scrollbars_enabled = True
+    caps = options.to_capabilities()
+    opts = caps.get(Options.KEY)
+    assert opts
+    assert 'foo' in opts['args']
+    assert opts['binary'] == '/bar'
+    assert opts['useOverlayScrollbars'] is True
+
+
+def test_starts_with_default_capabilities(options):
+    from selenium.webdriver import DesiredCapabilities
+    assert options._caps == DesiredCapabilities.WEBKITGTK
+
+
+def test_is_a_baseoptions(options):
+    from selenium.webdriver.common.options import BaseOptions
+    assert isinstance(options, BaseOptions)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..3d9b4c48
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,32 @@
+[tox]
+envlist = py{27,34,35,36}-{unit,chrome,edge,firefox,ie,remote,safari}, docs, flake8
+
+[testenv]
+passenv = DISPLAY PYTEST_ADDOPTS CI TRAVIS
+commands =
+  py{27,34,35,36}-unit: py.test -n=auto {posargs:test/unit}
+  py{27,36}-chrome: py.test --driver=Chrome {posargs}
+  py{27,36}-edge: py.test --driver=Edge {posargs}
+  py{27,36}-firefox: py.test --driver=Firefox {posargs}
+  py{27,36}-ie: py.test --driver=Ie {posargs}
+  py{27,36}-marionette: py.test --driver=Marionette {posargs}
+  py{27,36}-remote: py.test --driver=Remote {posargs}
+  py{27,36}-safari: py.test --driver=Safari {posargs}
+install_command = pip install -v --no-index --find-links=../third_party/py {opts} {packages}
+deps =
+  pytest==3.0.3
+  pytest-instafail==0.3.0
+  pytest-mock==1.5.0
+  py{27,33,34,35,36}-{unit,chrome,firefox,marionette}: pytest-xdist==1.15
+  urllib3==1.23
+
+[testenv:docs]
+skip_install = true
+deps =
+  Sphinx==1.8.2
+commands = sphinx-build -W -b html -d ../build/doctrees docs/source ../build/docs/api/py {posargs}
+
+[testenv:flake8]
+skip_install = true
+deps = flake8
+commands = flake8 {posargs}