← back to work
case study · /self-healing · open source

Self-healing locator framework.

A multi-model Appium framework that re-identifies elements at runtime when their locators break — across DOM heuristics and vision models. Built at L&T Technology Services, deployed in production for two T-Mobile US MVNEs.

production · LTTS appium java multi-model flask sqlite iOS + Android open source
↗ github ↗ medium ↗ linkedin

Locator maintenance is the boring tax on mobile automation.

Every UI change ships with a quiet bill: half a sprint of "fix the broken selectors." Multiply by two MVNE clients on two platforms (iOS + Android) on a weekly release cadence, and the team's #1 toil source is "what did findByXpath miss this week."

Existing tools were single-track: either pure DOM heuristics (brittle when the structure changes) or pure vision models (slow, expensive, drift-prone). Neither was good enough alone.

Cascade. Cheap heuristics first. Expensive models last.

  • Resolver tier 0 · the original locator

    Always tried first. If it works, the spec proceeds. Free.

  • Resolver tier 1 · DOM heuristics

    Walk neighbors of the cached locator. Try the same element by accessibility label, by class+text, by sibling-relative path. Sub-100ms. Catches ~70% of breakages.

  • Resolver tier 2 · text + visual hash

    If tier 1 fails, run an OCR pass on the screen, find the element by its expected text, hash the visual region, cache for next run. ~500ms.

  • Resolver tier 3 · vision model

    Last resort. Pass the screen + the original element's expected role to a multimodal model, ask "where is the X?" 2-3s.

  • Healing · update the cached locator

    When any tier 1+ resolver succeeds, update the locator cache so the next run hits tier 0 again. Drift recovers itself.

  • Telemetry

    Every healing event logs: which tier rescued it, how long it took, the new locator. Surfaces "this element is breaking weekly" patterns the team can fix at the source.

  • Centralized store · Flask + SQLite

    The locator cache and healing telemetry live in a shared Flask service backed by SQLite. Every engineer and CI run heals against — and contributes to — the same cache, so a fix found on one machine lands for the whole pod instead of dying in isolated local state.

Watch the cascade heal — then watch it pay off.

Pick a scenario, run the spec, and watch the resolver fall through the tiers until something catches. Every heal writes back to the shared cache — so the next run on that target lands at tier 0 instantly. The suite gets faster as it runs.

device screen
@label: continue
Continue
spec idle · press run
resolver cascade
tier 0 · cached locator
the original locator
free idle
tier 1 · DOM heuristics
neighbors, label, sibling path
<100ms idle
tier 2 · text + visual hash
OCR pass, hash the region
~500ms idle
tier 3 · vision model
multimodal, "where is the X?"
2–3s idle
Flask + SQLite · shared cache
continue_btn ~id/continue_button
healed via hits 128 next run tier 0
login_field ~id/email_input
healed via tier 1 hits 1,204 next run tier 0
telemetry · healing log
$ awaiting first run…

What changed after this shipped.

locator maintenance time
~14h/wk <2h/wk
Avg across the 6-engineer pod.
healing rate
93%
Of locator breakages resolved before the spec failed.
tier-3 share
6%
Vision model only invoked when cheaper tiers failed. Kept the bill small.

Lessons

  • Cascade beats monolith. The same instinct from CPU caches applies here.
  • Always heal back to the cheap tier. If you update the cache after every tier 2+ win, the suite gets faster as it runs, not slower.
  • Surface the recurring breakers. Healing makes the test suite robust, but the app still has unstable selectors. The telemetry let us fix the actual source.