GithubHelp home page GithubHelp logo

yashaka / selene Goto Github PK

View Code? Open in Web Editor NEW
688.0 57.0 148.0 27.86 MB

User-oriented Web UI browser tests in Python

Home Page: https://yashaka.github.io/selene/

License: MIT License

Python 99.09% JavaScript 0.42% HTML 0.41% Shell 0.08%
automation e2e-testing python selene selenide selenium testing web webdriver

selene's Introduction

Selene - User-oriented Web UI browser tests in Python (Selenide port)

Pre-release Version tests codecov Free MIT License

Downloads Project Template Code style: black

Join telegram chat https://t.me/selene_py Присоединяйся к чату https://t.me/selene_py_ru

Sign up for a course https://autotest.how/sdet-start Запишись на курс https://autotest.how/sdet-start-ru Реєструйся на курс https://autotest.how/sdet-start-uk

Main features:

  • User-oriented API for Selenium Webdriver (code like speak common English)
  • Ajax support (Smart implicit waiting and retry mechanism)
  • PageObjects support (all elements are lazy-evaluated objects)
  • Automatic driver management (no need to install and setup driver for quick local execution)

Selene was inspired by Selenide from Java world.

Tests with Selene can be built either in a simple straightforward "selenide" style or with PageObjects composed from Widgets i.e. reusable element components.

Currently, this official documentation is far from being complete. Please, use the following links to get started from scratch:

Find more below...

Versions

  • Latest recommended version to use is latest among 2.0.0rc*
    • it's a completely new version of selene, with improved API and speed
    • supports Python 3.8+
    • supports Selenium >=4.12.0
    • it's incompatible with 1.x
    • current master branch is pointed to 2.x
    • yet in alpha/beta stage, refining API, improving "migratability" and testing
    • most active Selene users already upgraded to 2.0 alpha/beta and have been using it in production during last 2 years
    • the only risk is API changes, some commands are in progress of deprecation and renaming
  • Latest version marked as stable is: 1.0.2
    • its sources and corresponding README version can be found at 1.x branch.
    • supports python 2.7, 3.5, 3.6, 3.7

THIS README DESCRIBES THE USAGE OF THE PRE-RELEASE version of Selene. For older docs look at 1.x branch.

Migration guide

From 1.0.2 to 2.0.0b<LATEST>:

  • upgrade to Python 3.7+
  • update selene to 2.0.0b<LATEST>
    • find&replace the collection.first() method from .first() to .first
    • ensure all conditions like text('foo') are used via be.* or have.* syntax
      • example:
        • find&replace all
          • (text('foo')) to (have.text('foo'))
          • (visible) to (be.visible)
        • smarter find&replace (with some manual refactoring)
          • .should(x, timeout=y) to .with_(timeout=y).should(x)
          • .should_not(be.*) to .should(be.not_.*) or .should(be.*.not_)
          • .should_not(have.*) to .should(have.no.*) or .should(have.*.not_)
          • .should_each(condition) to .should(condition.each)
        • and add corresponding imports: from selene import be, have
    • fix another broken imports if available
    • run tests, read deprecation warnings, and refactor to new style recommended in warning messages

Prerequisites

Python 3.8+

Given pyenv installed, installing needed version of Python is pretty simple:

$ pyenv install 3.8.13
$ pyenv global 3.8.13
$ python -V
Python 3.8.13

Installation

via poetry + pyenv (recommended)

GIVEN poetry and pyenv installed ...

AND

poetry new my-tests-with-selene
cd my-tests-with-selene
pyenv local 3.8.13

WHEN latest pre-release recommended version:

poetry add selene --allow-prereleases

WHEN latest stable version:

poetry add selene

THEN

poetry install

via pip

Latest recommended pre-release alpha version:

pip install selene --pre

Latest stable version:

pip install selene

from sources

GIVEN webdriver

THEN

git clone https://github.com/yashaka/selene.git
python setup.py install

or using pip:

