#
Config
#
A one cross-cutting-concern-like object to group all options
that might influence Selene behavior depending on context.
For example, config.timeout
is used in all "waiting" logic
of Selene commands. And config.base_url
is used
in browser.open(relative_url)
command.
As option, the driver instance is also considered. Moreover, this config is not just config, but fully manages the driver lifecycle. Actually, the "driver manager" is a part of this config.
While surfing through all available options, pay attention to terminology:
- all options that have a
driver
word in their name are related to driver management, and they are connected in a specific way:)- read more on this under
config.with_
doc section;)
- read more on this under
- all options that have a
strategy
word in their name directly influence the driver lifecycle in context of driver management. -
all options that are prefixed with
_
are considered "experimental" (their naming can be changed in the future, or even an option can be removed)
Examples:
Here's how you can build a driver with the instance of this config:
>>> from selene import Config
>>> config = Config()
>>> driver = config.driver # new instance, built on 1st access to `driver`
>>> assert driver.name == 'chrome'
Or pre-configuring the firefox driver:
>>> from selene import Config
>>> config = Config(driver_name='firefox')
>>> driver = config.driver
>>> assert driver.name == 'firefox'
Or post-configuring the firefox driver:
>>> from selene import Config
>>> config = Config()
>>> config.driver_name = 'firefox'
>>> driver = config.driver
>>> assert driver.name == 'firefox'
Selene has already predefined shared instance of Config, so you can economize on lines of code;)...
>>> from selene.support.shared import config
>>> config.driver_name = 'firefox'
>>> driver = config.driver
>>> assert driver.name == 'firefox'
Same shared Config instance is available as browser.config:
>>> from selene import browser
>>> browser.config.driver_name = 'firefox'
>>> driver = browser.config.driver
>>> assert driver.name == 'firefox'
There is an alternative style of customizing config.
The config.option_name = value
is known in programming
as "imperative programming" style. When you are creating
a new Config from scratch, you are actually using
a "declarative programming" style:
>>> from selene import Config
>>> my_config = Config(driver_name='firefox')
>>> driver = my_config.driver
>>> assert driver.name == 'firefox'
Here is an alternative declarative style of customizing new config by copying existing:
>>> from selene import browser
>>> my_config = browser.config.with_(driver_name='firefox')
>>> driver = my_config.driver
>>> assert driver.name == 'firefox'
>>> # AND...
>>> assert driver is not browser.config.driver # ;)
>>> assert browser.config.driver.name == 'chrome'
As you can see Selene config is closely related to the browser. Moreover, the same type of "declarative config copying" happens implicitly, when you apply "copying" to browser:
>>> from selene import browser
>>> second_browser = browser.with_(driver_name='firefox')
>>> assert second_browser.config.driver.name == 'firefox'
>>> # AND...
>>> assert second_browser.config.driver is not browser.config.driver # ;)
>>> assert browser.config.driver.name == 'chrome'
Moreover, if you need only a driver, you can have it
via browser.driver
shortcut, thus, completely hiding the config:
>>> from selene import browser
>>> second_browser = browser.with_(driver_name='firefox')
>>> assert second_browser.driver.name == 'firefox'
>>> # AND...
>>> assert second_browser.driver is not browser.driver # ;)
>>> assert browser.driver.name == 'chrome'
- such shortcut exists only for the
driver
option of config, not for other options liketimeout
orbase_url
. More nuances ofbrowser
behavior find in its docs;).
And here are some more examples of customizing config for common test automation use cases...
Scenario: "Run locally in headless Chrome"
>>> from selene import browser
>>> from selenium import webdriver
>>>
>>> options = webdriver.ChromeOptions()
>>> options.add_argument('--headless')
>>> # additional options:
>>> options.add_argument('--no-sandbox')
>>> options.add_argument('--disable-gpu')
>>> options.add_argument('--disable-notifications')
>>> options.add_argument('--disable-extensions')
>>> options.add_argument('--disable-infobars')
>>> options.add_argument('--enable-automation')
>>> options.add_argument('--disable-dev-shm-usage')
>>> options.add_argument('--disable-setuid-sandbox')
>>> browser.config.driver_options = options
Scenario: "Run remotely on Selenoid"
>>> import os
>>> from selene import browser
>>> from selenium import webdriver
>>>
>>> options = webdriver.ChromeOptions()
>>> options.browser_version = '100.0'
>>> options.set_capability(
>>> 'selenoid:options',
>>> {
>>> 'screenResolution': '1920x1080x24',
>>> 'enableVNC': True,
>>> 'enableVideo': True,
>>> 'enableLog': True,
>>> },
>>> )
>>> browser.config.driver_options = options
>>> browser.config.driver_remote_url = (
>>> f'https://{os.getenv("LOGIN")}:{os.getenv("PASSWORD")}@'
>>> f'selenoid.autotests.cloud/wd/hub'
>>> )
Scenario: "Run remotely on BrowserStack in iOS Safari"
>>> import os
>>> from selene import browser
>>> from selenium.webdriver.common.options import ArgOptions
>>>
>>> options = ArgOptions()
>>> options.set_capability(
>>> 'bstack:options',
>>> {
>>> 'deviceName': 'iPhone 14 Pro Max',
>>> # 'browserName': 'safari', # default for iPhone
>>> 'userName': os.getenv('BROWSERSTACK_USERNAME'),
>>> 'accessKey': os.getenv('BROWSERSTACK_ACCESS_KEY'),
>>> },
>>> )
>>> browser.config.driver_options = options
>>> browser.config.driver_remote_url = 'http://hub.browserstack.com/wd/hub'
Scenario: "Run locally in Android Chrome"
>>> from selene import browser
>>> from appium.options.common import AppiumOptions
>>>
>>> mobile_options = AppiumOptions()
>>> mobile_options.new_command_timeout = 60
>>> # Mandatory, also tells Selene to build Appium driver:
>>> mobile_options.platform_name = 'android'
>>> mobile_options.set_capability('browserName', 'chrome')
>>>
>>> browser.config.driver_options = mobile_options
>>> # Not mandatory, because it is the default value:
>>> # browser.config.driver_remote_url = 'http://127.0.0.1:4723/wd/hub'
Scenario: "Run locally in Android App"
>>> from selene import browser
>>> from appium.options.android import UiAutomator2Options
>>>
>>> android_options = UiAutomator2Options()
>>> android_options.new_command_timeout = 60
>>> android_options.app = 'wikipedia-alpha-universal-release.apk'
>>> android_options.app_wait_activity = 'org.wikipedia.*'
>>>
>>> browser.config.driver_options = android_options
Scenario: "Run remotely in Android App on BrowserStack"
>>> import os
>>> from selene import browser
>>> from appium.options.android import UiAutomator2Options
>>>
>>> options = UiAutomator2Options()
>>> options.app = 'bs://c700ce60cf13ae8ed97705a55b8e022f13c5827c'
>>> options.set_capability(
>>> 'bstack:options',
>>> {
>>> 'deviceName': 'Google Pixel 7',
>>> 'userName': os.getenv('BROWSERSTACK_USERNAME'),
>>> 'accessKey': os.getenv('BROWSERSTACK_ACCESS_KEY'),
>>> },
>>> )
>>> browser.config.driver_options = options
>>> browser.config.driver_remote_url = 'http://hub.browserstack.com/wd/hub'
By having config options that influences Selene behavior,
like config.timeout
and config.base_url
,
– together with complete "driver management",
we definitely break SRP principle... In the name of Good:D. Kind of;).
All this makes it far from being a simple options data class...
– yet kept as one "class for everything" to keep things easier to use,
especially taking into account some historical reasons of Selene design,
that was influenced a lot by the Selenide from Java world.
As a result sometimes options are not consistent with each other,
when we speak about different contexts of their usage.
For example, this same config,
once customized with config.driver_options = UiAutomator2Options()
,
will result in mobile driver built, but then all other web-related options,
for example, a config.base_url
will be not relevant.
Some of them will be ignored, while some of them,
for example js-related, like config.set_value_by_js
,
will break the code execution (JavaScript does not work in mobile apps).
In an ideal world, we would have to split this config into several ones,
starting BaseConfig and continuing with WebConfig, MobileConfig, etc.
Yet, we have what we have. This complicates things a bit,
especially for us, contributors of Selene,
but makes easier for newbies in a lot of "harder" cases,
like customizing same shared browser instance for multi-platform test runs,
when we have one test that works for all platforms.
Thus, we allow to do "harder" tasks easier for "less experienced" users.
Again, such "easiness" does not mean "simplicity" for us, contributors,
and also for advanced Selene users,
who want to customize things in a lot of ways
and have them easier to support on a long run.
But for now, let's keep it as is, considered as a trade-off.
build_driver_strategy: Callable[[Config], WebDriver] = _build_local_driver_by_name_or_remote_by_url_and_options
class-attribute
instance-attribute
#
A factory to build a driver instance based on this config instance.
The driver built with this factory will be available via config.driver
.
Hence, you can't use config.driver
directly inside this factory,
because it may lead to recursion.
The default factory builds:
- either a local driver by value specified in
config.driver_name
- or a local driver by browserName capability specified in
config.driver_options
- or remote driver by value specified in
config.driver_remote_url
- or mobile driver according to
config.driver_options
capabilities
driver_options: Optional[BaseOptions] = None
class-attribute
instance-attribute
#
Individual browser options to be used on building a driver.
Examples:
Can be used instead of config.driver_name
to tell Selene
which driver to build, e.g. just specifying
>>> from selene import browser
>>> from selenium import webdriver
>>>
>>> browser.config.driver_options = webdriver.FirefoxOptions()`
– will tell Selene to build a Firefox driver.
But usually you want something more specific, like specifying to run a browser in headless more:
driver_service: Optional[Service] = None
class-attribute
instance-attribute
#
Service instance for managing the starting and stopping of the driver. Might be useful, for example, for customizing driver executable path, if you want to use a custom driver executable instead of the one, downloaded by Selenium Manager automatically.
driver_remote_url: Optional[str] = None
class-attribute
instance-attribute
#
A URL to be used as remote server address to instantiate a RemoteConnection to be used by RemoteWebDriver to connect to the remote server.
Also known as command_executor
,
when passing on init: driver = remote.WebDriver(command_executor=HERE)
.
Currently we name it and type hint as URL,
but if you pass a RemoteConnection object,
it will work same way as in Selenium WebDriver.
driver_name: Optional[str] = None
class-attribute
instance-attribute
#
A desired name of the driver to build by config.build_driver_strategy
.
If not set (i.e. set to None, that is a current default value), the 'chrome' driver will be used by default.
It is ignored by default config.build_driver_strategy
if config.driver_remote_url
is set.
If you are going to provide your desired driver options
via config.driver_options
,
then Selene will try to guess the corresponding driver name
based on the options you provided. I.e. no need to provide both:
It's enough to provide only the options:
GIVEN set to any of: 'chrome', 'firefox', 'edge',
AND config.driver is left unset (default value is ...),
THEN default config.build_driver_strategy will automatically install drivers
AND build webdriver instance for you
AND this config will store the instance in config.driver
hold_driver_at_exit: bool = False
class-attribute
instance-attribute
#
Controls whether driver will be automatically quit at process exit or not.
Will not take much effect on Chrome for 4.5.0 < selenium versions <= 4.8.3 < ?.?.?, Because for some reason, Selenium of such versions kills driver by himself, regardless of what Selene thinks about it:D
rebuild_not_alive_driver: bool = False
class-attribute
instance-attribute
#
Controls whether driver should be automatically rebuilt when on next call to config.driver it was noticed as not alive (e.g. after quit or crash).
May slow down your tests if running against remote Selenium server, e.g. Grid or selenoid, because of additional request to check if driver is alive per each driver action.
Does not work if config.driver
was set manually to Callable[[], WebDriver]
.
Is a more "strong" option than config._reset_not_alive_driver_on_get_url
,
(enabled by default), that schedules rebuilding driver
on next access only inside "get url" logic.
driver: WebDriver = _ManagedDriverDescriptor(default=...)
class-attribute
instance-attribute
#
A driver instance with lifecycle managed by this config special options depending on their values and customization of this attribute.
Once driver-definition-related options are set
(like config.driver_options
, config.driver_remote_url
),
the driver will be built on first access to this attribute.
Thus, to build the driver with Selene,
you simply call config.driver
for the first time,
and usually the simplest way to access it
– is through either browser.config.driver
or even browser.driver
shortcut.
Moreover, usually you don't do this explicitly,
but rather use browser.open(url)
to build a driver and open a page.
Scenarios
GIVEN unset, i.e. equals to default ...
or None
,
WHEN accessed first time (e.g. via config.driver)
THEN it will be set to the instance built by config.build_driver_strategy
.
GIVEN set manually to an existing driver instance,
like: config.driver = Chrome()
THEN it will be reused as it is on any next access
WHEN reset to ...
OR None
THEN will be rebuilt by config.build_driver_strategy
GIVEN set manually to an existing driver instance (not callable),
like: config.driver = Chrome()
AND at some point of time the driver is not alive
like crashed or quit
AND config._reset_not_alive_driver_on_get_url
is set to True
,
that is default
WHEN driver.get(url) is requested under the hood
like at browser.open(url)
THEN config.driver will be reset to ...
AND thus will be rebuilt by config.build_driver_strategy
GIVEN set manually to a callable that returns WebDriver instance
(currently marked with FutureWarning, so might be deprecated)
WHEN accessed fist time
AND any next time
THEN will call the callable and return the result
GIVEN unset or set manually to not callable
AND config.hold_driver_at_exit
is set to False
(that is default)
WHEN the process exits
THEN driver will be quit.
timeout: float = 4
class-attribute
instance-attribute
#
A default timeout for all Selene waiting that happens under the hood of the majority of Selene commands and assertions.
poll_during_waits: int = 100
class-attribute
instance-attribute
#
A fake option, not currently used in Selene waiting:)
base_url: str = ''
class-attribute
instance-attribute
#
A base url to be used when opening a page with relative url.
Examples:
Instead of duplicating the same base url in all your tests:
>>> from selene import browser
>>> browser.open('https://mywebapp.xyz/signin')
>>> ...
>>> browser.open('https://mywebapp.xyz/signup')
>>> ...
>>> browser.open('https://mywebapp.xyz/profile')
>>> ...
You can set it once in your config and then just use relative urls:
window_width: Optional[int] = None
class-attribute
instance-attribute
#
If set, will be used to set the window width on next call to browser.open(url)
.
window_height: Optional[int] = None
class-attribute
instance-attribute
#
If set, will be used to set the window height on next call
to browser.open(url)
.
log_outer_html_on_failure: bool = False
class-attribute
instance-attribute
#
If set to True, will log outer html of the element on failure of any Selene command.
Is disabled by default, because:
- it might add too much of noise to the logs
- will not work on mobile app tests because under the hood - uses JavaScript
set_value_by_js: bool = False
class-attribute
instance-attribute
#
A flag to indicate whether to use JavaScript to set value of an element
on element.set_value(value)
for purposes of speeding up the test execution,
or as a workaround when default selenium-based implementation does not work.
type_by_js: bool = False
class-attribute
instance-attribute
#
A flag to indicate whether to use JavaScript to type text to an element
on element.type(text)
for purposes of speeding up the test execution,
or as a workaround when default selenium-based implementation does not work.
click_by_js: bool = False
class-attribute
instance-attribute
#
A flag to indicate whether to use JavaScript to click on element
via element.click()
, usually, as a workaround,
when default selenium-based implementation does not work.
wait_for_no_overlap_found_by_js: bool = False
class-attribute
instance-attribute
#
A flag to indicate whether to use JavaScript to detect overlapping elements
and wait for them to disappear, when calling commands like element.type(text)
.
It is needed because Selenium does not support overlapping elements detection
on any command except click
. Hence, when you call click
on an element,
and there is some overlay on top of it
(e.g. for the sake of indicating "loading in progress"),
that is going to disappear after some time,
then Selenium will detect such overlap,
that tells Selene to wait for it to disappear.
But for any other command (double_click, context_click, type, etc.)
Selenium will not and so Selene will not wait.
Hence, if you want to wait in such cases, turn on this option.
Just keep in mind, that it will work only for web tests, not mobile.
selector_to_by_strategy: Callable[[str], Tuple[str, str]] = lambda selector: (By.XPATH, selector) if selector.startswith('/') or selector.startswith('./') or selector.startswith('..') or selector.startswith('(') or selector.startswith('*/') else (By.CSS_SELECTOR, selector)
class-attribute
instance-attribute
#
A strategy to convert a selector string to a Selenium By type of selector, that is a 2-dimension tuple of selector type and selector value.
Can be useful to define custom selectors to be used on building common Selene
entities like browser.element(selector)
or browser.all(selector)
.
You can find a simple example of such strategy definition in the default value
of this option. Here goes a smarter example of building a custom strategy on top
of the default one, that will automatically convert a "one word" selector string
to the [data-testid=<WORD>]
css selector:
# tests/conftest.py
import re
import pytest
import selene
from selene.common.helpers import _HTML_TAGS
@pytest.fixture(scope='function', autouse=True)
def browser_management():
selene.browser.config.selector_to_by_strategy = lambda selector: (
# wrap into default strategy
selene.browser.config.selector_to_by_strategy(
# detected testid
f'[data-testid={selector}]'
if re.match(
# word_with_dashes_underscores_or_numbers
r'^[a-zA-Z_\d\-]+$',
selector,
)
and selector not in _HTML_TAGS
else selector
)
)
yield
selene.browser.quit()
hook_wait_failure: Optional[Callable[[TimeoutException], Exception]] = None
class-attribute
instance-attribute
#
A handler for all exceptions, thrown on failed waiting for timeout. Should process the original exception and rethrow it or the modified one.
reports_folder: Optional[str] = os.path.join(os.path.expanduser('~'), '.selene', 'screenshots', str(round(time.time() * 1000)))
class-attribute
instance-attribute
#
A folder to save screenshots and page sources on failure.
save_screenshot_on_failure: bool = True
class-attribute
instance-attribute
#
A flag to indicate whether to save screenshot on failure or not. If saved, will be also logged to the console on failure.
save_page_source_on_failure: bool = True
class-attribute
instance-attribute
#
A flag to indicate whether to save page source on failure or not. If saved, will be also logged to the console on failure.
with_(**options_to_override)
#
Returns (Config): A new config with overridden options that were specified as arguments.
All other config options will be shallow-copied
from the current config.
Those other options that are of immutable types,
like `int` - will be also copied by reference,
i.e. in a truly shallow way.
Parameters:
-
**options_to_override
(Any
, default:{}
) –options to override in the new config.
Technically "override" here means: "deep copy option storage and update its value to the specified one". All other option storages will be: "shallow copied from the current config".
If
driver_name
is amongoptions_to_override
, anddriver
is not among them, andself._override_driver_with_all_driver_like_options
is True, thendriver
will be implicitly added to the options to override, i.e.with_(driver_name='firefox')
will be equivalent towith_(driver_name='firefox', driver=...)
. The latter gives a readable and concise shortcut to spawn more than one browser:from selene import Config config = Config(timeout=10.0, base_url='https://autotest.how') chrome = config.driver # chrome is default browser firefox_config = config.with_(driver_name='firefox') firefox = firefox_config.driver edge_config = config.with_(driver_name='edge') edge = edge_config.driver
Same logic applies to
remote_url
, and all other config.driver options.
_save_screenshot_strategy#
Defines a strategy for saving a screenshot.
The default strategy saves a screenshot to a file,
and stores the path to config.last_screenshot
.
_save_screenshot_strategy: Callable[
[Config, Optional[str]], Any
] = lambda config, path=None: fp.thread(
path,
lambda path: (
config._generate_filename(suffix='.png') if path is None else path
),
lambda path: (
os.path.join(path, f'{next(config._counter)}.png')
if path and not path.lower().endswith('.png')
else path
),
fp.do(
fp.pipe(
os.path.dirname,
lambda folder: (
os.makedirs(folder)
if folder and not os.path.exists(folder)
else ...
),
)
),
fp.do(
lambda path: (
warnings.warn(
'name used for saved screenshot does not match file '
'type. It should end with an `.png` extension',
UserWarning,
)
if not path.lower().endswith('.png')
else ...
)
),
lambda path: (path if config.driver.get_screenshot_as_file(path) else None),
fp.do(
lambda path: setattr(config, 'last_screenshot', path)
),
)
_save_page_source_strategy#
Defines a strategy for saving a page source on failure.
The default strategy saves a page_source to a file,
and stores the path to config.last_page_source
.
_save_page_source_strategy: Callable[
[Config, Optional[str]], Any
] = lambda config, path=None: fp.thread(
path,
lambda path: (
config._generate_filename(suffix='.html') if path is None else path
),
lambda path: (
os.path.join(path, f'{next(config._counter)}.html')
if path and not path.lower().endswith('.html')
else path
),
fp.do(
fp.pipe(
os.path.dirname,
lambda folder: (
os.makedirs(folder)
if folder and not os.path.exists(folder)
else ...
),
)
),
fp.do(
lambda path: (
warnings.warn(
'name used for saved page source does not match file '
'type. It should end with an `.html` extension',
UserWarning,
)
if not path.lower().endswith('.html')
else ...
)
),
lambda path: (path, config.driver.page_source),
fp.do(lambda path_and_source: fp.write_silently(*path_and_source)),
lambda path_and_source: path_and_source[0],
fp.do(
lambda path: setattr(config, 'last_page_source', path)
),
)