UI modeling¶
Introduction¶
This guide shall explain you how does our framework represents things in ManageIQ and its UI and other endpoints.
Libraries¶
We use a couple of libraries we designed based on our experience and these libraries work together to bring us a good developer experience:
Navmazing - A UI navigation framework which registers navigation locations to classes that represent objects or their collections. We recommend reading the documentation on the PyPI package page. Referred to as NM.
Note
Responsible person: psavage
Widgetastic - Page Object Model on steroids. Allows mostly declarative specification (Django model inspired) of how does the UI look, what things are present, provides unified read and fill interface, rich logs and other useful magic. Referred to as WT.
Note
Responsible person: mfalesni
Sentaku - Library that allows you to create an object which will have multiple implementations of underlying methods, switching the implementations based on context. Curently (Oct 2017) being slowly rolled out in certain parts.
Note
Responsible people: ronny, psavage
Another important concept is Designing Models which you must read if you want to create new models as this guide assumes you are aware of these concepts already.
High-level process description¶
Here I will briefly describe the usual code flow how things interact. Think of it as a typical use case. Don’t worry if you don’t understand some of the concepts presented here, they will be explained later.
- A test wants to create something in the UI (
things_collection.create(...)
)- The
create
method needs to go to theAdd
page ofthings_collection
, therefore it asks NM- NM starts working by instantiating the final step for target location, then the step’s WT view and it checks whether it already is there. The view does not have to be specified, but it is specified for most of our navigation steps.
- If it is not there, it tries to do the same to the prerequisite location recursively until it reaches a location that is on screen now or some of the root locations, like LoginPage or BaseLoggedInPage which are the usual “safe points”.
- If there are any steps to be made afterwards, it starts backtracking back to the target location by executing the steps’ code.
- NM‘s navigation returns the WT view of the last step.
- With the WT view returned from NM knowing that we indeed are on that particular
location, what usually happens next is that the
create
method dumps a dictionary of data into view’s fill method.- WT‘s view fill goes through the view and fills each widget that was specified in the
dictionary with the appropriate value. Widgets whose names weren’t in the keys will be
skipped.
None
values are also skipped.
- WT‘s view fill goes through the view and fills each widget that was specified in the
dictionary with the appropriate value. Widgets whose names weren’t in the keys will be
skipped.
fill
tells you if it changed anything or not. You then decide what to do next. Usually what happens next is that some button is clicked and then a flash messages are asserted. You can also create an instance of another view if you know that by clicking the button you will get bumped somewhere else and then assert whether that location is displayed or not- Finally,
create
callsinstantiate
on the collection which takes all the required values and creates an instance of the thing that was just created in the UI
- The
Some caveats:
- There is a big difference between
int
andstr
. If you insert a numeric value into an ordinary input, you need to have it as a string. Of course if you use a widget where the only sensible values are numbers, then it will be operating with integers. But if values come eg. from YAML, then the numeric value is automatically considered a number unless quoted. - Remember that Widgetastic doesn’t touch fields which already have the required value. In that case
fill
returns False so you know that nothing was changed.
Navmazing¶
For the initial explanation on Navmazing, read the guide on PyPI.
We beef Navmazing up locally with extra features, like error detection and Widgetastic integration in ManageIQ tests. The important ones for developers are Widgetastic integration and object injection.
If you specify VIEW
on the navigation step class, it then has a default behaviour with
the am_i_here
method, which you don’t need to write then. You can also use the view
property
which gives you an instance of that view class. For writing steps, the most useful thing you will
use is the prerequisite_view
as you need to make that particular step from the previous location
and not from the one you are writing the step in.
You usually navigate withing the context of some object. Navmazing then passes the object into the
Widgetastic view as well - self.context["object"]
. You then use it to assert things in the UI
with the data pulled from the object, like titles and so on.
Let’s look at an example here:
@navigator.register(NamespaceCollection)
class Add(CFMENavigateStep):
VIEW = NamespaceAddView
prerequisite = NavigateToAttribute('parent', 'Details')
def step(self):
self.prerequisite_view.configuration.item_select('Add a New Namespace')
This piece of code tells us these things:
- We register this location against
NamespaceCollection
. - We name the location
Add
. - The location’s UI is represented by
NamespaceAddView
. - In order to get to this location, we first need to go to the
Details
location of this object’s parent (self.parent
). This is the declarative recursive relationship mentioned in the high level process description. - In order to get from parent’s details to the dialog for adding a new namespace, we need to click
on the “Add a New Namespace” item in the
Configuration
dropdown. We useprerequisite_view
because on the actualAdd
view there is no Configuration dropdown.
The step definition can also contain the resetter
method. That one is used when you have pages
that remember certain settings and you want to make sure, that before the step executes, the page
is in a known state.
The “root” navigation locations, like login page, dashboard, the initial pages of all menu item
destinations ... these are registered against so-called Server
instance. When dealing with the
appliance object, it is appliance.server
if you need to navigate to one of those.
If you are going to implement new models, make sure you look around for existing navigation locations that you may build on top of.
Warning
The actual step
method should ideally contain one singular action, like clicking
a button or selecting a thing from tree. This is not a hard requirement, but unless it is needed,
we should avoid it.
Also when picking a prerequisite, try avoiding unnecessary steps. And example would be a page with a tree on left side. If you know the tree path of your model object and the object has a parent, you don’t need to invoke parent’s details first and then go on with the actual object’s details, because it will select an item in the tree twice while you can just go straight for the object’s item since selecting the parent is not a prerequisite for getting there.
Widgetastic¶
For the initial explanation on Widgetastic, read the guide on PyPI.
If you know Django models, then Widgetastic should be very familiar and intuitive for you. If not, it should be intuitive.
Let’s start with a code sample:
from cfme.base.login import BaseLoggedInPage
class SomeForm(BaseLoggedInPage):
title = Text('#title_text')
name = Input(name='field_name')
type = BootstrapSelect(id='field_type')
@property
def is_displayed(self):
return self.title.text == 'Editing "{}"'.format(self.context['object'].name)
Note
All views (for the main UI) except the login page descend from BaseLoggedInPage
in
some manner.
In Widgetastic, interactive and non-interactive UI elements are represented by so-called widgets, which are classes that implement interaction with the UI element in a consistent manner.
Widgets are usally grouped on a View
, which itself is also a widget, so you can nest the
structure if you need.
Each widget has its own rules concerning constructor parameters, you should read the documentation for each of them.
Any sequential actions happen in the order of definition on the view. So if you fill some values by
feeding a dictionary into the view.fill(...)
method, it will always follow the order of
definition.
If you have a nested view and the order is important, you need to decorate it with View.nested
,
otherwise the view will be out of the assumed order.
You can fence the element lookup on the view by setting ROOT
to a locator. Then before any
element is looked up as a result of interaction of any of the widgets on the particular view, the
ROOT
element is looked up first and the following lookup happens in context of it. Imagine the
use case as if you had multiple boxes that have IDs and then have the same classes and no IDs on
things inside. This way you can divide and control.
Warning
If you want to instantiate a view for testing, use appliance.browser.create_view
and pass the view class and optionally the object that the view uses for asserting. If you want to
instantiate a plain widget, grab a Widgetastic’s Browser
(appliance.browser.widgetastic
)
and put it as the first argument before any widget’s init parameters. If you don’t pass the
browser, it will not work (I told you so).
Note
If you want to understand why, read about Python’s descriptors. If you instantiate a
Widget
without the browser or a parent widget as a first argument, the Widget
class
recognizes it and instead of instantiating an instance of that particular
Widget
it creates and returns an instance of WidgetDescriptor
that remembers the widget
class, args and kwargs and it then instantiates the true widget instance upon accessing on the
parent’s instance using descriptor protocol. Try accessing the same widget on a class and on the
instance. See the difference?
Apart from this simple usage, Widgetastic allows you to do a number of advanced constructs that are described in Widgetastic’s guide. You should familiarize with them. Especially with Version picking which is probably the most used feature.
Sentaku¶
WIP.