Quellcode durchsuchen

RHL-019 feat(routes): add route helpers and corresponding tests for navigation

Code_Uwe vor 1 Monat
Ursprung
Commit
dfba98fa23
2 geänderte Dateien mit 173 neuen und 0 gelöschten Zeilen
  1. 134 0
      lib/frontend/routes.js
  2. 39 0
      lib/frontend/routes.test.js

+ 134 - 0
lib/frontend/routes.js

@@ -0,0 +1,134 @@
+/**
+ * Frontend route helpers for internal navigation.
+ *
+ * Why this file exists:
+ * - Keep route building consistent across the UI (no stringly-typed URLs scattered everywhere).
+ * - Central place to adjust URL structure if we ever need it.
+ * - Easy to test in pure Node (no DOM / no Next runtime required).
+ *
+ * Notes:
+ * - We encode dynamic segments defensively via encodeURIComponent.
+ * - We validate inputs early to catch mistakes during development.
+ */
+
+/**
+ * Ensure a dynamic route segment is a non-empty string.
+ *
+ * @param {string} name - Human readable segment name for error messages.
+ * @param {unknown} value - The segment value.
+ * @returns {string} Trimmed segment.
+ */
+function requireSegment(name, value) {
+	if (typeof value !== "string") {
+		throw new Error(`Route segment "${name}" must be a string`);
+	}
+
+	const trimmed = value.trim();
+	if (!trimmed) {
+		throw new Error(`Route segment "${name}" must not be empty`);
+	}
+
+	return trimmed;
+}
+
+/**
+ * Encode a segment for safe usage in URLs.
+ *
+ * @param {string} name
+ * @param {unknown} value
+ * @returns {string}
+ */
+function encodeSegment(name, value) {
+	return encodeURIComponent(requireSegment(name, value));
+}
+
+/**
+ * Build an absolute path from named segments.
+ *
+ * @param {Array<{ name: string, value: unknown }>} parts
+ * @returns {string}
+ */
+function buildPath(parts) {
+	const encoded = parts.map((p) => encodeSegment(p.name, p.value));
+	return `/${encoded.join("/")}`;
+}
+
+/**
+ * /
+ */
+export function homePath() {
+	return "/";
+}
+
+/**
+ * /login
+ */
+export function loginPath() {
+	return "/login";
+}
+
+/**
+ * /:branch
+ *
+ * @param {string} branch
+ */
+export function branchPath(branch) {
+	return buildPath([{ name: "branch", value: branch }]);
+}
+
+/**
+ * /:branch/:year
+ *
+ * @param {string} branch
+ * @param {string} year
+ */
+export function yearPath(branch, year) {
+	return buildPath([
+		{ name: "branch", value: branch },
+		{ name: "year", value: year },
+	]);
+}
+
+/**
+ * /:branch/:year/:month
+ *
+ * @param {string} branch
+ * @param {string} year
+ * @param {string} month
+ */
+export function monthPath(branch, year, month) {
+	return buildPath([
+		{ name: "branch", value: branch },
+		{ name: "year", value: year },
+		{ name: "month", value: month },
+	]);
+}
+
+/**
+ * /:branch/:year/:month/:day
+ *
+ * @param {string} branch
+ * @param {string} year
+ * @param {string} month
+ * @param {string} day
+ */
+export function dayPath(branch, year, month, day) {
+	return buildPath([
+		{ name: "branch", value: branch },
+		{ name: "year", value: year },
+		{ name: "month", value: month },
+		{ name: "day", value: day },
+	]);
+}
+
+/**
+ * /:branch/search
+ *
+ * @param {string} branch
+ */
+export function searchPath(branch) {
+	return buildPath([
+		{ name: "branch", value: branch },
+		{ name: "search", value: "search" },
+	]);
+}

+ 39 - 0
lib/frontend/routes.test.js

@@ -0,0 +1,39 @@
+/* @vitest-environment node */
+
+import { describe, it, expect } from "vitest";
+import {
+	homePath,
+	loginPath,
+	branchPath,
+	yearPath,
+	monthPath,
+	dayPath,
+	searchPath,
+} from "./routes.js";
+
+describe("lib/frontend/routes", () => {
+	it("builds static paths", () => {
+		expect(homePath()).toBe("/");
+		expect(loginPath()).toBe("/login");
+	});
+
+	it("builds branch-based paths", () => {
+		expect(branchPath("NL01")).toBe("/NL01");
+		expect(yearPath("NL01", "2025")).toBe("/NL01/2025");
+		expect(monthPath("NL01", "2025", "12")).toBe("/NL01/2025/12");
+		expect(dayPath("NL01", "2025", "12", "31")).toBe("/NL01/2025/12/31");
+		expect(searchPath("NL01")).toBe("/NL01/search");
+	});
+
+	it("encodes dynamic segments defensively", () => {
+		expect(branchPath("NL 01")).toBe("/NL%2001");
+		expect(yearPath("NL01", "2025/evil")).toBe("/NL01/2025%2Fevil");
+	});
+
+	it("throws for invalid segments", () => {
+		expect(() => branchPath("")).toThrow(/must not be empty/i);
+		expect(() => yearPath("NL01", "")).toThrow(/year/i);
+		expect(() => monthPath("NL01", "2025", " ")).toThrow(/month/i);
+		expect(() => dayPath("NL01", "2025", "12", "")).toThrow(/day/i);
+	});
+});