<?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: Andrzej S.</title>
    <description>The latest articles on DEV Community by Andrzej S. (@phetphet).</description>
    <link>https://dev.to/phetphet</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%2F4038036%2Ffbcb98bc-d7b8-42e6-b899-3811354e8dfa.png</url>
      <title>DEV Community: Andrzej S.</title>
      <link>https://dev.to/phetphet</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/phetphet"/>
    <language>en</language>
    <item>
      <title>Stop letting your AI guess selectors.</title>
      <dc:creator>Andrzej S.</dc:creator>
      <pubDate>Mon, 20 Jul 2026 20:02:37 +0000</pubDate>
      <link>https://dev.to/phetphet/your-ai-writes-e2e-tests-that-look-right-and-fail-i-built-a-map-so-it-stops-guessing-468g</link>
      <guid>https://dev.to/phetphet/your-ai-writes-e2e-tests-that-look-right-and-fail-i-built-a-map-so-it-stops-guessing-468g</guid>
      <description>&lt;h1&gt;
  
  
  Your AI writes E2E tests that look right and fail. I built a map so it stops guessing.
&lt;/h1&gt;

&lt;p&gt;AI agents are surprisingly bad at writing end-to-end tests for real apps. Not demo apps - real ones. The kind with zero &lt;code&gt;data-testid&lt;/code&gt; attributes, three i18n locales, forms that load their defaults asynchronously, and selectors that only make sense if you wrote the frontend yourself.&lt;/p&gt;

&lt;p&gt;I watched agents do this at work: guess a selector, fail, guess again, loop, then finally produce a test that looks completely reasonable and fails on the first run. My favorite failure mode: the agent wrote &lt;code&gt;getByRole('tab', ...)&lt;/code&gt; for tabs that are - of course - plain buttons with click handlers. The test was well-written. It was also fiction.&lt;/p&gt;

&lt;p&gt;The root cause is simple: the agent has never seen your app. Runtime AI-navigation tools (browser-use, Stagehand, Playwright MCP) explore it live, but they have no persistent memory - every task starts from zero, and exploration burns tokens on every run.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;terra-incognita&lt;/strong&gt;, an open-source "app knowledge compiler". The idea:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A human records&lt;/strong&gt; navigation sessions in a real browser - you just click through your app once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A deterministic distiller&lt;/strong&gt; (no LLM, no API key) compiles those sessions into compact artifacts: an app map, an element dictionary with ranked selectors, a testability report, and a generated skill file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent consumes the map&lt;/strong&gt; when writing tests: prefer mapped selectors, never invent locators.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It is not a test recorder like codegen, not a runtime agent, not session replay. It compiles human ground truth into knowledge that any agent can read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it help? Depends how big a mess your app is.
&lt;/h2&gt;

&lt;p&gt;I benchmarked it with an A/B harness: same agent, same tasks, same turn budget - with and without the map. Beyond "does the test pass", I scored &lt;strong&gt;acceptance&lt;/strong&gt; (how many acceptance criteria the test actually asserts) and &lt;strong&gt;strict&lt;/strong&gt; (passes AND covers everything), because pass rates alone let narrow, under-asserting tests through.&lt;/p&gt;

&lt;p&gt;On a realistically messy production SPA (zero test-ids, three locales, async forms):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;small model went from 0/8 to 7/8 strict&lt;/strong&gt; with the map. Without it, its tests looked right and guessed wrong.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;mid-size model went from 29% to 54% strict&lt;/strong&gt;; acceptance-criteria coverage rose from 43% to 78%.&lt;/li&gt;
&lt;li&gt;The result I find most interesting: &lt;strong&gt;the small model with the map beat a ~7x more expensive model without it&lt;/strong&gt; on every quality metric. A map instead of a bigger model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the honest flip side: on an app that is already well-testable - stable roles, test-ids everywhere - the map produced &lt;strong&gt;zero lift&lt;/strong&gt;. There is nothing to fix. If that is your app, you do not need this tool. Maps are for wild territory.&lt;/p&gt;

