Skip to content

Commit 48f4e3d

Browse files
committed
more classic kwds, backport Press Keys from SeleniumLibrary
1 parent fc4c212 commit 48f4e3d

File tree

10 files changed

+255
-29
lines changed

10 files changed

+255
-29
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ See the [acceptance tests][] for examples.
5555

5656
python -m scripts.atest
5757

58+
# Free Software
59+
JupyterLibrary is Free Software under the BSD-3-Clause License. It contains code
60+
from a number of other projects:
61+
62+
- [SeleniumLibrary][] ([APL-2.0][selibrary-license])
63+
- backport of `Press Keys`
64+
- [Jyve][] ([BSD-3-Clause][jyve-license])
65+
- Initial implementations of robot keywords
66+
5867
[acceptance tests]: https://github.com/bollwyvl/robotframework-jupyterlab
5968
[Miniconda3]: https://conda.io/miniconda.html
6069
[binder-badge]: https://mybinder.org/badge_logo.svg
@@ -63,3 +72,9 @@ See the [acceptance tests][] for examples.
6372
[pipeline]: https://dev.azure.com/nickbollweg/nickbollweg/_build/latest?definitionId=2
6473
[docs-badge]: https://readthedocs.org/projects/robotframework-jupyterlibrary/badge/?version=latest
6574
[docs]: https://robotframework-jupyterlibrary.readthedocs.io
75+
76+
[SeleniumLibrary]: https://github.com/robotframework/SeleniumLibrary
77+
[selibrary-license]: https://github.com/robotframework/SeleniumLibrary/blob/master/LICENSE.txt
78+
79+
[Jyve]: https://github.com/deathbeds/jyve
80+
[jyve-license]: https://github.com/deathbeds/jyve/blob/master/LICENSE

atest/acceptance/classic/10_notebook.robot

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Library Process
66

77
*** Test Cases ***
88
IPython Notebook
9-
Open Jupyter Notebook Classic
10-
Launch a new Jupyter Notebook Classic Notebook
9+
Open Notebook Classic
10+
Launch a new Notebook Classic Notebook
11+
Add and Run Notebook Classic Code Cell
12+
Wait Until Notebook Classic Kernel Is Idle
1113
Capture Page Screenshot ipython.png

src/JupyterLibrary/core.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from robot.libraries.BuiltIn import BuiltIn
55
from SeleniumLibrary import SeleniumLibrary
6+
from SeleniumLibrary.keywords.element import ElementKeywords
67
from SeleniumLibrary.utils.librarylistener import LibraryListener
78

89
from .keywords import screenshots, server
@@ -11,6 +12,13 @@
1112
RESOURCES = join(dirname(__file__), "resources")
1213
CLIENTS = ["JupyterLab", "NotebookClassic"]
1314

15+
component_classes = [server.ServerKeywords, screenshots.ScreenshotKeywords]
16+
17+
if not hasattr(ElementKeywords, "press_keys"):
18+
from .keywords import keys
19+
20+
component_classes += [keys.KeysKeywords]
21+
1422

