tamash-selenium

tamash-selenium is a plug-and-play self-healing add-on for Selenium Java. All you need to do is wrap your WebDriver once — everything downstream (Page Objects, @FindBy fields, helper/util layers, inside a WebDriverWait) is healing-aware automatically.

No code changes required beyond the wrap, if you're following standard Selenium best practices.

Why you need this

Websites change often. A button gets renamed or moved, and your test can't find it anymore — even though the app still works fine for real users. Normally, that just means a broken test.

tamash-selenium fixes this automatically. When findElement can't find an element, it locates it on the live page — with a free rule-based matcher or an AI model — and retries. If it succeeds, your test keeps going. If not, it fails normally, exactly as it would have without the package. Every attempt is logged, and a real fix can be turned into permanent source code with one command — see Making a heal permanent below.

Here are the detailed steps to use this package.

Step 1: Install it

<dependency>
  <groupId>com.vibetestq.qtpsudhakar</groupId>
  <artifactId>tamash-selenium</artifactId>
  <version>0.2.0</version>
</dependency>

Pulls in Selenium 4 transitively. Selenium 4.6+ provisions the browser drivers itself. Requires Java 21+ at runtime.

junit-jupiter-api is an optional dependency (a JUnit 5 project already has it on its own classpath; a TestNG-only project must not pull it in transitively — it doesn't). TestNG and Cucumber are optional integrations too — this package declares org.testng:testng and io.cucumber:cucumber-java as provided, so add whichever your project already uses.

Step 2: Connect an AI provider (optional)

With no configuration at all, healing uses the rule-based tamash provider — no key, no network, no tokens; it text-matches the element's decoded name against the page's DOM accessibility snapshot and never guesses. Good for well-named, Page-Object-style suites.

For stronger healing (semantic reasoning over the page), pick one of Ollama, OpenAI, Anthropic (Claude), Google Gemini — or, if you don't have an API key issued to you, your own Claude or GitHub Copilot subscription instead.

Create a file named .env in your project root (or set the same names as real env vars / -D system properties — all three are read, in this precedence: OS env var → -D system property → .env):

# Master on/off switch. Leave this as true, or remove the line entirely.
HEALER_ENABLED=true

# Pick one: tamash | ollama | ollama-local | openai | anthropic | gemini
#           | claude-subscription | copilot-subscription
HEALER_PROVIDER=ollama

# Optional, off by default — see "Action recovery" below.
# HEALER_ACTION_RECOVERY_ENABLED=true

# --- Ollama Cloud (https://ollama.com) ---
OLLAMA_MODEL=gpt-oss:120b
OLLAMA_API_KEY=

# --- Self-hosted Ollama instead of Ollama Cloud ---
# HEALER_PROVIDER=ollama-local
# OLLAMA_LOCAL_MODEL=
# OLLAMA_LOCAL_BASE_URL=http://localhost:11434
# OLLAMA_LOCAL_API_KEY=            # optional — only if your deployment sits behind a gateway

# --- OpenAI ---
# OPENAI_MODEL=gpt-4o-mini
# OPENAI_API_KEY=

# --- Anthropic (Claude) ---
# ANTHROPIC_MODEL=claude-haiku-4-5
# ANTHROPIC_API_KEY=

# --- Google Gemini — use a -flash-lite model; a full -flash model thinks by default and is slow ---
# GEMINI_MODEL=gemini-flash-lite-latest
# GEMINI_API_KEY=

# --- Claude subscription (no API key — uses your Claude subscription) ---
# HEALER_PROVIDER=claude-subscription
# CLAUDE_SUBSCRIPTION_MODEL=claude-haiku-4-5
# CLAUDE_CODE_OAUTH_TOKEN=

# --- GitHub Copilot subscription (no API key — uses your Copilot subscription/free tier) ---
# HEALER_PROVIDER=copilot-subscription
# COPILOT_SUBSCRIPTION_MODEL=

Just fill in the API key and model for whichever one you want to use, and leave the rest as-is (or delete them).

Getting a free Ollama key (fastest way to get started)

Ollama Cloud is a quick, free way to get an API key without signing up for OpenAI/Anthropic/Gemini billing.

  1. Go to ollama.com and create an account.
  2. Once signed in, go to ollama.com/settings/keys.
  3. Create a new API key and copy it.
  4. Paste it into your .env file:
HEALER_ENABLED=true
HEALER_PROVIDER=ollama
OLLAMA_MODEL=gpt-oss:120b
OLLAMA_API_KEY=paste_your_key_here

That's all you need — no other variables required.

Using your Claude or GitHub Copilot subscription instead of an API key

If you don't have an API key issued to you but do have a personal Claude (Pro/Max/Team/Enterprise) or GitHub Copilot subscription (including the free tier), you can use that instead — no billing setup, no key to paste anywhere.

Claude subscription — works both locally and unattended in CI:

claude login                    # one-time, locally (needs the Claude Code CLI installed)
HEALER_PROVIDER=claude-subscription
CLAUDE_SUBSCRIPTION_MODEL=claude-haiku-4-5

For CI, generate a long-lived token once (claude setup-token) and set it as a secret instead of logging in interactively:

CLAUDE_CODE_OAUTH_TOKEN=the-token-you-copied

GitHub Copilot subscription — needs the optional com.github:copilot-sdk-java dependency and the copilot CLI signed in locally once:

copilot                          # sign in once, locally
HEALER_PROVIDER=copilot-subscription

One thing worth knowing: only Claude and Copilot support this "subscription, no API key, works in CI too" combination. Both were verified end-to-end against a real self-healing run in this project's own CI.

No AI at all: HEALER_PROVIDER=tamash

Resolves a broken locator by text-matching its decoded description against the page's DOM accessibility snapshot, plus the same near / adjacent structural widening the AI providers use. No key, no network, no tokens. The tradeoff: it never guesses — an ambiguous or weak match declines rather than picks, and there is no action-recovery fallback.

Step 3: Check your setup

mvn exec:java -Dexec.args="doctor"

Checks: AI provider connectivity (an actual call), the implicit-wait setting, brittle CSS/XPath locators bound to a non-descriptive variable name, locators declared inline in test files rather than a Page Object, and whether the coding-agent skill is installed and current. Color-coded output plus a summary table.

If it finds issues, the fastest fix is to open the project in an AI coding assistant and ask it to address what's flagged, or follow the bundled coding-agent skill, which does exactly that in a structured way.

Step 4: Use it in your tests

Whichever runner you use, the rule is the same: get the WebDriver from the integration (or wrap your own), write your test normally. Every element found through it is healing-aware.

Plain wrap — any runner, no integration required

import com.vibetestq.qtpsudhakar.tamash.SelfHealingDriver;

WebDriver driver = SelfHealingDriver.wrap(new ChromeDriver());   // RemoteWebDriver / Grid / cloud all fine

JUnit 5

import com.vibetestq.qtpsudhakar.tamash.junit.UseTamashSelenium;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

@UseTamashSelenium
class LoginTest {
  @Test
  void logsIn(WebDriver driver) {
    driver.get("https://the-internet.herokuapp.com/login");
    driver.findElement(By.id("username")).sendKeys("tomsmith");
    driver.findElement(By.id("password")).sendKeys("SuperSecretPassword!");
    driver.findElement(By.cssSelector("button[type='submit']")).click();
  }
}

WebDriver, JavascriptExecutor, and TakesScreenshot are all injectable (same instance). Fresh driver per method by default; TAMASH_REUSE_DRIVER=true for one per class.

@FindBy / PageFactory

Swap PageFactory.initElements for TamashPageFactory.initElements in your Page Object constructor — the only change:

import com.vibetestq.qtpsudhakar.tamash.pagefactory.TamashPageFactory;

public class LoginPage {
  @FindBy(id = "username")               WebElement usernameTextbox;
  @FindBy(css = "button[type='submit']") WebElement loginButton;

  public LoginPage(WebDriver driver) {
    TamashPageFactory.initElements(driver, this);
  }
}

@FindBy / @FindBys / @FindAll on WebElement fields heal; the healer's description is the field name, decoded (usernameTextbox → "Username (textbox)"). Resolution stays lazy, so @CacheLookup and AjaxElementLocatorFactory still work (new TamashFieldDecorator(driver, new AjaxElementLocatorFactory(driver, 10)) + PageFactory.initElements(decorator, page) for a custom factory). apply-heals rewrites the @FindBy(...) annotation directly. @FindBy List<WebElement> fields use Selenium's default (unhealed).

TestNG

Extend TamashSeleniumTestNgTest, use protected WebDriver driver. The listener auto-registers via ServiceLoader — no @Listeners, no testng.xml entry.

public class LoginTest extends TamashSeleniumTestNgTest {
  @Test public void logsIn() {
    driver.get("https://the-internet.herokuapp.com/login");
    driver.findElement(By.id("username")).sendKeys("tomsmith");
  }
}

Add your existing TestNG dependency (shipped provided). If your suite already has a mandatory base class, copy the four @Before/@AfterClass/Method methods out of TamashSeleniumTestNgTest and keep the ServiceLoader-registered listener.

Cucumber

Add com.vibetestq.qtpsudhakar.tamash.cucumber to your glue.

@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME,
    value = "com.acme.steps,com.vibetestq.qtpsudhakar.tamash.cucumber")
