<?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: Kevin Julián Martínez Escobar</title>
    <description>The latest articles on DEV Community by Kevin Julián Martínez Escobar (@kevinccbsg).</description>
    <link>https://dev.to/kevinccbsg</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%2F304931%2Fc83bb861-920b-46b2-a2d4-09428d595e85.jpeg</url>
      <title>DEV Community: Kevin Julián Martínez Escobar</title>
      <link>https://dev.to/kevinccbsg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kevinccbsg"/>
    <language>en</language>
    <item>
      <title>3 Developer Archetypes That Can Break a Team</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Tue, 22 Sep 2026 08:31:47 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/3-developer-archetypes-that-can-break-a-team-44co</link>
      <guid>https://dev.to/kevinccbsg/3-developer-archetypes-that-can-break-a-team-44co</guid>
      <description>&lt;p&gt;There are developers who make a team faster. There are developers who make a team better. And there are developers who slowly make a team dependent on them, without anyone noticing.&lt;/p&gt;

&lt;p&gt;The three I want to talk about are usually rewarded for it. They look experienced, productive, sometimes indispensable. AI is making all three of them easier to scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Senior Cynic
&lt;/h2&gt;

&lt;p&gt;You have met this one. Years of experience and a long list of things that do not work. Every new approach reminds them of something they tried five years ago. A simpler architecture will not scale. A different testing strategy was already tried. Another tool is not going to solve anything.&lt;/p&gt;

&lt;p&gt;Experience is valuable. It becomes a problem when it turns into a filter that rejects anything unfamiliar. The Senior Cynic is usually not protecting the architecture, they are protecting the mental model they have built over their career. Knowing why something failed in the past is useful. Assuming it cannot work in a different context is not.&lt;/p&gt;

&lt;p&gt;AI makes this cheaper. Ask for arguments and you get arguments: comparisons, trade-off tables, a full technical rationale for the position you already held. You no longer just have a strong opinion, you have twenty pages backing it up in ten seconds.&lt;/p&gt;

&lt;p&gt;The antidote is not optimism, it is evidence. Try the thing. Measure it. Let the current context weigh more than the muscle memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Guess Coder
&lt;/h2&gt;

&lt;p&gt;This one is easier to spot, because it shows up in the code. They build for a future that does not exist yet. A simple requirement becomes an abstraction. The abstraction becomes a framework. The framework needs configuration, the configuration needs an extension system, and now you maintain infrastructure for a problem you have never actually had.&lt;/p&gt;

&lt;p&gt;The intentions are usually good. They want to avoid technical debt. They want something that scales. They have read about SOLID, clean architecture, hexagonal architecture, event-driven systems, distributed everything. The catch is that principles without context turn into cargo cult engineering, and often the best architecture for the future is the simplest one you can change today.&lt;/p&gt;

&lt;p&gt;AI makes this effortless. Ask an agent to "make this extensible" and you get hundreds of lines of perfectly reasonable looking code. It compiles. The structure looks sophisticated. And you have just spent an afternoon solving a problem you do not have.&lt;/p&gt;

&lt;p&gt;The fix is not to stop thinking about the future, it is to separate &lt;strong&gt;known future requirements from imagined ones&lt;/strong&gt;. Build for what you know, keep the code easy to change, and validate an assumption before you turn it into architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Superhero
&lt;/h2&gt;

&lt;p&gt;This is the one I worry about most.&lt;/p&gt;

&lt;p&gt;The Superhero knows everything. The weird legacy service, how the deploy actually works, why that obscure config exists. When production breaks, everybody knows who to call. They are genuinely valuable, and that is exactly the problem.&lt;/p&gt;

&lt;p&gt;The team learns that the fastest route to a solution is to ask them. So people stop investigating. They stop learning the system. They stop taking ownership. The Superhero rarely pushes back on that, and sometimes reinforces it: "I'll just do it", "it's faster if I handle it", "I'll explain it later". Because they are good at solving problems, it works. In the short term.&lt;/p&gt;

&lt;p&gt;Then the organization starts rewarding the behavior. They become the person who saves releases and fixes incidents, and being indispensable starts to look like career progress. But there is a difference between being &lt;strong&gt;valuable&lt;/strong&gt; and being &lt;strong&gt;a dependency&lt;/strong&gt;. A healthy team should not need one specific person to keep operating.&lt;/p&gt;

&lt;p&gt;The goal is not to stop the Superhero from solving incidents. It is to make sure the next incident does not need the same person. Fix the problem, share what you learned, improve the system, hand the responsibility over, reduce the odds of it happening again. Otherwise you are not removing the fire, you are building a very efficient fire department around one person.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the three have in common
&lt;/h2&gt;

&lt;p&gt;They look nothing alike, and they optimize for the same thing: themselves, instead of the team's ability to operate without them. The Cynic protects their experience. The Guess Coder protects their imagined future. The Superhero protects their indispensability.&lt;/p&gt;

&lt;p&gt;AI amplifies all three. More arguments, more complexity, more changes landing faster than the rest of the team can follow them.&lt;/p&gt;

&lt;p&gt;Which is why I do not think AI adoption is mostly a tooling problem. It is a team problem. A team with a strong engineering culture gets amplified. A team with weak delegation, thin tests, knowledge silos and a taste for over-engineering gets amplified too, and much faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to optimize for instead
&lt;/h2&gt;

&lt;p&gt;Not developers who know everything, but developers who make knowledge accessible. Not developers who build for every possible future, but developers who keep today's system easy to change. Not developers who are indispensable, but developers who make the team less dependent on them.&lt;/p&gt;

&lt;p&gt;The strongest developer on a team is probably not the one who can solve every problem. It is &lt;strong&gt;the one who makes sure the team does not have to solve the same problem twice&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With AI in the loop, that distinction matters more, not less.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>teamwork</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your Agent Says the Tests Pass. Watch Them</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Tue, 15 Sep 2026 07:25:23 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/your-agent-says-the-tests-pass-watch-them-kgd</link>
      <guid>https://dev.to/kevinccbsg/your-agent-says-the-tests-pass-watch-them-kgd</guid>
      <description>&lt;p&gt;An agent opened a pull request on my repo last week. Eleven files changed, three new tests, all green.&lt;/p&gt;

&lt;p&gt;So what do you actually check?&lt;/p&gt;

&lt;p&gt;You can read the diff. You can read the test file and see what it asserts. Both of those tell you what the agent &lt;em&gt;believes&lt;/em&gt; it built. Neither tells you what a person using the thing would see, and that gap is where the interesting failures live.&lt;/p&gt;

&lt;p&gt;I added a label. A minute later there was a comment on the pull request:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Recording: 3 clip(s), one per test this branch added.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three short videos. One per test the branch added. I watched them in under a minute and knew more than the diff had told me in five.&lt;/p&gt;

&lt;h2&gt;
  
  
  The green tick got cheaper
&lt;/h2&gt;

&lt;p&gt;For twenty years a passing test was a reasonable proxy for "someone thought about this". A human wrote the test, then wrote code until it passed. The test carried the intent of a second person, or at least of the same person on a different day.&lt;/p&gt;

&lt;p&gt;When an agent writes the code and the test that proves the code, that separation is gone. The test is not an independent check. It is the same model's opinion, expressed twice.&lt;/p&gt;

&lt;p&gt;That does not make the test worthless. It makes it a &lt;strong&gt;claim&lt;/strong&gt; rather than &lt;strong&gt;evidence&lt;/strong&gt;, and claims need checking in a way that evidence does not.&lt;/p&gt;

&lt;p&gt;The cheapest check I have found is watching the thing run.&lt;/p&gt;

&lt;h2&gt;
  
  
  A test file tells you what it asserts, not what it looks like
&lt;/h2&gt;

&lt;p&gt;Here is a test that passes and proves nothing useful:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenDom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&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;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;Add project&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;be.visible&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;The button exists. It is visible. The test is green.&lt;/p&gt;

&lt;p&gt;It is also possible that the button sits on top of the heading, that the empty state it belongs to renders below the fold, or that clicking it does nothing at all because nobody asked for that yet. All of those are consistent with a green run and none of them are visible in the test file.&lt;/p&gt;

&lt;p&gt;You do not find those by reading harder. You find them by looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deterministic, not a demo
&lt;/h2&gt;

&lt;p&gt;The distinction that makes this useful rather than decorative: the clip is produced by the run, not recorded by a person.&lt;/p&gt;

&lt;p&gt;A demo video is a performance. Somebody walked through the app once, on their machine, at a moment when it happened to work, and the recording starts going stale the second it is saved. Six months later it shows a UI that no longer exists, and nobody notices because nobody regenerates it.&lt;/p&gt;

&lt;p&gt;A test recording is the same execution that produced the green tick, captured. Re-run the suite and you get a new clip. Change the behaviour and the clip changes with it, or the test fails and there is no clip at all. It cannot drift from the code, because it &lt;em&gt;is&lt;/em&gt; the code running.&lt;/p&gt;

&lt;p&gt;That is what makes it safe to attach to a pull request and safe to keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pacing detail that makes it watchable
&lt;/h2&gt;

