Skip to content

Commit

Permalink
fix: Support ? characters in serializer base (#821)
Browse files Browse the repository at this point in the history
* test: Rename tests to follow `it('description')` convention

* fix: Support ? characters in serializer base

Closes #812.
  • Loading branch information
franky47 authored Dec 20, 2024
1 parent b18c5b4 commit 8951598
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 23 deletions.
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

0 comments on commit 8951598

Please sign in to comment.