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 instance-attribute class-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

_schedule_driver_teardown_strategy: Callable[[Config, Callable[[], WebDriver]], None] = lambda config, get_driver: atexit.register(lambda : config._teardown_driver_strategy(config)(get_driver())) instance-attribute class-attribute #

Defines when drier will be teardown. Is supposed to use config._teardown_driver_strategy under the hood.

By default, it's registered as an atexit handler.

_teardown_driver_strategy: Callable[[Config, WebDriver], None] = lambda config: lambda driver: driver.quit() if not config.hold_driver_at_exit and config._is_driver_set_strategy(driver) and config._is_driver_alive_strategy(driver) else None instance-attribute class-attribute #

Defines how driver will be teardown.

By default it quits the driver if it's alive and not asked to be held at exit via config.hold_driver_at_exit.

_is_driver_set_strategy: Callable[[WebDriver], bool] = lambda driver: driver is not Ellipsis and driver is not None instance-attribute class-attribute #

Defines how to check if driver is set, and so defines how to "unset" or "reset" it.

_is_driver_alive_strategy: Callable[[WebDriver], bool] = lambda driver: driver.service.process is not None and driver.service.process.poll() is None if hasattr(driver, 'service') else on_error_return_false(lambda : driver.window_handles is not None) instance-attribute class-attribute #

Defines the logic of checking driver for being alive.

Is supposed to be used in context of triggering automatic driver rebuild, depending on context.

driver_options: Optional[BaseOptions] = None instance-attribute class-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 instance-attribute class-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 instance-attribute class-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 instance-attribute class-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

_override_driver_with_all_driver_like_options: bool = True instance-attribute class-attribute #

Controls whether driver will be deep copied with config.driver_name, config.driver_remote_url, and so for any other config.*driver* option. when customizing config via config.with_(**options_to_override).

Examples:

Building 2 drivers with implicit deep copy of driver storage:

>>> chrome_config = Config(
>>>     driver_name='chrome',
>>>     timeout=10.0,
>>>     base_url='https://autotest.how',
>>> )
>>> chrome = chrome_config.driver
>>> firefox_config = chrome_config.with_(driver_name='firefox')
>>> firefox = firefox_config.driver
>>> assert firefox is not chrome

Building 2 drivers with explicit deep copy of driver storage [1]:

>>> chrome_config = Config(
>>>     driver_name='chrome',
>>>     timeout=10.0,
>>>     base_url='https://autotest.how',
>>>     _override_driver_with_all_driver_like_options=False,
>>> )
>>> chrome = chrome_config.driver
>>> firefox_config = chrome_config.with_(driver_name='firefox', driver=...)
>>> firefox = firefox_config.driver
>>> assert firefox is not chrome

Building 2 drivers with explicit deep copy of driver storage [2]:

>>> chrome_config = Config(
>>>     driver_name='chrome',
>>>     timeout=10.0,
>>>     base_url='https://autotest.how',
>>> )
>>> chrome_config._override_driver_with_all_driver_like_options = False
>>> chrome = chrome_config.driver
>>> firefox_config = chrome_config.with_(name='firefox', driver=...)
>>> firefox = firefox_config.driver
>>> assert firefox is not chrome

Building 1 driver because driver storage was not copied:

>>> chrome_config = Config(
>>>     driver_name='chrome',
>>>     timeout=10.0,
>>>     base_url='https://autotest.how',
>>> )
>>> chrome_config._override_driver_with_all_driver_like_options = False
>>> chrome = chrome_config.driver
>>> firefox_config = chrome_config.with_(name='firefox')
>>> firefox = firefox_config.driver
>>> assert firefox is chrome  # o_O ;)

hold_driver_at_exit: bool = False instance-attribute class-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

_driver_get_url_strategy: Callable[[Config], Callable[[Optional[str]], None]] = _maybe_reset_driver_then_tune_window_and_get_with_base_url instance-attribute class-attribute #

Defines how to get url with driver depending on other options, like config.base_url.

Is used inside browser.open(relative_or_absolute_url), and defines its behavior correspondingly.

_reset_not_alive_driver_on_get_url: bool = True instance-attribute class-attribute #

Controls whether driver should be automatically reset and, thus, forced to be rebuilt, if it was noticed as not alive (e.g. after quit or crash) on next call to config.driver.get(url) (via config._driver_get_url_strategy).

Does not work if config.driver was set manually to Callable[[], WebDriver].

Is a more "week" option than config.rebuild_not_alive_driver, that is disabled by default, that forces to rebuild driver on any next access.

rebuild_not_alive_driver: bool = False instance-attribute class-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=Ellipsis) instance-attribute class-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 instance-attribute class-attribute #

A default timeout for all Selene waiting that happens under the hood of the majority of Selene commands and assertions.

base_url: str = '' instance-attribute class-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')
>>> ...

_get_base_url_on_open_with_no_args: bool = False instance-attribute class-attribute #

A flag to indicate whether to use config.base_url when opening a page with browser.open() command without arguments.

If you call browser.open() without arguments, it will just force the driver to be built and real browser to be opened.

Even if you have set config.base_url, to reuse it in your tests on browser.open, you have to specify the url explicitly, like browser.open('/') or at least browser.open('').

But there are cases where you would like to load base url on browser.open(), for example in context of cross-platform testing. Then you use this option;) See example at https://github.com/yashaka/selene/blob/master/examples/run_cross_platform

window_width: Optional[int] = None instance-attribute class-attribute #

If set, will be used to set the window width on next call to browser.open(url).

window_height: Optional[int] = None instance-attribute class-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 instance-attribute class-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 instance-attribute class-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 instance-attribute class-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 instance-attribute class-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.

reports_folder: Optional[str] = os.path.join(os.path.expanduser('~'), '.selene', 'screenshots', str(round(time.time() * 1000))) instance-attribute class-attribute #

A folder to save screenshots and page sources on failure.

save_screenshot_on_failure: bool = True instance-attribute class-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 instance-attribute class-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.

_counter: itertools.count = itertools.count(start=int(round(time.time() * 1000))) instance-attribute class-attribute #

A counter, currently used for incrementing screenshot and page source names

with_(**options_to_override) #

Returns:

  • 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

    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)
    ),
)

Last update: 2023-04-15
Created: 2023-04-15