<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged twd</title>
<link>https://coderlegion.com/tag/twd</link>
<description>Powered by Question2Answer</description>
<item>
<title>The Testing Trophy, and Where TWD Fits</title>
<link>https://coderlegion.com/32589/the-testing-trophy-and-where-twd-fits</link>
<description>&lt;p&gt;Most frontend teams I talk to have plenty of tests and still find out about broken screens from users. The unit tests are green, the refactor broke every one of them anyway, and nobody is sure the checkout flow works until someone clicks through it.&lt;/p&gt;
&lt;p&gt;That tension is exactly what the Testing Trophy is about.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.kevinmartinez.dev/twd-testing-trophy.png&quot; alt=&quot;The Testing Trophy with TWD on the integration layer&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-trophy&quot;&gt;The trophy&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications&quot;&gt;Kent C. Dodds&lt;/a&gt; drew testing as a trophy instead of a pyramid. Four layers, and the size of each one is roughly how much of your effort it deserves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Static:&lt;/strong&gt; typos and type errors caught as you write the code. TypeScript, ESLint.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unit:&lt;/strong&gt; one function, hook or component checked in isolation. Vitest, Jest.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration:&lt;/strong&gt; several units working together. The biggest part of the trophy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End to End:&lt;/strong&gt; a robot clicking through the deployed app, real backend included. Playwright, Cypress.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The shape comes from one line, written by Guillermo Rauch and popularized by Kent:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Write tests. Not too many. Mostly integration.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Each layer trades speed for confidence. Unit tests are cheap, but they can pass while the screen is broken. End-to-end tests give the most confidence, but they need the whole system running and are the slowest to write and run. Integration tests sit in the middle: they use the app the way a user does, and they stay fast and deterministic because the only thing faked is what you don't own.&lt;/p&gt;
&lt;p&gt;Kent's guiding principle for them:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The more your tests resemble the way your software is used, the more confidence they can give you.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;what-integration-means-on-the-frontend&quot;&gt;What integration means on the frontend&lt;/h3&gt;
&lt;p&gt;For a frontend app, an integration test renders the real thing (routes, providers, state, components) and mocks only the network. Then it does what a user would: visit a page, type, click, and check what is on the screen.&lt;/p&gt;
&lt;p&gt;The classic way to do this is &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/@testing-library/dom&quot;&gt;Testing Library&lt;/a&gt; with jsdom, in Vitest or Jest. It is a good setup and it is where most of us learned this style. It runs in a simulated DOM in your terminal, though, separate from the app you have open while you build.&lt;/p&gt;
&lt;h3 id=&quot;where-twd-fits&quot;&gt;Where TWD fits&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-js&quot;&gt;TWD&lt;/a&gt; is built for that middle layer, and it runs the tests in the browser you develop in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next to your app.&lt;/strong&gt; A sidebar in your dev server runs the tests against the real app. You watch each one drive the page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mock what you don't own.&lt;/strong&gt; API calls are answered by a service worker, and contract testing checks every mock against your OpenAPI spec, so the mocks don't drift from the real API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tests that read like the user.&lt;/strong&gt; Testing Library queries find elements by role, label and text.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The same tests everywhere.&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-cli&quot;&gt;twd-cli&lt;/a&gt; runs them headlessly in CI, with coverage and a video per test if you want one, and your AI agent can write, run and fix them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice this layer can carry a lot. On two production apps we run 872 tests on every pull request, mostly user flows, in one headless Chrome on a 2-vCPU GitHub runner, in about 5 minutes per app.&lt;/p&gt;
&lt;h3 id=&quot;what-it-doesn-t-replace&quot;&gt;What it doesn't replace&lt;/h3&gt;
&lt;p&gt;TWD doesn't cover the whole trophy, and it shouldn't.&lt;/p&gt;
&lt;p&gt;Keep a few end-to-end tests with Playwright or Cypress for the critical paths, the ones that need the real backend, the payment provider or the third-party SDK. Use unit tests for logic that is easier to check on its own. Keep TypeScript and ESLint at the base (and &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/linting&quot;&gt;twd-lint&lt;/a&gt; for your test files). Then let integration tests carry most of the weight.&lt;/p&gt;
&lt;h3 id=&quot;a-map-not-a-ritual&quot;&gt;A map, not a ritual&lt;/h3&gt;
&lt;p&gt;TWD doesn't ask you to pick a layer before you write a test. You automate the check you were already doing by hand while you build. The trophy just shows where those checks end up: mostly in the middle.&lt;/p&gt;
&lt;p&gt;The full page, with the trophy and the tools per layer, is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/testing-trophy&quot;&gt;twd.dev/testing-trophy&lt;/a&gt;. For the strategy itself, go to the source: Kent's &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.testingjavascript.com/&quot;&gt;TestingJavaScript.com&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32589/the-testing-trophy-and-where-twd-fits</guid>
<pubDate>Mon, 26 Oct 2026 19:17:07 +0000</pubDate>
</item>
<item>
<title>Your Agent Says the Tests Pass. Watch Them</title>
<link>https://coderlegion.com/27536/your-agent-says-the-tests-pass-watch-them</link>
<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;h3 id=&quot;the-green-tick-got-cheaper&quot;&gt;The green tick got cheaper&lt;/h3&gt;
&lt;p&gt;For twenty years a passing test was a reasonable proxy for &quot;someone thought about this&quot;. 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;h3 id=&quot;a-test-file-tells-you-what-it-asserts-not-what-it-looks-like&quot;&gt;A test file tells you what it asserts, not what it looks like&lt;/h3&gt;
&lt;p&gt;Here is a test that passes and proves nothing useful:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const el = await screenDom.findByRole(&quot;button&quot;, { name: &quot;Add project&quot; });
twd.should(el, &quot;be.visible&quot;);
&lt;/code&gt;&lt;/pre&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;h3 id=&quot;deterministic-not-a-demo&quot;&gt;Deterministic, not a demo&lt;/h3&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;h3 id=&quot;the-pacing-detail-that-makes-it-watchable&quot;&gt;The pacing detail that makes it watchable&lt;/h3&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 rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-cli&quot;&gt;twd-cli&lt;/a&gt; paces the execution itself rather than slowing the video down afterwards:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx twd-cli run --record --test &quot;checkout flow&quot;
&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx twd-cli run --record --record-pace 500 --test &quot;checkout flow&quot;
&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;twd-artifacts/
  todos-adds-a-todo.mp4
  todos-marks-a-todo-done.mp4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;in-ci-record-only-what-changed&quot;&gt;In CI, record only what changed&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;uses: BRIKEV/twd-cli/.github/actions/record@v1.8.0
with:
  changed-since: ${{ github.event.pull_request.base.sha }}
&lt;/code&gt;&lt;/pre&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 rel=&quot;nofollow&quot; href=&quot;https://twd.dev/recording#recording-in-ci&quot;&gt;twd.dev/recording#recording-in-ci&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/8otlaxgdllhvhu9987k3.png&quot; alt=&quot;Github PR with a record label and the comment of the videos recorded&quot;&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;h3 id=&quot;who-it-is-actually-for&quot;&gt;Who it is actually for&lt;/h3&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 &quot;that is not what I meant&quot;. 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 &quot;what did this used to do&quot; 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;h3 id=&quot;what-it-costs&quot;&gt;What it costs&lt;/h3&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>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27536/your-agent-says-the-tests-pass-watch-them</guid>
<pubDate>Thu, 17 Sep 2026 14:24:37 +0000</pubDate>
</item>
<item>
<title>Run Vue Component Tests Where Vue Runs: The Browser</title>
<link>https://coderlegion.com/25972/run-vue-component-tests-where-vue-runs-the-browser</link>
<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 rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-js&quot;&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 rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/@testing-library/vue&quot;&gt;@testing-library/vue&lt;/a&gt; inside a TWD test.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { afterEach, describe, it } from &quot;twd-js/runner&quot;;
import { twd, userEvent } from &quot;twd-js&quot;;
import { render, screen, cleanup } from &quot;@testing-library/vue&quot;;
import HomeView from &quot;../../views/HomeView.vue&quot;;
import { componentHost, restorePage } from &quot;../support/componentHost&quot;;

describe(&quot;HomeView component&quot;, () =&amp;gt; {
  afterEach(() =&amp;gt; {
    cleanup();
    restorePage();
  });

  it(&quot;increments the counter on click&quot;, async () =&amp;gt; {
    // componentHost() is a blank div on an empty page. More on it below.
    render(HomeView, { container: componentHost() });

    const button = await screen.findByTestId(&quot;counter-button&quot;);
    twd.should(button, &quot;contain.text&quot;, &quot;Count is 0&quot;);

    await userEvent.click(button);
    twd.should(button, &quot;contain.text&quot;, &quot;Count is 1&quot;);
  });
});
&lt;/code&gt;&lt;/pre&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;h3 id=&quot;why-this-works-at-all&quot;&gt;Why this works at all&lt;/h3&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;h3 id=&quot;the-setup&quot;&gt;The setup&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const HOST_ID = &quot;twd-component-host&quot;;
const APP_ROOT_ID = &quot;app&quot;;

let appRoot: HTMLElement | null = null;
let placeholder: Comment | null = null;

export function componentHost(): HTMLElement {
  detachApp();

  let host = document.getElementById(HOST_ID);
  if (!host) {
    host = document.createElement(&quot;div&quot;);
    host.id = HOST_ID;
  }
  if (!host.isConnected) {
    document.body.prepend(host);
  }

  host.innerHTML = &quot;&quot;;
  return host;
}

export function restorePage(): void {
  document.getElementById(HOST_ID)?.remove();
  attachApp();
}

function detachApp(): void {
  if (placeholder) return;

  const root = document.getElementById(APP_ROOT_ID);
  if (!root) return;

  appRoot = root;
  placeholder = document.createComment(&quot; app detached by twd component test &quot;);
  root.replaceWith(placeholder);
}

