diff --git a/packages/nuqs/src/serializer.test.ts b/packages/nuqs/src/serializer.test.ts index 5ef06ae5..0f6573b9 100644 --- a/packages/nuqs/src/serializer.test.ts +++ b/packages/nuqs/src/serializer.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'vitest' +import { describe, expect, it } from 'vitest' import type { Options } from './defs' import { parseAsArrayOf, @@ -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(''), @@ -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), @@ -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), @@ -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 @@ -177,7 +177,7 @@ describe('serializer', () => { }) expect(result).toBe('?str=') }) - test('supports urlKeys', () => { + it('supports urlKeys', () => { const serialize = createSerializer(parsers, { urlKeys: { bool: 'b', @@ -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') + }) }) diff --git a/packages/nuqs/src/serializer.ts b/packages/nuqs/src/serializer.ts index 272c05b1..bcb8665f 100644 --- a/packages/nuqs/src/serializer.ts +++ b/packages/nuqs/src/serializer.ts @@ -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 {