&lt;p&gt;The first time I tried recording a test suite I got a video that was mostly a flash. Tests run in milliseconds. A faithful recording of a fast test is unwatchable, which is why most attempts at this quietly get abandoned.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/twd-cli" rel="noopener noreferrer"&gt;twd-cli&lt;/a&gt; paces the execution itself rather than slowing the video down afterwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx twd-cli run &lt;span class="nt"&gt;--record&lt;/span&gt; &lt;span class="nt"&gt;--test&lt;/span&gt; &lt;span class="s2"&gt;"checkout flow"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Frames are captured at full rate and the pauses land where something just happened. The default is 300ms between actions, and &lt;code&gt;--record-pace&lt;/code&gt; moves it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx twd-cli run &lt;span class="nt"&gt;--record&lt;/span&gt; &lt;span class="nt"&gt;--record-pace&lt;/span&gt; 500 &lt;span class="nt"&gt;--test&lt;/span&gt; &lt;span class="s2"&gt;"checkout flow"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference between pacing the run and slowing the file matters more than it sounds. A slowed-down video is uniformly sluggish. A paced run has rhythm: the click, then a beat, then the thing that happened. You can follow it.&lt;/p&gt;

&lt;p&gt;Output is one file per matched test, named after the test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;twd-artifacts/
  todos-adds-a-todo.mp4
  todos-marks-a-todo-done.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  In CI, record only what changed
&lt;/h2&gt;

&lt;p&gt;Recording an entire suite on every pull request produces an archive nobody opens. The version that gets watched records only the tests the branch added or modified:&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BRIKEV/twd-cli/.github/actions/record@v1.8.0&lt;/span&gt;
&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;changed-since&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.base.sha }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The action installs ffmpeg, runs the recording, uploads the clips as an artifact, and gives you back a count and a URL you can put in a pull request comment. The inputs are documented at &lt;a href="https://twd.dev/recording#recording-in-ci" rel="noopener noreferrer"&gt;twd.dev/recording#recording-in-ci&lt;/a&gt;.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftorvqr8dixzyohptd9za.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftorvqr8dixzyohptd9za.png" alt="Github PR with a record label and the comment of the videos recorded" width="800" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three clips on a pull request is a review aid. Four hundred clips is a data retention problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it is actually for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The person who cannot read the test.&lt;/strong&gt; A product owner can watch fifteen seconds and say "that is not what I meant". They cannot say that about a test file, and they will not tell you they cannot read it. They will just approve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You, in six months.&lt;/strong&gt; The pull request that changed this behaviour has a clip in it. That is a faster answer to "what did this used to do" than any amount of archaeology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You, at 5pm.&lt;/strong&gt; Reviewing agent output is a new and genuinely tiring kind of work. The bottleneck stopped being how fast code gets written and became how much of it you can actually check. Video is the highest bandwidth per second of attention I have found.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;A label, or one flag. Recording requires ffmpeg 8 or later, and twd-cli validates it before launching the browser so you get a clear failure rather than a corrupt file.&lt;/p&gt;

&lt;p&gt;In my repo it is a &lt;code&gt;record&lt;/code&gt; label on the pull request, applied after the agent has finished. Not automatic, because not every change is worth a video, and the person deciding that is me.&lt;/p&gt;

&lt;p&gt;That is the whole trade. A minute of CI and a minute of watching, against approving a diff you skimmed because it was green.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>githubactions</category>
      <category>twd</category>
    </item>
    <item>
      <title>Your Tests Pass. Your Layout Is Broken</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Tue, 08 Sep 2026 21:18:14 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/your-tests-pass-your-layout-is-broken-5e8j</link>
      <guid>https://dev.to/kevinccbsg/your-tests-pass-your-layout-is-broken-5e8j</guid>
      <description>&lt;p&gt;You change one line of CSS. A &lt;code&gt;flex-direction&lt;/code&gt;, a &lt;code&gt;max-width&lt;/code&gt;, a padding on a shared card component.&lt;/p&gt;

&lt;p&gt;Every test passes. The button is still there, the text still says "Continue", the modal still opens. Green across the board.&lt;/p&gt;

&lt;p&gt;And the pricing page now has three columns overlapping on top of each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assertions were never going to catch that
&lt;/h2&gt;

&lt;p&gt;This is not a gap in your test suite. It is what assertions are.&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="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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;have.text&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="s1"&gt;Continue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&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;Those questions are about content and state. Is the text right, is the element there, is it enabled. Every one of them still answers yes on a page whose layout has collapsed, because none of them is looking at where anything sits.&lt;/p&gt;

&lt;p&gt;The information you need is geometry, and nothing in a normal test suite is measuring it.&lt;/p&gt;

&lt;h2&gt;
  
  
  So why doesn't everyone do visual testing?
&lt;/h2&gt;

&lt;p&gt;The tools have existed for years. Most teams either never adopt them or quietly stop looking at them, and the reason is not the invoice.&lt;/p&gt;

&lt;p&gt;It's the approval queue.&lt;/p&gt;

&lt;p&gt;A visual testing tool compares screenshots pixel by pixel. So a font renders half a pixel differently, or a shadow shifts, and you get forty changed screenshots to review. None of them is a bug. You click approve on all of them.&lt;/p&gt;

&lt;p&gt;Do that twice and you have learned a habit: approve the batch, do not look. From that point on your visual testing is a formality. You are paying for a service and reviewing nothing, which is worse than not having it, because now everyone believes it's covered.&lt;/p&gt;

&lt;p&gt;The failure mode is not "it costs money". It's "it produced so much noise that we stopped reading it".&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch the geometry, not the pixels
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/twd-js" rel="noopener noreferrer"&gt;twd-js&lt;/a&gt; ships a beta command called &lt;code&gt;matchLayout&lt;/code&gt; that starts from a different question. Not "are these two images identical", but "did anything move".&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;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;landing&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;It deliberately does not see a colour change. It does not see a changed string. It does not see a font rendering one pixel wider. Those either do not matter for layout or are already covered by the assertions you have.&lt;/p&gt;

&lt;p&gt;What it does see is a block that grew, a column that collapsed, a button that wrapped, a section that disappeared. When it fails, you get your own page backwith the areas that moved boxed in red:&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7jy4lglkuvti7w7310un.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7jy4lglkuvti7w7310un.png" alt="A landing page with the regions that moved boxed in red and orange" width="800" height="1203"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Red is a region that changed. Orange is a region that is new, because the page got taller. Everything else is left alone, and that is most of the page.&lt;/p&gt;

&lt;p&gt;Being coarse on purpose is the whole point. There is nothing to approve in bulk because the noise never gets generated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reference is a text file
&lt;/h2&gt;

&lt;p&gt;The other half of the approval problem is where the review happens.&lt;/p&gt;

&lt;p&gt;With a hosted tool, approving is a separate queue: another dashboard, another login, a list of thumbnails to get through. That separation is what turns reviewing into clicking.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;matchLayout&lt;/code&gt; writes a small text file next to your test and you commit it. When a layout changes, the diff shows up in the pull request you were already reading.&lt;br&gt;
There is no second queue, because approving is the code review.&lt;/p&gt;

&lt;p&gt;No screenshots in git. No container spinning up somewhere to render your page. No account.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The verdict is decided by &lt;a href="https://www.npmjs.com/package/twd-cli" rel="noopener noreferrer"&gt;twd-cli&lt;/a&gt;, which&lt;br&gt;
pins the viewport, so a snapshot never fails because you resized your browser while working.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx twd-cli run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First run writes the reference and passes. Change your CSS, run again, and you get the picture above. If the change was intentional, &lt;code&gt;--update-snapshots&lt;/code&gt; accepts it.&lt;/p&gt;

&lt;p&gt;The docs are at &lt;a href="https://twd.dev/layout-snapshots" rel="noopener noreferrer"&gt;twd.dev/layout-snapshots&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's beta, and honest about what it is: a guard on geometry, not a pixel comparison. If you want to know that a button is exactly &lt;code&gt;#1a1a1a&lt;/code&gt;, assert it.&lt;br&gt;
If you want to know that your footer did not just eat the page, this is the cheaper way to find out.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>css</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Run Vue Component Tests Where Vue Runs: The Browser</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Mon, 31 Aug 2026 09:13:04 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/run-vue-component-tests-where-vue-runs-the-browser-c2l</link>
      <guid>https://dev.to/kevinccbsg/run-vue-component-tests-where-vue-runs-the-browser-c2l</guid>
      <description>&lt;p&gt;A Vue component's job is to produce DOM in a browser. Most component tests ask it to do that somewhere else: in Node, against a DOM that jsdom simulates. That has been the default since &lt;code&gt;npm create vue@latest&lt;/code&gt; started offering Vitest with jsdom, and for plenty of tests it is the right trade. It does set a ceiling on what a green test proves, though. Nothing is ever drawn. Your CSS never runs and nothing has a size or a position, so a component can pass every assertion in the file and still be broken on screen.&lt;/p&gt;

&lt;p&gt;I co-maintain &lt;a href="https://www.npmjs.com/package/twd-js" rel="noopener noreferrer"&gt;twd-js&lt;/a&gt;, which runs tests inside your actual dev server, in a sidebar, next to the app. It was built for flow testing: visit a route, click through the app, assert on what the user sees. Component testing was the thing it did not do.&lt;/p&gt;

&lt;p&gt;Then I tried calling &lt;code&gt;render()&lt;/code&gt; from &lt;a href="https://www.npmjs.com/package/@testing-library/vue" rel="noopener noreferrer"&gt;@testing-library/vue&lt;/a&gt; inside a TWD test.&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;afterEach&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&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="s2"&gt;twd-js/runner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userEvent&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="s2"&gt;twd-js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cleanup&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="s2"&gt;@testing-library/vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;HomeView&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../../views/HomeView.vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;componentHost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;restorePage&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="s2"&gt;../support/componentHost&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HomeView component&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;afterEach&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="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;restorePage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;increments the counter on click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="c1"&gt;// componentHost() is a blank div on an empty page. More on it below.&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HomeView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;componentHost&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;counter-button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contain.text&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;Count is 0&lt;/span&gt;&lt;span class="dl"&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;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contain.text&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;Count is 1&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing broke. The component mounts into the page, the sidebar shows it running, and reactivity does what reactivity does, in a browser, against a DOM nobody had to simulate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works at all
&lt;/h2&gt;