class RunCucumberTest {}
import static com.vibetestq.qtpsudhakar.tamash.cucumber.TamashSeleniumScenario.driver;

public class LoginSteps {
  @When("I sign in as {string} / {string}")
  public void signIn(String user, String pass) {
    driver().findElement(By.id("username")).sendKeys(user);
    driver().findElement(By.id("password")).sendKeys(pass);
    driver().findElement(By.cssSelector("button[type='submit']")).click();
  }
}

Hooks run at order = 0. Per-scenario heals are attached via scenario.attach(...).

A quick tip for better results: descriptive names

By.id(...) / By.name(...) carry their own meaning. A raw By.cssSelector(...) / By.xpath(...) doesn't — bind it to a descriptive field / variable and the healer decodes the name: txtEmployeeId → "Employee Id (textbox)", submitButton → "Submit (button)" (deterministic, no AI). Decoding works when the locator is on the same line as its findElement call, or is a @FindBy / Page Object field the call references by name. When nothing decodes, the healer falls back to the raw selector text — the AI providers also receive the raw undecoded name, the broken selector, and the enclosing class name, so even a terse txtUsrNm gives the model something to expand.

Through a WebUtil layer: when a locator is passed into a helper — WebUtil.click(driver, loginButton) — the name is resolved from the caller's line (loginButton), not from the util's by parameter, by walking up the stack. Give util-call arguments locator-ish names and this works automatically.

