Skip to content

Commit 4fda11a

Browse files
committed
Add keyboard class and Element.press()
1 parent c514c4a commit 4fda11a

File tree

14 files changed

+373
-29
lines changed

14 files changed

+373
-29
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
cookies
3939
screenshot
4040
javascript
41+
keyboard
4142
selenium-keys
4243
iframes-and-alerts
4344
http-status-code-and-exception

docs/keyboard.rst

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
.. Copyright 2024 splinter authors. All rights reserved.
2+
Use of this source code is governed by a BSD-style
3+
license that can be found in the LICENSE file.
4+
5+
.. meta::
6+
:description: Keyboard
7+
:keywords: splinter, python, tutorial, documentation, selenium integration, selenium keys, keyboard events
8+
9+
++++++++
10+
Keyboard
11+
++++++++
12+
13+
The browser provides an interface for using the keyboard.
14+
15+
However, input is limited to the page. You cannot control the browser or your
16+
operating system using this.
17+
18+
Down
19+
----
20+
21+
Hold a key down.
22+
23+
.. code-block:: python
24+
25+
from splinter import Browser
26+
27+
28+
browser = Browser()
29+
browser.visit("https://duckduckgo.com/")
30+
browser.keyboard.down("CONTROL")
31+
32+
33+
Up
34+
--
35+
36+
Release a key. If the key is not held down, this will do nothing.
37+
38+
.. code-block:: python
39+
40+
from splinter import Browser
41+
42+
43+
browser = Browser()
44+
browser.visit("https://duckduckgo.com/")
45+
browser.keyboard.down("CONTROL")
46+
browser.keyboard.up("CONTROL")
47+
48+
49+
Press
50+
-----
51+
52+
Hold and then release a key pattern.
53+
54+
.. code-block:: python
55+
56+
from splinter import Browser
57+
58+
59+
browser = Browser()
60+
browser.visit("https://duckduckgo.com/")
61+
browser.keyboard.press("CONTROL")
62+
63+
Key patterns are keys separated by the '+' symbol.
64+
This allows multiple presses to be chained together:
65+
66+
.. code-block:: python
67+
68+
from splinter import Browser
69+
70+
71+
browser = Browser()
72+
browser.visit("https://duckduckgo.com/")
73+
browser.keyboard.press("CONTROL+a")
74+
75+
.. warning::
76+
Although a key pattern such as "SHIFT+awesome" will be accepted,
77+
the press method is designed for single keys. There may be unintended
78+
side effects to using it in place of Element.fill() or Element.type().
79+
80+
Element.press()
81+
~~~~~~~~~~~~~~~
82+
83+
Elements can be pressed directly.
84+
85+
.. code-block:: python
86+
87+
from splinter import Browser
88+
89+
90+
browser = Browser()
91+
browser.visit("https://duckduckgo.com/")
92+
elem = browser.find_by_css("#searchbox_input")
93+
elem.fill("splinter python")
94+
elem.press("ENTER")
95+
96+
results = browser.find_by_xpath("//section[@data-testid='mainline']/ol/li")
97+
98+
# Open in a new tab behind the current one.
99+
results.first.press("CONTROL+ENTER")

splinter/driver/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,20 @@ def type(self, value: str, slowly: bool = False) -> str: # NOQA: A003
871871
"""
872872
raise NotImplementedError
873873

874+
def press(self, key_pattern: str, delay: int = 0) -> None:
875+
"""Focus the element, hold, and then release the specified key pattern.
876+
877+
Arguments:
878+
key_pattern: Pattern of keys to hold and release.
879+
delay: Time, in seconds, to wait between key down and key up.
880+
881+
Example:
882+
883+
>>> browser.find_by_css('.my_element').press('CONTROL+a')
884+
885+
"""
886+
raise NotImplementedError
887+
874888
def select(self, value: str, slowly: bool = False) -> None:
875889
"""
876890
Select an ``<option>`` element in the element using the ``value`` of the ``<option>``.

