Add support for locating a nested GUI element

Support for locate() can be extended by registering additional location type and resolution logic via register_location on a TargetRegistry.

Suppose we have a custom UI editor that contains some buttons. The objective of a test is to click a specific button with a given label. We will therefore need to locate the button with the given label before a mouse click can be performed.

The test code we wanted to achieve looks like this:

container = UITester().find_by_id(ui, "some_container")
button_wrapper = container.locate(LabelledButton("OK"))

Define the location type

We can define the new LabelledButton location type:

class LabelledButton:
    ''' Locator for locating a push button by label.'''
    def __init__(self, label):
        self.label = label

Identify the target

Next, we need to know which object implements the GUI component. This is where implementation details start to come in (see Why is Target protected?). We can inspect the object being wrapped:

>>> container._target
<package.ui.qt.shiny_panel.ShinyPanel object at 0x7f940a3f10b8>
>>> button_wrapper._target
<package.ui.qt.shiny_button.ShinyButton object at 0x7fbcc3a63438>

Implement a solver

Say ShinyPanel keeps track of the buttons with a dictionary called _buttons where the names of the buttons are the keys of the dictionary. Then the logic to retrieving a button from a label can be written like this:

def get_button(wrapper, location):
    """ Returns a ShinyButton from a UIWrapper wrapping ShinyPanel."""
    # wrapper is an instance of UIWrapper
    # location is an instance of LabelledButton
    return wrapper.target._buttons[location.label]

The solvers can then be registered for the container UI target:

registry = TargetRegistry()
registry.register_location(
    target_class=ShinyPanel,
    locator_class=LabelledButton,
    solver=get_button,
)

Similar to register_interaction(), the signature of get_button is required by the register_location method. By setting the target_class and locator_class, we restrict the types of wrapper._target and location received by get_button respectively.

Apply it

Finally, we can use this registry with UITester:

tester = UITester(registries=[custom_registry])

If we have also added a custom ManyMouseClick interaction (see section Add support for performing actions or inspecting states), we can write test code like this:

container = UITester().find_by_id(ui, "some_container")
button_wrapper = container.locate(LabelledButton("OK"))
button_wrapper.perform(ManyMouseClick(n_times=10))

where both LabelledButton and ManyMouseClick are custom objects.