Explicit hintTamash.hint(...) — for keyword-driven suites, opaque names (txtSSN), or heavy indirection where no usable name reaches the call site:

public static void click(By locator, String name) {
  try (var h = Tamash.hint(name)) {          // ← the only addition
    getElement(locator).click();
  }
}

The hint takes precedence over the automatic decode and is passed to the AI provider as the element's name. It does not change whether a heal is attempted.

What gets healed (and what doesn't)

Intercepted on WebElement: click, sendKeys, clear, submit, and the read methods (getText, getAttribute, getDomAttribute, getDomProperty, getCssValue, isDisplayed, isEnabled, isSelected, getTagName, getAccessibleName, getAriaRole, getRect). Plus findElement on the driver and on elements — findElements (plural) is never healed, by design: it returns an empty list for a broken locator rather than throwing, and healing it would risk silently resolving to the wrong element for a presence check.

SituationHealed?
driver.findElement(brokenBy) / element.findElement(brokenBy)✅ at find time
@FindBy field, plain PageFactory.initElements(wrappedDriver, page)✅ (resolves through the wrapped driver)
Broken locator inside wait.until(...)ExpectedConditions, custom conditions✅ (first few polls deferred as "still loading", then it heals; the heal cache keeps the rest of the polls free)
A WebUtil / keyword layer wrapping any of the above
StaleElementReferenceException acting on an element whose page changed✅ (re-find with the original locator first, then heal)
ElementNotInteractableException / ElementClickInterceptedException⚙️ action recovery (opt-in, see below) — off by default: surfaces as the real exception
The Actions (advanced interactions) API, Select internals beyond the WebElement it wraps, anything inside a WebDriverWait's own polling internalsNot intercepted directly (a Select built on a healed element still works — the element itself heals)

SelfHealingDriver.wrap(...) pins Selenium's implicit wait to 0 (TAMASH_KEEP_IMPLICIT_WAIT=true to keep yours). Use explicit WebDriverWait for synchronisation as normal. TAMASH_ACTION_TIMEOUT_MS (default 10000) bounds the healer's own snapshot / JS calls, not your test's waits.

Assertions

A broken locator inside assertEquals(..., driver.findElement(x).getText()) heals — the assertion is checking page content, not whether that selector string is current. Healed assertion finds log as [self-healer][assertion] ….

HEALER_ASSERTIONSBehaviour
unset / healheal normally
warnheal, flag the test, print a summary of affected tests at JVM shutdown
stricta broken locator inside an assertion fails natively — no heal

Assert-absent is never healed (any mode): assertThrows(NoSuchElementException.class, …), ExpectedConditions.invisibilityOfElementLocated / stalenessOf / numberOfElementsToBeLessThan, and helper method names containing absent / notPresent / gone. Use driver.findElements(by).isEmpty() (never healed) for absence checks.

Getting a durable locator directly: Bindings.getDurable

import com.vibetestq.qtpsudhakar.tamash.bindings.Bindings;

By durable = Bindings.getDurable(driver, By.xpath("//div[3]/form/input[2]"));   // or (driver, by, "sendKeys")
driver.findElement(durable).sendKeys("value");

Resolves the given By to an element and derives the most durable By for it — the same derivation the healer uses internally. Throws if nothing durable could be derived.

Not paying for the same heal twice

A successful heal is remembered in .tamash-selenium/heals.jsonl. Next time that exact locator breaks the same way, the confirmed selector is tried first — no snapshot, no AI call. The cache persists on disk locally across runs; in CI it only helps within a single run (a fresh checkout has no cache). Landing the real fix with apply-heals is what eliminates repeat AI calls in CI.

Each line records the broken initialSelector, the structured suggestion (the source of truth apply-heals re-derives from), and — for quick human review — the rendered newLocator (By.name("firstName")) and newFindBy (@FindBy(name = "firstName")) forms. A one-shot heal that could not be reduced to a durable selector has no suggestion/newLocator and carries needsReview: true with a reviewNote.

Making a heal permanent: apply-heals

mvn test                                            # heals at runtime, records what it healed
mvn exec:java -Dexec.args="apply-heals --dry-run"   # preview the By.… rewrite
mvn exec:java -Dexec.args="apply-heals"             # apply it (prompts first, at a real terminal)

Rewrites a By.xxx("…") literal, a @FindBy(...) annotation, or a private final By field = By.x("…") declaration line. Each run writes apply-heals-report.json / .md under .tamash-selenium/ (timestamped copies under history/) and a verify-heals script (.sh / .cmd) that re-runs exactly the affected tests with HEALER_ENABLED=false — a pass proves the rewritten selector stands alone.

Flags: --dry-run, --logs-dir <path> (merge sharded heals.jsonl files from a sharded CI run), --yes / -y (skip the confirmation prompt — always skipped automatically in CI / non-interactive runs).

HTML step report

mvn test -DTAMASH_REPORT=target/tamash-report.html

Per-test step timeline (action, duration, entered value), which steps healed (recovered selector, provider, token cost), the DOM snapshot on an unrecovered failure, summary charts. Works for all three runners; zero overhead when TAMASH_REPORT is unset.

Action recovery (optional)

A second-order fallback for when the element was found but the action itself failed — ElementClickInterceptedException (something covers the element) or ElementNotInteractableException. Off by default; the real exception surfaces unchanged. Enable it to have an AI provider pick a recovery tactic — scroll into view, a direct JS click/dispatch bypassing Selenium's own interactability checks, or a short wait-then-retry:

HEALER_ACTION_RECOVERY_ENABLED=true

Needs a real AI provider — the rule-based tamash provider always declines a tactic (it has nothing to reason from). Verified in this project's own CI to reliably recover a click covered by an overlay, across OpenAI, Anthropic, and Gemini.

Stale elements are handled separately, always on, no flag needed: a StaleElementReferenceException first gets a cheap re-find with the original locator, then a full selector heal if that still fails.

Installing the coding-agent skill

The package ships an orchestration skill for AI coding assistants (Claude Code, Cursor, GitHub Copilot, and other tools that read .agents/skills/) that drives the local run → review → apply-heals → verify → land loop for you. Install it into your project:

mvn exec:java -Dexec.args="init-skill"

Copies SKILL.md + reference docs into both .claude/skills/tamash-selenium/ (Claude Code) and .agents/skills/tamash-selenium/ (the cross-tool standard). Same content in both; no per-agent format conversion.

FlagWhat it does
--target claude|agentsinstall just one location
--userinstall under your home directory — covers every project on the machine
--forceoverwrite a hand-edited copy
--dry-runshow what would happen, change nothing

A version marker is written so doctor can flag when the installed skill has fallen behind the package — re-run init-skill to refresh.

Environment variables

VariableDefaultPurpose
HEALER_ENABLEDtrueMaster switch. Any value other than false / 0 leaves healing on.
HEALER_PROVIDERtamashollama | ollama-local | openai | anthropic | gemini | claude-subscription | copilot-subscription | tamash.
HEALER_ASSERTIONShealheal | warn | strict. Assert-absent is never healed.
TAMASH_KEEP_IMPLICIT_WAITfalsetrue keeps your implicit wait (wrapping pins it to 0 otherwise).
OLLAMA_MODEL / OLLAMA_API_KEY / OLLAMA_BASE_URL— / — / https://ollama.comOllama Cloud.
OLLAMA_LOCAL_MODEL / OLLAMA_LOCAL_BASE_URL / OLLAMA_LOCAL_API_KEY— / http://localhost:11434 / —Self-hosted Ollama. Key optional.
OPENAI_MODEL / OPENAI_API_KEYOpenAI. No default model — must be set.
ANTHROPIC_MODEL / ANTHROPIC_API_KEYAnthropic (Claude). No default model — must be set.
GEMINI_MODEL / GEMINI_API_KEYGoogle Gemini. Use a -flash-lite model.
GEMINI_THINKINGoffon restores Gemini's default thinking budget (off sends reasoning_effort: low).
CLAUDE_CODE_OAUTH_TOKEN / CLAUDE_SUBSCRIPTION_MODEL— / claude-haiku-4-5claude-subscription.
COPILOT_SUBSCRIPTION_MODELcopilot-subscription.
HEALER_ACTION_RECOVERY_ENABLEDfalseOpt-in AI action recovery (scroll / force / wait / dispatch).
TAMASH_BROWSERchromechrome | firefox | edge | safari.
TAMASH_REUSE_DRIVERfalseReuse one driver per test class instead of one per method.
HEADLESStruefalse runs the browser headed.
TAMASH_ACTION_TIMEOUT_MS10000Bounds the healer's own snapshot / JS calls.
TAMASH_REPORTunsetOutput path for the HTML step report.
TAMASH_SOURCE_ROOTSunsetExtra source roots to search for Page Object description recovery, for multi-module builds.
TAMASH_DEBUGunsetPrint DOM-snapshot capture diagnostics to stderr.

CLI commands

CommandFlagsWhat it does
mvn exec:java -Dexec.args="doctor"--dir <path>Pre-flight checks: connectivity, implicit wait, locator naming, inline locators, agent-skill install state.
mvn exec:java -Dexec.args="apply-heals"--dry-run, --logs-dir <path>, --yesRewrite healed locators into source, write reports + a verify script.
mvn exec:java -Dexec.args="init-skill"--target claude|agents, --user, --force, --dry-run, --dir <path>Copy the coding-agent skill into .claude/skills/ and .agents/skills/.

License

Apache License, Version 2.0 — free to use, modify, and redistribute, including commercially, as long as you keep the copyright and license notices.

Support

Bugs, feature requests, and questions: open an issue on this repository. See the Getting support section on the home page for the template guide and issue-label filters.