pip install git+https://github.com/yashaka/selene.git

Usage

Quick Start

Simply...

from selene import browser, by, be, have

browser.open('https://google.com/ncr')
browser.element(by.name('q')).should(be.blank)\
    .type('selenium').press_enter()
browser.all('#rso>div').should(have.size_greater_than(5))\
    .first.should(have.text('Selenium automates browsers'))

OR with custom setup

from selene import browser, by, be, have

browser.config.driver_name = 'firefox'
browser.config.base_url = 'https://google.com'
browser.config.timeout = 2
# browser.config.* = ...

browser.open('/ncr')
browser.element(by.name('q')).should(be.blank) \
    .type('selenium').press_enter()
browser.all('#rso>div').should(have.size_greater_than(5)) \
    .first.should(have.text('Selenium automates browsers'))

OR more Selenide from java style:

from selene import browser, by, be, have
from selene.support.shared import config
from selene.support.shared.jquery_style import s, ss


config.browser_name = 'firefox'
config.base_url = 'https://google.com'
config.timeout = 2
# config.* = ...

browser.open('/ncr')
s(by.name('q')).should(be.blank) \
    .type('selenium').press_enter()
ss('#rso>div').should(have.size_greater_than(5)) \
    .first.should(have.text('Selenium automates browsers'))

Core Api

# Given:
from selenium.webdriver import Chrome

# AND chromedriver executable available in $PATH

# WHEN:
from selene import Browser, Config

browser = Browser(
    Config(
        driver=Chrome(),
        base_url='https://google.com',
        timeout=2,
    )
)

# AND:
browser.open('/ncr')

# AND:
# browser.element('//*[@name="q"]')).type('selenium').press_enter()
# OR...
# browser.element('[name=q]')).type('selenium').press_enter()
# OR...
from selene import by
# browser.element(by.name('q')).type('selenium').press_enter()
# OR...for total readability
query = browser.element(by.name('q'))
# actual search doesn't start on calling browser.element above, 
# i.e. the element is "lazy"... or in other words it serves as locator         
# Below, on calling actual first action, 
#     ⬇ the actual webelement is located first time
query.type('selenium').press_enter()       
#                      ⬆
#                  and here it's located again, i.e. the element is "dynamic"

# AND in case we need to filter collection of items 
#     by some condition like visibility:

from selene import be
results = browser.all('#rso>div').by(be.visible)

# THEN we can assert some condition:
from selene import have
# results.should(have.size_greater_than(5))
# results.first.should(have.text('Selenium automates browsers'))
# OR...
results.should(have.size_greater_than(5))\
    .first.should(have.text('Selenium automates browsers'))

# FINALLY the browser can be quit:
browser.quit()
# but it's not mandatory, because by default Selenes kills all drivers on exit
# that can be disabled by:
browser.config.hold_driver_at_exit = True

Automatic Driver and Browser Management

Instead of:

from selenium.webdriver import Chrome
from selene import Browser, Config

browser = Browser(
    Config(
        driver=Chrome(),
        base_url='https://google.com',
        timeout=2
    )
)

browser.open('/ncr')

You can simply use the browser instance predefined for you in selene module:

from selene import browser

browser.config.base_url = 'https://google.com'
browser.config.timeout = 2

browser.open('/ncr')

So you don't need to create you driver instance manually. It will be created for you automatically.

Yet, if you need some special case, like working with remote driver, etc., you can still use shared browser object with additional configuration:

from selenium import webdriver
from selene import browser

options = webdriver.ChromeOptions()
options.add_argument('--headless')
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
browser.config.driver_remote_url = 'http://localhost:4444/wd/hub', 
browser.config.base_url = 'https://google.com'
browser.config.timeout = 2

browser.open('/ncr')
...

But if you like to create the driver on your own, you can do it too:

from selenium import webdriver
from selene import browser

