Skip to main content

Making testing of UIs fantastic

Project description

https://travis-ci.org/RedHatQE/widgetastic.core.svg?branch=master https://coveralls.io/repos/github/RedHatQE/widgetastic.core/badge.svg?branch=master Documentation Status Code issues

Widgetastic - Making testing of UIs fantastic.

Written originally by Milan Falesnik (mfalesni@redhat.com, http://www.falesnik.net/) and other contributors since 2016.

Licensed under Apache license, Version 2.0

WARNING: Until this library reaches v1.0, the interfaces may change!

Introduction

Widgetastic is a Python library designed to abstract out web UI widgets into a nice object-oriented layer. This library includes the core classes and some basic widgets that are universal enough to exist in this core repository.

Features

  • Individual interactive and non-interactive elements on the web pages are represented as widgets; that is, classes with defined behaviour. A good candidate for a widget might be something a like custom HTML button.

  • Widgets are grouped on Views. A View descends from the Widget class but it is specifically designed to hold other widgets.

  • All Widgets (including Views because they descend from them) have a read/fill interface useful for filling in forms etc. This interface works recursively.

  • Views can be nested.

  • Widgets defined on Views are read/filled in exact order that they were defined. The only exception to this default behaviour is for nested Views as there is limitation in the language. However, this can be worked around by using View.nested decorator on the nested View.

  • Includes a wrapper around selenium functionality that tries to make the experience as hassle-free as possible including customizable hooks and built-in “JavaScript wait” code.

  • Views can define their root locators and those are automatically honoured in the element lookup in the child Widgets.

  • Supports Parametrized views.

  • Supports Version picking.

Projects using widgetastic

Installation

pip install -U widgetastic.core

Contributing

  • Fork

  • Clone

  • Create a branch in your repository for your feature or fix

  • Write the code, make sure you add unit tests.

  • Run tox to ensure your change does not break other things

  • Push

  • Create a pull request

Basic usage

from selenium import webdriver
from widgetastic.browser import Browser
from widgetastic.widget import View, Text, TextInput


# Subclass the default browser, add product_version property, plug in the hooks ...
class CustomBrowser(Browser):
    pass

# Create a view that represents a page
class MyView(View):
    a_text = Text('.//h3[@id="title"]')
    an_input = TextInput(name='my_input')

    # Or a portion of it
    @View.nested  # not necessary but you need it if you need to keep things ordered
    class my_subview(View):
        # You can specify a root locator, then this view responds to is_displayed and can be
        # used as a parent for widget lookup
        ROOT = 'div#somediv'
        another_text = Text('#h2')  # Whatever takes a locator can automatically detect simple CSS locators

selenium = webdriver.Firefox()  # For example
browser = CustomBrowser(selenium)

# Now we have the widgetastic browser ready for work
# Let's instantiate a view.
a_view = MyView(browser)
# ^^ you would typically come up with some way of integrating this in your framework.

# The defined widgets now work as you would expect
a_view.read()  # returns a recursive dictionary of values that all widgets provide via read()
a_view.a_text.text  # Accesses the text
# but the .text is widget-specific, so you might like to use just .read()
a_view.fill({'an_input': 'foo'})  # Fills an_input with foo and returns boolean whether anything changed
# Basically equivalent to:
a_view.an_input.fill('foo')  # Since views just dispatch fill to the widgets based on the order
a_view.an_input.is_displayed

Version picking

By version picking you can tackle the challenge of widgets changing between versions.

In order to use this feature, you have to provide product_version property in the Browser which should return the current version (ideally utils.Version, otherwise you would need to redefine the VERSION_CLASS on utils.VersionPick to point at you version handling class of choice) of the product tested.

Then you can version pick widgets on a view for example:

from widgetastic.utils import Version, VersionPick
from widgetastic.widget import View, TextInput

class MyVerpickedView(View):
    hostname = VersionPick({
        # Version.lowest will match anything lower than 2.0.0 here.
        Version.lowest(): TextInput(name='hostname'),
        '2.0.0': TextInput(name='host_name'),
    })

When you instantiate the MyVerpickedView and then subsequently access hostname it will automatically pick the right widget under the hood.

VersionPick is not limited to resolving widgets and can be used for anything.

Parametrized views

If there is a repeated pattern on a page that differs only by eg. a title or an id, widgetastic has a solution for that. You can use a ParametrizedView that takes an arbitrary number of parameters and then you can use the parameters eg. in locators.

from widgetastic.utils import ParametrizedLocator, ParametrizedString
from widgetastic.widget import ParametrizedView, TextInput

class MyParametrizedView(ParametrizedView):
    # Defining one parameter
    PARAMETERS = ('thing_id', )
    # ParametrizedLocator coerces to a string upon access
    # It follows similar formatting syntax as .format
    # You can use the xpath quote filter as shown
    ROOT = ParametrizedLocator('.//thing[@id={thing_id|quote}]')

    # Widget definition *args and values of **kwargs (only the first level) are processed as well
    widget = TextInput(name=ParametrizedString('#asdf_{thing_id}'))

# Then for invoking this:
view = MyParametrizedView(browser, additional_context={'thing_id': 'foo'})

It is also possible to nest the parametrized view inside another view, parametrized or otherwise. In this case the invocation of a nested view looks like a method call, instead of looking like a property. The invocation supports passing the arguments both ways, positional and keyword based.

from widgetastic.utils import ParametrizedLocator, ParametrizedString
from widgetastic.widget import ParametrizedView, TextInput, View

class MyView(View):
    class this_is_parametrized(ParametrizedView):
        # Defining one parameter
        PARAMETERS = ('thing_id', )
        # ParametrizedLocator coerces to a string upon access
        # It follows similar formatting syntax as .format
        # You can use the xpath quote filter as shown
        ROOT = ParametrizedLocator('.//thing[@id={thing_id|quote}]')

        # Widget definition *args and values of **kwargs (only the first level) are processed as well
        the_widget = TextInput(name=ParametrizedString('#asdf_{thing_id}'))

# We create the root view
view = MyView(browser)
# Now if it was an ordinary nested view, view.this_is_parametrized.the_widget would give us the
# nested view instance directly and then the the_widget widget. But this is a parametrized view
# and it will give us an intermediate object whose task is to collect the parameters upon
# calling and then pass them through into the real view object.
# This example will be invoking the parametrized view with the exactly same param like the
# previous example:
view.this_is_parametrized('foo')
# So, when we have that view, you can use it as you are used to
view.this_is_parametrized('foo').the_widget.do_something()
# Or with keyword params
view.this_is_parametrized(thing_id='foo').the_widget.do_something()

The parametrized views also support list-like access using square braces. For that to work, you need the all classmethod defined on the view so Widgetastic would be aware of all the items. You can access the parametrized views by member index [i] and slice [i:j].

It is also possible to iterate through all the occurences of the parametrized view. Let’s assume the previous code sample is still loaded and the this_is_parametrized class has the all() defined. In that case, the code would like like this:

for p_view in view.this_is_parametrized:
    print(p_view.the_widget.read())

This sample code would go through all the occurences of the parametrization. Remember that the all classmethod IS REQUIRED in this case.

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

widgetastic.core-0.4.0.tar.gz (45.5 kB view details)

Uploaded Source

File details

Details for the file widgetastic.core-0.4.0.tar.gz.

File metadata

File hashes

Hashes for widgetastic.core-0.4.0.tar.gz
Algorithm Hash digest
SHA256 96ea7054c7f06891526a6b16331023d72bc540e934289f7d76213c83f26abaec
MD5 c3728dae6f6a01e2fac3ebae0611f795
BLAKE2b-256 50936720d0dce29d541b25583e5441f436da87acd1a9beba57fe62b02e3860d8

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page