Files
template-react/src/apps/frontend/lib/routes.test.ts
T
pastilhasandClaude Opus 5 c08dab4243 Initial commit: Bun · Hono · React SPA · Postgres/Drizzle template
The starting point for an app with a React frontend. Same server, env,
database and API skeleton as template-htmx — the two differ only in what is
served at / — with a React 19 SPA, Tailwind v4 and shadcn/ui on top.

Carried from a working project rather than designed in the abstract, and
carrying the lessons that cost it time:

- The dev script states NODE_ENV=development rather than inheriting it. React
  resolves its runtime against NODE_ENV: Bun emits jsxDEV() in development
  and React's production jsx-dev-runtime defines jsxDEV as void 0. Inherit
  production while transforming for development and the app dies at first
  paint. Verified: the dev bundle resolves the development runtime and
  contains no void-0 stub.
- Asset URLs in components are relative and <base href="/"> is in the HTML,
  so a path-prefix proxy can rewrite one attribute and every asset follows. A
  test enforces it.
- Routing is in the hash, because the app may be mounted under a prefix it
  only learns at runtime and a hash means the same thing at either mount.
- index.html holds __PUBLIC_URL__ and gen-index.ts substitutes it, because
  Bun's HTML bundler fails the build on a root-relative favicon href.
- APP_ENV failing closed, env files before the ambient environment, the
  database enforced by name, one Postgres client per process — as in the
  htmx template.

The database has no tables. The App is a shell with one tab and no features,
named as a placeholder. Renaming `app` is the first step and the README lists
every place it appears.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 02:02:52 +00:00

48 lines
1.7 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { parseHash, TAB_LABELS, TABS } from "./routes";
describe("parseHash", () => {
test.each(["#home"])("reads %s", (hash) => {
expect(parseHash(hash).tab).toBe(hash.slice(1) as never);
});
// The hash a browser produces varies: some paths write `#/gallery`, and copied links arrive
// capitalised. All of them mean the same tab.
test.each(["#/home", "#Home", "home"])("normalises %s", (hash) => {
expect(parseHash(hash).tab).toBe("home");
});
// A refresh on the bare URL, and any hash left over from elsewhere, land somewhere valid rather
// than rendering nothing.
test.each(["", "#", "#nope", "#/../etc"])("falls back to home for %p", (hash) => {
expect(parseHash(hash).tab).toBe("home");
});
});
describe("nested routes", () => {
test("a collection id rides along in param", () => {
const route = parseHash("#home/0004853c-e097-4af9-b40b-0666594703cf");
expect(route.tab).toBe("home");
expect(route.param).toBe("0004853c-e097-4af9-b40b-0666594703cf");
});
test("a bare tab has no param", () => expect(parseHash("#home").param).toBeUndefined());
// A trailing slash is what you get from hand-editing the address bar.
test("a trailing slash is not a param", () => expect(parseHash("#home/").param).toBeUndefined());
test("an unknown tab with a param still falls back", () => {
expect(parseHash("#nope/123").tab).toBe("home");
});
});
describe("tabs", () => {
test("every tab has a label", () => {
for (const tab of TABS) expect(TAB_LABELS[tab]).toBeTruthy();
});
test("the hash written for a tab parses back to it", () => {
for (const tab of TABS) expect(parseHash(`#${tab}`).tab).toBe(tab);
});
});