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 4fda11a
Show file tree
Hide file tree
Showing 14 changed files with 373 additions and 29 deletions.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
cookies
screenshot
javascript
keyboard
selenium-keys
iframes-and-alerts
http-status-code-and-exception
Expand Down
99 changes: 99 additions & 0 deletions docs/keyboard.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
.. Copyright 2024 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.
.. meta::
:description: Keyboard
:keywords: splinter, python, tutorial, documentation, selenium integration, selenium keys, keyboard events

++++++++
Keyboard
++++++++

The browser provides an interface for using the keyboard.

However, input is limited to the page. You cannot control the browser or your
operating system using this.

Down
----

Hold a key down.

.. code-block:: python
from splinter import Browser
browser = Browser()
browser.visit("https://duckduckgo.com/")
browser.keyboard.down("CONTROL")
Up
--

Release a key. If the key is not held down, this will do nothing.

.. code-block:: python
from splinter import Browser
browser = Browser()
browser.visit("https://duckduckgo.com/")
browser.keyboard.down("CONTROL")
browser.keyboard.up("CONTROL")
Press
-----

Hold and then release a key pattern.

.. code-block:: python
from splinter import Browser
browser = Browser()
browser.visit("https://duckduckgo.com/")
browser.keyboard.press("CONTROL")
Key patterns are keys separated by the '+' symbol.
This allows multiple presses to be chained together:

.. code-block:: python
from splinter import Browser
browser = Browser()
browser.visit("https://duckduckgo.com/")
browser.keyboard.press("CONTROL+a")
.. warning::
Although a key pattern such as "SHIFT+awesome" will be accepted,
the press method is designed for single keys. There may be unintended
side effects to using it in place of Element.fill() or Element.type().

Element.press()
~~~~~~~~~~~~~~~

Elements can be pressed directly.

.. code-block:: python
from splinter import Browser
browser = Browser()
browser.visit("https://duckduckgo.com/")
elem = browser.find_by_css("#searchbox_input")
elem.fill("splinter python")
elem.press("ENTER")
results = browser.find_by_xpath("//section[@data-testid='mainline']/ol/li")
# Open in a new tab behind the current one.
results.first.press("CONTROL+ENTER")
14 changes: 14 additions & 0 deletions splinter/driver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,20 @@ def type(self, value: str, slowly: bool = False) -> str: # NOQA: A003
"""
raise NotImplementedError

def press(self, key_pattern: str, delay: int = 0) -> None:
"""Focus the element, hold, and then release the specified key pattern.
Arguments:
key_pattern: Pattern of keys to hold and release.
delay: Time, in seconds, to wait between key down and key up.
Example:
>>> browser.find_by_css('.my_element').press('CONTROL+a')
"""
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_pattern: str, delay: int = 0) -> None:
keyboard = Keyboard(self.driver, self._element)
keyboard.press(key_pattern, delay)

def click(self):
"""Click an element.
Expand Down
122 changes: 122 additions & 0 deletions splinter/driver/webdriver/keyboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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:
"""Representation of a keyboard.
Requires a WebDriver instance to use.
Arguments:
driver: The WebDriver instance to use.
element: Optionally, a WebElement to act on.
"""

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:
"""Given the string <key>, select the correct action for key down.
For modifier keys, use ActionChains.key_down().
For other keys, use ActionChains.send_keys() or ActionChains.send_keys_to_element()
"""
key_value = getattr(Keys, key, None)

if key_value:
chain = action_chain.key_down(key_value, self.element)
elif self.element:
chain = 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:
"""Given the string <key>, select the correct action for key up.
For modifier keys, use ActionChains.key_up().
For other keys, use ActionChains.send_keys() or ActionChains.send_keys_to_element()
"""
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":
"""Hold down on a key.
Arguments:
key: The name of a key to hold.
Example:
>>> b = Browser()
>>> Keyboard(b.driver).down('SHIFT')
"""
chain = ActionChains(self.driver)
chain = self._resolve_key_down_action(chain, key)
chain.perform()
return self

def up(self, key: str) -> "Keyboard":
"""Release a held key.
If <key> is not held down, this method has no effect.
Arguments:
key: The name of a key to release.
Example:
>>> b = Browser()
>>> Keyboard(b.driver).down('SHIFT')
>>> Keyboard(b.driver).up('SHIFT')
"""
chain = ActionChains(self.driver)
chain = self._resolve_key_up_action(chain, key)
chain.perform()
return self

def press(self, key_pattern: str, delay: int = 0) -> "Keyboard":
"""Hold and release a key pattern.
Key patterns are strings of key names separated by '+'.
The following are examples of key patterns:
- 'CONTROL'
- 'CONTROL+a'
- 'CONTROL+a+BACKSPACE+b'
Arguments:
key_pattern: Pattern of keys to hold and release.
delay: Time, in seconds, to wait between the hold and release.
Example:
>>> b = Browser()
>>> Keyboard(b.driver).press('CONTROL+a')
"""
keys_names = key_pattern.split("+")

chain = ActionChains(self.driver)

for item in keys_names:
chain = self._resolve_key_down_action(chain, item)

if delay:
chain = chain.pause(delay)

for item in keys_names:
chain = self._resolve_key_up_action(chain, item)

chain.perform()

return self
30 changes: 30 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,32 @@ 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_modifier(self):
keyboard = Keyboard(self.browser.driver)

keyboard.down("CONTROL")

elem = self.browser.find_by_css("#keypress_detect")

assert elem.first

def test_keyboard_up_modifier(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_modifier(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
Loading

0 comments on commit 4fda11a

Please sign in to comment.