&lt;p&gt;(These are small pilots - 1-3 runs per task, tasks written by me. Directional evidence, not a published benchmark. A methodology write-up is coming.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I did not expect: honesty needs a linter
&lt;/h2&gt;

&lt;p&gt;The generated skill tells the agent: if a flow is not on the map, say so instead of inventing selectors. Some models follow that rule 100% of the time. Others ignore it routinely and fabricate plausible-looking locators anyway.&lt;/p&gt;

&lt;p&gt;Prompt rules are advisory. So the tool ships a mechanical gate: &lt;code&gt;terra lint&lt;/code&gt; checks every locator in your test files against the recorded element dictionary and fails the build on anything that is not traceable to a real, observed element. The model's discipline stops mattering; the gate does not care whether it felt like following instructions today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The whole pipeline is deterministic and runs offline - record, distill, generate, lint. MIT licensed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;github.com/andrzej-stepien/terra-incognita&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terra record http://localhost:3000 &lt;span class="nt"&gt;--name&lt;/span&gt; checkout-flow
terra distill
terra generate-skill &lt;span class="nt"&gt;--target&lt;/span&gt; claude-code
terra lint tests/&lt;span class="k"&gt;**&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;.spec.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your app is a well-lit city grid, keep walking. If it is terra incognita - give your agent a map and tell me what breaks.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to reset 20 Zustand stores correctly on logout</title>
      <dc:creator>Andrzej S.</dc:creator>
      <pubDate>Mon, 20 Jul 2026 11:27:42 +0000</pubDate>
      <link>https://dev.to/phetphet/how-to-reset-20-zustand-stores-correctly-on-logout-1je8</link>
      <guid>https://dev.to/phetphet/how-to-reset-20-zustand-stores-correctly-on-logout-1je8</guid>
      <description>&lt;p&gt;A user logs out, another logs in on the same device - and suddenly sees the previous user's cart or permissions. Whether you have 3 stores or 20, resetting state on logout is a critical task that often falls through the cracks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the "Typical" Reset Fails
&lt;/h2&gt;

&lt;p&gt;The most common approach is usually: &lt;code&gt;reset: () =&amp;gt; set(initialState)&lt;/code&gt;. However, this fails in four specific ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Frozen dynamic values&lt;/strong&gt;: If you have &lt;code&gt;sessionId: crypto.randomUUID()&lt;/code&gt;, it was generated once at module load. Every "reset" restores that &lt;em&gt;same&lt;/em&gt; ID. A real reset needs to re-run the initializer.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Merge instead of replace&lt;/strong&gt;: Zustand's &lt;code&gt;set&lt;/code&gt; merges by default. Leftover keys from previous states survive. You need &lt;code&gt;set(fresh, true)&lt;/code&gt; to perform a full replacement.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Forgotten stores&lt;/strong&gt;: A manual &lt;code&gt;logout()&lt;/code&gt; function calling 20 different reset actions will eventually break the day someone adds store #21. You need a centralized registry.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Persist race conditions&lt;/strong&gt;: Resetting memory while &lt;code&gt;localStorage&lt;/code&gt; is still being read (rehydration) can cause the old data to overwrite your fresh state. If storage fails, Zustand might swallow the error, hanging your logout logic forever.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Solution: zustand-reset-manager
&lt;/h2&gt;

&lt;p&gt;I hit all of these walls in my own projects, so I built a small utility to handle it: &lt;code&gt;zustand-reset-manager&lt;/code&gt;. It’s MIT-licensed, has zero runtime dependencies, and supports Zustand v4/v5.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Define a Resettable Store
&lt;/h3&gt;

&lt;p&gt;The initializer keeps its exact Zustand signature, including middleware.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createResettableStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand-reset-manager&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useCart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createResettableStore&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CartState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)((&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// This will be re-generated on every reset!&lt;/span&gt;
  &lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;]&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;&lt;strong&gt;Update (v0.4.0):&lt;/strong&gt; the hooks promised in the comments are out, plus ordering - because with 20 stores the next question is always "but my session store must reset last".&lt;/p&gt;

&lt;p&gt;One place to react to every reset - logging, analytics, cleanup:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addResetListener&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;beforeReset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`resetting &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;afterReset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;Declare order instead of hand-sequencing 20 calls:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createResettableStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&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="nf"&gt;resetAllStores&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;logout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;dependsOn declares "reset me AFTER these" - bulk resets are topologically sorted (Kahn's algorithm), so cart and user reset before session, level by level; async resets are awaited per level, and independent stores within a level run in parallel. A cycle gets you a dev-mode warning and a safe fallback instead of a crash. reason is a free-form string that rides along to every hook, so "logout" vs "tenant-switch" vs "test" is one if away.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