1523
class JupyterLibrary(SeleniumLibrary):
1624
"""JupyterLibrary is a Jupyter testing library for Robot Framework."""
@@ -40,7 +48,7 @@ def __init__(
4048
screenshot_root_directory=None,
4149
)
4250
self.add_library_components(
43-
[server.ServerKeywords(self), screenshots.ScreenshotKeywords(self)]
51+
[Component(self) for Component in component_classes]
4452
)
4553
self.ROBOT_LIBRARY_LISTENER = JupyterLibraryListener()
4654

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
""" Backport of SeleniumLibrary >3.2.0's `Press Keys`
2+
"""
3+
from robot.utils import plural_or_not
4+
5+
# flake8: noqa
6+
from selenium.webdriver.common.action_chains import ActionChains
7+
from selenium.webdriver.common.keys import Keys
8+
from SeleniumLibrary.base import LibraryComponent, keyword
9+
from SeleniumLibrary.utils import is_truthy
10+
11+
12+
class KeysKeywords(LibraryComponent):
13+
@keyword
14+
def press_keys(self, locator=None, *keys):
15+
"""Simulates user pressing key(s) to an element or on the active browser.
16+
If ``locator`` evaluates as false, see `Boolean arguments` for more
17+
details, then the ``keys`` are sent to the currently active browser.
18+
Otherwise element is searched and ``keys`` are send to the element
19+
identified by the ``locator``. In later case, keyword fails if element
20+
is not found. See the `Locating elements` section for details about
21+
the locator syntax.
22+
``keys`` arguments can contain one or many strings, but it can not
23+
be empty. ``keys`` can also be a combination of
24+
[https://seleniumhq.github.io/selenium/docs/api/py/webdriver/selenium.webdriver.common.keys.html|Selenium Keys]
25+
and strings or a single Selenium Key. If Selenium Key is combined
26+
with strings, Selenium key and strings must be separated by the
27+
`+` character, like in `CONTROL+c`. Selenium Keys
28+
are space and case sensitive and Selenium Keys are not parsed
29+
inside of the string. Example AALTO, would send string `AALTO`
30+
and `ALT` not parsed inside of the string. But `A+ALT+O` would
31+
found Selenium ALT key from the ``keys`` argument. It also possible
32+
to press many Selenium Keys down at the same time, example
33+
'ALT+ARROW_DOWN`.
34+
If Selenium Keys are detected in the ``keys`` argument, keyword
35+
will press the Selenium Key down, send the strings and
36+
then release the Selenium Key. If keyword needs to send a Selenium
37+
Key as a string, then each character must be separated with
38+
`+` character, example `E+N+D`.
39+
`CTRL` is alias for
40+
[https://seleniumhq.github.io/selenium/docs/api/py/webdriver/selenium.webdriver.common.keys.html#selenium.webdriver.common.keys.Keys.CONTROL|Selenium CONTROL]
41+
and ESC is alias for
42+
[https://seleniumhq.github.io/selenium/docs/api/py/webdriver/selenium.webdriver.common.keys.html#selenium.webdriver.common.keys.Keys.ESCAPE|Selenium ESCAPE]
43+
New in SeleniumLibrary 3.3
44+
Examples:
45+
| `Press Keys` | text_field | AAAAA | | # Sends string "AAAAA" to element. |
46+
| `Press Keys` | None | BBBBB | | # Sends string "BBBBB" to currently active browser. |
47+
| `Press Keys` | text_field | E+N+D | | # Sends string "END" to element. |
48+
| `Press Keys` | text_field | XXX | YY | # Sends strings "XXX" and "YY" to element. |
49+
| `Press Keys` | text_field | XXX+YY | | # Same as above. |
50+
| `Press Keys` | text_field | ALT+ARROW_DOWN | | # Pressing "ALT" key down, then pressing ARROW_DOWN and then releasing both keys. |
51+
| `Press Keys` | text_field | ALT | ARROW_DOWN | # Pressing "ALT" key and then pressing ARROW_DOWN. |
52+
| `Press Keys` | text_field | CTRL+c | | # Pressing CTRL key down, sends string "c" and then releases CTRL key. |
53+
| `Press Keys` | button | RETURN | | # Pressing "ENTER" key to element. |
54+
"""
55+
parsed_keys = self._parse_keys(*keys)
56+
if is_truthy(locator):
57+
self.info("Sending key(s) %s to %s element." % (keys, locator))
58+
else:
59+
self.info("Sending key(s) %s to page." % str(keys))
60+
self._press_keys(locator, parsed_keys)
61+
62+
def _map_ascii_key_code_to_key(self, key_code):
63+
map = {
64+
0: Keys.NULL,
65+
8: Keys.BACK_SPACE,
66+
9: Keys.TAB,
67+
10: Keys.RETURN,
68+
13: Keys.ENTER,
69+
24: Keys.CANCEL,
70+
27: Keys.ESCAPE,
71+
32: Keys.SPACE,
72+
42: Keys.MULTIPLY,
73+
43: Keys.ADD,
74+
44: Keys.SEPARATOR,
75+
45: Keys.SUBTRACT,
76+
56: Keys.DECIMAL,
77+
57: Keys.DIVIDE,
78+
59: Keys.SEMICOLON,
79+
61: Keys.EQUALS,
80+
127: Keys.DELETE,
81+
}
82+
key = map.get(key_code)
83+
if key is None:
84+
key = chr(key_code)
85+
return key
86+
87+
def _map_named_key_code_to_special_key(self, key_name):
88+
try:
89+
return getattr(Keys, key_name)
90+
except AttributeError:
91+
message = "Unknown key named '%s'." % (key_name)
92+
self.debug(message)
93+
raise ValueError(message)
94+
95+
def _press_keys(self, locator, parsed_keys):
96+
if is_truthy(locator):
97+
element = self.find_element(locator)
98+
else:
99+
element = None
100+
for parsed_key in parsed_keys:
101+
actions = ActionChains(self.driver)
102+
special_keys = []
103+
for key in parsed_key:
104+
if self._selenium_keys_has_attr(key.original):
105+
special_keys = self._press_keys_special_keys(
106+
actions, element, parsed_key, key, special_keys
107+
)
108+
else:
109+
self._press_keys_normal_keys(actions, element, key)
110+
for special_key in special_keys:
111+
self.info("Releasing special key %s." % special_key.original)
112+
actions.key_up(special_key.converted)
113+
actions.perform()
114+
115+
def _press_keys_normal_keys(self, actions, element, key):
116+
self.info("Sending key%s %s" % (plural_or_not(key.converted), key.converted))
117+
if element:
118+
actions.send_keys_to_element(element, key.converted)
119+
else:
120+
actions.send_keys(key.converted)
121+
122+
def _press_keys_special_keys(self, actions, element, parsed_key, key, special_keys):
123+
if len(parsed_key) == 1 and element:
124+
self.info("Pressing special key %s to element." % key.original)
125+
actions.send_keys_to_element(element, key.converted)
126+
elif len(parsed_key) == 1 and not element:
127+
self.info("Pressing special key %s to browser." % key.original)
128+
actions.send_keys(key.converted)
129+
else:
130+
self.info("Pressing special key %s down." % key.original)
131+
actions.key_down(key.converted)
132+
special_keys.append(key)
133+
return special_keys
134+
135+
def _parse_keys(self, *keys):
136+
if not keys:
137+
raise AssertionError('"keys" argument can not be empty.')
138+
list_keys = []
139+
for key in keys:
140+
separate_keys = self._separate_key(key)
141+
separate_keys = self._convert_special_keys(separate_keys)
142+
list_keys.append(separate_keys)
143+
return list_keys
144+
145+
def _parse_aliases(self, key):
146+
if key == "CTRL":
147+
return "CONTROL"
148+
if key == "ESC":
149+
return "ESCAPE"
150+
return key
151+
152+
def _separate_key(self, key):
153+
one_key = ""
154+
list_keys = []
155+
for char in key:
156+
if char == "+" and one_key != "":
157+
list_keys.append(one_key)
158+
one_key = ""
159+
else:
160+
one_key += char
161+
if one_key:
162+
list_keys.append(one_key)
163+
return list_keys
164+
165+
def _convert_special_keys(self, keys):
166+
KeysRecord = namedtuple("KeysRecord", "converted, original")
167+
converted_keys = []
168+
for key in keys:
169+
key = self._parse_aliases(key)
170+
if self._selenium_keys_has_attr(key):
171+
converted_keys.append(KeysRecord(getattr(Keys, key), key))
172+
else:
173+
converted_keys.append(KeysRecord(key, key))
174+
return converted_keys
175+
176+
def _selenium_keys_has_attr(self, key):
177+
try:
178+
return hasattr(Keys, key)
179+
except UnicodeError: # To support Python 2 and non ascii characters.
180+
return False