options = webdriver.ChromeOptions()
options.add_argument('--headless')
# ... other arguments
browser.config.driver = webdriver.Remote(
  'http://localhost:4444/wd/hub', 
  options=options
)
# Once you start to build and set the driver on your own,
# probably you are going to fully manage it life cycle,
# thus, consider disabling the automatic driver reset on browser.open
# if driver was crashed or quit:
browser.config._reset_not_alive_driver_on_get_url = False
# And consider disabling the automatic driver quit on exit:
browser.config.hold_driver_at_exit = True
# Other common options will still be useful:
browser.config.base_url = 'https://google.com'
browser.config.timeout = 2

browser.open('/ncr')
...

# Finally, you can quit the driver manually:
browser.quit()

Advanced API

Sometimes you might need some extra things to reach your specific goals... Here go examples of Selene's command, query, custom conditions, .matching(condition) and .wait_until(condition)...

from selene import browser, have

...

###################################################
# Maybe you need some advanced actions on elements,
# e.g. for workaround something through js:

from selene import command

browser.element('#not-in-view').perform(command.js.scroll_into_view)

...

###################################################
# Probably you think that will need something like:

from selene import query

...

def my_int_from(text):
    return int(text.split(' ')[0])

product_text = browser.element('#price-label').get(query.text)
# ... to assert something not standard:
price = my_int_from(product_text)
assert price > 100

# But such version is very unstable in dynamic web world...
# Usually it's...
# either better to implement your custom condition:

from selene.core.condition import Condition
from selene.core.conditions import ElementCondition
from selene.core.entity import Element


def have_in_text_the_int_number_more_than(number) -> Condition[Element]:
    def fn(element: Element) -> None:
        text = element.get(query.text)
        parsed_number = my_int_from(text)
        if not parsed_number > number:
            raise AssertionError(
                f'actual text was: {text}'
                f'with parsed int number: {parsed_number}'
            )
    return ElementCondition(
        f'has in text the int number more than: {number}', 
        fn
    )


browser.element('#price-label').should(
    have_in_text_the_int_number_more_than(100)
)
'''
# You even can create your own project_package/selene_extensions/have.py
# with the following content:

from selene.support.conditioins.have import *

def int_number_more_than(number) -> Condition[Element]:
    def fn(element: Element) -> None:
        text = element.get(query.text)
        parsed_number = my_int_from(text)
        if not parsed_number > number:
            raise AssertionError(
                f'actual text was: {text}'
                f'with parsed int number: {parsed_number}'
            )
    return ElementCondition(
        f'has in text the int number more than: {number}', 
        fn
    )
    
# And then in your test:

from project_package.selene_extensions import have

browser.element('#price-label').should(have.text('Price: ') \
    .should(have.int_number_more_than(100))

# i.e. using it same style as in selene,
# with also access to all original selene conditions
'''

# Such condition-based alternative to the original `assert price > 100` is less fragile,
# because Python's `assert` does not have "implicit waiting",
# while Selene's `should` command does have ;)

# Furthermore, the good test is when you totally control your test data, 
# and the code like below:

product = browser.element('#to-remember-for-future')

product_text_before = product.get(query.text)
price_before = my_int_from(product_text_before)

... # some test steps

product_text_after = product.get(query.text)
price_after = my_int_from(product_text_after)

assert price_after > price_before

# – normally, should be refactored to something like:

product = browser.element('#to-remember-for-future')

product.should(have.text('100$'))

... # some test steps

product.should(have.text('125$'))


###############################################
# You might also think you need something like:

from selene import query

if browser.element('#i-might-say-yes-or-no').get(query.text) == 'yes':
    ...  # do something...

# Or:

from selene import query

if browser.all('.option').get(query.size) >= 2:
    ...  # do something...

# – maybe, one day, you really find a use case:)

# But for above cases, probably easier would be:

if browser.element('#i-might-say-yes-or-no').wait_until(have.text('yes')):
    ...  # do something

...

if browser.all('.i-will-appear').wait_until(have.size_greater_than_or_equal(2)):
    ...  # do something