splinter/driver/webdriver/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from splinter.driver import ElementAPI
2626
from splinter.driver.find_links import FindLinks
2727
from splinter.driver.webdriver.cookie_manager import CookieManager
28+
from splinter.driver.webdriver.keyboard import Keyboard
2829
from splinter.driver.xpath_utils import _concat_xpath_from_str
2930
from splinter.element_list import ElementList
3031
from splinter.exceptions import ElementDoesNotExist
@@ -266,6 +267,7 @@ def __init__(self, driver=None, wait_time=2):
266267
self.wait_time = wait_time
267268

268269
self.links = FindLinks(self)
270+
self.keyboard = Keyboard(driver)
269271

270272
self.driver = driver
271273
self._find_elements = self.driver.find_elements
@@ -792,6 +794,10 @@ def type(self, value, slowly=False): # NOQA: A003
792794
self._element.send_keys(value)
793795
return value
794796

797+
def press(self, key_pattern: str, delay: int = 0) -> None:
798+
keyboard = Keyboard(self.driver, self._element)
799+
keyboard.press(key_pattern, delay)
800+
795801
def click(self):
796802
"""Click an element.
797803
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from typing import Union
2+
3+
from selenium.webdriver.common.action_chains import ActionChains
4+
from selenium.webdriver.common.keys import Keys
5+
from selenium.webdriver.remote.webelement import WebElement
6+
7+
8+
class Keyboard:
9+
"""Representation of a keyboard.
10+
11+
Requires a WebDriver instance to use.
12+
13+
Arguments:
14+
driver: The WebDriver instance to use.
15+
element: Optionally, a WebElement to act on.
16+
"""
17+
18+
def __init__(self, driver, element: Union[WebElement, None] = None) -> None:
19+
self.driver = driver
20+
21+
self.element = element
22+
23+
def _resolve_key_down_action(self, action_chain: ActionChains, key: str) -> ActionChains:
24+
"""Given the string <key>, select the correct action for key down.
25+
26+
For modifier keys, use ActionChains.key_down().
27+
For other keys, use ActionChains.send_keys() or ActionChains.send_keys_to_element()
28+
"""
29+
key_value = getattr(Keys, key, None)
30+
31+
if key_value:
32+
chain = action_chain.key_down(key_value, self.element)
33+
elif self.element:
34+
chain = action_chain.send_keys_to_element(self.element, key)
35+
else:
36+
chain = action_chain.send_keys(key)
37+
38+
return chain
39+
40+
def _resolve_key_up_action(self, action_chain: ActionChains, key: str) -> ActionChains:
41+
"""Given the string <key>, select the correct action for key up.
42+
43+
For modifier keys, use ActionChains.key_up().
44+
For other keys, use ActionChains.send_keys() or ActionChains.send_keys_to_element()
45+
"""
46+
key_value = getattr(Keys, key, None)
47+
48+
chain = action_chain
49+
if key_value:
50+
chain = action_chain.key_up(key_value, self.element)
51+
52+
return chain
53+
54+
def down(self, key: str) -> "Keyboard":
55+
"""Hold down on a key.
56+
57+
Arguments:
58+
key: The name of a key to hold.
59+
60+
Example:
61+
62+
>>> b = Browser()
63+
>>> Keyboard(b.driver).down('SHIFT')
64+
"""
65+
chain = ActionChains(self.driver)
66+
chain = self._resolve_key_down_action(chain, key)
67+
chain.perform()
68+
return self
69+
70+
def up(self, key: str) -> "Keyboard":
71+
"""Release a held key.
72+
73+
If <key> is not held down, this method has no effect.
74+
75+
Arguments:
76+
key: The name of a key to release.
77+
78+
Example:
79+
80+
>>> b = Browser()
81+
>>> Keyboard(b.driver).down('SHIFT')
82+
>>> Keyboard(b.driver).up('SHIFT')
83+
"""
84+
chain = ActionChains(self.driver)
85+
chain = self._resolve_key_up_action(chain, key)
86+
chain.perform()
87+
return self
88+
89+
def press(self, key_pattern: str, delay: int = 0) -> "Keyboard":
90+
"""Hold and release a key pattern.
91+
92+
Key patterns are strings of key names separated by '+'.
93+
The following are examples of key patterns:
94+
- 'CONTROL'
95+
- 'CONTROL+a'
96+
- 'CONTROL+a+BACKSPACE+b'
97+
98+
Arguments:
99+
key_pattern: Pattern of keys to hold and release.
100+
delay: Time, in seconds, to wait between the hold and release.
101+
102+
Example:
103+
104+
>>> b = Browser()
105+
>>> Keyboard(b.driver).press('CONTROL+a')
106+
"""
107+
keys_names = key_pattern.split("+")
108+
109+
chain = ActionChains(self.driver)
110+
111+
for item in keys_names:
112+
chain = self._resolve_key_down_action(chain, item)
113+
114+
if delay:
115+
chain = chain.pause(delay)
116+
117+
for item in keys_names:
118+
chain = self._resolve_key_up_action(chain, item)
119+
120+
chain.perform()
121+
122+
return self

tests/element.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright 2012 splinter authors. All rights reserved.
22
# Use of this source code is governed by a BSD-style
33
# license that can be found in the LICENSE file.
4+
from .skip_if import skip_if_zope, skip_if_django, skip_if_flask
45

56

67
class ElementTest:
@@ -29,3 +30,32 @@ def test_element_html(self):
2930
assert (
3031
self.browser.find_by_id("html-property").html == 'inner <div class="inner-html">inner text</div> html test'
3132
)
33+
34+
@skip_if_zope
35+
@skip_if_django
36+
@skip_if_flask
37+
def test_element_press_modifier(self):
38+
elem = self.browser.find_by_css("[name='q']")
39+
elem.fill("hellox")
40+
elem.press("BACKSPACE")
41+
42+
assert elem.value == "hello"
43+
44+
@skip_if_zope
45+
@skip_if_django
46+
@skip_if_flask
47+
def test_element_press_key(self):
48+
elem = self.browser.find_by_css("[name='q']")
49+
elem.fill("hellox")
50+
elem.press("a")
51+
52+
assert elem.value == "helloxa"
53+
54+
@skip_if_zope
55+
@skip_if_django
56+
@skip_if_flask
57+
def test_element_press_combo(self):
58+
elem = self.browser.find_by_css("[name='q']")
59+
elem.press("SHIFT+a+BACKSPACE+b")
60+
61+
assert elem.value == "B"

tests/form_elements.py

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,7 @@
77
import pytest
88

99
from splinter.exceptions import ElementDoesNotExist
10-
11-
12-
def skip_if_zope(f):
13-
def wrapper(self, *args, **kwargs):
14-
if self.__class__.__name__ == "TestZopeTestBrowserDriver":
15-
return pytest.skip("skipping this test for zope testbrowser")
16-
else:
17-
f(self, *args, **kwargs)
18-
19-
return wrapper
20-
21-
22-
def skip_if_django(f):
23-
def wrapper(self, *args, **kwargs):
24-
if self.__class__.__name__ == "TestDjangoClientDriver":
25-
return pytest.skip("skipping this test for django")
26-
else:
27-
f(self, *args, **kwargs)
28-
29-
return wrapper
10+
from .skip_if import skip_if_zope, skip_if_django
3011

3112

3213
class FormElementsTest:

tests/keyboard.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from splinter.driver.webdriver import Keyboard
2+
3+
4+
class KeyboardTest:
5+
def test_keyboard_down_modifier(self):
6+
keyboard = Keyboard(self.browser.driver)
7+
8+
keyboard.down("CONTROL")
9+
10+
elem = self.browser.find_by_css("#keypress_detect")
11+
12+
assert elem.first
13+
14+
def test_keyboard_up_modifier(self):
15+
keyboard = Keyboard(self.browser.driver)
16+
17+
keyboard.down("CONTROL")
18+
keyboard.up("CONTROL")
19+
20+
elem = self.browser.find_by_css("#keyup_detect")
21+
22+
assert elem.first
23+
24+
def test_keyboard_press_modifier(self):
25+
keyboard = Keyboard(self.browser.driver)
26+
27+
keyboard.press("CONTROL")
28+
29+
elem = self.browser.find_by_css("#keyup_detect")
30+
31+
assert elem.first
32+
33+
def test_element_press_combo(self):
34+
keyboard = Keyboard(self.browser.driver)
35+
36+
keyboard.press("CONTROL+a")
37+
38+
elem = self.browser.find_by_css("#keypress_detect_a")
39+
40+
assert elem.first

0 commit comments

Comments
 (0)