Skip to content

Commit 042d9a5

Browse files
committed
feat: Add custom parser testing helpers
1 parent 98179a4 commit 042d9a5

File tree

5 files changed

+189
-8
lines changed

5 files changed

+189
-8
lines changed

packages/nuqs/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"files": [
3636
"dist/",
3737
"server.d.ts",
38+
"testing.d.ts",
3839
"adapters/react.d.ts",
3940
"adapters/next.d.ts",
4041
"adapters/next/app.d.ts",
@@ -60,6 +61,11 @@
6061
"import": "./dist/server.js",
6162
"require": "./esm-only.cjs"
6263
},
64+
"./testing": {
65+
"types": "./dist/testing.d.ts",
66+
"import": "./dist/testing.js",
67+
"require": "./esm-only.cjs"
68+
},
6369
"./adapters/react": {
6470
"types": "./dist/adapters/react.d.ts",
6571
"import": "./dist/adapters/react.js",

packages/nuqs/src/parsers.test.ts

+99-7
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,49 @@
11
import { describe, expect, test } from 'vitest'
22
import {
33
parseAsArrayOf,
4+
parseAsBoolean,
45
parseAsFloat,
56
parseAsHex,
67
parseAsInteger,
7-
parseAsIsoDateTime,
88
parseAsIsoDate,
9+
parseAsIsoDateTime,
10+
parseAsNumberLiteral,
911
parseAsString,
12+
parseAsStringEnum,
13+
parseAsStringLiteral,
1014
parseAsTimestamp
1115
} from './parsers'
16+
import { testParseThenSerialize, testSerializeThenParse } from './testing'
1217

1318
describe('parsers', () => {
19+
test('parseAsString', () => {
20+
expect(parseAsString.parse('')).toBe('')
21+
expect(parseAsString.parse('foo')).toBe('foo')
22+
testParseThenSerialize(parseAsString, 'foo')
23+
testSerializeThenParse(parseAsString, 'foo')
24+
})
1425
test('parseAsInteger', () => {
1526
expect(parseAsInteger.parse('')).toBeNull()
1627
expect(parseAsInteger.parse('1')).toBe(1)
1728
expect(parseAsInteger.parse('3.14')).toBe(3)
1829
expect(parseAsInteger.parse('3,14')).toBe(3)
1930
expect(parseAsInteger.serialize(3.14)).toBe('3')
31+
testParseThenSerialize(parseAsInteger, '3')
32+
testSerializeThenParse(parseAsInteger, 3)
33+
expect(() => testParseThenSerialize(parseAsInteger, '3.14')).toThrow()
34+
expect(() => testSerializeThenParse(parseAsInteger, 3.14)).toThrow()
35+
})
36+
test('parseAsHex', () => {
37+
expect(parseAsHex.parse('')).toBeNull()
38+
expect(parseAsHex.parse('1')).toBe(1)
39+
expect(parseAsHex.parse('a')).toBe(0xa)
40+
expect(parseAsHex.parse('g')).toBeNull()
41+
expect(parseAsHex.serialize(0xa)).toBe('0a')
42+
for (let i = 0; i < 256; i++) {
43+
const hex = i.toString(16).padStart(2, '0')
44+
testParseThenSerialize(parseAsHex, hex)
45+
testSerializeThenParse(parseAsHex, i)
46+
}
2047
})
2148
test('parseAsFloat', () => {
2249
expect(parseAsFloat.parse('')).toBeNull()
@@ -26,17 +53,31 @@ describe('parsers', () => {
2653
expect(parseAsFloat.serialize(3.14)).toBe('3.14')
2754
// https://0.30000000000000004.com/
2855
expect(parseAsFloat.serialize(0.1 + 0.2)).toBe('0.30000000000000004')
56+
testParseThenSerialize(parseAsFloat, '3.14')
57+
testSerializeThenParse(parseAsFloat, 3.14)
2958
})
30-
test('parseAsHex', () => {
31-
expect(parseAsHex.parse('')).toBeNull()
32-
expect(parseAsHex.parse('1')).toBe(1)
33-
expect(parseAsHex.parse('a')).toBe(0xa)
34-
expect(parseAsHex.parse('g')).toBeNull()
35-
expect(parseAsHex.serialize(0xa)).toBe('0a')
59+
test('parseAsBoolean', () => {
60+
expect(parseAsBoolean.parse('')).toBe(false)
61+
// In only triggers on 'true', everything else is false
62+
expect(parseAsBoolean.parse('true')).toBe(true)
63+
expect(parseAsBoolean.parse('false')).toBe(false)
64+
expect(parseAsBoolean.parse('0')).toBe(false)
65+
expect(parseAsBoolean.parse('1')).toBe(false)
66+
expect(parseAsBoolean.parse('yes')).toBe(false)
67+
expect(parseAsBoolean.parse('no')).toBe(false)
68+
expect(parseAsBoolean.serialize(true)).toBe('true')
69+
expect(parseAsBoolean.serialize(false)).toBe('false')
70+
testParseThenSerialize(parseAsBoolean, 'true')
71+
testSerializeThenParse(parseAsBoolean, true)
72+
testParseThenSerialize(parseAsBoolean, 'false')
73+
testSerializeThenParse(parseAsBoolean, false)
3674
})
75+
3776
test('parseAsTimestamp', () => {
3877
expect(parseAsTimestamp.parse('')).toBeNull()
3978
expect(parseAsTimestamp.parse('0')).toStrictEqual(new Date(0))
79+
testParseThenSerialize(parseAsTimestamp, '0')
80+
testSerializeThenParse(parseAsTimestamp, new Date(1234567890))
4081
})
4182
test('parseAsIsoDateTime', () => {
4283
expect(parseAsIsoDateTime.parse('')).toBeNull()
@@ -48,6 +89,8 @@ describe('parsers', () => {
4889
expect(parseAsIsoDateTime.parse(moment.slice(0, 16) + 'Z')).toStrictEqual(
4990
ref
5091
)
92+
testParseThenSerialize(parseAsIsoDateTime, moment)
93+
testSerializeThenParse(parseAsIsoDateTime, ref)
5194
})
5295
test('parseAsIsoDate', () => {
5396
expect(parseAsIsoDate.parse('')).toBeNull()
@@ -56,12 +99,61 @@ describe('parsers', () => {
5699
const ref = new Date(moment)
57100
expect(parseAsIsoDate.parse(moment)).toStrictEqual(ref)
58101
expect(parseAsIsoDate.serialize(ref)).toEqual(moment)
102+
testParseThenSerialize(parseAsIsoDate, moment)
103+
testSerializeThenParse(parseAsIsoDate, ref)
59104
})
105+
test('parseAsStringEnum', () => {
106+
enum Test {
107+
A = 'a',
108+
B = 'b',
109+
C = 'c'
110+
}
111+
const parser = parseAsStringEnum<Test>(Object.values(Test))
112+
expect(parser.parse('')).toBeNull()
113+
expect(parser.parse('a')).toBe('a')
114+
expect(parser.parse('b')).toBe('b')
115+
expect(parser.parse('c')).toBe('c')
116+
expect(parser.parse('d')).toBeNull()
117+
expect(parser.serialize(Test.A)).toBe('a')
118+
expect(parser.serialize(Test.B)).toBe('b')
119+
expect(parser.serialize(Test.C)).toBe('c')
120+
testParseThenSerialize(parser, 'a')
121+
testSerializeThenParse(parser, Test.A)
122+
})
123+
test('parseAsStringLiteral', () => {
124+
const parser = parseAsStringLiteral(['a', 'b', 'c'] as const)
125+
expect(parser.parse('')).toBeNull()
126+
expect(parser.parse('a')).toBe('a')
127+
expect(parser.parse('b')).toBe('b')
128+
expect(parser.parse('c')).toBe('c')
129+
expect(parser.parse('d')).toBeNull()
130+
expect(parser.serialize('a')).toBe('a')
131+
expect(parser.serialize('b')).toBe('b')
132+
expect(parser.serialize('c')).toBe('c')
133+
testParseThenSerialize(parser, 'a')
134+
testSerializeThenParse(parser, 'a')
135+
})
136+
test('parseAsNumberLiteral', () => {
137+
const parser = parseAsNumberLiteral([1, 2, 3] as const)
138+
expect(parser.parse('')).toBeNull()
139+
expect(parser.parse('1')).toBe(1)
140+
expect(parser.parse('2')).toBe(2)
141+
expect(parser.parse('3')).toBe(3)
142+
expect(parser.parse('4')).toBeNull()
143+
expect(parser.serialize(1)).toBe('1')
144+
expect(parser.serialize(2)).toBe('2')
145+
expect(parser.serialize(3)).toBe('3')
146+
testParseThenSerialize(parser, '1')
147+
testSerializeThenParse(parser, 1)
148+
})
149+
60150
test('parseAsArrayOf', () => {
61151
const parser = parseAsArrayOf(parseAsString)
62152
expect(parser.serialize([])).toBe('')
63153
// It encodes its separator
64154
expect(parser.serialize(['a', ',', 'b'])).toBe('a,%2C,b')
155+
testParseThenSerialize(parser, 'a,b')
156+
testSerializeThenParse(parser, ['a', 'b'])
65157
})
66158

67159
test('parseServerSide with default (#384)', () => {

packages/nuqs/src/testing.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { ParserBuilder } from './parsers'
2+
3+
/**
4+
* Test that a parser is bijective (serialize then parse gives back the same value).
5+
*
6+
* It will throw if the parser is not bijective (if the parsed value is not equal to the input value).
7+
* The parser's `eq` function is used to compare the values.
8+
*
9+
* Usage:
10+
* ```ts
11+
* // Expect it to pass (no error thrown)
12+
* testSerializeThenParse(myParser, 'foo')
13+
* // Expect it to fail
14+
* expect(() => testSerializeThenParse(myParser, 'bar')).toThrow()
15+
* ```
16+
*
17+
* @param parser The parser to test
18+
* @param input An input value to test against
19+
*/
20+
export function testSerializeThenParse<T>(parser: ParserBuilder<T>, input: T) {
21+
const serialized = parser.serialize(input)
22+
const parsed = parser.parse(serialized)
23+
if (parsed === null) {
24+
throw new Error(
25+
`[nuqs] testSerializeThenParse: parsed value is null (when parsing ${serialized} serialized from ${input})`
26+
)
27+
}
28+
if (!parser.eq(input, parsed)) {
29+
throw new Error(
30+
`[nuqs] parser is not bijective (in testSerializeThenParse)
31+
Expected value: ${typeof input === 'object' ? JSON.stringify(input) : input}
32+
Received parsed value: ${typeof parsed === 'object' ? JSON.stringify(parsed) : parsed}
33+
Serialized as: '${serialized}'
34+
`
35+
)
36+
}
37+
}
38+
39+
/**
40+
* Tests that a parser is bijective (parse then serialize gives back the same query string).
41+
*
42+
* It will throw if the parser is not bijective (if the serialized value is not equal to the input query).
43+
*
44+
* Usage:
45+
* ```ts
46+
* // Expect it to pass (no error thrown)
47+
* testParseThenSerialize(myParser, 'foo')
48+
* // Expect it to fail
49+
* expect(() => testParseThenSerialize(myParser, 'bar')).toThrow()
50+
* ```
51+
*
52+
* @param parser The parser to test
53+
* @param input A query string to test against
54+
*/
55+
export function testParseThenSerialize<T>(
56+
parser: ParserBuilder<T>,
57+
input: string
58+
) {
59+
const parsed = parser.parse(input)
60+
if (parsed === null) {
61+
throw new Error(
62+
`[nuqs] testParseThenSerialize: parsed value is null (when parsing ${input})`
63+
)
64+
}
65+
const serialized = parser.serialize(parsed)
66+
if (serialized !== input) {
67+
throw new Error(
68+
`[nuqs] parser is not bijective (in testParseThenSerialize)
69+
Expected query: '${input}'
70+
Received query: '${serialized}'
71+
Parsed value: ${parsed}
72+
`
73+
)
74+
}
75+
}

packages/nuqs/testing.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// This file is needed for projects that have `moduleResolution` set to `node`
2+
// in their tsconfig.json to be able to `import {} from 'nuqs/testing'`.
3+
// Other module resolutions strategies will look for the `exports` in `package.json`,
4+
// but with `node`, TypeScript will look for a .d.ts file with that name at the
5+
// root of the package.
6+
7+
export * from './dist/testing'

packages/nuqs/tsup.config.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ const entrypoints = {
3434
'adapters/testing': 'src/adapters/testing.ts'
3535
},
3636
server: {
37-
server: 'src/index.server.ts'
37+
server: 'src/index.server.ts',
38+
testing: 'src/testing.ts'
3839
}
3940
}
4041

0 commit comments

Comments
 (0)