Skip to content

Fallback selectors (self-healing)

UI elements sometimes change - a developer renames an AutomationId, or a new build ships with a slightly different control hierarchy. Fallback selectors let Dolphin try alternative criteria automatically before failing a test.


The problem

A locator breaks when the primary selector stops matching:

# Works today - AutomationId "btnSubmit"
btn = win.get_by_automation_id("btnSubmit")

# Next sprint: developer renamed it to "btnSave" - test fails

Adding a fallback

Pass a fallback list when creating the locator. Dolphin tries the primary criteria first; if that times out, it tries each fallback in order:

btn = win.locator(
    auto_id="btnSubmit",
    fallback=[
        {"auto_id": "btnSave"},
        {"title": "Submit", "control_type": "Button"},
    ]
)
btn.click()

If the primary fails and auto_id="btnSave" matches, Dolphin clicks that element and logs a self-healing event.


Image-based fallback

When UIA criteria all fail, fall back to template matching:

from dolphin_desktop import ImageLocator

btn = win.locator(
    auto_id="btnSubmit",
    fallback=[{"auto_id": "btnSave"}],
    image_fallback=ImageLocator("templates/submit_button.png"),
)
btn.click()

The template image is matched against the live screen using OpenCV. Requires pip install dolphin-desktop[vision].


Inspecting self-healing events

dolphin selfheal-stats --last 20
Last 3 self-healing event(s):

  [2024-06-10 14:32:01]  tests/test_submit.py::test_submit_form
    primary:  {'auto_id': 'btnSubmit'}
    fallback: {'auto_id': 'btnSave'}

This shows which selectors degraded and helps you update them before they break completely.


Best practices

  1. Prefer auto_id as primary - AutomationId is set by developers and is the most stable identifier.
  2. Add a title-based fallback - titles change with locale but rarely disappear.
  3. Use image fallback as last resort - image matching is slower and more fragile than UIA.
  4. Review selfheal-stats after every release - a self-healed test is a warning, not a pass.

Using fallbacks in Page Objects

objects/form_page.py
class FormPage:
    def __init__(self, window):
        self._win = window

    @property
    def _submit_button(self):
        return self._win.locator(
            auto_id="btnSubmit",
            fallback=[
                {"auto_id": "btnSave"},
                {"title": "Submit"},
            ],
        )

    def submit(self):
        self._submit_button.click()

Next steps