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 24, 2024
1 parent 57a39bd commit 6b4be0c
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 0 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
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
66 changes: 66 additions & 0 deletions tests/tests_webdriver/test_keyboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import platform

from splinter.driver.webdriver import Keyboard


def test_keyboard_down_modifier(browser, app_url):
browser.visit(app_url)

keyboard = Keyboard(browser.driver)

keyboard.down("CONTROL")

elem = browser.find_by_css("#keypress_detect")
assert elem.first


def test_keyboard_up_modifier(browser, app_url):
browser.visit(app_url)

keyboard = Keyboard(browser.driver)

keyboard.down("CONTROL")
keyboard.up("CONTROL")

elem = browser.find_by_css("#keyup_detect")
assert elem.first


def test_keyboard_press_modifier(browser, app_url):
browser.visit(app_url)

keyboard = Keyboard(browser.driver)

keyboard.press("CONTROL")

elem = browser.find_by_css("#keyup_detect")
assert elem.first


def test_element_press_combo(browser, app_url):
browser.visit(app_url)

keyboard = Keyboard(browser.driver)

keyboard.press("CONTROL+a")

elem = browser.find_by_css("#keypress_detect_a")
assert elem.first


def test_element_copy_paste(browser, app_url):
control_key = "META" if platform.system() == "Darwin" else "CONTROL"

browser.visit(app_url)

elem = browser.find_by_name("q")
elem.fill("Copy this value")
elem.press(f"{control_key}+a")
elem.press(f"{control_key}+c")
elem.clear()

assert elem.first.value == ""

elem.press(f"{control_key}+v")

assert elem.first.value == "Copy this Value"

0 comments on commit 6b4be0c

Please sign in to comment.