&lt;p&gt;Vue Testing Library is a thin layer. &lt;code&gt;render()&lt;/code&gt; mounts your component with &lt;code&gt;@vue/test-utils&lt;/code&gt; and binds &lt;code&gt;@testing-library/dom&lt;/code&gt; queries to the result. Neither of those is tied to jsdom. jsdom is just the DOM most people hand them.&lt;/p&gt;

&lt;p&gt;Give them a real one and the same code runs, except now the component is actually on a screen, with a real size, a real position, and your CSS applied to it. TWD already runs inside your app in the browser, so the real DOM is right there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;One helper, and no changes to your app.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;render()&lt;/code&gt; appends its container to &lt;code&gt;document.body&lt;/code&gt;, so the component lands after &lt;code&gt;#app&lt;/code&gt;, a full viewport below your layout. The app is also still on the page, so &lt;code&gt;screen&lt;/code&gt; matches its elements as well as the ones your test just rendered. Both of those are the same problem: the app is in the way.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HOST_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;twd-component-host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;APP_ROOT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;appRoot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Comment&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="kc"&gt;null&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;function&lt;/span&gt; &lt;span class="nf"&gt;componentHost&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;detachApp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HOST_ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;HOST_ID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isConnected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&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;function&lt;/span&gt; &lt;span class="nf"&gt;restorePage&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HOST_ID&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;attachApp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detachApp&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;APP_ROOT_ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;appRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;placeholder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; app detached by twd component test &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;attachApp&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;placeholder&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;appRoot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appRoot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;placeholder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;appRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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;Detaching the app root is not the same as emptying it. &lt;code&gt;app.innerHTML = ""&lt;/code&gt; pulls the DOM out from under Vue while its vnodes still point at those nodes, and the next router navigation patches elements that are no longer in the document. Moving the node out and putting it back leaves that correspondence intact, so &lt;code&gt;restorePage()&lt;/code&gt; returns a live app.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;prepend&lt;/code&gt; rather than &lt;code&gt;append&lt;/code&gt; puts the component at the top of the page. It also keeps it in normal flow, which matters in a browser: the page is offset to make room for the TWD sidebar, and anything in normal flow inherits that offset for free.&lt;/p&gt;

&lt;p&gt;One detail worth keeping: &lt;code&gt;cleanup()&lt;/code&gt; and &lt;code&gt;restorePage()&lt;/code&gt; belong in &lt;code&gt;afterEach&lt;/code&gt;, not &lt;code&gt;beforeEach&lt;/code&gt;. In jsdom the environment is torn down for you between files. In a real browser it is not, so renders stack up, and your flow tests need the app back before they run.&lt;/p&gt;

&lt;p&gt;That is the whole setup. Your app does not change.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2j862f55ml46lplqrnsa.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2j862f55ml46lplqrnsa.png" alt="twd testing component execution" width="799" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that changes how the test reads
&lt;/h2&gt;

&lt;p&gt;Here is the same idea on a view that fetches on mount and posts a form.&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;posts the form values when a todo is created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mockRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;getTodoList&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/todos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&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;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mockRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createTodo&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/todos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Write the Vue post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;In a real browser&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-09-01&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="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TodosView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;componentHost&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;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;getTodoList&lt;/span&gt;&lt;span class="dl"&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;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&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;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Title&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;Write the Vue post&lt;/span&gt;&lt;span class="dl"&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;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Description&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;In a real browser&lt;/span&gt;&lt;span class="dl"&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;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Date&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;2026-09-01&lt;/span&gt;&lt;span class="dl"&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;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&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;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;Create Todo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createTodo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Write the Vue post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;In a real browser&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-09-01&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing here is stubbed except the network. The component's &lt;code&gt;onMounted&lt;/code&gt;, its &lt;code&gt;ref&lt;/code&gt; state, the &lt;code&gt;v-model&lt;/code&gt; bindings and the axios call all run for real, and the assertion is on the request that actually left the browser. In jsdom that test usually starts with a &lt;code&gt;vi.mock&lt;/code&gt; of the API module, and from then on you are asserting that your component calls your mock correctly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;userEvent&lt;/code&gt; here comes from &lt;code&gt;twd-js&lt;/code&gt;, which re-exports &lt;a href="https://www.npmjs.com/package/@testing-library/user-event" rel="noopener noreferrer"&gt;@testing-library/user-event&lt;/a&gt; so there is nothing extra to install. It drives &lt;code&gt;v-model&lt;/code&gt; the way a person does, one key event at a time, and in a real browser those are real key events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both kinds of test, one run
&lt;/h2&gt;

&lt;p&gt;Component tests and flow tests are now the same kind of artifact. Files in the same project, listed in the same sidebar, running in the same browser session against the same bundle. There is no second runner, no second DOM, and no second config to keep in sync.&lt;/p&gt;

&lt;p&gt;The one thing to configure is the other direction. If you also run Vitest in the same repo, exclude the browser tests from it, or Vitest will collect them, find no &lt;code&gt;describe&lt;/code&gt; it recognises, and fail the run:&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="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;configDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;**/*.twd.test.*&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where each style fits
&lt;/h2&gt;

&lt;p&gt;Rendering a component in isolation is the right move when the component is the subject: a form's validation states, a modal that opens and closes, a table that sorts. You skip the navigation, you skip the fixtures, and the test says exactly what it is about.&lt;/p&gt;

&lt;p&gt;Flow tests stay the right move for anything that crosses a boundary. Routing, data loading, a sequence of screens, state that survives a navigation. Rendering a component in isolation to test those means rebuilding the app around it, which is how component test files end up longer than the components.&lt;/p&gt;

&lt;p&gt;The useful change is not that one replaced the other. It is that choosing between them is now a decision about scope, made per test, instead of a decision about which runner and which DOM you are committing to.&lt;/p&gt;

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

&lt;p&gt;If you already have Vue Testing Library tests, copy one, change the imports for &lt;code&gt;describe&lt;/code&gt; and &lt;code&gt;it&lt;/code&gt; to &lt;code&gt;twd-js/runner&lt;/code&gt;, and hand &lt;code&gt;render()&lt;/code&gt; the component host. The rest of the test stays as it is.&lt;/p&gt;

&lt;p&gt;The example app is on GitHub, with the same component tested in jsdom and in the browser side by side: &lt;a href="https://github.com/BRIKEV/twd-vue-example" rel="noopener noreferrer"&gt;BRIKEV/twd-vue-example&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I wrote the React version of this first, if you want the same walkthrough with &lt;code&gt;@testing-library/react&lt;/code&gt;: &lt;a href="https://dev.to/kevinccbsg/no-more-fake-dom-testing-library-unit-tests-in-the-real-browser-3p77"&gt;No More Fake DOM&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>twd</category>
      <category>vue</category>
      <category>frontend</category>
    </item>
    <item>
      <title>No More Fake DOM: Testing Library Unit Tests in the Real Browser</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Thu, 27 Aug 2026 18:34:24 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/no-more-fake-dom-testing-library-unit-tests-in-the-real-browser-3p77</link>
      <guid>https://dev.to/kevinccbsg/no-more-fake-dom-testing-library-unit-tests-in-the-real-browser-3p77</guid>
      <description>&lt;p&gt;Every Testing Library test you have written runs against a simulated DOM. You know this. You accepted it years ago, along with the small pile of things that come with it: no real layout, nothing ever drawn on a screen, &lt;code&gt;jsdom&lt;/code&gt; throwing on APIs it does not implement, and the quiet suspicion that a passing test and a working component are not quite the same claim.&lt;/p&gt;

&lt;p&gt;I co-maintain &lt;a href="https://www.npmjs.com/package/twd-js" rel="noopener noreferrer"&gt;twd-js&lt;/a&gt;, which runs tests inside your actual dev server. It was built for flow testing: visit a route, click through the app, assert on what the user sees. Component testing was the thing it did not do, and I said so in print when I &lt;a href="https://dev.to/kevinccbsg/in-browser-testing-vitest-browser-mode-and-twd-side-by-side-1i3j"&gt;compared it to Vitest Browser Mode&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then I tried calling &lt;code&gt;render()&lt;/code&gt; inside a TWD test, mostly to see what would break.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cleanup&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="s2"&gt;@testing-library/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;afterEach&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="s2"&gt;twd-js/runner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppProvider&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="s2"&gt;@/context/AppContext&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;twd&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="s2"&gt;twd-js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Add&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="s2"&gt;../Add&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;componentHost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;restorePage&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="s2"&gt;@/twd-tests/support/componentHost&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Add Component&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;afterEach&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="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;restorePage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renders the Add component&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// componentHost() is a blank div on an empty page. More on it below.&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AppProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Add&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AppProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;componentHost&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Add Item&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;be.visible&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing broke. The component mounts into the page, the sidebar shows it running, and the assertion checks an element that exists in a real browser. Same Testing Library API, no fake DOM under it.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fouwy4ewu43clgbbstm6s.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fouwy4ewu43clgbbstm6s.png" alt="component test in an actual browser"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;AppProvider&lt;/code&gt; is the real one, not a test double. &lt;code&gt;Add&lt;/code&gt; uses a hook that reads from context and posts to an API, and in a real browser both of those work, so there is nothing to stand in for. That turns out to matter more than the missing jsdom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works at all
&lt;/h2&gt;