# Or, by using non-waiting versions, if "you are in a rush:)":

if browser.element('#i-might-say-yes-or-no').matching(have.text('yes')):
    ...  # do something

...

if browser.all('.i-will-appear').matching(have.size_greater_than_or_equal(2)):
    ... # do something

Tutorials

TBD

More Examples

TBD

Contribution

see CONTRIBUTING.md

Release Workflow

see Release workflow

Changelog

see CHANGELOG.md

selene's People

Contributors

ak40u avatar akioyuki avatar aleksandr-kotlyar avatar bbartok76 avatar dependabot[bot] avatar dskard avatar dunossauro avatar gospodinkot avatar hotenov avatar igorvlasovkram avatar initial1ze avatar jacekziembla avatar kainjazz avatar kianku avatar ksmyshlyaev avatar kylefix avatar maratori avatar pupsikpic avatar rashevskyv avatar rina-tenitska avatar rodinant avatar roman-isakov avatar sergeypirogov avatar simonbaars avatar svetlanaponomarenko avatar vanderloos avatar vfalco02 avatar vikilib avatar yashaka avatar ysparrow avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

selene's Issues

No ability to get page url

For example I want to check page url:

main_page = LoginPage().open().login("admin","admin")

main_page.url.should_be("blablabla")

I didn't found the way in selene to get current page Url. Maybe we need to add such method

Add logging ability (the same way as Selenide)

It would be great if Selene has ability to collect event logs like it works in Selenide (package com.codeborne.selenide.logevents).

Nice to have:

  • Log common actions (visit, search for element, click, setting value to element, assertions)
  • Interface or another abstraction to implement your own logger (like com.codeborne.selenide.logevents. LogEventListener)
  • Implementation by default to display events in stdout (use print() or logging)

Review locator description implementation

Currently they are a bit over-complicated:

for element found by: By.Selene: (By.Selene: (<selenium.webdriver.firefox.webdriver.WebDriver (session="7b958441-d75f-3741-acdb-6878cdae507b")>).find_all(('css selector', '.will-appear'))).find_by(exact_text expecting: Kate)

make SeleneDriver to expose properties where WebDriver has them

Currently for all cases where WebDriver defines properties, SeleneDriver defines methods.
e.g.:

driver = FirefoxDriver()
...
assert driver.current_url == ...

but

driver = new SeleneDriver(FirefoxDriver())
...
assert driver.current_url() == ...

So the idea is to make SeleneDriver have same properties as WebDriver.

The fix should lie in the delegation.py implementation.

fix "double timeout" waiting for complicated locators

