<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mohammad Saddam Hossain</title>
    <description>The latest articles on DEV Community by Mohammad Saddam Hossain (@shossain).</description>
    <link>https://dev.to/shossain</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3899606%2F62e22b2c-f5b7-493d-836a-f5f80b3843af.png</url>
      <title>DEV Community: Mohammad Saddam Hossain</title>
      <link>https://dev.to/shossain</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shossain"/>
    <language>en</language>
    <item>
      <title>Parallel Selenium Tests in Java Without Thread-Safety Bugs</title>
      <dc:creator>Mohammad Saddam Hossain</dc:creator>
      <pubDate>Mon, 27 Jul 2026 01:44:02 +0000</pubDate>
      <link>https://dev.to/shossain/parallel-selenium-tests-in-java-without-thread-safety-bugs-5290</link>
      <guid>https://dev.to/shossain/parallel-selenium-tests-in-java-without-thread-safety-bugs-5290</guid>
      <description>&lt;p&gt;Your suite is green. Someone sets &lt;code&gt;thread-count="4"&lt;/code&gt; to get CI under ten minutes, and now&lt;br&gt;
three tests fail. Re-run: two fail, and not the same two. Run it locally and everything&lt;br&gt;
passes.&lt;/p&gt;

&lt;p&gt;That's not flakiness in your application. That's your test framework being unsafe under&lt;br&gt;
concurrency, and it was unsafe the whole time — one thread was just hiding it.&lt;/p&gt;

&lt;p&gt;The advice you'll find in ten minutes of searching is "wrap the &lt;code&gt;WebDriver&lt;/code&gt; in a&lt;br&gt;
&lt;code&gt;ThreadLocal&lt;/code&gt;." That's correct, and it's about a third of the answer. Parallel execution&lt;br&gt;
exposes at least four distinct bugs, and &lt;code&gt;ThreadLocal&lt;/code&gt; fixes exactly one of them. This&lt;br&gt;
post covers all four, what the real fix looks like for each, and how to check your own&lt;br&gt;
suite without waiting for CI to tell you.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why a shared WebDriver breaks
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;WebDriver&lt;/code&gt; is not thread-safe, and the Selenium project has never claimed otherwise. A&lt;br&gt;
driver instance is a stateful handle to one browser session: one current URL, one active&lt;br&gt;
window, one set of cookies. Two threads holding the same reference are two tests driving&lt;br&gt;
one browser.&lt;/p&gt;

&lt;p&gt;The classic version looks like this, and almost every home-grown framework has shipped it&lt;br&gt;
at some point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseTest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;WebDriver&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// ← the bug&lt;/span&gt;

    &lt;span class="nd"&gt;@BeforeMethod&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ChromeDriver&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@AfterMethod&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;tearDown&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;quit&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On one thread this is fine. On four, &lt;code&gt;setUp()&lt;/code&gt; runs four times and the last assignment&lt;br&gt;
wins — so all four tests drive whichever browser was created last, while the other three&lt;br&gt;
sit open and idle until the JVM exits. Then &lt;code&gt;tearDown()&lt;/code&gt; fires four times on that one&lt;br&gt;
driver, and three of them throw.&lt;/p&gt;

&lt;p&gt;The failures this produces are genuinely confusing, because none of them look like a&lt;br&gt;
concurrency bug:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NoSuchElementException&lt;/code&gt; on an element that is definitely on the page — because another
thread navigated away between your &lt;code&gt;get()&lt;/code&gt; and your &lt;code&gt;findElement()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NoSuchSessionException&lt;/code&gt; / &lt;code&gt;SessionNotCreatedException&lt;/code&gt; — another thread already called
&lt;code&gt;quit()&lt;/code&gt; on the session you're using.&lt;/li&gt;
&lt;li&gt;Assertions that fail with the &lt;em&gt;other&lt;/em&gt; test's data, especially anything reading a logged-in
user's name.&lt;/li&gt;
&lt;li&gt;Chrome processes surviving the build and slowly eating the agent's RAM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Making the field non-static doesn't fix it either if anything else in the suite reaches for&lt;br&gt;
a driver statically — listeners and screenshot helpers are the usual culprits, and we'll&lt;br&gt;
come back to those.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bug 1 — the shared driver: ThreadLocal ownership, done properly
&lt;/h2&gt;

&lt;p&gt;The fix is to give each thread its own driver and make it impossible to reach another&lt;br&gt;
thread's. &lt;code&gt;ThreadLocal&amp;lt;WebDriver&amp;gt;&lt;/code&gt; does that: every thread that calls &lt;code&gt;get()&lt;/code&gt; sees only the&lt;br&gt;
value that thread set.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DriverFactory&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WebDriver&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;DRIVER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;DriverFactory&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DRIVER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// idempotent: safe under retry&lt;/span&gt;
        &lt;span class="no"&gt;DRIVER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ChromeDriver&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;WebDriver&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;WebDriver&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DRIVER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"No WebDriver on thread "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentThread&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;". create() was never called, or quit() already ran."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;WebDriver&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DRIVER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;quit&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="no"&gt;DRIVER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;                   &lt;span class="c1"&gt;// ← the line people forget&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details in there matter more than the &lt;code&gt;ThreadLocal&lt;/code&gt; itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;DRIVER.remove()&lt;/code&gt; is not optional.&lt;/strong&gt; This is the single most common way a "thread-safe"&lt;br&gt;
driver factory still breaks. Test runners reuse threads — TestNG's thread pool, JUnit 5's&lt;br&gt;
parallel executor, and every CI agent that runs more than one suite per JVM. If you call&lt;br&gt;
&lt;code&gt;quit()&lt;/code&gt; without &lt;code&gt;remove()&lt;/code&gt;, the next test scheduled onto that thread inherits a&lt;br&gt;
&lt;code&gt;ThreadLocal&lt;/code&gt; entry pointing at a dead session, and you get &lt;code&gt;NoSuchSessionException&lt;/code&gt; from a&lt;br&gt;
test that never did anything wrong. Worse, on a long-lived JVM the entries are a genuine&lt;br&gt;
memory leak: &lt;code&gt;ThreadLocalMap&lt;/code&gt; keys are weak, values are not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The null check needs to name the thread.&lt;/strong&gt; When this does go wrong, the difference&lt;br&gt;
between &lt;code&gt;NullPointerException&lt;/code&gt; and a message telling you which thread had no driver is&lt;br&gt;
about forty minutes of debugging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotent creation buys you retries.&lt;/strong&gt; If a retry listener re-runs a failed test on the&lt;br&gt;
same thread, &lt;code&gt;create()&lt;/code&gt; firing twice must not orphan a browser. Returning early when a&lt;br&gt;
driver already exists is the cheap version; explicitly quitting the old one is the thorough&lt;br&gt;
version. Pick one deliberately.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bug 2 — shared state that isn't the driver
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ThreadLocal&lt;/code&gt; on the driver fixes the driver. It does nothing for everything else your&lt;br&gt;
suite shares, and this is where the second wave of failures comes from once the obvious one&lt;br&gt;
is gone.&lt;/p&gt;

