Repository: robotframework/SeleniumLibrary
Pull request #1469 (2019-09-30 16:10:43)

Added action-chain click option
This PR introduces an option to `Click Element` which enables a user to use Selenium ActionChains to issue a click action on an element instead of using the <web_element>.click() function. This enables a more "native" click of any element in the UI. This is in response to issue #1463 

JonKoser: Added tests and addressed comments
aaltat: The test are good start but they need to look deeper under the hood. Now Test ensures that click happens, but tests do not ensure that click happens by using action chains. Usually in this kind of situation, it is easiest to use [statuschecker](https://github.com/robotframework/statuschecker/) to verify that logs from Selenium are from actions chains and not from normal `click()`. Example [Choose File](https://github.com/robotframework/SeleniumLibrary/blob/master/atest/acceptance/keywords/choose_file.robot#L35) keyword uses it for same purposes. 

Patch:
diff --git a/.travis.yml b/.travis.yml
index c774c567..338f1aae 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,4 @@
 language: python
-sudo: required
 dist: bionic
 services:
   - xvfb
@@ -8,15 +7,11 @@ env:
     - GRID=False
     - PYPY=False
 addons:
-  apt:
-    sources:
-      - google-chrome
-    packages:
-      - google-chrome-stable
+  chrome: stable
 before_install:
   - CDVERSION=`curl http://chromedriver.storage.googleapis.com/LATEST_RELEASE`
   - echo $CDVERSION
-  - wget --no-verbose http://chromedriver.storage.googleapis.com/76.0.3809.68/chromedriver_linux64.zip
+  - wget --no-verbose http://chromedriver.storage.googleapis.com/$CDVERSION/chromedriver_linux64.zip
   - unzip chromedriver_linux64.zip
   - sudo chmod u+x chromedriver
   - sudo mv chromedriver /usr/bin/
diff --git a/setup.py b/setup.py
index b432dc35..c0d5e6f0 100755
--- a/setup.py
+++ b/setup.py
@@ -37,6 +37,7 @@ setup(
     keywords         = 'robotframework testing testautomation selenium webdriver web',
     platforms        = 'any',
     classifiers      = CLASSIFIERS,
+    python_requires  = '>=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4',
     install_requires = REQUIREMENTS,
     package_dir      = {'': 'src'},
     packages         = find_packages('src')
diff --git a/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py
index cacda0c3..8973b80f 100644
--- a/src/SeleniumLibrary/__init__.py
+++ b/src/SeleniumLibrary/__init__.py
@@ -42,7 +42,7 @@ from SeleniumLibrary.locators import ElementFinder
 from SeleniumLibrary.utils import LibraryListener, timestr_to_secs, is_truthy
 
 
-__version__ = '4.0.1.dev1'
+__version__ = '4.1.0.dev1'
 
 
 class SeleniumLibrary(DynamicCore):
diff --git a/src/SeleniumLibrary/keywords/browsermanagement.py b/src/SeleniumLibrary/keywords/browsermanagement.py
index 68cb7f29..f8418af2 100644
--- a/src/SeleniumLibrary/keywords/browsermanagement.py
+++ b/src/SeleniumLibrary/keywords/browsermanagement.py
@@ -56,10 +56,10 @@ class BrowserManagementKeywords(LibraryComponent):
             self.drivers.close()
 
     @keyword
-    def open_browser(self, url, browser='firefox', alias=None,
+    def open_browser(self, url=None, browser='firefox', alias=None,
                      remote_url=False, desired_capabilities=None,
                      ff_profile_dir=None, options=None, service_log_path=None):
-        """Opens a new browser instance to the given ``url``.
+        """Opens a new browser instance to the optional ``url``.
 
         The ``browser`` argument specifies which browser to use. The
         supported browsers are listed in the table below. The browser names
@@ -87,6 +87,9 @@ class BrowserManagementKeywords(LibraryComponent):
         Headless Chrome are new additions in SeleniumLibrary 3.1.0
         and require Selenium 3.8.0 or newer.
 
+        After opening the browser, it is possible to use optional 
+        ``url`` to navigate the browser to the desired address.
+
         Optional ``alias`` is an alias given for this browser instance and
         it can be used for switching between browsers. When same ``alias``
         is given with two `Open Browser` keywords, the first keyword will
@@ -189,6 +192,15 @@ class BrowserManagementKeywords(LibraryComponent):
         Then the `${options}` variable can be used as an argument to
         ``options``.
 
+        Example the ``options`` argument can be used to launch Chomium-based
+        applications which utilize the 
+        [https://bitbucket.org/chromiumembedded/cef/wiki/UsingChromeDriver|Chromium Embedded Framework]
+        . To lauch Chomium-based application, use ``options`` to define
+        `binary_location` attribute and use `add_argument` method to define
+        `remote-debugging-port` port for the application. Once the browser
+        is opened, the test can interact with the embedded web-content of
+        the system under test.
+
         Optional ``service_log_path`` argument defines the name of the
         file where to write the browser driver logs. If the
         ``service_log_path``  argument contain a  marker ``{index}``, it
@@ -203,6 +215,7 @@ class BrowserManagementKeywords(LibraryComponent):
         | `Open Browser` | http://example.com | Firefox | alias=Firefox                           |
         | `Open Browser` | http://example.com | Edge    | remote_url=http://127.0.0.1:4444/wd/hub |
         | `Open Browser` | about:blank        |         |                                         |
+        | `Open Browser` | browser=Chrome     |         |                                         |
 
         Alias examples:
         | ${1_index} =    | `Open Browser` | http://example.com | Chrome  | alias=Chrome     | # Opens new browser because alias is new.         |
@@ -216,9 +229,10 @@ class BrowserManagementKeywords(LibraryComponent):
         Example when using
         [https://seleniumhq.github.io/selenium/docs/api/py/webdriver_chrome/selenium.webdriver.chrome.options.html#selenium.webdriver.chrome.options.Options|Chrome options]
         method:
-        | `Open Browser` | http://example.com | Chrome | options=add_argument("--disable-popup-blocking"); add_argument("--ignore-certificate-errors") | # Sting format              |
-        |  ${options} =  |     Get Options    |        |                                                                                               | # Selenium options instance |
-        | `Open Browser` | http://example.com | Chrome | options=${options}                                                                            |                             |
+        | `Open Browser` | http://example.com | Chrome | options=add_argument("--disable-popup-blocking"); add_argument("--ignore-certificate-errors") | # Sting format                    |
+        |  ${options} =  |     Get Options    |        |                                                                                               | # Selenium options instance       |
+        | `Open Browser` | http://example.com | Chrome | options=${options}                                                                            |                                   |
+        | `Open Browser` | None               | Chrome | options=binary_location="/path/to/binary";add_argument("remote-debugging-port=port")          | # Start Chomium-based application |
 
         Example for FirefoxProfile
         | `Open Browser` | http://example.com | Firefox | ff_profile_dir=/path/to/profile                                                  | # Using profile from disk                       |
@@ -238,18 +252,21 @@ class BrowserManagementKeywords(LibraryComponent):
         accepting an instance of the `selenium.webdriver.FirefoxProfile`
         and support defining FirefoxProfile with methods and
         attributes are new in SeleniumLibrary 4.0.
+        
+        Making ``url`` optional is new in SeleniumLibrary 4.1.
         """
         index = self.drivers.get_index(alias)
         if index:
             self.info('Using existing browser from index %s.' % index)
             self.switch_browser(alias)
-            self.go_to(url)
+            if is_truthy(url):
+                self.go_to(url)
             return index
         return self._make_new_browser(url, browser, alias, remote_url,
                                       desired_capabilities, ff_profile_dir,
                                       options, service_log_path)
 
-    def _make_new_browser(self, url, browser='firefox', alias=None,
+    def _make_new_browser(self, url=None, browser='firefox', alias=None,
                           remote_url=False, desired_capabilities=None,
                           ff_profile_dir=None, options=None, service_log_path=None):
         if is_truthy(remote_url):
@@ -262,12 +279,13 @@ class BrowserManagementKeywords(LibraryComponent):
                                    options, service_log_path)
         driver = self._wrap_event_firing_webdriver(driver)
         index = self.ctx.register_driver(driver, alias)
-        try:
-            driver.get(url)
-        except Exception:
-            self.debug("Opened browser with session id %s but failed "
-                       "to open url '%s'." % (driver.session_id, url))
-            raise
+        if is_truthy(url):
+            try:
+                driver.get(url)
+            except Exception:
+                self.debug("Opened browser with session id %s but failed "
+                        "to open url '%s'." % (driver.session_id, url))
+                raise
         self.debug('Opened browser with session id %s.' % driver.session_id)
         return index
 
diff --git a/src/SeleniumLibrary/keywords/element.py b/src/SeleniumLibrary/keywords/element.py
index e81ecb90..4376b08b 100644
--- a/src/SeleniumLibrary/keywords/element.py
+++ b/src/SeleniumLibrary/keywords/element.py
@@ -571,7 +571,7 @@ newDiv.parentNode.style.overflow = 'hidden';
             self._click_with_modifier(locator, ['link', 'link'], modifier)
 
     @keyword
-    def click_element(self, locator, modifier=False):
+    def click_element(self, locator, modifier=False, action_chain=False):
         """Click the element identified by ``locator``.
 
         See the `Locating elements` section for details about the locator
@@ -586,18 +586,36 @@ newDiv.parentNode.style.overflow = 'hidden';
         [https://seleniumhq.github.io/selenium/docs/api/py/webdriver/selenium.webdriver.common.keys.html#selenium.webdriver.common.keys.Keys.ALT|ALT key]
         . If ``modifier`` does not match to Selenium Keys, keyword fails.
 
+        If ``action_chain`` argument is true, see `Boolean arguments` for more
+        details on how to set boolean argument, then keyword uses ActionChain
+        based click instead of the <web_element>.click() function. If both
+        ``action_chain`` and ``modifier`` are defined, the click will be
+        performed using ``modifier`` and ``action_chain`` will be ignored.
+
         Example:
-        | Click Element | id:button | | # Would click element without any modifiers. |
-        | Click Element | id:button | CTRL | # Would click element with CTLR key pressed down. |
-        | Click Element | id:button | CTRL+ALT | # Would click element with CTLR and ALT keys pressed down. |
+        | Click Element | id:button |                   | # Would click element without any modifiers.               |
+        | Click Element | id:button | CTRL              | # Would click element with CTLR key pressed down.          |
+        | Click Element | id:button | CTRL+ALT          | # Would click element with CTLR and ALT keys pressed down. |
+        | Click Element | id:button | action_chain=True | # Clicks the button using an Selenium  ActionChains        |
 
         The ``modifier`` argument is new in SeleniumLibrary 3.2
+        The ``action_chain`` argument is new in SeleniumLibrary 4.1
         """
-        if is_falsy(modifier):
+        if is_truthy(modifier):
+            self._click_with_modifier(locator, [None, None], modifier)
+        elif is_truthy(action_chain):
+            self._click_with_action_chain(locator)
+        else:
             self.info("Clicking element '%s'." % locator)
             self.find_element(locator).click()
-        else:
-            self._click_with_modifier(locator, [None, None], modifier)
+
+    def _click_with_action_chain(self, locator):
+        self.info("Clicking '%s' using an action chain." % locator)
+        action = ActionChains(self.driver)
+        element = self.find_element(locator)
+        action.move_to_element(element)
+        action.click()
+        action.perform()
 
     def _click_with_modifier(self, locator, tag, modifier):
         self.info("Clicking %s '%s' with %s." % (tag if tag[0] else 'element', locator, modifier))
diff --git a/src/SeleniumLibrary/keywords/screenshot.py b/src/SeleniumLibrary/keywords/screenshot.py
index e5fb6bff..daec0e2e 100644
--- a/src/SeleniumLibrary/keywords/screenshot.py
+++ b/src/SeleniumLibrary/keywords/screenshot.py
@@ -20,6 +20,7 @@ from robot.utils import get_link_path
 
 from SeleniumLibrary.base import LibraryComponent, keyword
 from SeleniumLibrary.utils import is_noney
+from SeleniumLibrary.utils.path_formatter import _format_path
 
 
 class ScreenshotKeywords(LibraryComponent):
@@ -130,7 +131,7 @@ class ScreenshotKeywords(LibraryComponent):
         index = 0
         while True:
             index += 1
-            formatted = filename.format(index=index)
+            formatted = _format_path(filename, index)
             path = os.path.join(directory, formatted)
             # filename didn't contain {index} or unique path was found
             if formatted == filename or not os.path.exists(path):
diff --git a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py
index 20ac4f46..3482cdde 100644
--- a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py
+++ b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py
@@ -28,6 +28,8 @@ from selenium.webdriver import FirefoxProfile
 
 from SeleniumLibrary.utils import is_falsy, is_truthy, is_noney, is_string, PY3
 from SeleniumLibrary.keywords.webdrivertools.sl_file_detector import SelLibLocalFileDetector
+from SeleniumLibrary.utils.path_formatter import _format_path
+
 if not PY3:
     FileNotFoundError = object
 
@@ -285,7 +287,7 @@ class WebDriverCreator(object):
             return None
         index = 1
         while True:
-            formatted = log_file.format(index=index)
+            formatted = _format_path(log_file, index)
             path = os.path.join(self.log_dir, formatted)
             # filename didn't contain {index} or unique path was found
             if formatted == log_file or not os.path.exists(path):
diff --git a/src/SeleniumLibrary/utils/path_formatter.py b/src/SeleniumLibrary/utils/path_formatter.py
new file mode 100644
index 00000000..f478cdad
--- /dev/null
+++ b/src/SeleniumLibrary/utils/path_formatter.py
@@ -0,0 +1,30 @@
+# Copyright 2008-2011 Nokia Networks
+# Copyright 2011-2016 Ryan Tomac, Ed Manlove and contributors
+# Copyright 2016-     Robot Framework Foundation
+#
+# 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.
+from SeleniumLibrary.base.robotlibcore import PY2
+
+
+# Cab be deleted when Python 2 is not anymore supported.
+def _format_path(file_path, index):
+    if PY2:
+        import string
+        return string.Formatter().vformat(file_path, (), _SafeFormatter(index=index))
+    return file_path.format_map(_SafeFormatter(index=index))
+
+
+class _SafeFormatter(dict):
+
+    def __missing__(self, key):
+        return '{%s}' % key


Test patch:
diff --git a/atest/acceptance/keywords/click_element.robot b/atest/acceptance/keywords/click_element.robot
index 51f21e2d..10e345ff 100644
--- a/atest/acceptance/keywords/click_element.robot
+++ b/atest/acceptance/keywords/click_element.robot
@@ -36,6 +36,14 @@ Double Click Element Error
     [Setup]    Go To Page "javascript/click.html"
     Double Click Element    id:öööö
 
+Click Element Action Chain
+    [Tags]    NoGrid
+    [Documentation]
+    ...    LOB 1:1 INFO        Clicking 'singleClickButton' using an action chain.
+    ...    LOG 2:5 DEBUG GLOB: *actions {"actions": [{*
+    Click Element    singleClickButton      action_chain=True
+    Element Text Should Be    output    single clicked
+
 *** Keywords ***
 Initialize Page
     [Documentation]    Initialize Page
diff --git a/atest/acceptance/keywords/click_element_modifier.robot b/atest/acceptance/keywords/click_element_modifier.robot
index 6f76be0f..103640d2 100644
--- a/atest/acceptance/keywords/click_element_modifier.robot
+++ b/atest/acceptance/keywords/click_element_modifier.robot
@@ -42,6 +42,11 @@ Click Element Wrong Modifier
     ...    ValueError: 'FOOBAR' modifier does not match to Selenium Keys
     ...    Click Element    Button    Foobar
 
+Click Element Action Chain and modifier
+    [Documentation]     LOG 2:1 INFO Clicking element 'Button' with CTRL.
+    Click Element    Button    modifier=CTRL    action_chain=True
+    Element Text Should Be    output    CTRL click
+    
 *** Keywords ***
 Initialize Page
     Reload Page
@@ -50,4 +55,4 @@ Initialize Page
 Close Popup Window
     Select Window    myName    timeout=5s
     Close Window
-    Select Window    MAIN      timeout=5s
\ No newline at end of file
+    Select Window    MAIN      timeout=5s
diff --git a/atest/acceptance/keywords/screenshots.robot b/atest/acceptance/keywords/screenshots.robot
index 6f5a0fcd..f8d5e32f 100644
--- a/atest/acceptance/keywords/screenshots.robot
+++ b/atest/acceptance/keywords/screenshots.robot
@@ -53,12 +53,12 @@ Set Screenshot Directory set back to previous value
 Capture page screenshot with unique index
     [Setup]    Remove Directory    ${OUTPUTDIR}/screenshot-and-index    recursive
     ${file1} =    Capture Page Screenshot    ${OUTPUTDIR}/screenshot-and-index/other-{index}-name.png
-    ${file2} =    Capture Page Screenshot    ${OUTPUTDIR}/screenshot-and-index/some-other-name-{index}.png
+    ${file2} =    Capture Page Screenshot    ${OUTPUTDIR}/screenshot-and-index/some-{other}-name-{index}.png
     ${file3} =    Capture Page Screenshot    ${OUTPUTDIR}/screenshot-and-index/other-{index}-name.png
     File Should Exist    ${OUTPUTDIR}/screenshot-and-index/other-1-name.png
     Should Be Equal    ${file1}    ${OUTPUTDIR}${/}screenshot-and-index${/}other-1-name.png
-    File Should Exist    ${OUTPUTDIR}/screenshot-and-index/some-other-name-1.png
-    Should Be Equal    ${file2}    ${OUTPUTDIR}${/}screenshot-and-index${/}some-other-name-1.png
+    File Should Exist    ${OUTPUTDIR}/screenshot-and-index/some-{other}-name-1.png
+    Should Be Equal    ${file2}    ${OUTPUTDIR}${/}screenshot-and-index${/}some-{other}-name-1.png
     File Should Exist    ${OUTPUTDIR}/screenshot-and-index/other-2-name.png
     Should Be Equal    ${file3}    ${OUTPUTDIR}${/}screenshot-and-index${/}other-2-name.png
 
diff --git a/atest/acceptance/keywords/scroll_into_view.robot b/atest/acceptance/keywords/scroll_into_view.robot
index 9cf75820..1170f82a 100644
--- a/atest/acceptance/keywords/scroll_into_view.robot
+++ b/atest/acceptance/keywords/scroll_into_view.robot
@@ -8,9 +8,6 @@ ${TEXT}=   You scrolled in div.
 
 *** Test Cases ***
 Verify Scroll Element Into View
-    [Documentation]    Marked temporally as non-critical because Chrome 76 has bug with
-    ...    Selenium ActionChains
-    [Tags]    known issue chrome    known issue headlesschrome
     [Setup]    Go To Page "scroll/index.html"
     ${initial_postion}=    Get Vertical Position    css:#target
     Scroll Element Into View    css:#target
diff --git a/utest/test/api/approved_files/test_filepath_unusual_characters.test_normal_file_path.approved.txt b/utest/test/api/approved_files/test_filepath_unusual_characters.test_normal_file_path.approved.txt
new file mode 100644
index 00000000..604154bd
--- /dev/null
+++ b/utest/test/api/approved_files/test_filepath_unusual_characters.test_normal_file_path.approved.txt
@@ -0,0 +1,11 @@
+Different file paths.
+
+0) /foo/file.log
+1) /foo/file-1.log
+2) /foo/file-0001.log
+3) /foo/file-{foo}.log
+4) /{foo}/file-1.log
+5) /foo/file-001.log
+6) /foo/1234-file-1234.log
+7) /foo/file-{in dex}.log
+8) /foo/file-{in@dex}.log
diff --git a/utest/test/api/test_filepath_unusual_characters.py b/utest/test/api/test_filepath_unusual_characters.py
new file mode 100644
index 00000000..67d6d9eb
--- /dev/null
+++ b/utest/test/api/test_filepath_unusual_characters.py
@@ -0,0 +1,43 @@
+import os
+
+import pytest
+from robot.utils import JYTHON
+
+try:
+    from approvaltests.approvals import verify_all
+    from approvaltests.reporters.generic_diff_reporter_factory import GenericDiffReporterFactory
+except ImportError:
+    if JYTHON:
+        verify = None
+        GenericDiffReporterFactory = None
+    else:
+        raise
+
+from SeleniumLibrary.utils.path_formatter import _format_path
+
+
+@pytest.fixture(scope='module')
+def reporter():
+    if JYTHON:
+        return None
+    else:
+        path = os.path.dirname(__file__)
+        reporter_json = os.path.abspath(os.path.join(path, '..', 'approvals_reporters.json'))
+        factory = GenericDiffReporterFactory()
+        factory.load(reporter_json)
+        return factory.get_first_working()
+
+
+@pytest.mark.skipif(JYTHON, reason='ApprovalTest does not work with Jython')
+def test_normal_file_path(reporter):
+    results = []
+    results.append(_format_path('/foo/file.log', 1))
+    results.append(_format_path('/foo/file-{index}.log', 1))
+    results.append(_format_path('/foo/file-{index}.log', '0001'))
+    results.append(_format_path('/foo/file-{foo}.log', 1))
+    results.append(_format_path('/{foo}/file-{index}.log', 1))
+    results.append(_format_path('/foo/file-{index:03}.log', 1))
+    results.append(_format_path('/foo/{index}-file-{index}.log', '1234'))
+    results.append(_format_path('/foo/file-{in dex}.log', '1234'))
+    results.append(_format_path('/foo/file-{in@dex}.log', '1234'))
+    verify_all('Different file paths.', results, reporter=reporter)
diff --git a/utest/test/keywords/test_click_modifier.py b/utest/test/keywords/test_click_modifier.py
index 197a1156..2ca98ea1 100644
--- a/utest/test/keywords/test_click_modifier.py
+++ b/utest/test/keywords/test_click_modifier.py
@@ -1,54 +1,63 @@
-import unittest
-
+import pytest
 from mockito import mock, unstub
 from selenium.webdriver.common.keys import Keys
 
 from SeleniumLibrary.keywords import ElementKeywords
 
 
-class ParsingModifierKeys(unittest.TestCase):
-
-    @classmethod
-    def setUpClass(cls):
-        cls.ctx = mock()
-        cls.element = ElementKeywords(cls.ctx)
-
-    @classmethod
-    def tearDownClass(cls):
-        unstub()
-
-    def test_parsing_one_mofier(self):
-        parsed = self.element.parse_modifier('CTRL')
-        self.assertEqual(parsed, [Keys.CONTROL])
-        parsed = self.element.parse_modifier('esc')
-        self.assertEqual(parsed, [Keys.ESCAPE])
-        parsed = self.element.parse_modifier('ESCAPE')
-        self.assertEqual(parsed, [Keys.ESCAPE])
-        parsed = self.element.parse_modifier('control')
-        self.assertEqual(parsed, [Keys.CONTROL])
-        parsed = self.element.parse_modifier('alt')
-        self.assertEqual(parsed, [Keys.ALT])
-        parsed = self.element.parse_modifier('sHifT')
-        self.assertEqual(parsed, [Keys.SHIFT])
-
-    def test_parsing_multiple_modifiers(self):
-        parsed = self.element.parse_modifier('ctrl+shift')
-        self.assertEqual(parsed, [Keys.CONTROL, Keys.SHIFT])
-        parsed = self.element.parse_modifier('ctrl+alt+shift')
-        self.assertEqual(parsed, [Keys.CONTROL, Keys.ALT, Keys.SHIFT])
-        parsed = self.element.parse_modifier(' ctrl + alt +shift ')
-        self.assertEqual(parsed, [Keys.CONTROL, Keys.ALT, Keys.SHIFT])
-
-    def test_invalid_modifier(self):
-        with self.assertRaisesRegexp(ValueError, "'FOO' modifier "):
-            self.element.parse_modifier('FOO')
-        with self.assertRaisesRegexp(ValueError, "'FOO' modifier "):
-            self.element.parse_modifier('FOO+CTRL')
-        with self.assertRaisesRegexp(ValueError, "'FOO' modifier "):
-            self.element.parse_modifier('CTRL+FOO')
-        with self.assertRaisesRegexp(ValueError, "'CTRLFOO' modifier "):
-            self.element.parse_modifier('CTRLFOO')
-
-    def test_invalid_key_separator(self):
-        with self.assertRaisesRegexp(ValueError, "'CTRL-CTRL' modifier "):
-            self.element.parse_modifier('CTRL-CTRL')
+@pytest.fixture(scope='module')
+def element():
+    ctx = mock()
+    return ElementKeywords(ctx)
+
+
+def teardown_module():
+    unstub()
+
+
+def test_parsing_one_modifier(element):
+    parsed = element.parse_modifier('CTRL')
+    assert parsed == [Keys.CONTROL]
+    parsed = element.parse_modifier('esc')
+    assert parsed == [Keys.ESCAPE]
+    parsed = element.parse_modifier('ESCAPE')
+    assert parsed == [Keys.ESCAPE]
+    parsed = element.parse_modifier('control')
+    assert parsed == [Keys.CONTROL]
+    parsed = element.parse_modifier('alt')
+    assert parsed == [Keys.ALT]
+    parsed = element.parse_modifier('sHifT')
+    assert parsed == [Keys.SHIFT]
+
+
+def test_parsing_multiple_modifiers(element):
+    parsed = element.parse_modifier('ctrl+shift')
+    assert parsed == [Keys.CONTROL, Keys.SHIFT]
+    parsed = element.parse_modifier('ctrl+alt+shift')
+    assert parsed == [Keys.CONTROL, Keys.ALT, Keys.SHIFT]
+    parsed = element.parse_modifier(' ctrl + alt +shift ')
+    assert parsed == [Keys.CONTROL, Keys.ALT, Keys.SHIFT]
+
+
+def test_invalid_modifier(element):
+    with pytest.raises(ValueError) as error:
+        element.parse_modifier('FOO')
+    assert "'FOO' modifier " in str(error.value)
+
+    with pytest.raises(ValueError) as error:
+        element.parse_modifier('FOO+CTRL')
+    assert "'FOO' modifier " in str(error.value)
+
+    with pytest.raises(ValueError) as error:
+        element.parse_modifier('CTRL+FOO')
+    assert "'FOO' modifier " in str(error.value)
+
+    with pytest.raises(ValueError) as error:
+        element.parse_modifier('CTRLFOO')
+    assert "'CTRLFOO' modifier " in str(error.value)
+
+
+def test_invalid_key_separator(element):
+    with pytest.raises(ValueError) as error:
+        element.parse_modifier('CTRL-CTRL')
+    assert "'CTRL-CTRL' modifier " in str(error.value)
diff --git a/utest/test/keywords/test_keyword_arguments_browsermanagement.py b/utest/test/keywords/test_keyword_arguments_browsermanagement.py
index 5cda8a55..643fc534 100644
--- a/utest/test/keywords/test_keyword_arguments_browsermanagement.py
+++ b/utest/test/keywords/test_keyword_arguments_browsermanagement.py
@@ -1,6 +1,6 @@
 import unittest
 
-from mockito import mock, unstub, when
+from mockito import mock, unstub, when, verify, ANY
 
 from SeleniumLibrary.keywords import BrowserManagementKeywords
 
@@ -12,6 +12,7 @@ class KeywordArgumentsElementTest(unittest.TestCase):
         ctx.event_firing_webdriver = None
         ctx._browser = mock()
         ctx._drivers = mock()
+        self.ctx = ctx
         self.brorser = BrowserManagementKeywords(ctx)
 
     def tearDown(self):
@@ -31,3 +32,31 @@ class KeywordArgumentsElementTest(unittest.TestCase):
         alias = self.brorser.open_browser(url, alias='None',
                                           remote_url=remote_url)
         self.assertEqual(alias, None)
+
+    def test_same_alias(self):
+        url = 'https://github.com/robotframework'
+        alias = 'tidii'
+        driver = mock()
+        driver.session_id = 'foobar'
+        self.ctx.driver = driver
+        when(self.ctx._drivers).get_index(alias).thenReturn(1)
+        when(self.ctx._drivers).switch(1).thenReturn(driver)
+        self.brorser.open_browser(url=url, alias=alias)
+        verify(driver, times=1).get(url)
+
+    def test_open_browser_no_get(self):
+        browser = mock()
+        when(self.brorser)._make_driver('firefox', None,
+                                        None, False, None, None).thenReturn(browser)
+        self.brorser.open_browser()
+        verify(browser, times=0).get(ANY)
+
+    def test_same_alias_and_not_get(self):
+        alias = 'tidii'
+        driver = mock()
+        driver.session_id = 'foobar'
+        self.ctx.driver = driver
+        when(self.ctx._drivers).get_index(alias).thenReturn(1)
+        when(self.ctx._drivers).switch(1).thenReturn(driver)
+        self.brorser.open_browser(alias=alias)
+        verify(driver, times=0).get(ANY)
diff --git a/utest/test/keywords/test_keyword_arguments_element.py b/utest/test/keywords/test_keyword_arguments_element.py
index 4d8150af..f20dff99 100644
--- a/utest/test/keywords/test_keyword_arguments_element.py
+++ b/utest/test/keywords/test_keyword_arguments_element.py
@@ -1,35 +1,41 @@
-import unittest
-
+import pytest
 from mockito import mock, unstub, when
 
 from SeleniumLibrary.keywords import ElementKeywords
 
 
-class KeywordArgumentsElementTest(unittest.TestCase):
+@pytest.fixture(scope='function')
+def element():
+    ctx = mock()
+    ctx._browser = mock()
+    return ElementKeywords(ctx)
+
+
+def teardown_function():
+    unstub()
+
 
-    def setUp(self):
-        ctx = mock()
-        ctx._browser = mock()
-        self.element = ElementKeywords(ctx)
+def test_locator_should_match_x_times(element):
+    locator = '//div'
+    when(element).find_elements(locator).thenReturn([])
+    with pytest.raises(AssertionError) as error:
+        element.locator_should_match_x_times(locator, 1)
+    assert 'should have matched' in str(error.value)
 
-    def tearDown(self):
-        unstub()
+    with pytest.raises(AssertionError) as error:
+        element.locator_should_match_x_times(locator, 1, 'foobar')
+    assert 'foobar' in str(error.value)
 
-    def test_locator_should_match_x_times(self):
-        locator = '//div'
-        when(self.element).find_elements(locator).thenReturn([])
-        with self.assertRaisesRegexp(AssertionError, 'should have matched'):
-            self.element.locator_should_match_x_times(locator, 1)
 
-        with self.assertRaisesRegexp(AssertionError, 'foobar'):
-            self.element.locator_should_match_x_times(locator, 1, 'foobar')
+def test_element_text_should_be(element):
+    locator = '//div'
+    webelement = mock()
+    webelement.text = 'text'
+    when(element).find_element(locator).thenReturn(webelement)
+    with pytest.raises(AssertionError) as error:
+        element.element_text_should_be(locator, 'not text')
+    assert 'should have been' in str(error.value)
 
-    def test_element_text_should_be(self):
-        locator = '//div'
-        element = mock()
-        element.text = 'text'
-        when(self.element).find_element(locator).thenReturn(element)
-        with self.assertRaisesRegexp(AssertionError, 'should have been'):
-            self.element.element_text_should_be(locator, 'not text')
-        with self.assertRaisesRegexp(AssertionError, 'foobar'):
-            self.element.element_text_should_be(locator, 'not text', 'foobar')
+    with pytest.raises(AssertionError) as error:
+        element.element_text_should_be(locator, 'not text', 'foobar')
+    assert 'foobar' in str(error.value)
diff --git a/utest/test/keywords/test_keyword_arguments_formelement.py b/utest/test/keywords/test_keyword_arguments_formelement.py
index b1f74208..e8629046 100644
--- a/utest/test/keywords/test_keyword_arguments_formelement.py
+++ b/utest/test/keywords/test_keyword_arguments_formelement.py
@@ -1,5 +1,4 @@
-import unittest
-
+import pytest
 from mockito import mock, unstub, when
 
 from SeleniumLibrary.keywords import FormElementKeywords
@@ -8,65 +7,80 @@ from SeleniumLibrary.keywords import FormElementKeywords
 FALSES = ['False', False, '', None, 'NONE']
 
 
-class KeywordArgumentsElementTest(unittest.TestCase):
-
-    def setUp(self):
-        ctx = mock()
-        ctx._browser = mock()
-        self.form = FormElementKeywords(ctx)
-        self.ctx = ctx
-
-    def tearDown(self):
-        unstub()
-
-    def test_submit_form_false(self):
-        element = mock()
-        when(self.form).find_element('tag:form', tag='form').thenReturn(element)
-        for false in FALSES:
-            self.form.submit_form()
-        self.form.submit_form()
-
-    def test_submit_form_true(self):
-        element = mock()
-        when(self.form).find_element('//form', tag='form').thenReturn(element)
-        self.form.submit_form('//form')
-
-    def test_textfield_should_contain(self):
-        locator = '//input'
-        element = mock()
-        when(self.form).find_element(locator, 'text field').thenReturn(element)
-        when(element).get_attribute('value').thenReturn('no')
-        with self.assertRaisesRegexp(AssertionError, 'should have contained'):
-            self.form.textfield_should_contain(locator, 'text')
-        with self.assertRaisesRegexp(AssertionError, 'foobar'):
-            self.form.textfield_should_contain(locator, 'text', 'foobar')
-
-    def test_textfield_value_should_be(self):
-        locator = '//input'
-        element = mock()
-        when(self.form).find_element(locator, 'text field').thenReturn(element)
-        when(element).get_attribute('value').thenReturn('no')
-        with self.assertRaisesRegexp(AssertionError, 'text field'):
-            self.form.textfield_value_should_be(locator, 'value')
-        with self.assertRaisesRegexp(AssertionError, 'foobar'):
-            self.form.textfield_value_should_be(locator, 'value', 'foobar err')
-
-    def test_textarea_should_contain(self):
-        locator = '//input'
-        element = mock()
-        when(self.form).find_element(locator, 'text area').thenReturn(element)
-        when(element).get_attribute('value').thenReturn('no')
-        with self.assertRaisesRegexp(AssertionError, 'should have contained'):
-            self.form.textarea_should_contain(locator, 'value')
-        with self.assertRaisesRegexp(AssertionError, 'foobar error'):
-            self.form.textarea_should_contain(locator, 'value', 'foobar error')
-
-    def test_textarea_value_should_be(self):
-        locator = '//input'
-        element = mock()
-        when(self.form).find_element(locator, 'text area').thenReturn(element)
-        when(element).get_attribute('value').thenReturn('no')
-        with self.assertRaisesRegexp(AssertionError, 'should have had'):
-            self.form.textarea_value_should_be(locator, 'value')
-        with self.assertRaisesRegexp(AssertionError, 'foobar'):
-            self.form.textarea_value_should_be(locator, 'value', 'foobar')
+@pytest.fixture(scope='function')
+def form():
+    ctx = mock()
+    ctx.driver = mock()
+    return FormElementKeywords(ctx)
+
+
+def teardown_function():
+    unstub()
+
+
+def test_submit_form_false(form):
+    element = mock()
+    when(form).find_element('tag:form', tag='form').thenReturn(element)
+    for false in FALSES:
+        form.submit_form()
+    form.submit_form()
+
+def test_submit_form_true(form):
+    element = mock()
+    when(form).find_element('//form', tag='form').thenReturn(element)
+    form.submit_form('//form')
+
+def test_textfield_should_contain(form):
+    locator = '//input'
+    element = mock()
+    when(form).find_element(locator, 'text field').thenReturn(element)
+    when(element).get_attribute('value').thenReturn('no')
+    with pytest.raises(AssertionError) as error:
+        form.textfield_should_contain(locator, 'text')
+    assert 'should have contained' in str(error.value)
+
+    with pytest.raises(AssertionError) as error:
+        form.textfield_should_contain(locator, 'text', 'foobar')
+    assert 'foobar' in str(error.value)
+
+
+def test_textfield_value_should_be(form):
+    locator = '//input'
+    element = mock()
+    when(form).find_element(locator, 'text field').thenReturn(element)
+    when(element).get_attribute('value').thenReturn('no')
+    with pytest.raises(AssertionError) as error:
+        form.textfield_value_should_be(locator, 'value')
+    assert 'text field' in str(error.value)
+
+    with pytest.raises(AssertionError) as error:
+        form.textfield_value_should_be(locator, 'value', 'foobar err')
+    assert 'foobar' in str(error.value)
+
+
+def test_textarea_should_contain(form):
+    locator = '//input'
+    element = mock()
+    when(form).find_element(locator, 'text area').thenReturn(element)
+    when(element).get_attribute('value').thenReturn('no')
+    with pytest.raises(AssertionError) as error:
+        form.textarea_should_contain(locator, 'value')
+    assert 'should have contained' in str(error.value)
+
+    with pytest.raises(AssertionError) as error:
+        form.textarea_should_contain(locator, 'value', 'foobar error')
+    assert 'foobar error' in str(error.value)
+
+
+def test_textarea_value_should_be(form):
+    locator = '//input'
+    element = mock()
+    when(form).find_element(locator, 'text area').thenReturn(element)
+    when(element).get_attribute('value').thenReturn('no')
+    with pytest.raises(AssertionError) as error:
+        form.textarea_value_should_be(locator, 'value')
+    assert 'should have had' in str(error.value)
+
+    with pytest.raises(AssertionError) as error:
+        form.textarea_value_should_be(locator, 'value', 'foobar')
+    assert 'foobar' in str(error.value)
diff --git a/utest/test/keywords/test_keyword_arguments_waiting.py b/utest/test/keywords/test_keyword_arguments_waiting.py
index f05d3caa..9ecae00f 100644
--- a/utest/test/keywords/test_keyword_arguments_waiting.py
+++ b/utest/test/keywords/test_keyword_arguments_waiting.py
@@ -1,33 +1,42 @@
-import unittest
-
+import pytest
 from mockito import mock, unstub, when
 
 from SeleniumLibrary.keywords import WaitingKeywords
 
+TIMEOUT = 0.01
+
+
+@pytest.fixture(scope='module')
+def waiting():
+    ctx = mock()
+    ctx.driver = mock()
+    ctx.timeout = TIMEOUT
+    return WaitingKeywords(ctx)
+
+
+def teardown_module():
+    unstub()
+
+
+def test_wait_for_condition(waiting):
+    condition = 'return document.getElementById("intro")'
+    error = 'did not become true'
+    with pytest.raises(AssertionError) as error:
+        waiting.wait_for_condition(condition)
+    assert 'did not become true' in str(error.value)
+
+    with pytest.raises(AssertionError) as error:
+        waiting.wait_for_condition(condition, 'None', 'foobar')
+    assert 'foobar' in str(error.value)
+
+
+def test_wait_until_page_contains(waiting):
+    text = 'text'
+    when(waiting).is_text_present(text).thenReturn(None)
+    with pytest.raises(AssertionError) as error:
+        waiting.wait_until_page_contains(text)
+    assert "Text 'text' did not" in str(error.value)
 
-class KeywordArgumentsWaitingKeywordsTest(unittest.TestCase):
-
-    def setUp(self):
-        self.ctx = mock()
-        self.ctx.driver = mock()
-        self.ctx.timeout = 0.01
-        self.waiting = WaitingKeywords(self.ctx)
-
-    def tearDown(self):
-        unstub()
-
-    def test_wait_for_condition(self):
-        condition = 'return document.getElementById("intro")'
-        error = 'did not become true'
-        with self.assertRaisesRegexp(AssertionError, error):
-            self.waiting.wait_for_condition(condition)
-        with self.assertRaisesRegexp(AssertionError, 'foobar'):
-            self.waiting.wait_for_condition(condition, 'None', 'foobar')
-
-    def test_wait_until_page_contains(self):
-        text = 'text'
-        when(self.waiting).is_text_present(text).thenReturn(None)
-        with self.assertRaisesRegexp(AssertionError, "Text 'text' did not"):
-            self.waiting.wait_until_page_contains(text)
-        with self.assertRaisesRegexp(AssertionError, "error"):
-            self.waiting.wait_until_page_contains(text, 'None', 'error')
+    with pytest.raises(AssertionError) as error:
+        waiting.wait_until_page_contains(text, 'None', 'error')
+    assert 'error' in str(error.value)
diff --git a/utest/test/keywords/test_waiting_stale_element_refereance_exception.py b/utest/test/keywords/test_waiting_stale_element_refereance_exception.py
index 49e2e80a..1960789b 100644
--- a/utest/test/keywords/test_waiting_stale_element_refereance_exception.py
+++ b/utest/test/keywords/test_waiting_stale_element_refereance_exception.py
@@ -1,84 +1,94 @@
-import unittest
-
+import pytest
 from selenium.common.exceptions import StaleElementReferenceException
 from mockito import mock, when, unstub
 
 from SeleniumLibrary.keywords import WaitingKeywords
 
+TIMEOUT = 0.5
+
 
 def _raise(*a):
     raise StaleElementReferenceException('Darn')
 
 
-class TableKeywordsTest(unittest.TestCase):
-
-    @classmethod
-    def setUpClass(cls):
-        cls.ctx = mock()
-        cls.waiting = WaitingKeywords(cls.ctx)
-        cls.timeout = 0.5
-        cls.count = 0
-
-    def tearDown(self):
-        unstub()
-
-    def test_wait_until_element_is_visible(self):
-        locator = '//div'
-        element = mock()
-        when(self.waiting).find_element(locator, required=False).thenReturn(element)
-        when(element).is_displayed().thenRaise(StaleElementReferenceException()).thenReturn(True)
-        self.waiting.wait_until_element_is_visible(locator, self.timeout)
-
-    def test_wait_until_element_is_visible_fails(self):
-        locator = '//div'
-        element = mock()
-        when(self.waiting).find_element(locator, required=False).thenReturn(element)
-        when(element).is_displayed().thenRaise(StaleElementReferenceException('foo'))
-        with self.assertRaisesRegexp(AssertionError, 'Message: foo'):
-            self.waiting.wait_until_element_is_visible(locator, self.timeout)
-
-    def test_wait_until_element_is_not_visible(self):
-        locator = '//div'
-        element = mock()
-        when(self.waiting).find_element(locator, required=False).thenReturn(element)
-        when(element).is_displayed().thenRaise(StaleElementReferenceException()).thenReturn(False)
-        self.waiting.wait_until_element_is_not_visible(locator, self.timeout)
-
-    def test_wait_until_element_is_enabled(self):
-        locator = '//div'
-        element = mock()
-        when(self.waiting).find_element(locator, None).thenReturn(element)
-        when(element).is_enabled().thenRaise(StaleElementReferenceException()).thenReturn(True)
-        self.waiting.wait_until_element_is_enabled(locator, self.timeout)
-
-    def test_wait_until_element_is_enabled_get_attribute_readonly(self):
-        locator = '//div'
-        element = mock()
-        when(self.waiting).find_element(locator, None).thenReturn(element)
-        when(element).is_enabled().thenReturn(True)
-        when(element).get_attribute('readonly').thenRaise(StaleElementReferenceException()).thenReturn(None)
-        self.waiting.wait_until_element_is_enabled(locator, self.timeout)
-
-    def test_wait_until_element_is_enabled_fails(self):
-        locator = '//div'
-        element = mock()
-        when(self.waiting).find_element(locator, None).thenReturn(element)
-        when(element).is_enabled().thenRaise(StaleElementReferenceException('foo'))
-        with self.assertRaisesRegexp(AssertionError, 'Message: foo'):
-            self.waiting.wait_until_element_is_enabled(locator, self.timeout)
-
-    def test_wait_until_element_contains(self):
-        locator = '//div'
-        text = 'foo'
-        element1, element2 = mock(), mock({'text': 'foobar'})
-        element1.__class__.text = property(_raise)
-        when(self.waiting).find_element(locator).thenReturn(element1).thenReturn(element2)
-        self.waiting.wait_until_element_contains(locator, text, self.timeout)
-
-    def test_wait_until_element_does_not_contain(self):
-        locator = '//div'
-        text = 'foo'
-        element1, element2 = mock(), mock({'text': 'tidii'})
-        element1.__class__.text = property(_raise)
-        when(self.waiting).find_element(locator).thenReturn(element1).thenReturn(element2)
-        self.waiting.wait_until_element_does_not_contain(locator, text, self.timeout)
+
+@pytest.fixture(scope='module')
+def waiting():
+    ctx = mock()
+    ctx.timeout = TIMEOUT
+    return WaitingKeywords(mock())
+
+
+def teardown_module():
+    unstub()
+
+
+def test_wait_until_element_is_visible(waiting):
+    locator = '//div'
+    element = mock()
+    when(waiting).find_element(locator, required=False).thenReturn(element)
+    when(element).is_displayed().thenRaise(StaleElementReferenceException()).thenReturn(True)
+    waiting.wait_until_element_is_visible(locator, TIMEOUT)
+
+
+def test_wait_until_element_is_visible_fails(waiting):
+    locator = '//div'
+    element = mock()
+    when(waiting).find_element(locator, required=False).thenReturn(element)
+    when(element).is_displayed().thenRaise(StaleElementReferenceException('foo'))
+    with pytest.raises(AssertionError) as error:
+        waiting.wait_until_element_is_visible(locator, TIMEOUT)
+    assert 'Message: foo' in str(error.value)
+
+
+def test_wait_until_element_is_not_visible(waiting):
+    locator = '//div'
+    element = mock()
+    when(waiting).find_element(locator, required=False).thenReturn(element)
+    when(element).is_displayed().thenRaise(StaleElementReferenceException()).thenReturn(False)
+    waiting.wait_until_element_is_not_visible(locator, TIMEOUT)
+
+
+def test_wait_until_element_is_enabled(waiting):
+    locator = '//div'
+    element = mock()
+    when(waiting).find_element(locator, None).thenReturn(element)
+    when(element).is_enabled().thenRaise(StaleElementReferenceException()).thenReturn(True)
+    waiting.wait_until_element_is_enabled(locator, TIMEOUT)
+
+
+def test_wait_until_element_is_enabled_get_attribute_readonly(waiting):
+    locator = '//div'
+    element = mock()
+    when(waiting).find_element(locator, None).thenReturn(element)
+    when(element).is_enabled().thenReturn(True)
+    when(element).get_attribute('readonly').thenRaise(StaleElementReferenceException()).thenReturn(None)
+    waiting.wait_until_element_is_enabled(locator, TIMEOUT)
+
+
+def test_wait_until_element_is_enabled_fails(waiting):
+    locator = '//div'
+    element = mock()
+    when(waiting).find_element(locator, None).thenReturn(element)
+    when(element).is_enabled().thenRaise(StaleElementReferenceException('foo'))
+    with pytest.raises(AssertionError) as error:
+        waiting.wait_until_element_is_enabled(locator, TIMEOUT)
+    assert 'Message: foo' in str(error.value)
+
+
+def test_wait_until_element_contains(waiting):
+    locator = '//div'
+    text = 'foo'
+    element1, element2 = mock(), mock({'text': 'foobar'})
+    element1.__class__.text = property(_raise)
+    when(waiting).find_element(locator).thenReturn(element1).thenReturn(element2)
+    waiting.wait_until_element_contains(locator, text, TIMEOUT)
+
+
+def test_wait_until_element_does_not_contain(waiting):
+    locator = '//div'
+    text = 'foo'
+    element1, element2 = mock(), mock({'text': 'tidii'})
+    element1.__class__.text = property(_raise)
+    when(waiting).find_element(locator).thenReturn(element1).thenReturn(element2)
+    waiting.wait_until_element_does_not_contain(locator, text, TIMEOUT)
diff --git a/utest/test/locators/test_elementfinder.py b/utest/test/locators/test_elementfinder.py
index 82c3f1d5..cfbdfd25 100644
--- a/utest/test/locators/test_elementfinder.py
+++ b/utest/test/locators/test_elementfinder.py
@@ -1,538 +1,625 @@
-import unittest
-
+import pytest
 from mockito import any, mock, verify, when, unstub
 
 from SeleniumLibrary.errors import ElementNotFound
 from SeleniumLibrary.locators.elementfinder import ElementFinder
 
 
-class ParseLocatorTests(unittest.TestCase):
-
-    def setUp(self):
-        self.finder = ElementFinder(None)
-
-    def test_implicit_xpath(self):
-        self._verify_parse_locator('//foo', 'xpath', '//foo')
-        self._verify_parse_locator('(//foo)', 'xpath', '(//foo)')
-        self._verify_parse_locator('//id=bar', 'xpath', '//id=bar')
-
-    def test_no_separator(self):
-        self._verify_parse_locator('foo', 'default', 'foo')
-        self._verify_parse_locator('', 'default', '')
-
-    def test_equal_sign_as_separator(self):
-        self._verify_parse_locator('class=foo', 'class', 'foo')
-        self._verify_parse_locator('id=foo=bar', 'id', 'foo=bar')
-
-    def test_colon_as_separator(self):
-        self._verify_parse_locator('class:foo', 'class', 'foo')
-        self._verify_parse_locator('id:foo:bar', 'id', 'foo:bar')
-
-    def test_use_first_separator_when_both_are_used(self):
-        self._verify_parse_locator('id:foo=bar', 'id', 'foo=bar')
-        self._verify_parse_locator('id=foo:bar', 'id', 'foo:bar')
-
-    def test_preserve_trailing_whitespace(self):
-        self._verify_parse_locator('//foo/bar  ', 'xpath', '//foo/bar  ')
-        self._verify_parse_locator('class=foo  ', 'class', 'foo  ')
-
-    def test_remove_whitespace_around_prefix_and_separator(self):
-        self._verify_parse_locator('class = foo', 'class', 'foo')
-        self._verify_parse_locator('class : foo', 'class', 'foo')
-        self._verify_parse_locator('  id  = foo = bar  ', 'id', 'foo = bar  ')
-        self._verify_parse_locator('  id  : foo : bar  ', 'id', 'foo : bar  ')
-
-    def test_separator_without_matching_prefix_is_ignored(self):
-        self._verify_parse_locator('no=match', 'default', 'no=match')
-        self._verify_parse_locator('no:match', 'default', 'no:match')
-
-    def test_registered_strategy_can_be_used_as_prefix(self):
-        self._verify_parse_locator('registered=no', 'default', 'registered=no')
-        self._verify_parse_locator('registered:no', 'default', 'registered:no')
-        self.finder.register('registered', lambda *args: None, persist=True)
-        self._verify_parse_locator('registered=yes!!', 'registered', 'yes!!')
-        self._verify_parse_locator('registered:yes!!', 'registered', 'yes!!')
-
-    def _verify_parse_locator(self, locator, prefix, criteria):
-        parse_locator = self.finder._parse_locator
-        self.assertEqual(parse_locator(locator), (prefix, criteria))
-
-
-class ElementFinderParentTests(unittest.TestCase):
-
-    def setUp(self):
-        self.ctx = mock()
-        self.ctx.driver = self.driver = mock()
-        self.finder = ElementFinder(self.ctx)
-
-    def tearDown(self):
-        unstub()
-
-    def test_parent_is_not_webelement(self):
-        with self.assertRaises(ValueError):
-            self.finder.find("//div", parent='//button')
-
-    def test_find_by_xpath_parent_is_webelement(self):
-        webelement = mock()
-        when(self.finder)._is_webelement(webelement).thenReturn(True)
-        when(self.finder)._is_webelement('//div').thenReturn(False)
-        when(webelement).find_elements_by_xpath('//div').thenReturn([mock()])
-        self.finder.find('//div', parent=webelement)
-        verify(webelement).find_elements_by_xpath('//div')
-
-    def test_find_by_identifier_parent_is_webelement(self):
-        webelement = mock()
-        when(self.finder)._is_webelement(webelement).thenReturn(True)
-        when(self.finder)._is_webelement('identifier=value').thenReturn(False)
-        when(webelement).find_elements_by_id('value').thenReturn([mock()])
-        when(webelement).find_elements_by_name('value').thenReturn([mock()])
-        self.finder.find('identifier=value', parent=webelement)
-        verify(webelement).find_elements_by_name('value')
-        verify(webelement).find_elements_by_id('value')
-
-    def test_find_by_id_parent_is_webelement(self):
-        webelement = mock()
-        when(self.finder)._is_webelement(webelement).thenReturn(True)
-        when(self.finder)._is_webelement('id=value').thenReturn(False)
-        when(webelement).find_elements_by_id('value').thenReturn([mock()])
-        self.finder.find('id=value', parent=webelement)
-        verify(webelement).find_elements_by_id("value")
-
-    def test_find_by_name_parent_is_webelement(self):
-        webelement = mock()
-        when(self.finder)._is_webelement(webelement).thenReturn(True)
-        when(self.finder)._is_webelement('name=value').thenReturn(False)
-        when(webelement).find_elements_by_name('value').thenReturn([mock()])
-        self.finder.find('name=value', parent=webelement)
-        verify(webelement).find_elements_by_name("value")
-
-    def test_find_by_dom__parent_is_webelement(self):
-        webelement = mock()
-        when(self.finder)._is_webelement(webelement).thenReturn(True)
-        when(self.finder)._is_webelement('dom=value').thenReturn(False)
-        when(self.finder)._disallow_webelement_parent(webelement).thenRaise(
-            ValueError('This method does not allow webelement as parent'))
-        with self.assertRaisesRegexp(ValueError, 'not allow webelement as parent'):
-            self.finder.find('dom=value', parent=webelement)
-
-    def test_find_by_sizzle_parent_is_webelement(self):
-        webelement = mock()
-        when(self.finder)._is_webelement(webelement).thenReturn(True)
-        when(self.finder)._is_webelement('sizzle=div.class').thenReturn(False)
-        when(self.finder)._disallow_webelement_parent(webelement).thenRaise(
-            ValueError('This method does not allow webelement as parent'))
-        with self.assertRaisesRegexp(ValueError, 'not allow webelement as parent'):
-            self.finder.find('sizzle=div.class', parent=webelement)
-
-    def test_find_by_link_text_parent_is_webelement(self):
-        webelement = mock()
-        when(self.finder)._is_webelement(webelement).thenReturn(True)
-        when(self.finder)._is_webelement('link=My Link').thenReturn(False)
-        when(webelement).find_elements_by_link_text(
-            'My Link').thenReturn([mock()])
-        self.finder.find('link=My Link', parent=webelement)
-        verify(webelement).find_elements_by_link_text("My Link")
-
-    def test_find_by_partial_link_text_parent_is_webelement(self):
-        webelement = mock()
-        when(self.finder)._is_webelement(webelement).thenReturn(True)
-        when(self.finder)._is_webelement('partial link=My L').thenReturn(False)
-        when(webelement).find_elements_by_partial_link_text(
-            'My L').thenReturn([mock()])
-        self.finder.find('partial link=My L', parent=webelement)
-        verify(webelement).find_elements_by_partial_link_text("My L")
-
-    def test_find_by_css_parent_is_webelement(self):
-        webelement = mock()
-        when(self.finder)._is_webelement(webelement).thenReturn(True)
-        when(self.finder)._is_webelement('css=div').thenReturn(False)
-        when(webelement).find_elements_by_css_selector(
-            'div').thenReturn([mock()])
-        self.finder.find('css=div', parent=webelement)
-        verify(webelement).find_elements_by_css_selector("div")
-
-    def test_find_by_class_parent_is_webelement(self):
-        webelement = mock()
-        when(self.finder)._is_webelement(webelement).thenReturn(True)
-        when(self.finder)._is_webelement('class=name').thenReturn(False)
-        when(webelement).find_elements_by_class_name(
-            'name').thenReturn([mock()])
-        self.finder.find('class=name', parent=webelement)
-        verify(webelement).find_elements_by_class_name("name")
-
-    def test_find_by_tag_name_parent_is_webelement(self):
-        webelement = mock()
-        when(self.finder)._is_webelement(webelement).thenReturn(True)
-        when(self.finder)._is_webelement('tag=name').thenReturn(False)
-        when(webelement).find_elements_by_tag_name(
-            'name').thenReturn([mock()])
-        self.finder.find('tag=name', parent=webelement)
-        verify(webelement).find_elements_by_tag_name("name")
-
-    def test_find_sc_locator_parent_is_webelement(self):
-        webelement = mock()
-        when(self.finder)._is_webelement(webelement).thenReturn(True)
-        when(self.finder)._is_webelement('scLocator=div').thenReturn(False)
-        when(self.finder)._disallow_webelement_parent(webelement).thenRaise(
-            ValueError('This method does not allow webelement as parent'))
-        with self.assertRaisesRegexp(ValueError, 'not allow webelement as parent'):
-            self.finder.find('scLocator=div', parent=webelement)
-
-    def test_find_by_default_parent_is_webelement(self):
-        xpath = "//*[(@id='name' or @name='name')]"
-        webelement = mock()
-        when(self.finder)._is_webelement(webelement).thenReturn(True)
-        when(self.finder)._is_webelement('default=name').thenReturn(False)
-        when(webelement).find_elements_by_xpath(
-            xpath).thenReturn([mock()])
-        self.finder.find('default=name', parent=webelement)
-        verify(webelement).find_elements_by_xpath(xpath)
-
-
-class ElementFinderTests(unittest.TestCase):
-
-    def setUp(self):
-        self.ctx = mock()
-        self.ctx.driver = self.driver = mock()
-        self.finder = ElementFinder(self.ctx)
-
-    def tearDown(self):
-        unstub()
-
-    def test_non_existing_prefix(self):
-        with self.assertRaises(ElementNotFound):
-            self.finder.find("something=test1")
-        with self.assertRaises(ElementNotFound):
-            self.finder.find("foo:bar")
-
-    def test_find_with_no_tag(self):
-        self.finder.find("test1", required=False)
-        verify(self.driver).find_elements_by_xpath("//*[(@id='test1' or "
-                                                         "@name='test1')]")
-
-    def test_find_with_explicit_default_strategy(self):
-        self.finder.find("default=test1", required=False)
-        verify(self.driver).find_elements_by_xpath("//*[(@id='test1' or "
-                                                         "@name='test1')]")
-
-    def test_find_with_explicit_default_strategy_and_equals(self):
-        self.driver.current_url = "http://localhost/mypage.html"
-        self.finder.find("default=page.do?foo=bar", tag='a', required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//a[(@id='page.do?foo=bar' or @name='page.do?foo=bar' or "
-            "@href='page.do?foo=bar' or "
-            "normalize-space(descendant-or-self::text())='page.do?foo=bar' or "
-            "@href='http://localhost/page.do?foo=bar')]")
-
-    def test_find_with_tag(self):
-        self.finder.find("test1", tag='div', required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//div[(@id='test1' or @name='test1')]")
-
-    def test_find_with_locator_with_apos(self):
-        self.finder.find("test '1'", required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//*[(@id=\"test '1'\" or @name=\"test '1'\")]")
-
-    def test_find_with_locator_with_quote(self):
-        self.finder.find("test \"1\"", required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//*[(@id='test \"1\"' or @name='test \"1\"')]")
-
-    def test_find_with_locator_with_quote_and_apos(self):
-        self.finder.find("test \"1\" and '2'", required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//*[(@id=concat('test \"1\" and ', \"'\", '2', \"'\", '') "
-            "or @name=concat('test \"1\" and ', \"'\", '2', \"'\", ''))]")
-
-    def test_find_with_a(self):
-        self.driver.current_url = "http://localhost/mypage.html"
-        self.finder.find("test1", tag='a', required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//a[(@id='test1' or @name='test1' or @href='test1' or "
-            "normalize-space(descendant-or-self::text())='test1' or "
-            "@href='http://localhost/test1')]")
-
-    def test_find_with_link_synonym(self):
-        self.driver.current_url = "http://localhost/mypage.html"
-        self.finder.find("test1", tag='link', required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//a[(@id='test1' or @name='test1' or @href='test1' or "
-            "normalize-space(descendant-or-self::text())='test1' or "
-            "@href='http://localhost/test1')]")
-
-    def test_find_with_img(self):
-        self.driver.current_url = "http://localhost/mypage.html"
-        self.finder.find("test1", tag='img', required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//img[(@id='test1' or @name='test1' or @src='test1' or "
-            "@alt='test1' or @src='http://localhost/test1')]")
-
-    def test_find_with_image_synonym(self):
-        self.driver.current_url = "http://localhost/mypage.html"
-        self.finder.find("test1", tag='image', required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//img[(@id='test1' or @name='test1' or @src='test1' or "
-            "@alt='test1' or @src='http://localhost/test1')]")
-
-    def test_find_with_input(self):
-        self.driver.current_url = "http://localhost/mypage.html"
-        self.finder.find("test1", tag='input', required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//input[(@id='test1' or @name='test1' or @value='test1' or "
-            "@src='test1' or @src='http://localhost/test1')]")
-
-    def test_find_with_radio_button_synonym(self):
-        self.driver.current_url = "http://localhost/mypage.html"
-        self.finder.find("test1", tag='radio button', required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//input[@type='radio' and (@id='test1' or @name='test1' or "
-            "@value='test1' or @src='test1' or "
-            "@src='http://localhost/test1')]")
-
-    def test_find_with_checkbox_synonym(self):
-        self.driver.current_url = "http://localhost/mypage.html"
-        self.finder.find("test1", tag='checkbox', required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//input[@type='checkbox' and (@id='test1' or @name='test1' or "
-            "@value='test1' or @src='test1' or "
-            "@src='http://localhost/test1')]")
-
-    def test_find_with_file_upload_synonym(self):
-        self.driver.current_url = "http://localhost/mypage.html"
-        self.finder.find("test1", tag='file upload', required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//input[@type='file' and (@id='test1' or @name='test1' or "
-            "@value='test1' or @src='test1' or "
-            "@src='http://localhost/test1')]")
-
-    def test_find_with_text_field_synonym(self):
-        self.driver.current_url = "http://localhost/mypage.html"
-        self.finder.find("test1", tag='text field', required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//input[@type[. = 'date' or . = 'datetime-local' or . = 'email' or "
-            ". = 'month' or . = 'number' or . = 'password' or . = 'search' or "
-            ". = 'tel' or . = 'text' or . = 'time' or . = 'url' or . = 'week' or . = 'file'] and "
-            "(@id='test1' or @name='test1' or @value='test1' or @src='test1' or "
-            "@src='http://localhost/test1')]")
-
-    def test_find_with_button(self):
-        self.finder.find("test1", tag='button', required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//button[(@id='test1' or @name='test1' or @value='test1' or "
-            "normalize-space(descendant-or-self::text())='test1')]")
-
-    def test_find_with_select(self):
-        self.finder.find("test1", tag='select', required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//select[(@id='test1' or @name='test1')]")
-
-    def test_find_with_list_synonym(self):
-        self.finder.find("test1", tag='list', required=False)
-        verify(self.driver).find_elements_by_xpath(
-            "//select[(@id='test1' or @name='test1')]")
-
-    def test_find_with_implicit_xpath(self):
-        elements = self._make_mock_elements('div', 'a', 'span', 'a')
-        when(self.driver).find_elements_by_xpath(
-            "//*[(@test='1')]").thenReturn(elements)
-        result = self.finder.find("//*[(@test='1')]", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("//*[(@test='1')]", tag='a',
-                                  first_only=False)
-        self.assertEqual(result, [elements[1], elements[3]])
-
-    def test_find_by_identifier(self):
-        id_elements = self._make_mock_elements('div', 'a')
-        name_elements = self._make_mock_elements('span', 'a')
-        when(self.driver).find_elements_by_id("test1").thenReturn(
-            list(id_elements)).thenReturn(list(id_elements))
-        when(self.driver).find_elements_by_name("test1").thenReturn(
-            list(name_elements)).thenReturn(list(name_elements))
-        all_elements = list(id_elements)
-        all_elements.extend(name_elements)
-        result = self.finder.find("identifier=test1", first_only=False)
-        self.assertEqual(result, all_elements)
-        result = self.finder.find("identifier=test1", tag='a',
-                                  first_only=False)
-        self.assertEqual(result, [id_elements[1], name_elements[1]])
-
-    def test_find_by_id(self):
-        elements = self._make_mock_elements('div', 'a', 'span', 'a')
-        when(self.driver).find_elements_by_id("test1").thenReturn(
-            elements)
-        result = self.finder.find("id=test1", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("id=test1", tag='a', first_only=False)
-        self.assertEqual(result, [elements[1], elements[3]])
-
-    def test_find_by_name(self):
-        elements = self._make_mock_elements('div', 'a', 'span', 'a')
-        when(self.driver).find_elements_by_name("test1").thenReturn(
-            elements)
-        result = self.finder.find("name=test1", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("name=test1", tag='a', first_only=False)
-        self.assertEqual(result, [elements[1], elements[3]])
-
-    def test_find_by_xpath(self):
-        elements = self._make_mock_elements('div', 'a', 'span', 'a')
-        when(self.driver).find_elements_by_xpath(
-            "//*[(@test='1')]").thenReturn(elements)
-        result = self.finder.find("xpath=//*[(@test='1')]", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("xpath=//*[(@test='1')]", tag='a',
-                                  first_only=False)
-        self.assertEqual(result, [elements[1], elements[3]])
-
-    def test_find_by_dom(self):
-        elements = self._make_mock_elements('div', 'a', 'span', 'a')
-        elems = [elements[1], elements[3]]
-        when(self.driver).execute_script(
-            "return document.getElementsByTagName('a');").thenReturn(elems)
-        result = self.finder.find("dom=document.getElementsByTagName('a')",
-                                  first_only=False)
-        self.assertEqual(result, [elements[1], elements[3]])
-
-    def test_find_by_link_text(self):
-        elements = self._make_mock_elements('div', 'a', 'span', 'a')
-        when(self.driver).find_elements_by_link_text(
-            "my link").thenReturn(elements)
-        result = self.finder.find("link=my link", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("link=my link", tag='a', first_only=False)
-        self.assertEqual(result, [elements[1], elements[3]])
-
-    def test_find_by_partial_link_text(self):
-        elements = self._make_mock_elements('div', 'a', 'span', 'a')
-        when(self.driver).find_elements_by_partial_link_text(
-            "my link").thenReturn(elements)
-        result = self.finder.find("partial link=my link", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("partial link=my link", tag='a',
-                                  first_only=False)
-        self.assertEqual(result, [elements[1], elements[3]])
-
-    def test_find_by_css_selector(self):
-        elements = self._make_mock_elements('div', 'a', 'span', 'a')
-        when(self.driver).find_elements_by_css_selector(
-            "#test1").thenReturn(elements)
-        result = self.finder.find("css=#test1", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("css=#test1", tag='a', first_only=False)
-        self.assertEqual(result, [elements[1], elements[3]])
-
-    def test_find_by_class_names(self):
-        elements = self._make_mock_elements('div', 'a', 'span', 'a')
-        when(self.driver).find_elements_by_class_name(
-            "test1").thenReturn(elements)
-        result = self.finder.find("class=test1", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("class=test1", tag='a', first_only=False)
-        self.assertEqual(result, [elements[1], elements[3]])
-
-    def test_find_by_tag_name(self):
-        elements = self._make_mock_elements('div', 'a', 'span', 'a')
-        when(self.driver).find_elements_by_tag_name(
-            "div").thenReturn(elements)
-        result = self.finder.find("tag=div", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("tag=div", tag='a', first_only=False)
-        self.assertEqual(result, [elements[1], elements[3]])
-
-    def test_find_with_sloppy_prefix(self):
-        elements = self._make_mock_elements('div', 'a', 'span', 'a')
-        when(self.driver).find_elements_by_id("test1").thenReturn(
-            elements)
-        when(self.driver).find_elements_by_partial_link_text(
-            "test1").thenReturn(elements)
-        result = self.finder.find("ID=test1", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("iD=test1", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("id=test1", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("  id =test1", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("  partiallink =test1", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("  p art iallin k =test1", first_only=False)
-        self.assertEqual(result, elements)
-
-    def test_find_with_sloppy_criteria(self):
-        elements = self._make_mock_elements('div', 'a', 'span', 'a')
-        when(self.driver).find_elements_by_id("test1  ").thenReturn(
-            elements)
-        result = self.finder.find("id= test1  ", first_only=False)
-        self.assertEqual(result, elements)
-
-    def test_find_by_id_with_synonym_and_constraints(self):
-        elements = self._make_mock_elements('div', 'input', 'span', 'input',
-                                            'a', 'input', 'div', 'input',
-                                            'input')
-        elements[1].set_attribute('type', 'radio')
-        elements[3].set_attribute('type', 'checkbox')
-        elements[5].set_attribute('type', 'text')
-        elements[7].set_attribute('type', 'file')
-        elements[8].set_attribute('type', 'email')
-        when(self.driver).find_elements_by_id("test1").thenReturn(
-            elements)
-        result = self.finder.find("id=test1", first_only=False)
-        self.assertEqual(result, elements)
-        result = self.finder.find("id=test1", tag='input', first_only=False)
-        self.assertEqual(result, [elements[1], elements[3], elements[5],
-                                  elements[7], elements[8]])
-        result = self.finder.find("id=test1", tag='radio button',
-                                  first_only=False)
-        self.assertEqual(result, [elements[1]])
-        result = self.finder.find("id=test1", tag='checkbox', first_only=False)
-        self.assertEqual(result, [elements[3]])
-        result = self.finder.find("id=test1", tag='text field',
-                                  first_only=False)
-        self.assertEqual(result, [elements[5], elements[7], elements[8]])
-        result = self.finder.find("id=test1", tag='file upload',
-                                  first_only=False)
-        self.assertEqual(result, [elements[7]])
-
-    def test_find_returns_bad_values(self):
-        # selenium.webdriver.ie.webdriver.WebDriver sometimes returns these
-        # and ChromeDriver has also returned None:
-        # https://github.com/SeleniumHQ/selenium/issues/4555
-        locators = ('find_elements_by_id', 'find_elements_by_name',
-                    'find_elements_by_xpath', 'find_elements_by_link_text',
-                    'find_elements_by_css_selector',
-                    'find_elements_by_tag_name')
-        for bad_value in (None, {'': None}):
-            for func_name in locators:
-                when_find_func = getattr(when(self.driver), func_name)
-                when_find_func(any()).thenReturn(bad_value)
-            for locator in ("identifier=it", "id=it", "name=it", "xpath=//div",
-                            "link=it", "css=div.it", "tag=div", "default"):
-                result = self.finder.find(locator, required=False,
-                                          first_only=False)
-                self.assertEqual(result, [])
-                result = self.finder.find(locator, tag='div', required=False,
-                                          first_only=False)
-                self.assertEqual(result, [])
-
-    def _make_mock_elements(self, *tags):
-        elements = []
-        for tag in tags:
-            element = self._make_mock_element(tag)
-            elements.append(element)
-        return elements
-
-    def _make_mock_element(self, tag):
-        element = mock()
-        element.tag_name = tag
-        element.attributes = {}
-
-        def set_attribute(name, value):
-            element.attributes[name] = value
-        element.set_attribute = set_attribute
-
-        def get_attribute(name):
-            return element.attributes[name]
-        element.get_attribute = get_attribute
-
-        return element
+@pytest.fixture(scope='function')
+def finder():
+    ctx = mock()
+    ctx.driver = mock()
+    return ElementFinder(ctx)
+
+
+def teardown_function():
+    unstub()
+
+
+def test_implicit_xpath():
+    _verify_parse_locator('//foo', 'xpath', '//foo')
+    _verify_parse_locator('(//foo)', 'xpath', '(//foo)')
+    _verify_parse_locator('//id=bar', 'xpath', '//id=bar')
+
+
+def test_no_separator():
+    _verify_parse_locator('foo', 'default', 'foo')
+    _verify_parse_locator('', 'default', '')
+
+
+def test_equal_sign_as_separator():
+    _verify_parse_locator('class=foo', 'class', 'foo')
+    _verify_parse_locator('id=foo=bar', 'id', 'foo=bar')
+
+
+def test_colon_as_separator():
+    _verify_parse_locator('class:foo', 'class', 'foo')
+    _verify_parse_locator('id:foo:bar', 'id', 'foo:bar')
+
+
+def test_use_first_separator_when_both_are_used():
+    _verify_parse_locator('id:foo=bar', 'id', 'foo=bar')
+    _verify_parse_locator('id=foo:bar', 'id', 'foo:bar')
+
+
+def test_preserve_trailing_whitespace():
+    _verify_parse_locator('//foo/bar  ', 'xpath', '//foo/bar  ')
+    _verify_parse_locator('class=foo  ', 'class', 'foo  ')
+
+
+def test_remove_whitespace_around_prefix_and_separator():
+    _verify_parse_locator('class = foo', 'class', 'foo')
+    _verify_parse_locator('class : foo', 'class', 'foo')
+    _verify_parse_locator('  id  = foo = bar  ', 'id', 'foo = bar  ')
+    _verify_parse_locator('  id  : foo : bar  ', 'id', 'foo : bar  ')
+
+
+def test_separator_without_matching_prefix_is_ignored():
+    _verify_parse_locator('no=match', 'default', 'no=match')
+    _verify_parse_locator('no:match', 'default', 'no:match')
+
+
+def test_registered_strategy_can_be_used_as_prefix():
+    _verify_parse_locator('registered=no', 'default', 'registered=no')
+    _verify_parse_locator('registered:no', 'default', 'registered:no')
+    finder = ElementFinder(None)
+    finder.register('registered', lambda *args: None, persist=True)
+    _verify_parse_locator('registered=yes!!', 'registered', 'yes!!', finder)
+    _verify_parse_locator('registered:yes!!', 'registered', 'yes!!', finder)
+
+
+def _verify_parse_locator(locator, prefix, criteria, finder=None):
+    if not finder:
+        finder = ElementFinder(None)
+    parse_locator = finder._parse_locator
+    assert parse_locator(locator), (prefix, criteria)
+
+
+def test_parent_is_not_webelement(finder):
+    with pytest.raises(ValueError):
+        finder.find("//div", parent='//button')
+
+
+def test_find_by_xpath_parent_is_webelement(finder):
+    webelement = mock()
+    when(finder)._is_webelement(webelement).thenReturn(True)
+    when(finder)._is_webelement('//div').thenReturn(False)
+    when(webelement).find_elements_by_xpath('//div').thenReturn([mock()])
+    finder.find('//div', parent=webelement)
+    verify(webelement).find_elements_by_xpath('//div')
+
+
+def test_find_by_identifier_parent_is_webelement(finder):
+    webelement = mock()
+    when(finder)._is_webelement(webelement).thenReturn(True)
+    when(finder)._is_webelement('identifier=value').thenReturn(False)
+    when(webelement).find_elements_by_id('value').thenReturn([mock()])
+    when(webelement).find_elements_by_name('value').thenReturn([mock()])
+    finder.find('identifier=value', parent=webelement)
+    verify(webelement).find_elements_by_name('value')
+    verify(webelement).find_elements_by_id('value')
+
+
+def test_find_by_id_parent_is_webelement(finder):
+    webelement = mock()
+    when(finder)._is_webelement(webelement).thenReturn(True)
+    when(finder)._is_webelement('id=value').thenReturn(False)
+    when(webelement).find_elements_by_id('value').thenReturn([mock()])
+    finder.find('id=value', parent=webelement)
+    verify(webelement).find_elements_by_id("value")
+
+
+def test_find_by_name_parent_is_webelement(finder):
+    webelement = mock()
+    when(finder)._is_webelement(webelement).thenReturn(True)
+    when(finder)._is_webelement('name=value').thenReturn(False)
+    when(webelement).find_elements_by_name('value').thenReturn([mock()])
+    finder.find('name=value', parent=webelement)
+    verify(webelement).find_elements_by_name("value")
+
+
+def test_find_by_dom__parent_is_webelement(finder):
+    webelement = mock()
+    when(finder)._is_webelement(webelement).thenReturn(True)
+    when(finder)._is_webelement('dom=value').thenReturn(False)
+    when(finder)._disallow_webelement_parent(webelement).thenRaise(
+        ValueError('This method does not allow webelement as parent'))
+    with pytest.raises(ValueError) as error:
+        finder.find('dom=value', parent=webelement)
+    assert 'not allow webelement as parent' in str(error.value)
+
+
+def test_find_by_sizzle_parent_is_webelement(finder):
+    webelement = mock()
+    when(finder)._is_webelement(webelement).thenReturn(True)
+    when(finder)._is_webelement('sizzle=div.class').thenReturn(False)
+    when(finder)._disallow_webelement_parent(webelement).thenRaise(
+        ValueError('This method does not allow webelement as parent'))
+    with pytest.raises(ValueError) as error:
+        finder.find('sizzle=div.class', parent=webelement)
+    assert 'not allow webelement as parent' in str(error.value)
+
+
+def test_find_by_link_text_parent_is_webelement(finder):
+    webelement = mock()
+    when(finder)._is_webelement(webelement).thenReturn(True)
+    when(finder)._is_webelement('link=My Link').thenReturn(False)
+    when(webelement).find_elements_by_link_text(
+        'My Link').thenReturn([mock()])
+    finder.find('link=My Link', parent=webelement)
+    verify(webelement).find_elements_by_link_text("My Link")
+
+
+def test_find_by_partial_link_text_parent_is_webelement(finder):
+    webelement = mock()
+    when(finder)._is_webelement(webelement).thenReturn(True)
+    when(finder)._is_webelement('partial link=My L').thenReturn(False)
+    when(webelement).find_elements_by_partial_link_text(
+        'My L').thenReturn([mock()])
+    finder.find('partial link=My L', parent=webelement)
+    verify(webelement).find_elements_by_partial_link_text("My L")
+
+
+def test_find_by_css_parent_is_webelement(finder):
+    webelement = mock()
+    when(finder)._is_webelement(webelement).thenReturn(True)
+    when(finder)._is_webelement('css=div').thenReturn(False)
+    when(webelement).find_elements_by_css_selector(
+        'div').thenReturn([mock()])
+    finder.find('css=div', parent=webelement)
+    verify(webelement).find_elements_by_css_selector("div")
+
+
+def test_find_by_class_parent_is_webelement(finder):
+    webelement = mock()
+    when(finder)._is_webelement(webelement).thenReturn(True)
+    when(finder)._is_webelement('class=name').thenReturn(False)
+    when(webelement).find_elements_by_class_name(
+        'name').thenReturn([mock()])
+    finder.find('class=name', parent=webelement)
+    verify(webelement).find_elements_by_class_name("name")
+
+
+def test_find_by_tag_name_parent_is_webelement(finder):
+    webelement = mock()
+    when(finder)._is_webelement(webelement).thenReturn(True)
+    when(finder)._is_webelement('tag=name').thenReturn(False)
+    when(webelement).find_elements_by_tag_name(
+        'name').thenReturn([mock()])
+    finder.find('tag=name', parent=webelement)
+    verify(webelement).find_elements_by_tag_name("name")
+
+
+def test_find_sc_locator_parent_is_webelement(finder):
+    webelement = mock()
+    when(finder)._is_webelement(webelement).thenReturn(True)
+    when(finder)._is_webelement('scLocator=div').thenReturn(False)
+    when(finder)._disallow_webelement_parent(webelement).thenRaise(
+        ValueError('This method does not allow webelement as parent'))
+    with pytest.raises(ValueError) as error:
+        finder.find('scLocator=div', parent=webelement)
+    assert 'not allow webelement as parent' in str(error.value)
+
+
+def test_find_by_default_parent_is_webelement(finder):
+    xpath = "//*[(@id='name' or @name='name')]"
+    webelement = mock()
+    when(finder)._is_webelement(webelement).thenReturn(True)
+    when(finder)._is_webelement('default=name').thenReturn(False)
+    when(webelement).find_elements_by_xpath(
+        xpath).thenReturn([mock()])
+    finder.find('default=name', parent=webelement)
+    verify(webelement).find_elements_by_xpath(xpath)
+
+
+def test_non_existing_prefix(finder):
+    with pytest.raises(ElementNotFound):
+        finder.find("something=test1")
+
+    with pytest.raises(ElementNotFound):
+        finder.find("foo:bar")
+
+
+def test_find_with_no_tag(finder):
+    driver = _get_driver(finder)
+    finder.find("test1", required=False)
+    verify(driver).find_elements_by_xpath("//*[(@id='test1' or "
+                                          "@name='test1')]")
+
+
+def test_find_with_explicit_default_strategy(finder):
+    driver = _get_driver(finder)
+    finder.find("default=test1", required=False)
+    verify(driver).find_elements_by_xpath("//*[(@id='test1' or "
+                                          "@name='test1')]")
+
+
+def test_find_with_explicit_default_strategy_and_equals(finder):
+    driver = _get_driver(finder)
+    driver.current_url = "http://localhost/mypage.html"
+    finder.find("default=page.do?foo=bar", tag='a', required=False)
+    verify(driver).find_elements_by_xpath(
+        "//a[(@id='page.do?foo=bar' or @name='page.do?foo=bar' or "
+        "@href='page.do?foo=bar' or "
+        "normalize-space(descendant-or-self::text())='page.do?foo=bar' or "
+        "@href='http://localhost/page.do?foo=bar')]")
+
+
+def test_find_with_tag(finder):
+    driver = _get_driver(finder)
+    finder.find("test1", tag='div', required=False)
+    verify(driver).find_elements_by_xpath(
+        "//div[(@id='test1' or @name='test1')]")
+
+
+def test_find_with_locator_with_apos(finder):
+    driver = _get_driver(finder)
+    finder.find("test '1'", required=False)
+    verify(driver).find_elements_by_xpath(
+        "//*[(@id=\"test '1'\" or @name=\"test '1'\")]")
+
+
+def test_find_with_locator_with_quote(finder):
+    driver = _get_driver(finder)
+    finder.find("test \"1\"", required=False)
+    verify(driver).find_elements_by_xpath(
+        "//*[(@id='test \"1\"' or @name='test \"1\"')]")
+
+
+def test_find_with_locator_with_quote_and_apos(finder):
+    driver = _get_driver(finder)
+    finder.find("test \"1\" and '2'", required=False)
+    verify(driver).find_elements_by_xpath(
+        "//*[(@id=concat('test \"1\" and ', \"'\", '2', \"'\", '') "
+        "or @name=concat('test \"1\" and ', \"'\", '2', \"'\", ''))]")
+
+
+def test_find_with_a(finder):
+    driver = _get_driver(finder)
+    driver.current_url = "http://localhost/mypage.html"
+    finder.find("test1", tag='a', required=False)
+    verify(driver).find_elements_by_xpath(
+        "//a[(@id='test1' or @name='test1' or @href='test1' or "
+        "normalize-space(descendant-or-self::text())='test1' or "
+        "@href='http://localhost/test1')]")
+
+
+def test_find_with_link_synonym(finder):
+    driver = _get_driver(finder)
+    driver.current_url = "http://localhost/mypage.html"
+    finder.find("test1", tag='link', required=False)
+    verify(driver).find_elements_by_xpath(
+        "//a[(@id='test1' or @name='test1' or @href='test1' or "
+        "normalize-space(descendant-or-self::text())='test1' or "
+        "@href='http://localhost/test1')]")
+
+
+def test_find_with_img(finder):
+    driver = _get_driver(finder)
+    driver.current_url = "http://localhost/mypage.html"
+    finder.find("test1", tag='img', required=False)
+    verify(driver).find_elements_by_xpath(
+        "//img[(@id='test1' or @name='test1' or @src='test1' or "
+        "@alt='test1' or @src='http://localhost/test1')]")
+
+
+def test_find_with_image_synonym(finder):
+    driver = _get_driver(finder)
+    driver.current_url = "http://localhost/mypage.html"
+    finder.find("test1", tag='image', required=False)
+    verify(driver).find_elements_by_xpath(
+        "//img[(@id='test1' or @name='test1' or @src='test1' or "
+        "@alt='test1' or @src='http://localhost/test1')]")
+
+
+def test_find_with_input(finder):
+    driver = _get_driver(finder)
+    driver.current_url = "http://localhost/mypage.html"
+    finder.find("test1", tag='input', required=False)
+    verify(driver).find_elements_by_xpath(
+        "//input[(@id='test1' or @name='test1' or @value='test1' or "
+        "@src='test1' or @src='http://localhost/test1')]")
+
+
+def test_find_with_radio_button_synonym(finder):
+    driver = _get_driver(finder)
+    driver.current_url = "http://localhost/mypage.html"
+    finder.find("test1", tag='radio button', required=False)
+    verify(driver).find_elements_by_xpath(
+        "//input[@type='radio' and (@id='test1' or @name='test1' or "
+        "@value='test1' or @src='test1' or "
+        "@src='http://localhost/test1')]")
+
+
+def test_find_with_checkbox_synonym(finder):
+    driver = _get_driver(finder)
+    driver.current_url = "http://localhost/mypage.html"
+    finder.find("test1", tag='checkbox', required=False)
+    verify(driver).find_elements_by_xpath(
+        "//input[@type='checkbox' and (@id='test1' or @name='test1' or "
+        "@value='test1' or @src='test1' or "
+        "@src='http://localhost/test1')]")
+
+
+def test_find_with_file_upload_synonym(finder):
+    driver = _get_driver(finder)
+    driver.current_url = "http://localhost/mypage.html"
+    finder.find("test1", tag='file upload', required=False)
+    verify(driver).find_elements_by_xpath(
+        "//input[@type='file' and (@id='test1' or @name='test1' or "
+        "@value='test1' or @src='test1' or "
+        "@src='http://localhost/test1')]")
+
+
+def test_find_with_text_field_synonym(finder):
+    driver = _get_driver(finder)
+    driver.current_url = "http://localhost/mypage.html"
+    finder.find("test1", tag='text field', required=False)
+    verify(driver).find_elements_by_xpath(
+        "//input[@type[. = 'date' or . = 'datetime-local' or . = 'email' or "
+        ". = 'month' or . = 'number' or . = 'password' or . = 'search' or "
+        ". = 'tel' or . = 'text' or . = 'time' or . = 'url' or . = 'week' or . = 'file'] and "
+        "(@id='test1' or @name='test1' or @value='test1' or @src='test1' or "
+        "@src='http://localhost/test1')]")
+
+
+def test_find_with_button(finder):
+    driver = _get_driver(finder)
+    finder.find("test1", tag='button', required=False)
+    verify(driver).find_elements_by_xpath(
+        "//button[(@id='test1' or @name='test1' or @value='test1' or "
+        "normalize-space(descendant-or-self::text())='test1')]")
+
+
+def test_find_with_select(finder):
+    driver = _get_driver(finder)
+    finder.find("test1", tag='select', required=False)
+    verify(driver).find_elements_by_xpath(
+        "//select[(@id='test1' or @name='test1')]")
+
+
+def test_find_with_list_synonym(finder):
+    driver = _get_driver(finder)
+    finder.find("test1", tag='list', required=False)
+    verify(driver).find_elements_by_xpath(
+        "//select[(@id='test1' or @name='test1')]")
+
+
+def test_find_with_implicit_xpath(finder):
+    driver = _get_driver(finder)
+    elements = _make_mock_elements('div', 'a', 'span', 'a')
+    when(driver).find_elements_by_xpath(
+        "//*[(@test='1')]").thenReturn(elements)
+    result = finder.find("//*[(@test='1')]", first_only=False)
+    assert result == elements
+
+    result = finder.find("//*[(@test='1')]", tag='a',
+                         first_only=False)
+    assert result == [elements[1], elements[3]]
+
+
+def test_find_by_identifier(finder):
+    driver = _get_driver(finder)
+    id_elements = _make_mock_elements('div', 'a')
+    name_elements = _make_mock_elements('span', 'a')
+    when(driver).find_elements_by_id("test1").thenReturn(
+        list(id_elements)).thenReturn(list(id_elements))
+    when(driver).find_elements_by_name("test1").thenReturn(
+        list(name_elements)).thenReturn(list(name_elements))
+    all_elements = list(id_elements)
+    all_elements.extend(name_elements)
+    result = finder.find("identifier=test1", first_only=False)
+    assert result == all_elements
+    result = finder.find("identifier=test1", tag='a',
+                         first_only=False)
+    assert result == [id_elements[1], name_elements[1]]
+
+
+def test_find_by_id(finder):
+    driver = _get_driver(finder)
+    elements = _make_mock_elements('div', 'a', 'span', 'a')
+    when(driver).find_elements_by_id("test1").thenReturn(
+        elements)
+    result = finder.find("id=test1", first_only=False)
+    assert result == elements
+    result = finder.find("id=test1", tag='a', first_only=False)
+    assert result == [elements[1], elements[3]]
+
+
+def test_find_by_name(finder):
+    driver = _get_driver(finder)
+    elements = _make_mock_elements('div', 'a', 'span', 'a')
+    when(driver).find_elements_by_name("test1").thenReturn(elements)
+    result = finder.find("name=test1", first_only=False)
+    assert result == elements
+    result = finder.find("name=test1", tag='a', first_only=False)
+    assert result == [elements[1], elements[3]]
+
+
+def test_find_by_xpath(finder):
+    driver = _get_driver(finder)
+    elements = _make_mock_elements('div', 'a', 'span', 'a')
+    when(driver).find_elements_by_xpath(
+        "//*[(@test='1')]").thenReturn(elements)
+    result = finder.find("xpath=//*[(@test='1')]", first_only=False)
+    assert result == elements
+    result = finder.find("xpath=//*[(@test='1')]", tag='a',
+                         first_only=False)
+    assert result == [elements[1], elements[3]]
+
+
+def test_find_by_dom(finder):
+    driver = _get_driver(finder)
+    elements = _make_mock_elements('div', 'a', 'span', 'a')
+    elems = [elements[1], elements[3]]
+    when(driver).execute_script(
+        "return document.getElementsByTagName('a');").thenReturn(elems)
+    result = finder.find("dom=document.getElementsByTagName('a')",
+                         first_only=False)
+    assert result == [elements[1], elements[3]]
+
+
+def test_find_by_link_text(finder):
+    driver = _get_driver(finder)
+    elements = _make_mock_elements('div', 'a', 'span', 'a')
+    when(driver).find_elements_by_link_text(
+        "my link").thenReturn(elements)
+    result = finder.find("link=my link", first_only=False)
+    assert result == elements
+    result = finder.find("link=my link", tag='a', first_only=False)
+    assert result == [elements[1], elements[3]]
+
+
+def test_find_by_partial_link_text(finder):
+    driver = _get_driver(finder)
+    elements = _make_mock_elements('div', 'a', 'span', 'a')
+    when(driver).find_elements_by_partial_link_text(
+        "my link").thenReturn(elements)
+    result = finder.find("partial link=my link", first_only=False)
+    assert result == elements
+    result = finder.find("partial link=my link", tag='a',
+                         first_only=False)
+    assert result == [elements[1], elements[3]]
+
+
+def test_find_by_css_selector(finder):
+    driver = _get_driver(finder)
+    elements = _make_mock_elements('div', 'a', 'span', 'a')
+    when(driver).find_elements_by_css_selector(
+        "#test1").thenReturn(elements)
+    result = finder.find("css=#test1", first_only=False)
+    assert result == elements
+    result = finder.find("css=#test1", tag='a', first_only=False)
+    assert result == [elements[1], elements[3]]
+
+
+def test_find_by_class_names(finder):
+    driver = _get_driver(finder)
+    elements = _make_mock_elements('div', 'a', 'span', 'a')
+    when(driver).find_elements_by_class_name(
+        "test1").thenReturn(elements)
+    result = finder.find("class=test1", first_only=False)
+    assert result == elements
+    result = finder.find("class=test1", tag='a', first_only=False)
+    assert result == [elements[1], elements[3]]
+
+
+def test_find_by_tag_name(finder):
+    driver = _get_driver(finder)
+    elements = _make_mock_elements('div', 'a', 'span', 'a')
+    when(driver).find_elements_by_tag_name(
+        "div").thenReturn(elements)
+    result = finder.find("tag=div", first_only=False)
+    assert result == elements
+    result = finder.find("tag=div", tag='a', first_only=False)
+    assert result == [elements[1], elements[3]]
+
+
+def test_find_with_sloppy_prefix(finder):
+    driver = _get_driver(finder)
+    elements = _make_mock_elements('div', 'a', 'span', 'a')
+    when(driver).find_elements_by_id("test1").thenReturn(elements)
+    when(driver).find_elements_by_partial_link_text(
+        "test1").thenReturn(elements)
+    result = finder.find("ID=test1", first_only=False)
+    assert result == elements
+    result = finder.find("iD=test1", first_only=False)
+    assert result == elements
+    result = finder.find("id=test1", first_only=False)
+    assert result == elements
+    result = finder.find("  id =test1", first_only=False)
+    assert result == elements
+    result = finder.find("  partiallink =test1", first_only=False)
+    assert result == elements
+    result = finder.find("  p art iallin k =test1", first_only=False)
+    assert result == elements
+
+
+def test_find_with_sloppy_criteria(finder):
+    driver = _get_driver(finder)
+    elements = _make_mock_elements('div', 'a', 'span', 'a')
+    when(driver).find_elements_by_id("test1  ").thenReturn(elements)
+    result = finder.find("id= test1  ", first_only=False)
+    assert result == elements
+
+
+def test_find_by_id_with_synonym_and_constraints(finder):
+    driver = _get_driver(finder)
+    elements = _make_mock_elements('div', 'input', 'span', 'input',
+                                        'a', 'input', 'div', 'input',
+                                        'input')
+    elements[1].set_attribute('type', 'radio')
+    elements[3].set_attribute('type', 'checkbox')
+    elements[5].set_attribute('type', 'text')
+    elements[7].set_attribute('type', 'file')
+    elements[8].set_attribute('type', 'email')
+    when(driver).find_elements_by_id("test1").thenReturn(elements)
+    result = finder.find("id=test1", first_only=False)
+    assert result == elements
+    result = finder.find("id=test1", tag='input', first_only=False)
+    assert result == [elements[1], elements[3], elements[5],
+                      elements[7], elements[8]]
+    result = finder.find("id=test1", tag='radio button',
+                         first_only=False)
+    assert result == [elements[1]]
+    result = finder.find("id=test1", tag='checkbox', first_only=False)
+    assert result == [elements[3]]
+    result = finder.find("id=test1", tag='text field',
+                         first_only=False)
+    assert result == [elements[5], elements[7], elements[8]]
+    result = finder.find("id=test1", tag='file upload',
+                         first_only=False)
+    assert result == [elements[7]]
+
+
+def test_find_returns_bad_values(finder):
+    # selenium.webdriver.ie.webdriver.WebDriver sometimes returns these
+    # and ChromeDriver has also returned None:
+    # https://github.com/SeleniumHQ/selenium/issues/4555
+    driver = _get_driver(finder)
+    locators = ('find_elements_by_id', 'find_elements_by_name',
+                'find_elements_by_xpath', 'find_elements_by_link_text',
+                'find_elements_by_css_selector',
+                'find_elements_by_tag_name')
+    for bad_value in (None, {'': None}):
+        for func_name in locators:
+            when_find_func = getattr(when(driver), func_name)
+            when_find_func(any()).thenReturn(bad_value)
+        for locator in ("identifier=it", "id=it", "name=it", "xpath=//div",
+                        "link=it", "css=div.it", "tag=div", "default"):
+            result = finder.find(locator, required=False,
+                                 first_only=False)
+            assert result == []
+            result = finder.find(locator, tag='div', required=False,
+                                 first_only=False)
+            assert result == []
+
+
+def _make_mock_elements(*tags):
+    elements = []
+    for tag in tags:
+        element = _make_mock_element(tag)
+        elements.append(element)
+    return elements
+
+
+def _make_mock_element(tag):
+    element = mock()
+    element.tag_name = tag
+    element.attributes = {}
+
+    def set_attribute(name, value):
+        element.attributes[name] = value
+
+    element.set_attribute = set_attribute
+
+    def get_attribute(name):
+        return element.attributes[name]
+
+    element.get_attribute = get_attribute
+
+    return element
+
+
+def _get_driver(finder):
+    return finder.ctx.driver