Giter Club home page Giter Club logo

widgetastic.core's Introduction

widgetastic.core

Python supported versions

image

image

image

Documentation Status

Widgetastic - Making testing of UIs fantastic.

Written originally by Milan Falesnik ([email protected], 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!

Projects using widgetastic

Installation

Contributing

  • Fork
  • Clone
  • Create a branch in your repository for your feature or fix
  • Write the code, make sure you add unit tests.
  • Use pre-commit when committing to enforce code style
  • Run pytest to run unit tests
  • Push to your fork and create a pull request
  • Observe checks in GitHub for further docs and build testing

widgetastic.core's People

Contributors

bsquizz avatar dhlavac avatar digitronik avatar gdrosos avatar ikerreyes avatar izapolsk avatar john-dupuy avatar jrusz avatar lightofheaven1994 avatar mayurilahane avatar mfalesni avatar mkoura avatar mshriver avatar otsuman avatar pre-commit-ci[bot] avatar psav avatar psegedy avatar ronnypfannschmidt avatar rsnyman avatar tpapaioa 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

Watchers

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

widgetastic.core's Issues

Two different modes: pedantic(slow)/lax(fast)

I was thinking about this before, widgetastic is currently very pedantic with page checking so it is relatively slow. There is a possibility to keep these check obligatory only after events like clicking and the rest would be invoked only if the selenium step triggers an error - like element lookup or such - and after the check the step would be repeated once more. The modes would be switchable.

This should only probably concern changes inside Browser class.

locator arg placement inconsistency across widgets

Some of widgets are accepting locator arg as first arg, e.g. Text(), Table(), Select(), etc.
Others (usually inputs) - as a third or forth, the first one for them is name. Few examples are TextInput(), Checkbox().
This brings some confusion, as sometimes i have to specify locator= and sometimes i don't:

class MyView(View):
    table = Table('//locator')
    selected = Checkbox(locator='//locator')
    flash = FlashMessages('//locator')
    file = FileInput(locator='//locator')
    message = Text('//locator')
    input = TextInput(locator='//locator')

If i knew locator is always third arg - i'd always define my widgets like Text(locator='//locator'). If i knew it's always first - i'd never have to specify locator= at all. Current state of things just provokes to make common mistake and introduces some extra time for debugging :)

Provide method to pull browser debug console content

Firefox/Chrome/Edge provide a JS console in their debug/developer consoles (F12 launched).

If possible, the default Browser/plugin should provide a method to pull the console content in order to aid in UI debugging.

Support relative paths

We need to have support for relatives path in locators like:

class test(Widget):
    ROOT = ParametrizedLocator("{@my_custom_var}/a")
    CHILD_1 = '{}/a/span'.format(ROOT)
    CHILD_2 = '{}/button/span'.format(ROOT)
    def __init__(self, parent, my_custom_var, logger=None):
        Widget.__init__(self, parent, logger=logger)
        self.my_custom_var= my_custom_var
    def fill(self, values):
        access ROOT
        access CHILD_1
        access CHILD_2

Switchable nested views based on value of another widget

Sometimes eg. a select dropdown changes the next part of a form upon change. There is a possibility of creating a proxy that will, upon accessing itself, check the value of the reference widget and pick the correct view that was registered with the proxy.

Example:

class AView(View):
    reference_widget = SomeWidget()
    
    form_remainder = ChangingViewProxy(value='reference_widget')

    @form_remainder.register('value 1')
    class ViewForValue1(View):
        w = Widget()

    @form_remainder.register('value 2')
    class ViewForValue2(View):
        w = AnotherWidget()

The next step of this would be fully dynamic views, but that will require more thinking about design and implementation. This, so far, requires that when you fill, you have to nest the remaining values into the form_remainder (if taking this sample's terminology). The fully dynamic form would take care of it in single level.

[RFE] Add ability to define DEFAULT_LOCATOR for widget

With views and ROOT approach, we have a lot of situations when only 1 widget of kind is present in single view. Or different use case - we have widgets, which in 99% cases have the same locator, e.g. some .//div[contains(@class, "progress progress-striped")].
For such cases it's very handy to be able not to specify any locator at all and have some default one used instead:

class MyView(View):
    progress = ProgressBar()

But to be able to do that, i have to override widget's __init__ each time with smth like that:

        def __init__(self, parent, locator=None, logger=None):
        """Provide common progress bar locator if it wasn't specified."""
        Widget.__init__(self, parent, logger=logger)
        if not locator:
            locator = './/div[contains(@class, "progress progress-striped")]'
        self.locator = locator

It would be really nice to have such ability out of the box.

My suggestion - we could define some class attr for widget, e.g. DEFAULT_LOCATOR and slightly update GenericLocatorWidget with smth like:

-    def __init__(self, parent, locator, logger=None):
+    def __init__(self, parent, locator=None, logger=None):
         Widget.__init__(self, parent, logger=logger)
+        if not locator:
+            locator = getattr(self, 'DEFAULT_LOCATOR', None)
         self.locator = locator

This way i could optionally specify class attr DEFAULT_LOCATOR for my widget and i wouldn't have to override entire method each time:

class ProgressBar(GenericLocatorWidget):
    DEFAULT_LOCATOR = './/div[contains(@class, "progress progress-striped")]'
    # ...

Thoughts, other ideas?

As a user I want to see full locator path at any given moment

We should be able to see both in logs and in debug full locator description no matter where I am currently executing my code. For example:

ROOT = ".//a"
CHECKBOX = ".//input"

So for checkbox I should see not only ".//a//input", but full path like "//view_or_whatever//a//input"

Including another view like it was a part of a view in the place included

class SomeView(View):
    x = Widget()
    y = Widget()

class ViewA(View):
    a = Widget()

    someview = View.include(SomeView)

    b = Widget()

# Will be equivalent to:
class ViewA(View):
    a = Widget()

    x = Widget()
    y = Widget()

    b = Widget()

The view that contains included views will look up undefined attributes in the included view in the order of definition. fill and read operation will appear to have flat structure instead of nested one.

partial_match does not support parenthesis () in object

During instance reconfigure, user is required to supply a new flavor
Navigate to instance details and them got to Configuration > Reconfigure this instance

The name of the flavor appears along with it;s properties like this:
m12.tiny (1 CPU, 0.0625 GB RAM, 3.0 GB Root Disk)

It seems that because parenthesis () are special characters, partial match is failing

So, view.form.flavor.fill('m12.tiny (1 CPU, 0.0625 GB RAM, 3.0 GB Root Disk)') is working
But: view.form.flavor.fill('m12.tiny') is not

We need the ability to partially match string with parenthesis ()

Table widget: tbody row indexing starts from 1 when there's a row in table header

Reproduced on following table: https://gist.github.com/abalakh/277f5561ddd1a0bd010bb06104c515b0
I think it should be reproducible on following simplified table (haven't tried though):

<table>
    <thead>
        <tr>
            <th><input type="checkbox"></th>
            <th><span>RPM Name</span></th>
            <th><span>Architecture</span></th>
            <th><span>Version</span></th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><input type="checkbox"></td>
            <td>walrus</td>
            <td></td>
            <td>Version 0.71-1</td>
            <td>
                <button type="button">
                    <span>Edit</span>
                </button>
            </td>
        </tr>
    </tbody>
</table>

The table has only 1 row in <tbody>, but there's also a row in <thead>.
When i was using older widgetastic (0.21.1) table.rows() returned 1 row with row.index == 0 for such table, after upgrading to 0.21.6 there's still 1 row but row.index == 1.
I think it's most likely due to extra row inside <thead>, and the regression was introduced in #104

The `before_fill` return should modify `fill.was_changed`

I think that when a before_fill method is implemented for a view, its return value should modify was_changed in View.fill().

https://github.com/RedHatQE/widgetastic.core/blob/master/src/widgetastic/widget.py#L744

Should be a 1-line-ish change, and covers when a before_fill modifies the form.

For example, a view with a dropdown that modifies the remaining portion of the form. If this dropdown is filled in before_fill as a way to ensure it is always selected before the rest of the form is filled, and no other changes are made as a result, the caller's fill() returns False even though the form was modified.

Thoughts @mfalesni?

TableRow.__locator__ Breaks on DynamicTable Editing

In extending Table/TableRow for a DynamicTable to implement row_add and row_save, it has been found that the TableRow.__locator__ method is not functional for all tables in the MIQ UI.

Investigation points to the static +1 that exists in the locator. For some tables this is functional, for others it overshoots the index, where there are only 2 rows it looks for the 3rd.

https://github.com/RedHatQE/widgetastic.core/blob/master/src/widgetastic/widget.py#L1142

ConditionalView feature request: use default if control isn't present

I'm implementing Base View which will be used in a lot of places. This view uses ConditionalView.
I suspect there will be places where reference control for Conditional View won't be present.
It would be good if ConditionalView chose default view for cases when reference control was absent.

Table widget: Add support of rowspans

That is more expanding functionality of table widget than a bug
So when we have a rowspan attribute for td we get into situation when we have for example 5 columns for one row and only 4 for others. That get us into exception when /td[5] cannot be found

image

<table class="table table-fixed" id="inherited_puppetclasses_parameters">
  <thead class="white-header">
    <tr>
      <th class="col-md-2">Puppet Class</th>
      <th class="col-md-2">Name</th>
      <th class="col-md-2">Type</th>
      <th class="col-md-5">Value</th>
      <th class="col-md-1 ca">Omit</th>
    </tr>
  </thead>
  <tbody>
    <tr id="puppetclass_6_params[29]" class="fields ">
        <td rowspan="7" class="ellipsis" data-original-title="" title="">ui_test_variables</td>
        <td class="ellipsis param_name" data-original-title="" title="">
             bdAnZDTEij
        </td>
        <td class="ellipsis" data-original-title="" title="">
            Smart Variables
        </td>
        <td>
            <div class="input-group">
            <span class="input-group-addon"><a rel="popover" data-content="<b>Description:</b> <br/>
            <b>Type:</b> string<br/>
            <b>Matcher:</b> Default value<br/>
            <b>Inherited value:</b> NzcbGqKzEN" ...
              <span class="input-group-btn">         
              <button name="button" ...</button>
              <a ... data-original-title="Override this value">... </a>
              </span>
            </div>
         </td>
         <td class="ca">
           <input value="29" ...>
         </td>
    </tr>
    <tr id="puppetclass_6_params[27]" class="fields ">
        <td class="ellipsis param_name" data-original-title="" title="">
            GVdtHhlbUT
        </td>
        <td class="ellipsis" data-original-title="" title="">
          Smart Variables
        </td>
        <td>
           ...
        </td>
        <td class="ca">
          ...
        </td>
    </tr>
  ...

  </tbody>
</table>

"Ignore" decorator

Write a decorator that will make widgetastic widget classes wrapped so when they are defined on a widget, they won't be detected as widgets and when accessed will return the class itself, useful for definitions.

E TypeError: __init__() got an unexpected keyword argument 'prov_class'

I am getting this problem with HEAD on 3ec95dd518d28b800a3f2c99fc8bcf8f7ca5c7ee

>       network_provider = collection.all()[0]

cfme/tests/networks/test_sdn_inventory_collection.py:132: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cfme/networks/provider/__init__.py:294: in all
    provider=self.filters.get('provider')))
cfme/modeling/base.py:119: in instantiate
    return self.ENTITY.from_collection(self, *args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

cls = <class 'cfme.networks.provider.NetworkProvider'>
collection = NetworkProviderCollection(filters={'provider': AzureProvider(endpoints={'defau...d-8922-c9b8e2c3f100', subscription_id='c9e72ccc-b20e-48bd-a0c8-879c6dbcbfbb')})
k = ()
kw = {'name': 'azure Network Manager', 'prov_class': <class 'cfme.networks.provider.NetworkProvider'>, 'provider': AzurePro...cloud.provider.azure.AzureEndpoint o...0ed-8922-c9b8e2c3f100', subscription_id='c9e72ccc-b20e-48bd-a0c8-879c6dbcbfbb')}

    @classmethod
    def from_collection(cls, collection, *k, **kw):
>       return cls(collection, *k, **kw)
E       TypeError: __init__() got an unexpected keyword argument 'prov_class'

I found out it is introduced with 0284b75a90.

Support fill by value for downdowns

In the old framework we had the ability to fill a Select/Downdown by Value as well as by Text. In WT we can currently only fill by Text. The consequence is that some dropdowns are set up in such a way that they are showing a name and then another name in brackets, like name (name), or something similar. To get around this, the team has implemented a partial match, but the problem with this is that if another entry is there with the same name or beginning with the same name, then it could do an erroneous match. I propose that we introduce a by-value/by-text system so that we can setup a widget to be by-text by default and then allow the user to fill it with by-value to override, and of course the opposite is also true. This isn't hitting us yet, but I believe it will do sooner or later.

Included views do not call child_widget_accessed on the hosting view.

Accessing a widget defined inside an included view does not invoke child_widget_accessed on the hosting view where the includer was placed.

Could be worked around by having the View being included define child_widget_accessed calling to the parent view's child_widget_accessed if use_parent is true.

How to navigate to a given view?

In the example on the readme it shows how to build a view and access some of its elements but it does not show how to navigate to that given view. Should I use webdriver to navigate or widgetastic offers some way to navigate.

Smart is_displayed

Currently, we have is_displayed that only returns true or false without further explanation. It would be helpful to see which exact part of the is_displayed failed. Therefore I would propose this:

  • Create a descriptor that will be set as is_displayed instead of the property and will hold a set of rules/checks for the displayed check.
  • The descriptor would implement __nonzero__ to act as a boolean value, therefore making it usable in if and such expressions.
  • On the __nonzero__ resolution it would store the result of each partial check in some sort of dictionary.
  • It would provide a method that would either directly raise an exception or just provide an information about those checks that did not pass.
if not view.is_displayed:
    raise Exception(view.is_displayed.why)

Or something like that, this is just a concept. (the sample provided would have caching problems)

The rules would be flexible - a string could represent either widget name (it would call is_displayed on that one) or if it would not be a widget then it would assume it is an attribute to be read. It could also provide some basic checkers like you could do a value comparison ... the discussion is open.

Parametrized widgets

We already have parametrized views but a parametrized Widget would also be nice.

Current code sample:

   def _remove_recipient(self, email):
        Text(self, ".//a[text()='{}']".format(email)).click()

What I would imagine:

class Foo(View):
    w = Text(ParametrizedLocator('//something[@important={name|quote}]'))

v = "instance of Foo"
# Widget recognizes that there is a parametrized locator in args and returns a proxy
v.w == "some proxy"
v.w.read() # => triggers an error because it is a proxy
v.w(name='foobar').__locator__() == '//something[@important="foobar"]'
v.w(name='foobar').read() == "something"

widgetastic.utils.crop_string_middle is py3-incompatible

Reproducer:

Py2:

Python 2.7.13 (v2.7.13:a06454b1afa1, Dec 17 2016, 12:39:47)

In [1]: from widgetastic.utils import crop_string_middle

In [2]: crop_string_middle('9311026077199426039824341772797765984569479798833999
   ...: 0444')
Out[2]: u'93110260771994...797988339990444'

Py3:

Python 3.6.4 (v3.6.4:d48ecebad5, Dec 18 2017, 21:07:28)

In [1]: from widgetastic.utils import crop_string_middle

In [2]: crop_string_middle('9311026077199426039824341772797765984569479798833999
   ...: 0444')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-90bd37e1b4c8> in <module>()
----> 1 crop_string_middle('93110260771994260398243417727977659845694797988339990444')

~/workspace/py3a/lib/python3.6/site-packages/widgetastic/utils.py in crop_string_middle(s, length, cropper)
    596         return s
    597     half = (length - len(cropper)) / 2
--> 598     return s[:half] + cropper + s[-half - 1:]
    599
    600

TypeError: slice indices must be integers or None or have an __index__ method

Root cause:

This line:

half = (length - len(cropper)) / 2

It returns float in py3, when slice expects int. For int compatible with both py2 and py3, // should be used instead:

half = (length - len(cropper)) // 2

Since crop_string_middle is used in browser.text(), this bug blocks any testing in py3 in case any line length is >32 chars.

Get wrong browser object for non top view in hierarchy

What we do:

class AirgunBrowser(Browser):
    def wait_for_element(...):
        return super(AirgunBrowser, self).wait_for_element(...)

Then we do:

class MyView(View):
    def is_displayed(self):
        return self.browser.wait_for_element(...)

class MyOtherView(View):
    class MyTab(Tab):
        view = MyView()

In result when we call view we have wrong browser object type in its is_displayed method:
self.browser == BrowserParentWrapper not AirgunBrowser that will get us to
super(type, obj): obj must be an instance or subtype of type exception in wait_for_element method

Fix doc build

Hello guys,

My personal Jenkins is still building the documentation and I'd like to remove that job from it. Can you take a look at making the build work on RTD or anywhere else?

@psav @RonnyPfannschmidt

How to use CSS selectors?

Is it possible to use CSS selectors to locate elements? If not, are you planning to support that?

Table widget: incorrect row index processing when using filters for table with header in rows

I have the following table:
image
What makes it special is headers being located inside table body (which is still all right and should be supported according to class Table code):

<table class="table table-bordered table-striped table-two-pane">
  <tbody><tr>
    <th><a href="/discovery_rules?order=name+ASC">Name</a></th>
    <th><a href="/discovery_rules?order=priority+ASC">Priority</a></th>
    <th><a href="/discovery_rules?order=search+ASC">Query</a></th>
    <th>Host Group</th>
    <th>Hosts/Limit</th>
    <th><a href="/discovery_rules?order=enabled+ASC">Enabled</a></th>
    <th></th>
  </tr>
    <tr>
      <td class="col-md-3 display-two-pane"><a data-id="aid_discovery_rules_1-test_edit" href="/discovery_rules/1-test/edit"><span>test</span></a></td>
      <td>1</td>
      <td><span>cpu_count = 1</span></td>
      <td></td>
      <td>0 / 0</td>
      <td>true</td>
      <td><div class="btn-group"><span class="btn btn-sm btn-default"><a data-id="aid_discovered_hosts" href="/discovered_hosts?search=cpu_count+%3D+1">Discovered Hosts</a></span><a class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" data-id="aid_not_defined" href="#" aria-expanded="false"><span class="caret"></span></a><ul class="dropdown-menu pull-right"><li><a data-id="aid_hosts" href="/hosts?search=discovery_rule+%3D+%22test%22">Associated Hosts</a></li> <li><a data-id="aid_discovery_rules_1-test_disable" data-confirm="Disable rule 'test'?" href="/discovery_rules/1-test/disable">Disable</a></li> <li><a data-confirm="Delete rule 'test'?" data-id="aid_discovery_rules_1-test" rel="nofollow" data-method="delete" href="/discovery_rules/1-test">Delete</a></li></ul></div></td>
    </tr>
</tbody></table>

When i play with rows without any filters, everything works as expected:

> view.table.row().read()
{'Name': 'test', 'Priority': '1', 'Query': 'cpu_count = 1', 'Host Group': '', 'Hosts/Limit': '0 / 0', 'Enabled': 'true', 6: ['Associated Hosts', 'Disable', 'Delete']}

But when i try to apply the filter, e.g. by name, i receive an exception:

> view.table.row(name='test').read()
{NoSuchElementException}Message: Could not find an element './tbody/tr[3]|./tr[not(./th)][3]'

According to exception message, widgetastic tries to fetch the third row, when there's only 2 inside that table.
Now just to make sure:

> view.table.row().index
1
> view.table.row(name='test').index
2

There's definitely an issue with row index calculation.

Element not found with Zalenium webdriver and scrolled down

On a page written in react, when executing tests on remote Zalenium webdriver, and the opened page is scrolled down that some particular Button is not visible, that button.click can not find the button by locator.
But when scrolling the page up, button.click works fine.

Added this workaround: https://github.com/Kiali-QE/kiali-qe-python/blob/master/kiali_qe/components/__init__.py#L461
Happened on "Metrics Settings" checkbox filter button.

screenshot from 2018-11-29 13-09-55

Support multiple widgets in single table cell

In some cases we have multiple widgets in single table cell - e.g. 2 buttons in 'Actions' column, but currently Table widget supports only 1 widget per column.
We can try to workaround that by moving those widgets into separate view and assigning that view to table cell, but that will create some boilerplate (not to mention no one tried it out so no guarantees it will actually work :) ).
Ideally i just want to be able to specify some dict with widgets per table column, e.g:

resources = SatTable(
    locator='//table',
    column_widgets={
        'Version': Text('.//a'),
        'Status': PublishPromoteProgressBar(),
        'Actions': {'clone': Button(id='clone'), 'delete': Button(id='delete')}
    },
)

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.