DEV Community

Testing HTMX apps in the real browser with TWD

HTMX apps are refreshingly simple: the server returns HTML fragments, and HTMX swaps them into the page in response to hx-get, hx-post, 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.

twd-js 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.

TWD sidebar running tests inside an HTMX app

Setup: load both from a CDN

Load HTMX as usual, then add an import map for TWD next to it. esm.sh resolves TWD's internal dependency, so there is nothing else to wire up.

<script src="https://unpkg.com/htmx.org@2.0.10/dist/htmx.min.js"></script>
<script type="importmap">
  {
    "imports": {
      "twd-js": "https://esm.sh/twd-js@1.8.2",
      "twd-js/runner": "https://esm.sh/twd-js@1.8.2/runner",
      "twd-js/bundled": "https://esm.sh/twd-js@1.8.2/bundled"
    }
  }
</script>
Enter fullscreen mode Exit fullscreen mode

Your markup stays pure HTMX:

<button hx-get="/api/todos" hx-target="#todo-list" hx-swap="innerHTML">Load todos</button>
<ul id="todo-list"></ul>
Enter fullscreen mode Exit fullscreen mode

Test against the real backend

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 Nuxt example uses for real-backend testing.

A tiny dev-only reset endpoint gives every test a clean starting point:

import { twd, userEvent, screenDom } from 'twd-js';
import { describe, it, beforeEach } from 'twd-js/runner';

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

  it('loads the seeded todos', async () => {
    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');
  });
});
Enter fullscreen mode Exit fullscreen mode

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 findBy* (which retries until the element appears) instead of the synchronous getBy*.

A note on mocking

TWD's service worker will happily intercept HTMX's requests, the same as any fetch or XHR. Today twd.mockRequest 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.

Try it

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 twd-htmx. 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.

Top comments (1)

Collapse
 
topstar_ai profile image
Luis Cruz

I appreciate how the article highlights the importance of testing HTMX apps in the real browser, as the simplicity of HTMX can make traditional testing tools feel overly complex. The use of TWD to run tests inside the real browser, as shown in the example with the todo list, seems like a great approach. The advice to use findBy* instead of getBy* when querying for elements that are swapped in asynchronously by HTMX is particularly valuable, as it ensures that tests wait for the elements to appear before attempting to interact with them. Have you considered exploring how TWD could be used to test more complex HTMX applications, such as those that involve multiple async requests or nested HTML fragments?