# Self-Healing UI Test Addon

A lightweight, plug-and-play **Cucumber + Selenium** test layer that:

- **logs every step's pass/fail**, timing and error to **PostgreSQL**;
- **fingerprints** each element it touches on a successful run (a "map" of the test);
- **self-heals** when a locator breaks — it scans the live DOM, matches the stored
  fingerprint, and recovers the element within the same call;
- **learns**: every locator that has ever worked (including past heals) is tried first next time.

No sidecar services, no Docker, one Postgres schema, one `find()` call. It is a slimmer,
in-process take on the idea behind [Healenium](https://healenium.io) — which stores heal data
in its own Postgres-backed backend + selector-imitator services over HTTP. Here everything runs
in-process and we own the schema, so we can also record the exact per-step pass/fail audit.

## Running the recorded tests (CI) — start here

Tests here are generated by the **automate_auto** recorder: record by clicking your app, click
**Generate**, push this project to your own git, and let Jenkins run `mvn test`. **No file edits.**

**Prerequisites** (only where tests run, e.g. the Jenkins agent): **JDK 17**, **Maven**, and a
**browser** (Selenium Manager auto-downloads the driver). Postgres is *optional* (self-healing
history) — the database + tables **auto-create** on first run, and tests still run without it.

**Run:**
```bash
mvn test                                        # everything
mvn test -Dcucumber.filter.tags="@myLoginTest"  # one recorded test (each gets a unique @<name> tag)
```

**Optional CI knobs (safe defaults):**

| Goal | How |
|------|-----|
| Parallel (~1 browser per core) | `mvn test -Dtest.forkCount=1C` |
| Retry flaky scenarios | `mvn test -Dtest.rerun=2` |
| Selenium Grid / cloud instead of local | `HEALING_REMOTE_URL=http://grid:4444/wd/hub` |
| Headless / browser / DB / driver | `HEALING_HEADLESS`, `HEALING_BROWSER`, `HEALING_DB_*`, `HEALING_DRIVER_PATH` |

Env vars override `src/main/resources/healing.properties`. **Jenkins:** point a Pipeline job at the
generated `Jenkinsfile` (has a `TAGS` build param). **Docker:** `docker build -t healing-tests .`
then `docker run --rm healing-tests mvn -B test -Dcucumber.filter.tags="@myTest"`.

**Layout:** each recorded test gets its own package + runner (`Run<Name>Test`) so two tests never
clash on duplicate steps. Self-healing features → `features/`, plain → `features_plain/`. Reports in
`target/cucumber-reports/`, screenshots in `target/healing-screenshots/`.

The rest of this document describes the **self-healing framework internals**.

## Reporting & integrations (optional)

Every run writes an **Allure** result set to `target/allure-results` — view it with the Allure CLI:

```bash
allure serve target/allure-results
```

Install the CLI once: `scoop install allure` (Windows), `brew install allure` (macOS), or from
https://github.com/allure-framework/allure2/releases.

To push a pass/fail summary or results to **Slack, Microsoft Teams, TestRail, or Xray**, copy
`reporting.properties.example` to `reporting.properties` (gitignored) and fill in only what you use,
then after a run:

```bash
mvn test
java -cp target/test-classes com.example.support.TestReporter
```

Unconfigured targets (and air-gapped runs) are skipped. TestRail matches results by a `C<number>` in
the **scenario name** (e.g. `Scenario: C1234 …`). In CI, set the same values as `REPORT_*` secrets —
the generated `Jenkinsfile` / GitHub Actions run the reporter automatically.

## How it works

```
step code ── driver.find(ByKey.of("login.submit"), By.cssSelector("button[type=submit]"))
                       │
                       ▼
              LocatorResolver.find()
              ├─ primary matches?  → yes → save Fingerprint + locator history, return element
              └─ no  → heal:  history locators → fingerprint-derived locators → DOM scan
                              score each against the stored Fingerprint (weighted similarity)
                              best ≥ threshold?  → log heal_event, promote locator, return element
                                               else → NoSuchElementException (a real failure)
```

Healing lives at the **element-resolution boundary** (inside `find()`), *not* by re-running
Cucumber steps — re-running a whole step risks double side-effects (double submits). The step
never sees the miss; the heal is logged out of band.

> **Just want to add this to an existing project?** Follow [SETUP.md](SETUP.md) — a step-by-step,
> copy-paste guide (including how to point at a driver you downloaded yourself). The section below
> is the same thing in brief.

## Drop into an existing By-based project (zero rewrites)

If your project already locates elements with `By.id` / `By.cssSelector` / `By.xpath`, wrap your
driver once and **change nothing else**:

```java
// before:  WebDriver driver = new ChromeDriver();
WebDriver driver = new SelfHealingWebDriver(new ChromeDriver());

// unchanged — these now fingerprint on success and self-heal on failure:
driver.findElement(By.id("username")).sendKeys("alice");
driver.findElement(By.cssSelector("button[type=submit]")).click();
driver.findElement(By.xpath("//h1[@class='dashboard-title']"));
```

Healing is keyed on the locator string, so it stays stable as long as your test keeps the same
locator — i.e. exactly when the *page* changed but your code didn't. Step logging via
`StepLoggingPlugin` is independent and optional; add it to your runner to also get per-step
pass/fail rows and heals FK-linked to steps. Set `HEALING_DB_URL` and the schema auto-creates.

Use the `ByKey` API (below) only if you additionally want history to survive *you* changing a
locator in your own test code.

## Adopt it (explicit ByKey form)

1. **Depend on it** — add this module to your test project (or copy `src/main/`).
2. **Point it at your DB + app** via env vars (preferred) or `src/main/resources/healing.properties`:
   - `HEALING_DB_URL`, `HEALING_DB_USER`, `HEALING_DB_PASSWORD` — the schema is created
     automatically on first run (idempotent `CREATE TABLE IF NOT EXISTS`).
   - `BASE_URL` — the app under test. `HEALING_BROWSER` (`chrome`/`firefox`), `HEALING_HEADLESS`.
3. **Wire the runner** — glue package `com.clanstop.healing` + the `StepLoggingPlugin`
   (see `RunCucumberTest`). That is the whole integration surface.
4. **Write steps** — inject `DriverManager`, then call
   `drivers.get().find(ByKey.of("logical.name"), By.…)` instead of `findElement`. The `ByKey`
   is a stable name so fingerprints/history survive a locator change.

```java
public class LoginSteps {
    private final DriverManager drivers;
    public LoginSteps(DriverManager drivers) { this.drivers = drivers; }

    @When("I sign in as {string} with password {string}")
    public void signIn(String user, String pass) {
        var d = drivers.get();
        d.find(ByKey.of("login.username"), By.id("username")).sendKeys(user);
        d.find(ByKey.of("login.submit"), By.cssSelector("button[type=submit]")).click();
    }
}
```

## Run

```bash
# schema auto-creates on first run; set your DB + target app first
export HEALING_DB_URL=jdbc:postgresql://localhost:5432/healing
export BASE_URL=https://your-app.example.com
mvn test
```

## Screenshots on failure + "what changed" on heal

- **Failed step → screenshot.** When a step fails, `StepLoggingPlugin` saves a PNG to
  `HEALING_SCREENSHOT_DIR` (default `target/healing-screenshots`) and stores the path in
  `step_result.screenshot_path`. Toggle with `HEALING_SCREENSHOT_ON_FAILURE=false`. No per-step
  hook needed — the plugin has the exact step status, so skipped steps don't produce noise.
- **Heal → diff of what changed + a screenshot.** Every successful heal records a human-readable
  description in `heal_event.changes`, e.g. `id 'submit-btn'→'submit-button'; class +[v2] -[old];
  text ~similar`, computed by diffing the remembered fingerprint against the element it healed to.
  It also captures a screenshot with the healed element **outlined in red** (path in
  `heal_event.screenshot_path`), so you can eyeball exactly what it matched. Toggle with
  `HEALING_SCREENSHOT_ON_HEAL=false`.

## Inspect what happened

```sql
-- step outcomes (with failure screenshots) for the latest run
SELECT step_ordinal, step_text, status, duration_ms, error_message, screenshot_path
FROM step_result WHERE run_id = (SELECT max(id) FROM test_run) ORDER BY id;

-- every heal: confidence, winning strategy, what changed, and a highlighted screenshot
SELECT logical_key, failed_by, healed_by_type, healed_by_value, score, strategy,
       changes, screenshot_path, healed_at
FROM heal_event ORDER BY healed_at DESC;
```

## Tuning & limits

- `HEALING_THRESHOLD` (default `0.72`) — raise to be stricter about accepting a heal.
- `HEALING_VOLATILE_ID_REGEX` — ids matching this are treated as auto-generated and down-weighted.
- `HEALING_DOMSCAN_LIMIT` — cap on elements scored in the broad fallback.
- **Best-effort persistence**: if Postgres is down, logging is skipped and tests still run;
  healing then works only from in-run in-memory history.
- **Scope (v1)**: heals within the current document/frame. Shadow DOM and cross-iframe
  healing are not yet supported.
- **First run**: with empty history/fingerprints, only the primary locator is available — so
  your locators should pass on run 1, which seeds the tables.
```
