test(timeline): add minimal jsdom/testing-library DOM net

Phase 2 (b)-lite, slice 1: establish a thin DOM test lane so the upcoming
virtualized scroll rewrite ships with automated coverage, not just manual checks.

The repo's test runner is `node --test` over `*.test.mjs` with no jsdom and no
JSX transform, so component rendering wasn't testable. This adds a second lane
that coexists with the existing pure-logic lane:

- esbuild `load` hook transforms .ts/.tsx (TS + JSX, automatic React runtime)
- global-jsdom installs DOM globals via `--import`
- @testing-library/react + /dom for render/query
- `pnpm test` now runs both lanes (test:unit + test:dom)
- DOM tests are `*.dom.test.tsx`

Deliberately thin: jsdom has no layout engine, so it does not cover
react-virtual's real measurement/scroll math — those stay on the
manual/visual verification pass (see FEASIBILITY.md). One smoke test proves
the net is wired.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
2026-06-15 19:16:00 -07:00
co-authored by Taylor Ho
parent 606dfac37e
commit fc2302b5e4
5 changed files with 837 additions and 12 deletions
+8 -1
View File
@@ -11,7 +11,9 @@
"lint": "biome lint .",
"check": "biome check . && pnpm check:file-sizes",
"format": "biome format --write .",
"test": "node --import ./test-loader.mjs --experimental-strip-types --test 'src/**/*.test.mjs'",
"test": "pnpm test:unit && pnpm test:dom",
"test:unit": "node --import ./test-loader.mjs --experimental-strip-types --test 'src/**/*.test.mjs'",
"test:dom": "node --import ./test-dom-setup.mjs --test 'src/**/*.dom.test.tsx'",
"preview": "vite preview",
"tauri": "tauri",
"test:e2e": "pnpm build && playwright test",
@@ -83,9 +85,14 @@
"@tanstack/router-plugin": "^1.167.12",
"@tanstack/virtual-file-routes": "^1.161.7",
"@tauri-apps/cli": "~2.11",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^6.0.0",
"esbuild": "^0.28.1",
"global-jsdom": "^29.0.0",
"jsdom": "^29.1.1",
"nostr-tools": "^2.23.3",
"postcss": "^8.5.8",
"tailwindcss": "^4.3.0",
+18
View File
@@ -0,0 +1,18 @@
import assert from "node:assert/strict";
import { afterEach, test } from "node:test";
import { cleanup, render, screen } from "@testing-library/react";
afterEach(cleanup);
// Smoke test that proves the (b)-lite DOM net is wired: jsdom globals exist,
// esbuild transforms TSX, React renders into the document, and Testing Library
// can query it. The real virtualized-scroll DOM tests build on this lane.
function Greeting({ name }: { name: string }) {
return <p>Hello, {name}!</p>;
}
test("DOM harness: jsdom + esbuild + testing-library render a component", () => {
render(<Greeting name="Springfield" />);
assert.ok(screen.getByText("Hello, Springfield!"));
});
+70
View File
@@ -0,0 +1,70 @@
// DOM test loader — extends the pure-logic resolver in `test-loader-hooks.mjs`
// with a `load` hook so JSX/TSX components can be rendered under jsdom.
//
// The default `pnpm test` lane (node --test over *.test.mjs) runs pure logic and
// imports .ts via `--experimental-strip-types`, which strips types but does NOT
// transform JSX. The DOM lane needs real component rendering, so it routes .ts
// and .tsx through esbuild (TS + JSX -> JS, automatic React runtime). This keeps
// the net thin: one transform dep, no second test runner.
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { transform } from "esbuild";
const srcRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"src",
);
const repoRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
// Extensionless `@/` and relative specifiers resolve .tsx first (components),
// then fall back to .ts (logic) — so both lanes of source resolve under jsdom.
export function resolve(specifier, context, nextResolve) {
if (specifier === "@features-manifest") {
const resolved = path.join(repoRoot, "preview-features.json");
return nextResolve(resolved, context);
}
if (specifier.startsWith("@/")) {
const stripped = specifier.slice(2);
if (path.extname(stripped)) {
return nextResolve(`${srcRoot}/${stripped}`, context);
}
return nextResolve(`${srcRoot}/${stripped}.tsx`, context).catch(() =>
nextResolve(`${srcRoot}/${stripped}.ts`, context),
);
}
if (
(specifier.startsWith("./") || specifier.startsWith("../")) &&
!path.extname(specifier) &&
context.parentURL
) {
const tsx = new URL(`${specifier}.tsx`, context.parentURL).href;
return nextResolve(tsx, context).catch(() =>
nextResolve(new URL(`${specifier}.ts`, context.parentURL).href, context),
);
}
return nextResolve(specifier, context);
}
export async function load(url, context, nextLoad) {
if (url.endsWith(".ts") || url.endsWith(".tsx")) {
const filename = fileURLToPath(url);
const source = await readFile(filename, "utf8");
const { code } = await transform(source, {
loader: url.endsWith(".tsx") ? "tsx" : "ts",
jsx: "automatic",
format: "esm",
target: "es2022",
sourcemap: "inline",
sourcefile: filename,
});
return { format: "module", source: code, shortCircuit: true };
}
return nextLoad(url, context);
}
+15
View File
@@ -0,0 +1,15 @@
// DOM test bootstrap for the (b)-lite net. Imported via `--import` so it runs
// before any test module loads:
// 1. install a jsdom `window`/`document`/etc. into globalThis (global-jsdom)
// 2. register the JSX/TSX-transforming module loader
//
// Scope is deliberately thin: just enough DOM to render React components and
// exercise the virtualized scroll path. jsdom has no layout engine, so it does
// NOT cover react-virtual's real measurement/scroll math — those stay on the
// manual/visual verification pass. See FEASIBILITY.md.
import "global-jsdom/register";
import { register } from "node:module";
register("./test-dom-loader-hooks.mjs", import.meta.url);
+726 -11
View File
File diff suppressed because it is too large Load Diff