<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Sahil Sharma QA]]></title><description><![CDATA[Sahil Sharma QA]]></description><link>https://sahilsharmaqa.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6acb88afdb902bfaa8a78af9/1d6b29a1-6cc4-47c0-8c81-b359b92860e3.png</url><title>Sahil Sharma QA</title><link>https://sahilsharmaqa.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 15 Oct 2026 15:57:32 GMT</lastBuildDate><atom:link href="https://sahilsharmaqa.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Fixing broken Playwright locators for 0 tokens: a small decision model (Laya or Jev) first, the Playwright Test agent (Claude) only when needed]]></title><description><![CDATA[Most broken locators don’t need a big model thinking for a couple of minutes. So I built a three-part healer: Laya, a small local model, makes the quick decision, a re-run script checks every fix, and]]></description><link>https://sahilsharmaqa.hashnode.dev/fixing-broken-playwright-locators-for-0-tokens-a-small-decision-model-laya-or-jev-first-the-playwright-test-agent-claude-only-when-needed</link><guid isPermaLink="true">https://sahilsharmaqa.hashnode.dev/fixing-broken-playwright-locators-for-0-tokens-a-small-decision-model-laya-or-jev-first-the-playwright-test-agent-claude-only-when-needed</guid><category><![CDATA[playwright]]></category><category><![CDATA[test-automation]]></category><category><![CDATA[AI]]></category><category><![CDATA[jev]]></category><category><![CDATA[laya]]></category><category><![CDATA[Playwright MCP]]></category><category><![CDATA[Playwright Test Agents]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Sahil Sharma]]></dc:creator><pubDate>Sun, 11 Oct 2026 13:40:46 GMT</pubDate><content:encoded><![CDATA[<p><em>Most broken locators don’t need a big model thinking for a couple of minutes. So I built a three-part healer: Laya, a small local model, makes the quick decision, a re-run script checks every fix, and the Playwright Test agent (Claude) only gets the cases that really need it. Simple breaks get fixed in seconds for zero tokens. Here are two scenarios I measured, and a few things I didn't see coming.</em></p>
<h2>TL;DR</h2>
<table>
<thead>
<tr>
<th>Broken locator</th>
<th>Who fixed it</th>
<th>Tokens</th>
<th>Time</th>
</tr>
</thead>
<tbody><tr>
<td>Button renamed (new id and text)</td>
<td>Local decision model (Laya)</td>
<td><strong>0</strong></td>
<td>~0.25 s decision + ~7 s verification</td>
</tr>
<tr>
<td>Button turned into an icon "→"</td>
<td>Playwright Test agent (Claude), after Laya failed safely</td>
<td>~26,000</td>
<td>~2 minutes</td>
</tr>
</tbody></table>
<p>Every simple break the small model handles is one agent session you don't have to pay for, in tokens or in waiting.</p>
<hr />
<h2>The problem</h2>
<p>You know this one. A developer renames a button. Its <code>data-test</code> goes from <code>proceed-1</code> to something else, the text changes from "Proceed to checkout" to "Continue to checkout", and the next morning your checkout test is red:</p>
<pre><code class="language-plaintext">waiting for getByTestId('proceed-1')
</code></pre>
<p>Nothing is broken for the user. The test just lost track of a button. Fixing it by hand takes about five minutes, but it keeps happening. Handing it to an AI agent saves your time, but you pay in tokens and a couple of minutes of agent work for what is often a one-word change.</p>
<p>I looked at three ways of using AI here, and they cost very different amounts:</p>
<table>
<thead>
<tr>
<th>Approach</th>
<th>Where the AI works</th>
<th>Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Runtime agent (an LLM drives the browser, e.g. via Playwright MCP)</td>
<td>Every step of every run</td>
<td>Tokens on every CI run</td>
</tr>
<tr>
<td>Playwright Test agent (Claude), the healer</td>
<td>Once, when a test breaks. It edits the test.</td>
<td>Tokens per fix, CI stays plain</td>
</tr>
<tr>
<td>A small "System One" decision model</td>
<td>Once, when a test breaks, for simple decisions</td>
<td>Almost free per fix</td>
</tr>
</tbody></table>
<p>What I wanted to know was how many breaks the cheap, fast third option can handle on its own, and how to pass the rest to the second option without it going wrong.</p>
<h2>What I used</h2>
<p><strong>Playwright</strong> runs the tests and does the clicking. I tried everything on a Playwright suite for the public <a href="https://practicesoftwaretesting.com">Practice Software Testing (Toolshop)</a> demo shop. It has page objects, fixtures and a full checkout flow, so it's a reasonable stand-in for a real project.</p>
<p><strong>A System One decision model, Jev or Laya.</strong> These are a different kind of model. They don't write text. You give them a state and a typed question ("which of these options?"), and in one quick pass they return a choice, a probability for every option, and a calibrated confidence.</p>
<ul>
<li><p><a href="https://docs.typesafe.ai/api">Jev</a> (TypeSafe AI) is hosted and paid, about $0.084 per million input tokens. Output is free.</p>
</li>
<li><p><a href="https://github.com/NandhaKishorM/laya">Laya</a> is open source (Apache-2.0) and runs on your own machine.</p>
</li>
</ul>
<p>Both use the same API (<code>POST /v1/systemone</code>), so moving between them is a URL change. I used Laya for everything below, on a laptop's integrated Intel Arc GPU. Once warm, it took about 150 ms per decision. Nothing in the design depends on which one you pick.</p>
<p><strong>The Playwright Test agent (Claude)</strong> is Playwright's official <a href="https://playwright.dev/docs/test-agents">healer agent</a>. It's my fallback for breaks the small model can't handle.</p>
<h2>Where this fits: maintenance, not creation</h2>
<p>A test suite has two phases, and AI helps differently in each.</p>
<p><strong>Creating tests.</strong> This is where large models shine. The Playwright Test agents (Claude) can explore the app, plan the scenarios and write the test scripts for you: the planner, the generator and the healer working together. It's a one-time cost per test, and well worth it. Nothing in this post replaces that.</p>
<p><strong>Maintaining tests.</strong> This is the part that never ends. Once the tests exist, the app keeps changing: buttons get renamed, ids change, text gets reworded. Every change breaks a locator, and every break needs healing. Most of these fixes are small, but they happen again and again, and if every one goes to a large agent, you pay tokens and minutes every time.</p>
<p>This project focuses on that second phase: the <strong>healing</strong>. The idea is simple. Let a small, fast model handle the routine fixes, and keep the Playwright Test agent (Claude) for the breaks that really need its reasoning.</p>
<p>Two choices follow from that:</p>
<ul>
<li><p><strong>The healer runs only when a test breaks.</strong> Your CI keeps running plain Playwright specs, so while tests pass, healing costs nothing.</p>
</li>
<li><p><strong>Every fix ends as a code change a human reviews.</strong> The healer doesn't quietly adapt the test at run time. It edits the page object, proves the fix by re-running the test, and leaves the diff for you, the same way the Playwright Test agent (Claude) works.</p>
</li>
</ul>
<h2>How a System One model works</h2>
<p>A large language model like Claude reads a prompt and writes an answer, one token at a time. A System One model like Laya or Jev does something much smaller. You give it a situation and a multiple-choice question, and in one quick pass it gives you back the option it picked, a probability for every option, and a confidence score for the answer. It doesn't write sentences or explain itself. Think of it as a fast, gut-feel decision rather than slow, step-by-step reasoning.</p>
<p>In this project, the situation is the intent of the broken locator, and the options are the clickable elements Playwright found on the page. Simplified, one question looks like this:</p>
<pre><code class="language-plaintext">Intent: button "Proceed to checkout" — cart (step 1) to sign-in (step 2)
Which of these is it now?
  button "Continue to checkout"
  button "Continue Shopping"
  link   "Practice Software Testing - Toolshop"
  ...
