Skip to content

Commit

Permalink
Add keyboard class and Element.press()
Browse files Browse the repository at this point in the history
  • Loading branch information
jsfehler committed Jun 12, 2024
1 parent c514c4a commit 38dbd33
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 29 deletions.
17 changes: 17 additions & 0 deletions splinter/driver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``<option>`` element in the element using the ``value`` of the ``<option>``.
Expand Down
6 changes: 6 additions & 0 deletions splinter/driver/webdriver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
69 changes: 69 additions & 0 deletions splinter/driver/webdriver/keyboard.py
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions tests/element.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -29,3 +30,34 @@ def test_element_html(self):
assert (
self.browser.find_by_id("html-property").html == 'inner <div class="inner-html">inner text</div> 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"
21 changes: 1 addition & 20 deletions tests/form_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
40 changes: 40 additions & 0 deletions tests/keyboard.py
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions tests/skip_if.py
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions tests/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@
addShadowRoot();
}, false);

document.onkeydown = function (e) {
if (e.key === "Control") {
$('body').append('<div id="keypress_detect">Added when "Control" key is pressed down.</div>');
}
if (e.key === "a") {
$('body').append('<div id="keypress_detect_a">Added when "a" key is pressed down.</div>');
}
};

document.onkeyup = function (e) {
e = e || window.event;
if (e.key === "Control") {
$('body').append('<div id="keyup_detect">Added when "Control" key is released.</div>');
}
};

$(document).ready(function() {
$(".draggable").draggable();
$(".droppable").droppable({
Expand Down
5 changes: 3 additions & 2 deletions tests/test_webdriver_chrome.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions tests/test_webdriver_edge_chromium.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions tests/test_webdriver_firefox.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions tests/test_webdriver_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pytest

from .base import WebDriverTests
from .keyboard import KeyboardTest
from .fake_webapp import EXAMPLE_APP
from splinter import Browser

Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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
Expand Down

0 comments on commit 38dbd33

Please sign in to comment.