&lt;p&gt;Look for these three:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Page objects with mutable fields.&lt;/strong&gt; A page object that caches a &lt;code&gt;WebElement&lt;/code&gt;, or stores&lt;br&gt;
"the row I just clicked," is fine on one thread and a race on four. Page objects should&lt;br&gt;
hold a driver reference and nothing else — every piece of per-test state belongs in the&lt;br&gt;
test, or in a thread-scoped context object.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static test data.&lt;/strong&gt; A &lt;code&gt;static String orderId&lt;/code&gt; written by one test and read by another is&lt;br&gt;
the most common non-driver race. So is a shared &lt;code&gt;SimpleDateFormat&lt;/code&gt; (not thread-safe), a&lt;br&gt;
shared &lt;code&gt;HashMap&lt;/code&gt; used as a scratchpad, and a static counter used to generate "unique"&lt;br&gt;
usernames without an atomic type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anything a listener writes to.&lt;/strong&gt; A TestNG &lt;code&gt;ITestListener&lt;/code&gt; or JUnit 5 extension is invoked&lt;br&gt;
on the test's own thread, which is convenient — but if it accumulates into a plain&lt;br&gt;
&lt;code&gt;ArrayList&amp;lt;Result&amp;gt;&lt;/code&gt;, four threads appending concurrently will silently drop entries or&lt;br&gt;
throw &lt;code&gt;ArrayIndexOutOfBoundsException&lt;/code&gt; from inside &lt;code&gt;ArrayList.add&lt;/code&gt;. Use a concurrent&lt;br&gt;
collection, or a per-thread accumulator merged at the end.&lt;/p&gt;

&lt;p&gt;The rule that survives contact with real suites: &lt;strong&gt;anything a test writes to must be&lt;br&gt;
thread-scoped or thread-safe. There is no third option.&lt;/strong&gt; If you can't say which of the two&lt;br&gt;
a field is, it's a bug you haven't hit yet.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bug 3 — screenshots and reports attributed to the wrong test
&lt;/h2&gt;

&lt;p&gt;This one doesn't fail the build, which is why it survives so long. It corrupts your&lt;br&gt;
evidence instead.&lt;/p&gt;

&lt;p&gt;A failure screenshot helper that reaches for a driver via a static field, or writes to a&lt;br&gt;
fixed filename like &lt;code&gt;target/failure.png&lt;/code&gt;, will happily capture thread 3's browser for&lt;br&gt;
thread 1's failure. You then spend an afternoon debugging a screenshot of a page the failing&lt;br&gt;
test never visited.&lt;/p&gt;

&lt;p&gt;Both halves need fixing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capture from the failing thread's driver&lt;/strong&gt; — the listener runs on the test's thread, so
pull from your &lt;code&gt;ThreadLocal&lt;/code&gt;, not from a field someone assigned during setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write to a unique path&lt;/strong&gt; — include the test method name and something disambiguating
(a timestamp, or the invocation count for data-driven tests). Two &lt;code&gt;@DataProvider&lt;/code&gt; rows of
the same method failing at once will otherwise overwrite each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same applies to any report aggregator. If you're using ExtentReports, the per-test node&lt;br&gt;
has to be held in a &lt;code&gt;ThreadLocal&lt;/code&gt; too, or your steps interleave across tests into an&lt;br&gt;
unreadable report.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bug 4 — more browsers than the machine or grid can hold
&lt;/h2&gt;

&lt;p&gt;Thread count and browser count are not the same number, and treating them as one is how&lt;br&gt;
parallel suites go from "flaky" to "the agent fell over."&lt;/p&gt;

&lt;p&gt;Chrome is roughly 300–700 MB of RSS per instance with a real page loaded. Eight threads on&lt;br&gt;
a 4 GB CI container is not a thread-safety bug — it's the OOM killer, and it shows up as&lt;br&gt;
&lt;code&gt;SessionNotCreatedException&lt;/code&gt;, &lt;code&gt;chrome not reachable&lt;/code&gt;, or a build that simply dies with no&lt;br&gt;
useful log. Against a remote Grid or a cloud vendor, the same over-subscription shows up as&lt;br&gt;
queued sessions that time out, or as sessions your plan won't grant.&lt;/p&gt;

&lt;p&gt;So cap the concurrency separately from the thread count.&lt;/p&gt;
&lt;h3&gt;
  
  
  Cap concurrency separately from thread count
&lt;/h3&gt;

&lt;p&gt;The pattern is a semaphore between the test and driver creation. Threads that can't get a&lt;br&gt;
slot &lt;strong&gt;wait&lt;/strong&gt; rather than fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Semaphore&lt;/span&gt; &lt;span class="no"&gt;SLOTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Semaphore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// fair&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DRIVER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="no"&gt;SLOTS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tryAcquire&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SECONDS&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No browser slot free after 30s"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InterruptedException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentThread&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;interrupt&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Interrupted waiting for a browser slot"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="no"&gt;DRIVER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ChromeDriver&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and release the permit in &lt;code&gt;quit()&lt;/code&gt;, in a &lt;code&gt;finally&lt;/code&gt;. The &lt;code&gt;true&lt;/code&gt; argument makes the semaphore&lt;br&gt;
fair, which stops one unlucky thread from starving while others cycle through.&lt;/p&gt;

&lt;p&gt;Why wait instead of fail fast? Because a test that waits four seconds for a slot is a&lt;br&gt;
correct test. A test that fails because the machine was momentarily busy is a false red,&lt;br&gt;
and false reds are how teams learn to ignore CI.&lt;/p&gt;

&lt;p&gt;Set the cap from what the &lt;em&gt;environment&lt;/em&gt; can hold, not from what the suite would like. On CI&lt;br&gt;
that's usually memory divided by ~700 MB, minus one. On a cloud grid it's your plan's&lt;br&gt;
concurrency limit.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bug ThreadLocal can't touch: waits
&lt;/h2&gt;

