Kaynağa Gözat

RHL-022 feat(breadcrumbs): add normalizeNumericOptions and buildExplorerDropdownItems functions with tests

Code_Uwe 4 hafta önce
ebeveyn
işleme
869e02e242

+ 107 - 0
lib/frontend/explorer/breadcrumbDropdowns.js

@@ -0,0 +1,107 @@
+import {
+	branchPath,
+	yearPath,
+	monthPath,
+	dayPath,
+} from "@/lib/frontend/routes";
+import { formatMonthLabel } from "@/lib/frontend/explorer/formatters";
+import { sortNumericStringsDesc } from "@/lib/frontend/explorer/sorters";
+
+/**
+ * normalizeNumericOptions
+ *
+ * Pure helper:
+ * - Ensures array input
+ * - Filters empty values
+ * - Deduplicates
+ * - Sorts descending (latest first) for numeric strings
+ *
+ * @param {string[]|null|undefined} options
+ * @returns {string[]|null}
+ */
+export function normalizeNumericOptions(options) {
+	if (!Array.isArray(options) || options.length === 0) return null;
+
+	const unique = Array.from(
+		new Set(options.map((x) => String(x)).filter(Boolean))
+	);
+	if (unique.length === 0) return null;
+
+	return sortNumericStringsDesc(unique);
+}
+
+/**
+ * buildExplorerDropdownItems
+ *
+ * Builds dropdown menu items for year/month/day breadcrumb segments.
+ * This function is intentionally pure and React-free to keep it testable.
+ *
+ * @param {{
+ *   branch: string,
+ *   year?: string|null,
+ *   month?: string|null,
+ *   day?: string|null,
+ *   yearOptions?: string[]|null,
+ *   monthOptions?: string[]|null,
+ *   dayOptions?: string[]|null
+ * }} input
+ * @returns {{
+ *   yearItems: Array<{ value: string, label: string, href: string }> | null,
+ *   monthItems: Array<{ value: string, label: string, href: string }> | null,
+ *   dayItems: Array<{ value: string, label: string, href: string }> | null
+ * }}
+ */
+export function buildExplorerDropdownItems({
+	branch,
+	year = null,
+	month = null,
+	day = null,
+	yearOptions = null,
+	monthOptions = null,
+	dayOptions = null,
+}) {
+	const years = normalizeNumericOptions(yearOptions);
+	const months = normalizeNumericOptions(monthOptions);
+	const days = normalizeNumericOptions(dayOptions);
+
+	const yearItems =
+		year && years
+			? years.map((y) => ({
+					value: y,
+					label: y,
+					href: yearPath(branch, y),
+			  }))
+			: null;
+
+	const monthItems =
+		year && month && months
+			? months.map((m) => ({
+					value: m,
+					label: formatMonthLabel(m),
+					href: monthPath(branch, year, m),
+			  }))
+			: null;
+
+	const dayItems =
+		year && month && day && days
+			? days.map((d) => ({
+					value: d,
+					label: d,
+					href: dayPath(branch, year, month, d),
+			  }))
+			: null;
+
+	return { yearItems, monthItems, dayItems };
+}
+
+/**
+ * buildBranchCrumbHref
+ *
+ * Tiny helper to keep the breadcrumb component simple.
+ *
+ * @param {string} branch
+ * @returns {string}
+ */
+export function buildBranchCrumbHref(branch) {
+	return branchPath(branch);
+}

+ 69 - 0
lib/frontend/explorer/breadcrumbDropdowns.test.js

@@ -0,0 +1,69 @@
+/* @vitest-environment node */
+
+import { describe, it, expect } from "vitest";
+import {
+	normalizeNumericOptions,
+	buildExplorerDropdownItems,
+} from "./breadcrumbDropdowns";
+
+describe("lib/frontend/explorer/breadcrumbDropdowns", () => {
+	describe("normalizeNumericOptions", () => {
+		it("returns null for non-arrays or empty arrays", () => {
+			expect(normalizeNumericOptions(null)).toBe(null);
+			expect(normalizeNumericOptions(undefined)).toBe(null);
+			expect(normalizeNumericOptions([])).toBe(null);
+		});
+
+		it("deduplicates and sorts numeric strings descending", () => {
+			expect(normalizeNumericOptions(["1", "10", "2", "2", ""])).toEqual([
+				"10",
+				"2",
+				"1",
+			]);
+		});
+	});
+
+	describe("buildExplorerDropdownItems", () => {
+		it("builds year items only when year is present", () => {
+			const res = buildExplorerDropdownItems({
+				branch: "NL01",
+				year: "2024",
+				yearOptions: ["2023", "2024", "2025"],
+			});
+
+			expect(res.yearItems?.[0]).toMatchObject({
+				value: "2025",
+				label: "2025",
+				href: "/NL01/2025",
+			});
+		});
+
+		it("builds month items with German labels", () => {
+			const res = buildExplorerDropdownItems({
+				branch: "NL01",
+				year: "2024",
+				month: "10",
+				monthOptions: ["01", "10"],
+			});
+
+			// October must become "Oktober (10)" per formatter
+			expect(res.monthItems?.find((x) => x.value === "10")?.label).toBe(
+				"Oktober (10)"
+			);
+		});
+
+		it("builds day items only when day is present", () => {
+			const res = buildExplorerDropdownItems({
+				branch: "NL01",
+				year: "2024",
+				month: "10",
+				day: "23",
+				dayOptions: ["01", "23"],
+			});
+
+			expect(res.dayItems?.find((x) => x.value === "23")?.href).toBe(
+				"/NL01/2024/10/23"
+			);
+		});
+	});
+});