Guidelines
This document outlines essential guidelines for using Widgetastic.core effectively. Anyone using this library should consult these guidelines to ensure they are following best practices and not violating any framework conventions.
Note
These guidelines are based on the framework’s architecture and design principles. Following them ensures your code is maintainable, reliable, and consistent with the framework’s expectations.
While Writing New Widgets
Read/Fill Interface
All widgets should implement the standard read/fill interface:
read() Method
Return Type:
objectCompatibility: Whatever is returned from
read()must be compatible withfill()Round-trip Requirement:
obj.fill(obj.read())must work at any timeException Handling:
read()may throw aDoNotReadThisWidgetexception if reading the widget is pointless (e.g., in current form state it is hidden). This is achieved by invoking thedo_not_read_this_widget()function.
fill() Method
Return Type:
True|FalseInput Compatibility:
fill(value)must be able to ingest whatever was returned byread()Round-trip Requirement:
obj.fill(obj.read())must work at any timeException: An exception to this rule is only acceptable in the case where this 1:1 direct mapping would cause severe inconvenience
Return Value Rules: -
fillMUST returnTrueif it changed anything during filling -fillMUST returnFalseif it has not changed anything during filling
Optional Methods
Any of these methods may be omitted if it is appropriate based on the UI widget interactions
It is recommended that all widgets have at least
read()but in cases like buttons where you don’t read or fill, it is understandable that there is neither of those
Example
class MyWidget(Widget):
def read(self):
"""Read current widget value."""
return self.browser.text(self)
def fill(self, value):
"""Fill widget with value."""
current = self.read()
if value == current:
return False # No change
self.browser.fill(self, str(value))
return True # Changed
# Verify round-trip works
widget = MyWidget(browser, locator="#my-widget")
value = widget.read()
widget.fill(value) # Should work without errors
Widget Initialization
Signature Pattern
The __init__ must follow the standard pattern:
If you want your widget to accept parameters
aandb, you must create signature like this: -__init__(self, parent, a, b, logger=None)
Parent Class Initialization
The first line of the widget must call out to the root class in order to set things up properly: -
Widget.__init__(self, parent, logger=logger)
Example
class CustomInput(Widget):
def __init__(self, parent, input_id, placeholder=None, logger=None):
"""Initialize custom input widget."""
Widget.__init__(self, parent, logger=logger)
self.input_id = input_id
self.placeholder = placeholder
def __locator__(self):
return f"#input-{self.input_id}"
Locator Definition
Requirement
Widgets MUST define
__locator__in some wayViews do not have to, but can do it to fence the element lookup in its child widgets
Locator Return Types
You can write __locator__ method yourself. It should return anything that can be turned into a locator by SmartLocator:
'#foo'(CSS selector)'//div[@id="foo"]'(XPath)SmartLocator(xpath='...')(Locator object instance)etc.
Important Restrictions
__locator__MUST NOT returnElementHandleinstances to prevent stale element issues
Automatic Generation
If you use a
ROOTclass attribute, especially in combination withParametrizedLocator, a__locator__is generated automatically for you
Example
class MyWidget(Widget):
# Option 1: Using ROOT attribute (automatic __locator__)
ROOT = "#my-widget"
# Option 2: Custom __locator__ method
def __locator__(self):
return f"#widget-{self.widget_id}"
# Option 3: Using ParametrizedLocator
ROOT = ParametrizedLocator(".//div[@id={@widget_id|quote}]")
State Management
General Principle
Widgets should keep its internal state in reasonable size Ideally none, but e.g., caching header names of tables is perfectly acceptable
Saving
ElementHandleinstances in the widget instance is not recommended
Caching Guidelines
Think about what to cache and when to invalidate
Never store
ElementHandleobjectsTry to shorten the lifetime of any single
ElementHandleas much as possibleThis will help against stale element issues
Logging
Standard Practice
Widgets shall log using
self.loggerThat ensures the log message is prefixed with the widget name and location
This gives more insight about what is happening
Example
class MyWidget(Widget):
def fill(self, value):
self.logger.info(f"Filling widget with value: {value}")
# ... fill logic ...
self.logger.debug(f"Fill completed, new value: {self.read()}")
When Using Widgets (and Views)
WidgetDescriptor and Lazy Creation
Understanding WidgetDescriptor
Bear in mind that when you do
MySuperWidget('foo', 'bar')in python interpreter, you are not getting an actual widget object, but rather an instance ofWidgetDescriptor
Creating Real Widget Instances
In order to create a real widget object, you have to have widgetastic
Browserinstance around and prepend it to the argumentsThe call to create a real widget instance would look like: -
MySuperWidget(wt_browser, 'foo', 'bar')
Automatic Browser Prepending
This browser prepending is done automatically by
WidgetDescriptorwhen you access it on aViewor anotherWidgetAll of these means that the widget objects are created lazily
Example
class MyView(View):
my_widget = MySuperWidget('foo', 'bar')
view = MyView(browser)
# When you access view.my_widget, WidgetDescriptor automatically:
# 1. Prepends browser to arguments
# 2. Creates the actual widget instance
# 3. Returns the real widget object
widget = view.my_widget # Now it's a real widget instance
Nested Views
Filling and Reading
Views can be nested
Filling and reading nested views is simple
Each view is read/filled as a dictionary
The required dictionary structure is exactly the same as the nested class structure
Example
class InnerView(View):
field1 = TextInput("#field1")
field2 = TextInput("#field2")
class OuterView(View):
inner = View.nested(InnerView)
other_field = TextInput("#other")
view = OuterView(browser)
# Fill nested view
view.fill({
'inner': {
'field1': 'value1',
'field2': 'value2'
},
'other_field': 'value3'
})
Widget Order and View.nested Decorator
Order Preservation
Views remember the order in which the Widgets were placed on it
Each
WidgetDescriptorhas a sequential number on itThis is used when filling or reading widgets, ensuring proper filling order
Nested Views Exception
This would normally also apply to Views since they are also descendants of
WidgetBut since you are not instantiating the view when creating nested views, this mechanism does not work
Solution: @View.nested Decorator
You can ensure the
Viewgets wrapped in aWidgetDescriptorand therefore in correct order by placing a@View.nesteddecorator on the nested view
Example
class InnerView(View):
field1 = TextInput(id="field1")
field2 = TextInput(id="field2")
class OuterView(View):
@View.nested
class inner(View):
field1 = TextInput(id="field1")
field2 = TextInput(id="field2")
other_field = TextInput(id="other")
View Lifecycle Hooks
Optional Methods
Views can optionally define
before_fill(values)andafter_fill(was_change)
before_fill
Invoked right before filling gets started
You receive the filling dictionary in the values parameter
You can act appropriately (e.g., validation, preparation)
after_fill
Invoked right after the fill ended
was_changetells you whether there was any change or notUseful for post-fill actions (e.g., waiting for updates, logging)
Example
class MyView(View):
field1 = TextInput(id="field1")
field2 = TextInput(id="field2")
def before_fill(self, values):
"""Called before filling starts."""
self.logger.info(f"About to fill with: {values}")
# Could validate values here
def after_fill(self, was_change):
"""Called after filling completes."""
if was_change:
self.logger.info("View was successfully filled")
else:
self.logger.debug("No changes were made")
When Using Browser (also applies when writing Widgets)
Use Widgetastic Browser Methods
General Rule
Ensure you use the widgetastic Browser methods rather than direct Playwright Locator methods where possible
Example
Instead of
locator.text_content()usebrowser.text(locator)This applies for all such circumstances
These calls usually do not invoke more than their original counterparts
They only invoke some workarounds if some known issue arises
Check what the
Browser(sub)class offers and if you miss something, create a PR
Example
# BAD: Direct Playwright method
element = browser.element("#my-element")
text = element.text_content()
# GOOD: Widgetastic Browser method
text = browser.text("#my-element")
Automatic Parent Resolution
Simplified Syntax
You don’t necessarily have to specify
self.browser.element(..., parent=self)when you are writing a query inside a widget implementationWidgetastic figures this out and does it automatically
Example
class MyWidget(Widget):
def get_child_text(self):
# Widgetastic automatically uses self as parent
return self.browser.text(".//span", parent=self)
# Can also be written as:
# return self.browser.text(".//span") # parent=self is automatic
Method Arguments and Element Resolution
Simplified Method Calls
Most of the methods that implement the getters, that would normally be on the element object, take an argument or two for themselves
The rest of
*argsand**kwargsis shoved insideelement()method for resolutionSo constructs like
self.browser.get_attribute('id', self.browser.element('locator', parent=foo))are not neededJust write
self.browser.get_attribute('id', 'locator', parent=foo)Check the method definitions on the
Browserclass to see that
Example
# BAD: Nested element() call
element = self.browser.element('locator', parent=foo)
attr = self.browser.get_attribute('id', element)
# GOOD: Direct method call
attr = self.browser.get_attribute('id', 'locator', parent=foo)
Intelligent Element Selection
Automatic Filtering
element()method tries to apply a rudimentary intelligence on the element it resolvesIf a locator resolves to a single element, it returns it
If the locator resolves to multiple elements, it tries to filter out the invisible elements and return the first visible one
If none of them is visible, it just returns the first one
Under normal circumstances, standard Playwright resolution returns all matching elements
Example
# If multiple elements match, widgetastic automatically:
# 1. Filters out invisible elements
# 2. Returns the first visible one
# 3. Falls back to first element if none are visible
element = browser.element(".//button") # Intelligent selection
Avoid Nested Locator Calls
Important Rule
DO NOT use nested locator calls
Use
self.browser.element('locator', parent=element)insteadThis approach is safer and more consistent with the framework architecture
Example
# BAD: Nested locator calls
parent = browser.element("#parent")
child = parent.locator(".//child") # Don't do this!
# GOOD: Use browser.element with parent parameter
parent = browser.element("#parent")
child = browser.element(".//child", parent=parent)
Summary
These guidelines ensure that your Widgetastic code is:
Consistent: Follows framework conventions and patterns
Maintainable: Easy to understand and modify
Reliable: Avoids common pitfalls like stale element issues
Efficient: Uses framework features optimally
Remember: No current exceptions are to be taken as a precedent. Always follow these guidelines unless there’s a compelling reason documented in the framework itself.