557 lines
12 KiB
TypeScript
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
|
|
}
|