Files
Calendar_Frontend/lib/url-state.test.ts
PaddyandClaude Opus 5.5 0cda40f85e Rewrite the calendar frontend in Next.js
Replaces the Angular 18 app with Next.js 16 / React 19 / Tailwind 4 / shadcn,
the same stack and design system as nachklang-feedback, planned in
docs/rewrite-plan.md.

- Admin: all calendars in one mobile-friendly list (month groups, calendar
  chips, Kommend/Vergangen/Alle, search, sort, filters in the URL), an edit
  sheet for create/edit/duplicate/delete, publish confirmation, and an
  "Abonnieren" dialog with subscription links for every calendar.
- Public landing page: subscribe links from env instead of hardcoded, plus
  the next public events, server-rendered and revalidated every 5 minutes.
- All times handled explicitly as Europe/Berlin; whole-day events stored as
  00:00-23:59. Fixes carried over from the Angular app: the move dialog
  leaking a cancelled calendar change, saves that looked saved when they
  failed, in-place Date mutation, and duplicated calendar-id mappings.
- Same shared-session auth as before (proxy.ts gate, /admin/me, 401 vs 403).
- 112 Vitest tests over lib/; docs in docs/ (architecture, features,
  gotchas, API wish list, deployment).

Needs the API branch feature/calendar-ical-escaping-subscriptions deployed
first (GET /calendar/events/subscriptions).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:15:59 +02:00

77 lines
2.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { DEFAULT_LIST_STATE, parseListState, serializeListState, toQueryString, withRange } from "@/lib/url-state";
const parse = (qs: string) => parseListState(new URLSearchParams(qs));
describe("parseListState", () => {
it("defaults to every calendar except Birthdays, upcoming, by start", () => {
expect(parse("")).toEqual({ calendarIds: [1, 2, 4, 3], range: "future", sort: "start_asc", query: "" });
});
it("reads calendars by slug, dropping unknown ones and duplicates", () => {
expect(parse("cal=choir,nope,birthdays,choir").calendarIds).toEqual([4, 5]);
});
it("keeps an explicitly empty selection", () => {
expect(parse("cal=").calendarIds).toEqual([]);
});
it("sorts the past newest first by default", () => {
expect(parse("range=past").sort).toBe("start_desc");
expect(parse("range=past&sort=start_asc").sort).toBe("start_asc");
});
it("falls back to defaults for unknown range and sort", () => {
expect(parse("range=soon&sort=random")).toMatchObject({ range: "future", sort: "start_asc" });
});
it("reads range, sort and search", () => {
expect(parse("range=past&sort=created_desc&q=probe")).toMatchObject({
range: "past",
sort: "created_desc",
query: "probe",
});
});
});
describe("serializeListState", () => {
it("leaves the default view out of the URL", () => {
expect(serializeListState(DEFAULT_LIST_STATE).toString()).toBe("");
});
it("treats the default calendars in another order as the default", () => {
expect(serializeListState({ ...DEFAULT_LIST_STATE, calendarIds: [3, 4, 2, 1] }).toString()).toBe("");
});
it("round-trips a non-default view", () => {
const state = { calendarIds: [5], range: "all" as const, sort: "start_desc" as const, query: "geburtstag" };
expect(parseListState(serializeListState(state))).toEqual(state);
});
it("writes an empty selection as cal=", () => {
expect(serializeListState({ ...DEFAULT_LIST_STATE, calendarIds: [] }).toString()).toBe("cal=");
});
});
describe("withRange", () => {
it("swaps a default sort for the new range's default, keeping the URL clean", () => {
const past = withRange(DEFAULT_LIST_STATE, "past");
expect(past.sort).toBe("start_desc");
expect(serializeListState(past).toString()).toBe("range=past");
expect(withRange(past, "all").sort).toBe("start_asc");
});
it("keeps a sort someone picked", () => {
const picked = { ...DEFAULT_LIST_STATE, sort: "created_desc" as const };
expect(withRange(picked, "past").sort).toBe("created_desc");
});
});
describe("toQueryString", () => {
it("keeps the calendar list readable but escapes commas in the search", () => {
const qs = toQueryString({ ...DEFAULT_LIST_STATE, calendarIds: [1, 4], query: "a,b" });
expect(qs).toBe("cal=public,choir&q=a%2Cb");
expect(parseListState(new URLSearchParams(qs))).toMatchObject({ calendarIds: [1, 4], query: "a,b" });
});
});