Skip to content

Commit f1c13a0

Browse files
test(router-core): add search param serialization / deserialization unit tests (#4987)
Just some tests for `defaultParseSearch` and `defaultStringifySearch`, because they're used a lot and are very public and can handle user inputs (as in final human users, not devs). These tests are just documentation of the current behavior. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Added a comprehensive test suite for URL query-string serialization/deserialization covering empty values, primitives, complex/nested structures, special characters, circular/self-references, and unusual inputs (e.g., Number/String objects, Promise, Date). * Verifies round-trip isomorphism and behavior when parsing externally sourced or malformed query strings. * No public API changes and no user-facing changes. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent f2190d7 commit f1c13a0

File tree

1 file changed

+101
-0
lines changed

1 file changed

+101
-0
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { describe, expect, test } from 'vitest'
2+
import { defaultParseSearch, defaultStringifySearch } from '../src'
3+
4+
describe('Search Params serialization and deserialization', () => {
5+
/*
6+
* JSON-compatible objects can be serialized into a string,
7+
* and then deserialized back into the original object.
8+
*/
9+
test.each([
10+
[{}, ''],
11+
[{ foo: '' }, '?foo='],
12+
[{ foo: 'bar' }, '?foo=bar'],
13+
[{ foo: 'bar baz' }, '?foo=bar+baz'],
14+
[{ foo: 123 }, '?foo=123'],
15+
[{ foo: '123' }, '?foo=%22123%22'],
16+
[{ foo: true }, '?foo=true'],
17+
[{ foo: 'true' }, '?foo=%22true%22'],
18+
[{ foo: null }, '?foo=null'],
19+
[{ foo: 'null' }, '?foo=%22null%22'],
20+
[{ foo: 'undefined' }, '?foo=undefined'],
21+
[{ foo: {} }, '?foo=%7B%7D'],
22+
[{ foo: '{}' }, '?foo=%22%7B%7D%22'],
23+
[{ foo: [] }, '?foo=%5B%5D'],
24+
[{ foo: '[]' }, '?foo=%22%5B%5D%22'],
25+
[{ foo: [1, 2, 3] }, '?foo=%5B1%2C2%2C3%5D'],
26+
[{ foo: '1,2,3' }, '?foo=1%2C2%2C3'],
27+
[{ foo: { bar: 'baz' } }, '?foo=%7B%22bar%22%3A%22baz%22%7D'],
28+
[{ 0: 1 }, '?0=1'],
29+
[{ 'foo=bar': 1 }, '?foo%3Dbar=1'],
30+
[{ '{}': 1 }, '?%7B%7D=1'],
31+
[{ '': 1 }, '?=1'],
32+
[{ '=': '=' }, '?%3D=%3D'],
33+
[{ '=': '', '': '=' }, '?%3D=&=%3D'],
34+
[{ 'foo=2&bar': 3 }, '?foo%3D2%26bar=3'],
35+
[{ 'foo?': 1 }, '?foo%3F=1'],
36+
[{ foo: 'bar=' }, '?foo=bar%3D'],
37+
[{ foo: '2&bar=3' }, '?foo=2%26bar%3D3'],
38+
])('isomorphism %j', (input, expected) => {
39+
const str = defaultStringifySearch(input)
40+
expect(str).toEqual(expected)
41+
expect(defaultParseSearch(str)).toEqual(input)
42+
})
43+
44+
test('undefined values are removed during stringification', () => {
45+
const str = defaultStringifySearch({ foo: 'bar', bar: undefined })
46+
expect(str).toEqual('?foo=bar')
47+
expect(defaultParseSearch(str)).toEqual({ foo: 'bar' })
48+
})
49+
50+
test('[edge case] self-reference serializes to "object Object"', () => {
51+
const obj = {} as any
52+
obj.self = obj
53+
const str = defaultStringifySearch(obj)
54+
expect(str).toEqual('?self=%5Bobject+Object%5D')
55+
expect(defaultParseSearch(str)).toEqual({ self: '[object Object]' })
56+
})
57+
58+
/*
59+
* It is able to parse strings that could not have come
60+
* from the serializer.
61+
*
62+
* This can be useful because search params can be manipulated
63+
* by human users.
64+
*/
65+
test.each([
66+
['?foo={}', { foo: {} }],
67+
['?foo=[]', { foo: [] }],
68+
['?foo=1,2,3', { foo: '1,2,3' }],
69+
['?foo={"bar":"baz"}', { foo: { bar: 'baz' } }],
70+
['?foo=1&foo=2', { foo: [1, 2] }],
71+
['?foo=""', { foo: '' }],
72+
['?foo=""""', { foo: '""""' }],
73+
['?foo=()', { foo: '()' }],
74+
['?foo=[{}]', { foo: [{}] }],
75+
])('alien deserialization %s', (input, expected) => {
76+
const obj = defaultParseSearch(input)
77+
expect(obj).toEqual(expected)
78+
expect(defaultStringifySearch(obj)).not.toBe(input)
79+
})
80+
81+
/*
82+
* It can serialize stuff that really shouldn't be passed as input.
83+
* But just in case, this test serves as documentation of "what would happen"
84+
* if you did.
85+
*/
86+
test('[edge case] inputs that are not primitive objects', () => {
87+
expect(defaultStringifySearch(new Number(99))).toEqual('')
88+
expect(defaultStringifySearch({ foo: new Number(99) })).toEqual('?foo=99')
89+
expect(defaultStringifySearch(new String('foo'))).toEqual('?0=f&1=o&2=o')
90+
expect(defaultStringifySearch(new Promise(() => {}))).toEqual('')
91+
expect(defaultStringifySearch({ foo: new Promise(() => {}) })).toEqual(
92+
'?foo=%7B%7D',
93+
)
94+
expect(defaultStringifySearch([1])).toEqual('?0=1')
95+
const date = new Date('2024-11-18')
96+
expect(defaultStringifySearch(date)).toEqual('')
97+
expect(defaultStringifySearch({ foo: date })).toEqual(
98+
'?foo=%222024-11-18T00%3A00%3A00.000Z%22',
99+
)
100+
})
101+
})

0 commit comments

Comments
 (0)