&lt;p&gt;Testing Library was never tied to jsdom. &lt;code&gt;@testing-library/react&lt;/code&gt; renders a component into a DOM node and &lt;code&gt;@testing-library/dom&lt;/code&gt; queries it. jsdom is just the DOM most people hand it. Give it a real one and the same code runs, except now the component is actually on a screen, with a real size, a real position, and your CSS applied to it.&lt;/p&gt;

&lt;p&gt;TWD already runs inside your app in the browser, so the real DOM is right there. &lt;code&gt;render()&lt;/code&gt; uses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;One helper, and no changes to your app.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;render()&lt;/code&gt; appends its container to &lt;code&gt;document.body&lt;/code&gt;, so the component lands after the app, a full viewport below the layout. The app is also still on the page, so &lt;code&gt;screen&lt;/code&gt; matches its elements as well as the ones your test just rendered. Both of those are the same problem: the app is in the way.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HOST_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;twd-component-host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;APP_ROOT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;appRoot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Comment&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="kc"&gt;null&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;function&lt;/span&gt; &lt;span class="nf"&gt;componentHost&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;detachApp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HOST_ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;HOST_ID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isConnected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&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;function&lt;/span&gt; &lt;span class="nf"&gt;restorePage&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HOST_ID&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;attachApp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detachApp&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;APP_ROOT_ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;appRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;placeholder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; app detached by twd component test &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;attachApp&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;placeholder&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;appRoot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appRoot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;placeholder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;appRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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;Detaching the app root is not the same as emptying it. &lt;code&gt;root.innerHTML = ""&lt;/code&gt; pulls the DOM out from under React while it still holds references to those nodes, and the app does not come back. Moving the node out and putting it back leaves the references intact, so &lt;code&gt;restorePage()&lt;/code&gt; returns a live app.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;prepend&lt;/code&gt; rather than &lt;code&gt;append&lt;/code&gt; puts the component at the top of the page instead of below where the app used to be, so you can watch it run without scrolling.&lt;/p&gt;

&lt;p&gt;The payoff is that &lt;code&gt;screen&lt;/code&gt; behaves exactly like it does in jsdom. The only thing in the document is what your test rendered, which also covers anything the component renders through a portal, since that lands on &lt;code&gt;document.body&lt;/code&gt; rather than inside the container.&lt;/p&gt;

&lt;p&gt;One detail worth keeping: &lt;code&gt;cleanup()&lt;/code&gt; and &lt;code&gt;restorePage()&lt;/code&gt; belong in &lt;code&gt;afterEach&lt;/code&gt;, not &lt;code&gt;beforeEach&lt;/code&gt;. In jsdom the environment is torn down for you between files. In a real browser it is not, so renders stack up, and your flow tests need the app back before they run.&lt;/p&gt;

&lt;p&gt;That is the whole setup. Your app does not change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two traps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;screenDom&lt;/code&gt; will not find your component.&lt;/strong&gt; TWD exposes &lt;code&gt;screenDom&lt;/code&gt;, a wrapper around Testing Library queries scoped to your app root so it never matches the TWD sidebar. Testing Library's &lt;code&gt;render()&lt;/code&gt; mounts outside that root. I probed it in a real app to be sure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;container.parentElement=&amp;lt;body&amp;gt;   inside #root=false
screen: found   screenDom: not found   screenDomGlobal: found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So use Testing Library's own &lt;code&gt;screen&lt;/code&gt;, or TWD's &lt;code&gt;screenDomGlobal&lt;/code&gt;, which queries the whole document. If you pick &lt;code&gt;screenDomGlobal&lt;/code&gt;, keep queries specific, because it can also match elements inside the sidebar.&lt;/p&gt;

&lt;p&gt;Everything else works normally. &lt;code&gt;twd.should&lt;/code&gt; takes any element you hand it, whether a query found it in your app or in a component you just rendered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your test pattern probably says &lt;code&gt;.ts&lt;/code&gt;.&lt;/strong&gt; Component tests are &lt;code&gt;.tsx&lt;/code&gt;. A pattern of &lt;code&gt;/**/*.twd.test.ts&lt;/code&gt; skips them silently, with no error and no missing-file warning. They simply never appear in the sidebar:&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="nf"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;testFilePattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/**/*.twd.test.{ts,tsx}&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you also run Vitest in the same repo, exclude the browser tests from it. Vitest matches &lt;code&gt;*.test.tsx&lt;/code&gt; by default, collects the TWD files, finds no &lt;code&gt;describe&lt;/code&gt; it recognises, and fails the run with &lt;code&gt;No test suite found in file&lt;/code&gt;:&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="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;configDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;**/*.twd.test.*&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Both kinds of test, one run
&lt;/h2&gt;

&lt;p&gt;This is the part I did not expect to matter as much as it does.&lt;/p&gt;

&lt;p&gt;Component tests and flow tests are now the same kind of artifact. They are files in the same project, running in the same browser, in the same session, against the same instrumented bundle. So one command runs both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npx twd-cli run
&lt;span class="go"&gt;Running 14 test(s)...
Code coverage data written to .nyc_output/out.json

--- Run complete ---
  Passed: 14 | Failed: 0 | Skipped: 0
  Duration: 6.9s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten of those drive the whole app through routing, search, sorting and deletion. Four render a single dialog component in isolation. One coverage file comes out the other end, covering both.&lt;/p&gt;

&lt;p&gt;That last part is usually where multi-runner setups get tedious. Component coverage in one report, end-to-end coverage in another, and a merge step that someone maintains. Here there is nothing to merge, because there was only ever one run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each style fits
&lt;/h2&gt;

&lt;p&gt;Rendering a component in isolation is the right move when the component is the subject: a form's validation states, a dialog that opens and closes, a table that sorts. You skip the navigation, you skip the fixtures, and the test says exactly what it is about.&lt;/p&gt;

&lt;p&gt;Flow tests stay the right move for anything that crosses a boundary. Routing, data loading, a sequence of screens, state that survives a navigation. Rendering a component in isolation to test those means rebuilding the app around it, which is how component test files end up longer than the components.&lt;/p&gt;

&lt;p&gt;The useful change is not that one replaced the other. It is that choosing between them is now a decision about scope, made per test, instead of a decision about which runner and which DOM you are committing to.&lt;/p&gt;

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

&lt;p&gt;If you already have Testing Library tests, the fastest way to see this is to copy one, change the imports for &lt;code&gt;describe&lt;/code&gt; and &lt;code&gt;it&lt;/code&gt; to &lt;code&gt;twd-js/runner&lt;/code&gt;, and hand &lt;code&gt;render()&lt;/code&gt; the component host.&lt;/p&gt;

&lt;p&gt;The example app from this post is on GitHub, with the same component tested in jsdom and in the browser side by side: &lt;a href="https://github.com/kevinccbsg/frontend-challenge" rel="noopener noreferrer"&gt;kevinccbsg/frontend-challenge&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the next post I look at what happened when I put those two versions next to each other. The jsdom test mocked the hook it was testing through, and once I stopped mocking, it turned out the mocks had been doing most of the work.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>twd</category>
      <category>react</category>
      <category>frontend</category>
    </item>
    <item>
      <title>AI Made Code Review the Bottleneck. Attach the UI to Your PR</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Wed, 29 Jul 2026 18:57:30 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/ai-made-code-review-the-bottleneck-attach-the-ui-to-your-pr-575</link>
      <guid>https://dev.to/kevinccbsg/ai-made-code-review-the-bottleneck-attach-the-ui-to-your-pr-575</guid>
      <description>&lt;p&gt;You open a frontend pull request. The diff is 400 lines, an AI agent wrote most of it, and the description says "adds the checkout flow". CI is green.&lt;/p&gt;

&lt;p&gt;You read the diff. It looks reasonable. You still have no idea what the UI actually does.&lt;/p&gt;

&lt;p&gt;So you do what you always do. You pull the branch, install dependencies, start the dev server, and click through the flow yourself.&lt;/p&gt;

&lt;p&gt;Ten minutes later you approve the PR.&lt;/p&gt;

&lt;p&gt;That ten minutes is the bottleneck now.&lt;/p&gt;

&lt;p&gt;AI made writing code dramatically faster. Reviewing frontend behavior did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests solve part of the problem
&lt;/h2&gt;

&lt;p&gt;Tests already reduce a large part of that uncertainty.&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://twd.dev/" rel="noopener noreferrer"&gt;twd-js&lt;/a&gt;, they execute inside a real browser, against the real DOM, with the network mocked so runs stay deterministic. A green run means the entire flow actually executed.&lt;/p&gt;

&lt;p&gt;But a green run only tells the reviewer that the assertions passed.&lt;/p&gt;

&lt;p&gt;It does not tell them what the user sees.&lt;/p&gt;

&lt;p&gt;Was the validation message attached to the correct field?&lt;/p&gt;

&lt;p&gt;Did the modal actually close?&lt;/p&gt;

&lt;p&gt;Does the empty state look intentional, or does it look broken?&lt;/p&gt;

&lt;p&gt;Those are exactly the things reviewers still verify manually, and they are difficult to express as assertions alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the flow in the pull request
&lt;/h2&gt;

&lt;p&gt;Your tests already drive the application through the exact flow under review.&lt;/p&gt;

&lt;p&gt;If reviewers could simply watch that execution, they would not need to pull the branch just to understand the UI.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;twd-cli&lt;/code&gt; can record the run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx twd-cli run &lt;span class="nt"&gt;--record&lt;/span&gt; &lt;span class="nt"&gt;--test&lt;/span&gt; &lt;span class="s2"&gt;"checkout flow"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;twd-artifacts/checkout-flow.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop the video directly into the pull request description.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first problem you'll hit
&lt;/h2&gt;

&lt;p&gt;The first time you try this, you discover something unexpected.&lt;/p&gt;

&lt;p&gt;The tests finish in about a second.&lt;/p&gt;

&lt;p&gt;The recording is technically correct.&lt;/p&gt;

&lt;p&gt;It is also almost impossible for a human to follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Slowing the video down is the wrong fix
&lt;/h2&gt;

&lt;p&gt;The obvious solution is to stretch the recording afterwards.&lt;/p&gt;

&lt;p&gt;A simple ffmpeg filter can do that, but it stretches the same frames across a longer duration. As playback slows down, the effective frame rate drops.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Effective FPS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;td&gt;1.00s&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.5x&lt;/td&gt;
&lt;td&gt;1.90s&lt;/td&gt;
&lt;td&gt;15.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.25x&lt;/td&gt;
&lt;td&gt;3.90s&lt;/td&gt;
&lt;td&gt;7.7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At quarter speed, you're watching an 8 FPS video.&lt;/p&gt;

&lt;p&gt;It also slows every moment equally. The interesting interactions and the idle time both become longer. The result is not easier to understand. It is simply slower.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is where architecture matters
&lt;/h2&gt;

&lt;p&gt;Most testing tools record a browser that is being driven from the outside.&lt;/p&gt;

&lt;p&gt;The recording is a byproduct of automation, so once execution has finished, post processing is your only option.&lt;/p&gt;

&lt;p&gt;TWD works differently.&lt;/p&gt;

&lt;p&gt;Its commands execute as JavaScript inside the page itself.&lt;/p&gt;

&lt;p&gt;Because TWD owns the command loop, it can control &lt;strong&gt;when commands execute&lt;/strong&gt;, not just how the recording is encoded.&lt;/p&gt;

&lt;p&gt;Instead of stretching frames afterwards, it spaces out execution while the test is running.&lt;/p&gt;

&lt;p&gt;Frames are still captured at full frame rate.&lt;/p&gt;

&lt;p&gt;Pauses happen only after meaningful interactions.&lt;/p&gt;

&lt;p&gt;Typing is paced character by character instead of appearing all at once.&lt;/p&gt;

&lt;p&gt;That produces a recording that feels natural to watch without sacrificing smoothness.&lt;/p&gt;

&lt;p&gt;Which is why recordings are paced by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Default: 300ms pause after each command&lt;/span&gt;
npx twd-cli run &lt;span class="nt"&gt;--record&lt;/span&gt; &lt;span class="nt"&gt;--test&lt;/span&gt; &lt;span class="s2"&gt;"checkout flow"&lt;/span&gt;

&lt;span class="c"&gt;# Better for demos or stakeholder reviews&lt;/span&gt;
npx twd-cli run &lt;span class="nt"&gt;--record&lt;/span&gt; &lt;span class="nt"&gt;--record-pace&lt;/span&gt; 500 &lt;span class="nt"&gt;--test&lt;/span&gt; &lt;span class="s2"&gt;"checkout flow"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, values between &lt;strong&gt;200ms and 500ms&lt;/strong&gt; are usually enough to make interactions comfortable to follow while keeping recordings concise.&lt;/p&gt;

&lt;p&gt;The result is still a smooth 30 FPS video.&lt;/p&gt;

&lt;p&gt;Only the moments that matter become easier to observe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the reviewer gets
&lt;/h2&gt;

&lt;p&gt;The reviewer watches the actual flow.&lt;/p&gt;

&lt;p&gt;Driven by the actual tests.&lt;/p&gt;

&lt;p&gt;Running against the actual application.&lt;/p&gt;

&lt;p&gt;Not a screen recording someone remembered to make.&lt;/p&gt;

&lt;p&gt;Not a Loom video that becomes outdated after the next commit.&lt;/p&gt;

&lt;p&gt;The recording is regenerated every time the tests run.&lt;/p&gt;

&lt;p&gt;The review changes from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Does this diff look correct?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Is this the behavior we actually wanted?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the question worth a human's attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottleneck moved
&lt;/h2&gt;

&lt;p&gt;Every improvement in software development moves the bottleneck somewhere else.&lt;/p&gt;

&lt;p&gt;Today, AI has made writing code significantly faster.&lt;/p&gt;

&lt;p&gt;Frontend review is still largely manual because reviewers need to understand behavior, not just source code.&lt;/p&gt;

&lt;p&gt;A green checkmark confirms that the tests passed.&lt;/p&gt;

&lt;p&gt;It does not show what happened.&lt;/p&gt;

&lt;p&gt;Browser based tests can produce something much more valuable than a success badge.&lt;/p&gt;

&lt;p&gt;They can produce evidence.&lt;/p&gt;

&lt;p&gt;Sometimes, that evidence is a short video that saves every reviewer ten minutes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx twd-cli run &lt;span class="nt"&gt;--record&lt;/span&gt; &lt;span class="nt"&gt;--test&lt;/span&gt; &lt;span class="s2"&gt;"the flow you changed"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your tests already know how to reproduce the feature, your pull request should show it.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://twd.dev/recording" rel="noopener noreferrer"&gt;Recording Runs documentation&lt;/a&gt; for all available options.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>codereview</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Testing microfrontends with TWD, one suite per team</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Mon, 27 Jul 2026 20:21:04 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/testing-microfrontends-with-twd-one-suite-per-team-18kg</link>
      <guid>https://dev.to/kevinccbsg/testing-microfrontends-with-twd-one-suite-per-team-18kg</guid>
      <description>&lt;p&gt;Microfrontends split a frontend into apps that build, deploy and version on their own. One team ships React 19, another is still on React 17 and migrating away from it, a third runs Vue. That independence is the point, and it is usually why the architecture got chosen.&lt;/p&gt;

&lt;p&gt;It also means there is no single test setup anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  What usually happens to testing
&lt;/h2&gt;

&lt;p&gt;Each team picks its own stack, and the same product ends up with three runners, three sets of conventions, and three different definitions of a passing build. Or testing gets pushed into one shared end-to-end suite that lives outside every team's repo and belongs to nobody. When that suite goes red, the argument about whose change broke it takes longer than the fix.&lt;/p&gt;

&lt;p&gt;Neither outcome is anyone's plan. Both are what you get when the architecture splits and the testing story does not follow it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing is harder to skip now
&lt;/h2&gt;

&lt;p&gt;A lot of frontend code is generated rather than typed. Reading all of it line by line does not scale, and the useful question is not whether it looks right, it is whether the app still does what it claims. That answer has to come from the real thing, and it has to be cheap enough that every team gets it on every change.&lt;/p&gt;

&lt;p&gt;Splitting a frontend is a reasonable way to move faster. Splitting it and losing the check that the pieces still work is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same runner, whatever the framework
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://twd.dev" rel="noopener noreferrer"&gt;TWD&lt;/a&gt; runs tests inside the real browser, in a sidebar next to the app. It does not care which framework rendered the page, so the React team, the legacy React team and the Vue team all install the same thing.&lt;/p&gt;

&lt;p&gt;Setup stays small. On &lt;a href="https://vite.dev" rel="noopener noreferrer"&gt;Vite&lt;/a&gt; it is a plugin. On other bundlers it is a few lines in the dev entry point, plus one command to drop in the mock service worker. Nothing ships to production.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkw596hva2i7xjrwu853f.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkw596hva2i7xjrwu853f.png" alt="Three microfrontends running side by side, each with its own TWD sidebar and its own passing suite: React 19, React 17 and Vue 3" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each app keeps its own installation, its own tests and its own mocked endpoints. No shared fixture file, no shared test repo, nothing to coordinate before a team can ship. If you work with external vendors, "ship a green TWD suite" is an acceptance criterion they can satisfy in their own pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests that outlive the framework
&lt;/h2&gt;

&lt;p&gt;The suites read like &lt;a href="https://testing-library.com" rel="noopener noreferrer"&gt;Testing Library&lt;/a&gt;: &lt;code&gt;findByRole&lt;/code&gt;, &lt;code&gt;findByText&lt;/code&gt;, a click. Anyone who has written a React Testing Library test knows the shape already.&lt;/p&gt;

&lt;p&gt;Because the assertions run against the DOM and the accessibility tree, the same test works against a React class component, a React hooks component and a Vue component. That matters most for the team on the old version. The usual reason a legacy microfrontend exists is that someone is migrating away from it one route at a time, and tests written this way do not get rewritten when that migration lands. The suite is the part that survives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Living documentation, in the tab you already have open
&lt;/h2&gt;

&lt;p&gt;The sidebar sits next to the running app. The tests are executable examples of what each page does, in the same place you develop, so a new team member reads behaviour by running it instead of guessing from the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same suites in CI
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/twd-cli" rel="noopener noreferrer"&gt;twd-cli&lt;/a&gt; runs the same tests headless. Each team points it at its own dev server and runs its own suite in its own pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx twd-cli run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody blocks anybody. Nothing gets rewritten into a second framework for the pipeline. What you watched pass in the sidebar is what runs on the pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing to know
&lt;/h2&gt;

&lt;p&gt;Today TWD is set up per microfrontend, and each one is tested on its own. Testing the fully composed page, with every microfrontend mounted together, is not something it does yet. In practice the split suits the architecture: teams own their own apps, and their tests follow the same boundary.&lt;/p&gt;

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

&lt;p&gt;Microfrontends give every team its own framework, its own release and its own repo. They should not cost you the tests.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TWD: &lt;a href="https://twd.dev" rel="noopener noreferrer"&gt;twd.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Example repo (three microfrontends, three frameworks, a TWD suite each): &lt;a href="https://github.com/BRIKEV/twd-module-federation" rel="noopener noreferrer"&gt;github.com/BRIKEV/twd-module-federation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Getting started: &lt;a href="https://twd.dev/getting-started" rel="noopener noreferrer"&gt;twd.dev/getting-started&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Frameworks and bundlers: &lt;a href="https://twd.dev/frameworks" rel="noopener noreferrer"&gt;twd.dev/frameworks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Writing tests: &lt;a href="https://twd.dev/writing-tests" rel="noopener noreferrer"&gt;twd.dev/writing-tests&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;API mocking: &lt;a href="https://twd.dev/api-mocking" rel="noopener noreferrer"&gt;twd.dev/api-mocking&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CI execution: &lt;a href="https://twd.dev/ci-execution" rel="noopener noreferrer"&gt;twd.dev/ci-execution&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>testing</category>
      <category>twd</category>
      <category>microfrontends</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Real-browser testing for Open Cells apps with TWD</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Mon, 27 Jul 2026 08:39:38 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/real-browser-testing-for-open-cells-apps-with-twd-5ema</link>
      <guid>https://dev.to/kevinccbsg/real-browser-testing-for-open-cells-apps-with-twd-5ema</guid>
      <description>&lt;p&gt;&lt;a href="https://www.opencells.dev" rel="noopener noreferrer"&gt;Open Cells&lt;/a&gt; is an open-source framework from BBVA for building web apps out of Web Components. It leans on &lt;a href="https://lit.dev" rel="noopener noreferrer"&gt;Lit&lt;/a&gt;, real Shadow DOM, a hash-based router, and channels for state. If you build with Cells, you are working with real custom elements the whole way down. That is what makes Cells apps feel solid. It is also what makes them interesting to test.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual Vite stack, and the gap
&lt;/h2&gt;

&lt;p&gt;A Vite app usually reaches for two tools: &lt;a href="https://vitest.dev" rel="noopener noreferrer"&gt;Vitest&lt;/a&gt; for component tests and &lt;a href="https://playwright.dev" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; for end-to-end. Both are solid. For a Web Components app, each leaves a gap.&lt;/p&gt;

&lt;p&gt;Vitest runs in Node against a simulated DOM. For custom elements that means rebuilding the parts that matter most: Shadow DOM, element upgrades, the router, browser storage. You spend your effort standing in for the browser instead of using it, and you test an approximation of your app.&lt;/p&gt;

&lt;p&gt;Playwright drives a real browser, but a separate one, outside the tab you develop in. It shines for full end-to-end flows and is heavier for the everyday component check.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twd.dev" rel="noopener noreferrer"&gt;TWD&lt;/a&gt; offers a third place to test: the real browser, in a sidebar next to your app. Your Cells app renders exactly as it always does, and the tests run against it right there. Real DOM, real components with their Shadow DOM intact, real browser APIs. The only thing you replace is the network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not just Cells
&lt;/h2&gt;

&lt;p&gt;TWD is framework-agnostic. It works with any client-rendered frontend: React, Vue, Angular, Solid, Lit and Open Cells, even HTMX or plain vanilla JavaScript. If your app renders in the browser, TWD tests it in the browser. Cells is simply where the real-browser approach pays off most, because Cells is Web Components all the way down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup is small
&lt;/h2&gt;

&lt;p&gt;Cells apps run on &lt;a href="https://vite.dev" rel="noopener noreferrer"&gt;Vite&lt;/a&gt;, and TWD installs as a Vite plugin. Add it to the config and the TWD sidebar appears next to your app, discovering your tests. Nothing to wire into your entry file, nothing shipped to production.&lt;/p&gt;

&lt;p&gt;The tests read like &lt;a href="https://testing-library.com" rel="noopener noreferrer"&gt;Testing Library&lt;/a&gt;: &lt;code&gt;findByRole&lt;/code&gt;, &lt;code&gt;findByText&lt;/code&gt;, a click. Anyone who has written a React Testing Library test already knows the shape.&lt;/p&gt;

&lt;p&gt;Because the tests run in a real browser, browser APIs are real. localStorage, sessionStorage, cookies: no mocks, no shims. A feature that reads a saved preference or a stored session is tested against the browser actually doing it. When you need to cut the app off from the outside world, you mock the network, so a page that calls a backend runs against fixed responses and stays deterministic and offline.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F23jji5mfai4q2fvjsyf5.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F23jji5mfai4q2fvjsyf5.png" alt="The TWD sidebar running the Cells app's page tests in the browser" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage, without changing the build
&lt;/h2&gt;

&lt;p&gt;TWD also produces coverage. A coverage-only dev server instruments the app, the headless runner collects it, and you get a standard report. No change to how the app is built beyond that coverage serve. The same tests run in CI with one command, so what you write in the dev tab is what runs on every pull request.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9x5c2s46heyy1ttievkj.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9x5c2s46heyy1ttievkj.png" alt="nyc coverage report generated from the in-browser tests" width="799" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;For a framework built on Web Components, the real browser is the natural place to test. Cells gives you real custom elements. TWD lets you test them as they actually run.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TWD: &lt;a href="https://twd.dev" rel="noopener noreferrer"&gt;twd.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Example repo (a Cells app tested with TWD): &lt;a href="https://github.com/BRIKEV/twd-cells" rel="noopener noreferrer"&gt;github.com/BRIKEV/twd-cells&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Writing tests: &lt;a href="https://twd.dev/writing-tests" rel="noopener noreferrer"&gt;twd.dev/writing-tests&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;API mocking: &lt;a href="https://twd.dev/api-mocking" rel="noopener noreferrer"&gt;twd.dev/api-mocking&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CI execution: &lt;a href="https://twd.dev/ci-execution" rel="noopener noreferrer"&gt;twd.dev/ci-execution&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Coverage: &lt;a href="https://twd.dev/coverage" rel="noopener noreferrer"&gt;twd.dev/coverage&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>testing</category>
      <category>twd</category>
      <category>webcomponents</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Testing HTMX apps in the real browser with TWD</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Wed, 22 Jul 2026 20:48:23 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/testing-htmx-apps-in-the-real-browser-with-twd-3j4e</link>
      <guid>https://dev.to/kevinccbsg/testing-htmx-apps-in-the-real-browser-with-twd-3j4e</guid>
      <description>&lt;p&gt;HTMX apps are refreshingly simple: the server returns HTML fragments, and HTMX swaps them into the page in response to &lt;code&gt;hx-get&lt;/code&gt;, &lt;code&gt;hx-post&lt;/code&gt;, and friends. There is usually no bundler and no client framework. That simplicity is the point, and it is also why the usual testing tools feel wrong. A jsdom unit test cannot see an HTMX swap, and a full end-to-end runner is a lot of machinery to bolt onto an app that deliberately avoids machinery.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/twd-js" rel="noopener noreferrer"&gt;twd-js&lt;/a&gt; fits the HTMX model well. It runs tests inside the real browser, so it sees exactly what HTMX does to the DOM, and it installs from a CDN with an import map, so it adds no build step of its own.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh8xt7s3g4rx49zi1d8pf.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh8xt7s3g4rx49zi1d8pf.png" alt="TWD sidebar running tests inside an HTMX app" width="800" height="663"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup: load both from a CDN
&lt;/h2&gt;

&lt;p&gt;Load HTMX as usual, then add an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap" rel="noopener noreferrer"&gt;import map&lt;/a&gt; for TWD next to it. &lt;a href="https://esm.sh" rel="noopener noreferrer"&gt;esm.sh&lt;/a&gt; resolves TWD's internal dependency, so there is nothing else to wire up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/htmx.org@2.0.10/dist/htmx.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"importmap"&lt;/span&gt;&lt;span class="nt"&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;imports&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;twd-js&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;https://esm.sh/twd-js@1.8.2&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;twd-js/runner&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;https://esm.sh/twd-js@1.8.2/runner&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;twd-js/bundled&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;https://esm.sh/twd-js@1.8.2/bundled&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your markup stays pure HTMX:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"/api/todos"&lt;/span&gt; &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#todo-list"&lt;/span&gt; &lt;span class="na"&gt;hx-swap=&lt;/span&gt;&lt;span class="s"&gt;"innerHTML"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Load todos&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"todo-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test against the real backend
&lt;/h2&gt;

&lt;p&gt;The important design choice for HTMX is what to assert against. Because HTMX endpoints return HTML fragments rather than JSON, the cleanest approach is to run the tests against your real HTML-returning backend and reset it between tests, rather than mocking. This is the same pattern the &lt;a href="https://github.com/BRIKEV/twd-nuxt-example" rel="noopener noreferrer"&gt;Nuxt example&lt;/a&gt; uses for real-backend testing.&lt;/p&gt;

&lt;p&gt;A tiny dev-only reset endpoint gives every test a clean starting point:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screenDom&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;twd-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;beforeEach&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;twd-js/runner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Todo List (HTMX)&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/reset&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loads the seeded todos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenDom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Load todos&lt;/span&gt;&lt;span class="dl"&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;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loadButton&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// HTMX swaps asynchronously, so use findBy* (which waits) rather than getBy*.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;todo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenDom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Learn TWD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one detail worth internalizing: HTMX swaps happen after an async request, and there is no mock to await, so query with Testing Library's &lt;code&gt;findBy*&lt;/code&gt; (which retries until the element appears) instead of the synchronous &lt;code&gt;getBy*&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on mocking
&lt;/h2&gt;

&lt;p&gt;TWD's service worker will happily intercept HTMX's requests, the same as any &lt;code&gt;fetch&lt;/code&gt; or XHR. Today &lt;code&gt;twd.mockRequest&lt;/code&gt; serializes responses as JSON, which is a natural fit for JSON APIs but not for HTML fragments, so for HTMX we recommend the real-backend approach above. First-class HTML-fragment mocking for hypermedia frameworks is on the roadmap.&lt;/p&gt;

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

&lt;p&gt;A complete example, an HTMX todo list with load, create, and delete, a small Express backend with a reset endpoint, in-browser tests, and CI, lives in &lt;a href="https://github.com/BRIKEV/twd-htmx" rel="noopener noreferrer"&gt;twd-htmx&lt;/a&gt;. It is served as static files with TWD loaded straight from the CDN, so you can read the whole thing top to bottom without a build step in sight.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>htmx</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Test your vanilla JS app in the real browser, with no build step</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Wed, 22 Jul 2026 20:47:10 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/test-your-vanilla-js-app-in-the-real-browser-with-no-build-step-4mmk</link>
      <guid>https://dev.to/kevinccbsg/test-your-vanilla-js-app-in-the-real-browser-with-no-build-step-4mmk</guid>
      <description>&lt;p&gt;Not every app has a bundler. Plenty of real software is still a folder of HTML and JavaScript served as static files: internal tools, landing pages, small products, and anything deliberately kept simple. These apps ship and they matter, but their testing story is thin.&lt;/p&gt;

&lt;p&gt;The usual options do not fit. Jest plus jsdom tests a fake DOM, so tests pass while the real page breaks. Playwright is a lot of stack, and its own runner, for an app whose whole appeal is that it has no stack. What is missing is something in between: real browser behavior, without a build step to earn it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/twd-js" rel="noopener noreferrer"&gt;twd-js&lt;/a&gt; is that middle option. It runs your tests inside the real browser, in a sidebar next to your app, using Testing Library queries you already know. And because its bundled build is self-contained, you can load it from a CDN with an import map. No bundler, no &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhzkfqdjvp7ton8g4fivc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhzkfqdjvp7ton8g4fivc.png" alt="TWD sidebar running tests inside a vanilla JS app" width="799" height="676"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup: one import map
&lt;/h2&gt;

&lt;p&gt;TWD's bundled entry ships everything it needs, and &lt;a href="https://esm.sh" rel="noopener noreferrer"&gt;esm.sh&lt;/a&gt; resolves its internal dependency automatically. So the entire install is an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap" rel="noopener noreferrer"&gt;import map&lt;/a&gt; in your HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"importmap"&lt;/span&gt;&lt;span class="nt"&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;imports&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;twd-js&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;https://esm.sh/twd-js@1.8.2&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;twd-js/runner&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;https://esm.sh/twd-js@1.8.2/runner&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;twd-js/bundled&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;https://esm.sh/twd-js@1.8.2/bundled&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then boot TWD from your entry module. With no bundler there is no &lt;code&gt;import.meta.glob&lt;/code&gt;, so you list your test files by hand: a label pointing at a lazy import of each one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initTWD&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;twd-js/bundled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&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="nf"&gt;initTWD&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="s1"&gt;./tests/helloWorld.twd.js&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tests/helloWorld.twd.js&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="s1"&gt;./tests/todoList.twd.js&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tests/todoList.twd.js&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="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&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;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;serviceWorkerUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/mock-sw.js&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;localhost&lt;/code&gt; guard keeps TWD out of production. Wrap your app in a known root such as &lt;code&gt;&amp;lt;div id="app"&amp;gt;&lt;/code&gt; so TWD's scoped queries know where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tests read like Testing Library
&lt;/h2&gt;

&lt;p&gt;Test files import from the same bare specifiers the import map defines. If your team has ever written a Testing Library test, this is familiar:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screenDom&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;twd-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&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;twd-js/runner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Counter&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;increments on click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenDom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Count is 0&lt;/span&gt;&lt;span class="dl"&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;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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;have.text&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="s1"&gt;Count is 1&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mocking works too
&lt;/h2&gt;

&lt;p&gt;TWD ships a mock service worker. The library loads from the CDN, but the service worker has to be served from your own origin, because browsers only register same-origin service workers. Run &lt;code&gt;npx twd-js init public&lt;/code&gt; (or download &lt;code&gt;mock-sw.js&lt;/code&gt; from the CDN) once, and API mocking works exactly like a bundled setup:&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;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mockRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getTodos&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/todos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Learn TWD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&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;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/todos&lt;/span&gt;&lt;span class="dl"&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;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getTodos&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;h2&gt;
  
  
  The part that pays off later
&lt;/h2&gt;

&lt;p&gt;Here is the argument for starting with TWD even on a throwaway vanilla app: the tests are not tied to vanilla JS. They are Testing Library queries and TWD commands against the rendered DOM. The day you outgrow plain HTML and move the app to React, Vue, or Solid, the test files come with you unchanged. Only the setup changes: the import map becomes a &lt;code&gt;devDependency&lt;/code&gt; and a Vite plugin.&lt;/p&gt;

&lt;p&gt;That makes TWD a low-commitment way to add real tests now, and a migration you never have to redo. A working end-to-end example, counter and todo list, API mocking, and CI, lives in &lt;a href="https://github.com/BRIKEV/twd-vanillajs" rel="noopener noreferrer"&gt;twd-vanillajs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>The easiest way to test a legacy Create React App</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:15:20 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/the-easiest-way-to-test-a-legacy-create-react-app-bgj</link>
      <guid>https://dev.to/kevinccbsg/the-easiest-way-to-test-a-legacy-create-react-app-bgj</guid>
      <description>&lt;p&gt;Create React App was &lt;a href="https://react.dev/blog/2025/02/14/sunsetting-create-react-app" rel="noopener noreferrer"&gt;officially deprecated&lt;/a&gt; in February 2025. The apps built with it didn't disappear. If you maintain one, you know the drill: the app works, it ships, and the Vite migration sits on the backlog behind everything that makes money.&lt;/p&gt;

&lt;p&gt;The testing story is where that hurts. Your options look like Jest plus jsdom, where tests pass while the real page breaks, or bolting Playwright onto an app that's in maintenance mode. One is too far from the browser, the other is a lot of stack for an app nobody wants to invest in.&lt;/p&gt;

&lt;p&gt;There's a third option. &lt;a href="https://www.npmjs.com/package/twd-js" rel="noopener noreferrer"&gt;twd-js&lt;/a&gt; runs your tests inside the real browser, in the dev server you already have open, using Testing Library queries you already know. Every TWD guide assumes Vite, but the library doesn't care about your bundler. It works on CRA's Webpack with one block of code.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmuqiqs1d8z5t91ax8led.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmuqiqs1d8z5t91ax8led.png" alt="TWD sidebar running tests inside a Create React App" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the TWD sidebar next to a CRA app. The tests drive the actual page: real DOM, real router, real fetch calls intercepted by a service worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Install the library and copy the mock service worker into &lt;code&gt;public/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; twd-js
npx twd-js init public
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vite projects get a plugin that handles test discovery. On Webpack you wire it yourself with &lt;code&gt;require.context&lt;/code&gt; in &lt;code&gt;src/index.js&lt;/code&gt;:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;development&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="c1"&gt;// Webpack's version of import.meta.glob&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;twd&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;test&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;ts$/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testModules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;key&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="nx"&gt;testModules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initTWD&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;twd-js/bundled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;initTWD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testModules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;serviceWorkerUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/mock-sw.js&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;NODE_ENV&lt;/code&gt; guard keeps all of it out of the running production app.&lt;/p&gt;

&lt;p&gt;One detail that surprised me: the test files are TypeScript (&lt;code&gt;.twd.test.ts&lt;/code&gt;) inside a plain JavaScript CRA project, and that just works. CRA's Babel pipeline strips types without the &lt;code&gt;typescript&lt;/code&gt; package installed. You don't get type checking on the tests, but they compile and run.&lt;/p&gt;

&lt;h2&gt;
  
  
  You already know the queries
&lt;/h2&gt;

&lt;p&gt;TWD's &lt;code&gt;screenDom&lt;/code&gt; wraps &lt;a href="https://www.npmjs.com/package/@testing-library/dom" rel="noopener noreferrer"&gt;@testing-library/dom&lt;/a&gt; and its &lt;code&gt;userEvent&lt;/code&gt; wraps the Testing Library user-event package. If your team writes Testing Library unit tests today, the tests read the same. The difference is where they run.&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;twd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screenDom&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="s2"&gt;twd-js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&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="s2"&gt;twd-js/runner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello World Page&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should display the welcome title and counter button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenDom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Welcome to TWD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;counterButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenDom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Count is 0&lt;/span&gt;&lt;span class="dl"&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;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counterButton&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counterButton&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;have.text&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;Count is 1&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;npm start&lt;/code&gt;, open the app, and the sidebar lists your suites. Click one and watch it drive the page you're looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mocking the API
&lt;/h2&gt;

