Skip to content

#

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:)
  • 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 like timeout or base_url. More nuances of browser 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:

>>> from selene import browser
>>> from selenium import webdriver
>>>
>>> options = webdriver.ChromeOptions()
>>> options.add_argument('--headless')
>>> browser.config.driver_options = options

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:

config.driver_name = 'chrome'
config.driver_options = ChromeOptions()

It's enough to provide only the options:

config.driver_options = ChromeOptions()

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:

>>> from selene import browser
>>> browser.config.base_url = 'https://mywebapp.xyz'
>>> browser.open('/signin')
>>> ...
>>> browser.open('/signup')
>>> ...
>>> browser.open('/profile')
>>> ...

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 among options_to_override, and driver is not among them, and self._override_driver_with_all_driver_like_options is True, then driver will be implicitly added to the options to override, i.e. with_(driver_name='firefox') will be equivalent to with_(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)
    ),
)