Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Support ? characters in serializer base #821

Merged
merged 2 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 34 additions & 21 deletions packages/nuqs/src/serializer.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, test } from 'vitest'
import { describe, expect, it } from 'vitest'
import type { Options } from './defs'
import {
parseAsArrayOf,
Expand All @@ -16,91 +16,91 @@ const parsers = {
}

describe('serializer', () => {
test('empty', () => {
it('handles empty inputs', () => {
const serialize = createSerializer(parsers)
const result = serialize({})
expect(result).toBe('')
})
test('one item', () => {
it('handles a single item', () => {
const serialize = createSerializer(parsers)
const result = serialize({ str: 'foo' })
expect(result).toBe('?str=foo')
})
test('several items', () => {
it('handles several items', () => {
const serialize = createSerializer(parsers)
const result = serialize({ str: 'foo', int: 1, bool: true })
expect(result).toBe('?str=foo&int=1&bool=true')
})
test("null items don't show up", () => {
it('does not render null items', () => {
const serialize = createSerializer(parsers)
const result = serialize({ str: null })
expect(result).toBe('')
})
test('with string base', () => {
it('handles a string base', () => {
const serialize = createSerializer(parsers)
const result = serialize('/foo', { str: 'foo' })
expect(result).toBe('/foo?str=foo')
})
test('with string base with search params', () => {
it('handles a string base with search params', () => {
const serialize = createSerializer(parsers)
const result = serialize('/foo?bar=egg', { str: 'foo' })
expect(result).toBe('/foo?bar=egg&str=foo')
})
test('with URLSearchParams base', () => {
it('handles a URLSearchParams base', () => {
const serialize = createSerializer(parsers)
const search = new URLSearchParams('?bar=egg')
const result = serialize(search, { str: 'foo' })
expect(result).toBe('?bar=egg&str=foo')
})
test('Does not mutate existing params with URLSearchParams base', () => {
it('does not mutate existing params with URLSearchParams base', () => {
const serialize = createSerializer(parsers)
const searchBefore = new URLSearchParams('?str=foo')
const result = serialize(searchBefore, { str: 'bar' })
expect(result).toBe('?str=bar')
expect(searchBefore.get('str')).toBe('foo')
})
test('with URL base', () => {
it('handles a URL base', () => {
const serialize = createSerializer(parsers)
const url = new URL('https://example.com/path')
const result = serialize(url, { str: 'foo' })
expect(result).toBe('https://example.com/path?str=foo')
})
test('with URL base and search params', () => {
it('handles a URL base and merges search params', () => {
const serialize = createSerializer(parsers)
const url = new URL('https://example.com/path?bar=egg')
const result = serialize(url, { str: 'foo' })
expect(result).toBe('https://example.com/path?bar=egg&str=foo')
})
test('null value deletes from base', () => {
it('deletes a null value from base', () => {
const serialize = createSerializer(parsers)
const result = serialize('?str=bar&int=-1', { str: 'foo', int: null })
expect(result).toBe('?str=foo')
})
test('null deletes all from base', () => {
it('deletes all from base with a global null', () => {
const serialize = createSerializer(parsers)
const result = serialize('?str=bar&int=-1', null)
expect(result).toBe('')
})
test('null keeps search params not managed by the serializer', () => {
it('keeps search params not managed by the serializer when fed null', () => {
const serialize = createSerializer(parsers)
const result = serialize('?str=foo&external=kept', null)
expect(result).toBe('?external=kept')
})
test('clears value when setting null for search param that has a default value', () => {
it('clears value when setting null for a search param that has a default value', () => {
const serialize = createSerializer({
int: parseAsInteger.withDefault(0)
})
const result = serialize('?int=1&str=foo', { int: null })
expect(result).toBe('?str=foo')
})
test('clears value when setting null for search param that is set to its default value', () => {
it('clears value when setting null for æ search param that is set to its default value', () => {
const serialize = createSerializer({
int: parseAsInteger.withDefault(0)
})
const result = serialize('?int=0&str=foo', { int: null })
expect(result).toBe('?str=foo')
})
test('clears value when setting the default value (`clearOnDefault: true` is the default)', () => {
it('clears value when setting the default value (`clearOnDefault: true` is the default)', () => {
const serialize = createSerializer({
int: parseAsInteger.withDefault(0),
str: parseAsString.withDefault(''),
Expand All @@ -117,7 +117,7 @@ describe('serializer', () => {
})
expect(result).toBe('')
})
test('keeps value when setting the default value when `clearOnDefault: false`', () => {
it('keeps value when setting the default value when `clearOnDefault: false`', () => {
const options: Options = { clearOnDefault: false }
const serialize = createSerializer({
int: parseAsInteger.withOptions(options).withDefault(0),
Expand All @@ -139,7 +139,7 @@ describe('serializer', () => {
'?int=0&str=&bool=false&arr=&json={%22foo%22:%22bar%22}'
)
})
test('support for global clearOnDefault option', () => {
it('supports a global clearOnDefault option', () => {
const serialize = createSerializer(
{
int: parseAsInteger.withDefault(0),
Expand All @@ -161,7 +161,7 @@ describe('serializer', () => {
'?int=0&str=&bool=false&arr=&json={%22foo%22:%22bar%22}'
)
})
test('parser clearOnDefault takes precedence over global clearOnDefault', () => {
it('gives precedence to parser clearOnDefault over global clearOnDefault', () => {
const serialize = createSerializer(
{
int: parseAsInteger
Expand All @@ -177,7 +177,7 @@ describe('serializer', () => {
})
expect(result).toBe('?str=')
})
test('supports urlKeys', () => {
it('supports urlKeys', () => {
const serialize = createSerializer(parsers, {
urlKeys: {
bool: 'b',
Expand All @@ -188,4 +188,17 @@ describe('serializer', () => {
const result = serialize({ str: 'foo', int: 1, bool: true })
expect(result).toBe('?s=foo&i=1&b=true')
})
it('supports ? in the values', () => {
const serialize = createSerializer(parsers)
const result = serialize({ str: 'foo?bar', int: 1, bool: true })
expect(result).toBe('?str=foo?bar&int=1&bool=true')
})
it('supports & in the base', () => {
// Repro for https://github.com/47ng/nuqs/issues/812
const serialize = createSerializer(parsers)
const result = serialize('https://example.com/path?issue=is?here', {
str: 'foo?bar'
})
expect(result).toBe('https://example.com/path?issue=is?here&str=foo?bar')
})
})
4 changes: 2 additions & 2 deletions packages/nuqs/src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ function isBase(base: any): base is Base {

function splitBase(base: Base) {
if (typeof base === 'string') {
const [path = '', search] = base.split('?')
return [path, new URLSearchParams(search)] as const
const [path = '', ...search] = base.split('?')
return [path, new URLSearchParams(search.join('?'))] as const
} else if (base instanceof URLSearchParams) {
return ['', new URLSearchParams(base)] as const // Operate on a copy of URLSearchParams, as derived classes may restrict its allowed methods
} else {
Expand Down
Loading