&lt;p&gt;Parallel execution doesn't create wait bugs. It exposes the ones you already had.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A sleep is calibrated against a load profile.&lt;/strong&gt; &lt;code&gt;Thread.sleep(2000)&lt;/code&gt; was chosen on an&lt;br&gt;
idle laptop talking to a warm backend. Run four browsers on one agent and that machine no&lt;br&gt;
longer exists — same code, different CPU contention, different response times. Sleeps also&lt;br&gt;
cost you on every green run: forty sleeps × 2 seconds is eighty seconds per thread that you&lt;br&gt;
pay even when the page was ready in 200 ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mixing implicit and explicit waits is undefined behaviour.&lt;/strong&gt; Selenium says so in its own&lt;br&gt;
docs — set &lt;code&gt;implicitlyWait&lt;/code&gt; and then use &lt;code&gt;WebDriverWait&lt;/code&gt;, and the two compose in ways&lt;br&gt;
neither one documents. You ask for 5 seconds and wait 15, or you get a &lt;code&gt;NoSuchElementException&lt;/code&gt;&lt;br&gt;
before your explicit condition ever polls. Under load that unpredictability widens, because&lt;br&gt;
the driver is retrying against a slower page. Set the implicit wait to zero and use explicit&lt;br&gt;
waits only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Visible" is not "loaded."&lt;/strong&gt; A wait for element visibility tells you something rendered.&lt;br&gt;
It doesn't tell you the table has rows. Under parallel the gap between &lt;em&gt;rendered&lt;/em&gt; and&lt;br&gt;
&lt;em&gt;populated&lt;/em&gt; gets wider, because your backend is serving four suites at once. Wait for the&lt;br&gt;
condition you actually mean: row count above zero, spinner detached, text equal to what you&lt;br&gt;
expect.&lt;/p&gt;

&lt;p&gt;Explicit, condition-based waits are the only kind that degrade gracefully. A polling wait on&lt;br&gt;
a busy machine just takes longer. A sleep tuned for an idle machine simply becomes wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  Audit your own suite: seven checks
&lt;/h2&gt;

&lt;p&gt;Run these against your repo before you raise the thread count again. Each one takes a minute&lt;br&gt;
except the last, which takes a CI run and is worth more than the other six.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;static WebDriver&lt;/code&gt; anywhere.&lt;/strong&gt; &lt;code&gt;grep -rn "static WebDriver" src/&lt;/code&gt; — every hit is a
bug, including the "temporary" one in a utility class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your driver holder calls &lt;code&gt;remove()&lt;/code&gt;.&lt;/strong&gt; Open the class that owns the &lt;code&gt;ThreadLocal&lt;/code&gt; and
search for &lt;code&gt;.remove()&lt;/code&gt;. No hit means stale sessions on reused threads plus a slow leak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No mutable, non-thread-safe fields on shared objects.&lt;/strong&gt; Walk your page objects and
step-definition classes: a field should be &lt;code&gt;final&lt;/code&gt; and thread-safe, or it shouldn't be a
field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count your sleeps.&lt;/strong&gt; &lt;code&gt;grep -rn "Thread.sleep" src/ | wc -l&lt;/code&gt; — that number is how
sensitive your suite is to machine load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screenshot paths carry the test name.&lt;/strong&gt; Look at the artifacts from your last red build.
If you see &lt;code&gt;failure.png&lt;/code&gt; or &lt;code&gt;screenshot (3).png&lt;/code&gt;, your evidence isn't attributable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The browser cap is a separate number from the thread count.&lt;/strong&gt; Search your config for
both. If there's only one number, the two are coupled and one of them is wrong for your
CI box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the whole suite twice at your target thread count.&lt;/strong&gt; Different failures each time
means shared state. The same failures both times means real bugs — or a genuinely flaky
application. This single check separates those two categories better than any amount of
reading.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  One answer: don't own the lifecycle at all
&lt;/h2&gt;

&lt;p&gt;Everything above is roughly two hundred lines of infrastructure that has nothing to do with&lt;br&gt;
your application. A driver holder, a semaphore, a screenshot listener that knows about&lt;br&gt;
threads, a config knob for concurrency. It gets written once, reviewed once, and then&lt;br&gt;
maintained — across Selenium upgrades, across a CI migration, and again at the next company.&lt;br&gt;
That's the maintenance tax, and it's the same argument as&lt;br&gt;
&lt;a href="https://seleniumboot.com/blog/hidden-cost-of-every-selenium-framework-youve-built?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=devto"&gt;The Hidden Cost of Every Selenium Framework You've Built&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One way out is to not own that layer. Selenium Boot — the Spring Boot of Selenium — ships&lt;br&gt;
these fixes as the default path: &lt;code&gt;DriverManager&lt;/code&gt; is a &lt;code&gt;ThreadLocal&lt;/code&gt; holder that calls&lt;br&gt;
&lt;code&gt;remove()&lt;/code&gt; on quit; concurrent sessions are capped by a fair &lt;code&gt;Semaphore&lt;/code&gt; sized from&lt;br&gt;
&lt;code&gt;execution.maxActiveSessions&lt;/code&gt;, and a thread that can't get a slot waits up to 30 seconds&lt;br&gt;
instead of failing; screenshots and report nodes resolve from the calling thread. It never&lt;br&gt;
hides Selenium — &lt;code&gt;getDriver()&lt;/code&gt; returns the real &lt;code&gt;WebDriver&lt;/code&gt;, and you can drop to raw&lt;br&gt;
&lt;code&gt;By&lt;/code&gt; / &lt;code&gt;WebElement&lt;/code&gt; the moment the conventions don't fit.&lt;/p&gt;

&lt;p&gt;The configuration is the whole of the setup — this is all of &lt;code&gt;selenium-boot.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;execution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
  &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://example.com&lt;/span&gt;
  &lt;span class="na"&gt;parallel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;methods&lt;/span&gt;      &lt;span class="c1"&gt;# none | methods | classes&lt;/span&gt;
  &lt;span class="na"&gt;threadCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
  &lt;span class="na"&gt;maxActiveSessions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;   &lt;span class="c1"&gt;# fair semaphore; tests wait up to 30s for a slot&lt;/span&gt;

