User-oriented Web UI browser tests in Python
MIT License
Bot releases are visible (Hide)
As simple as that:
from selene import browser, command
...
browser.element('#point1').click(xoffset=-5, yoffset=5) # relative from center
browser.element('#point1').click() # still works as before (clicking at center)
# with js too:
browser.element('#point1').perform(command.js.click(xoffset=-5, yoffset=5))
browser.element('#point1').perform(command.js.click()) # also works
browser.element('#point1').perform(command.js.click) # still works as before
# or:
browser.element('#point1').with_(click_by_js=True).click(xoffset=-5, yoffset=5)
Seems like the send_keys(Keys.COMMAND + 'a' + Keys.NULL)
receipe has stopped working since some Selenium version...
So we update the command.select_all implementation to be based on ActionChains, and also work both on browser and element. Here go two examples that demonstrate the new behavior:
when called on element:
page.opened_with_body('<input id="text-field" value="text"></input>')
browser.element('#text-field').perform(command.select_all).type('reset')
browser.element('#text-field').should(have.value('reset'))
when called on browser:
page.opened_with_body('<input id="text-field" value="text"></input>')
browser.element('#text-field').click() # <- MANDATORY to make the input focused
browser.perform(command.select_all)
browser.element('#text-field').type('reset')
browser.element('#text-field').should(have.value('reset'))
Allows to simplify custom conditions implementation to something like:
class have:
@staticmethod
def attribute(entity):
if entity.attribute is None:
raise AssertionError('attribute is None')
Since the have.attribute
staticmethod will already have __qualname__
defined and equal to 'have.attribute'
, that will result in same rendering of the condition name in error messages on failed waiting (entity.wait.for_(condition)
) or assertion (via entity.should(condition)
).
Published by yashaka 8 months ago
– by removed stacktrace in processing of timeout exception at wait.py (thanks to @jacekziembla)
Published by yashaka 9 months ago
browser._actions
is an instance of experimental _Actions class – an alternative implementation of ActionChains from Selenium...
So you can use:
from selene import browser
from selene.support.shared.jquery_style import s
browser._actions.move_to(s('#point1')).pause(1).click_and_hold(s('#point1')).pause(1).move_by_offset(0, 5).move_to(s('#point2')).pause(1).release().perform()
instead of something like:
from selene import browser
from selene.support.shared.jquery_style import s
from selenium.webdriver.common.action_chains import ActionChains
ActionChains(browser.driver).move_to_element(s('#point1').locate()).pause(1).click_and_hold(s('#point1').locate()).pause(1).move_by_offset(0, 5).move_to_element(s('#point2').locate()).pause(1).release().perform()
or actually even instead of this:
from selene import browser, be
from selene.support.shared.jquery_style import s
from selenium.webdriver.common.action_chains import ActionChains
ActionChains(browser.driver).move_to_element(s('#point1').should(be.in_dom).locate()).pause(1).click_and_hold(s('#point1').should(be.in_dom).locate()).pause(1).move_by_offset(0, 5).move_to_element(s('#point2').should(be.in_dom).locate()).pause(1).release().perform()
Here are advantages of Selene's _actions over Selenium's ActionChains:
.perform
– it will be automatically retried in context of common Selene's implicit waiting logicHere are some open points regarding this implementation and why this feature is marked as experimental:
.perform
is needed at all... can hardly imagine any failure there that can be fixed by retryingfrom selene import Browser
that is unchanged!)Published by yashaka 9 months ago
Published by yashaka 9 months ago
when Selenium can interact with simple draggable controls:
browser.element('#volume-slider-thumb').perform(command.drag_and_drop_to(browser.element('#volume-up')))
browser.element('#volume-slider-thumb').perform(command.drag_and_drop_to(browser.element('#volume-up'), _assert_location_changed=True))
_assert_location_changed=True
is marked as experimental by _
prefix,browser.element('#volume-slider-thumb').perform(command.drag_and_drop_by_offset(x=-10, y=0))
when Selenium can not interact with simple draggable controls:
browser.element('#volume-slider-thumb').perform(command.js.drag_and_drop_to(browser.element('#volume-up')))
when there is no input element with type file, and you need to simulate the "drop file" by JS:
browser.element('#drag-file-here-to-upload').perform(command.js.drop_file('/path/to/file'))
Find more examples at these tests:
Published by yashaka about 1 year ago
Published by yashaka about 1 year ago
Published by yashaka about 1 year ago
Hence, 4.0.0 should be kind of supported now... But Selene's tests, if executed on macOS arm64 – are very unstable with chromedriver downloaded by wdm 4.0.0 :(, failing with error:
selenium.common.exceptions.WebDriverException: Message: Service /Users/yashaka/.wdm/drivers/chromedriver/mac64/115.0.5790.114/chromedriver-mac-arm64/chromedriver unexpectedly exited. Status code was: -9
That's why we still freeze wdm to 3.8.6, but on your own risk you can try 4.0.0.
Published by yashaka about 1 year ago
webdriver-manager is still frozen to 3.8.6, though there are already 4.0.
Remember that on MacOS you probably have either to install Chrome for Testing or specify browser location manually via:
from selene import browser
from selenium import webdriver
browser.config.driver_options = webdriver.ChromeOptions()
browser.config.driver_options.binary_location = (
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
)
browser.open('https://www.ecosia.org/')
See more in 2.0.0rc3 release notes.
Published by yashaka about 1 year ago
Fixes #536 wdm issue by patching wdm of 3.8.6 version to workaround the following error:
ValueError: There is no such driver by url https://chromedriver.storage.googleapis.com/LATEST_RELEASE_115.0.5790
This hotfix is really hot:), so might break something. Use it on your own risk.
If something went wrong, roll back to 2.0.0rc2.
If you don't use Selene, feel free to copy the patch, adapt it for your liking and use to fix wdm at your context.
In Selene we also froze webdriver_manager version to 3.8.6, so it will not be updated automatically and our hotfix will not be broken :D. Let's see how it goes further... One day we hope to remove hotfix and unfreeze webdriver_manager version.
Should work for new versions of Chrome from v115 out of the box.
If you use webdriver_manager on your own, you can do the following trick to patch it with the fix:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.core.utils import ChromeType
from selene import support
chrome_driver = webdriver.Chrome(
service=Service(
support._extensions.webdriver_manager.patch._to_find_chromedrivers_from_115(
ChromeDriverManager(chrome_type=ChromeType.GOOGLE)
).install()
)
)
Notice underscore prefixes in module and patch function names at _extensions.webdriver_manager.patch._to_find_chromedrivers_from_115
. Use it on your own risk, as it is marked as private and experimental;).
Remember that currently on macOS the fix itself might not be enough, for Chrome versions less than 117, you probably will have to install Chrome for Testing browser instead of Chrome and fix it with xattr -cr 'Google Chrome for Testing.app'
command. An alternative to installing Chrome for Testing, can be setting binary location manually via:
from selene import browser
from selenium import webdriver
browser.config.driver_options = webdriver.ChromeOptions()
browser.config.driver_options.binary_location = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
Published by yashaka over 1 year ago
Before:
config.driver_name
was 'chrome'
by defaultNow:
config.driver_name
is None
by default
config.driver_options
usually is enough to guess the driver name,config.driver_options = FirefoxOptions()
Just in case you want, e.g. to use own driver executable like:
from selene import browser
from selenium.webdriver.chrome.service import Service
from selenium import webdriver
browser.config.driver_service = Service('/path/to/my/chromedriver')
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless')
browser.config.driver_options = chrome_options
from selene import browser, by, have, command
browser.open('https://www.ecosia.org/')
browser.element(by.name('q')).type('selene').should(have.value('selene'))
browser.element(by.name('q')).perform(command.select_all).type('github yashaka selene')
browser.element(by.name('q')).should(have.value('github yashaka selene'))
Probably might be useful for cases where normal element.set_value(text)
, while based on webelement.clear(); webelement.send_keys(text)
, - does not work, in most cases because of some events handled on clear()
.
More relevant to the mobile case. Might work for web too, but not tested fully for web, not covered with tests. That's why is still marked with _
as experimental.
Published by yashaka over 1 year ago
Any driver instance passed to browser.config.driver
will be automatically quit at exit, unless browser.config.hold_driver_at_exit = True
, that is False
by default
... if happened to be not alive, e.g. after quit or crash. This was relevant in the past, but not for manually set drivers. Not it works for all cases by default, including manually set driver by browser.config.driver = my_driver_instance
. To disable this behavior set browser.config._reset_not_alive_driver_on_get_url = False
(currently this option is still marked as experimental with _
prefix, it may be renamed till final 2.0 release).
Once automatic rebuild is disabled, you can schedule rebuild on next access to driver by setting browser.config.driver = ...
(besides ellipsis, setting to None
also works). This is actually what is done inside browser.open(url)
if browser.config._reset_not_alive_driver_on_get_url = True
and driver is not alive.
There is another "rebuild" option in config that is disabled by default: browser.config.rebuild_not_alive_driver
. It is used to rebuild driver on any next access to it, if it is not alive. This is different from browser.config._reset_not_alive_driver_on_get_url
that resets driver (scheduling to be rebuilt) only on next call to browser.open(url)
. Take into account that enabling this option may leed to slower tests when running on remote drivers, because it will check if driver is alive on any access to it, not only on browser.open(url)
.
... except Browser
class itself, of course (but this might be changed somewhere in 3.0🙃)
For example, config.browser_name
is deprecated in favor of config.driver_name
. Main reason – «browser» term is not relevant to mobile testing, where in a lot of cases we test user actions in app, not browser.
from selene import browser
– to be used instead of from selene.support.shared import browser
.
No difference between Config and SharedConfig anymore. The new, completely refactored, Config is now used everywhere and allows to customize browser instance in a more convenient way.
Adds ability to use browser.with_(**config_options_to_override)
to create new browser instance, for example:
from selene import browser
chrome = browser
firefox = browser.with_(driver_name='firefox')
edge = browser.with_(driver_name='edge')
...
# customizing all browsers at once:
browser.config.timeout = 10
as alternative to:
from selene import Browser, Config
chrome = Browser(Config())
firefox = Browser(Config(driver_name='firefox'))
edge = Browser(Config(driver_name='edge'))
...
# customizing all browsers:
chrome.config.timeout = 10
firefox.config.timeout = 10
edge.config.timeout = 10
browser.config.driver_options
+ browser.config.driver_remote_url
Finally, you can delegate building driver to config manager by passing driver_options
and driver_remote_url
to it:
import dotenv
from selenium import webdriver
from selene import browser, have
def test_complete_task():
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 # <- 🥳
project_config = dotenv.dotenv_values()
browser.config.driver_remote_url = ( # <- 🎉🎉🎉
f'https://{project_config["LOGIN"]}:{project_config["PASSWORD"]}@'
f'selenoid.autotests.cloud/wd/hub'
)
browser.open('http://todomvc.com/examples/emberjs/')
browser.should(have.title_containing('TodoMVC'))
browser.element('#new-todo').type('a').press_enter()
browser.element('#new-todo').type('b').press_enter()
browser.element('#new-todo').type('c').press_enter()
browser.all('#todo-list>li').should(have.exact_texts('a', 'b', 'c'))
browser.open()
without argsWill just open driver or do nothing if driver is already opened.
Can also load page from browser.config.base_url
if it is set and additional experimental browser.config._get_base_url_on_open_with_no_args = True
option is set (that is False
by default).
browser.open
, but...but can be configured as follows:
browser.config.__reset_not_alive_driver_on_get_url = False
,True
by defaultbrowser.config.driver
,browser.config.rebuild_not_alive_driver = True
(that is False
by default)Yet you have to install it manually. But given installed via pip install Appium-Python-Client
or something like poetry add Appium-Python-Client
, running tests on mobile devices is as easy as...
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
from selene import browser, have
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
# # Possible, but not needed, because will be used by default:
# browser.config.driver_remote_url = 'http://127.0.0.1:4723/wd/hub'
by_id = lambda id: (AppiumBy.ID, f'org.wikipedia.alpha:id/{id}')
# GIVEN
browser.open()
browser.element(by_id('fragment_onboarding_skip_button')).click()
# WHEN
browser.element((AppiumBy.ACCESSIBILITY_ID, 'Search Wikipedia')).click()
browser.element(by_id('search_src_text')).type('Appium')
# THEN
browser.all(by_id('page_list_item_title')).should(
have.size_greater_than(0)
)
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
from selene import browser, have
options = UiAutomator2Options()
options.app = 'bs://c700ce60cf13ae8ed97705a55b8e022f13c5827c'
options.set_capability(
'bstack:options',
{
'deviceName': 'Google Pixel 7',
'userName': 'adminadminovych_qzqzqz',
'accessKey': 'qzqzqzqzqzqzqzqzqzqz',
},
)
browser.config.driver_options = options
browser.config.driver_remote_url = 'http://hub.browserstack.com/wd/hub'
by_id = lambda id: (AppiumBy.ID, f'org.wikipedia.alpha:id/{id}')
# GIVEN
browser.open() # not needed, but to explicitly force appium to open app
# WHEN
browser.element((AppiumBy.ACCESSIBILITY_ID, 'Search Wikipedia')).click()
browser.element(by_id('search_src_text')).type('Appium')
# THEN
browser.all(by_id('page_list_item_title')).should(
have.size_greater_than(0)
)
https://github.com/yashaka/selene/tree/master/examples
browser.save_screenshot
in favor of browser.get(query.screenshot_saved())
browser.save_page_source
in favor of browser.get(query.page_source_saved())
browser.last_screenshot
in favor of browser.config.last_screenshot
browser.last_page_source
in favor of browser.config.last_page_source
match.browser_has_js_returned
in favor of match.browser_has_script_returned
have.js_returned
in favor of have.script_returned
have.js_returned_true(...)
in favor of have.script_returned(True, ...)
browser.config.get_or_create_driver
browser.config.reset_driver
selene.browser.config.driver = ...
browser.config.browser_name
in favor of browser.config.driver_name
Published by yashaka over 1 year ago
from selene import browser
;)Published by yashaka almost 2 years ago
Published by yashaka about 2 years ago
from selene.support.shared import browser
from selene import command
# calling on element
overlay = browser.element('#overlay')
overlay.perform(command.js.set_style_property('display', 'none'))
# can be also called on collection of elements:
ads = browser.all('[id^=google_ads][id$=container__]')
ads.perform(command.js.set_style_property('display', 'none'))
have.values
and have.values_containing
have.texts
& have.exact_texts
– flatten passed lists of textsThis allows to pass args as lists (even nested) not just as varagrs.
from selene.support.shared import browser
from selene import have
"""
# GIVEN html page with:
<table>
<tr class="row">
<td class="cell">A1</td><td class="cell">A2</td>
</tr>
<tr class="row">
<td class="cell">B1</td><td class="cell">B2</td>
</tr>
</table>
"""
browser.all('.cell').should(
have.exact_texts('A1', 'A2', 'B1', 'B2')
)
browser.all('.cell').should(
have.exact_texts(['A1', 'A2', 'B1', 'B2'])
)
browser.all('.cell').should(
have.exact_texts(('A1', 'A2', 'B1', 'B2'))
)
browser.all('.cell').should(
have.exact_texts(
('A1', 'A2'),
('B1', 'B2'),
)
)
because all string normalization is already done by Selenium Webdriver.
Published by yashaka about 2 years ago
Now, you can set only one axis dimension for the browser, and it will change it on browser.open
. Before it would change browser window size only if both width and height were set;)
element
or self
from the script passed to element.execute_script(script_on_self, *arguments)Examples:
from selene.support.shared import browser
browser.element('[id^=google_ads]').execute_script('element.remove()')
# OR
browser.element('[id^=google_ads]').execute_script('self.remove()')
'''
# are shortcuts to
browser.execute_script('arguments[0].remove()', browser.element('[id^=google_ads]')())
'''
browser.element('input').execute_script('element.value=arguments[0]', 'new value')
# OR
browser.element('input').execute_script('self.value=arguments[0]', 'new value')
'''
# are shortcuts to
browser.execute_script('arguments[0].value=arguments[1]', browser.element('input').locate(), 'new value')
'''
collection.second
shortcut to collection[1]
element.locate() -> WebElement
, collection.locate() -> List[WebElement]
#284
... as more human-readable aliases to element() and collection() correspondingly
entity.__raw__
It's a «dangled» property and so consider it an experimental/private feature.
For element and collection – it's same as .locate()
.
For browser
it's same as .driver
;)
Read more on it at this comment to #284
... as aliases to element(), collection() correspondingly
... in favor of .execute_script(script_on_self, *arguments) that uses access to arguments (NOT args!) in the script.
Deprecated because the «tab» term is not relevant for mobile context.
Use a browser.close()
or browser.driver.close()
instead.
The deprecation mark was removed from the browser.close()
correspondingly.
browser.clear_session_storage()
and browser.clear_local_storage()
Deprecated because of js nature and not-relevance for mobile context;
Use browser.perform(command.js.clear_session_storage)
and browser.perform(command.js.clear_local_storage)
instead
from selene.support.shared import browser
# before this version ...
browser.element('input').execute_script('arguments[0].value=arguments[1]', 'new value')
# NOW:
browser.element('input').execute_script('element.value=arguments[0]', 'new value')
browser.elements(selector)
in favor of browser.all(selector)
browser.ss(selector)
in favor of browser.all(selector)
browser.s(selector)
in favor of browser.element(selector)
element.get_actual_webelement()
in favor of element.locate()
collection.get_actual_webelements()
in favor of collection.locate()
collection.should(HERE)
collection.should(element_condition.each)
browser.all('.selene-user').should(hava.css_class('cool').each)
Published by yashaka about 2 years ago
The older style is totally deprecated now:
collection.should(element_condition)
and collection.should_each(element_condition)
collection.should(element_condition.each)
use instead:
import selene
element: selene.Element = ...
collection: selene.Collection = ...
browser: selene.Browser = ...
or:
from selene import Element, Collection, Browser
element: Element = ...
collection: Collection = ...
browser: Browser = ...
Published by yashaka about 2 years ago
see reasons at:
Published by yashaka about 2 years ago
caching(self)
in favor of cashed(self)
all_by(self, condition) -> Collection
in favor of by(condition)
filter_by(self, condition) -> Collection
in favor of by(condition)
find_by(self, condition) -> Element
size(self) -> int
in favor of __len__(self)
Published by yashaka about 2 years ago
browser.all(selector).by(condition)
to filter collectionfrom selene.support.shared import browser
from selene import have
browser.open('https://todomvc.com/examples/emberjs/')
browser.element('#new-todo').type('a').press_enter()
browser.element('#new-todo').type('b').press_enter()
browser.element('#new-todo').type('c').press_enter()
browser.all('#todo-list>li').by(have.text('b')).first.element('.toggle').click()
browser.all('#todo-list>li').by(have.css_class('active')).should(have.texts('a', 'c'))
browser.all('#todo-list>li').by(have.no.css_class('active')).should(have.texts('b'))
Hence, considering to deprecate:
collection.filtered_by(condition)
in favor of collection.by(condition)
collection.element_by(condition)
in favor of collection.by(condition).first
collection.even
and collection.odd
shortcutsfrom selene.support.shared import browser
from selene import have
browser.open('https://todomvc.com/examples/emberjs/')
browser.element('#new-todo').type('1').press_enter()
browser.element('#new-todo').type('2').press_enter()
browser.element('#new-todo').type('3').press_enter()
browser.all('#todo-list>li').even.should(have.texts('2'))
browser.all('#todo-list>li').odd.should(have.texts('1', '3'))
collection.sliced(start, stop, step)
Now you can achieve more readable collection.sliced(step=2)
instead of awkward collection.sliced(None, None, 2)
Remember that you still can use less readable but more concise collection[::2]
;)