diff --git a/splinter/driver/__init__.py b/splinter/driver/__init__.py index a57ea263f..e9714ca23 100644 --- a/splinter/driver/__init__.py +++ b/splinter/driver/__init__.py @@ -871,6 +871,23 @@ def type(self, value: str, slowly: bool = False) -> str: # NOQA: A003 """ raise NotImplementedError + def press(self, key: str, delay: int = 0) -> None: + """Focus the element and press the specified key pattern. + + Key names are case sensitive. + + Arguments: + key_name: Name of the key to press. + delay: Time, in seconds, to wait between key down and key up. + + Example: + + >>> browser.find_by_css('.my_element').press('ENTER') + >>> browser.find_by_css('.my_element').press('SHIFT+awesome') + + """ + raise NotImplementedError + def select(self, value: str, slowly: bool = False) -> None: """ Select an ```` element in the element using the ``value`` of the ````. diff --git a/splinter/driver/webdriver/__init__.py b/splinter/driver/webdriver/__init__.py index cd8049f9c..6cf063400 100644 --- a/splinter/driver/webdriver/__init__.py +++ b/splinter/driver/webdriver/__init__.py @@ -25,6 +25,7 @@ from splinter.driver import ElementAPI from splinter.driver.find_links import FindLinks from splinter.driver.webdriver.cookie_manager import CookieManager +from splinter.driver.webdriver.keyboard import Keyboard from splinter.driver.xpath_utils import _concat_xpath_from_str from splinter.element_list import ElementList from splinter.exceptions import ElementDoesNotExist @@ -266,6 +267,7 @@ def __init__(self, driver=None, wait_time=2): self.wait_time = wait_time self.links = FindLinks(self) + self.keyboard = Keyboard(driver) self.driver = driver self._find_elements = self.driver.find_elements @@ -792,6 +794,10 @@ def type(self, value, slowly=False): # NOQA: A003 self._element.send_keys(value) return value + def press(self, key: str, delay: int = 0) -> None: + keyboard = Keyboard(self.driver, self._element) + keyboard.press(key, delay) + def click(self): """Click an element. diff --git a/splinter/driver/webdriver/keyboard.py b/splinter/driver/webdriver/keyboard.py new file mode 100644 index 000000000..ff7ed8ffd --- /dev/null +++ b/splinter/driver/webdriver/keyboard.py @@ -0,0 +1,69 @@ +from typing import Union + +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.remote.webelement import WebElement + + +class Keyboard: + """""" + + def __init__(self, driver, element: Union[WebElement, None] = None) -> None: + self.driver = driver + + self.element = element + + def _resolve_key_down_action(self, action_chain: ActionChains, key: str) -> ActionChains: + # If in selenium keys, use it, if not, assume literal. + key_value = getattr(Keys, key, None) + + if key_value: + chain = action_chain.key_down(key_value, self.element) + elif self.element: + action_chain.send_keys_to_element(self.element, key) + else: + chain = action_chain.send_keys(key) + + return chain + + def _resolve_key_up_action(self, action_chain: ActionChains, key: str) -> ActionChains: + key_value = getattr(Keys, key, None) + + chain = action_chain + if key_value: + chain = action_chain.key_up(key_value, self.element) + + return chain + + def down(self, key: str) -> "Keyboard": + """""" + chain = ActionChains(self.driver) + chain = self._resolve_key_down_action(chain, key) + chain.perform() + return self + + def up(self, key: str) -> "Keyboard": + """""" + chain = ActionChains(self.driver) + chain = self._resolve_key_up_action(chain, key) + chain.perform() + return self + + def press(self, key: str, delay: int = 0) -> "Keyboard": + """""" + key_pattern = key.split("+") + + chain = ActionChains(self.driver) + + for item in key_pattern: + chain = self._resolve_key_down_action(chain, item) + + if delay: + chain = chain.pause(delay) + + for item in key_pattern: + chain = self._resolve_key_up_action(chain, item) + + chain.perform() + + return self diff --git a/tests/element.py b/tests/element.py index 978c8087f..f4a1302a4 100644 --- a/tests/element.py +++ b/tests/element.py @@ -1,6 +1,7 @@ # Copyright 2012 splinter authors. All rights reserved. # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. +from .skip_if import skip_if_zope, skip_if_django, skip_if_flask class ElementTest: @@ -29,3 +30,34 @@ def test_element_html(self): assert ( self.browser.find_by_id("html-property").html == 'inner inner text html test' ) + + @skip_if_zope + @skip_if_django + @skip_if_flask + def test_element_press_modifier(self): + elem = self.browser.find_by_css("[name='q']") + elem.fill("hellox") + + elem.press("BACKSPACE") + + assert elem.value == "hello" + + @skip_if_zope + @skip_if_django + @skip_if_flask + def test_element_press_key(self): + elem = self.browser.find_by_css("[name='q']") + elem.fill("hellox") + + elem.press("a") + + assert elem.value == "helloxa" + + @skip_if_zope + @skip_if_django + @skip_if_flask + def test_element_press_combo(self): + elem = self.browser.find_by_css("[name='q']") + elem.press("SHIFT+a+BACKSPACE+b") + + assert elem.value == "B" diff --git a/tests/form_elements.py b/tests/form_elements.py index bbdb08be2..7e25ecf64 100644 --- a/tests/form_elements.py +++ b/tests/form_elements.py @@ -7,26 +7,7 @@ import pytest from splinter.exceptions import ElementDoesNotExist - - -def skip_if_zope(f): - def wrapper(self, *args, **kwargs): - if self.__class__.__name__ == "TestZopeTestBrowserDriver": - return pytest.skip("skipping this test for zope testbrowser") - else: - f(self, *args, **kwargs) - - return wrapper - - -def skip_if_django(f): - def wrapper(self, *args, **kwargs): - if self.__class__.__name__ == "TestDjangoClientDriver": - return pytest.skip("skipping this test for django") - else: - f(self, *args, **kwargs) - - return wrapper +from .skip_if import skip_if_zope, skip_if_django class FormElementsTest: diff --git a/tests/keyboard.py b/tests/keyboard.py new file mode 100644 index 000000000..b02032824 --- /dev/null +++ b/tests/keyboard.py @@ -0,0 +1,40 @@ +from splinter.driver.webdriver import Keyboard + + +class KeyboardTest: + def test_keyboard_down(self): + keyboard = Keyboard(self.browser.driver) + + keyboard.down("CONTROL") + + elem = self.browser.find_by_css("#keypress_detect") + + assert elem.first + + def test_keyboard_up(self): + keyboard = Keyboard(self.browser.driver) + + keyboard.down("CONTROL") + keyboard.up("CONTROL") + + elem = self.browser.find_by_css("#keyup_detect") + + assert elem.first + + def test_keyboard_press(self): + keyboard = Keyboard(self.browser.driver) + + keyboard.press("CONTROL") + + elem = self.browser.find_by_css("#keyup_detect") + + assert elem.first + + def test_element_press_combo(self): + keyboard = Keyboard(self.browser.driver) + + keyboard.press("CONTROL+a") + + elem = self.browser.find_by_css("#keypress_detect_a") + + assert elem.first diff --git a/tests/skip_if.py b/tests/skip_if.py new file mode 100644 index 000000000..3875515bf --- /dev/null +++ b/tests/skip_if.py @@ -0,0 +1,31 @@ +import pytest + + +def skip_if_zope(f): + def wrapper(self, *args, **kwargs): + if self.__class__.__name__ == "TestZopeTestBrowserDriver": + return pytest.skip("skipping this test for zope testbrowser") + else: + f(self, *args, **kwargs) + + return wrapper + + +def skip_if_django(f): + def wrapper(self, *args, **kwargs): + if self.__class__.__name__ == "TestDjangoClientDriver": + return pytest.skip("skipping this test for django") + else: + f(self, *args, **kwargs) + + return wrapper + + +def skip_if_flask(f): + def wrapper(self, *args, **kwargs): + if self.__class__.__name__ == "TestFlaskClientDriver": + return pytest.skip("skipping this test for flask") + else: + f(self, *args, **kwargs) + + return wrapper diff --git a/tests/static/index.html b/tests/static/index.html index a8265e73e..9e547c1a1 100644 --- a/tests/static/index.html +++ b/tests/static/index.html @@ -32,6 +32,22 @@ addShadowRoot(); }, false); + document.onkeydown = function (e) { + if (e.key === "Control") { + $('body').append('Added when "Control" key is pressed down.'); + } + if (e.key === "a") { + $('body').append('Added when "a" key is pressed down.'); + } + }; + + document.onkeyup = function (e) { + e = e || window.event; + if (e.key === "Control") { + $('body').append('Added when "Control" key is released.'); + } + }; + $(document).ready(function() { $(".draggable").draggable(); $(".droppable").droppable({ diff --git a/tests/test_webdriver_chrome.py b/tests/test_webdriver_chrome.py index 07426abc7..35e0bada6 100644 --- a/tests/test_webdriver_chrome.py +++ b/tests/test_webdriver_chrome.py @@ -5,10 +5,11 @@ from .base import get_browser from .base import WebDriverTests +from .keyboard import KeyboardTest from .fake_webapp import EXAMPLE_APP -class TestChromeBrowser(WebDriverTests): +class TestChromeBrowser(WebDriverTests, KeyboardTest): @pytest.fixture(autouse=True, scope="class") def setup_browser(self, request): request.cls.browser = get_browser("chrome", fullscreen=False) @@ -20,7 +21,7 @@ def visit_example_app(self, request): self.browser.visit(EXAMPLE_APP) -class TestChromeBrowserFullscreen(WebDriverTests): +class TestChromeBrowserFullscreen(WebDriverTests, KeyboardTest): @pytest.fixture(autouse=True, scope="class") def setup_browser(self, request): request.cls.browser = get_browser("chrome", fullscreen=True) diff --git a/tests/test_webdriver_edge_chromium.py b/tests/test_webdriver_edge_chromium.py index b88b6d004..d86994712 100644 --- a/tests/test_webdriver_edge_chromium.py +++ b/tests/test_webdriver_edge_chromium.py @@ -5,10 +5,11 @@ from .base import get_browser from .base import WebDriverTests +from .keyboard import KeyboardTest from .fake_webapp import EXAMPLE_APP -class TestEdgeChromiumBrowser(WebDriverTests): +class TestEdgeChromiumBrowser(WebDriverTests, KeyboardTest): @pytest.fixture(autouse=True, scope="class") def setup_browser(self, request): request.cls.browser = get_browser("edge", fullscreen=False) @@ -20,7 +21,7 @@ def visit_example_app(self, request): self.browser.visit(EXAMPLE_APP) -class TestEdgeChromiumBrowserFullscreen(WebDriverTests): +class TestEdgeChromiumBrowserFullscreen(WebDriverTests, KeyboardTest): @pytest.fixture(autouse=True, scope="class") def setup_browser(self, request): request.cls.browser = get_browser("edge", fullscreen=True) diff --git a/tests/test_webdriver_firefox.py b/tests/test_webdriver_firefox.py index 6debe3828..8de7eb419 100644 --- a/tests/test_webdriver_firefox.py +++ b/tests/test_webdriver_firefox.py @@ -7,11 +7,12 @@ from .base import get_browser from .base import WebDriverTests +from .keyboard import KeyboardTest from .fake_webapp import EXAMPLE_APP from splinter.config import Config -class TestFirefoxBrowser(WebDriverTests): +class TestFirefoxBrowser(WebDriverTests, KeyboardTest): @pytest.fixture(autouse=True, scope="class") def setup_browser(self, request): request.cls.browser = get_browser("firefox", fullscreen=False) @@ -22,7 +23,7 @@ def visit_example_app(self, request): self.browser.visit(EXAMPLE_APP) -class TestFirefoxBrowserFullScreen(WebDriverTests): +class TestFirefoxBrowserFullScreen(WebDriverTests, KeyboardTest): @pytest.fixture(autouse=True, scope="class") def setup_browser(self, request): request.cls.browser = get_browser("firefox", fullscreen=True) diff --git a/tests/test_webdriver_remote.py b/tests/test_webdriver_remote.py index 4987349aa..0333b4f18 100644 --- a/tests/test_webdriver_remote.py +++ b/tests/test_webdriver_remote.py @@ -7,6 +7,7 @@ import pytest from .base import WebDriverTests +from .keyboard import KeyboardTest from .fake_webapp import EXAMPLE_APP from splinter import Browser @@ -21,7 +22,7 @@ def selenium_server_is_running(): return "WebDriver Hub" in page_contents -class TestRemoteBrowserFirefox(WebDriverTests): +class TestRemoteBrowserFirefox(WebDriverTests, KeyboardTest): @pytest.fixture(autouse=True, scope="class") def setup_browser(self, request): request.cls.browser = Browser("remote", browser="firefox") @@ -41,7 +42,7 @@ def test_should_be_able_to_change_user_agent(self): pass -class TestRemoteBrowserChrome(WebDriverTests): +class TestRemoteBrowserChrome(WebDriverTests, KeyboardTest): @pytest.fixture(autouse=True, scope="class") def setup_browser(self, request): request.cls.browser = Browser("remote", browser="chrome") @@ -62,7 +63,7 @@ def test_should_be_able_to_change_user_agent(self): @pytest.mark.macos -class TestRemoteBrowserSafari(WebDriverTests): +class TestRemoteBrowserSafari(WebDriverTests, KeyboardTest): @pytest.fixture(autouse=True, scope="class") def setup_browser(self, request): # test with statement. It can't be used as simple test