&lt;span class="na"&gt;browser&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chrome&lt;/span&gt;
  &lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;timeouts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;explicit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;pageLoad&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One honest limit: this covers the framework's own state. It cannot make &lt;em&gt;your&lt;/em&gt; static&lt;br&gt;
&lt;code&gt;orderId&lt;/code&gt; thread-safe. Bug 2 stays your job under any framework, including this one.&lt;/p&gt;

&lt;p&gt;More detail in the &lt;a href="https://docs.seleniumboot.com/docs/guides/parallel?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=devto"&gt;parallel execution guide&lt;/a&gt;&lt;br&gt;
and the &lt;a href="https://docs.seleniumboot.com/docs/configuration?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=devto"&gt;configuration reference&lt;/a&gt;,&lt;br&gt;
or the &lt;a href="https://seleniumboot.com/why?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=devto"&gt;why it exists&lt;/a&gt; page if you want the design argument first.&lt;/p&gt;

&lt;h2&gt;
  
  
  When building it yourself is the right call
&lt;/h2&gt;

&lt;p&gt;Plenty of teams should just write the code above.&lt;/p&gt;

&lt;p&gt;If you have one suite, one team, and no parallel ambition beyond four threads, thirty lines&lt;br&gt;
of &lt;code&gt;ThreadLocal&lt;/code&gt; is genuinely enough — adopting a framework to avoid it is a bad trade. If&lt;br&gt;
you have a hard constraint no framework will match, like a bespoke driver provider or an&lt;br&gt;
unusual grid topology, own the layer that touches it. And if your team wants to &lt;em&gt;learn&lt;/em&gt;&lt;br&gt;
concurrency properly, building it is a good way to do that; the debugging is the point.&lt;/p&gt;

&lt;p&gt;None of this is a Playwright argument. If you're greenfield with no Selenium investment,&lt;br&gt;
evaluate Playwright on its merits. This post is for the suites that already exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure is in the framework, not the app
&lt;/h2&gt;

&lt;p&gt;"Green on one thread, red on four" is a property of your test framework, not your&lt;br&gt;
application. It's a shared driver, or shared state that isn't the driver, or evidence&lt;br&gt;
captured from the wrong thread, or more browsers than the machine can hold — and now you can&lt;br&gt;
tell which. Fix the four, then raise the thread count on purpose instead of hopefully.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.seleniumboot.com/docs/getting-started?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=devto"&gt;Get started with Selenium Boot in five minutes →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>testing</category>
      <category>selenium</category>
      <category>testng</category>
    </item>
    <item>
      <title>The Hidden Cost of Every Selenium Framework You've Built</title>
      <dc:creator>Mohammad Saddam Hossain</dc:creator>
      <pubDate>Fri, 17 Jul 2026 03:11:19 +0000</pubDate>
      <link>https://dev.to/shossain/the-hidden-cost-of-every-selenium-framework-youve-built-4eb</link>
      <guid>https://dev.to/shossain/the-hidden-cost-of-every-selenium-framework-youve-built-4eb</guid>
      <description>&lt;p&gt;You didn't set out to build a framework. You set out to test a login form.&lt;/p&gt;

&lt;p&gt;But somewhere between the first &lt;code&gt;WebDriver driver = new ChromeDriver()&lt;/code&gt; and the fiftieth flaky CI run, you built one anyway. There's a &lt;code&gt;BaseTest&lt;/code&gt;. There's a &lt;code&gt;DriverFactory&lt;/code&gt;. There's a &lt;code&gt;WaitUtils&lt;/code&gt; class that everyone copies, and no one fully trusts. There's a reporting hack bolted onto TestNG listeners, and a block of CI YAML that only one person understands.&lt;/p&gt;

&lt;p&gt;That's a framework. You just never called it one — and that's exactly why it's so expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The framework you didn't mean to build
&lt;/h2&gt;

&lt;p&gt;Here's the pattern, repeated at nearly every Java shop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The BaseTest that grows a little every sprint&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseTest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;WebDriver&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@BeforeMethod&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ChromeDriver&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* options someone tuned in 2022 */&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;manage&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;timeouts&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;implicitlyWait&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="c1"&gt;// …plus retries, screenshots, and env switching bolted on over time&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@AfterMethod&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;tearDown&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ITestResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatus&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;ITestResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FAILURE&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// take a screenshot… somehow… attach it… somewhere&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;quit&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks harmless. It's ten lines. But it never stays ten lines, because production testing keeps asking for more: parallel execution, a second browser, cloud grids, retry-on-flake, a report your manager will actually open. Each request adds a little more plumbing — and every line of that plumbing is code &lt;strong&gt;you&lt;/strong&gt; now own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five costs nobody budgets for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Maintenance you can't schedule.&lt;/strong&gt; Selenium 4 lands. ChromeDriver changes its options API. A dependency bump breaks your screenshot logic. None of this is on the roadmap, all of it is on you, and it always arrives the week before a release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Onboarding that lives in someone's head.&lt;/strong&gt; A new engineer can't just read the docs — there are no docs. Onboarding is "sit with Priya and she'll explain the wait helpers." The framework's real specification is tribal knowledge, and it walks out the door when people leave.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Parallelism bugs that only appear under load.&lt;/strong&gt; Home-grown &lt;code&gt;BaseTest&lt;/code&gt; classes are notorious for sharing a &lt;code&gt;WebDriver&lt;/code&gt; across threads by accident. It passes locally and on a quiet CI box, then corrupts state the moment you scale to real parallelism. Thread-safety is hard, and you're debugging it on top of your actual product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A reporting and CI layer rebuilt from scratch.&lt;/strong&gt; Every team re-solves the same problems: parse results into something readable, embed failure screenshots, force headless in CI, tune the thread count, wire up JUnit XML so Jenkins understands it. It's weeks of work that produces nothing your users ever see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The opportunity cost.&lt;/strong&gt; Every hour spent maintaining the harness is an hour not spent writing tests that catch real bugs. You hired test engineers to improve quality — and a chunk of their time goes to being unpaid framework maintainers instead.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The cost isn't that a home-grown framework is bad. It's that it's &lt;strong&gt;infrastructure you're maintaining for free&lt;/strong&gt;, and the bill compounds with every project that rebuilds it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  "But it's just a BaseTest"
&lt;/h2&gt;

&lt;p&gt;That's the trap. It's just a &lt;code&gt;BaseTest&lt;/code&gt; — until it's a &lt;code&gt;BaseTest&lt;/code&gt;, a &lt;code&gt;BasePage&lt;/code&gt;, a &lt;code&gt;DriverFactory&lt;/code&gt;, a &lt;code&gt;ConfigLoader&lt;/code&gt;, a &lt;code&gt;RetryAnalyzer&lt;/code&gt;, a &lt;code&gt;ScreenshotListener&lt;/code&gt;, a &lt;code&gt;ReportBuilder&lt;/code&gt;, and a folder of utilities that three other repos have already forked and diverged from.&lt;/p&gt;

&lt;p&gt;You didn't decide to maintain a framework. You accreted one. And because it grew a line at a time, nobody ever made the call to ask: &lt;em&gt;should we be building this at all?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What if that framework were just… maintained for you?
&lt;/h2&gt;

&lt;p&gt;This is the entire idea behind &lt;strong&gt;Selenium Boot&lt;/strong&gt;. Your real alternative to a home-grown framework isn't "no framework" — it's &lt;em&gt;someone else's, maintained, tested, and documented&lt;/em&gt;. Selenium Boot is that framework: Spring Boot's conventions, applied to Selenium. You add one dependency and the plumbing is handled.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoginTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseTest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/login"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;                       &lt;span class="c1"&gt;// baseUrl from config&lt;/span&gt;
        &lt;span class="n"&gt;getByLabel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Username"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"admin"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;getByRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Role&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BUTTON&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sign in"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;click&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// auto-waits, no sleep()&lt;/span&gt;
        &lt;span class="n"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getByText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Welcome"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isVisible&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;                      &lt;span class="c1"&gt;// web-first, retries until true&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No driver setup. No teardown. No &lt;code&gt;Thread.sleep()&lt;/code&gt;. No brittle CSS selectors. The driver lifecycle is thread-safe by default, retries and reporting are built in, and CI configures itself. The five costs above? They're now someone else's job — and that someone ships the fixes, writes the docs, and tests the parallel path so you don't have to.&lt;/p&gt;

&lt;p&gt;Crucially, it &lt;strong&gt;never hides Selenium&lt;/strong&gt;. When the conventions don't fit, drop straight down to raw &lt;code&gt;WebDriver&lt;/code&gt; / &lt;code&gt;By&lt;/code&gt; / &lt;code&gt;WebElement&lt;/code&gt;. It's opinionated, not a cage — and it brings Playwright's best ideas (accessibility-first locators, auto-waiting, web-first assertions) &lt;em&gt;into&lt;/em&gt; the Selenium ecosystem you already run, keeping your Grid, your Java, and your team's skills.&lt;/p&gt;

&lt;p&gt;It's not a Playwright replacement — the architecture is genuinely different, and if you're greenfield with no Selenium investment, Playwright is a reasonable choice. This is for the many teams with years of Selenium/Java/Grid they can't rip out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep your test code. Delete the plumbing.
&lt;/h2&gt;

&lt;p&gt;Look at your repo. The test &lt;em&gt;intent&lt;/em&gt; — the part that says "a valid user can log in" — is genuinely yours, and it's valuable. The &lt;code&gt;BaseTest&lt;/code&gt;, the &lt;code&gt;DriverFactory&lt;/code&gt;, the wait-utils? That's the part you never wanted to own.&lt;/p&gt;

&lt;p&gt;Selenium Boot lets teams focus on testing, not framework engineering. Keep the first part. Delete the second.&lt;/p&gt;

&lt;p&gt;If you've built the framework in this post — and most Java teams have — that's not a criticism. It's just a bill you no longer have to pay.&lt;/p&gt;

&lt;p&gt;Selenium Boot is open source (Apache-2.0), Java 17+, TestNG and JUnit 5. Full disclosure: it's my project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://seleniumboot.com/?utm_campaign=devto" rel="noopener noreferrer"&gt;Get started in five minutes →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>testing</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Zero-config Cucumber + Selenium in Java: no @Before, no driver setup, no cleanup</title>
      <dc:creator>Mohammad Saddam Hossain</dc:creator>
      <pubDate>Sat, 02 May 2026 15:55:17 +0000</pubDate>
      <link>https://dev.to/shossain/zero-config-cucumber-selenium-in-java-no-before-no-driver-setup-no-cleanup-341h</link>
      <guid>https://dev.to/shossain/zero-config-cucumber-selenium-in-java-no-before-no-driver-setup-no-cleanup-341h</guid>
      <description>&lt;h2&gt;
  
  
  The Cucumber + Selenium boilerplate problem
&lt;/h2&gt;

&lt;p&gt;Every Cucumber + Selenium project in Java starts the same way. Before you write a single step, you write the infrastructure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Hooks&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;WebDriver&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Before&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;WebDriverManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chromedriver&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;ChromeOptions&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ChromeOptions&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;manage&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;timeouts&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;implicitlyWait&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ChromeDriver&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;DriverHolder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@After&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;tearDown&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Scenario&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isFailed&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;screenshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="nc"&gt;TakesScreenshot&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getScreenshotAs&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OutputType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BYTES&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;attach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screenshot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"image/png"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Failure screenshot"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;quit&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;DriverHolder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you wire a &lt;code&gt;ThreadLocal&lt;/code&gt; for parallel safety. Then you figure out how to get it into step definitions. Then you add a static helper class. Then someone wants screenshots embedded in the Selenium Boot HTML report too, so you add that. Then CI needs headless mode...&lt;/p&gt;

&lt;p&gt;By the time the first feature file runs, you've written 200 lines of plumbing that has nothing to do with your tests.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the step definitions should look like
&lt;/h2&gt;

&lt;p&gt;This is what a step definition file should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoginSteps&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseCucumberSteps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Given&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"the user is on the login page"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onLoginPage&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@When&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"they login as {string} with password {string}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LoginPage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getDriver&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;login&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Then&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"the dashboard should be visible"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;dashboardVisible&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dashboard"&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;isVisible&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pure Gherkin logic. No driver setup. No cleanup. No thread-locals. No screenshot code. The infrastructure should live somewhere else — ideally somewhere you never have to touch.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to get there
&lt;/h2&gt;

&lt;p&gt;The right place for the infrastructure is a framework-level &lt;code&gt;@Before&lt;/code&gt;/&lt;code&gt;@After&lt;/code&gt; hook that runs automatically for every project. Cucumber discovers hooks by scanning the &lt;code&gt;glue&lt;/code&gt; packages — so if you put your hooks in a published library and include that package in &lt;code&gt;glue&lt;/code&gt;, they run without any code in the consumer project.&lt;/p&gt;

&lt;p&gt;Here's the structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selenium-boot (library)
└── com.seleniumboot.cucumber
    ├── CucumberHooks.java       ← @Before + @After per scenario
    ├── BaseCucumberSteps.java   ← getDriver(), open(), assertThat()
    ├── BaseCucumberTest.java    ← runner base class
    └── CucumberStepLogger.java  ← pipes step names into HTML report

your-test-project
└── com.yourcompany.bdd
    ├── CucumberRunner.java      ← @CucumberOptions + extends BaseCucumberTest
    └── steps/
        └── LoginSteps.java      ← extends BaseCucumberSteps, pure steps only
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The framework hook
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CucumberHooks&lt;/code&gt; is the core of the integration. It runs before and after every scenario — the consumer project never sees it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CucumberHooks&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Before&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;beforeScenario&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Scenario&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;FrameworkBootstrap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;        &lt;span class="c1"&gt;// idempotent — safe to call per-scenario&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;testId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildTestId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;CucumberContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setScenario&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;SeleniumBootContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCurrentTestId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;ExecutionMetrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;markStart&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;DriverManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createDriver&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;           &lt;span class="c1"&gt;// reads selenium-boot.yml, applies config&lt;/span&gt;
        &lt;span class="nc"&gt;HookRegistry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onTestStart&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@After&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;afterScenario&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Scenario&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;testId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SeleniumBootContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCurrentTestId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isFailed&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Screenshot into Selenium Boot HTML report&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ScreenshotManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;capture&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sanitize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
            &lt;span class="nc"&gt;ExecutionMetrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;recordScreenshot&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Screenshot bytes attached to Cucumber's own HTML report&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ScreenshotManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;captureAsBase64&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;attach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Base64&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDecoder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;decode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"image/png"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Failure"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="nc"&gt;HookRegistry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onTestFailure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="nc"&gt;ExecutionMetrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;recordStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolveStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="nc"&gt;ExecutionMetrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;markEnd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;DriverManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;quitDriver&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;CucumberContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;SeleniumBootContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clearCurrentTestId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@After(order = 20000)&lt;/code&gt; runs before any user-defined &lt;code&gt;@After(order = 10000)&lt;/code&gt; — so the screenshot is captured while the page is still loaded, before user cleanup hooks navigate away.&lt;/p&gt;




&lt;h2&gt;
  
  
  The step definition base class
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;BaseCucumberSteps&lt;/code&gt; gives step definitions access to the driver and the framework API without static helpers or thread-local lookups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseCucumberSteps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;WebDriver&lt;/span&gt; &lt;span class="nf"&gt;getDriver&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;DriverManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDriver&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// ThreadLocal — safe for parallel&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;getDriver&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SeleniumBootContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConfig&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getExecution&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getBaseUrl&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SeleniumBootContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConfig&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getExecution&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getBaseUrl&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;getDriver&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;Locator&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;css&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;     &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Locator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofCss&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;css&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;Locator&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;          &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Locator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;LocatorAssert&lt;/span&gt; &lt;span class="nf"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt; &lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;SeleniumAssert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// auto-retrying assertion&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;Scenario&lt;/span&gt; &lt;span class="nf"&gt;getScenario&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CucumberContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getScenario&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The runner
&lt;/h2&gt;

&lt;p&gt;The runner is the one piece that belongs in the consumer project — it's how you specify where your features and step packages are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@CucumberOptions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;features&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"src/test/resources/features"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;glue&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"com.yourcompany.bdd.steps"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"com.seleniumboot.cucumber"&lt;/span&gt;&lt;span class="o"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;plugin&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"pretty"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"com.seleniumboot.cucumber.CucumberStepLogger"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CucumberRunner&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseCucumberTest&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;com.seleniumboot.cucumber&lt;/code&gt; in &lt;code&gt;glue&lt;/code&gt; is the key — it makes Cucumber discover &lt;code&gt;CucumberHooks&lt;/code&gt; automatically.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CucumberStepLogger&lt;/code&gt; is a &lt;code&gt;ConcurrentEventListener&lt;/code&gt; that intercepts &lt;code&gt;TestStepFinished&lt;/code&gt; events and pipes step names and their pass/fail status into the Selenium Boot HTML report step timeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CucumberStepLogger&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ConcurrentEventListener&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;CURRENT_STEP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setEventPublisher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EventPublisher&lt;/span&gt; &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerHandlerFor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TestStepStarted&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;onStepStarted&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerHandlerFor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TestStepFinished&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;onStepFinished&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onStepStarted&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TestStepStarted&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTestStep&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;PickleStepTestStep&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="no"&gt;CURRENT_STEP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStepText&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onStepFinished&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TestStepFinished&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CURRENT_STEP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="no"&gt;CURRENT_STEP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="nc"&gt;StepLogger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mapStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getResult&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getStatus&lt;/span&gt;&lt;span class="o"&gt;()),&lt;/span&gt;
                        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getResult&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getStatus&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FAILED&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  IDE single-scenario execution
&lt;/h2&gt;

&lt;p&gt;When you right-click a scenario in IntelliJ and click Run, the IDE uses its own Cucumber runner — not your &lt;code&gt;CucumberRunner&lt;/code&gt; class — so &lt;code&gt;@CucumberOptions&lt;/code&gt; is ignored and &lt;code&gt;CucumberHooks&lt;/code&gt; is never discovered.&lt;/p&gt;

&lt;p&gt;The fix: a &lt;code&gt;cucumber.properties&lt;/code&gt; file. Cucumber reads it regardless of which runner launches the scenario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# src/test/resources/cucumber.properties
&lt;/span&gt;&lt;span class="py"&gt;cucumber.glue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;com.yourcompany.bdd.steps,com.seleniumboot.cucumber&lt;/span&gt;
&lt;span class="py"&gt;cucumber.plugin&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;pretty,com.seleniumboot.cucumber.CucumberStepLogger&lt;/span&gt;
&lt;span class="py"&gt;cucumber.monochrome&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One file. No IDE configuration needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parallel execution
&lt;/h2&gt;

&lt;p&gt;Parallel safety comes for free. The driver is stored in a &lt;code&gt;ThreadLocal&lt;/code&gt; via &lt;code&gt;DriverManager&lt;/code&gt;, &lt;code&gt;CucumberContext&lt;/code&gt; stores the &lt;code&gt;Scenario&lt;/code&gt; in a &lt;code&gt;ThreadLocal&lt;/code&gt;, and &lt;code&gt;ExecutionMetrics&lt;/code&gt; uses a &lt;code&gt;ConcurrentHashMap&lt;/code&gt; keyed by testId. Each scenario thread is fully isolated.&lt;/p&gt;

&lt;p&gt;Enable parallel execution in &lt;code&gt;selenium-boot.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;execution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parallel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;methods&lt;/span&gt;
  &lt;span class="na"&gt;threadCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
  &lt;span class="na"&gt;maxActiveSessions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What you get without writing it
&lt;/h2&gt;

&lt;p&gt;By having &lt;code&gt;com.seleniumboot.cucumber&lt;/code&gt; in your glue path, every scenario automatically gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome or Firefox based on &lt;code&gt;selenium-boot.yml&lt;/code&gt; — no WebDriverManager calls&lt;/li&gt;
&lt;li&gt;Headless in CI — detected automatically&lt;/li&gt;
&lt;li&gt;Screenshot on failure embedded in both the Selenium Boot HTML report and Cucumber's HTML report&lt;/li&gt;
&lt;li&gt;Step names in the HTML report step timeline&lt;/li&gt;
&lt;li&gt;Metrics tracked — duration, pass/fail, retry count&lt;/li&gt;
&lt;li&gt;Parallel-safe ThreadLocal isolation&lt;/li&gt;
&lt;li&gt;Self-healing locators if enabled&lt;/li&gt;
&lt;li&gt;AI failure analysis if configured&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The step definitions contain nothing but business logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using it in Selenium Boot
&lt;/h2&gt;

&lt;p&gt;All of this is built into &lt;a href="https://github.com/seleniumboot/selenium-boot" rel="noopener noreferrer"&gt;Selenium Boot&lt;/a&gt;. Add the dependency and the two Cucumber JARs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.github.seleniumboot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;selenium-boot&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.10.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.cucumber&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;cucumber-java&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;7.20.1&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.cucumber&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;cucumber-testng&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;7.20.1&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full docs at &lt;a href="https://seleniumboot.github.io/selenium-boot/docs/cucumber" rel="noopener noreferrer"&gt;seleniumboot.github.io/selenium-boot/docs/cucumber&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;How does your team currently handle WebDriver setup in Cucumber step definitions? Drop it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>selenium</category>
      <category>cucumber</category>
      <category>testing</category>
    </item>
    <item>
      <title>I built self-healing Selenium locators in Java — here's how it works</title>
      <dc:creator>Mohammad Saddam Hossain</dc:creator>
      <pubDate>Mon, 27 Apr 2026 04:26:34 +0000</pubDate>
      <link>https://dev.to/shossain/i-built-self-healing-selenium-locators-in-java-heres-how-it-works-21me</link>
      <guid>https://dev.to/shossain/i-built-self-healing-selenium-locators-in-java-heres-how-it-works-21me</guid>
      <description>&lt;h2&gt;
  
  
  The problem every Selenium team hits
&lt;/h2&gt;

&lt;p&gt;Your test suite was green last week. Today it's failing on 12 tests. The error is always the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;org.openqa.selenium.TimeoutException: Expected condition failed:
waiting for visibility of element located by By.cssSelector: #submit-btn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You open the app. The button is right there. You inspect it — the &lt;code&gt;id&lt;/code&gt; changed from &lt;code&gt;submit-btn&lt;/code&gt; to &lt;code&gt;btn-submit&lt;/code&gt;. A frontend developer renamed it during a refactor and didn't tell QA.&lt;/p&gt;

&lt;p&gt;This is the #1 cause of flaky, high-maintenance Selenium suites. Locators are brittle by nature. The moment the DOM changes, your tests break — even when the feature itself works perfectly.&lt;/p&gt;

&lt;p&gt;The standard advice is "use better locators" (data-testid, aria labels). Good advice. But you're inheriting 300 tests you didn't write, running against an app you don't own, and you need them green &lt;em&gt;today&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What if the framework tried to fix itself?
&lt;/h2&gt;

&lt;p&gt;The idea behind self-healing locators is simple: when a locator times out, don't immediately fail — try a set of fallback strategies derived from the original locator, and if one works, continue the test.&lt;/p&gt;

&lt;p&gt;Here's the contract:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The primary locator fails (timeout)&lt;/li&gt;
&lt;li&gt;The framework extracts fallback candidates from the locator descriptor&lt;/li&gt;
&lt;li&gt;Each fallback is tried in order&lt;/li&gt;
&lt;li&gt;If one succeeds, the test continues — and the heal is logged for the developer to review later&lt;/li&gt;
&lt;li&gt;If none succeed, the original exception is thrown&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The test doesn't silently lie. It passes, but it marks itself as &lt;strong&gt;healed&lt;/strong&gt; so you know a locator needs updating.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parsing &lt;code&gt;By.toString()&lt;/code&gt; to extract fallbacks
&lt;/h2&gt;

&lt;p&gt;The interesting engineering problem is: given a &lt;code&gt;By&lt;/code&gt; object, what fallback strategies can you derive?&lt;/p&gt;

&lt;p&gt;Selenium's &lt;code&gt;By&lt;/code&gt; doesn't expose its internals via a clean API — but &lt;code&gt;By.toString()&lt;/code&gt; is predictable. &lt;code&gt;By.cssSelector: #submit-btn&lt;/code&gt; tells you everything you need.&lt;/p&gt;

