Skip to content

Commit 8951598

Browse files
authored
fix: Support ? characters in serializer base (#821)
* test: Rename tests to follow `it('description')` convention * fix: Support ? characters in serializer base Closes #812.
1 parent b18c5b4 commit 8951598

File tree

2 files changed

+36
-23
lines changed

2 files changed

+36
-23
lines changed

packages/nuqs/src/serializer.test.ts

+34-21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, test } from 'vitest'
1+
import { describe, expect, it } from 'vitest'
22
import type { Options } from './defs'
33
import {
44
parseAsArrayOf,
@@ -16,91 +16,91 @@ const parsers = {
1616
}
1717

1818
describe('serializer', () => {
19-
test('empty', () => {
19+
it('handles empty inputs', () => {
2020
const serialize = createSerializer(parsers)
2121
const result = serialize({})
2222
expect(result).toBe('')
2323
})
24-
test('one item', () => {
24+
it('handles a single item', () => {
2525
const serialize = createSerializer(parsers)
2626
const result = serialize({ str: 'foo' })
2727
expect(result).toBe('?str=foo')
2828
})
29-
test('several items', () => {
29+
it('handles several items', () => {
3030
const serialize = createSerializer(parsers)
3131
const result = serialize({ str: 'foo', int: 1, bool: true })
3232
expect(result).toBe('?str=foo&int=1&bool=true')
3333
})
34-
test("null items don't show up", () => {
34+
it('does not render null items', () => {
3535
const serialize = createSerializer(parsers)
3636
const result = serialize({ str: null })
3737
expect(result).toBe('')
3838
})
39-
test('with string base', () => {
39+
it('handles a string base', () => {
4040
const serialize = createSerializer(parsers)
4141
const result = serialize('/foo', { str: 'foo' })
4242
expect(result).toBe('/foo?str=foo')
4343
})
44-
test('with string base with search params', () => {
44+
it('handles a string base with search params', () => {
4545
const serialize = createSerializer(parsers)
4646
const result = serialize('/foo?bar=egg', { str: 'foo' })
4747
expect(result).toBe('/foo?bar=egg&str=foo')
4848
})
49-
test('with URLSearchParams base', () => {
49+
it('handles a URLSearchParams base', () => {
5050
const serialize = createSerializer(parsers)
5151
const search = new URLSearchParams('?bar=egg')
5252
const result = serialize(search, { str: 'foo' })
5353
expect(result).toBe('?bar=egg&str=foo')
5454
})
55-
test('Does not mutate existing params with URLSearchParams base', () => {
55+
it('does not mutate existing params with URLSearchParams base', () => {
5656
const serialize = createSerializer(parsers)
5757
const searchBefore = new URLSearchParams('?str=foo')
5858
const result = serialize(searchBefore, { str: 'bar' })
5959
expect(result).toBe('?str=bar')
6060
expect(searchBefore.get('str')).toBe('foo')
6161
})
62-
test('with URL base', () => {
62+
it('handles a URL base', () => {
6363
const serialize = createSerializer(parsers)
6464
const url = new URL('https://example.com/path')
6565
const result = serialize(url, { str: 'foo' })
6666
expect(result).toBe('https://example.com/path?str=foo')
6767
})
68-
test('with URL base and search params', () => {
68+
it('handles a URL base and merges search params', () => {
6969
const serialize = createSerializer(parsers)
7070
const url = new URL('https://example.com/path?bar=egg')
7171
const result = serialize(url, { str: 'foo' })
7272
expect(result).toBe('https://example.com/path?bar=egg&str=foo')
7373
})
74-
test('null value deletes from base', () => {
74+
it('deletes a null value from base', () => {
7575
const serialize = createSerializer(parsers)
7676
const result = serialize('?str=bar&int=-1', { str: 'foo', int: null })
7777
expect(result).toBe('?str=foo')
7878
})
79-
test('null deletes all from base', () => {
79+
it('deletes all from base with a global null', () => {
8080
const serialize = createSerializer(parsers)
8181
const result = serialize('?str=bar&int=-1', null)
8282
expect(result).toBe('')
8383
})
84-
test('null keeps search params not managed by the serializer', () => {
84+
it('keeps search params not managed by the serializer when fed null', () => {
8585
const serialize = createSerializer(parsers)
8686
const result = serialize('?str=foo&external=kept', null)
8787
expect(result).toBe('?external=kept')
8888
})
89-
test('clears value when setting null for search param that has a default value', () => {
89+
it('clears value when setting null for a search param that has a default value', () => {
9090
const serialize = createSerializer({
9191
int: parseAsInteger.withDefault(0)
9292
})
9393
const result = serialize('?int=1&str=foo', { int: null })
9494
expect(result).toBe('?str=foo')
9595
})
96-
test('clears value when setting null for search param that is set to its default value', () => {
96+
it('clears value when setting null for æ search param that is set to its default value', () => {
9797
const serialize = createSerializer({
9898
int: parseAsInteger.withDefault(0)
9999
})
100100
const result = serialize('?int=0&str=foo', { int: null })
101101
expect(result).toBe('?str=foo')
102102
})
103-
test('clears value when setting the default value (`clearOnDefault: true` is the default)', () => {
103+
it('clears value when setting the default value (`clearOnDefault: true` is the default)', () => {
104104
const serialize = createSerializer({
105105
int: parseAsInteger.withDefault(0),
106106
str: parseAsString.withDefault(''),
@@ -117,7 +117,7 @@ describe('serializer', () => {
117117
})
118118
expect(result).toBe('')
119119
})
120-
test('keeps value when setting the default value when `clearOnDefault: false`', () => {
120+
it('keeps value when setting the default value when `clearOnDefault: false`', () => {
121121
const options: Options = { clearOnDefault: false }
122122
const serialize = createSerializer({
123123
int: parseAsInteger.withOptions(options).withDefault(0),
@@ -139,7 +139,7 @@ describe('serializer', () => {
139139
'?int=0&str=&bool=false&arr=&json={%22foo%22:%22bar%22}'
140140
)
141141
})
142-
test('support for global clearOnDefault option', () => {
142+
it('supports a global clearOnDefault option', () => {
143143
const serialize = createSerializer(
144144
{
145145
int: parseAsInteger.withDefault(0),
@@ -161,7 +161,7 @@ describe('serializer', () => {
161161
'?int=0&str=&bool=false&arr=&json={%22foo%22:%22bar%22}'
162162
)
163163
})
164-
test('parser clearOnDefault takes precedence over global clearOnDefault', () => {
164+
it('gives precedence to parser clearOnDefault over global clearOnDefault', () => {
165165
const serialize = createSerializer(
166166
{
167167
int: parseAsInteger
@@ -177,7 +177,7 @@ describe('serializer', () => {
177177
})
178178
expect(result).toBe('?str=')
179179
})
180-
test('supports urlKeys', () => {
180+
it('supports urlKeys', () => {
181181
const serialize = createSerializer(parsers, {
182182
urlKeys: {
183183
bool: 'b',
@@ -188,4 +188,17 @@ describe('serializer', () => {
188188
const result = serialize({ str: 'foo', int: 1, bool: true })
189189
expect(result).toBe('?s=foo&i=1&b=true')
190190
})
191+
it('supports ? in the values', () => {
192+
const serialize = createSerializer(parsers)
193+
const result = serialize({ str: 'foo?bar', int: 1, bool: true })
194+
expect(result).toBe('?str=foo?bar&int=1&bool=true')
195+
})
196+
it('supports & in the base', () => {
197+
// Repro for https://github.com/47ng/nuqs/issues/812
198+
const serialize = createSerializer(parsers)
199+
const result = serialize('https://example.com/path?issue=is?here', {
200+
str: 'foo?bar'
201+
})
202+
expect(result).toBe('https://example.com/path?issue=is?here&str=foo?bar')
203+
})
191204
})

packages/nuqs/src/serializer.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ function isBase(base: any): base is Base {
8181

8282
function splitBase(base: Base) {
8383
if (typeof base === 'string') {
84-
const [path = '', search] = base.split('?')
85-
return [path, new URLSearchParams(search)] as const
84+
const [path = '', ...search] = base.split('?')
85+
return [path, new URLSearchParams(search.join('?'))] as const
8686
} else if (base instanceof URLSearchParams) {
8787
return ['', new URLSearchParams(base)] as const // Operate on a copy of URLSearchParams, as derived classes may restrict its allowed methods
8888
} else {

0 commit comments

Comments
 (0)