Example of test (from https://github.com/yashaka/selene/blob/master/tests/integration/error_messages_test.py) that shows the problem:

def test_inner_selement_search_fails_with_message_when_implicitly_waits_for_condition_mismatch_on_parent_element():
    GIVEN_PAGE.opened_with_body('''
    <div id='hidden-container' style='display:none'>
        <button id='button'>You still can't click me, ha ha! :P</button>
    </div>
    ''')
    config.timeout = 0.1

    with pytest.raises(TimeoutException) as ex:
        s('#hidden-container').element('#button').click()

    # todo: consider removing second entrance of "failed wile waiting..." from the error message
    assert exception_message(ex) == \
        ['Message: ',
         '            failed while waiting 0.1 seconds',
         '            to assert Visible',
         "            for element located by: ('css selector', '#hidden-container').find(('css selector', '#button')),",
         '',
         '            reason: Message: ',
         '            failed while waiting 0.1 seconds',
         '            to assert Visible',
         "            for element located by: ('css selector', '#hidden-container'),",
         '',
         '            reason: Condition Mismatch']

test passes, but shows that it waited twice during 0.1 seconds...

Elements overlapping : Element is not clickable at point. Other element would receive the click

Hello, dear yashaka!

I have an issue with clicking on some elements, which overlapping by others in Chrome:

selenium.common.exceptions.WebDriverException: Message: unknown error: Element is not clickable at point (885, 403). Other element would receive the click: <iframe title="Каталог государственных сайтов" scrolling="no" frameborder="0" allowtransparency="0" style="background: none 0px 0px repeat scroll transparent !important; border: 0px !important; border-radius: 0px !important; bottom: 0px !important; box-shadow: none !important; box-sizing: content-box !important; cursor: auto !important; float: none !important; margin: 0px !important; max-height: none !important; max-width: none !important; min-height: 0px !important; min-width: 0px !important; opacity: 1 !important; overflow: hidden !important; outline: none !important; padding: 0px !important; right: auto !important; transform: none !important; visibility: visible !important; zoom: normal !important; top: 0px !important; left: 0px !important; height: 638px !important; width: 1025px !important; position: absolute !important; z-index: 2147473647 !important;"></iframe>
(Session info: chrome=54.0.2840.59)
(Driver info: chromedriver=2.23.414176 (4eb5bd96518311cc565323e59f08c6cfc140dcae),platform=Linux 4.2.0-35-generic x86_64)

In our testing framework we are using action chains a lot, to click directly in point of element:

actions().move_to_element_with_offset(to_element=element, xoffset=xoffset, yoffset=yoffset).click().perform()

And that's work good, and stable in Chrome 54 and FF 48.

Is it good idea to implement more methods in SElement using action chains, to give precise control over element actions? Or is it an issue of not assuring any condition?

Here some minimal working environment to catch error:
https://gist.github.com/lulunevermind/ac8e829fa5394ee3b899df7b743d3bd1

consider adding SeleneCollection#each method

GIVEN html:

<div>
   <div class="parent"> <div class="inner"> ... </div> ... </div>
   <div class="parent"> <div class="inner special"> ... </div> ... </div>
   <div class="parent"> <div class="inner special"> ... </div> ... </div>
</div>

THEN find all .parent where .inner is also .special by

ss(".inner").filtered_by(have.css_class("special")).each(by_xpath(".."))

Find By alternative solution

It's possible to use selenium's By.* instead on customs like s(locator, by=By.CSS). So, CSS is default. And if you have to use any other: s("//xpath", By.XPATH).
Also it's possible to make something like that: s(xpath("//xpath")).

Make a smart scrolling to the element

If you switch between screen resolutions (13' and 23' etc.), some tests may fail because an element is visible for 23' and isn't visible for 13'. So, as one of way to handle this, we can try to catch errors with invisible elements and try to scroll to them.

consider adding SeleneCollection#size in addition to len(SeleneCollection)

This will not be idiomatic Python, but actually I woul name this a Python's quirk...

Guido told that Python is object oriented, but this code "len(collection)" does not looks like object oriented... :) And is not easy enough for beginners...

I would prefer "put dot and see what it has" over "go to Python docs and learn all python methods..."

consider making all actions to return "cashed" version of SeleneElement

In code like:

s("#element").hover().click()

there is actually no need to refind element after hover...
So why not optimize Selene here and make it more efficient...

Moreover, even if we need to find some "inner" element after "first" action:

s("#element").hover().element(".inner").click()

we actually do not need a lazy proxy object for element(".inner")
just because nobody will save something like: s("#element").hover().element(".inner") to the variable...

This mean, that for this case, we need some alternative to the s.element method, that will do exact search, not create an "element finder" proxy object...

And here the good candidate for this alternative may be a method with the name "find":

s("#element").hover().find(".inner").click()

If used even before any action... This method should do exact search, and return cached element...

Add ability to set up custom time of waiting for element loading

Before make any action with element, selene waits for config.timeout seconds to load an element. But it's possible that for some elements need to set up other timeout. For example, some ADJX request is very slow and config.timeout is not enough.
It can be implemented as:
s(".css", load_during=15) or
s(".csss"). load_during(15) or anything like this.
Finally, this means selene will wait up to specified time before make some action.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.