#
Module overview#
This module contains a set of advanced commands that can be used in addition
to the standard Selene commands. Given a Selene entity,
i.e. an object of type Browser | Collection | Element
,
a standard Selene command is any method of the entity (among entity.*
methods)
that performs an action on entity and returns the entity itself.
Then an advanced command is the one defined outside the entity class
and given named as advanced_command
then can be executed on entity
via entity.perform(advanced_command)
.
The idiomatic way to use advanced commands is to import the whole module:
from selene import browser, command # ❗️over from ...command import drag_and_drop_to
slider = browser.element('#ContinuousSlider+*')
slider_thumb = slider.element('.MuiSlider-thumb')
slider_volume_up = slider.element('[data-testid=VolumeUpIcon]')
slider_thumb_input = slider_thumb.element('input')
# GIVEN
browser.open('https://mui.com/material-ui/react-slider/#ContinuousSlider')
# WHEN
slider_thumb.perform(command.drag_and_drop_to(slider_volume_up)) # ⬅️ used via module
# THEN
slider_thumb_input.should(have.value('100'))
Thus you don't need to remember all available advanced commands,
you just import the module and select the one you need
from the list of suggestions among command.*
.
Why do we need a separate module for advanced commands?#
advanced commands are defined outside of the entity class, because they can hardly be implemented in most versatile way. Some of them will work only in a specific web application context, others will not work on a mobile device, etc. Thus, by separating advanced commands from the standard ones, we emphasize for the end user of Selene – the importance of more conscious use of them.
Tip
Yet you can always extend Selene entities with your own commands built in.
The list of advanced commands in this module is far from exhaustive, and there is no goal to make it complete, because in many cases, the end user will need his own list of custom commands specific to his application context. But this list can be a good starting point for such custom commands. Taking the latter into account we try to keep implementation of the commands in this module – as simple as possible, so that the end user can easily understand them and use as examples to implement own custom commands. That's why we avoid following DRY principle here, and prefer pure selenium code over reusing already implemented in Selene helpers.
How to implement custom advanced commands?#
In case you need your own set of custom commands for Selene,
we recommend the following pattern.
Given your project named as my_tests_project
, in the root package
of your project, at proper place, create your own module command.py
:
Example: custom command without parameters#
# Full path can be: my_tests_project/extensions/selene/command.py
# Next import is an important part of the “pattern”
# It will allow to reuse all existing advanced Selene commands.
# Thus you are extending Selene commands, without doubling efforts in usage.
from selene.core.command import *
# Some imports below are not mandatory,
# because are already among `*` from the import above,
# but we still mention them below for self-documentation purposes.
# To customize commands representation in logs
# by wrapping them into Command object:
from selene.core.wait import Command
# For type hints:
from selene import Element, Browser, Collection
# Usually you build your custom commands on top of pure Selenium's ActionChains
from selenium.webdriver import ActionChains, Keys
# To define current platform:
import sys
# Here goes an actual custom command implementation...
# We prefix command with underscore by marking it as "not for actual use",
# because we want to build another version of this command,
# with a more representative name (more on that later...)
def _select_all_and_copy(
# by providing two entity types in type hints...
entity: Element | Browser,
# – we self-document the fact
# that command will work on both Element and Browser
):
'''Selects all text under the focus if called on browser
or all text in the element if called on element,
then copies it to the clipboard.
For both selecting and copying uses OS-based keys combination.
If had been failed, then is logged to console with it's function name,
i.e. '_select_all_and_copy', for example:
Timed out after 4s, while waiting for:
browser.element(('css selector', '#new-task'))._select_all_and_copy
'''
_COMMAND_KEY = Keys.COMMAND if sys.platform == 'darwin' else Keys.CONTROL
actions = ActionChains(entity.config.driver)
# select all
actions.key_down(_COMMAND_KEY)
if isinstance(entity, Element):
actions.send_keys_to_element(entity.locate(), 'a')
else:
actions.send_keys('a')
actions.key_up(_COMMAND_KEY)
# copy
actions.key_down(_COMMAND_KEY)
if isinstance(entity, Element):
actions.send_keys_to_element(entity.locate(), 'c')
else:
actions.send_keys('c')
actions.key_up(_COMMAND_KEY)
actions.perform()
# Any function on entity that returns void – is already a valid command,
# and can be used as follows:
# >>> from my_tests_project.extensions.selene import command
# >>> browser.element('#new-task').perform(command._select_all_and_copy)
# Then if failed, it will be logged as:
# Timed out after 4s, while waiting for:
# browser.element(('css selector', '#new-task'))._select_all_and_copy
# If we want a more representative name in logs,
# we can wrap such command-as-function into Command object:
select_all_and_copy: Command[Element | Browser] = Command(
'send «select all» and «copy» keys shortcut',
_select_all_and_copy,
)
# Then we can use it as follows:
# >>> from my_tests_project.extensions.selene import command
# >>> browser.element('#new-task').perform(command.select_all_and_copy)
# And if failed, it will be logged as:
# Timed out after 4s, while waiting for:
# browser.element(('css selector', '#new-task')).send «select all»
# and «copy» keys shortcut
Tuning the usage of custom commands module#
Note, that since we used from selene.core.command import *
when defining our custom commands module, we don't need in usage to import both –
original selene module and our custom new one:
from selene import browser, command as original
from my_project_root.extensions.selene import command
browser.open('https://todomvc-emberjs-app.autotest.how/')
browser.element('#new-todo').type('foo').perform(command.select_all_and_copy)
browser.element('#new-todo').perform(original.js.set_value('reset'))
It's completely enough here and everywhere in your project to use only your own module import:
from selene import browser,
from my_project_root.extensions.selene import command
browser.open('https://todomvc-emberjs-app.autotest.how/')
browser.element('#new-todo').type('foo').perform(command.select_all_and_copy)
browser.element('#new-todo').perform(command.js.set_value('reset'))
When applying the “Quick fix” functionality of your IDE of choice
to the command
term in the code yet without import:
from selene import browser,
browser.open('https://todomvc-emberjs-app.autotest.how/')
browser.element('#new-todo').type('foo').perform(command.select_all_and_copy)
browser.element('#new-todo').perform(command.js.set_value('reset'))
You will get both suggestions, and, maybe with not quite handy sorting:
If you find uncomfortable to allways waste
an additional time to “select the second one from the list”,
you can name your module as action.py
,
then you'll probably get the top-sorted suggestion
of action from my_project_root.extensions.selene
import
for the code like:
from selene import browser,
browser.open('https://todomvc-emberjs-app.autotest.how/')
browser.element('#new-todo').type('foo').perform(action.select_all_and_copy)
browser.element('#new-todo').perform(action.js.set_value('reset'))
Yet keeping the already defined naming in Selene – the “command” one – has its own benefits for the purpose of consistency and less amount of terminology. But for you to decide. You can find your own name that better suits your project context.
Example: custom command with parameter#
Sometimes your command needs an additional parameter. Then you have to implement the so called “command builder”, for example:
def press_sequentially(keys: str):
def action(element: Element):
actions = ActionChains(element.config.driver)
for key in keys:
actions.send_keys_to_element(element.locate(), Keys.END + key)
actions.perform()
return Command(f'press sequentially: {keys}', action)
Here the actual command is the action
function
defined inside the definition of the press_sequentially
command builder,
and returned from it wrapped in a more “descriptive” Command
object.
For more examples of how to build your own custom commands see the actual implementation of Selene's advanced commands in this module.
The actual list of commands ↙️#
copy: Command[Element | Browser] = Command('send «copy» keys shortcut as ctrl+c for win/linux or cmd+c for mac', __copy)
module-attribute
#
paste: Command[Element | Browser] = Command('send «paste» keys shortcut as ctrl+v for win/linux or cmd+v for mac', __paste)
module-attribute
#
select_all: Command[Element | Browser] = Command('send «select all» keys shortcut as ctrl+a or cmd+a for mac', __select_all_actions)
module-attribute
#
js
#
A container for JavaScript-based commands.
Examples:
>>> from selene import browser, command
>>> browser.element('#new-todo').perform(command.js.set_value('abc'))
Danger
Don't use them in mobile context! JavaScript doesn't work their.
clear_local_storage: Command[Browser] = Command('clear local storage', lambda browser: browser.driver.execute_script('window.localStorage.clear()'))
class-attribute
instance-attribute
#
clear_session_storage: Command[Browser] = Command('clear local storage', lambda browser: browser.driver.execute_script('window.sessionStorage.clear()'))
class-attribute
instance-attribute
#
click = __ClickWithOffset()
class-attribute
instance-attribute
#
remove: Command[Union[Element, Collection]] = Command('remove', lambda entity: entity.execute_script('element.remove()') if not hasattr(entity, '__iter__') else [element.execute_script('element.remove()') for element in entity])
class-attribute
instance-attribute
#
scroll_into_view: Command[Element] = Command('scroll into view', lambda element: element.execute_script('element.scrollIntoView(true)'))
class-attribute
instance-attribute
#
set_style_display_to_block: Command[Union[Element, Collection]] = Command('set element.style.display="block"', lambda entity: entity.execute_script('element.style.display="block"') if not hasattr(entity, '__iter__') else [element.execute_script('element.style.display="block"') for element in entity])
class-attribute
instance-attribute
#
set_style_display_to_none: Command[Union[Element, Collection]] = Command('set element.style.display="none"', lambda entity: entity.execute_script('element.style.display="none"') if not hasattr(entity, '__iter__') else [element.execute_script('element.style.display="none"') for element in entity])
class-attribute
instance-attribute
#
set_style_visibility_to_hidden: Command[Union[Element, Collection]] = Command('set element.style.visibility="hidden"', lambda entity: entity.execute_script('element.style.visibility="hidden"') if not hasattr(entity, '__iter__') else [element.execute_script('element.style.visibility="hidden"') for element in entity])
class-attribute
instance-attribute
#
set_style_visibility_to_visible: Command[Union[Element, Collection]] = Command('set element.style.visibility="visible"', lambda entity: entity.execute_script('element.style.visibility="visible"') if not hasattr(entity, '__iter__') else [element.execute_script('element.style.visibility="visible"') for element in entity])
class-attribute
instance-attribute
#
drag_and_drop_to(target)
staticmethod
#
Simulates drag and drop via JavaScript.
Warning
May not work everywhere. Among known cases: does not work on Material UI React Continuous Slider where the normal drag and drop works fine.
drop_file(path)
staticmethod
#
Simulates via JavaScript the “drag and drop” of file into self (this element).
The command is useful in cases,
when there is no actual hidden input of type file to send_keys(path)
to.
Parameters:
-
path
(str
) –an absolute path to the file
set_style_property(name, value)
staticmethod
#
set_value(value)
staticmethod
#
type(keys)
staticmethod
#
copy_and_paste(text)
#
Copies text to clipboard programmatically and pastes it to the element by pressing OS-based keys combination.
Requires pyperclip package to be installed.
Does not support mobile context. Not tested with desktop apps.
drag_and_drop_by_offset(x, y)
#
drag_and_drop_to(target, _assert_location_changed=False)
#
Parameters:
-
target
(Element
) –a destination element to drag and drop to
-
_assert_location_changed
(bool
, default:False
) –False by default, but if True, then will assert that element was dragged to the new location, hence forcing a command retry if command was under waiting. This option is marked as experimental (by leading underscore), it may be renamed or removed completely.
long_press(duration=1.0)
#
A mobile “long press” command, also known as “touch and hold”.
Parameters:
-
duration
(float
, default:1.0
) –duration of the hold between press and release in seconds
Warning
Designed for the mobile context only. Not tested for web.
press_sequentially(text)
#
Presses each key (letter) in text sequentially to the element.
The pure webelement.send_keys already does it, but this command simulates more slow human-like typing by applying send_keys to each key of the text passed with additional END key press before each next key to ensure that each next key is typed at the end of the text.
Such weird simulation might help with some rare cases of "slow" text fields, that extensively loads some other content on each key press, for example the content of auto-suggestions, etc.