import { describe, expect, test } from "vitest" import { z } from "zod" import { parseSearchParams, serializeSearchParams } from "./searchParams" describe("Parse search params", () => { test("with flat values", () => { const searchParams = getSearchParams("city=stockholm&hotel=123") const result = parseSearchParams(searchParams) expect(result).toEqual({ city: "stockholm", hotel: "123", }) }) test("with comma separated array", () => { const searchParams = getSearchParams( "filter=1831,1383,971,1607&packages=ABC,XYZ" ) const result = parseSearchParams(searchParams, { typeHints: { packages: "COMMA_SEPARATED_ARRAY", filter: "COMMA_SEPARATED_ARRAY", }, }) expect(result).toEqual({ filter: ["1831", "1383", "971", "1607"], packages: ["ABC", "XYZ"], }) }) test("with comma separated array with single value", () => { const searchParams = getSearchParams( "details.packages=ABC&filter=1831&rooms[0].packages=XYZ" ) const result = parseSearchParams(searchParams, { typeHints: { filter: "COMMA_SEPARATED_ARRAY", packages: "COMMA_SEPARATED_ARRAY", }, }) expect(result).toEqual({ filter: ["1831"], details: { packages: ["ABC"], }, rooms: [ { packages: ["XYZ"], }, ], }) }) test("with nested object", () => { const searchParams = getSearchParams( "room.details.adults=1&room.ratecode=ABC&room.details.children=2&room.filters=1,2,3,4" ) const result = parseSearchParams(searchParams, { typeHints: { filters: "COMMA_SEPARATED_ARRAY", }, }) expect(result).toEqual({ room: { ratecode: "ABC", filters: ["1", "2", "3", "4"], details: { adults: "1", children: "2", }, }, }) }) test("with array of objects", () => { const searchParams = getSearchParams( "room[0].adults=1&room[0].ratecode=ABC&room[1].adults=2&room[1].ratecode=DEF" ) const result = parseSearchParams(searchParams) expect(result).toEqual({ room: [ { adults: "1", ratecode: "ABC", }, { adults: "2", ratecode: "DEF", }, ], }) }) test("with array defined out of order", () => { const searchParams = getSearchParams("room[1].adults=1&room[0].adults=2") const result = parseSearchParams(searchParams) expect(result).toEqual({ room: [ { adults: "2", }, { adults: "1", }, ], }) }) test("with nested array of objects", () => { const searchParams = getSearchParams( "room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3" ) const result = parseSearchParams(searchParams) expect(result).toEqual({ room: [ { adults: "1", child: [ { age: "2", }, ], }, { adults: "2", child: [ { age: "3", }, ], }, ], }) }) test("can handle array syntax with primitive values", () => { const searchParams = getSearchParams("room[1]=1&room[0]=2") const result = parseSearchParams(searchParams) expect(result).toEqual({ room: ["2", "1"], }) }) test("can rename search param keys", () => { const searchParams = getSearchParams( "city=stockholm&hotel=123&room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3" ) const result = parseSearchParams(searchParams, { keyRenameMap: { hotel: "hotelId", room: "rooms", age: "childAge", }, }) expect(result).toEqual({ city: "stockholm", hotelId: "123", rooms: [ { adults: "1", child: [ { childAge: "2", }, ], }, { adults: "2", child: [ { childAge: "3", }, ], }, ], }) }) test("with schema validation", () => { const searchParams = getSearchParams( "room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3" ) const result = parseSearchParams(searchParams, { schema: z.object({ room: z.array( z.object({ adults: z.string(), child: z.array( z.object({ age: z.string(), }) ), }) ), }), }) expect(result).toEqual({ room: [ { adults: "1", child: [ { age: "2", }, ], }, { adults: "2", child: [ { age: "3", }, ], }, ], }) }) test("throws when schema validation fails", () => { const searchParams = getSearchParams("city=stockholm") expect(() => parseSearchParams(searchParams, { schema: z.object({ city: z.string(), hotel: z.string(), }), }) ).toThrow() }) test("with value coercion", () => { const searchParams = getSearchParams( "room[0].adults=1&room[0].enabled=true" ) const result = parseSearchParams(searchParams, { schema: z.object({ room: z.array( z.object({ adults: z.coerce.number(), enabled: z.coerce.boolean(), }) ), }), }) expect(result).toEqual({ room: [ { adults: 1, enabled: true, }, ], }) }) }) describe("Serialize search params", () => { test("with flat values", () => { const obj = { city: "stockholm", hotel: "123", } const result = serializeSearchParams(obj) expect(decodeURIComponent(result.toString())).toEqual( "city=stockholm&hotel=123" ) }) test("with comma separated array", () => { const obj = { filter: ["1831", "1383", "971", "1607"], } const result = serializeSearchParams(obj, { typeHints: { filter: "COMMA_SEPARATED_ARRAY", }, }) expect(decodeURIComponent(result.toString())).toEqual( "filter=1831,1383,971,1607" ) }) test("with comma separated array with single value", () => { const obj = { details: { packages: ["ABC"], }, filter: ["1831"], rooms: [ { packages: ["XYZ"], }, ], } const result = serializeSearchParams(obj, { typeHints: { filter: "COMMA_SEPARATED_ARRAY", packages: "COMMA_SEPARATED_ARRAY", }, }) expect(decodeURIComponent(result.toString())).toEqual( "details.packages=ABC&filter=1831&rooms[0].packages=XYZ" ) }) test("with nested object", () => { const obj = { room: { ratecode: "ABC", filters: ["1", "2", "3", "4"], details: { adults: "1", children: "2", }, }, } const result = serializeSearchParams(obj, { typeHints: { filters: "COMMA_SEPARATED_ARRAY", }, }) expect(decodeURIComponent(result.toString())).toEqual( "room.ratecode=ABC&room.filters=1,2,3,4&room.details.adults=1&room.details.children=2" ) }) test("with array of objects", () => { const obj = { room: [ { adults: "1", ratecode: "ABC", }, { adults: "2", ratecode: "DEF", }, ], } const result = serializeSearchParams(obj) expect(decodeURIComponent(result.toString())).toEqual( "room[0].adults=1&room[0].ratecode=ABC&room[1].adults=2&room[1].ratecode=DEF" ) }) test("with nested array of objects", () => { const obj = { room: [ { adults: "1", child: [ { age: "2", }, ], }, { adults: "2", child: [ { age: "3", }, ], }, ], } const result = serializeSearchParams(obj) expect(decodeURIComponent(result.toString())).toEqual( "room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3" ) }) test("can handle array syntax with primitive values", () => { const obj = { room: ["2", "1"], } const result = serializeSearchParams(obj) expect(decodeURIComponent(result.toString())).toEqual("room[0]=2&room[1]=1") }) test("can rename search param keys", () => { const obj = { city: "stockholm", hotelId: "123", rooms: [ { adults: "1", child: [ { childAge: "2", }, ], }, { adults: "2", child: [ { childAge: "3", }, ], }, ], } const result = serializeSearchParams(obj, { keyRenameMap: { hotelId: "hotel", rooms: "room", childAge: "age", }, }) expect(decodeURIComponent(result.toString())).toEqual( "city=stockholm&hotel=123&room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3" ) }) test("with initial search params", () => { const initialSearchParams = new URLSearchParams("city=stockholm&hotel=123") const obj = { hotel: "456", filter: ["1831", "1383"], packages: ["ABC"], } const result = serializeSearchParams(obj, { initialSearchParams, typeHints: { packages: "COMMA_SEPARATED_ARRAY", }, }) expect(decodeURIComponent(result.toString())).toEqual( "city=stockholm&hotel=456&filter[0]=1831&filter[1]=1383&packages=ABC" ) }) test("with initial search params and removing existing parameter", () => { const initialSearchParams = new URLSearchParams( "city=stockholm&hotel=123&filters=123,456,789" ) const obj = { hotel: null, filters: ["123", "789"], } const result = serializeSearchParams(obj, { initialSearchParams, typeHints: { filters: "COMMA_SEPARATED_ARRAY", }, }) expect(decodeURIComponent(result.toString())).toEqual( "city=stockholm&filters=123,789" ) }) test("with initial search params and removing values in array", () => { const initialSearchParams = new URLSearchParams( "room[0].adults=1&room[0].rateCode=ABC&room[1].adults=2&room[2].adults=3&room[3].adults=4" ) const obj = { room: [ { adults: 1, rateCode: null, }, { adults: 3, }, { adults: 4, }, ], } const result = serializeSearchParams(obj, { initialSearchParams, }) expect(decodeURIComponent(result.toString())).toEqual( "room[0].adults=1&room[1].adults=3&room[2].adults=4" ) }) }) describe("Parse serialized search params", () => { test("should return the same object", () => { const obj = { city: "stockholm", hotelId: "123", filter: ["1831", "1383", "971", "1607"], details: { packages: ["ABC"], }, rooms: [ { packages: ["XYZ"], }, ], } const searchParams = serializeSearchParams(obj, { keyRenameMap: { hotelId: "hotel", rooms: "room", }, typeHints: { filter: "COMMA_SEPARATED_ARRAY", packages: "COMMA_SEPARATED_ARRAY", }, }) const searchParamsObj = searchParamsToObject(searchParams) const result = parseSearchParams(searchParamsObj, { keyRenameMap: { hotel: "hotelId", room: "rooms", }, typeHints: { filter: "COMMA_SEPARATED_ARRAY", packages: "COMMA_SEPARATED_ARRAY", }, }) expect(result).toEqual(obj) }) }) // Simulates what Next does behind the scenes for search params const getSearchParams = (input: string) => { const searchParams = new URLSearchParams(input) return searchParamsToObject(searchParams) } const searchParamsToObject = (searchParams: URLSearchParams) => { const obj: Record = {} for (const [key, value] of searchParams.entries()) { obj[key] = value } return obj }