Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add keyboard class and Element.press() #1295

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
154 changes: 154 additions & 0 deletions docs/keyboard.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
.. 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. This triggers
keyboard events inside the current browser window.

.. note:: Input detection is limited to the page. You cannot control the browser
or your operating system directly using the keyboard.

The keyboard interface is generally used to trigger modifier keys.
For text input, using the keyboard is not recommended. Instead, use the
:func:`element.fill() <splinter.driver.ElementAPI.fill>` method.

.. note:: The control modifier key is different across operating systems.
e.g.: macOS uses `COMMAND` and Windows & Linux use `CONTROL`.
For a cross-platform solution, `CTRL` can be used and will be resolved
for you.

Actions
=======

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().

Press Using a Context Manager
-----------------------------

Using the `pressed()` method, a context manager will be invoked.
The specified key will be held down, then released when the block is exited.

.. code-block:: python

from splinter import Browser


browser = Browser()
browser.visit("https://duckduckgo.com/")
with browser.keyboard.pressed("SHIFT"):
browser.find_by_css("[@name='q']").fill('splinter')


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")

Cookbook
========

Copy & Paste
------------

.. code-block:: python

browser.visit(https://duckduckgo.com/)

elem = browser.find_by_css("#searchbox_input").first

elem.fill("Let's copy this value")

browser.keyboard.press("CTRL+a")
browser.keyboard.press("CTRL+c")

assert elem.value == ""

elem.click()

browser.keyboard.press("CTRL+v")

assert elem.value == "Let's copy this value"
14 changes: 14 additions & 0 deletions splinter/driver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,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
155 changes: 155 additions & 0 deletions splinter/driver/webdriver/keyboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import contextlib
import platform
from typing import Iterator, 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_control_key(self) -> str:
"""Get the correct name for the control modifier key based on the current operating system.

macOS: META
Windows/Linux: CONTROL

"""
return "META" if platform.system() == "Darwin" else "CONTROL"

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()
"""
if key == "CTRL":
key = self._resolve_control_key()

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()
"""
if key == "CTRL":
key = self._resolve_control_key()

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

@contextlib.contextmanager
def pressed(self, key_pattern: str) -> Iterator[None]:
"""Hold a key pattern inside a `with` block and releas it upon exit.

Arguments:
key_pattern: Pattern of keys to hold and release.

Example:
>>> b = Browser()
>>> with b.keyboard.pressed('CONTROL'):
>>> ...
"""
self.down(key_pattern)
yield
self.up(key_pattern)
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
Loading
Loading