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 a WidgetDescriptor, so it won’t participate in the ordering mechanism. Using the @View.nested decorator on an inner class ensures the view is properly wrapped in a WidgetDescriptor and 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">, input name="asdf_foo", description name="desc_foo", checkbox name="active_foo"

  • Thing “bar”: Container <div id="bar">, input name="asdf_bar", description name="desc_bar", checkbox name="active_bar"

  • Thing “baz”: Container <div id="baz">, input name="asdf_baz", description name="desc_baz", checkbox name="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.VersionPick uses this to select appropriate widgets based on browser version

  • Parametrized Strings: widgetastic.utils.ParametrizedString uses 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">, input name="action1_widget", select name="action1_options", checkbox name="action1_enabled"

  • Action type 2: Container <div id="action_form_2">, input name="action2_widget", select name="action2_priority", input name="action2_notes"

  • Action type 3: Container <div id="action_form_3">, input name="action3_widget", input name="action3_config", select name="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 ROOT locator 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_displayed property 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