Files
web/apps/scandic-web/utils/searchParams.test.ts
2025-06-30 09:49:30 +02:00

557 lines
12 KiB
TypeScript

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<string, any> = {}
for (const [key, value] of searchParams.entries()) {
obj[key] = value
}
return obj
}