</code></pre>
<p>The model answers with the number of the option it picked, and the confidence score tells the healer whether to trust it. You'll see real answers in the scenarios below.</p>
<p>Small models like these are touchy about how a question is asked. Rewording it or shuffling the options can swing the confidence a lot (I'll share the numbers in the lessons at the end). So code builds every question, never by hand, always in the same tested format: at most 10 options, always in page order. The exact template is in my repo if you want to see it.</p>
<h2>How it works</h2>
<p>Here is the whole thing on one page: the healer, the guardrails around it, and the point where the Playwright Test agent (Claude) takes over. The numbers on the flow cards match the numbered rules further down.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6acb88afdb902bfaa8a78af9/f59790fa-85b6-4b41-95e8-774b38a2b352.gif" alt="" style="display:block;margin:0 auto" />

<p>And here is the same flow as it runs in the terminal:</p>
<pre><code class="language-plaintext">npx playwright test ── ✅ pass ──► done (no AI involved)
        │
        ❌ waiting for getByTestId('proceed-1')
        ▼
npm run heal
   1. REPORT   which locator broke?           test-results/results.json
   2. MEMORY   what was it meant to be?       @intent note in the page object
   3. EYES     what was on the page?          snapshot saved when the test failed
   4. LAYA     which element is it now?       choice + confidence, ~0.2 s, free
   5. FIX + VERIFY: edit page object → re-run the failing test
        ├── ✅ passes → keep it; a human reviews the diff
        └── ❌ fails, or confidence &lt; threshold → UNDO → hand-off note → Playwright Test agent (Claude)
</code></pre>
<p>A few of these steps need a bit more explanation.</p>
<p><strong>Memory.</strong> A test id tells you where an element was, not what it was for. So I added a short note above each important locator:</p>
<pre><code class="language-ts">/** @intent button "Proceed to checkout" — cart (step 1) to sign-in (step 2) */
readonly proceed1Button: Locator = this.page.getByTestId('proceed-1');
</code></pre>
<p>The purpose matters here, because the shop has three different buttons that are all called "Proceed to checkout", one for each checkout step.</p>
<p><strong>Eyes:</strong> When a test fails, a Playwright fixture lists every visible clickable element on the live page, with its real attributes and in page order, and attaches that list to the test report. The healer runs later and doesn't need a browser to know what the page looked like.</p>
<p><strong>Decision:</strong> Laya gets the intent plus at most 10 candidate elements, and it answers with just a number: which candidate it is. The healer then builds the new locator from that element's real attributes (its <code>data-test</code>, or its role and name). The model can't invent a selector, because it only picks from things Playwright actually found on the page. It can still pick the wrong one, though, and that's why every fix gets verified.</p>
<p><strong>Safety rail:</strong> The healer edits the page object and re-runs the failing test. If the test still fails, it puts the file back exactly as it was.</p>
<p><strong>Hand-off:</strong> If Laya isn't confident, or its fix fails, the healer writes a ready-made request for the Playwright Test agent (Claude). In my setup I paste it in by hand, on purpose: I want a person to decide when it's worth spending tokens. This is a choice, not a limitation. If your team prefers a fully automatic flow, the healer can start the Playwright Test agent (Claude) on its own through the Claude Code command line, passing the same request. Teams that automate it usually have the agent open a pull request for review, so the fix still gets a human check before it's merged.</p>
<h3>The rules I ended up with</h3>
<p>Some of these were design decisions from day one. Others I added after getting burned. They kick in at different points of a heal, so here they are in order.</p>
<p><strong>Before the model is asked</strong></p>
<ol>
<li><p><strong>Heal only after a failure.</strong> CI runs plain Playwright specs, so while your tests pass, nothing costs a token.</p>
</li>
<li><p><strong>Know which locator broke, and what it was for.</strong> When a test fails, Playwright writes the error into <code>test-results/results.json</code>. The healer reads the broken locator from it (<code>getByTestId('proceed-1')</code>) and finds every page object that defines it. Those are the "listed locators" you'll see later, and the only ones a fix is allowed to touch. Each one carries an <code>@intent</code> note, so the model knows what the element was for.</p>
</li>
</ol>
<p><strong>When the model is asked</strong></p>
<ol>
<li><strong>The model chooses, it never invents.</strong> Laya (or Jev) picks from elements that actually exist on the page and never writes a selector. The healer builds the locator from the chosen element's real attributes.</li>
</ol>
<p><strong>After the model answers</strong></p>
<ol>
<li><p><strong>No confidence, no edit.</strong> Below the threshold, the code stays untouched and the healer goes straight to the hand-off.</p>
</li>
<li><p><strong>Verify every fix.</strong> The healer re-runs the failing test, and if it still fails, the change is undone automatically. This is the rule that saved me in scenario 2.</p>
</li>
</ol>
<p><strong>When it goes to the Playwright Test agent (Claude)</strong></p>
<ol>
<li><p><strong>Give the agent strict rules.</strong> The hand-off note lists the locators from rule 2, with file, line and property name, and the agent may change only those. If the app itself looks broken, it explains instead of editing the test. The agent does write the new selector itself, so its fix gets the same re-run check.</p>
</li>
<li><p><strong>Keep a human in the loop.</strong> Someone reviews every fix, and spending paid tokens is a human call too. I paste the hand-off note in by hand, but you can automate it and have the agent open a pull request instead.</p>
</li>
</ol>
<hr />
<h2>Scenario 1: the button is renamed, and Laya fixes it alone</h2>
<p>I simulated the developers renaming the cart's checkout button.</p>
<table>
<thead>
<tr>
<th></th>
<th>Before</th>
<th>After</th>
</tr>
</thead>
<tbody><tr>
<td><code>data-test</code></td>
<td><code>proceed-1</code></td>
<td><code>checkout-continue</code></td>
</tr>
<tr>
<td>Text</td>
<td>"Proceed to checkout"</td>
<td>"Continue to checkout"</td>
</tr>
</tbody></table>
<p>The test breaks, and a plain text search for "Proceed to checkout" would have broken too.</p>
<p>This is what Laya saw and answered (10 candidates, top three shown):</p>
<pre><code class="language-plaintext">0.878  button "Continue to checkout"   &lt;- picked
0.092  button "Continue Shopping"
0.006  link   "Practice Software Testing - Toolshop"
confidence 0.78, 243 ms
</code></pre>
<p>It matched meaning, not text. "Proceed" and "Continue to checkout" do the same job. It also stepped around a nasty trap: "Continue Shopping" shares a word with the new button but does the opposite thing.</p>
<p>From there the healer did its job. It built the new locator from the picked element's real id, <code>getByTestId('checkout-continue')</code>, and updated it in both page objects that used <code>proceed-1</code>. It found the duplicate through the <code>@intent</code> notes. Then it re-ran the test, which passed in 6.8 s, and printed "Fixed and verified". I reviewed the diff and that was it.</p>
<p>The cost was 0 tokens. It was about 0.25 s of Laya time plus one test re-run, all on my laptop.</p>
<h2>Scenario 2: the button becomes an icon, Laya fails safely, and the agent steps in</h2>
<p>This time I replaced the button's text with an arrow.</p>
<table>
<thead>
<tr>
<th></th>
<th>Before</th>
<th>After</th>
</tr>
</thead>
<tbody><tr>
<td><code>data-test</code></td>
<td><code>proceed-1</code></td>
<td><code>btn-next</code></td>
</tr>
<tr>
<td>Text</td>
<td>"Proceed to checkout"</td>
<td>"→"</td>
</tr>
</tbody></table>
<p>Nothing on the page says "checkout" anymore. Here's what Laya answered:</p>
<pre><code class="language-plaintext">0.653  button "Continue Shopping"   &lt;- picked   ❌ wrong
0.218  button "→"
confidence 0.51, 241 ms
</code></pre>
<p>Laya was wrong, and 0.51 was just above my 0.5 threshold, so the healer applied the fix. Then the safety rail earned its place. The re-run took the test back to shopping, where it failed at the next step. The healer undid its change, so the page objects were back exactly as they had been, and it wrote the hand-off note:</p>
<pre><code class="language-plaintext">Use the playwright-test-healer agent to fix the failing test
tests/spec/e2e/checkout/checkout.spec.ts:7 (project toolshop-e2e).
The locator getByTestId('proceed-1') no longer finds anything. It is defined in:
- tests/pages/pom/cart.page.ts:6 (proceed1Button)
- tests/pages/pom/checkout.page.ts:15 (proceed1Button)
It was meant to be: button "Proceed to checkout" — cart (step 1) to sign-in (step 2).
The fast local healer (Laya) could not fix it: it tried getByTestId('continue-shopping'),
but the test still failed, so the change was undone.
Rules: change only the locator(s) listed above, keep the @intent comments, …
If the app itself looks broken, do not change the test; explain why instead.
</code></pre>
<p>The Playwright Test agent (Claude) took it from there. It ran the test and read the snapshot of the failing page. It worked out that the "→" sitting next to "Continue Shopping" was the checkout button. Then it changed both locators to <code>getByRole('button', { name: '→' })</code> and re-ran the test.</p>
<p>That cost about 26,000 tokens, 12 tool calls and roughly 2 minutes.</p>
<p>On its run, the test also hit an unrelated, intermittent failure in the billing-address form. The agent reported it and left it alone, exactly as the rules said. I re-ran the test twice afterwards and it passed both times, so it's a flaky step in the suite that's now on my list.</p>
<hr />
<h2>Where the tokens and time actually go</h2>
<table>
<thead>
<tr>
<th></th>
<th>Scenario 1 (renamed)</th>
<th>Scenario 2 (icon only)</th>
</tr>
</thead>
<tbody><tr>
<td>Fixed by</td>
<td>Laya</td>
<td>Playwright Test agent (Claude)</td>
</tr>
<tr>
<td>Laya</td>
<td>✅ right, confidence 0.78</td>
<td>❌ wrong, confidence 0.51, undone by the safety rail</td>
</tr>
<tr>
<td>Tokens</td>
<td>0</td>
<td>~26k</td>
</tr>
<tr>
<td>Time</td>
<td>~0.25 s decision + ~7 s verification</td>
<td>~2 min</td>
</tr>
</tbody></table>
<p>The savings come from four places.</p>
<p><strong>Simple breaks are free and take seconds.</strong> Renames and rewordings are, in my experience, the most common kind of locator break, and Laya fixes and verifies them locally. The one agent run I measured cost about 26k tokens and 2 minutes, so every break Laya handles avoids a session like that. I didn't send scenario 1 to the agent, so I can't give you its exact cost. It would have been the same kind of session.</p>
<p><strong>Passing tests cost nothing.</strong> No AI runs in CI. A runtime agent, by comparison, has an LLM read the page at every step of every run.</p>
<p><strong>Hard breaks get a head start.</strong> The hand-off note already holds the diagnosis (broken locator, files, intent, what Laya tried), so the agent doesn't start from zero.</p>
<p><strong>You decide when to spend.</strong> The paid fallback only starts when a person pastes the hand-off note.</p>
<p>Time matters as much as tokens here. A Laya decision takes about 0.2 s, so the total is dominated by re-running the test to verify the fix, and you'd want to do that anyway.</p>
<p>If I had to draw it, it's an escalation ladder, and each step costs more and can handle more:</p>
<pre><code class="language-plaintext">Laya (free, ~0.2 s) → verify → Playwright Test agent (Claude) (tokens, ~minutes) → human
</code></pre>
<hr />
<h2>Limits, and what I'd do about them</h2>
<p>This is a prototype built to answer one question, so it has limits. Most of them come from my setup, not from the approach.</p>
<ul>
<li><p><strong>I only handle</strong> <code>getByTestId</code> <strong>breaks for now.</strong> The idea isn't tied to test ids, but my code is. Other locator types (<code>getByRole</code>, <code>getByText</code>, <code>getByLabel</code>, CSS) fail with their own error messages, so they need small changes in my repo: extend the failure pattern in <code>heal.ts</code> to recognise them, and match them to their <code>@intent</code> note in <code>intents.ts</code>. The decision part stays the same, because the model only ever chooses between elements on the page.</p>
</li>
<li><p><strong>I only collect clickable elements.</strong> Inputs, dropdowns and checkboxes can be added to the page snapshot in the same way.</p>
</li>
<li><p><strong>Elements without text or a label are invisible to the model.</strong> That's partly why scenario 2 failed. Adding <code>aria-label</code>s to icon-only buttons fixes that and helps accessibility too.</p>
</li>
<li><p><strong>Small context.</strong> Laya's English model reads about 512 tokens, so busy pages get trimmed to 10 candidates, and the right one can get cut. If you need more room, switch to Jev, which handles about 32k to 64k tokens and up to 255 options per question. Jev does have a cost, though. It's hosted and paid (about \(0.084 per million input tokens, output is free), so the "0 tokens" in the title applies to the Laya setup I ran. Even so, Jev is far cheaper than the Playwright Test agent (Claude): a 512-token question costs roughly \)0.00004, while the one agent run I measured used about 26,000 tokens. It's the same API, so only the config changes (plus raising the 10-option cap I set). Laya's multilingual model, with up to about 8k tokens, is a free middle ground.</p>
</li>
<li><p><strong>My 0.5 threshold isn't calibrated.</strong> A wrong answer scored 0.51. The proper fix is to run the healer over many simulated breaks, record confidence against correctness, and pick the threshold from that data. Verification stays as the safety net either way.</p>
</li>
<li><p><strong>The hand-off is manual, by choice.</strong> A team happy to spend the tokens could start the Playwright Test agent (Claude) automatically through the Claude Code command line, ideally opening a pull request for review instead of changing tests silently.</p>
</li>
</ul>
<h2>Try it</h2>
<p>The code is in my GitHub repo: <a href="https://github.com/sahil9256/playwright-laya-healer">playwright-laya-healer</a>. Both scenarios are included as a switch, so you can watch Laya fix the first one and hand the second to the Playwright Test agent (Claude).</p>
<p>You'll need Node.js 24+, Python for Laya (CPU works, a GPU is faster), and Claude Code to run the Playwright Test agent (Claude) fallback.</p>
<hr />
<h2>About me</h2>
<p>I'm Sahil, an SDET and AI-powered QA engineer. I help teams build test automation that is faster to maintain and cheaper to run, using AI where it actually helps. More of my work is at <a href="https://sahilsharmaqa.com/">sahilsharmaqa.com</a>.</p>
]]></content:encoded></item></channel></rss>