redhatqe / widgetastic.core Goto Github PK
View Code? Open in Web Editor NEWMaking testing of UIs fantastic.
License: Other
Making testing of UIs fantastic.
License: Other
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"
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
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 ()
Xpaths like this .//text()[normalize-space(.)='some_text']
don't work.
neither documentation not setup mention requred fixture which should come with pytest-localserver package.
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.
Make it possible to designate fields as unimportant for fill, therefore reducing the number of Skipping fill of 'foo' because value was not specified
messages
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.
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.
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.
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.
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.
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
<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>
widgetastic.core/src/widgetastic/widget/table.py
Lines 187 to 190 in 9d94e28
raises an error as the dir method cant be appended
I have the following table:
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.
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.
Probably introduce some kind of ALL
locator that will be detecting all the instances of view and retrieve the parameters for browsing them
Introduce a helper function that will "re-pack" the original logger and append a [xyz]
formatted index/key info at the end.
Use it with table and possibly parametrized views.
Currently, we only collapse args and values of kwargs. It should be possible to selectively enable collapsing of values in whitelisted kwargs values as well (example - table - widgets= kwarg.)
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?
After recent Table widget changes, rows indexing starts from 1, while column indexing starts from 0 which is confusing.
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.
The TODO has been recorded in SummaryTable in MIQ/Integration_tests/widgetastic_manageiq:
https://github.com/ManageIQ/integration_tests/blob/master/widgetastic_manageiq.py#L709
@mfalesni Suggested in PR review this should be implemented at the core and extended to widgetastic_manageiq.
Recording the TODO as an issue for visibility and tasking.
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.
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
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 :)
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"
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')}
},
)
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?
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.
I'd be happy if ConditionalView supported something like the following:
items = ConditionalSwitchableView(reference='parent.toolbar.view_selector')
Is it possible to use CSS selectors to locate elements? If not, are you planning to support that?
We should able to access other instances of ParametrizedView.
More info for this need could be found in this PR: ManageIQ/integration_tests#4769
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:
is_displayed
instead of the property and will hold a set of rules/checks for the displayed check.__nonzero__
to act as a boolean value, therefore making it usable in if
and such expressions.__nonzero__
resolution it would store the result of each partial check in some sort of dictionary.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.
Currently it assumes things, like log level and such.
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
We need to implement support of frames and switching between them.
i have a able where one header row is a plain checkbox without text,
this is takes as "not a header" and then the header indexes are miscalculated in turn
test upcoming
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
So I have encountered this while I was calling fill method of a dropdown menu using partial_match. See the following actions taken in IPython:
http://pastebin.test.redhat.com/546345
I think this illustrates the situation quite well. Bottom line is: When I call fill method with exact string of the desired item, everything works OK. However when I use partial match with path of the desired item, it does not work.
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?
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
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.
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.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.