Self-healing test automation framework for fragile frontends. When a selector breaks, PhoenixQA doesn't crash β it heals.
Frontend tests break constantly β not because the feature is broken, but because the page underneath it changed.
A class was renamed. A data-testid rotated. A wrapper <div> appeared around a button after a refactor. A component moved into a Shadow DOM boundary.
PhoenixQA intercepts those failures, feeds the context to an LLM, and either:
- proposes a fix for human review (Safe Mode β live)
- applies the fix automatically within a confidence/budget policy and continues (Autonomous Mode β live)
Every decision β human-reviewed or autonomous β is logged today, including which provider made the call, how many tokens it cost, and how long it took. Once Healing History (Sprint 7) lands, that log becomes the basis for a self-training loop that improves future healing (Sprint 8) β not yet built, but the logging that feeds it already is.
"Test fails even though the app is fine" has more than one root cause. PhoenixQA classifies failures into four types, but builds them in phases rather than all at once:
| Failure type | Status |
|---|---|
selector_not_found β classic renamed/rotated selector |
β Live (Sprint 2-5) |
detached_from_dom β framework re-render mid-action |
π¬ Investigated (Sprint 6A) β no reproduction found for Locator-based automation across four escalating attempts; deprioritized based on current evidence, not proven impossible β see docs/gaps.md Gap #4 |
not_visible β element exists but hidden/blocked |
π Candidate for the next vertical slice β not yet started |
timeout_waiting β never reaches an actionable state |
π Candidate for the next vertical slice β not yet started |
Why phase it: better to prove the full pipeline (collect β analyze β heal
β validate) end-to-end on one well-understood failure type first, then
extend to the others with real lessons learned β rather than build four
shallow strategies at once. See LEARNINGS.md for the full reasoning, or
docs/gaps.md for a quick-scan status table of every
open architectural question.
Note on detached_from_dom: the original working assumption (based
on direct Selenium/Salesforce Lightning experience) was that framework
re-render-mid-action failures would be the dominant real-world case for
PhoenixQA to handle. Sprint 6A tested that assumption directly β a real
reproduction mechanism (componentRemount.jsx) was built and run across
four escalating configurations, ending with a deterministic,
zero-timing-luck trigger. The consistent finding: no observable
DETACHED_FROM_DOM failure, at any setting, for this project's specific
interaction pattern. Research into Playwright's own documentation and
issue tracker offers a likely explanation, not just a coincidence β
Locator-based actions (all BasePage ever uses; never ElementHandle)
are documented to retry automatically on mid-action detachment, which
would plausibly absorb much of the failure class Selenium-style
automation is vulnerable to. This is deliberately framed as evidence
supporting a deprioritization, not proof that the failure type doesn't
exist β see LEARNINGS.md "Sprint 6A conclusion" for the precise scope
of the claim, the full hypothesis β experiment β finding β decision
trail, sources, and what it means for the project's remaining scope.
Important framing shift for the remaining failure types (Gap #12):
for selector_not_found, the selector itself is what's broken, and the
fix is a replacement selector. For the remaining failure types the
selector may be perfectly correct β what actually failed is the
in-flight ACTION (e.g. a click against an element that isn't yet
actionable, or never becomes actionable in time). PhoenixQA treats
selector_not_found as the special case where "recover the action" happens
to mean "propose a new selector" β the other failure types recover the
action a different way (retry with a wait, dismiss an overlay, scroll
into view, etc.). See docs/gaps.md Gap #12 and LEARNINGS.md Sprint 6.
How this project approaches quality as a whole β not just unit
tests on the framework's own code, but a layered strategy covering
integration, end-to-end behavior against a real LLM, regression
benchmarking of healing effectiveness, and non-functional resilience to
malformed model output β is laid out in
docs/testing-strategy.md.
The open empirical questions driving the project's direction β does
an LLM meaningfully outperform a cheap heuristic, which failure types
actually justify LLM-based healing, is model confidence a reliable
proxy for correctness, and others β are collected in one place in
docs/research_hypotheses.md, separate
from docs/gaps.md's architectural TODOs: a gap is something to build,
a hypothesis is something to find out.
Test Failure
β
βΌ
Failure Classifier β FailureType (selector_not_found, detached_from_dom, ...)
β
βΌ
Context Collector β one collector per FailureType (target architecture):
β selector_collector.py (DOM snapshot, weighted scoring) β live
β [next failure type] (planned β NOT_VISIBLE/TIMEOUT_WAITING)
βΌ
LLM Analyzer β Ollama (local) or Anthropic API β structured HealingAction
β SelectorReplacement (selector_not_found) β live
β RetryStrategy / WaitStrategy / VisibilityStrategy β declared, not yet implemented
β
ββββΊ Safe Mode β Human reviews full context, accepts/rejects β Ground Truth
β
ββββΊ Autonomous Mode β Confidence gate + budget check, auto-applies fix, retries
β
βΌ
Healing History β SQLite log of all decisions (Sprint 7)
β
βΌ
Self-Training β Few-shot context for better future repairs (Sprint 8)
Note: PhoenixQA recovers the ABILITY to perform an action after a
failure β it does not judge whether the resulting behavior was
business-correct (e.g. "did login actually succeed"). That judgment
stays with the test's own assertions, same as it always has. See
docs/gaps.md Gap #11 for why this boundary is deliberate.
Autonomous Mode raises one of three distinct exception types depending on why it didn't heal β HealingRejectedError (bad/low-confidence proposal), HealingLimitExceededError (budget exhausted), HealingFailedError (provider/API crashed) β so a CI failure report says exactly what happened, not just "healing didn't work."
Current implementation status: the Context Collector β LLM Analyzer β HealingAction split above is the target architecture for the next phase of work. As of today, ContextCollector is still a single class, prompt_templates.py is still one module, and providers still return HealingProposal (the selector_not_found-only shape) β the polymorphic collector/prompt structure and the HealingAction hierarchy are decided but not yet implemented. See docs/known-limitations.md for the precise current-vs-planned boundary.
Chaos App isn't randomized weirdness β each level isolates one variable and answers a specific research question. This is closer to a controlled experiment than a typical "Playwright + sample app" portfolio repo.
| Level | Mechanisms (cumulative) | Research question |
|---|---|---|
| LOW | selector rotation | Does the test survive a selector rename? |
| MEDIUM | + DOM structure mutation | Does the test survive a UI refactor? |
| HIGH | + async delay | Does the test survive a refactor + timing issues? |
Shadow DOM is a separate, independent flag (SHADOW_DOM_ENABLED), not a 4th level β it's a different kind of difficulty (structural DOM access), combinable with any level above (e.g. HIGH + Shadow DOM tests refactor + timing + structural access at once).
Mechanisms ranked by real-world realism (most enterprise frontends break this way most often):
| Mechanism | Realism | Why |
|---|---|---|
| DOM Mutation | 10/10 | UI library upgrades, wrapper changes, component migrations |
| Selector Rotation | 9/10 | Classic β renamed class/id/data-testid |
| Async Delay | 8/10 | Lazy loading, animations, network-dependent rendering |
| Shadow DOM | 5/10 | Real, but narrower β mostly Web Components / LWC-style platforms |
A 5th, independent mechanism, component remount / detach-mid-action (componentRemount.jsx, COMPONENT_REMOUNT_ENABLED), was built in Sprint 6A specifically to give detached_from_dom something real to classify against. It's implemented and verified live, but β see the Scope section above β did not reproduce the target failure against Locator-based interactions even under a deterministic, zero-timing-randomness trigger. It remains in the codebase as a verified research tool, not as an active target for the next healing strategy.
Controlling the chaos level:
# chaos_app/.env
VITE_CHAOS_LEVEL=HIGH # LOW | MEDIUM | HIGH
VITE_SHADOW_DOM_ENABLED=true # true | false β independent of level
VITE_COMPONENT_REMOUNT_ENABLED=false # true | false β independent of level, see note aboveEdit chaos_app/.env, then restart npm run dev. The "Active chaos config" panel at the top of the running app confirms which mechanisms are live β no guessing required.
Mechanism overrides (development/verification only, never for the official benchmark): each core mechanism (selector_rotation, dom_mutation, async_delay) can be forced on/off independently of the active level via VITE_OVERRIDE_SELECTOR_ROTATION / VITE_OVERRIDE_DOM_MUTATION / VITE_OVERRIDE_ASYNC_DELAY, useful for isolating one mechanism's effect during development without inventing a new named chaos level for every combination. CHAOS_LEVELS's three named scenarios remain the only thing the eventual benchmark runner configures via plain CHAOS_LEVEL.
End goal (Sprint 8 β Healing Benchmark Runner): run the full suite at every level, comparing No Healer vs. Heuristic Healer vs. LLM Healer β not just "it works," but "here's exactly how much the LLM adds over a cheap fuzzy-match baseline, and where."
| Chaos Level | No Healer | Heuristic Healer | LLM Healer |
|---|---|---|---|
| LOW | ~72% | ?% | ~98% |
| MEDIUM | ~51% | ?% | ~95% |
| HIGH | ~29% | ?% | ~90% |
The middle column is the actual experiment β a simple fuzzy/Levenshtein selector matcher (zero LLM calls, same provider interface as Anthropic/Ollama) might already solve a surprising fraction of cases. Without this baseline, "90% healed" doesn't prove the LLM was necessary.
Autonomous Mode has hard stop conditions from day one (max_attempts_total, token budget, max_time_per_heal) β no infinite LLM retry loops in CI, by design, not as a later hardening pass. Budget is tracked in tokens and elapsed time, never in currency β model pricing changes over time, token counts don't. See docs/architecture-decisions.md for the full reasoning.
PhoenixQA/
βββ LEARNINGS.md # chronological journal β problem β analysis β decision β test β conclusion
βββ docs/ # thematic indexes (fast lookup by topic, not by sprint)
β βββ gaps.md # all numbered architectural gaps, status at a glance
β βββ architecture-decisions.md
β βββ known-limitations.md
β βββ future-ideas.md
β βββ testing-strategy.md # unit/integration/e2e/regression-benchmark/non-functional plan + actual state
β βββ research_hypotheses.md # open empirical questions the project is trying to answer, not just architectural gaps to fix
βββ chaos_app/ # React/Vite β intentionally unstable test target
β βββ src/chaos/ # selectorRotation, domMutation, asyncDelay, shadowDom, componentRemount (Sprint 6A)
βββ phoenix/
β βββ collector/ # failure_classifier, context_collector (router, planned) + collectors/ (per-FailureType, planned)
β βββ healing/ # healer, safe_mode, decision_logger, autonomous_mode
β βββ ai/ # base_provider, ollama_provider, anthropic_provider,
β β # prompts/ (per-FailureType, planned), response_parser, provider_factory
β βββ training/ # Healing history (Sprint 7)
β βββ reporting/ # Allure Phoenix Healing Report (Sprint 9)
βββ pages/ # Page Objects for Chaos App (POM pattern)
βββ tests/
β βββ chaos/ # tests running against Chaos App
β βββ unit/ # tokenizer, classifier, parser, logger, healer tests
β βββ integration/
βββ config/
| Provider | When to use |
|---|---|
ollama |
Air-gapped / NDA environments, local LLM |
anthropic |
Cloud projects, best quality healing suggestions |
Switch via single env variable. No code changes.
| Sprint | Focus | Status |
|---|---|---|
| Sprint 0 | Repo scaffold, config, AI provider stubs | β Done |
| Sprint 1 | Chaos App β React/Vite, selector rotation, DOM mutation, async delay, Shadow DOM | β Done |
| Sprint 2 | Context Collector β selector_not_found only (DOM snapshot, weighted scoring) |
β Done |
| Sprint 3 | LLM Analyzer β prompt engineering, structured JSON response, confidence score | β Done |
| Sprint 4 | Safe Mode β Human-in-the-loop terminal review, JSON-lines decision log | β Done |
| Sprint 5 | Autonomous Mode β stop conditions (attempts/tokens/time budget), confidence policy gate, distinct exception types | β Done |
| Sprint 6 | Failure type expansion β architecture decided (action-recovery reframing, polymorphic collector, split prompts, HealingAction hierarchy); target failure type redirected after Sprint 6A findings |
π§ In progress |
| Sprint 6A | componentRemount.jsx (TIMEOUT + MOUSEDOWN triggers) + classifier extended to recognize DETACHED_FROM_DOM |
β
Done β controlled experiment (4 escalating configurations) found no reproduction against Locator-based automation; a finding about Playwright's architecture, not a mechanism gap. See LEARNINGS.md "Sprint 6A conclusion" |
| Sprint 6B-D | DetachedFromDomCollector / detached_prompt.py / RetryStrategy end-to-end, as originally scoped |
βΈοΈ Redirected to NOT_VISIBLE/TIMEOUT_WAITING based on the Sprint 6A finding; specific target not yet chosen |
| Sprint 7 | Healing History β SQLite store, decision log, healing correctness definition | β³ Planned |
| Sprint 8 | Healing Benchmark Runner β Heuristic provider baseline, few-shot self-training, Safe vs Auto metrics | β³ Planned |
| Sprint 9 | Allure Phoenix Report, CI/CD, demo GIF | β³ Planned |
Sprint 6 architectural decisions made before any code was written (full reasoning in LEARNINGS.md, indexed in docs/gaps.md Gap #12 and docs/architecture-decisions.md):
- The non-
selector_not_foundfailure types are reframed as action recovery, not selector replacement β there may be nothing wrong with the selector itself. ContextCollectorbecomes a router over oneBaseContextCollectorsubclass perFailureType, replacing the current if/elif ladder.- The prompt layer splits the same way β one prompt module per
FailureType, since "find a selector" and "should this action be retried, and after what wait" are different cognitive tasks for the model. HealingProposalis retired as the universal provider return type, replaced by aHealingActionhierarchy (SelectorReplacement,RetryStrategy,WaitStrategy,VisibilityStrategy) β a required, blocking refactor touchingHealer,safe_mode.py,decision_logger.py, andresponse_parser.py.
These four decisions remain sound regardless of which specific failure type gets the next vertical slice β they were designed to generalize, not built around detached_from_dom specifically. What changed after Sprint 6A is only the answer to "which failure type comes next": a controlled experiment against Playwright's Locator API (see the Scope section above and LEARNINGS.md "Sprint 6A conclusion" for the full hypothesis β experiment β finding β decision trail) found that detached_from_dom isn't a high-value target for this architecture right now, so the next vertical slice targets not_visible or timeout_waiting instead β the specific choice is an open decision.
# 1. Clone
git clone https://github.com/MarcinMikula/PhoenixQA.git
cd PhoenixQA
# 2. Install Python deps
pip install -r requirements.txt
playwright install chromium
# 3. Configure
cp .env.example .env
# Edit .env β choose AI provider, chaos level
# 4. Run the Chaos App (test target)
cd chaos_app
npm install
cp .env.example .env
npm run dev
# β http://localhost:5173
# 5. In a SEPARATE terminal (npm run dev keeps step 4's terminal busy):
cd ..
# Run tests against it β both Safe Mode (Sprint 4) and Autonomous Mode
# (Sprint 5) are live. Switch via .env: HEALING_MODE=safe | autonomous
#
# -s is REQUIRED for Safe Mode: it prompts for accept/reject via input(),
# and pytest swallows stdin/stdout without -s β the prompt never
# reaches the terminal and the run just hangs with no explanation.
# Autonomous Mode doesn't need -s (no prompts), but it doesn't hurt either.
pytest tests/chaos/ -m chaos -sHealing is confirmed working live as of Sprint 5 β Safe Mode and Autonomous Mode have both been run end-to-end against the real Chaos App and a local LLM, with selectors successfully healed and retried in place.
The actual demo artifact, though, is parked until Sprint 9: rather than a pile of terminal screenshots, the plan is a single Allure Healing Dashboard (success rate, healing timeline, confidence distribution, top repaired selectors, failure reasons, budget usage, provider comparison) β built once Sprint 6-8 (failure type expansion, healing history, benchmark runner) produce real data for it to render. See docs/future-ideas.md for the reasoning.
PhoenixQA is one piece of a larger AI-powered QA toolkit:
| Repo | Role |
|---|---|
| qa-automation-framework | POM/SOM skeleton β PhoenixQA heals its selectors |
| defect-pilot | AI bug reproduction & retest agent |
| llm-qa-toolkit | LLM-as-judge test framework for AI chatbots |
MIT