DOM Testing with Happy DOM and Testing Library

Better-faster testing of rendered pages using a DOM library and accessibility-oriented assertions.

We now 11ty development using tooling-friendly TS and TSX, for those that prefer such things. We also have testing with the super-cool Vitest.

Our test right now asserts a string. We're going to want richer testing. Let's hook up Happy DOM as a fake web browser and Testing Library for role-based assertions.

Why? As React and Jest showed with jsdom, it can be very convenient to do unit test development, 100% in Node.js and the IDE, without having a full browser application involved. This gives a very fast response cycle when combined with:

  • Vitest watch mode
  • Vitest's change detection which only re-runs needed tests
  • Running Vitest under WebStorm debugger to easily poke around

Let's see this in practice. First, over to package.json to add two dependencies -- Happy-DOM and Testing Library:

{
	"name": "eleventy-tsx",
	"version": "1.0.0",
	"description": "Demo of Eleventy 3, ESM, and TS/TSX",
	"scripts": {
		"build": "tsx node_modules/@11ty/eleventy/cmd.cjs --config=eleventy.config.ts",
		"start": "tsx node_modules/@11ty/eleventy/cmd.cjs --config=eleventy.config.ts --serve --incremental",
		"test": "vitest run"
	},
	"keywords": [],
	"author": "Paul Everitt <pauleveritt@me.com>",
	"license": "ISC",
	"type": "module",
	"dependencies": {
		"@11ty/eleventy": "^3.0.0-alpha.6",
		"jsx-async-runtime": "^0.1.8"
	},
	"devDependencies": {
		"@testing-library/dom": "^10.0.0",
		"happy-dom": "^14.7.1",
		"prettier": "^3.2.5",
		"tsx": "^4.7.0",
		"vitest": "^1.5.0"
	}
}

Our vitest.config.js file needs to be told to use Happy-DOM as the global document. Do this by adding an environment entry in test:

import { defineConfig } from "vitest/config";

export default defineConfig({
	esbuild: {
		jsx: "transform",
		jsxInject: "import { jsx } from 'jsx-async-runtime/jsx-runtime'",
		jsxFactory: "jsx",
		jsxImportSource: "jsx-async-runtime",
	},
	test: {
		environment: "happy-dom",
		include: ["./site/**/*.test.tsx"],
	},
});

Our index.test.tsx file can now do a real DOM with the Testing Library approach to assertions:

import { expect, test } from "vitest";
import { renderToString } from "jsx-async-runtime";
import { Index } from "./index.11ty";
import { screen } from "@testing-library/dom";

test("render index", async () => {
	const result = <Index />;
	document.body.innerHTML = await renderToString(result);
	expect(screen.getByText("Hello TSX")).toBeTruthy();
});

Testing Library's philosophy and Happy-DOM's machinery make a great combination. It feels like you have Playwright or Selenium in a real browser. But you're still in the speed and philosophy of unit tests.

With the infrastructure in place, we'll start reworking our site and add some more components in the next step.