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.
- Go to ollama.com and create an account.
- Once signed in, go to ollama.com/settings/keys.
- Create a new API key and copy it.
- Paste it into your
.envfile:
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 hint — Tamash.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.
| Situation | Healed? |
|---|---|
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 internals | Not 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_ASSERTIONS | Behaviour |
|---|---|
unset / heal | heal normally |
warn | heal, flag the test, print a summary of affected tests at JVM shutdown |
strict | a 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.
| Flag | What it does |
|---|---|
--target claude|agents | install just one location |
--user | install under your home directory — covers every project on the machine |
--force | overwrite a hand-edited copy |
--dry-run | show 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
| Variable | Default | Purpose |
|---|---|---|
HEALER_ENABLED | true | Master switch. Any value other than false / 0 leaves healing on. |
HEALER_PROVIDER | tamash | ollama | ollama-local | openai | anthropic | gemini | claude-subscription | copilot-subscription | tamash. |
HEALER_ASSERTIONS | heal | heal | warn | strict. Assert-absent is never healed. |
TAMASH_KEEP_IMPLICIT_WAIT | false | true keeps your implicit wait (wrapping pins it to 0 otherwise). |
OLLAMA_MODEL / OLLAMA_API_KEY / OLLAMA_BASE_URL | — / — / https://ollama.com | Ollama Cloud. |
OLLAMA_LOCAL_MODEL / OLLAMA_LOCAL_BASE_URL / OLLAMA_LOCAL_API_KEY | — / http://localhost:11434 / — | Self-hosted Ollama. Key optional. |
OPENAI_MODEL / OPENAI_API_KEY | — | OpenAI. No default model — must be set. |
ANTHROPIC_MODEL / ANTHROPIC_API_KEY | — | Anthropic (Claude). No default model — must be set. |
GEMINI_MODEL / GEMINI_API_KEY | — | Google Gemini. Use a -flash-lite model. |
GEMINI_THINKING | off | on restores Gemini's default thinking budget (off sends reasoning_effort: low). |
CLAUDE_CODE_OAUTH_TOKEN / CLAUDE_SUBSCRIPTION_MODEL | — / claude-haiku-4-5 | claude-subscription. |
COPILOT_SUBSCRIPTION_MODEL | — | copilot-subscription. |
HEALER_ACTION_RECOVERY_ENABLED | false | Opt-in AI action recovery (scroll / force / wait / dispatch). |
TAMASH_BROWSER | chrome | chrome | firefox | edge | safari. |
TAMASH_REUSE_DRIVER | false | Reuse one driver per test class instead of one per method. |
HEADLESS | true | false runs the browser headed. |
TAMASH_ACTION_TIMEOUT_MS | 10000 | Bounds the healer's own snapshot / JS calls. |
TAMASH_REPORT | unset | Output path for the HTML step report. |
TAMASH_SOURCE_ROOTS | unset | Extra source roots to search for Page Object description recovery, for multi-module builds. |
TAMASH_DEBUG | unset | Print DOM-snapshot capture diagnostics to stderr. |
CLI commands
| Command | Flags | What 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>, --yes | Rewrite 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.