function attachApp(): void {
  if (!placeholder || !appRoot) return;

  placeholder.replaceWith(appRoot);
  placeholder = null;
  appRoot = null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Detaching the app root is not the same as emptying it. &lt;code&gt;app.innerHTML = &quot;&quot;&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;h3 id=&quot;the-part-that-changes-how-the-test-reads&quot;&gt;The part that changes how the test reads&lt;/h3&gt;
&lt;p&gt;Here is the same idea on a view that fetches on mount and posts a form.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;it(&quot;posts the form values when a todo is created&quot;, async () =&amp;gt; {
  await twd.mockRequest(&quot;getTodoList&quot;, {
    method: &quot;GET&quot;,
    url: &quot;/api/todos&quot;,
    response: [],
    status: 200,
  });
  await twd.mockRequest(&quot;createTodo&quot;, {
    method: &quot;POST&quot;,
    url: &quot;/api/todos&quot;,
    status: 201,
    response: { id: &quot;1&quot;, title: &quot;Write the Vue post&quot;, description: &quot;In a real browser&quot;, date: &quot;2026-09-01&quot; },
  });

  render(TodosView, { container: componentHost() });
  await twd.waitForRequest(&quot;getTodoList&quot;);

  await userEvent.type(await screen.findByLabelText(&quot;Title&quot;), &quot;Write the Vue post&quot;);
  await userEvent.type(screen.getByLabelText(&quot;Description&quot;), &quot;In a real browser&quot;);
  await userEvent.type(screen.getByLabelText(&quot;Date&quot;), &quot;2026-09-01&quot;);

  await userEvent.click(screen.getByRole(&quot;button&quot;, { name: &quot;Create Todo&quot; }));

  const rule = await twd.waitForRequest(&quot;createTodo&quot;);
  expect(rule.request).to.deep.equal({
    title: &quot;Write the Vue post&quot;,
    description: &quot;In a real browser&quot;,
    date: &quot;2026-09-01&quot;,
  });
});
&lt;/code&gt;&lt;/pre&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 rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/@testing-library/user-event&quot;&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;h3 id=&quot;both-kinds-of-test-one-run&quot;&gt;Both kinds of test, one run&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;test: {
  exclude: [...configDefaults.exclude, &quot;**/*.twd.test.*&quot;],
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;where-each-style-fits&quot;&gt;Where each style fits&lt;/h3&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;h3 id=&quot;try-it&quot;&gt;Try it&lt;/h3&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 rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-vue-example&quot;&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 rel=&quot;nofollow&quot; href=&quot;https://dev.to/kevinccbsg/no-more-fake-dom-testing-library-unit-tests-in-the-real-browser-3p77&quot;&gt;No More Fake DOM&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25972/run-vue-component-tests-where-vue-runs-the-browser</guid>
<pubDate>Tue, 01 Sep 2026 07:35:19 +0000</pubDate>
</item>
<item>
<title>No More Fake DOM: Testing Library Unit Tests in the Real Browser</title>
<link>https://coderlegion.com/25909/no-more-fake-dom-testing-library-unit-tests-in-the-real-browser</link>
<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 rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-js&quot;&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 rel=&quot;nofollow&quot; href=&quot;https://dev.to/kevinccbsg/in-browser-testing-vitest-browser-mode-and-twd-side-by-side-1i3j&quot;&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { render, screen, cleanup } from &quot;@testing-library/react&quot;;
import { describe, it, afterEach } from &quot;twd-js/runner&quot;;
import { AppProvider } from &quot;@/context/AppContext&quot;;
import { twd } from &quot;twd-js&quot;;
import { Add } from &quot;../Add&quot;;
import { componentHost, restorePage } from &quot;@/twd-tests/support/componentHost&quot;;

describe(&quot;Add Component&quot;, () =&amp;gt; {
  afterEach(() =&amp;gt; {
    cleanup();
    restorePage();
  });

  it(&quot;renders the Add component&quot;, () =&amp;gt; {
    // componentHost() is a blank div on an empty page. More on it below.
    render(&amp;lt;AppProvider&amp;gt;&amp;lt;Add /&amp;gt;&amp;lt;/AppProvider&amp;gt;, { container: componentHost() });

    twd.should(screen.getByText(&quot;Add Item&quot;), &quot;be.visible&quot;);
  });
});
&lt;/code&gt;&lt;/pre&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;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/gj02099nimid4kw24kaz.png&quot; alt=&quot;component test in an actual browser&quot;&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;h3 id=&quot;why-this-works-at-all-1&quot;&gt;Why this works at all&lt;/h3&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;h3 id=&quot;the-setup-1&quot;&gt;The setup&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const HOST_ID = &quot;twd-component-host&quot;;
const APP_ROOT_ID = &quot;root&quot;;

let appRoot: HTMLElement | null = null;
let placeholder: Comment | null = null;

export function componentHost(): HTMLElement {
  detachApp();

  let host = document.getElementById(HOST_ID);
  if (!host) {
    host = document.createElement(&quot;div&quot;);
    host.id = HOST_ID;
  }
  if (!host.isConnected) {
    document.body.prepend(host);
  }

  host.innerHTML = &quot;&quot;;
  return host;
}

export function restorePage(): void {
  document.getElementById(HOST_ID)?.remove();
  attachApp();
}

function detachApp(): void {
  if (placeholder) return;

  const root = document.getElementById(APP_ROOT_ID);
  if (!root) return;

  appRoot = root;
  placeholder = document.createComment(&quot; app detached by twd component test &quot;);
  root.replaceWith(placeholder);
}

function attachApp(): void {
  if (!placeholder || !appRoot) return;

  placeholder.replaceWith(appRoot);
  placeholder = null;
  appRoot = null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Detaching the app root is not the same as emptying it. &lt;code&gt;root.innerHTML = &quot;&quot;&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;h3 id=&quot;two-traps&quot;&gt;Two traps&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&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;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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;twd({
  testFilePattern: &quot;/**/*.twd.test.{ts,tsx}&quot;,
}),
&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;test: {
  exclude: [...configDefaults.exclude, &quot;**/*.twd.test.*&quot;],
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;both-kinds-of-test-one-run-1&quot;&gt;Both kinds of test, one run&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-console&quot;&gt;$ npx twd-cli run
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;/code&gt;&lt;/pre&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;h3 id=&quot;where-each-style-fits-1&quot;&gt;Where each style fits&lt;/h3&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;h3 id=&quot;try-it-1&quot;&gt;Try it&lt;/h3&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 rel=&quot;nofollow&quot; href=&quot;https://github.com/kevinccbsg/frontend-challenge&quot;&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>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25909/no-more-fake-dom-testing-library-unit-tests-in-the-real-browser</guid>
<pubDate>Mon, 31 Aug 2026 07:54:58 +0000</pubDate>
</item>
<item>
<title>AI Made Code Review the Bottleneck. Attach the UI to Your PR</title>
<link>https://coderlegion.com/24024/ai-made-code-review-the-bottleneck-attach-the-ui-to-your-pr</link>
<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 &quot;adds the checkout flow&quot;. 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;h3 id=&quot;tests-solve-part-of-the-problem&quot;&gt;Tests solve part of the problem&lt;/h3&gt;
&lt;p&gt;Tests already reduce a large part of that uncertainty.&lt;/p&gt;
&lt;p&gt;With &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/&quot;&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;h3 id=&quot;put-the-flow-in-the-pull-request&quot;&gt;Put the flow in the pull request&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx twd-cli run --record --test &quot;checkout flow&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That produces:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;twd-artifacts/checkout-flow.mp4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Drop the video directly into the pull request description.&lt;/p&gt;
&lt;h3 id=&quot;the-first-problem-you-ll-hit&quot;&gt;The first problem you'll hit&lt;/h3&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;h3 id=&quot;slowing-the-video-down-is-the-wrong-fix&quot;&gt;Slowing the video down is the wrong fix&lt;/h3&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;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;right&quot;&gt;Speed &lt;/th&gt;&lt;th align=&quot;right&quot;&gt;Duration &lt;/th&gt;&lt;th align=&quot;right&quot;&gt;Effective FPS&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;right&quot;&gt;1x &lt;/td&gt;&lt;td align=&quot;right&quot;&gt;   1.00s &lt;/td&gt;&lt;td align=&quot;right&quot;&gt;           30&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;right&quot;&gt;0.5x &lt;/td&gt;&lt;td align=&quot;right&quot;&gt;   1.90s &lt;/td&gt;&lt;td align=&quot;right&quot;&gt;         15.3&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;right&quot;&gt;0.25x &lt;/td&gt;&lt;td align=&quot;right&quot;&gt;   3.90s &lt;/td&gt;&lt;td align=&quot;right&quot;&gt;          7.7&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;h3 id=&quot;this-is-where-architecture-matters&quot;&gt;This is where architecture matters&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Default: 300ms pause after each command
npx twd-cli run --record --test &quot;checkout flow&quot;

# Better for demos or stakeholder reviews
npx twd-cli run --record --record-pace 500 --test &quot;checkout flow&quot;
&lt;/code&gt;&lt;/pre&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;h3 id=&quot;what-the-reviewer-gets&quot;&gt;What the reviewer gets&lt;/h3&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;&quot;Does this diff look correct?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;to:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Is this the behavior we actually wanted?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is the question worth a human's attention.&lt;/p&gt;
&lt;h3 id=&quot;the-bottleneck-moved&quot;&gt;The bottleneck moved&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx twd-cli run --record --test &quot;the flow you changed&quot;
&lt;/code&gt;&lt;/pre&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 rel=&quot;nofollow&quot; href=&quot;https://twd.dev/recording&quot;&gt;Recording Runs documentation&lt;/a&gt; for all available options.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24024/ai-made-code-review-the-bottleneck-attach-the-ui-to-your-pr</guid>
<pubDate>Mon, 03 Aug 2026 07:01:36 +0000</pubDate>
</item>
<item>
<title>Testing microfrontends with TWD: one runner, three frameworks</title>
<link>https://coderlegion.com/23704/testing-microfrontends-with-twd-one-runner-three-frameworks</link>
<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;h3 id=&quot;what-usually-happens-to-testing&quot;&gt;What usually happens to testing&lt;/h3&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;h3 id=&quot;testing-is-harder-to-skip-now&quot;&gt;Testing is harder to skip now&lt;/h3&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;h3 id=&quot;the-same-runner-whatever-the-framework&quot;&gt;The same runner, whatever the framework&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev&quot;&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 rel=&quot;nofollow&quot; href=&quot;https://vite.dev&quot;&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;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/wuihsl81zuz3dc15qjfq.png&quot; alt=&quot;Three microfrontends running side by side, each with its own TWD sidebar and its own passing suite: React 19, React 17 and Vue 3&quot;&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, &quot;ship a green TWD suite&quot; is an acceptance criterion they can satisfy in their own pipeline.&lt;/p&gt;
&lt;h3 id=&quot;tests-that-outlive-the-framework&quot;&gt;Tests that outlive the framework&lt;/h3&gt;
&lt;p&gt;The suites read like &lt;a rel=&quot;nofollow&quot; href=&quot;https://testing-library.com&quot;&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;h3 id=&quot;living-documentation-in-the-tab-you-already-have-open&quot;&gt;Living documentation, in the tab you already have open&lt;/h3&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;h3 id=&quot;the-same-suites-in-ci&quot;&gt;The same suites in CI&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-cli&quot;&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx twd-cli run
&lt;/code&gt;&lt;/pre&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;h3 id=&quot;one-thing-to-know&quot;&gt;One thing to know&lt;/h3&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;h3 id=&quot;try-it-2&quot;&gt;Try it&lt;/h3&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 rel=&quot;nofollow&quot; href=&quot;https://twd.dev&quot;&gt;twd.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Example repo (three microfrontends, three frameworks, a TWD suite each): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-module-federation&quot;&gt;github.com/BRIKEV/twd-module-federation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Getting started: &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/getting-started&quot;&gt;twd.dev/getting-started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Frameworks and bundlers: &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/frameworks&quot;&gt;twd.dev/frameworks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Writing tests: &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/writing-tests&quot;&gt;twd.dev/writing-tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;API mocking: &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/api-mocking&quot;&gt;twd.dev/api-mocking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CI execution: &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/ci-execution&quot;&gt;twd.dev/ci-execution&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Videos</category>
<guid isPermaLink="true">https://coderlegion.com/23704/testing-microfrontends-with-twd-one-runner-three-frameworks</guid>
<pubDate>Tue, 28 Jul 2026 22:29:39 +0000</pubDate>
</item>
<item>
<title>Real-browser testing for Open Cells apps with TWD</title>
<link>https://coderlegion.com/23610/real-browser-testing-for-open-cells-apps-with-twd</link>
<description>&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.opencells.dev&quot;&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 rel=&quot;nofollow&quot; href=&quot;https://lit.dev&quot;&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;h3 id=&quot;the-usual-vite-stack-and-the-gap&quot;&gt;The usual Vite stack, and the gap&lt;/h3&gt;
&lt;p&gt;A Vite app usually reaches for two tools: &lt;a rel=&quot;nofollow&quot; href=&quot;https://vitest.dev&quot;&gt;Vitest&lt;/a&gt; for component tests and &lt;a rel=&quot;nofollow&quot; href=&quot;https://playwright.dev&quot;&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 rel=&quot;nofollow&quot; href=&quot;https://twd.dev&quot;&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;h3 id=&quot;not-just-cells&quot;&gt;Not just Cells&lt;/h3&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;h3 id=&quot;setup-is-small&quot;&gt;Setup is small&lt;/h3&gt;
&lt;p&gt;Cells apps run on &lt;a rel=&quot;nofollow&quot; href=&quot;https://vite.dev&quot;&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 rel=&quot;nofollow&quot; href=&quot;https://testing-library.com&quot;&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;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/gqn3n1ctx7nqmvthxvzc.png&quot; alt=&quot;The TWD sidebar running the Cells app's page tests in the browser&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;coverage-without-changing-the-build&quot;&gt;Coverage, without changing the build&lt;/h3&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;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/wab24dxo7bff1t6uvh4h.png&quot; alt=&quot;nyc coverage report generated from the in-browser tests&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;try-it-3&quot;&gt;Try it&lt;/h3&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 rel=&quot;nofollow&quot; href=&quot;https://twd.dev&quot;&gt;twd.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Example repo (a Cells app tested with TWD): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-cells&quot;&gt;github.com/BRIKEV/twd-cells&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Writing tests: &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/writing-tests&quot;&gt;twd.dev/writing-tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;API mocking: &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/api-mocking&quot;&gt;twd.dev/api-mocking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CI execution: &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/ci-execution&quot;&gt;twd.dev/ci-execution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Coverage: &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/coverage&quot;&gt;twd.dev/coverage&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23610/real-browser-testing-for-open-cells-apps-with-twd</guid>
<pubDate>Mon, 27 Jul 2026 20:12:59 +0000</pubDate>
</item>
<item>
<title>Testing HTMX apps in the real browser with TWD</title>
<link>https://coderlegion.com/23392/testing-htmx-apps-in-the-real-browser-with-twd</link>
<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 rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-js&quot;&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;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/bk54c3v9flr200opkz9h.png&quot; alt=&quot;TWD sidebar running tests inside an HTMX app&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;setup-load-both-from-a-cdn&quot;&gt;Setup: load both from a CDN&lt;/h3&gt;
&lt;p&gt;Load HTMX as usual, then add an &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap&quot;&gt;import map&lt;/a&gt; for TWD next to it. &lt;a rel=&quot;nofollow&quot; href=&quot;https://esm.sh&quot;&gt;esm.sh&lt;/a&gt; resolves TWD's internal dependency, so there is nothing else to wire up.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script src=&quot;https://unpkg.com/htmx.org@2.0.10/dist/htmx.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script type=&quot;importmap&quot;&amp;gt;
  {
    &quot;imports&quot;: {
      &quot;twd-js&quot;: &quot;https://esm.sh/twd-js@1.8.2&quot;,
      &quot;twd-js/runner&quot;: &quot;https://esm.sh/twd-js@1.8.2/runner&quot;,
      &quot;twd-js/bundled&quot;: &quot;https://esm.sh/twd-js@1.8.2/bundled&quot;
    }
  }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your markup stays pure HTMX:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;button hx-get=&quot;/api/todos&quot; hx-target=&quot;#todo-list&quot; hx-swap=&quot;innerHTML&quot;&amp;gt;Load todos&amp;lt;/button&amp;gt;
&amp;lt;ul id=&quot;todo-list&quot;&amp;gt;&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;test-against-the-real-backend&quot;&gt;Test against the real backend&lt;/h3&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 rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-nuxt-example&quot;&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { twd, userEvent, screenDom } from 'twd-js';
import { describe, it, beforeEach } from 'twd-js/runner';

describe('Todo List (HTMX)', () =&amp;gt; {
  beforeEach(async () =&amp;gt; {
    await fetch('/api/reset', { method: 'POST' });
  });

  it('loads the seeded todos', async () =&amp;gt; {
    await twd.visit('/');

    const loadButton = await screenDom.getByRole('button', { name: 'Load todos' });
    await userEvent.click(loadButton);

    // HTMX swaps asynchronously, so use findBy* (which waits) rather than getBy*.
    const todo = await screenDom.findByText('Learn TWD');
    twd.should(todo, 'be.visible');
  });
});
&lt;/code&gt;&lt;/pre&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;h3 id=&quot;a-note-on-mocking&quot;&gt;A note on mocking&lt;/h3&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;h3 id=&quot;try-it-4&quot;&gt;Try it&lt;/h3&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 rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-htmx&quot;&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>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23392/testing-htmx-apps-in-the-real-browser-with-twd</guid>
<pubDate>Fri, 24 Jul 2026 09:04:43 +0000</pubDate>
</item>
<item>
<title>Test your vanilla JS app in the real browser, with no build step</title>
<link>https://coderlegion.com/23391/test-your-vanilla-js-app-in-the-real-browser-with-no-build-step</link>
<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 rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-js&quot;&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;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/33ydmb3diq39yosnck3b.png&quot; alt=&quot;TWD sidebar running tests inside a vanilla JS app&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;setup-one-import-map&quot;&gt;Setup: one import map&lt;/h3&gt;
&lt;p&gt;TWD's bundled entry ships everything it needs, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://esm.sh&quot;&gt;esm.sh&lt;/a&gt; resolves its internal dependency automatically. So the entire install is an &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap&quot;&gt;import map&lt;/a&gt; in your HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script type=&quot;importmap&quot;&amp;gt;
  {
    &quot;imports&quot;: {
      &quot;twd-js&quot;: &quot;https://esm.sh/twd-js@1.8.2&quot;,
      &quot;twd-js/runner&quot;: &quot;https://esm.sh/twd-js@1.8.2/runner&quot;,
      &quot;twd-js/bundled&quot;: &quot;https://esm.sh/twd-js@1.8.2/bundled&quot;
    }
  }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script type=&quot;module&quot;&amp;gt;
  import { initTWD } from 'twd-js/bundled';

  if (location.hostname === 'localhost') {
    initTWD(
      {
        './tests/helloWorld.twd.js': () =&amp;gt; import('/tests/helloWorld.twd.js'),
        './tests/todoList.twd.js': () =&amp;gt; import('/tests/todoList.twd.js'),
      },
      { open: true, position: 'left', serviceWorker: true, serviceWorkerUrl: '/mock-sw.js' },
    );
  }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&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=&quot;app&quot;&amp;gt;&lt;/code&gt; so TWD's scoped queries know where to look.&lt;/p&gt;
&lt;h3 id=&quot;the-tests-read-like-testing-library&quot;&gt;The tests read like Testing Library&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { twd, userEvent, screenDom } from 'twd-js';
import { describe, it } from 'twd-js/runner';

describe('Counter', () =&amp;gt; {
  it('increments on click', async () =&amp;gt; {
    await twd.visit('/');
    const button = await screenDom.getByText('Count is 0');
    await userEvent.click(button);
    twd.should(button, 'have.text', 'Count is 1');
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;mocking-works-too&quot;&gt;Mocking works too&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;await twd.mockRequest('getTodos', {
  method: 'GET',
  url: '/api/todos',
  response: [{ id: '1', title: 'Learn TWD' }],
  status: 200,
});
await twd.visit('/todos');
await twd.waitForRequest('getTodos');
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-part-that-pays-off-later&quot;&gt;The part that pays off later&lt;/h3&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 rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-vanillajs&quot;&gt;twd-vanillajs&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23391/test-your-vanilla-js-app-in-the-real-browser-with-no-build-step</guid>
<pubDate>Fri, 24 Jul 2026 09:03:09 +0000</pubDate>
</item>
<item>
<title>The easiest way to test a legacy Create React App</title>
<link>https://coderlegion.com/22790/the-easiest-way-to-test-a-legacy-create-react-app</link>
<description>&lt;p&gt;Create React App was &lt;a rel=&quot;nofollow&quot; href=&quot;https://react.dev/blog/2025/02/14/sunsetting-create-react-app&quot;&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 rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-js&quot;&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;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/xefjfwydzhybtyu1h51f.png&quot; alt=&quot;TWD sidebar running tests inside a Create React App&quot;&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;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;
&lt;p&gt;Install the library and copy the mock service worker into &lt;code&gt;public/&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install --save-dev twd-js
npx twd-js init public
&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if (process.env.NODE_ENV === &quot;development&quot;) {
  // Webpack's version of import.meta.glob
  const context = require.context(&quot;./&quot;, true, /\.twd\.test\.ts$/);

  const testModules = {};
  context.keys().forEach((key) =&amp;gt; {
    testModules[key] = async () =&amp;gt; Promise.resolve(context(key));
  });

  const { initTWD } = await import('twd-js/bundled');

  initTWD(testModules, {
    open: true,
    search: true,
    serviceWorker: true,
    serviceWorkerUrl: '/mock-sw.js',
  });
}
&lt;/code&gt;&lt;/pre&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;h3 id=&quot;you-already-know-the-queries&quot;&gt;You already know the queries&lt;/h3&gt;
&lt;p&gt;TWD's &lt;code&gt;screenDom&lt;/code&gt; wraps &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/@testing-library/dom&quot;&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { twd, userEvent, screenDom } from &quot;twd-js&quot;;
import { describe, it } from &quot;twd-js/runner&quot;;

describe(&quot;Hello World Page&quot;, () =&amp;gt; {
  it(&quot;should display the welcome title and counter button&quot;, async () =&amp;gt; {
    await twd.visit(&quot;/&quot;);

    const title = await screenDom.getByText(&quot;Welcome to TWD&quot;);
    twd.should(title, &quot;be.visible&quot;);

    const counterButton = await screenDom.getByText(&quot;Count is 0&quot;);
    await userEvent.click(counterButton);
    twd.should(counterButton, &quot;have.text&quot;, &quot;Count is 1&quot;);
  });
});
&lt;/code&gt;&lt;/pre&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;h3 id=&quot;mocking-the-api&quot;&gt;Mocking the API&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;it(&quot;should create a todo&quot;, async () =&amp;gt; {
  await twd.mockRequest(&quot;getTodoList&quot;, {
    method: &quot;GET&quot;,
    url: &quot;/api/todos&quot;,
    response: [],
    status: 200,
  });
  await twd.mockRequest(&quot;createTodo&quot;, {
    method: &quot;POST&quot;,
    url: &quot;/api/todos&quot;,
    response: { id: &quot;1&quot; },
    status: 200,
  });
  await twd.visit(&quot;/todos&quot;);

  await userEvent.type(await screenDom.getByLabelText(&quot;Title&quot;), &quot;Test Todo&quot;);
  await userEvent.type(await screenDom.getByLabelText(&quot;Description&quot;), &quot;Test Description&quot;);
  await userEvent.type(await screenDom.getByLabelText(&quot;Date&quot;), &quot;2024-12-20&quot;);
  await userEvent.click(await screenDom.getByRole(&quot;button&quot;, { name: &quot;Create Todo&quot; }));

  const rule = await twd.waitForRequest(&quot;createTodo&quot;);
  expect(rule.request).to.deep.equal({
    title: &quot;Test Todo&quot;,
    description: &quot;Test Description&quot;,
    date: &quot;2024-12-20&quot;,
  });
});
&lt;/code&gt;&lt;/pre&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;h3 id=&quot;two-gotchas-and-the-fixes-&quot;&gt;Two gotchas (and the fixes)&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;test&quot;: &quot;react-scripts test --testPathIgnorePatterns=src/twd-tests&quot;
&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;eslintConfig&quot;: {
  &quot;extends&quot;: [&quot;react-app&quot;, &quot;react-app/jest&quot;],
  &quot;overrides&quot;: [
    {
      &quot;files&quot;: [&quot;src/twd-tests/**/*&quot;, &quot;**/*.twd.test.*&quot;],
      &quot;settings&quot;: {
        &quot;testing-library/utils-module&quot;: &quot;off&quot;,
        &quot;testing-library/custom-renders&quot;: &quot;off&quot;,
        &quot;testing-library/custom-queries&quot;: &quot;off&quot;
      },
      &quot;rules&quot;: {
        &quot;jest/valid-expect&quot;: &quot;off&quot;,
        &quot;jest/valid-expect-in-promise&quot;: &quot;off&quot;
      }
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything stays strict for your regular Jest tests.&lt;/p&gt;
&lt;h3 id=&quot;ci-in-one-job&quot;&gt;CI in one job&lt;/h3&gt;
&lt;p&gt;The same tests run headlessly with &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-cli&quot;&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Start CRA dev server
  run: |
    nohup npm start &amp;gt; cra.log 2&amp;gt;&amp;amp;1 &amp;amp;
    npx wait-on http://localhost:3000
  env:
    CI: true
    BROWSER: none

- name: Run TWD tests
  uses: BRIKEV/twd-cli/.github/actions/run@main
&lt;/code&gt;&lt;/pre&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 rel=&quot;nofollow&quot; href=&quot;https://brikev.github.io/twd/contract-testing&quot;&gt;contract testing&lt;/a&gt; if that's your kind of thing.&lt;/p&gt;
&lt;h3 id=&quot;the-whole-thing-is-a-repo&quot;&gt;The whole thing is a repo&lt;/h3&gt;
&lt;p&gt;Everything above comes from a working example with green CI: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-create-react-app&quot;&gt;twd-create-react-app&lt;/a&gt;. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://brikev.github.io/twd/frameworks#create-react-app-cra&quot;&gt;CRA guide in the TWD docs&lt;/a&gt; covers the same steps.&lt;/p&gt;
&lt;h3 id=&quot;when-you-finally-migrate-to-vite&quot;&gt;When you finally migrate to Vite&lt;/h3&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>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22790/the-easiest-way-to-test-a-legacy-create-react-app</guid>
<pubDate>Thu, 16 Jul 2026 07:19:49 +0000</pubDate>
</item>
<item>
<title>Automated Accessibility Testing: axe-core, Keyboard Navigation, and WCAG in the Browser</title>
<link>https://coderlegion.com/21693/automated-accessibility-testing-axe-core-keyboard-navigation-and-wcag-in-the-browser</link>
<description>&lt;p&gt;Automated accessibility testing usually means one thing: point a scanner at the page, let it check the initial HTML against a set of WCAG rules, and turn green. Missing alt text, an unlabeled input, low-contrast text at load. Caught. Done.&lt;/p&gt;
&lt;p&gt;Then a user opens the menu, submits the empty form, expands the accordion, and lands in a state your scanner never saw. The newly revealed panel steals focus and never gives it back. The modal traps a keyboard user with no way out. None of that exists in the first render, so none of it was tested.&lt;/p&gt;
&lt;p&gt;That is the gap this post is about. Accessibility is not a property of a page, it is a property of every state a page can reach, and most of those states only exist after someone interacts. The fix is to run your accessibility checks in a real browser, after the interaction, with two complementary tools: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/axe-core&quot;&gt;axe-core&lt;/a&gt; for WCAG rule checks, and keyboard navigation tests for focus order. This walks through both.&lt;/p&gt;
&lt;h3 id=&quot;the-gap-accessibility-tests-that-run-in-jsdom&quot;&gt;The gap: accessibility tests that run in jsdom&lt;/h3&gt;
&lt;p&gt;The default setup runs your tests in jsdom. Render a component with Testing Library, run axe-core over it with &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/jest-axe&quot;&gt;jest-axe&lt;/a&gt; or &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/vitest-axe&quot;&gt;vitest-axe&lt;/a&gt;, assert no violations. It is fast, and it is fine for catching the obvious stuff: a button with no accessible name, an input with no label, a misused ARIA attribute.&lt;/p&gt;
&lt;p&gt;It also has a hard ceiling. jsdom does no layout and no rendering, so any check that depends on what actually paints on screen cannot run there. axe-core's &lt;code&gt;color-contrast&lt;/code&gt; rule is the clearest example: in jsdom it comes back as &quot;incomplete,&quot; not pass or fail, because there is nothing to measure. And that is before you get to anything interactive.&lt;/p&gt;
&lt;p&gt;What it cannot tell you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Whether focus moves somewhere sensible after a dialog opens, and comes back when it closes.&lt;/li&gt;
&lt;li&gt;Whether the validation summary that renders after a failed submit is actually announced and reachable.&lt;/li&gt;
&lt;li&gt;Whether your tab order matches the visual order once a section expands.&lt;/li&gt;
&lt;li&gt;Whether a custom component announces its name, role, and state to a screen reader after it changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are not edge cases. They are where most real accessibility complaints come from. And they all depend on layout, focus, and event behavior that jsdom only approximates. To test them you need the real thing: a real browser, real focus, real events.&lt;/p&gt;
&lt;p&gt;That is the part TWD is built for. Tests run inside the actual browser, against your real app, and you drive them the way a user would. Which means you can scan accessibility &lt;em&gt;after&lt;/em&gt; the interaction, not just before it.&lt;/p&gt;
&lt;h3 id=&quot;approach-1-axe-core-scanned-after-the-interaction&quot;&gt;Approach 1: axe-core, scanned after the interaction&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/axe-core&quot;&gt;axe-core&lt;/a&gt; is the engine behind most accessibility tooling. It is excellent at the rule-based checks: contrast, names, roles, ARIA misuse. The trick is not running axe. The trick is running it against the state that matters.&lt;/p&gt;
&lt;p&gt;A small helper turns axe results into a readable failure instead of a wall of JSON:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import axe from &quot;axe-core&quot;;

type AxeResults = import(&quot;axe-core&quot;).AxeResults;
type RunOptions = import(&quot;axe-core&quot;).RunOptions;
type ElementContext = import(&quot;axe-core&quot;).ElementContext;

/**
 * Run axe-core against the page (or a scoped element) and throw a
 * formatted error if there are any accessibility violations.
 */
export async function checkA11y(
  context?: ElementContext,
  options?: RunOptions
): Promise&amp;lt;AxeResults&amp;gt; {
  const results = await axe.run(context ?? document, options);

  if (results.violations.length) {
    const report = results.violations
      .map((v, i) =&amp;gt; `${i + 1}. [${v.impact ?? &quot;n/a&quot;}] ${v.id}: ${v.help}\n   ${v.helpUrl}`)
      .join(&quot;\n\n&quot;);
    throw new Error(
      `Found ${results.violations.length} accessibility violation(s):\n\n${report}`
    );
  }

  return results;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A baseline scan of the page that loads is the easy case:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { twd, userEvent, screenDom } from &quot;twd-js&quot;;
import { describe, it, beforeEach } from &quot;twd-js/runner&quot;;
import { checkA11y } from &quot;./helpers/axe&quot;;

// Only WCAG 2.0/2.1 A &amp;amp; AA rules. Skips axe &quot;best-practice&quot; rules
// (region, landmark-one-main, ...) which are recommendations, not failures.
const WCAG = {
  runOnly: {
    type: &quot;tag&quot; as const,
    values: [&quot;wcag2a&quot;, &quot;wcag2aa&quot;, &quot;wcag21a&quot;, &quot;wcag21aa&quot;],
  },
};

describe(&quot;Accessibility&quot;, () =&amp;gt; {
  beforeEach(() =&amp;gt; {
    twd.clearRequestMockRules();
  });

  it(&quot;should have no WCAG 2 A/AA violations on the home page&quot;, async () =&amp;gt; {
    await twd.visit(&quot;/&quot;);
    await checkA11y(document, WCAG);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the part every tool can do. Here is the part that needs a real browser and a real interaction: scanning a state that only exists &lt;em&gt;after&lt;/em&gt; the user does something.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;it(&quot;should stay accessible after form validation errors render&quot;, async () =&amp;gt; {
  await twd.mockRequest(&quot;getTodoList&quot;, {
    method: &quot;GET&quot;,
    url: &quot;/api/todos&quot;,
    response: [],
    status: 200,
  });
  await twd.visit(&quot;/todos&quot;);
  await twd.waitForRequest(&quot;getTodoList&quot;);

  // Submit the empty form to force the validation error messages to render.
  const submitButton = await screenDom.findByRole(&quot;button&quot;, { name: &quot;Create Todo&quot; });
  await userEvent.click(submitButton);

  // Confirm the new dynamic content actually appeared before scanning.
  const error = await screenDom.findByText(&quot;Title is required&quot;);
  twd.should(error, &quot;be.visible&quot;);

  // Scope the scan to just the form so the report is about this component.
  const form = await screenDom.findByTestId(&quot;todo-form&quot;);
  await checkA11y(form, WCAG);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The error messages in that test do not exist in the initial HTML. They render after a failed submit. A static scan never reaches them, so it never checks whether they are associated with their fields, announced, or reachable. Driving the interaction first, then scoping the scan to the form, checks the state a user actually lands in.&lt;/p&gt;
&lt;p&gt;Scoping matters too. Passing the form element instead of &lt;code&gt;document&lt;/code&gt; keeps the report about the component you just exercised, rather than re-flagging everything else on the page.&lt;/p&gt;
&lt;h3 id=&quot;approach-2-keyboard-navigation-and-focus-order&quot;&gt;Approach 2: keyboard navigation and focus order&lt;/h3&gt;
&lt;p&gt;axe will not tell you whether your tab order makes sense. It cannot. Whether &lt;kbd&gt;Tab&lt;/kbd&gt; moves through controls in an order that matches the visual flow, whether focus is visible, whether a control is even reachable by keyboard: that is behavior, and you have to drive it.&lt;/p&gt;
&lt;p&gt;Because TWD tests run in the real browser, &lt;code&gt;userEvent&lt;/code&gt; drives real focus. So you can walk the page the way a keyboard user does and assert where focus lands at each step.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;it(&quot;keyboard navigation should work&quot;, async () =&amp;gt; {
  await twd.visit(&quot;/chat&quot;);

  const user = userEvent.setup();

  // Tab from the top of the page. First stop should be the back link.
  await user.tab();
  const backToLanding = screenDom.getByRole(&quot;link&quot;, { name: &quot;← Back to Landing&quot; });
  twd.should(backToLanding, &quot;be.focused&quot;);

  // Tab again. Focus should land on the prompt input, and typing should work.
  await user.tab();
  await user.keyboard(&quot;write some text&quot;);
  const textarea = screenDom.getByLabelText(&quot;Main prompt input (required)&quot;);
  twd.should(textarea, &quot;have.text&quot;, &quot;write some text&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a deterministic check of two things a scanner cannot see: the focus order is correct (back link, then input), and the input is reachable and usable by keyboard alone. If someone later drops a non-focusable &lt;code&gt;div&lt;/code&gt; with a click handler into the flow, or reorders the DOM so tab order no longer matches the layout, this test fails. You find out in the run, not from a user report.&lt;/p&gt;
&lt;p&gt;You can extend the same pattern to the cases that bite hardest: open a dialog and assert focus moved into it, close it and assert focus returned to the trigger, tab to the end of a menu and assert it does not escape into the page behind it.&lt;/p&gt;
&lt;h3 id=&quot;watch-out-with-automated-accessibility-testing&quot;&gt;Watch out with automated accessibility testing&lt;/h3&gt;
&lt;p&gt;Automated accessibility testing has a ceiling. Rule-based tools catch roughly a third of WCAG issues. axe is very good inside that third, and driving the interaction first stretches it to states a load-time scan cannot reach, but it is still a third. The rest needs a human: real screen reader testing, and judgment about whether labels, content order, and motion actually make sense.&lt;/p&gt;
&lt;p&gt;So treat this as the regression net, not the whole strategy. It is the part you can run on every change, in CI, that stops obvious and post-interaction failures from sliding back in between manual audits.&lt;/p&gt;
&lt;h3 id=&quot;why-this-matters-to-us&quot;&gt;Why this matters to us&lt;/h3&gt;
&lt;p&gt;We build the tool this runs on, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-js&quot;&gt;twd-js&lt;/a&gt;, and we treat its own accessibility as a first-class requirement, not a nice-to-have. A tool whose entire job is to help people ship correct software has no business locking anyone out of using it. So the sidebar UI is fully keyboard navigable with a logical focus order and no traps, the focus indicator is visible in both light and dark themes, and text and controls meet WCAG contrast ratios. We had it audited by an external accessibility consultancy against WCAG 2.2 Level AA, and it came back at full conformance. You can read the &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/accessibility-statement&quot;&gt;accessibility statement&lt;/a&gt; for the full scope and methodology.&lt;/p&gt;
&lt;h3 id=&quot;try-it-5&quot;&gt;Try it&lt;/h3&gt;
&lt;p&gt;Both approaches are just tests, running in your real app, in a real browser:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install twd-js
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/&quot;&gt;twd.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The axe-core helper and the validation-state example live in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd&quot;&gt;tutorial repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Source and issues: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd&quot;&gt;github.com/BRIKEV/twd&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Point a scan at the page that loads if you want a baseline. Then write the one that clicks the button first. That second test is the one your users were waiting for.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21693/automated-accessibility-testing-axe-core-keyboard-navigation-and-wcag-in-the-browser</guid>
<pubDate>Wed, 01 Jul 2026 12:52:42 +0000</pubDate>
</item>
<item>
<title>In-Browser Testing: Vitest Browser Mode and TWD, Side by Side</title>
<link>https://coderlegion.com/20851/in-browser-testing-vitest-browser-mode-and-twd-side-by-side</link>
<description>&lt;p&gt;Testing components in a simulated DOM (jsdom, happy-dom) has always felt like a compromise. What we actually want is tests that run in a real browser. So when Vitest Browser Mode shipped, I was genuinely excited to try it.&lt;/p&gt;
&lt;p&gt;I co-maintain &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/&quot;&gt;TWD (Testing While Developing)&lt;/a&gt;, an open-source tool built around in-browser testing, so I'm not a neutral party here. But the point isn't to declare a winner. It's to notice what each tool feels like to live with, where each one is clearly strong, and which scope each is really built for. I pointed both at the same TODO app for a working session: user flows, routing, network mocking. Here's the honest read.&lt;/p&gt;
&lt;h3 id=&quot;1-setup&quot;&gt;1. Setup&lt;/h3&gt;
&lt;p&gt;The first difference shows up before you write a line of code.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://vitest.dev/guide/browser/&quot;&gt;Vitest&lt;/a&gt; Browser Mode asks you to make a few choices up front: your framework, and a browser provider (Playwright or WebdriverIO) that becomes a dependency in your repo. It makes sense for what it is, a runner that has to drive a real browser engine, but it's a layer you set up and keep around.&lt;/p&gt;
&lt;p&gt;TWD's in-browser side is one package plus a single command to drop the service worker into your public directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install --save-dev twd-js
npx twd-js init public --save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's the whole setup to start writing tests. Different shapes for different goals: Vitest sets up its own browser-driving layer, while TWD installs into the dev environment you already have.&lt;/p&gt;
&lt;h3 id=&quot;2-the-standalone-runner&quot;&gt;2. The standalone runner&lt;/h3&gt;
&lt;p&gt;Boot up Vitest Browser Mode and you get a dedicated standalone server with a dashboard: a sidebar, runner reports, code views, even mobile responsive toggles.&lt;/p&gt;
&lt;p&gt;It's also young, and a couple of rough edges show that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Accessibility.&lt;/strong&gt; Some contrast issues, and the test navigation tree doesn't have interactive controls yet to collapse or expand sections. The kind of thing that tends to get polished as a tool matures.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7m4hkf0pwzv5s1zm5r4e.png&quot; alt=&quot;Vitest browser no interactive elements&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No clear live execution trace.&lt;/strong&gt; You don't get a command-by-command log in the sidebar while a test runs, which is something I lean on in TWD.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ggf87mop699niko4bk7g.png&quot; alt=&quot;TWD live execution trace&quot;&gt;&lt;/p&gt;
&lt;p&gt;It ships with an example, and the example is a standalone component. That fits the intent: the tool leans toward component testing, and it's good at it.&lt;/p&gt;
&lt;h3 id=&quot;3-component-testing-vs-full-app-flows&quot;&gt;3. Component testing vs full app flows&lt;/h3&gt;
&lt;p&gt;This is where the scopes split, and I think it's the most useful thing to understand about the two tools. The Vitest Browser docs lean toward component testing, and that's the lane it's built for. When I went the other way and tried to render the full app to test a flow with routing, I had to bring more setup along:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Path aliases.&lt;/strong&gt; If your app uses &lt;code&gt;@/components/*&lt;/code&gt;, you replicate that bundler config so Vitest understands it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global styles.&lt;/strong&gt; With Tailwind it's straightforward (&lt;code&gt;setupFiles: ['./src/index.css']&lt;/code&gt;). With other CSS tooling it's more config to work out.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's the test I wrote for a counter button wrapped in a React Router provider:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { expect, test } from 'vitest'
import { render } from 'vitest-browser-react'
import { RouterProvider } from 'react-router'
import router from '../src/AppRoutes'
import { userEvent } from 'vitest/browser'

test('renders name and increments count', async () =&amp;gt; {
  const { getByText } = await render(
    &amp;lt;RouterProvider router={router} /&amp;gt;
  )
  const title = getByText('Welcome to TWD')
  expect(title).toBeInTheDocument()

  const counterButton = getByText('Count is 0')
  expect(counterButton).toBeInTheDocument()

  await userEvent.click(counterButton)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even for something this basic I sometimes had to restart the run to get the state to sync. None of this is a flaw so much as a sign of which job the tool is shaped around: these are small things on a TODO app, but they add up on a full app.&lt;/p&gt;
&lt;p&gt;That's the core difference in approach. There's no separate &quot;rendering&quot; phase inside a sandbox wrapper with TWD. It injects into your already-running app and adapts to your project, rather than the project adapting to it.&lt;/p&gt;
&lt;h3 id=&quot;4-network-mocking&quot;&gt;4. Network mocking&lt;/h3&gt;
&lt;p&gt;This is the part that matters most to me, because in TWD we treat network interception as a pillar of frontend testing rather than an afterthought: you mock a request and own its response from the test.&lt;/p&gt;
&lt;p&gt;Vitest Browser Mode leaves network mocking to you, by design. You have three routes:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Option &lt;/th&gt;&lt;th&gt;Level &lt;/th&gt;&lt;th&gt;Trade-off&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/msw&quot;&gt;MSW&lt;/a&gt; &lt;/td&gt;&lt;td&gt;Real network (service worker) &lt;/td&gt;&lt;td&gt;Most realistic. The request actually fires and you assert on bodies. Vitest officially recommends it for browser-mode mocking. More setup: worker file in &lt;code&gt;public/&lt;/code&gt;, handlers.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;vi.mock('@/api/todos')&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Module &lt;/td&gt;&lt;td&gt;Simplest. Replace the API functions with fakes. But you bypass the HTTP client and the URL, so you're testing component plus router, not the contract.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;MSW is the one I'd reach for, and the nice surprise is that it supports the test-owns-the-response pattern TWD's &lt;code&gt;mockRequest&lt;/code&gt; gives you. &lt;code&gt;worker.use()&lt;/code&gt; prepends a one-off handler that takes precedence, and an &lt;code&gt;afterEach(() =&amp;gt; worker.resetHandlers())&lt;/code&gt; wipes it after each test, so nothing leaks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { http, HttpResponse } from 'msw'

test('shows empty state', async () =&amp;gt; {
  worker.use(
    http.get('http://localhost:3001/api/todos', () =&amp;gt; HttpResponse.json([]))
  )
  // render, assert &quot;No todos yet&quot;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can keep zero defaults and have every test declare its own handler (closest to the TWD style), or keep in-memory defaults and override per test for edge cases. You can also capture &lt;code&gt;await request.json()&lt;/code&gt; inside the handler and assert on it, mirroring a payload check.&lt;/p&gt;
&lt;p&gt;So both styles are available. The difference is mostly where the work lives: MSW means standing up handlers as your own little backend, where in TWD that interception is built in. &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev/contract-testing-setup&quot;&gt;Contract testing&lt;/a&gt; sits in the same place: it's part of TWD out of the box, and in Vitest it's something you'd add on top.&lt;/p&gt;
&lt;h3 id=&quot;5-ci-and-coverage&quot;&gt;5. CI and coverage&lt;/h3&gt;
&lt;p&gt;Both run well in CI. It depends mostly on the browser install step on your runner, and once that's set up execution is fast either way. twd-cli handles the headless side for TWD and isn't hard to wire up.&lt;/p&gt;
&lt;p&gt;Coverage is the one spot where the defaults differ. Vitest reports it out of the box, while in TWD you add the Istanbul plugin. That's a little setup rather than a real obstacle, but the out-of-the-box default is a nice touch on Vitest's side.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Both tools earned their place by the end of the session. Just not in the same place.&lt;/p&gt;
&lt;p&gt;Vitest Browser Mode is backed by a strong team and a large community, the terminal execution was genuinely good, and coverage comes for free. For component testing, a design system or a UI kit, try it: it's a real step up from testing components in a simulated DOM.&lt;/p&gt;
&lt;p&gt;For a full production app driven by user flows, TWD is the one I reach for. The flow tests were possible in Vitest too, just with more setup along the way, and I'd rather stay in the tab I'm already developing in than switch to a separate browser window.&lt;/p&gt;
&lt;p&gt;For TWD: &lt;strong&gt;testing isn't a separate phase you run after coding&lt;/strong&gt;, it's part of development. That's the bias, stated plainly. If you're doing component work, Vitest Browser Mode is worth your time. If you're testing whole flows while you build, that's the lane TWD was made for.&lt;/p&gt;
&lt;p&gt;The TWD is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev&quot;&gt;twd.dev&lt;/a&gt;. The repo is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd&quot;&gt;BRIKEV/twd&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20851/in-browser-testing-vitest-browser-mode-and-twd-side-by-side</guid>
<pubDate>Wed, 17 Jun 2026 22:17:53 +0000</pubDate>
</item>
<item>
<title>Testing Nuxt apps in the real browser, against a real backend</title>
<link>https://coderlegion.com/20850/testing-nuxt-apps-in-the-real-browser-against-a-real-backend</link>
<description>&lt;p&gt;You just built a Nuxt todo page. It loads from a Nitro route, adds a todo, deletes one, and it works when you click around. Now you want a test that proves it keeps working.&lt;/p&gt;
&lt;p&gt;The usual answer is to leave the app, stand up a separate test stack, and rebuild the page in a fake environment with every route mocked. TWD takes the other path: test it where you already are, while you are still building it.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-js&quot;&gt;TWD&lt;/a&gt; runs your tests inside the Nuxt dev server, in the real browser tab where the app already runs. The same todo page gets tested end to end, against your real backend. No route mocks, no separate e2e stack, no jsdom.&lt;/p&gt;
&lt;p&gt;Here is the full setup.&lt;/p&gt;
&lt;h3 id=&quot;setup-1&quot;&gt;Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install --save-dev twd-js
npx twd-js init public --save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the Vite plugin in &lt;code&gt;nuxt.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { twd } from 'twd-js/vite-plugin'

export default defineNuxtConfig({
  vite: {
    plugins: [
      twd({ testFilePattern: '/**/*.twd.test.{ts,tsx}' }),
    ],
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two Nuxt-specific bits to wire up, both one-time.&lt;/p&gt;
&lt;h3 id=&quot;1-inject-the-sidebar-in-app-vue&quot;&gt;1. Inject the sidebar in app.vue&lt;/h3&gt;
&lt;p&gt;Nuxt does not serve a Vite &lt;code&gt;index.html&lt;/code&gt;. Nitro renders the HTML, so the plugin cannot auto-inject its sidebar script. Point your app at the virtual module yourself, dev-only, in &lt;code&gt;app.vue&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
if (import.meta.dev) {
  useHead({
    script: [{ src: '/_nuxt/@id/virtual:twd/init', type: 'module' }],
  })
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;NuxtPage /&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the &lt;code&gt;/_nuxt/&lt;/code&gt; prefix, that is Nuxt's Vite base. The &lt;code&gt;import.meta.dev&lt;/code&gt; guard keeps the tag out of production.&lt;/p&gt;
&lt;h3 id=&quot;2-reload-the-browser-when-a-test-file-changes&quot;&gt;2. Reload the browser when a test file changes&lt;/h3&gt;
&lt;p&gt;On Nuxt 4 with Vite 7, editing a &lt;code&gt;.twd.test&lt;/code&gt; file does not reload the browser on its own. This small plugin fixes it: it watches your test files and triggers a reload when one changes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// npm install --save-dev chokidar
import chokidar from 'chokidar'
import type { Plugin } from 'vite'

function twdTestHmr(): Plugin {
  const isTestFile = (file: string) =&amp;gt; /\.twd\.test\.tsx?$/.test(file)
  return {
    name: 'twd-test-hmr',
    apply: 'serve',
    configureServer(server) {
      // Under Nuxt, Vite's config.root is the app/ srcDir, where the tests live.
      const watcher = chokidar.watch(server.config.root, {
        ignored: (path) =&amp;gt; /node_modules|\.nuxt|\.output|\.git/.test(path),
        ignoreInitial: true,
      })
      const reload = (file: string) =&amp;gt; {
        if (!isTestFile(file)) return
        // Drop the cached module so the reload serves fresh code.
        const graph = server.environments?.client?.moduleGraph ?? server.moduleGraph
        for (const mod of graph.getModulesByFile(file) ?? []) {
          graph.invalidateModule(mod)
        }
        server.ws.send({ type: 'full-reload', path: '*' })
      }
      watcher.on('change', reload).on('add', reload).on('unlink', reload)
      server.httpServer?.once('close', () =&amp;gt; void watcher.close())
    },
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add &lt;code&gt;twdTestHmr()&lt;/code&gt; to the &lt;code&gt;plugins&lt;/code&gt; array next to &lt;code&gt;twd()&lt;/code&gt;. Edit a test, the browser reloads.&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;npm run dev&lt;/code&gt;. Sidebar on the left, your app on the right.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2dyn3qub25h2566oo5wv.png&quot; alt=&quot;Nuxt app with twd tests running&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;testing-against-the-real-backend&quot;&gt;Testing against the real backend&lt;/h3&gt;
&lt;p&gt;You do not mock routes. The tests hit your dev server and your database for real. The only thing you need is a clean starting point for each test.&lt;/p&gt;
&lt;p&gt;A dev-only endpoint gives you that. It resets the database to a seeded baseline, guarded so it never exists in production:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// server/api/__test/reset.post.ts
export default defineEventHandler(() =&amp;gt; {
  if (!import.meta.dev) {
    throw createError({ statusCode: 404, statusMessage: 'Not found' })
  }
  resetDb()
  return { ok: true, todos: listTodos() }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;resetDb()&lt;/code&gt; deletes every row, resets the id sequence so seeded ids are deterministic, and re-seeds. The &lt;code&gt;import.meta.dev&lt;/code&gt; guard means the route returns 404 in a production build.&lt;/p&gt;
&lt;p&gt;Call it before each test, then drive the real UI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { twd, userEvent, screenDom } from 'twd-js'
import { describe, it, beforeEach } from 'twd-js/runner'

describe('Todos (real SQLite backend)', () =&amp;gt; {
  beforeEach(async () =&amp;gt; {
    // Reset the database to its seeded state. No route mocking.
    await fetch('/api/__test/reset', { method: 'POST' })
  })

  it('creates a todo end to end', async () =&amp;gt; {
    await twd.visit('/todos')
    await userEvent.type(screenDom.getByTestId('todo-input'), 'Buy milk')
    await userEvent.click(screenDom.getByTestId('todo-add'))

    twd.should(await screenDom.findByText('Buy milk'), 'be.visible')
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No mock rules. The &lt;code&gt;POST&lt;/code&gt; persists to the database, the list refreshes, and the assertion runs against the real rendered DOM. Delete a row and assert it is gone, same shape. When a test fails, it failed against the real backend, not against a mock you forgot to update.&lt;/p&gt;
&lt;h3 id=&quot;try-it-6&quot;&gt;Try it&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Sample repo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-nuxt-example&quot;&gt;BRIKEV/twd-nuxt-example&lt;/a&gt;. Nuxt 4, a SQLite-backed todo list, the HMR plugin, the dev-only reset endpoint, and tests that run against the real backend.&lt;/li&gt;
&lt;li&gt;Docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev&quot;&gt;twd.dev&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install --save-dev twd-js
npx twd-js init public --save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the plugin to &lt;code&gt;nuxt.config.ts&lt;/code&gt;, drop the dev-only script into &lt;code&gt;app.vue&lt;/code&gt;, add the HMR plugin, write your first &lt;code&gt;.twd.test.ts&lt;/code&gt;. The sidebar opens itself.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20850/testing-nuxt-apps-in-the-real-browser-against-a-real-backend</guid>
<pubDate>Wed, 17 Jun 2026 22:17:03 +0000</pubDate>
</item>
<item>
<title>Playwright vs TWD: A Frontend Developer's Honest Comparison</title>
<link>https://coderlegion.com/20648/playwright-vs-twd-a-frontend-developers-honest-comparison</link>
<description>&lt;p&gt;I had both &lt;a rel=&quot;nofollow&quot; href=&quot;https://playwright.dev/docs/intro&quot;&gt;Playwright&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;twd.dev&quot;&gt;TWD&lt;/a&gt; pointed at the same small app for a working day. Same backend, same UI, the same bugs to chase. The point wasn't to declare a winner. It was to notice what each one feels like to live with, especially when an AI agent is in the loop too.&lt;/p&gt;
&lt;p&gt;Two different rhythms. Different things they make easy. Different things they make slower. One of them I ended up reaching for much more often.&lt;/p&gt;
&lt;h3 id=&quot;the-first-thing-you-notice-where-the-tests-live&quot;&gt;The first thing you notice: where the tests live&lt;/h3&gt;
&lt;p&gt;Playwright spawns its own browser. A separate window, a separate context, a separate world. You write tests in Node, run them, watch them happen in a window you can look at but not really use.&lt;/p&gt;
&lt;p&gt;TWD lives in the tab you're already developing in. The sidebar sits on top of your app while you work. Click run on a test, the test drives the same page you were just clicking around on, then hands it back to you. You can keep using the app.&lt;/p&gt;
&lt;p&gt;That single difference shapes most of what comes after. When you can keep using the app while tests run, you reach for them more often. When tests take you out of the app to another window, you reach less.&lt;/p&gt;
&lt;h3 id=&quot;the-feedback-loop&quot;&gt;The feedback loop&lt;/h3&gt;
&lt;p&gt;Inside TWD, the cycle is short: write a line, save, click run, watch. If something fails, the failure shows up next to the code that produced it. You're never out of the dev tab.&lt;/p&gt;
&lt;p&gt;Playwright's loop is longer in feel, even when the run itself is fast. There's a separate browser, a terminal, a report, sometimes a trace viewer. Each piece is fine on its own. Together they add up to context-switching cost.&lt;/p&gt;
&lt;p&gt;It isn't that one runs in CI and the other doesn't. Both have a headless CLI runner. Both are fine in CI. The difference is which side of the day each one is designed around. TWD is shaped for the inside of a dev session and treats CI as the export target. Playwright is shaped for the run-the-whole-thing pass and treats local dev as a smaller slice of that.&lt;/p&gt;
&lt;h3 id=&quot;the-debugging-story-and-why-it-matters-more-now-&quot;&gt;The debugging story (and why it matters more now)&lt;/h3&gt;
&lt;p&gt;Both runners are good at telling you why something failed. They tell you in different shapes.&lt;/p&gt;
&lt;p&gt;When a TWD test fails, the error is usually about something specific. &quot;Couldn't find this label.&quot; &quot;The request payload didn't match.&quot; &quot;This element wasn't visible.&quot; It's the vocabulary you used to write the test, applied to what just happened. The diagnosis is the test reading itself back to you, in a sentence or two.&lt;/p&gt;
&lt;p&gt;When a Playwright test fails, you get richer artifacts. Screenshots, traces, a structured dump of the accessibility tree. The diagnosis is forensic. It's good at telling you what the browser looked like just before things went wrong.&lt;/p&gt;
&lt;p&gt;That forensic detail has a cost that didn't matter much when humans were the only audience. Now that the audience is often an AI agent reading test output as part of a session, the cost shows up. A single Playwright failure can drop kilobytes of locator HTML, class lists, and page snapshots into the context window. The agent reads all of it. The token bill scales accordingly. On a long debugging loop where the agent runs the suite, reads the failure, edits a file, runs the suite again, that footprint stacks up fast.&lt;/p&gt;
&lt;p&gt;TWD's failures are one-liners. The label name. The payload diff. The selector that didn't match. Cheap to read, cheap for an agent to act on, almost always enough to point at the line that broke.&lt;/p&gt;
&lt;p&gt;If your debugging loop involves an LLM in any way, the per-error footprint stops being a stylistic preference and starts being a budget line.&lt;/p&gt;
&lt;h3 id=&quot;what-you-get-without-having-to-build-it&quot;&gt;What you get without having to build it&lt;/h3&gt;
&lt;p&gt;This is where the gap shows up the most.&lt;/p&gt;
&lt;p&gt;TWD ships with coverage. Run the headless runner and you get a report. It also ships with contract testing: every mock you write gets validated against your OpenAPI spec on every run. You don't wire it up. It just runs.&lt;/p&gt;
&lt;p&gt;Playwright's job is testing. Coverage and contract testing aren't part of that job by design. You can add them. There are packages and recipes for both. None of it is hard, but none of it is free either. You write fixtures, capture data, post-process the output, integrate with reporters. The work isn't huge, but it's the kind of work that quietly gets deprioritized.&lt;/p&gt;
&lt;p&gt;This isn't a knock on Playwright. It's a question of which tool defines its scope to include the surrounding tooling, and which one stays narrow on purpose.&lt;/p&gt;
&lt;h3 id=&quot;how-they-actually-compose&quot;&gt;How they actually compose&lt;/h3&gt;
&lt;p&gt;The clean answer for choosing Playwright is real cross-browser. If your app has to work identically across Chromium, Firefox, and WebKit, Playwright gives you that natively. TWD runs in whichever browser you're developing in, one engine at a time. That's the lane. It's a real one. Most apps don't actually live in it, though.&lt;/p&gt;
&lt;p&gt;But the framing that earned the most ground for me during the session wasn't &quot;pick one.&quot; It was layering them.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TWD on the inside.&lt;/strong&gt; Every day. The tab you're already developing in. Component mocks, network mocks, fast feedback. Coverage and contract testing carried into CI as part of the same stack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Playwright at the gate.&lt;/strong&gt; A small set of true black-box smokes that run in real Chromium, Firefox, and WebKit before a deploy. Login flow, checkout, anything that has to behave identically across engines. Half a dozen tests at most.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most teams don't need 200 Playwright tests. They need 200 TWD tests and 6 Playwright tests. The math gets cheaper, the dev loop stays fast, and the cross-browser worry stays answered.&lt;/p&gt;
&lt;p&gt;That's the stack worth running, if you're going to run both.&lt;/p&gt;
&lt;h3 id=&quot;what-the-session-left-me-with&quot;&gt;What the session left me with&lt;/h3&gt;
&lt;p&gt;Both tools earned their place by the end. Just not in the same place.&lt;/p&gt;
&lt;p&gt;TWD was the right hand for everything I wrote and rewrote during the session: tests in the dev tab, instant feedback, errors short enough to read at a glance. The headless mode brought coverage and contract checks into CI without a separate setup. That's a lot of testing surface from one config.&lt;/p&gt;
&lt;p&gt;Playwright was the right hand for the cross-engine question. Not for every spec. For the small set that has to behave identically across browsers.&lt;/p&gt;
&lt;p&gt;Two tools, two scopes, one healthy boundary between them. That's the shape of it.&lt;/p&gt;
&lt;p&gt;The TWD runner is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev&quot;&gt;twd.dev&lt;/a&gt;. The repo is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-docs-tutorial/tree/demo/playwright-vs-twd&quot;&gt;BRIKEV/twd-docs-tutorial&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20648/playwright-vs-twd-a-frontend-developers-honest-comparison</guid>
<pubDate>Mon, 15 Jun 2026 16:52:22 +0000</pubDate>
</item>
<item>
<title>The easiest way to test React Router v7 apps</title>
<link>https://coderlegion.com/20647/the-easiest-way-to-test-react-router-v7-apps</link>
<description>&lt;p&gt;You wrote a React Router v7 route. It has a &lt;code&gt;loader&lt;/code&gt;, an &lt;code&gt;action&lt;/code&gt;, a form, some interactivity. Now you want to test it. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://reactrouter.com/start/framework/testing&quot;&gt;official docs&lt;/a&gt; hand you two paths and neither one covers the case you actually have.&lt;/p&gt;
&lt;p&gt;Path one is &lt;code&gt;createRoutesStub&lt;/code&gt; plus &lt;code&gt;@testing-library/react&lt;/code&gt;, running in a terminal-based test runner. It works for small reusable components that consume router hooks. The docs explicitly warn against using it for full route components with the framework-mode &lt;code&gt;Route.*&lt;/code&gt; types: &quot;not designed for (and is arguably incompatible with) direct testing of Route components&quot;. So if you want to test the actual route the user is going to load, this path stops short.&lt;/p&gt;
&lt;p&gt;Path two is Playwright or Cypress. The docs themselves point you there for route-level tests: &quot;we recommend you do that via an Integration/E2E test... against a running application&quot;. That works, but you've left your dev loop, you've added a separate test stack, and the feedback cycle gets long. It's QA-shaped, not developer-shaped.&lt;/p&gt;
&lt;p&gt;There's a middle path. Use the same &lt;code&gt;createRoutesStub&lt;/code&gt; the docs recommend for components, but render it inside the &lt;strong&gt;real browser&lt;/strong&gt;, in the &lt;strong&gt;real dev server&lt;/strong&gt;, against the &lt;strong&gt;real DOM&lt;/strong&gt;, with the dev loop still attached. That's what &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-js&quot;&gt;TWD&lt;/a&gt; does. The framework's own utility, in the right environment.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x6lb49bn5m362y8g0197.png&quot; alt=&quot;TWD sidebar running tests against a React Router page&quot;&gt;&lt;/p&gt;
&lt;p&gt;That's the TWD sidebar on the left, the app on the right. The sidebar is running real &lt;code&gt;createRoutesStub&lt;/code&gt; tests against the live page. No jsdom, no headless Chrome, no separate process.&lt;/p&gt;
&lt;h3 id=&quot;setup-2&quot;&gt;Setup&lt;/h3&gt;
&lt;p&gt;Install:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install --save-dev twd-js
npx twd-js init public --save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the Vite plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// vite.config.ts
import { reactRouter } from '@react-router/dev/vite'
import { defineConfig } from 'vite'
import { twd } from 'twd-js/vite-plugin'

export default defineConfig({
  plugins: [
    reactRouter(),
    twd({ testFilePattern: '/**/*.twd.test.{ts,tsx}' }),
  ],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There's one SSR-mode workaround you'll hit exactly once. React Router v7 framework mode does not use a Vite-served &lt;code&gt;index.html&lt;/code&gt;. The framework's own middleware renders HTML from &lt;code&gt;app/root.tsx&lt;/code&gt;, and Vite's &lt;code&gt;transformIndexHtml&lt;/code&gt; hook never fires for that HTML. The plugin can't auto-inject its sidebar bootstrap script. The fix is one dev-only block in &lt;code&gt;app/root.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;head&amp;gt;
  &amp;lt;Meta /&amp;gt;
  &amp;lt;Links /&amp;gt;
  {import.meta.env.DEV &amp;amp;&amp;amp; (
    &amp;lt;script type=&quot;module&quot; src=&quot;/@id/virtual:twd/init&quot; /&amp;gt;
  )}
&amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The plugin still registers the virtual module. Vite still serves it at &lt;code&gt;/@id/...&lt;/code&gt;. You're just pointing your SSR'd template at it yourself. The &lt;code&gt;import.meta.env.DEV&lt;/code&gt; guard keeps the tag out of production.&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;npm run dev&lt;/code&gt;. Sidebar on the left, your app on the right.&lt;/p&gt;
&lt;h3 id=&quot;a-fallback-testing-route&quot;&gt;A fallback &lt;code&gt;/testing&lt;/code&gt; route&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;createRoutesStub&lt;/code&gt; builds a tiny in-memory router. To mount it, you need a DOM container and a React root. The cleanest way is a dedicated route that exists only as a mounting point for tests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/routes/testing-page.tsx
export default function TestPage() {
  return &amp;lt;div data-testid=&quot;testing-page&quot; style={{ minHeight: '100vh' }} /&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add it to your route table at &lt;code&gt;/testing&lt;/code&gt;. Then a small utility navigates to it and hands you a fresh root in &lt;code&gt;beforeEach&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/twd-tests/utils.ts
import { createRoot } from 'react-dom/client'
import { twd, screenDomGlobal } from 'twd-js'

let root: ReturnType&amp;lt;typeof createRoot&amp;gt; | undefined

export async function setupReactRoot() {
  if (root) { root.unmount(); root = undefined }
  await twd.visit('/testing')
  const container = await screenDomGlobal.findByTestId('testing-page')
  root = createRoot(container)
  return root
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's the whole harness. One route, seven lines of utility. The cost is small and you reuse it across every test.&lt;/p&gt;
&lt;h3 id=&quot;writing-a-route-level-test&quot;&gt;Writing a route-level test&lt;/h3&gt;
&lt;p&gt;This is the test the official docs say not to write with &lt;code&gt;createRoutesStub&lt;/code&gt;. In TWD it works, because the stub is rendering into the same real document your app lives in. Full &lt;code&gt;Route.*&lt;/code&gt; types, full hooks, real DOM.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/twd-tests/todoList.twd.test.tsx
import { twd, expect, userEvent, screenDom } from 'twd-js'
import { describe, it, beforeEach } from 'twd-js/runner'
import {
  createRoutesStub,
  useLoaderData,
  useParams,
  useMatches,
} from 'react-router'
import TodoListPage from '~/routes/todolist'
import todoListMock from './mocks/todoList.json'
import { setupReactRoot } from './utils'

describe('Todo List page', () =&amp;gt; {
  let root: Awaited&amp;lt;ReturnType&amp;lt;typeof setupReactRoot&amp;gt;&amp;gt;

  beforeEach(async () =&amp;gt; {
    root = await setupReactRoot()
  })

  it('renders todos from the loader', async () =&amp;gt; {
    const Stub = createRoutesStub([
      {
        path: '/',
        Component: () =&amp;gt; {
          const loaderData = useLoaderData()
          const params = useParams()
          const matches = useMatches() as any
          return (
            &amp;lt;TodoListPage
              loaderData={loaderData}
              params={params}
              matches={matches}
            /&amp;gt;
          )
        },
        loader() {
          return { todos: todoListMock }
        },
      },
    ])

    root.render(&amp;lt;Stub /&amp;gt;)
    await twd.wait(300)

    const firstTodoTitle = await screenDom.getByText('Learn TWD')
    twd.should(firstTodoTitle, 'be.visible')

    const firstTodoDate = await screenDom.getByText('Date: 2024-12-20')
    twd.should(firstTodoDate, 'be.visible')
  })

  it('submits the create-todo action with the right payload', async () =&amp;gt; {
    let payload: Record&amp;lt;string, string&amp;gt; | null = null
    const Stub = createRoutesStub([
      {
        path: '/todos',
        Component: /* same wrapper */ () =&amp;gt; null,
        loader() { return { todos: [] } },
        async action({ request }) {
          const formData = await request.formData()
          payload = Object.fromEntries(formData) as Record&amp;lt;string, string&amp;gt;
          return null
        },
      },
    ])

    root.render(&amp;lt;Stub initialEntries={['/todos']} /&amp;gt;)

    const titleInput = await screenDom.getByLabelText('Title')
    await userEvent.type(titleInput, 'Test Todo')

    const submitButton = await screenDom.getByRole('button', { name: 'Create Todo' })
    await userEvent.click(submitButton)

    expect(payload).to.deep.equal({ title: 'Test Todo' /* ... */ })
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The pattern is exactly the one the official docs show. The wrapper component pulls &lt;code&gt;useLoaderData&lt;/code&gt; / &lt;code&gt;useParams&lt;/code&gt; / &lt;code&gt;useMatches&lt;/code&gt; and forwards them as props. The stub provides a loader and (optionally) an action. You render, you interact, you assert. The only difference from the docs example is where it runs.&lt;/p&gt;
&lt;h3 id=&quot;backend-tests-any-runner-works&quot;&gt;Backend tests: any runner works&lt;/h3&gt;
&lt;p&gt;Framework mode splits the route in two: the &lt;code&gt;loader&lt;/code&gt; and &lt;code&gt;action&lt;/code&gt; are server-side, the component is client-side. The two halves get tested by different tools, and the backend half is the easier one.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;loader&lt;/code&gt; and &lt;code&gt;action&lt;/code&gt; are async functions that take a &lt;code&gt;Request&lt;/code&gt; and a context. They have no DOM, no router, no rendering. Test them with whatever your project already uses: vitest, jest, node's built-in test runner, anything. Here it is in vitest as one example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/routes/todolist.test.ts
import { describe, it, expect, vi } from 'vitest'
import { loader, action } from './todolist'
import * as api from '~/api/todos'

it('loader returns todos from the api layer', async () =&amp;gt; {
  vi.spyOn(api, 'fetchTodos').mockResolvedValue([
    { id: '1', title: 'Learn TWD', description: '', date: '2024-12-20' },
  ])

  const result = await loader({
    request: new Request('http://test/todos'),
    params: {},
    context: new Map() as any,
  })

  expect(result.todos).toHaveLength(1)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Whatever runner you pick, the test boots in milliseconds and runs anywhere Node runs. It's a function call.&lt;/p&gt;
&lt;p&gt;Two suites, one per layer. When the backend test fails, the loader broke. When the TWD test fails, the rendering broke. You stop debugging the wrong layer.&lt;/p&gt;
&lt;h3 id=&quot;why-this-works-same-utility-different-environment&quot;&gt;Why this works: same utility, different environment&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;createRoutesStub&lt;/code&gt; is a great piece of API. The official docs scope it to non-route components for a fair reason: mounting a full route component into jsdom is genuinely brittle, because framework-mode &lt;code&gt;Route.*&lt;/code&gt; types, auto-imports, and the dev loop weren't built for a synthetic DOM. You can spend more time wrestling the harness than writing the test.&lt;/p&gt;
&lt;p&gt;TWD changes one thing about that picture. The test runs inside your real Vite dev server, in the real browser tab where your app already runs. Auto-imports resolve. Types match runtime. The dev loop is still there. With the surrounding environment in place, the same &lt;code&gt;createRoutesStub&lt;/code&gt; scales up to route-level components without fighting anything.&lt;/p&gt;
&lt;p&gt;That change pulls a few extras along with it. The sidebar is part of your dev loop, not a separate thing you switch into. The same tab where you're building the page runs the tests. The DOM you're testing is the DOM you're using. Click around manually, run a test, watch the assertion render in real time, fix it, re-run. No context switch, no second window, no headless screenshot to interpret.&lt;/p&gt;
&lt;h3 id=&quot;try-it-7&quot;&gt;Try it&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Sample repo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-react-router&quot;&gt;BRIKEV/twd-react-router&lt;/a&gt;. React Router v7 in framework mode, a todo list with loader and action, the harness route wired up, GitHub Actions CI configured against a json-server backend (the part most sample repos forget).&lt;/li&gt;
&lt;li&gt;Docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev&quot;&gt;twd.dev&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install --save-dev twd-js
npx twd-js init public --save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the plugin to &lt;code&gt;vite.config.ts&lt;/code&gt;, drop the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag into &lt;code&gt;app/root.tsx&lt;/code&gt;, add the &lt;code&gt;/testing&lt;/code&gt; route, write your first &lt;code&gt;.twd.test.tsx&lt;/code&gt;. The sidebar opens itself.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20647/the-easiest-way-to-test-react-router-v7-apps</guid>
<pubDate>Mon, 15 Jun 2026 16:50:54 +0000</pubDate>
</item>
<item>
<title>TWD setup is now two Vite plugins and zero app code</title>
<link>https://coderlegion.com/19001/twd-setup-is-now-two-vite-plugins-and-zero-app-code</link>
<description>&lt;p&gt;Setting up &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-js&quot;&gt;TWD&lt;/a&gt; used to mean adding a block of dev-only code to your app's entry file — a dynamic import for the runner, a test glob, a service-worker config, and a &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-relay&quot;&gt;twd-relay&lt;/a&gt; browser client. It worked, but it never really belonged there.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;twd-js@1.8&lt;/code&gt; and &lt;code&gt;twd-relay@1.2&lt;/code&gt;, both packages ship Vite plugins. Setup is two entries in &lt;code&gt;vite.config.ts&lt;/code&gt; and &lt;strong&gt;nothing in &lt;code&gt;main.tsx&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-new-setup&quot;&gt;The new setup&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;vite.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { defineConfig } from &quot;vite&quot;;
import react from &quot;@vitejs/plugin-react&quot;;
import { twd } from &quot;twd-js/vite-plugin&quot;;
import { twdRemote } from &quot;twd-relay/vite&quot;;

export default defineConfig({
    plugins: [
        react(),
        twd({
            testFilePattern: &quot;/**/*.twd.test.ts&quot;,
            open: false,
            position: &quot;right&quot;,
            search: true,
        }),
        twdRemote(),
    ],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;main.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React from &quot;react&quot;;
import ReactDOM from &quot;react-dom/client&quot;;
import { RouterProvider } from &quot;react-router&quot;;
import { router } from &quot;./routes/router&quot;;
import &quot;./styles/index.css&quot;;

ReactDOM.createRoot(document.getElementById(&quot;root&quot;)!).render(
    &amp;lt;RouterProvider router={router} /&amp;gt;,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's the whole setup. &lt;code&gt;twd()&lt;/code&gt; owns the sidebar, glob discovery, and service-worker registration. &lt;code&gt;twdRemote()&lt;/code&gt; attaches the relay to the Vite dev server and auto-injects the browser client into &lt;code&gt;index.html&lt;/code&gt;. Both plugins use &lt;code&gt;apply: 'serve'&lt;/code&gt;, so production builds are untouched.&lt;/p&gt;
&lt;h3 id=&quot;what-it-replaces&quot;&gt;What it replaces&lt;/h3&gt;
&lt;p&gt;For comparison, here's what a TWD entry file looked like a few weeks ago:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;if (import.meta.env.DEV) {
    const { initTWD } = await import(&quot;twd-js/bundled&quot;);
    const tests = import.meta.glob(&quot;./**/*.twd.test.ts&quot;);
    initTWD(tests, {
        open: false,
        position: &quot;right&quot;,
        serviceWorker: true,
        serviceWorkerUrl: &quot;/mock-sw.js&quot;,
        search: true,
    });

    const { createBrowserClient } = await import(&quot;twd-relay/browser&quot;);
    const client = createBrowserClient({
        url: `${window.location.origin}/__twd/ws`,
    });
    client.connect();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two top-level &lt;code&gt;await import&lt;/code&gt;s, a glob, a service-worker URL that had to stay in sync with the runner, a WebSocket URL that had to match the relay path, and config repeating defaults. All of it dev-only, all of it sitting above &lt;code&gt;ReactDOM.createRoot&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;After the upgrade, that block is gone. No &lt;code&gt;if (import.meta.env.DEV)&lt;/code&gt;, no dynamic imports, no relay client. The dev-tooling story lives entirely in &lt;code&gt;vite.config.ts&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;why-it-matters&quot;&gt;Why it matters&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;One source of truth for the wiring.&lt;/strong&gt; The &lt;code&gt;serviceWorkerUrl&lt;/code&gt;, the SW served by the dev server, the WebSocket path used by the relay, and the path the browser client connects to were all strings in different files that had to agree. Now the plugins own them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No top-level await for tooling.&lt;/strong&gt; The &lt;code&gt;await import(&quot;twd-js/bundled&quot;)&lt;/code&gt; was loading a chunk that had nothing to do with your app, before React was allowed to mount.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tooling lives in tooling config.&lt;/strong&gt; New developers reading &lt;code&gt;main.tsx&lt;/code&gt; shouldn't have to mentally &lt;code&gt;if (import.meta.env.DEV)&lt;/code&gt;-out a quarter of the file to understand startup. The plugin model is what the rest of the Vite ecosystem already does — &lt;code&gt;@vitejs/plugin-react&lt;/code&gt;, Tailwind, Tanstack Router devtools — and TWD now matches.&lt;/p&gt;
&lt;h3 id=&quot;non-vite-projects&quot;&gt;Non-Vite projects&lt;/h3&gt;
&lt;p&gt;Webpack, Angular CLI, Rollup, esbuild, Rspack — anywhere the Vite plugins don't apply — keep the manual API. &lt;code&gt;initTWD&lt;/code&gt; and &lt;code&gt;createBrowserClient&lt;/code&gt; stay public exports forever. &lt;code&gt;twdRemote({ autoConnect: false })&lt;/code&gt; is also there as an escape hatch for Vite projects that want to wire the browser client by hand.&lt;/p&gt;
&lt;h3 id=&quot;try-it-8&quot;&gt;Try it&lt;/h3&gt;
&lt;p&gt;The runner is at &lt;a rel=&quot;nofollow&quot; href=&quot;https://twd.dev&quot;&gt;https://twd.dev&lt;/a&gt;. Upgrade to &lt;code&gt;twd-js@1.8&lt;/code&gt; and &lt;code&gt;twd-relay@1.2&lt;/code&gt;, drop the dev-only block from &lt;code&gt;main.tsx&lt;/code&gt;, add the two plugins to &lt;code&gt;vite.config.ts&lt;/code&gt;, and you're done.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19001/twd-setup-is-now-two-vite-plugins-and-zero-app-code</guid>
<pubDate>Tue, 26 May 2026 15:30:45 +0000</pubDate>
</item>
<item>
<title>Modern Frontend Frameworks Are Failing at Testing</title>
<link>https://coderlegion.com/9093/modern-frontend-frameworks-are-failing-at-testing</link>
<description>&lt;p&gt;A few weeks after releasing &lt;a rel=&quot;nofollow&quot; href=&quot;https://brikev.github.io/twd/&quot;&gt;TWD&lt;/a&gt;, a tool designed to integrate testing directly into the development workflow. I started creating testing recipes for different frontend frameworks. While doing that, a few patterns became very clear.&lt;/p&gt;
&lt;h3 id=&quot;what-works&quot;&gt;What works&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Frontend-focused applications (SPAs) are relatively easy to test using tools like TWD and traditional runners such as Vitest.&lt;/li&gt;
&lt;li&gt;You can validate real user interactions, keep tests close to the UI, and iterate quickly.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-doesn-t&quot;&gt;What doesn’t&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;SSR frameworks make testing unnecessarily confusing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Frameworks like &lt;strong&gt;Next.js, Astro, TanStack, Qwik, and Solid&lt;/strong&gt; provide little to no guidance on how to test applications in a reliable and maintainable way. Most of them simply &lt;strong&gt;delegate testing to external tools&lt;/strong&gt; like Playwright or Cypress (&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/testing&quot;&gt;example next.js docs&lt;/a&gt;), treating testing as an afterthought rather than part of the framework itself.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-with-ssr-frameworks&quot;&gt;The problem with SSR frameworks&lt;/h3&gt;
&lt;p&gt;SSR frameworks blur the line between backend and frontend. In theory, this sounds powerful. In practice, it creates serious testing problems.&lt;/p&gt;
&lt;p&gt;Yes, you need to test both frontend and backend behavior, but SSR frameworks don’t offer a clear or opinionated way to do that. If you look at their documentation, testing is either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Completely missing&lt;/li&gt;
&lt;li&gt;Mentioned briefly&lt;/li&gt;
&lt;li&gt;Outsourced to E2E tools like Playwright or Cypress&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This effectively means: &lt;em&gt;“Here’s how to build fast. Testing is your problem.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;From a company perspective—especially one building a &lt;strong&gt;long-lived product&lt;/strong&gt;—this is a big red flag. You want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tests that are easy to write&lt;/li&gt;
&lt;li&gt;Tests that reliably fail when something is actually broken&lt;/li&gt;
&lt;li&gt;Tests that help reproduce bugs&lt;/li&gt;
&lt;li&gt;Tests that evolve with the product&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most SSR frameworks don’t provide that. They optimize for speed of development, but you start accumulating &lt;strong&gt;testing debt from day one&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-but-you-can-unit-test-right-&quot;&gt;“But you can unit test, right?”&lt;/h3&gt;
&lt;p&gt;Technically, yes. But in practice, unit testing frontend code often becomes &lt;em&gt;testing for the sake of testing&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;What usually happens:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You mock half of the system&lt;/li&gt;
&lt;li&gt;You test implementation details&lt;/li&gt;
&lt;li&gt;You don’t test real user interactions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Eventually, you fall back to Playwright. And now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tests live outside your app&lt;/li&gt;
&lt;li&gt;They’re slower&lt;/li&gt;
&lt;li&gt;They’re harder to debug&lt;/li&gt;
&lt;li&gt;They don’t feel like part of your development workflow&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is extremely common in &lt;strong&gt;Next.js applications&lt;/strong&gt;. To this day, I haven’t seen a &lt;strong&gt;truly robust testing setup&lt;/strong&gt; for Next.js that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fails only when something meaningful is broken&lt;/li&gt;
&lt;li&gt;Avoids implementation details&lt;/li&gt;
&lt;li&gt;Is easy to write and maintain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By “robust,” I mean tests that actually protect the product—not just increase coverage numbers.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;react-router-loaders-and-actions-an-exception&quot;&gt;React Router loaders and actions: an exception&lt;/h3&gt;
&lt;p&gt;In the React ecosystem, I found &lt;strong&gt;one notable exception: React Router&lt;/strong&gt;, specifically createRoutesStub.&lt;/p&gt;
&lt;p&gt;This API is interesting because it allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mock routes with loaders and actions&lt;/li&gt;
&lt;li&gt;Create different scenarios per page&lt;/li&gt;
&lt;li&gt;Test frontend behavior realistically&lt;/li&gt;
&lt;li&gt;test loaders and actions separately as pure functions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This separation of concerns is something many frameworks try to replicate—but React Router gets it right.&lt;/p&gt;
&lt;p&gt;The API design makes testing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Straightforward&lt;/li&gt;
&lt;li&gt;Explicit&lt;/li&gt;
&lt;li&gt;Predictable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even better, migrating from SPA data mode to framework (SSR) mode is &lt;strong&gt;incremental and low-risk&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;After researching multiple alternatives, and considering that testing is one of the most critical aspects of a product, &lt;strong&gt;React Router is the only SSR-capable solution I’d personally choose today.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;using-twd-with-react-router&quot;&gt;Using TWD with React Router&lt;/h3&gt;
&lt;p&gt;When building TWD, we noticed how powerful &lt;code&gt;createRoutesStub&lt;/code&gt; was—and decided to use it in a different way.&lt;/p&gt;
&lt;p&gt;Instead of testing routes in isolation, we:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Added a dedicated testing route&lt;/li&gt;
&lt;li&gt;Executed TWD tests inside the real application&lt;/li&gt;
&lt;li&gt;Rendered pages using route stubs&lt;/li&gt;
&lt;li&gt;Controlled loaders, actions, and scenarios per test&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the setup.&lt;/p&gt;
&lt;h4 id=&quot;route-configuration&quot;&gt;Route configuration&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { type RouteConfig, index, route } from &quot;@react-router/dev/routes&quot;;

export default [
  index(&quot;routes/home.tsx&quot;),
  route(&quot;todos&quot;, &quot;routes/todolist.tsx&quot;),
  route(&quot;testing&quot;, &quot;routes/testing-page.tsx&quot;),
] satisfies RouteConfig;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;testing-page&quot;&gt;Testing page&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/routes/testing-page.tsx
let twdInitialized = false;

export async function clientLoader() {
  if (import.meta.env.DEV) {
    const testModules = import.meta.glob(&quot;../**/*.twd.test.{ts,tsx}&quot;);
    if (!twdInitialized) {
      const { initTWD } = await import('twd-js/bundled');
      initTWD(testModules, {
        serviceWorker: false,
      });
      twdInitialized = true;
    }
    return {};
  } else {
    return {};
  }
}

export default function TestPage() {
  return (
    &amp;lt;div data-testid=&quot;testing-page&quot; &amp;gt;
      &amp;lt;h1 style={{ opacity: 0.5, fontFamily: 'system-ui, sans-serif' }}&amp;gt;TWD Test Page&amp;lt;/h1&amp;gt;
      &amp;lt;p style={{ opacity: 0.5, fontFamily: 'system-ui, sans-serif' }}&amp;gt;
        This page is used as a mounting point for TWD tests.
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7x1ryhxb18xxu79hyiac.png&quot; alt=&quot;twd testing page&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;example-test&quot;&gt;Example test&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { createRoot } from &quot;react-dom/client&quot;;
import { twd, screenDomGlobal } from &quot;twd-js&quot;;

let root: ReturnType&amp;lt;typeof createRoot&amp;gt; | undefined;

export async function setupReactRoot() {
  if (root) {
    root.unmount();
    root = undefined;
  }

  // Navigate to the empty test page
  await twd.visit('/testing');
  
  // Get the container from the test page
  const container = await screenDomGlobal.findByTestId('testing-page');
  root = createRoot(container);
  return root;
}

describe(&quot;Hello World Test&quot;, () =&amp;gt; {
  // root instance to re render the stub
  let root: ReturnType&amp;lt;typeof createRoot&amp;gt; | undefined;

  beforeEach(async () =&amp;gt; {
    // preparing the root instance
    root = await setupReactRoot();
  });

  it(&quot;should render home page test&quot;, async () =&amp;gt; {
    // mocking the component
    const Stub = createRoutesStub([
      {
        path: &quot;/&quot;,
        Component: () =&amp;gt; {
          const loaderData = useLoaderData();
          const params = useParams();
          const matches = useMatches() as any;
          return &amp;lt;Home loaderData={loaderData} params={params} matches={matches} /&amp;gt;;
        },
        loader() {
          return { title: &quot;Home Page test&quot; };
        },
      },
    ]);

    // Render the Stub
    root!.render(&amp;lt;Stub /&amp;gt;);
    await twd.wait(300);
    // Check for the element within our test container
    // We scope the search to the container to be safe, or just search globally since the harness is empty otherwise
    const h1 = await screenDom.findByRole('heading', { level: 1 });
    twd.should(h1, 'have.text', 'Home Page test');
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the test runs, the route is rendered inside the real application, fully interactive, with all assertions passing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/375uz5i9ru0tsi8r17zi.png&quot; alt=&quot;twd test&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;At the end of the day, you have a choice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep using tools that optimize for speed but accumulate testing debt&lt;/li&gt;
&lt;li&gt;Or adopt an approach where testing is part of everyday development&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For SSR applications, &lt;strong&gt;React Router + TWD&lt;/strong&gt; is the first setup I’ve seen that truly supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real UI testing&lt;/li&gt;
&lt;li&gt;Maintainability and easier to debug&lt;/li&gt;
&lt;li&gt;Long-lived products&lt;/li&gt;
&lt;li&gt;Fast feedback&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you work on large applications with complex requirements, this approach scales far better than traditional SSR testing strategies.&lt;/p&gt;
&lt;p&gt;An example on how to integrate &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-react-router&quot;&gt;twd + react router&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Integration Testing</category>
<guid isPermaLink="true">https://coderlegion.com/9093/modern-frontend-frameworks-are-failing-at-testing</guid>
<pubDate>Tue, 30 Dec 2025 12:12:28 +0000</pubDate>
</item>
<item>
<title>TWD 1.3.x release - Multiple framework support</title>
<link>https://coderlegion.com/8455/twd-1-3-x-release-multiple-framework-support</link>
<description>&lt;p&gt;One of the biggest milestones we’ve been working on is now live:&lt;br&gt;
&lt;strong&gt;TWD 1.3.x brings full multi-framework support with a single unified setup.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This means you can now drop TWD into React, Vue, Solid, and Angular projects using the &lt;strong&gt;exact same initialization code&lt;/strong&gt;, thanks to the new bundled build.&lt;/p&gt;
&lt;p&gt;At its simplest, all you need is this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/main.tsx (or .ts)
if (import.meta.env.DEV) {
  const { initTWD } = await import('twd-js/bundled');
  const tests = import.meta.glob(&quot;./**/*.twd.test.ts&quot;);
  
  initTWD(tests, { open: true, position: 'left' });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;react&quot;&gt;React&lt;/h3&gt;
&lt;p&gt;React continues to have the most flexible integration: use the standard setup for advanced customization or the bundled one for simplicity.&lt;/p&gt;
&lt;h4 id=&quot;standard-setup-full-control-&quot;&gt;Standard Setup (full control)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;if (import.meta.env.DEV) {
  const testModules = import.meta.glob(&quot;./**/*.twd.test.ts&quot;);
  const { initTests, twd, TWDSidebar } = await import('twd-js');
  
  initTests(testModules, &amp;lt;TWDSidebar open={true} position=&quot;left&quot; /&amp;gt;, createRoot);
  twd.initRequestMocking().catch(console.error);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;simplified-setup-bundled-&quot;&gt;Simplified Setup (bundled)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;if (import.meta.env.DEV) {
  const { initTWD } = await import('twd-js/bundled');
  const tests = import.meta.glob(&quot;./**/*.twd.test.ts&quot;);
  
  initTWD(tests, { open: true, position: 'left' });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd/tree/main/examples&quot;&gt;View React Examples&lt;/a&gt;&lt;/strong&gt; - Multiple React examples available in the repository.&lt;/p&gt;
&lt;h3 id=&quot;vue&quot;&gt;Vue&lt;/h3&gt;
&lt;p&gt;For Vue applications, use the bundled version of TWD. This ensures that the React runtime required by TWD's UI is handled correctly without conflicting with your Vue app. Request mocking is automatically initialized.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-vue-example&quot;&gt;Vue Example Repository&lt;/a&gt;&lt;/strong&gt; - Complete working example with advanced scenarios.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'

if (import.meta.env.DEV) {
  // Use the bundled version
  const { initTWD } = await import('twd-js/bundled');
  const tests = import.meta.glob(&quot;./**/*.twd.test.ts&quot;)
  
  // Request mocking is automatically initialized
  initTWD(tests);
}

createApp(App).mount('#app')
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;solid&quot;&gt;Solid&lt;/h3&gt;
&lt;p&gt;For Solid.js applications, use the bundled version of TWD. This ensures that the React runtime required by TWD's UI is handled correctly without conflicting with your Solid app. Request mocking is automatically initialized.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-solid-example&quot;&gt;Solid Example Repository&lt;/a&gt;&lt;/strong&gt; - Complete Solid.js integration example.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// src/main.tsx
/* @refresh reload */
import { render } from 'solid-js/web';
import 'solid-devtools';

import App from './App';

if (import.meta.env.DEV) {
  const { initTWD } = await import('twd-js/bundled');
  const tests = import.meta.glob(&quot;./**/*.twd.test.ts&quot;);
  
  // Request mocking is automatically initialized
  initTWD(tests);
}

const root = document.getElementById('root');

if (import.meta.env.DEV &amp;amp;&amp;amp; !(root instanceof HTMLElement)) {
  throw new Error(
    'Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?',
  );
}

render(() =&amp;gt; &amp;lt;App /&amp;gt;, root!);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;notes-for-solid&quot;&gt;Notes for Solid&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;This setup works with &lt;strong&gt;Solid + Vite&lt;/strong&gt; applications&lt;/li&gt;
&lt;li&gt;Solid Start compatibility has not been tested yet, but may work with similar configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;angular&quot;&gt;Angular&lt;/h3&gt;
&lt;p&gt;Angular applications can also use the bundled version. Note that you might need to manually construct the &lt;code&gt;tests&lt;/code&gt; object if your build tool doesn't support glob imports in the same way. Request mocking is automatically initialized.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-angular-example&quot;&gt;Angular Example Repository&lt;/a&gt;&lt;/strong&gt; - Working Angular integration example.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
import { isDevMode } from '@angular/core';

if (isDevMode()) {
  const { initTWD } = await import('twd-js/bundled');
  
  // Define your test files manually or use a compatible glob importer
  const tests = {
    './twd-tests/helloWorld.twd.test.ts': () =&amp;gt; import('./twd-tests/helloWorld.twd.test'),
    './twd-tests/todoList.twd.test.ts': () =&amp;gt; import('./twd-tests/todoList.twd.test'),
  };
  
  // Request mocking is automatically initialized
  initTWD(tests);
}

bootstrapApplication(App, appConfig)
  .catch((err) =&amp;gt; console.error(err));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;TWD 1.3.x introduces a universal setup that lets you write a test and run it in any framework—React, Vue, Solid, or Angular. The new bundled version provides a single, simple initialization method that works everywhere.&lt;/p&gt;
&lt;p&gt;You get the full TWD experience in every environment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Testing While Developing&lt;/strong&gt; with the live in-app sidebar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fast, interactive test creation&lt;/strong&gt; directly inside your running app&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Straightforward setup&lt;/strong&gt; with just one init function&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic request mocking&lt;/strong&gt; included by default&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI-ready tests&lt;/strong&gt; that behave the same in development and pipelines&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy to write, easy to maintain&lt;/strong&gt;, and now fully &lt;strong&gt;framework-agnostic&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With TWD 1.3.x, your tests become portable, consistent, and reusable—&lt;strong&gt;write them once, run them anywhere.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Testing</category>
<guid isPermaLink="true">https://coderlegion.com/8455/twd-1-3-x-release-multiple-framework-support</guid>
<pubDate>Sun, 14 Dec 2025 11:23:48 +0000</pubDate>
</item>
</channel>
</rss>