Advanced¶
Here you can see how to create a custom widget or create your own colander types with validators.
Encoded values widget¶
Let’s pretend we have to manage a simple key-value widget: a sort of dictionary like structure where both keys and values are string types like shown in the following picture.
Final interaction with the widget¶
For example you might have a I set the encoded values field with:
BDD statement like the
following one:
@UI @edit @CANBusFamily @encoded
Scenario: Add a CAN bus family encoded
Given I am logged in as Administrator
And I am on the CANBusFamiliesPage page
When I click on the Add button
And I fill in the name of the form
And I set the encoded values field with:
{"0": "zero", "1": "one"}
And I submit the form
Then a success popup message appears
implemented just with:
@pytest_bdd.when(pytest_bdd.parsers.cfparse(
'I set the encoded values field with:\n{encoded_values:json}',
extra_types=dict(json=json.loads)))
def set_encoded_values(navigation, encoded_values):
""" Set encoded values """
navigation.page.encoded_values = encoded_values
On the page instance you can simply set the values you want to apply in one shot:
page.encoded_values = {'company1': 'Company ONE', 'company2': 'Company TWO'}
...
or interact step by step thanks to the widget region:
page.getWidgetRegion('encoded_values').click_add()
...
Final page form setup configuration¶
You can add a new row, delete a row, add a key and a value for each row. If you want you can also create some validators and contraints to your values.
On the page side we need a schema and a page or region object inheriting from the base classes
provided by pypom_form
with a dictionary like colander type (Mapping
) and a custom
widget EncodedValuesWidget
:
class MyEditPageSchema(BaseEditSchema):
encoded_values = colander.SchemaNode(
colander.Mapping(unknown='preserve'),
selector=('css', '#metric-tabs-2'),
pypom_widget=EncodedValuesWidget(),
)
)
class MyEditPage(BaseEditPage):
schema_factory = MyEditPageSchema
Widget implementation¶
And now let’s see our pretend custom widget implementation. The widget itself is based on:
- a widget
EncodedValuesWidget
, it will let you interact with the input elements if you want to set a value or read the actual value on the browser (eg:{'company1': 'Company 1'}
). The widget is internally based on a widget region calledEncodedValuesWidgetRegion
- a widget region
EncodedValuesWidgetRegion
, providing the main logics used by the widget itself and available on the page if you want to interact step by step instead of assign a whole dictionary like. For example you can add a new row, delete it, change a single value or key and so on. So you can interact with theEncodedValuesWidgetRegion
like a dictionary: set or get values, iterate on them, etc. The widget region inner logics are demanded to subregionsEncodedValueRegion
(dynamic regions) for each row. The subregions controls how to set a key, a value, delete the item row. - the inner element is the region
EncodedValueRegion
. They are instanciated dynamically by the widget region and they provides a schema_factory containing akey
and avalue
string properties you can interact with
Each key-value pair Here you can see how to create a custom widget, for example a dictionary like widget with a key and a value (encoded values widget) or create your own
Let’s see the resulting code:
import colander
from pypom_form.form import BaseFormRegion
from pypom_form.widgets import (
BaseWidget,
BaseWidgetRegion,
)
class EncodedValueRegionSchema(colander.MappingSchema):
""" EncodedValueRegion schema for encoded values """
key = colander.SchemaNode(
colander.String(),
selector=('css', 'input[type="number"]'),
)
value = colander.SchemaNode(
colander.String(),
selector=('css', 'input[type="text"]'),
)
class EncodedValueRegion(BaseFormRegion):
""" Single encoded value region with key, value and delete button.
This is a subregion returned dynamically by
the EncodedValuesWidgetRegion for each key-value pair.
Each subregion exposes a key and a value.
You can delete subregion instance through the ``delete`` method,
"""
schema_factory = EncodedValueRegionSchema
DELETE_SELECTOR = ('css', '.administration_list_delete')
def delete(self):
""" Delete region """
self.find_element(*self.DELETE_SELECTOR).click()
class EncodedValuesWidgetRegion(BaseWidgetRegion):
""" Encoded values widget region
You can interact with your page using dictionary-like
operations.
>>> region = page.getWidgetRegion('encoded_values')
>>> region['0'] = 'ZERO'
>>> region['0']
You can also iterate on subregions for each key-value pair:
>>> region.encoded_value_regions[0].key = '1'
>>> region.encoded_value_regions[0].value = 'one'
Or add a new key-value pair without interact:
>>> subregion = region.click_add()
>>> subregion.key = '1'
>>> subregion.value = 'ONE'
Access to one key-value pair and interact with it:
>>> region.encoded_value_regions[0].value = 'one'
Or delete a mapping:
>>> del region['0']
"""
REGIONS_ROW_SELECTOR = ('css', 'tbody > tr')
ADD_BUTTON_SELECTOR = ('css', '.add_button')
def click_add(self):
""" Click add and returns a subregion """
previous_len = len(self)
self.find_element(*self.ADD_BUTTON_SELECTOR).click()
self.wait.until(lambda s: len(self) == previous_len+1)
return self.encoded_value_regions[0]
@property
def encoded_value_regions(self):
""" Encoded values regions"""
return [EncodedValueRegion(self, root=root) for root in
self.find_elements(*self.REGIONS_ROW_SELECTOR)]
def clear(self):
""" clear all values """
for region in self.encoded_value_regions:
region.delete()
def copy(self):
values = {}
for key, value in self.items():
values[key] = value
return values
def items(self):
return [(key, self[key]) for key in self]
def update(self, **values):
for key, value in values.items():
self[key] = value
def __getitem__(self, key):
for region in self.encoded_value_regions:
if region.key == key:
return region.value
raise KeyError
def __setitem__(self, key, value):
regions = [item for item in self.encoded_value_regions
if item.key == key]
if not regions:
regions = [self.click_add()]
region = regions[0]
region.value = value
if region.key != key:
region.key = key
def __delitem__(self, key):
self[key].delete()
def __contains__(self, key):
for key_item in self:
if key_item == key:
return True
return False
def __len__(self):
return len(self.encoded_value_regions)
def __iter__(self):
for region in self.encoded_value_regions:
yield region.key
def __repr__(self):
return "%r(%r)" % (self.__class__, self.copy())
class EncodedValuesWidget(BaseWidget):
""" This is the EncodedValuesWidget """
region_class = EncodedValuesWidgetRegion
def getter_factory(self):
def _getter(page):
reg = self.getWidgetRegion(page)
value = reg.copy()
return self.field.deserialize(value)
return _getter
def setter_factory(self):
def _setter(page, value):
reg = self.getWidgetRegion(page)
reg.clear()
value = self.field.serialize(value)
reg.update(**value)
return _setter
Final considerations¶
Now you have a dictionary like edit widget reusable across different page objects sharing
the same data structures powered by regions and subregions. The widget interaction on page
objects empowered by pypom_form
widgets is as easy as dealing with a Python dictionary but
you can also perform custom interactions using the widget region API.
So thanks to pypom_form
widgets you can deal with rich UI widgets hiding the complexity
making things easy for a great development and testing experience.
Extending Colander¶
We won’t cover how to add your own custom colander types or validators, instead we’ll address you to the Colander documentation online: