<?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: Jim</title>
    <description>The latest articles on DEV Community by Jim (@jimtt).</description>
    <link>https://dev.to/jimtt</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2530872%2Fcf922eee-34d3-4733-aa90-47fff05c1170.webp</url>
      <title>DEV Community: Jim</title>
      <link>https://dev.to/jimtt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jimtt"/>
    <language>en</language>
    <item>
      <title>Why your recorded UI tests break after every redesign — and how to build replay that survives</title>
      <dc:creator>Jim</dc:creator>
      <pubDate>Wed, 02 Sep 2026 10:13:17 +0000</pubDate>
      <link>https://dev.to/jimtt/why-your-recorded-ui-tests-break-after-every-redesign-and-how-to-build-replay-that-survives-50g6</link>
      <guid>https://dev.to/jimtt/why-your-recorded-ui-tests-break-after-every-redesign-and-how-to-build-replay-that-survives-50g6</guid>
      <description>&lt;p&gt;If you've ever recorded a UI test, shipped one button change to production, and watched 40 tests explode in CI — you know exactly why "record and replay" has a bad reputation.&lt;/p&gt;

&lt;p&gt;I've spent the past year building a browser testing tool, and I want to talk about the unglamorous engineering that decides whether a recorded test survives a redesign — or dies on first contact. Not the AI magic. The four problems underneath it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A single locator is a single point of failure
&lt;/h2&gt;

&lt;p&gt;Most recorders store exactly one locator per element — an XPath or a CSS chain — and freeze it at record time. That's the root of almost every "my tests broke" story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;XPath like &lt;code&gt;//div[2]/main/section[3]/button[1]&lt;/code&gt; breaks when someone adds one &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; to the layout.&lt;/li&gt;
&lt;li&gt;CSS chains like &lt;code&gt;.v-btn.theme--dark &amp;gt; .v-btn__content&lt;/code&gt; break on any styling refactor.&lt;/li&gt;
&lt;li&gt;Even &lt;code&gt;data-testid&lt;/code&gt; — the community's favorite answer — isn't bulletproof. Third-party components don't have it, and a cleanup sprint that renames IDs silently kills dozens of tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix: don't store one answer, store a ranked list of candidates.&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"click"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add to cart button"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"candidates"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"strategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"add-to-cart"&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;"strategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"role-text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"button 'Add to cart'"&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;"strategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add to cart"&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;"strategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"css"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".btn-primary.cart-action"&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;"strategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xpath"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"//button[contains(., 'Add to cart')]"&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="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;At replay time, try the strongest match first and fall through on a miss. Then — and this matters more than people think — record &lt;strong&gt;which candidate matched&lt;/strong&gt;. If your test passed via the XPath fallback, the page has changed in a way that deserves human review, even though the run is green.&lt;/p&gt;

&lt;p&gt;Ranking rules that survived contact with real apps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Semantic attributes first&lt;/strong&gt; (&lt;code&gt;data-testid&lt;/code&gt;, &lt;code&gt;aria-label&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;) — most stable, but often missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role + accessible name&lt;/strong&gt; — survives styling changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text content&lt;/strong&gt; — human-meaningful; great for buttons and links, useless for inputs, fragile under copy edits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS chains and index-based XPath last&lt;/strong&gt; — useful signals, never a primary answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No single strategy wins. The goal is that a test degrades gracefully instead of snapping.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Synthetic events are not real input
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;element.click()&lt;/code&gt; from JavaScript is not a click.&lt;/p&gt;

&lt;p&gt;It invokes the event handlers, but it skips the browser's native input pipeline: no focus management, no &lt;code&gt;:active&lt;/code&gt; state, no scroll-into-view, different behavior with native controls like &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;, date pickers, and file inputs. Tests pass on synthetic events and fail for real users — or the reverse.&lt;/p&gt;

&lt;p&gt;The sturdier path is driving input at the browser level. The Chrome DevTools Protocol dispatches events through the same pipeline a real mouse and keyboard use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Input.dispatchMouseEvent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mousePressed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;button&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;left&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;clickCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Input.dispatchMouseEvent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mouseReleased&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;button&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;left&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;clickCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For typing, &lt;code&gt;Input.insertText&lt;/code&gt; behaves much closer to a human than setting &lt;code&gt;.value&lt;/code&gt; and firing an &lt;code&gt;input&lt;/code&gt; event.&lt;/p&gt;

&lt;p&gt;The trade-off: browser-level input is stricter — and that's the point. If a cookie banner covers your button, a CDP click fails, &lt;em&gt;correctly&lt;/em&gt;, because a human couldn't click it either. Synthetic events would have "passed" while hiding a real bug. The price is that you must handle overlays deliberately instead of pretending they don't exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Don't fail at the first miss — and don't hide the miss
&lt;/h2&gt;

&lt;p&gt;When the primary path can't locate an element, naive tools do one of two dumb things: fail instantly (flaky suite), or silently fall back (the test drifts away from what it was testing). Both destroy trust in the suite.&lt;/p&gt;

&lt;p&gt;The distinction that matters is &lt;em&gt;why&lt;/em&gt; the element wasn't found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not there yet&lt;/strong&gt; — hydration, lazy loading, an animation still running. Wait and retry with a deadline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There, but covered&lt;/strong&gt; — modal, toast, cookie banner. This is a real finding; surface it, don't wait it out forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There, but different&lt;/strong&gt; — the element moved or re-rendered. Try the next candidate, and log that you did.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the failure ladder looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CDP locate (primary candidates, ranked)
  → retry within deadline          # "not there yet"
  → DOM-level fallback             # CDP hit-test missed
  → next candidate in the list     # element changed
  → fail, with full evidence       # never silently
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every fallback gets logged and shown in the run report. A green run that used three fallbacks is not the same as a green run that didn't — and your team should see the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The expensive part isn't running tests. It's triaging failures.
&lt;/h2&gt;

&lt;p&gt;Do the math on flaky tests: one ambiguous failure costs a QA engineer 15–30 minutes of "is this a real bug or is it the test?" Twenty failures a day is a person-day of triage, most of it wasted on non-bugs.&lt;/p&gt;

&lt;p&gt;So the most valuable feature of a testing tool isn't execution speed — it's the quality of evidence attached to a failure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;screenshot at the exact failing step&lt;/li&gt;
&lt;li&gt;the step list up to the failure, with which locator each step matched&lt;/li&gt;
&lt;li&gt;the candidate list the failed step tried&lt;/li&gt;
&lt;li&gt;console errors and page context (URL, viewport, browser version)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We use AI to summarize the likely cause, but the recorded steps stay the source of truth. That's deliberate: an AI that silently "fixes" tests is just moving the drift somewhere you can't see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What record-and-replay is &lt;em&gt;not&lt;/em&gt; for
&lt;/h2&gt;

&lt;p&gt;Honest limits, because trust beats hype:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It won't replace unit tests or API tests. UI tests are for user-visible behavior.&lt;/li&gt;
&lt;li&gt;It's weakest for canvas-heavy visualizations and deeply random data flows.&lt;/li&gt;
&lt;li&gt;Best fit: critical-path regression — checkout, onboarding, admin workflows. The longer and more business-critical the flow, the more replay stability pays off.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;That's the core of what I've learned building &lt;a href="https://www.icuecast.ai" rel="noopener noreferrer"&gt;CueCast&lt;/a&gt; — a no-code tool built around these ideas: multi-candidate matching, browser-level input, and evidence-first failures. The techniques above work in Playwright or Selenium too; steal them either way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you handle locator brittleness?&lt;/strong&gt; &lt;code&gt;data-testid&lt;/code&gt; everywhere? Playwright's &lt;code&gt;getByRole&lt;/code&gt;? Visual AI matching? Curious what's actually holding up at your scale.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>webdev</category>
      <category>automation</category>
      <category>qa</category>
    </item>
  </channel>
</rss>