&lt;p&gt;Here's the core of the parser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;buildFallbacks&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fallbacks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"By.cssSelector: "&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;css&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;substring&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"By.cssSelector: "&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// #submit-btn → By.id("submit-btn")&lt;/span&gt;
        &lt;span class="nc"&gt;Matcher&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pattern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#([\\w-]+)"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;css&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="n"&gt;fallbacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;

        &lt;span class="c1"&gt;// [name='username'] → By.name("username")&lt;/span&gt;
        &lt;span class="nc"&gt;Matcher&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pattern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\\[name=['\"]?([\\w-]+)['\"]?]"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;css&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="n"&gt;fallbacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;

        &lt;span class="c1"&gt;// .primary-btn → By.className("primary-btn")&lt;/span&gt;
        &lt;span class="nc"&gt;Matcher&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pattern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\\.([\\w-]+)"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;css&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="n"&gt;fallbacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;

        &lt;span class="c1"&gt;// [data-testid='submit'] → By.cssSelector("[data-testid='submit']")&lt;/span&gt;
        &lt;span class="nc"&gt;Matcher&lt;/span&gt; &lt;span class="n"&gt;testid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pattern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\\[data-testid=['\"]?([\\w-]+)['\"]?]"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;css&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="n"&gt;fallbacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cssSelector&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[data-testid='"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;testid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"']"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// [placeholder='Email'] → By.cssSelector("[placeholder='Email']")&lt;/span&gt;
        &lt;span class="nc"&gt;Matcher&lt;/span&gt; &lt;span class="n"&gt;ph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pattern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\\[placeholder=['\"]([^'\"]+)['\"]]"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;css&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ph&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="n"&gt;fallbacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cssSelector&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[placeholder='"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ph&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"']"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"By.xpath: "&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;xpath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;substring&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"By.xpath: "&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// @id='submit-btn' → By.id("submit-btn")&lt;/span&gt;
        &lt;span class="nc"&gt;Matcher&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pattern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@id=['\"]([^'\"]+)['\"]"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xpath&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="n"&gt;fallbacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;

        &lt;span class="c1"&gt;// @name='username' → By.name("username")&lt;/span&gt;
        &lt;span class="nc"&gt;Matcher&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pattern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@name=['\"]([^'\"]+)['\"]"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xpath&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="n"&gt;fallbacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;

        &lt;span class="c1"&gt;// text()='Submit' → By.xpath("//*[text()='Submit']")&lt;/span&gt;
        &lt;span class="nc"&gt;Matcher&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Pattern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"text\\(\\)=['\"]([^'\"]+)['\"]"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xpath&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="n"&gt;fallbacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;xpath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"//*[text()='"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"']"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fallbacks&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the healing attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;WebElement&lt;/span&gt; &lt;span class="nf"&gt;tryHeal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebDriver&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;By&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fallbacks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildFallbacks&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fallbacks&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;WebElement&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findElement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fallback&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isDisplayed&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;logHeal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// record for report&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NoSuchElementException&lt;/span&gt; &lt;span class="n"&gt;ignored&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// all fallbacks failed&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Where it hooks into the wait layer
&lt;/h2&gt;

&lt;p&gt;Self-healing only makes sense at the &lt;strong&gt;wait layer&lt;/strong&gt;, not raw &lt;code&gt;findElement&lt;/code&gt;. You want it to fire after you've already waited the full timeout — otherwise you'd be bailing out of waits too early.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;WebElement&lt;/span&gt; &lt;span class="nf"&gt;waitForVisible&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;By&lt;/span&gt; &lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WebDriverWait&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;until&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExpectedConditions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;visibilityOfElementLocated&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TimeoutException&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;NoSuchElementException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Primary locator failed — try healing&lt;/span&gt;
        &lt;span class="nc"&gt;WebElement&lt;/span&gt; &lt;span class="n"&gt;healed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SelfHealingLocator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tryHeal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;healed&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;healed&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// nothing worked, surface the real error&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of your framework calls &lt;code&gt;waitForVisible&lt;/code&gt; before every interaction — so every click, type, and assertion gets self-healing automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Logging heals without noise
&lt;/h2&gt;

&lt;p&gt;A healed locator is a &lt;strong&gt;warning&lt;/strong&gt;, not a success. You want to know about it — but you don't want it to drown in test output. The right pattern is to accumulate healed locator records and export them after the suite runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;target/healed-locators.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LoginTest#submitForm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"original"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"By.cssSelector: #submit-btn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"healed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"By.id: btn-submit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-27T10:42:11"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A developer reviews this file after the run, updates the locator in the page object, and the test is back to clean. The framework does not silently swallow the problem — it surfaces it in a structured, actionable way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using it in Selenium Boot
&lt;/h2&gt;

&lt;p&gt;I built all of this into &lt;a href="https://github.com/seleniumboot/selenium-boot" rel="noopener noreferrer"&gt;Selenium Boot&lt;/a&gt;, an open-source zero-boilerplate Selenium + TestNG framework. To enable self-healing, you add one line to your config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# selenium-boot.yml&lt;/span&gt;
&lt;span class="na"&gt;locators&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selfHealing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No code changes. Every &lt;code&gt;waitForVisible&lt;/code&gt; and &lt;code&gt;waitForClickable&lt;/code&gt; call in your tests now has automatic fallback healing.&lt;/p&gt;

&lt;p&gt;In the HTML report, healed tests get a &lt;strong&gt;⚠ healed&lt;/strong&gt; badge so they're immediately visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ LoginTest#submitForm  [⚠ healed]   1.2s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;target/healed-locators.json&lt;/code&gt; gives you the exact diff to fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  What healing covers (and what it doesn't)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Heals well:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt; changed (most common — developers rename IDs during refactors)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; attribute changed&lt;/li&gt;
&lt;li&gt;Class name changed (single-class selectors)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-testid&lt;/code&gt; added/renamed&lt;/li&gt;
&lt;li&gt;Text-based XPath when the element text is stable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Won't heal:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structurally removed elements (the feature was deleted — this should fail)&lt;/li&gt;
&lt;li&gt;Deep CSS selector chains (&lt;code&gt;div.container &amp;gt; ul &amp;gt; li:nth-child(3)&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Dynamic content that changes every render&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Self-healing is a &lt;strong&gt;safety net for legitimate DOM drift&lt;/strong&gt;, not a substitute for well-maintained locators. The goal is to keep the suite green through minor frontend changes while giving you a clear log of what needs updating.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bigger takeaway
&lt;/h2&gt;

&lt;p&gt;Locator brittleness is a maintenance tax, not an inherent property of Selenium. With ~100 lines of &lt;code&gt;By.toString()&lt;/code&gt; parsing and a hook in your wait layer, you can cut a significant chunk of false failures — and turn every heal into a clear, actionable signal for the team.&lt;/p&gt;

&lt;p&gt;The full implementation is in &lt;a href="https://github.com/seleniumboot/selenium-boot" rel="noopener noreferrer"&gt;Selenium Boot on GitHub&lt;/a&gt;. If you're building a Selenium framework from scratch or inheriting one that's brittle, it's worth a look.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example Screenshot Of The Report
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxwg2ac5xlphraign16qh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxwg2ac5xlphraign16qh.png" alt=" " width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's your current strategy for dealing with locator churn? Drop it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>selenium</category>
      <category>testing</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
