UNPKG

@wroud/navigation

Version:

A flexible, pattern-matching navigation system for JavaScript applications with built-in routing, browser integration, and navigation state management

292 lines (241 loc) 10.5 kB
import { describe, test, expect, beforeEach } from "vitest"; import { TriePatternMatching } from "./TriePatternMatching.js"; import type { ExtractRouteParams } from "./types.js"; describe("Date and JSON Parameter Support", () => { let patternMatcher: TriePatternMatching; beforeEach(() => { patternMatcher = new TriePatternMatching({ trailingSlash: false }); }); describe("Date parameter types", () => { test("should encode and decode Date parameters", () => { const pattern = "/events/:date<date>"; const testDate = new Date("2024-01-15T10:30:00.000Z"); patternMatcher.addPattern(pattern); // Test encoding Date to URL const url = patternMatcher.encode(pattern, { date: testDate }); expect(url).toBe("/events/2024-01-15T10:30:00.000Z"); // Test decoding URL back to Date const decoded = patternMatcher.decode(pattern, url); expect(decoded).not.toBeNull(); expect(decoded!.date).toBeInstanceOf(Date); expect(decoded!.date.getTime()).toBe(testDate.getTime()); }); test("should handle Date parameters in wildcard arrays", () => { const pattern = "/schedule/:dates<date>*"; const testDates = [ new Date("2024-01-15T10:30:00.000Z"), new Date("2024-02-20T14:45:00.000Z"), new Date("2024-03-10T09:15:00.000Z"), ]; patternMatcher.addPattern(pattern); // Test encoding Date array to URL const url = patternMatcher.encode(pattern, { dates: testDates }); expect(url).toBe( "/schedule/2024-01-15T10:30:00.000Z/2024-02-20T14:45:00.000Z/2024-03-10T09:15:00.000Z", ); // Test decoding URL back to Date array const decoded = patternMatcher.decode(pattern, url); expect(decoded).not.toBeNull(); expect(Array.isArray(decoded!.dates)).toBe(true); const decodedDates = decoded!.dates; expect(decodedDates).toHaveLength(3); decodedDates.forEach((date, index) => { expect(date).toBeInstanceOf(Date); expect(date.getTime()).toBe(testDates[index]!.getTime()); }); }); test("should handle invalid Date strings", () => { const pattern = "/events/:date<date>"; patternMatcher.addPattern(pattern); // Should throw error for invalid date string expect(() => { patternMatcher.decode(pattern, "/events/invalid-date-string"); }).toThrow(/Invalid date/); }); test("should validate Date parameter types during encoding", () => { const pattern = "/events/:date<date>"; patternMatcher.addPattern(pattern); // Should throw error when trying to encode non-Date value expect(() => { patternMatcher.encode(pattern, { date: "not-a-date" as any }); }).toThrow(/not of type 'date'/); }); }); describe("JSON parameter types", () => { test("should encode and decode JSON object parameters", () => { const pattern = "/api/:config<json>"; const testConfig = { enabled: true, timeout: 5000, endpoints: ["api1", "api2"], metadata: { version: "1.0", author: "test" }, }; patternMatcher.addPattern(pattern); // Test encoding JSON object to URL const url = patternMatcher.encode(pattern, { config: testConfig }); expect(url).toBe(`/api/${JSON.stringify(testConfig)}`); // Test decoding URL back to JSON object const decodedUrl = `/api/${JSON.stringify(testConfig)}`; const decoded = patternMatcher.decode(pattern, decodedUrl); expect(decoded).not.toBeNull(); expect(decoded!.config).toEqual(testConfig); }); test("should handle JSON parameters in wildcard arrays", () => { const pattern = "/configs/:settings<json>*"; const testSettings = [ { theme: "dark", fontSize: 14 }, { lang: "en", region: "US" }, { debug: true, level: "info" }, ]; patternMatcher.addPattern(pattern); // Test encoding JSON array to URL const url = patternMatcher.encode(pattern, { settings: testSettings }); const expectedSegments = testSettings .map((s) => JSON.stringify(s)) .join("/"); expect(url).toBe(`/configs/${expectedSegments}`); // Test decoding URL back to JSON array const decodedUrl = `/configs/${testSettings.map((s) => JSON.stringify(s)).join("/")}`; const decoded = patternMatcher.decode(pattern, decodedUrl); expect(decoded).not.toBeNull(); expect(Array.isArray(decoded!.settings)).toBe(true); expect(decoded!.settings).toEqual(testSettings); }); test("should handle invalid JSON strings", () => { const pattern = "/api/:config<json>"; patternMatcher.addPattern(pattern); // Should throw error for invalid JSON string expect(() => { patternMatcher.decode(pattern, "/api/invalid-json-{"); }).toThrow(/Invalid JSON/); }); test("should validate JSON parameter types during encoding", () => { const pattern = "/api/:config<json>"; patternMatcher.addPattern(pattern); // Should throw error when trying to encode non-object value expect(() => { patternMatcher.encode(pattern, { config: "not-an-object" as any }); }).toThrow(/not of type 'json'/); // Should throw error for Date objects (they should use date type) expect(() => { patternMatcher.encode(pattern, { config: new Date() }); }).toThrow(/not of type 'json'/); // Should throw error for null values expect(() => { patternMatcher.encode(pattern, { config: null as any }); }).toThrow(/not of type 'json'/); }); }); describe("Mixed Date and JSON parameters", () => { test("should handle routes with both Date and JSON parameters", () => { const pattern = "/events/:date<date>/config/:settings<json>"; const testDate = new Date("2024-01-15T10:30:00.000Z"); const testSettings = { notifications: true, reminders: ["email", "sms"] }; patternMatcher.addPattern(pattern); // Test encoding const url = patternMatcher.encode(pattern, { date: testDate, settings: testSettings, }); expect(url).toBe( `/events/${testDate.toISOString()}/config/${JSON.stringify(testSettings)}`, ); // Test decoding const decodedUrl = `/events/${testDate.toISOString()}/config/${JSON.stringify(testSettings)}`; const decoded = patternMatcher.decode(pattern, decodedUrl); expect(decoded).not.toBeNull(); expect(decoded!.date).toBeInstanceOf(Date); expect(decoded!.date.getTime()).toBe(testDate.getTime()); expect(decoded!.settings).toEqual(testSettings); }); test("should handle complex nested objects with dates", () => { const pattern = "/reports/:metadata<json>"; const testMetadata = { title: "Annual Report", authors: ["John Doe", "Jane Smith"], createdAt: "2024-01-15T10:30:00.000Z", // Note: stored as string in JSON config: { format: "pdf", pages: 120, sections: ["intro", "data", "conclusion"], }, }; patternMatcher.addPattern(pattern); // Test round-trip const url = patternMatcher.encode(pattern, { metadata: testMetadata }); const decoded = patternMatcher.decode(pattern, url); expect(decoded).not.toBeNull(); expect(decoded!.metadata).toEqual(testMetadata); }); }); describe("Type safety with template literal types", () => { test("should infer correct types for Date parameters", () => { // Type inference test type DateParams = ExtractRouteParams<"/events/:date<date>">; // Should infer: { date: Date } const dateParams: DateParams = { date: new Date() }; expect(dateParams.date).toBeInstanceOf(Date); }); test("should infer correct types for JSON parameters", () => { // Type inference test type JsonParams = ExtractRouteParams<"/api/:config<json>">; // Should infer: { config: object } const jsonParams: JsonParams = { config: { key: "value" } }; expect(typeof jsonParams.config).toBe("object"); }); test("should infer correct types for wildcard Date parameters", () => { // Type inference test type WildcardDateParams = ExtractRouteParams<"/schedule/:dates<date>*">; // Should infer: { dates: Date[] } const wildcardParams: WildcardDateParams = { dates: [new Date(), new Date()], }; expect(Array.isArray(wildcardParams.dates)).toBe(true); wildcardParams.dates.forEach((date) => { expect(date).toBeInstanceOf(Date); }); }); }); describe("Edge cases", () => { test("should handle empty objects", () => { const pattern = "/api/:config<json>"; patternMatcher.addPattern(pattern); const emptyObj = {}; const url = patternMatcher.encode(pattern, { config: emptyObj }); const decoded = patternMatcher.decode(pattern, url); expect(decoded!.config).toEqual(emptyObj); }); test("should handle nested arrays and objects", () => { const pattern = "/data/:payload<json>"; const complexPayload = { users: [ { id: 1, name: "Alice", roles: ["admin", "user"] }, { id: 2, name: "Bob", roles: ["user"] }, ], metadata: { total: 2, filters: { active: true, department: "IT" }, timestamps: ["2024-01-01", "2024-01-02"], }, }; patternMatcher.addPattern(pattern); const url = patternMatcher.encode(pattern, { payload: complexPayload }); const decoded = patternMatcher.decode(pattern, url); expect(decoded!.payload).toEqual(complexPayload); }); test("should handle Date epoch boundaries", () => { const pattern = "/time/:timestamp<date>"; patternMatcher.addPattern(pattern); // Test Unix epoch const epochDate = new Date(0); const url = patternMatcher.encode(pattern, { timestamp: epochDate }); const decoded = patternMatcher.decode(pattern, url); expect(decoded!.timestamp.getTime()).toBe(0); // Test far future date const futureDate = new Date("2099-12-31T23:59:59.999Z"); const url2 = patternMatcher.encode(pattern, { timestamp: futureDate }); const decoded2 = patternMatcher.decode(pattern, url2); expect(decoded2!.timestamp.getTime()).toBe(futureDate.getTime()); }); }); });