src/JupyterLibrary/keywords/screenshots.py

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
11
from robot.libraries.BuiltIn import BuiltIn
22
from SeleniumLibrary.base import LibraryComponent, keyword
33

4-
5-
try:
6-
import cv2
7-
except ImportError:
8-
cv2 = None
9-
10-
try:
11-
from PIL import Image
12-
except ImportError:
13-
Image = None
4+
from PIL import Image
145

156

167
class ScreenshotKeywords(LibraryComponent):
@@ -36,19 +27,6 @@ def normalize_bounding_box(self, bbox):
3627

3728
@keyword
3829
def crop_image(self, in_file, x, y, width, height, out_file=None):
39-
if cv2:
40-
return self.crop_with_opencv(in_file, x, y, width, height, out_file)
41-
elif Image:
42-
return self.crop_with_pillow(in_file, x, y, width, height, out_file)
43-
44-
def crop_with_opencv(self, in_file, x, y, width, height, out_file=None):
45-
out_file = out_file or in_file
46-
im = cv2.imread(in_file)
47-
im = im[int(y) : int(y + height), int(x) : int(x + width)]
48-
cv2.imwrite(out_file, im)
49-
return out_file
50-
51-
def crop_with_pillow(self, in_file, x, y, width, height, out_file=None):
5230
out_file = out_file or in_file
5331
img = Image.open(in_file)
5432
area = img.crop((int(x), int(y), int(x + width), int(y + height)))

src/JupyterLibrary/resources/JupyterLab/Notebook.robot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Resource JupyterLibrary/resources/JupyterLab/Selectors.robot
33

