Views
Views are the cornerstone of widgetastic’s architecture. They organize widgets into logical groups that represent pages, sections, or components of your application. This tutorial covers different types of views.
Note
Prerequisites: Complete Basic Widgets tutorial first.
Understanding Views
A View is a container that groups related widgets together. Think of it as representing a page, dialog, or section of your web application. A View descends from the Widget class but it is specifically designed to hold other widgets.
Basic View Example
1"""Basic View Example
2
3This example demonstrates creating and using a basic View.
4"""
5
6from widgetastic.widget import View, Text
7
8
9class TestingPageView(View):
10 # Read the main page title
11 main_title = Text(locator=".//h1[@id='wt-core-title']")
12 # Read the sub title
13 sub_title = Text(locator='.//p[@class="subtitle"]')
14 # Define non existing element
15 non_existing_element = Text(locator='.//div[@id="non-existing-element"]')
16
17
18page = TestingPageView(browser) # noqa: F821
19
20# Check if element exist on page or not
21print(f"Main title is displayed: {page.main_title.is_displayed}")
22print(f"Non-existing element is displayed: {page.non_existing_element.is_displayed}")
23
24# Reading text content
25print(f"Page title: {page.main_title.read()}")
26print(f"Sub title: {page.sub_title.read()}")
Note
WidgetDescriptor and Lazy Creation:
When you define widgets on a view (e.g., main_title = Text(locator= ".//h1[@id='wt-core-title']")), you’re not creating
an actual widget object immediately. Instead, a WidgetDescriptor is created. The actual widget
instance is created lazily when you access it (e.g., view.main_title), at which point the browser
is automatically prepended to the widget’s arguments. This lazy creation mechanism ensures widgets
are only instantiated when needed and have access to the correct browser context.
View Hierarchy and Nesting
Views can contain other views, creating hierarchical structures that mirror your application’s layout. This allows you to organize complex pages into manageable, reusable components.
There are two approaches to create nested views in widgetastic:
Note
In our testing page, we have a View Testing section. Under this section, we have normal view, parametrized view and conditional switchable view. Let’s see how to create nested views for these sections.
1. Attribute Assignment
This approach creates standalone view classes and assigns them as attributes using View.nested():
1"""Nested Views - Attribute Assignment Approach
2
3This example demonstrates creating nested views using View.nested().
4"""
5
6from widgetastic.widget import View, Text, TextInput, Checkbox
7
8
9class NormalViewTesting(View):
10 """Normal View under View testing."""
11
12 ROOT = ".//div[contains(@class, 'normal-view')]"
13 title = Text(locator=".//div[@class='widget-title']")
14 name = TextInput(id="normal_name")
15 email = TextInput(id="normal_email")
16 terms = Checkbox(id="normal_terms")
17 submit = Text(locator=".//button[@id='normal_submit']")
18
19
20class ParametrizedViewTesting(View):
21 """Parametrized View under View testing."""
22
23 ROOT = ".//div[contains(@class, 'parametrized-view')]"
24 title = Text(locator=".//div[@class='widget-title']")
25 # Some other widgets
26
27
28class ConditionalSwitchableViewTesting(View):
29 """Conditional Switchable View under View testing."""
30
31 ROOT = ".//div[contains(@class, 'conditional-switchable-view')]"
32 title = Text(locator=".//div[@class='widget-title']")
33 # Some other widgets
34
35
36class ViewTesting(View):
37 normal_view = View.nested(NormalViewTesting)
38 parametrized_view = View.nested(ParametrizedViewTesting)
39 conditional_switchable_view = View.nested(ConditionalSwitchableViewTesting)
40
41
42# Access nested elements
43view = ViewTesting(browser) # noqa: F821
44
45print(f"Normal view is displayed: {view.normal_view.is_displayed}")
46print(f"Normal view title: {view.normal_view.title.read()}")
47print(f"Parametrized view title: {view.parametrized_view.title.read()}")
48print(f"Conditional switchable view: {view.conditional_switchable_view.read()}")
2. Inner Classes
This approach defines view classes as inner classes with the @View.nested decorator:
1"""Nested Views - Inner Classes Approach
2
3This example demonstrates creating nested views using @View.nested decorator.
4"""
5
6from widgetastic.widget import View, Text, TextInput, Checkbox
7
8
9class ViewTesting(View):
10 @View.nested
11 class normal_view(View): # noqa
12 """Normal View under View testing."""
13
14 ROOT = ".//div[contains(@class, 'normal-view')]"
15 title = Text(locator=".//div[@class='widget-title']")
16 name = TextInput(id="normal_name")
17 email = TextInput(id="normal_email")
18 terms = Checkbox(id="normal_terms")
19 submit = Text(locator=".//button[@id='normal_submit']")
20
21 @View.nested
22 class parametrized_view(View): # noqa
23 """Parametrized View under View testing."""
24
25 ROOT = ".//div[contains(@class, 'parametrized-view')]"
26 title = Text(locator=".//div[@class='widget-title']")
27 # Some other widgets
28
29 @View.nested
30 class conditional_switchable_view(View): # noqa
31 """Conditional Switchable View under View testing."""
32
33 ROOT = ".//div[contains(@class, 'conditional-switchable-view')]"
34 title = Text(locator=".//div[@class='widget-title']")
35 # Some other widgets
36
37
38# Access nested elements
39view = ViewTesting(browser) # noqa: F821
40
41print(f"Normal view is displayed: {view.normal_view.is_displayed}")
42print(f"Normal view title: {view.normal_view.title.read()}")
43print(f"Parametrized view title: {view.parametrized_view.title.read()}")
44print(f"Conditional switchable view: {view.conditional_switchable_view.read()}")
Note
Understanding @View.nested Decorator:
The @View.nested decorator is not strictly necessary for basic functionality, but it provides
important benefits that become critical in certain scenarios:
Widget Ordering: Views remember the order in which widgets are placed on them, which is important for fill/read operations. When you use
View.nested()as an attribute assignment (Method 1), the nested view doesn’t get wrapped in aWidgetDescriptor, so it won’t participate in the ordering mechanism. Using the@View.nesteddecorator on an inner class ensures the view is properly wrapped in aWidgetDescriptorand maintains correct order for fill/read operations.Proper Initialization: Guarantees correct parent-child relationships and browser context propagation
Cleaner Organization: Keeps related views grouped within the parent class, improving code readability
When to use @View.nested: Use it when widget ordering matters for your fill/read operations, or when you want to ensure proper WidgetDescriptor wrapping for consistency with the framework’s design.
ROOT Locator Scoping
The ROOT attribute defines the container for a view. All widgets in that view are searched within this container, providing proper scoping:
1"""ROOT Locator Scoping Example
2
3This example demonstrates how ROOT locators scope widget searches.
4"""
5
6from widgetastic.widget import View, Text, TextInput
7
8
9class NormalViewTesting(View):
10 ROOT = ".//div[contains(@class, 'normal-view')]" # All widgets scoped to this section
11
12 # These widgets are found within `ROOT`.
13 title = Text(locator=".//div[@class='widget-title']")
14 name = TextInput(id="normal_name")
15
16
17# Without ROOT, widgets would search the entire page
18# With ROOT, widgets only search within .//div[contains(@class, 'normal-view')].
19
20view = NormalViewTesting(browser) # noqa: F821
21print(f"View title: {view.title.read()}")
22print(f"Name input is displayed: {view.name.is_displayed}")
Parametrized Views
widgetastic.widget.ParametrizedView are useful when you need to create a view for a repeated pattern on a page that differs only by eg. a title or an id.
For example, if you have a page with a list of items, you can use a parametrized view to create a view for each item.
You can then use the parameters eg. in locators to create a view for each item.
ParametrizedView Example
Look at our testing page, Under Parametrized view testing section, you can see three similar containers that follow the same pattern but with different identifiers.
Thing “foo”: Container
<div id="foo">, inputname="asdf_foo", descriptionname="desc_foo", checkboxname="active_foo"Thing “bar”: Container
<div id="bar">, inputname="asdf_bar", descriptionname="desc_bar", checkboxname="active_bar"Thing “baz”: Container
<div id="baz">, inputname="asdf_baz", descriptionname="desc_baz", checkboxname="active_baz"
Without ParametrizedView, you’d need to create separate view classes for each “thing” or write repetitive code with hardcoded locators for each variation. This becomes unmaintainable when testing multiple similar components.
ParametrizedView solves this by letting you define a single template view that can be reused with different parameters. The thing_id parameter gets injected into locators and widget definitions at runtime, allowing one view class to handle all “thing” variations on the testing page.
1"""ParametrizedView Example
2
3This example demonstrates using ParametrizedView to handle repeated UI patterns.
4"""
5
6from widgetastic.utils import ParametrizedLocator, ParametrizedString
7from widgetastic.widget import ParametrizedView, TextInput, Checkbox
8
9
10class ThingContainerView(ParametrizedView):
11 # Defining one parameter
12 PARAMETERS = ("thing_id",)
13 # ParametrizedLocator coerces to a string upon access
14 # It follows similar formatting syntax as .format
15 # You can use the xpath quote filter as shown
16 ROOT = ParametrizedLocator(".//div[@id={thing_id|quote}]")
17
18 # Widget definition *args and values of **kwargs (only the first level) are processed as well
19 the_widget = TextInput(name=ParametrizedString("asdf_{thing_id}"))
20 description = TextInput(name=ParametrizedString("desc_{thing_id}"))
21 active = Checkbox(name=ParametrizedString("active_{thing_id}"))
22
23
24# Then for invoking this. create a view for foo.
25view = ThingContainerView(browser, additional_context={"thing_id": "foo"}) # noqa: F821
26
27# Fill the foo container
28print("Filling container 'foo':")
29view.the_widget.fill("Test input for foo")
30view.description.fill("Description for foo")
31view.active.fill(True)
32print(f"Foo container values: {view.read()}")
33
34# Create parametrized view for bar
35bar_view = ThingContainerView(browser, additional_context={"thing_id": "bar"}) # noqa: F821
36bar_view.the_widget.fill("Test input for bar")
37print(f"Bar container widget value: {bar_view.the_widget.read()}")
Nested Parametrized View Example
It is also possible to nest the parametrized view inside another view, parametrized or otherwise. In this case the invocation of a nested view looks like a method call, instead of looking like a property. The invocation supports passing the arguments both ways, positional and keyword based.
1"""Nested Parametrized View Example
2
3This example demonstrates nesting a parametrized view inside another view.
4"""
5
6from widgetastic.utils import ParametrizedLocator, ParametrizedString
7from widgetastic.widget import ParametrizedView, TextInput, Checkbox, View, Text
8
9
10class ParametrizedViewTesting(View):
11 """Parametrized View under View testing."""
12
13 ROOT = ".//div[contains(@class, 'parametrized-view')]"
14 title = Text(locator=".//div[@class='widget-title']")
15
16 class thing_container_view(ParametrizedView): # noqa
17 # Defining one parameter
18 PARAMETERS = ("thing_id",)
19 # ParametrizedLocator coerces to a string upon access
20 ROOT = ParametrizedLocator(".//div[@id={thing_id|quote}]")
21
22 # Widget definition processed with parameters
23 the_widget = TextInput(name=ParametrizedString("asdf_{thing_id}"))
24 description = TextInput(name=ParametrizedString("desc_{thing_id}"))
25 active = Checkbox(name=ParametrizedString("active_{thing_id}"))
26
27
28# We create the root view
29view = ParametrizedViewTesting(browser) # noqa: F821
30
31# Now if it was an ordinary nested view, view.thing_container_view.the_widget would give us the
32# nested view instance directly and then the the_widget widget. But this is a parametrized view
33# and it will give us an intermediate object whose task is to collect the parameters upon
34# calling and then pass them through into the real view object.
35
36# This example will be invoking the parametrized view with the exactly same param like the
37# previous example:
38print("Accessing parametrized container 'foo'")
39foo_container = view.thing_container_view("foo")
40
41# So, when we have that view, you can use it as you are used to
42view.thing_container_view("foo").the_widget.fill("Test for foo")
43print(f"Filled foo container: {view.thing_container_view('foo').the_widget.read()}")
44
45view.thing_container_view("bar").the_widget.fill("Test for bar")
46print(f"Filled bar container: {view.thing_container_view('bar').the_widget.read()}")
47
48view.thing_container_view("baz").the_widget.fill("Test for baz")
49print(f"Filled baz container: {view.thing_container_view('baz').the_widget.read()}")
50
51# Or with keyword params
52view.thing_container_view(thing_id="foo").the_widget.fill("Test for foo with keyword")
53print(f"Using keyword param: {view.thing_container_view(thing_id='foo').the_widget.read()}")
The parametrized views also support list-like access using square braces. For that to work, you need the all classmethod defined on the view so Widgetastic would be aware of all the items. You can access the parametrized views by member index [i] and slice [i:j].
It is also possible to iterate through all the occurrences of the parametrized view. Let’s assume the previous code sample is still loaded and the thing_container_view class has the all() defined. In that case, the code would like like this:
1"""Parametrized View Iteration Example
2
3This example demonstrates iterating through all occurrences of a parametrized view.
4"""
5
6from widgetastic.utils import ParametrizedLocator, ParametrizedString
7from widgetastic.widget import ParametrizedView, TextInput, Checkbox, View, Text
8
9
10class ParametrizedViewTesting(View):
11 """Parametrized View under View testing."""
12
13 ROOT = ".//div[contains(@class, 'parametrized-view')]"
14 title = Text(locator=".//div[@class='widget-title']")
15
16 class thing_container_view(ParametrizedView): # noqa
17 # Defining one parameter
18 PARAMETERS = ("thing_id",)
19 # ParametrizedLocator coerces to a string upon access
20 ROOT = ParametrizedLocator(".//div[@id={thing_id|quote}]")
21
22 # Widget definition processed with parameters
23 the_widget = TextInput(name=ParametrizedString("asdf_{thing_id}"))
24 description = TextInput(name=ParametrizedString("desc_{thing_id}"))
25 active = Checkbox(name=ParametrizedString("active_{thing_id}"))
26
27 @classmethod
28 def all(cls, browser):
29 # Get all the thing_id values from the page
30 elements = browser.elements(".//div[@class='thing']")
31 # Return a list of tuples, each containing the thing_id value
32 return [(browser.get_attribute("id", el),) for el in elements]
33
34
35# We create the root view
36view = ParametrizedViewTesting(browser) # noqa: F821
37
38print("Iterating through all thing containers:")
39for container_view in view.thing_container_view:
40 container_view.the_widget.fill("do something with the widget")
41 print(f"Container values: {container_view.read()}")
Note
This sample code would go through all the occurrences of the parametrization. Remember that the all classmethod IS REQUIRED in this case.
You can also pass the widgetastic.utils.ParametrizedString instance as a constructor parameter into widget instantiation on the view class.
Because it utilizes Constructor Object Collapsing, it will resolve itself automatically.
Constructor Object Collapsing
Constructor object collapsing is a powerful mechanism that allows objects to lazily resolve themselves into different objects during widget instantiation. This is used internally by several widgetastic utilities like widgetastic.utils.VersionPick for Version picking and widgetastic.utils.ParametrizedString for parametrized views.
How It Works
By using widgetastic.utils.ConstructorResolvable, you can create an object that can lazily resolve itself into a different object upon widget instantiation. The key is to subclass this class and implement .resolve(self, parent_object) where parent_object is the to-be parent of the widget.
Why It’s Useful
This mechanism enables:
Lazy Evaluation: Objects can decide their final form only when they have full context
Dynamic Resolution: The same constructor parameter can resolve to different values based on runtime conditions
Version Picking:
widgetastic.utils.VersionPickuses this to select appropriate widgets based on browser versionParametrized Strings:
widgetastic.utils.ParametrizedStringuses this to inject parameters during widget construction
Note
Most users won’t need to implement their own ConstructorResolvable classes, as the built-in ones (VersionPick, ParametrizedString, ParametrizedLocator) cover most use cases.
Conditional Views
Handle dynamic UI sections that change based on application state using conditional views.
If you have forms in your product whose parts change depending on previous selections, you might like to use the widgetastic.widget.ConditionalSwitchableView.
It will allow you to represent different kinds of views under one widget name.
ConditionalSwitchableView Example
Look at our testing page, Under Conditional view testing section, you can see a form with a dropdown and three different views.
Action type 1: Container
<div id="action_form_1">, inputname="action1_widget", selectname="action1_options", checkboxname="action1_enabled"Action type 2: Container
<div id="action_form_2">, inputname="action2_widget", selectname="action2_priority", inputname="action2_notes"Action type 3: Container
<div id="action_form_3">, inputname="action3_widget", inputname="action3_config", selectname="action3_mode"
1"""ConditionalSwitchableView Example
2
3This example demonstrates using ConditionalSwitchableView to handle dynamic UI sections.
4"""
5
6from widgetastic.widget import ConditionalSwitchableView, View, TextInput, Select, Checkbox
7
8
9class ConditionalSwitchableViewTesting(View):
10 ROOT = "#conditional_form_container"
11
12 foo = TextInput(name="foo_value") # For multi-widget reference
13 action_type = Select(name="action_type")
14
15 action_form = ConditionalSwitchableView(reference="action_type")
16
17 # Simple value matching. If Action type 1 is selected in the select, use this view.
18 # And if the action_type value does not get matched, use this view as default
19 @action_form.register("Action type 1", default=True)
20 class ActionType1Form(View):
21 ROOT = "#action_form_1"
22 widget = TextInput(name="action1_widget")
23 options = Select(name="action1_options")
24 enabled = Checkbox(name="action1_enabled")
25
26 # You can use a callable to declare the widget values to compare
27 @action_form.register(lambda action_type: action_type == "Action type 2")
28 class ActionType2Form(View):
29 ROOT = "#action_form_2"
30 widget = TextInput(name="action2_widget")
31 priority = Select(name="action2_priority")
32 notes = TextInput(name="action2_notes")
33
34 # With callable, you can use values from multiple widgets
35 @action_form.register(
36 lambda action_type, foo: action_type == "Action type 3" and foo == "special"
37 )
38 class ActionType3Form(View):
39 ROOT = "#action_form_3"
40 widget = TextInput(name="action3_widget")
41 config = TextInput(name="action3_config")
42 mode = Select(name="action3_mode")
43
44
45view = ConditionalSwitchableViewTesting(browser) # noqa: F821
46
47# Switch content by changing selector
48print("Filling Action type 1 form:")
49view.action_type.fill("Action type 1")
50view.action_form.widget.fill("Test input for type 1")
51view.action_form.options.fill("Option 1")
52view.action_form.enabled.fill(True)
53print(f"Action form values: {view.action_form.read()}")
54
55# Switch to action type 2 content
56print("Filling Action type 2 form:")
57view.action_type.fill("Action type 2")
58view.action_form.widget.fill("Test input for type 2")
59view.action_form.priority.fill("High")
60view.action_form.notes.fill("Important notes")
61print(f"Action form values: {view.action_form.read()}")
62
63# Switch to action type 3 with multi-widget condition
64print("Filling Action type 3 form (requires foo='special'):")
65view.foo.fill("special") # Required for condition
66view.action_type.fill("Action type 3")
67view.action_form.widget.fill("Test input for type 3")
68view.action_form.config.fill("advanced config")
69print(f"Action form values: {view.action_form.read()}")
You can see it gives you the flexibility of decision based on the values in the view.
Simple Conditional Widget Registration
1"""Simple Conditional Widget Registration Example
2
3This example demonstrates registering a simple widget directly with ConditionalSwitchableView.
4"""
5
6from widgetastic.widget import ConditionalSwitchableView, View, TextInput, Select
7
8
9class SimpleConditionalWidgetView(View):
10 bar = Select(
11 name="bar"
12 ) # Reference widget; depends on the value of this widget we will decide widget to use.
13
14 conditional_widget = ConditionalSwitchableView(reference="bar")
15
16 # Register simple widget directly without creating a class
17 conditional_widget.register(
18 "Action type 1", default=True, widget=TextInput(name="simple_widget")
19 )
20
21
22view = SimpleConditionalWidgetView(browser) # noqa: F821
23
24# When bar is set to 'Action type 1', conditional_widget becomes available.
25view.bar.fill("Action type 1")
26print(f"Conditional widget is displayed (Action type 1): {view.conditional_widget.is_displayed}")
27view.conditional_widget.fill("Direct widget input")
28print("Filled conditional widget with: 'Direct widget input'")
29
30# When bar is set to 'Other', conditional_widget becomes unavailable.
31view.bar.fill("Other")
32print(f"Conditional widget is displayed (Other): {view.conditional_widget.is_displayed}")
View-Level Operations
Batch Operations
Views support batch operations like fill and read on all their widgets.
1"""View Batch Operations Example
2
3This example demonstrates batch fill and read operations on views.
4"""
5
6from widgetastic.widget import View, TextInput, Checkbox
7
8
9class NormalView(View):
10 ROOT = "#normal_view_container"
11
12 name = TextInput(id="normal_name")
13 email = TextInput(id="normal_email")
14 terms = Checkbox(id="normal_terms")
15
16
17# Fill multiple fillable widgets at once
18form = NormalView(browser) # noqa: F821
19form_data = {
20 "name": "Foo Bar",
21 "email": "foo.bar@example.com",
22 "terms": True,
23}
24
25print(f"Filling form with: {form_data}")
26form.fill(form_data)
27
28# Read all fillable widgets in the view
29current_values = form.read()
30print(f"Current form values: {current_values}")
View Lifecycle Hooks
Views can optionally define before_fill(values) and after_fill(was_change) methods to
intercept the fill process:
1"""View Lifecycle Hooks Example
2
3This example demonstrates using before_fill and after_fill hooks.
4"""
5
6from widgetastic.widget import View, TextInput, Checkbox
7
8
9class FormView(View):
10 ROOT = "#normal_view_container"
11 name = TextInput(id="normal_name")
12 email = TextInput(id="normal_email")
13 terms = Checkbox(id="normal_terms")
14
15 def before_fill(self, values):
16 """Called right before filling starts."""
17 # self.logger.info(f"About to fill form with: {values}")
18 print(f"About to fill form with: {values}")
19
20 # You can validate values, prepare the form, etc.
21 # Return value is ignored
22
23 def after_fill(self, was_change):
24 """Called right after filling completes."""
25 if was_change:
26 # self.logger.info("Form was successfully filled with new values")
27 print("Form was successfully filled with new values")
28 # Could wait for form updates, verify changes, etc.
29 else:
30 # self.logger.debug("No changes were made to the form")
31 print("No changes were made to the form")
32 # Return value is ignored
33
34
35form = FormView(browser) # noqa: F821
36form.fill({"name": "John", "email": "john@example.com", "terms": True})
37# before_fill is called first, then widgets are filled, then after_fill is called
38
39# Read all fillable widgets in the view
40current_values = form.read()
41print(current_values)
View State Checking
If we don’t specify a ROOT locator, it will be considered as displayed every time.
but if we specify a ROOT locator, it will be considered as displayed only when the root locator is present on web page.
1"""View State Checking Examples
2
3This example demonstrates how ROOT locators affect is_displayed behavior.
4"""
5
6from widgetastic.widget import View, TextInput
7
8
9# Example 1: Without ROOT locator
10class NormalView(View):
11 # Without root locator, it will be considered as displayed every time.
12 name = TextInput(id="normal_name")
13
14
15view = NormalView(browser) # noqa: F821
16print(f"View without ROOT - name is displayed: {view.name.is_displayed}")
17
18
19# Example 2: With ROOT locator
20class NormalViewWithRoot(View):
21 ROOT = "#normal_view_container"
22 name = TextInput(id="normal_name")
23
24
25view_with_root = NormalViewWithRoot(browser) # noqa: F821
26print(f"View with ROOT - name is displayed: {view_with_root.name.is_displayed}")
27
28
29# Example 3: Custom is_displayed property
30class NormalViewCustom(View):
31 name = TextInput(id="normal_name")
32
33 @property
34 def is_displayed(self):
35 # We can take support of other widgets to check if the view is displayed
36 return self.name.is_displayed
37
38
39view_custom = NormalViewCustom(browser) # noqa: F821
40print(f"View with custom is_displayed: {view_custom.is_displayed}")
Note
View is_displayed property is important to know when you are using views to navigate between pages.
So it recommended to specify a ROOT locator for all views. If you don’t want to specify a ROOT locator,
then tried to add custom is_displayed property to the view.
Best Practices for Views
When designing views in widgetastic, following best practices will help you create maintainable, readable, and robust automation code. Here are some key guidelines:
Use Descriptive Names: Name your view classes according to their purpose or the section of the application they represent. This makes your code self-explanatory and easier to navigate.
# Good: Clear purpose class LoginFormView(View): pass class UserProfileSettingsView(View): pass # Avoid: Generic names class View1(View): pass
Group Related Widgets: Organize widgets within a view so that each view contains only widgets relevant to a specific page, dialog, or component. Avoid mixing unrelated widgets in a single view.
# Group related functionality class SearchView(View): search_input = TextInput(id="search") search_button = Button(id="search-btn") results_table = Table(id="results") # Don't mix unrelated widgets class BadView(View): login_field = TextInput(id="login") # Login functionality checkout_btn = Button(id="checkout") # Shopping functionality settings_link = Text("a#settings") # Settings functionality
Leverage ROOT Locators: Always define a
ROOTlocator for your views to scope widget searches to the correct section of the page. This prevents accidental matches and improves reliability.# Scope widgets to specific sections class SidebarView(View): ROOT = "#sidebar" menu_item1 = Text("a[href='/dashboard']") menu_item2 = Text("a[href='/profile']")
Prefer Nested Views for Complex Pages: For pages with multiple sections or repeated patterns, use nested views or parametrized views to mirror the application’s structure. This keeps your code modular and reusable.
# Nested views class UserProfilePage(View): ROOT = "#user-profile" @View.nested class personal_info(View): ROOT = "#personal-section" first_name = TextInput("#first_name") last_name = TextInput("#last_name") @View.nested class preferences(View): ROOT = "#preferences-section" theme = Select("#theme") language = Select("#language")
Customize is_displayed When Needed: If a view cannot be reliably detected by a single locator, override the
is_displayedproperty to implement custom logic using one or more widgets.class CustomView(View): ROOT = "#custom-view" custom_widget = TextInput("#custom-widget") @property def is_displayed(self): return self.custom_widget.is_displayed
Summary
Views are essential for organizing and structuring your automation code:
Basic Views: Container for related widgets
Nested Views: Hierarchical page structures
Parametrized Views: Handle repeated UI patterns
Conditional Views: Adapt to dynamic content
View Operations: Batch read/fill operations