&lt;p&gt;The service worker intercepts requests before they leave the browser, so tests are deterministic without a backend running (&lt;code&gt;expect&lt;/code&gt; ships with twd-js, no chai install needed):&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should create a todo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mockRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;getTodoList&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/todos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&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;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mockRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createTodo&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/todos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&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;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/todos&lt;/span&gt;&lt;span class="dl"&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;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&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;screenDom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Title&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;Test Todo&lt;/span&gt;&lt;span class="dl"&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;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&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;screenDom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Description&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;Test Description&lt;/span&gt;&lt;span class="dl"&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;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&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;screenDom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Date&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;2024-12-20&lt;/span&gt;&lt;span class="dl"&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;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&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;screenDom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&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;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;Create Todo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createTodo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Test Todo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Test Description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2024-12-20&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last assertion checks the exact body your form submitted. If someone renames a field in the payload, this fails in the sidebar while you're developing, not in QA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two gotchas (and the fixes)
&lt;/h2&gt;

&lt;p&gt;CRA's tooling assumes every test file in &lt;code&gt;src/&lt;/code&gt; is a Jest test. TWD tests are not, so there are two collisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jest picks up the TWD tests.&lt;/strong&gt; &lt;code&gt;react-scripts test&lt;/code&gt; matches &lt;code&gt;*.twd.test.ts&lt;/code&gt; and tries to run browser tests in jsdom. Exclude them in the &lt;code&gt;test&lt;/code&gt; script:&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="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;"react-scripts test --testPathIgnorePatterns=src/twd-tests"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ESLint flags the assertions.&lt;/strong&gt; The &lt;code&gt;react-app/jest&lt;/code&gt; preset reports TWD's chai-style &lt;code&gt;expect(...).to.deep.equal(...)&lt;/code&gt; as &lt;code&gt;jest/valid-expect&lt;/code&gt; errors, and CRA surfaces them as compile errors in the dev server. Relax the rules for TWD test files only:&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="nl"&gt;"eslintConfig"&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;"extends"&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="s2"&gt;"react-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"react-app/jest"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"overrides"&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;"files"&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="s2"&gt;"src/twd-tests/**/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"**/*.twd.test.*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"settings"&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;"testing-library/utils-module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"testing-library/custom-renders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"testing-library/custom-queries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&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;"rules"&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;"jest/valid-expect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"jest/valid-expect-in-promise"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&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;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;Everything stays strict for your regular Jest tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI in one job
&lt;/h2&gt;

&lt;p&gt;The same tests run headlessly with &lt;a href="https://www.npmjs.com/package/twd-cli" rel="noopener noreferrer"&gt;twd-cli&lt;/a&gt;, a Puppeteer-based runner. After the usual checkout and &lt;code&gt;npm ci&lt;/code&gt;, the whole job is: start the dev server, run the CLI.&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="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;Start CRA dev server&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;nohup npm start &amp;gt; cra.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/span&gt;
    &lt;span class="s"&gt;npx wait-on http://localhost:3000&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;CI&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;BROWSER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&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;Run TWD tests&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BRIKEV/twd-cli/.github/actions/run@main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI can also validate every request mock against an OpenAPI spec, so your mocks can't silently drift from the real API. See &lt;a href="https://brikev.github.io/twd/contract-testing" rel="noopener noreferrer"&gt;contract testing&lt;/a&gt; if that's your kind of thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole thing is a repo
&lt;/h2&gt;

&lt;p&gt;Everything above comes from a working example with green CI: &lt;a href="https://github.com/BRIKEV/twd-create-react-app" rel="noopener noreferrer"&gt;twd-create-react-app&lt;/a&gt;. The &lt;a href="https://brikev.github.io/twd/frameworks#create-react-app-cra" rel="noopener noreferrer"&gt;CRA guide in the TWD docs&lt;/a&gt; covers the same steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you finally migrate to Vite
&lt;/h2&gt;

&lt;p&gt;Here's the payoff that outlasts the maintenance work. These tests are agnostic to your bundler. They drive the real DOM in a real browser, not your webpack config, so nothing in them is tied to CRA.&lt;/p&gt;

&lt;p&gt;When the Vite migration finally gets prioritized, you don't rewrite the suite. You run it against the new build and confirm the app behaves exactly as before. TWD runs on Vite too, so the tests carry over untouched. The setup you added to survive on CRA is the setup that tells you the migration worked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your app might be legacy. Your tests don't have to be.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Production Bug You Can't Reproduce by Clicking</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Fri, 03 Jul 2026 11:40:59 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/the-production-bug-you-cant-reproduce-by-clicking-1k4d</link>
      <guid>https://dev.to/kevinccbsg/the-production-bug-you-cant-reproduce-by-clicking-1k4d</guid>
      <description>&lt;p&gt;A payment request showed up in production logs with &lt;code&gt;country: "ROW"&lt;/code&gt; at the top level and &lt;code&gt;billing_address.country: "FRA"&lt;/code&gt; inside the customer object. Two different countries in the same body. The frontend clearly sent it, but nobody could make it happen again.&lt;/p&gt;

&lt;p&gt;Every team has a bug like this. The payload contradicts itself, the logs prove it happened, and no amount of clicking through the app reproduces it. That's usually the signature of a timing bug: something failed, or arrived late, at exactly the wrong moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Manual Testing Can't Touch It
&lt;/h2&gt;

&lt;p&gt;We tried to reproduce it by hand first. No luck.&lt;/p&gt;

&lt;p&gt;The reason is structural, not lack of effort. To trigger this bug you need one specific API call, the price recalculation that fires when the user changes country, to either fail or respond slowly, at the exact moment the user submits the payment. Manually, you have no lever for that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can't make the real backend fail once, on demand, for one request.&lt;/li&gt;
&lt;li&gt;Browser devtools throttling is global and clumsy: it slows everything, not the one call you care about.&lt;/li&gt;
&lt;li&gt;Even if you get lucky once, you can't do it twice. A repro you can't repeat is not a repro.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the bug stays "unconfirmed", the ticket goes stale, and the payload keeps showing up in the logs every few weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Agent Reads Code, Not Screens
&lt;/h2&gt;

&lt;p&gt;This is where the AI agent changed the approach. Instead of guessing from the outside, it read the code and found that the app keeps the country in two places: one in the billing form the user fills in, and one in app state, set when the page first loads. When the user picks a different country, the app fires a background request to update prices, and only when that request succeeds does the app state catch up with the form.&lt;/p&gt;

&lt;p&gt;That gap between "the form changed" and "the app state caught up" is the whole bug. Two ways to fall into it, found in minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The background request fails.&lt;/strong&gt; The app state keeps the old country, the form shows the new one, and the pay button still works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The user pays too fast.&lt;/strong&gt; The payment is built before the background request comes back, so it still reads the old country.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both are timing conditions. Both are exactly the kind of thing you can't produce by clicking. And both are trivial to express as &lt;a href="https://twd.dev/" rel="noopener noreferrer"&gt;TWD&lt;/a&gt; mocks, because in TWD the test controls the server's behavior per request.&lt;/p&gt;

&lt;p&gt;The failure case is one mock:&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;await&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mockRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recalcFail&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;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/pricing/quote&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recalculation failed&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;selectBillingCountry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;France&lt;/span&gt;&lt;span class="dl"&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;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recalcFail&lt;/span&gt;&lt;span class="dl"&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;clickSubmitPayment&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createPayment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The exact payload from the production logs&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROW&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billing_address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FRA&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;Note the assertion: &lt;code&gt;twd.waitForRequest&lt;/code&gt; returns the intercepted body, so the test checks what the app actually sent over the wire, not what the UI displayed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the Race with a Delayed Mock
&lt;/h2&gt;

&lt;p&gt;The second path needs timing control: the recalculation must succeed, but slowly. TWD mocks accept a &lt;code&gt;delay&lt;/code&gt;, so the service worker holds the response while the test keeps going:&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;await&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mockRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recalcSlow&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;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/pricing/quote&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cartResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&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;selectBillingCountry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;France&lt;/span&gt;&lt;span class="dl"&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;clickSubmitPayment&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// pay before the recalculation lands&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createPayment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ROW&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// stale, the store never updated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;setTimeout&lt;/code&gt; in the test, no flaky sleep, no real backend behaving badly on cue. The race is deterministic because the delay is part of the mock definition. The condition we could never hit manually now reproduces on every single run.&lt;/p&gt;

&lt;p&gt;Both tests passed on the first attempt. Weeks of "cannot reproduce" closed in one session.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Made the Difference
&lt;/h2&gt;

&lt;p&gt;Two things, and neither is magic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The agent reads code.&lt;/strong&gt; A human reproducing a bug works from the UI inward and has to guess where the timing window is. An agent works from the code outward: it finds the store, the sync mechanism, and the gaps, then writes tests that target them directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The mock layer makes timing a test input.&lt;/strong&gt; Failures and delays are declared per request, in the test file, running against your real app in the browser. That turns "race condition" from something you hope to catch into something you specify.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole loop ran through &lt;a href="https://github.com/BRIKEV/twd-relay#readme" rel="noopener noreferrer"&gt;twd-relay&lt;/a&gt;, which drives the browser tab you already have open, so every repro attempt was visible as it executed. After the fix, the same two tests stayed in the suite as regression coverage.&lt;/p&gt;

&lt;p&gt;If you have a ticket that says "cannot reproduce" and a log line that says otherwise, this combination is worth trying: let the agent read the code and put the timing in a mock.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>twd</category>
      <category>ai</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