44
*** Keywords ***
55
Add and Run JupyterLab Code Cell
6-
[Arguments] ${code}
6+
[Arguments] ${code}=print("hello world")
77
[Documentation] Add a ``code`` cell to the currently active notebook and run it.
88
Click Element css:${JLAB CSS NB TOOLBAR} ${JLAB CSS ICON ADD}
99
Sleep 0.1s
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
*** Settings ***
2+
Resource JupyterLibrary/resources/NotebookClassic/Selectors.robot
3+
4+
5+
*** Keywords ***
6+
Execute Notebook Classic Command
7+
[Arguments] ${command} ${accept}=${True} ${close}=${True}
8+
[Documentation] Use the Notebook Classic Command Pop-up
9+
... to run a command and ``accept`` any resulting dialogs, then ``close``
10+
... the Command Palette.
11+
Press Keys None CTRL+SHIFT+p
12+
Input Text css:${JNC CSS CMD INPUT} ${command}
13+
Click Element css:${JNC CSS CMD ITEM}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
*** Settings ***
2+
Resource JupyterLibrary/resources/NotebookClassic/Selectors.robot
3+
4+
*** Keywords ***
5+
Add and Run Notebook Classic Code Cell
6+
[Arguments] ${code}=print("hello world")
7+
[Documentation] Add a ``code`` cell to the currently active notebook and run it.
8+
Click Element css:${JNC CSS NB TOOLBAR} ${JNC CSS ICON ADD}
9+
Sleep 0.1s
10+
Click Element css:${JNC CSS CELL}
11+
Execute JavaScript document.querySelector("${JNC CSS CELL}").CodeMirror.setValue(`${code}`)
12+
Click Element css:${JNC CSS NB TOOLBAR} ${JNC CSS ICON RUN}
13+
14+
Wait Until Notebook Classic Kernel Is Idle
15+
[Documentation] Wait for a kernel to be busy, and then stop being busy
16+
Wait Until Page Does Not Contain Element ${JNC CSS NB KERNEL BUSY}
17+
Wait Until Page Does Not Contain ${JNC TEXT BUSY PROMPT}

src/JupyterLibrary/resources/NotebookClassic/Selectors.robot

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,16 @@ ${JNC CSS TREE NEW BUTTON} \#new-dropdown-button
44
${JNC CSS TREE NEW MENU} \#new-menu
55
${JNC CSS NB KERNEL ICON} \#kernel_indicator_icon
66
${JNC CSS NB KERNEL IDLE} .kernel_idle_icon
7+
${JNC CSS NB KERNEL BUSY} .kernel_budy_icon
8+
${JNC TEXT BUSY PROMPT} In [*]:
9+
10+
${JNC CSS CMD PALETTE} .modal.cmd-palette.in
11+
${JNC CSS CMD INPUT} ${JNC CSS CMD PALETTE} input[type="search"]
12+
${JNC CSS CMD ITEM} ${JNC CSS CMD PALETTE} .typeahead-result li > a
13+
14+
${JNC CSS NB TOOLBAR} \#maintoolbar-container
15+
16+
${JNC CSS ICON ADD} .fa-plus
17+
${JNC CSS ICON RUN} .fa-step-forward
18+
19+
${JNC CSS CELL} \#notebook-container .cell:last-of-type .CodeMirror

src/JupyterLibrary/resources/NotebookClassic/Tree.robot

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Documentation Keywords for working with the Jupyter Notebook Clasic web applic
66
77

88
*** Keywords ***
9-
Open Jupyter Notebook Classic
9+
Open Notebook Classic
1010
[Arguments] ${browser}=headlessfirefox ${nbserver}=${None} ${url}=${EMPTY} &{configuration}
1111
[Documentation] Open Jupyter Notebook Classic, served from the given (or most-recently-started)
1212
... ``nbserver`` in a ``browser`` (or ``headlessfirefox``) or ``url``,
@@ -17,7 +17,7 @@ Open Jupyter Notebook Classic
1717
${final_url} = Set Variable If "${url}" ${url} ${nbserver_url}tree?token=${token}
1818
Open Browser url=${final_url} browser=${browser} &{configuration}
1919

20-
Launch a new Jupyter Notebook Classic Notebook
20+
Launch a new Notebook Classic Notebook
2121
[Arguments] ${kernel}=Python 3
2222
[Documentation] Use the Jupyter Notebook Classic tree to launch a
2323
... Notebook with the given ``kernel``

0 commit comments

Comments
 (0)