@tldraw/validate 5.1.1 → 5.2.0-canary.05c017c18b15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ import * as T from '../lib/validation'
2
+ import { ValidationError } from '../lib/validation'
3
+
4
+ describe('§11 Dictionaries', () => {
5
+ it('[DI1] dict validates every key and value, prefixing the key on failure', () => {
6
+ const validator = T.dict(T.string, T.number)
7
+ const value = { alice: 100, bob: 85 }
8
+ expect(validator.validate(value)).toBe(value)
9
+
10
+ expect(() => validator.validate({ alice: 'x' })).toThrow(
11
+ 'At alice: Expected number, got a string'
12
+ )
13
+
14
+ const shortKeys = T.dict(
15
+ T.string.check((key) => {
16
+ if (key.length > 2) throw new ValidationError('key too long')
17
+ }),
18
+ T.number
19
+ )
20
+ expect(() => shortKeys.validate({ abc: 1 })).toThrow('At abc: key too long')
21
+ })
22
+
23
+ it('[DI1] non-objects and null are rejected; arrays pass the object check', () => {
24
+ const validator = T.dict(T.string, T.number)
25
+ expect(() => validator.validate(null)).toThrow('Expected object, got null')
26
+ expect(() => validator.validate('x')).toThrow('Expected object, got a string')
27
+
28
+ const arr = [1, 2]
29
+ expect(validator.validate(arr)).toBe(arr)
30
+ })
31
+
32
+ it('[DI2] jsonDict validates string keys and JSON values', () => {
33
+ const value = { a: 'x', b: 42, c: ['a', 'b'], d: { nested: true }, e: null }
34
+ expect(T.jsonDict().validate(value)).toBe(value)
35
+ expect(() => T.jsonDict().validate({ a: () => {} })).toThrow(
36
+ 'At a: Expected json serializable value, got function'
37
+ )
38
+ })
39
+
40
+ it('[DI3] known-good validation returns the known-good object when nothing changed', () => {
41
+ const validator = T.dict(T.string, T.number)
42
+ const knownGood = validator.validate({ a: 1, b: 2 })
43
+ expect(validator.validateUsingKnownGoodVersion(knownGood, { a: 1, b: 2 })).toBe(knownGood)
44
+ })
45
+
46
+ it('[DI3] added keys are fully validated and produce the new object', () => {
47
+ const validator = T.dict(T.string, T.number)
48
+ const knownGood = validator.validate({ a: 1, b: 2 })
49
+
50
+ const added = { a: 1, b: 2, c: 3 }
51
+ expect(validator.validateUsingKnownGoodVersion(knownGood, added)).toBe(added)
52
+ expect(() =>
53
+ validator.validateUsingKnownGoodVersion(knownGood, { a: 1, b: 2, c: 'x' })
54
+ ).toThrow('At c: Expected number, got a string')
55
+ })
56
+
57
+ it('[DI3] removed keys are detected', () => {
58
+ const validator = T.dict(T.string, T.number)
59
+ const knownGood = validator.validate({ a: 1, b: 2 })
60
+ const removed = { a: 1 }
61
+ expect(validator.validateUsingKnownGoodVersion(knownGood, removed)).toBe(removed)
62
+
63
+ // a swap (one removed, one added) is also detected
64
+ const swapped = { a: 1, c: 2 }
65
+ expect(validator.validateUsingKnownGoodVersion(knownGood, swapped)).toBe(swapped)
66
+ })
67
+
68
+ it('[DI3] changed values are revalidated incrementally', () => {
69
+ let calls = 0
70
+ const counting = new T.Validator<number>((value) => {
71
+ calls++
72
+ return T.number.validate(value)
73
+ })
74
+ const validator = T.dict(T.string, counting)
75
+ const knownGood = validator.validate({ a: 1, b: 2 })
76
+ calls = 0
77
+
78
+ const next = { a: 1, b: 3 }
79
+ expect(validator.validateUsingKnownGoodVersion(knownGood, next)).toBe(next)
80
+ expect(calls).toBe(1)
81
+
82
+ expect(() => validator.validateUsingKnownGoodVersion(knownGood, { a: 1, b: 'x' })).toThrow(
83
+ 'At b: Expected number, got a string'
84
+ )
85
+ })
86
+ })
@@ -0,0 +1,28 @@
1
+ import * as T from '../lib/validation'
2
+
3
+ describe('§8 Enums', () => {
4
+ it('[EN1] setEnum accepts exactly the set members', () => {
5
+ const validator = T.setEnum(new Set(['red', 'green', 'blue']))
6
+ expect(validator.validate('red')).toBe('red')
7
+ expect(validator.validate('blue')).toBe('blue')
8
+ expect(() => validator.validate('yellow')).toThrow(
9
+ 'Expected "red" or "green" or "blue", got yellow'
10
+ )
11
+ })
12
+
13
+ it('[EN1] the failure message JSON-stringifies the allowed values and string-interpolates the actual one', () => {
14
+ expect(() => T.setEnum(new Set([1, 2])).validate(3)).toThrow('Expected 1 or 2, got 3')
15
+ expect(() => T.setEnum(new Set(['a', 'b'])).validate({ toString: () => 'OBJ' })).toThrow(
16
+ 'Expected "a" or "b", got OBJ'
17
+ )
18
+ })
19
+
20
+ it('[EN2] literalEnum behaves as a setEnum over its arguments', () => {
21
+ const validator = T.literalEnum('light', 'dark', 'auto')
22
+ expect(validator.validate('light')).toBe('light')
23
+ expect(validator.validate('auto')).toBe('auto')
24
+ expect(() => validator.validate('blue')).toThrow(
25
+ 'Expected "light" or "dark" or "auto", got blue'
26
+ )
27
+ })
28
+ })
@@ -0,0 +1,124 @@
1
+ import * as T from '../lib/validation'
2
+ import { ValidationError } from '../lib/validation'
3
+
4
+ describe('§2 Validation errors and paths', () => {
5
+ it('[E1] exposes name, rawMessage, and path', () => {
6
+ const error = new ValidationError('msg', ['a', 0, 'b'])
7
+ expect(error).toBeInstanceOf(Error)
8
+ expect(error.name).toBe('ValidationError')
9
+ expect(error.rawMessage).toBe('msg')
10
+ expect(error.path).toEqual(['a', 0, 'b'])
11
+
12
+ const noPath = new ValidationError('msg')
13
+ expect(noPath.path).toEqual([])
14
+ })
15
+
16
+ it('[E2] prefixes the formatted path onto the message when the path is non-empty', () => {
17
+ expect(new ValidationError('msg', ['a', 0, 'b']).message).toBe('At a.0.b: msg')
18
+ })
19
+
20
+ it('[E2] uses the raw message alone when the path is empty', () => {
21
+ expect(new ValidationError('msg').message).toBe('msg')
22
+ expect(new ValidationError('msg', []).message).toBe('msg')
23
+
24
+ try {
25
+ T.string.validate(5)
26
+ throw new Error('should have thrown')
27
+ } catch (error) {
28
+ expect((error as Error).message).toBe('Expected string, got a number')
29
+ }
30
+ })
31
+
32
+ it('[E3] joins string segments with dots and renders numeric indices', () => {
33
+ const validator = T.object({
34
+ toad: T.object({
35
+ friends: T.arrayOf(T.object({ name: T.string })),
36
+ }),
37
+ })
38
+
39
+ expect(() =>
40
+ validator.validate({ toad: { friends: [{ name: 'bird' }, { name: 1235 }] } })
41
+ ).toThrow('At toad.friends.1.name: Expected string, got a number')
42
+ })
43
+
44
+ it('[E4] appends parenthesized segments without a dot', () => {
45
+ const animal = T.union('type', {
46
+ cat: T.object({ type: T.literal('cat'), id: T.string, meow: T.boolean }),
47
+ dog: T.object({ type: T.literal('dog'), id: T.string, bark: T.boolean }),
48
+ })
49
+ const nested = T.object({ animal })
50
+
51
+ expect(() => nested.validate({ animal: { type: 'cat', meow: 'yes', id: 'abc123' } })).toThrow(
52
+ 'At animal(type = cat).meow: Expected boolean, got a string'
53
+ )
54
+ })
55
+
56
+ it('[E4] merges consecutive parenthesized segments into one group', () => {
57
+ const validator = T.union('type', {
58
+ cat: T.object({ type: T.literal('cat'), n: T.number }).check('named', () => {
59
+ throw new ValidationError('boom')
60
+ }),
61
+ })
62
+
63
+ expect(() => validator.validate({ type: 'cat', n: 1 })).toThrow(
64
+ 'At (type = cat, check named): boom'
65
+ )
66
+ })
67
+
68
+ it('[E5] strips id = … content from formatted paths', () => {
69
+ const validator = T.object({
70
+ thing: T.union('id', {
71
+ foo: T.object({ id: T.literal('foo'), n: T.number }),
72
+ }),
73
+ })
74
+
75
+ expect(() => validator.validate({ thing: { id: 'foo', n: 'bad' } })).toThrow(
76
+ 'At thing().n: Expected number, got a string'
77
+ )
78
+ })
79
+
80
+ it('[E6] indents every line after the first in multi-line messages', () => {
81
+ const validator = T.object({
82
+ x: T.number.check(() => {
83
+ throw new ValidationError('line1\nline2\nline3')
84
+ }),
85
+ })
86
+
87
+ expect(() => validator.validate({ x: 1 })).toThrow('At x: line1\n line2\n line3')
88
+ })
89
+
90
+ it('[E7] accumulates the path outside-in while preserving rawMessage', () => {
91
+ const validator = T.object({
92
+ users: T.arrayOf(T.object({ email: T.string })),
93
+ })
94
+
95
+ try {
96
+ validator.validate({ users: [{ email: 'a@b.com' }, { email: 42 }] })
97
+ throw new Error('should have thrown')
98
+ } catch (error) {
99
+ expect(error).toBeInstanceOf(ValidationError)
100
+ expect((error as ValidationError).rawMessage).toBe('Expected string, got a number')
101
+ expect((error as ValidationError).path).toEqual(['users', 1, 'email'])
102
+ expect((error as ValidationError).message).toBe(
103
+ 'At users.1.email: Expected string, got a number'
104
+ )
105
+ }
106
+ })
107
+
108
+ it('[E7] wraps non-ValidationError exceptions using their toString', () => {
109
+ const validator = T.object({
110
+ x: T.number.check(() => {
111
+ throw new Error('plain')
112
+ }),
113
+ })
114
+
115
+ try {
116
+ validator.validate({ x: 1 })
117
+ throw new Error('should have thrown')
118
+ } catch (error) {
119
+ expect(error).toBeInstanceOf(ValidationError)
120
+ expect((error as ValidationError).rawMessage).toBe('Error: plain')
121
+ expect((error as ValidationError).path).toEqual(['x'])
122
+ }
123
+ })
124
+ })
@@ -0,0 +1,15 @@
1
+ import * as T from '../lib/validation'
2
+
3
+ describe('§16 Index keys', () => {
4
+ it('[IK1] accepts valid fractional index keys', () => {
5
+ expect(T.indexKey.validate('a0')).toBe('a0')
6
+ expect(T.indexKey.validate('a1J')).toBe('a1J')
7
+ })
8
+
9
+ it('[IK1] rejects invalid index keys', () => {
10
+ expect(() => T.indexKey.validate('a')).toThrow('Expected an index key, got "a"')
11
+ expect(() => T.indexKey.validate('a00')).toThrow('Expected an index key, got "a00"')
12
+ expect(() => T.indexKey.validate('')).toThrow('Expected an index key, got ""')
13
+ expect(() => T.indexKey.validate(5)).toThrow('Expected string, got a number')
14
+ })
15
+ })
@@ -0,0 +1,108 @@
1
+ import * as T from '../lib/validation'
2
+
3
+ describe('§13 JSON values', () => {
4
+ it('[J1] accepts JSON scalars, arrays, and plain objects recursively', () => {
5
+ const value = { name: 'Alice', scores: [1, 2, 3], active: true, meta: null, nested: { a: 'b' } }
6
+ expect(T.jsonValue.validate(value)).toBe(value)
7
+ expect(T.jsonValue.validate('x')).toBe('x')
8
+ expect(T.jsonValue.validate(42)).toBe(42)
9
+ expect(T.jsonValue.validate(false)).toBe(false)
10
+ expect(T.jsonValue.validate(null)).toBe(null)
11
+ })
12
+
13
+ it('[J1] accepts non-finite numbers', () => {
14
+ expect(T.jsonValue.validate(NaN)).toBe(NaN)
15
+ expect(T.jsonValue.validate(Infinity)).toBe(Infinity)
16
+ })
17
+
18
+ it('[J2] rejects non-JSON values anywhere in the structure', () => {
19
+ class Foo {}
20
+ expect(T.jsonValue.isValid(undefined)).toBe(false)
21
+ expect(T.jsonValue.isValid(() => {})).toBe(false)
22
+ expect(T.jsonValue.isValid(1n)).toBe(false)
23
+ expect(T.jsonValue.isValid(Symbol('s'))).toBe(false)
24
+ expect(T.jsonValue.isValid(new Foo())).toBe(false)
25
+ expect(T.jsonValue.isValid({ a: undefined })).toBe(false)
26
+ expect(T.jsonValue.isValid([1, () => {}])).toBe(false)
27
+ expect(T.jsonValue.isValid({ deep: { deeper: [1n] } })).toBe(false)
28
+ })
29
+
30
+ it('[J2] rejects sparse arrays, whose holes read as undefined', () => {
31
+ // eslint-disable-next-line no-sparse-arrays
32
+ expect(T.jsonValue.isValid([1, , 3])).toBe(false)
33
+ expect(T.jsonValue.isValid(new Array(3))).toBe(false)
34
+ })
35
+
36
+ it('[J3] accepts null-prototype and structured-clone objects as plain objects', () => {
37
+ expect(T.jsonValue.isValid(Object.create(null))).toBe(true)
38
+ expect(T.jsonValue.isValid(structuredClone({ a: 1 }))).toBe(true)
39
+ })
40
+
41
+ it('[J4] the full-validation failure message reports the typeof of the root value', () => {
42
+ expect(() => T.jsonValue.validate({ a: undefined })).toThrow(
43
+ 'Expected json serializable value, got object'
44
+ )
45
+ expect(() => T.jsonValue.validate(() => {})).toThrow(
46
+ 'Expected json serializable value, got function'
47
+ )
48
+ })
49
+
50
+ it('[J5] known-good validation is incremental and identity-preserving for arrays', () => {
51
+ const knownGood = T.jsonValue.validate([1, [2], 'x'])
52
+ expect(T.jsonValue.validateUsingKnownGoodVersion(knownGood, [1, [2], 'x'])).toBe(knownGood)
53
+
54
+ const changed = [1, [2], 'y']
55
+ expect(T.jsonValue.validateUsingKnownGoodVersion(knownGood, changed)).toBe(changed)
56
+
57
+ const longer = [1, [2], 'x', 4]
58
+ expect(T.jsonValue.validateUsingKnownGoodVersion(knownGood, longer)).toBe(longer)
59
+
60
+ const shorter = [1, [2]]
61
+ expect(T.jsonValue.validateUsingKnownGoodVersion(knownGood, shorter)).toBe(shorter)
62
+ })
63
+
64
+ it('[J5] known-good validation is incremental and identity-preserving for objects', () => {
65
+ const knownGood = T.jsonValue.validate({ a: [1, 2], b: 'x' })
66
+ expect(T.jsonValue.validateUsingKnownGoodVersion(knownGood, { a: [1, 2], b: 'x' })).toBe(
67
+ knownGood
68
+ )
69
+
70
+ const changed = { a: [1, 2], b: 'y' }
71
+ expect(T.jsonValue.validateUsingKnownGoodVersion(knownGood, changed)).toBe(changed)
72
+
73
+ const removed = { a: [1, 2] }
74
+ expect(T.jsonValue.validateUsingKnownGoodVersion(knownGood, removed)).toBe(removed)
75
+
76
+ const added = { a: [1, 2], b: 'x', c: 1 }
77
+ expect(T.jsonValue.validateUsingKnownGoodVersion(knownGood, added)).toBe(added)
78
+ })
79
+
80
+ it('[J5] added and changed entries are validated', () => {
81
+ const knownGoodArr = T.jsonValue.validate([1, 2])
82
+ expect(() => T.jsonValue.validateUsingKnownGoodVersion(knownGoodArr, [1, () => {}])).toThrow()
83
+ expect(() =>
84
+ T.jsonValue.validateUsingKnownGoodVersion(knownGoodArr, [1, 2, () => {}])
85
+ ).toThrow()
86
+
87
+ const knownGoodObj = T.jsonValue.validate({ a: 1 })
88
+ expect(() => T.jsonValue.validateUsingKnownGoodVersion(knownGoodObj, { a: () => {} })).toThrow()
89
+ expect(() =>
90
+ T.jsonValue.validateUsingKnownGoodVersion(knownGoodObj, { a: 1, b: () => {} })
91
+ ).toThrow()
92
+ })
93
+
94
+ it('[J6] a shape mismatch falls back to a full validation of the new value', () => {
95
+ const knownGoodArr = T.jsonValue.validate([1])
96
+ const obj = { a: 1 }
97
+ expect(T.jsonValue.validateUsingKnownGoodVersion(knownGoodArr, obj)).toBe(obj)
98
+
99
+ const knownGoodObj = T.jsonValue.validate({ a: 1 })
100
+ const arr = [1]
101
+ expect(T.jsonValue.validateUsingKnownGoodVersion(knownGoodObj, arr)).toBe(arr)
102
+
103
+ expect(T.jsonValue.validateUsingKnownGoodVersion(knownGoodObj, 'x')).toBe('x')
104
+ expect(() => T.jsonValue.validateUsingKnownGoodVersion(knownGoodObj, () => {})).toThrow(
105
+ 'Expected json serializable value, got function'
106
+ )
107
+ })
108
+ })
@@ -0,0 +1,34 @@
1
+ import * as T from '../lib/validation'
2
+
3
+ describe('§14 Models', () => {
4
+ it('[M1] model prefixes the model name onto validation failures', () => {
5
+ const shape = T.model('shape', T.object({ id: T.string, x: T.number, y: T.number }))
6
+
7
+ const value = { id: 'abc123', x: 132, y: 0 }
8
+ expect(shape.validate(value)).toBe(value)
9
+
10
+ expect(() => shape.validate({ id: 'abc123', x: 132, y: NaN })).toThrow(
11
+ 'At shape.y: Expected a number, got NaN'
12
+ )
13
+ expect(() =>
14
+ T.model(
15
+ 'shape',
16
+ T.object({ id: T.string, color: T.setEnum(new Set(['red', 'green', 'blue'])) })
17
+ ).validate({ id: 'abc13', color: 'rubbish' })
18
+ ).toThrow('At shape.color: Expected "red" or "green" or "blue", got rubbish')
19
+ })
20
+
21
+ it('[M2] known-good validation delegates to the inner validator with the same prefixing', () => {
22
+ const user = T.model('user', T.object({ id: T.string, n: T.number }))
23
+ const knownGood = user.validate({ id: 'x', n: 1 })
24
+
25
+ expect(user.validateUsingKnownGoodVersion(knownGood, { id: 'x', n: 1 })).toBe(knownGood)
26
+
27
+ const next = { id: 'x', n: 2 }
28
+ expect(user.validateUsingKnownGoodVersion(knownGood, next)).toBe(next)
29
+
30
+ expect(() => user.validateUsingKnownGoodVersion(knownGood, { id: 'x', n: 'bad' })).toThrow(
31
+ 'At user.n: Expected number, got a string'
32
+ )
33
+ })
34
+ })
@@ -0,0 +1,75 @@
1
+ import * as T from '../lib/validation'
2
+
3
+ describe('§5 optional and nullable', () => {
4
+ it('[NO1] nullable accepts null and passes everything else to the inner validator', () => {
5
+ const validator = T.nullable(T.string)
6
+ expect(validator.validate(null)).toBe(null)
7
+ expect(validator.validate('hello')).toBe('hello')
8
+ expect(() => validator.validate(undefined)).toThrow('Expected string, got undefined')
9
+ expect(() => validator.validate(5)).toThrow('Expected string, got a number')
10
+ })
11
+
12
+ it('[NO1] the .nullable() method behaves like T.nullable', () => {
13
+ expect(T.string.nullable().validate(null)).toBe(null)
14
+ expect(T.string.nullable().validate('hello')).toBe('hello')
15
+ })
16
+
17
+ it('[NO2] optional accepts undefined and passes everything else to the inner validator', () => {
18
+ const validator = T.optional(T.string)
19
+ expect(validator.validate(undefined)).toBe(undefined)
20
+ expect(validator.validate('hello')).toBe('hello')
21
+ expect(() => validator.validate(null)).toThrow('Expected string, got null')
22
+ })
23
+
24
+ it('[NO2] the .optional() method behaves like T.optional', () => {
25
+ expect(T.string.optional().validate(undefined)).toBe(undefined)
26
+ expect(T.string.optional().validate('hello')).toBe('hello')
27
+ })
28
+
29
+ it('[NO3] known-good validation short-circuits null/undefined new values', () => {
30
+ let calls = 0
31
+ const counting = new T.Validator<string>((value) => {
32
+ calls++
33
+ return T.string.validate(value)
34
+ })
35
+
36
+ expect(T.optional(counting).validateUsingKnownGoodVersion('a', undefined)).toBe(undefined)
37
+ expect(T.nullable(counting).validateUsingKnownGoodVersion('a', null)).toBe(null)
38
+ expect(calls).toBe(0)
39
+ })
40
+
41
+ it('[NO3] a null/undefined known-good value forces a full inner validate', () => {
42
+ expect(T.string.optional().validateUsingKnownGoodVersion(undefined, 'a')).toBe('a')
43
+ expect(() => T.string.optional().validateUsingKnownGoodVersion(undefined, 5)).toThrow(
44
+ 'Expected string, got a number'
45
+ )
46
+ expect(T.string.nullable().validateUsingKnownGoodVersion(null, 'a')).toBe('a')
47
+ expect(() => T.string.nullable().validateUsingKnownGoodVersion(null, 5)).toThrow(
48
+ 'Expected string, got a number'
49
+ )
50
+ })
51
+
52
+ it('[NO3] otherwise the inner known-good path is used, preserving identity', () => {
53
+ const validator = T.object({ a: T.number }).optional()
54
+ const knownGood = validator.validate({ a: 1 })
55
+ expect(validator.validateUsingKnownGoodVersion(knownGood, { a: 1 })).toBe(knownGood)
56
+
57
+ const next = { a: 2 }
58
+ expect(validator.validateUsingKnownGoodVersion(knownGood, next)).toBe(next)
59
+ })
60
+
61
+ it('[NO4] optional/nullable wrappers keep transforming validators exempt from the dev same-value check', () => {
62
+ expect(
63
+ T.string
64
+ .refine((s) => s.toUpperCase())
65
+ .optional()
66
+ .validate('a')
67
+ ).toBe('A')
68
+ expect(
69
+ T.string
70
+ .refine((s) => s.toUpperCase())
71
+ .nullable()
72
+ .validate('a')
73
+ ).toBe('A')
74
+ })
75
+ })
@@ -0,0 +1,114 @@
1
+ import * as T from '../lib/validation'
2
+
3
+ describe('§7 Numbers', () => {
4
+ it('[N1] number accepts finite numbers and rejects everything else with specific messages', () => {
5
+ expect(T.number.validate(42)).toBe(42)
6
+ expect(T.number.validate(-3.14)).toBe(-3.14)
7
+ expect(T.number.validate(0)).toBe(0)
8
+
9
+ expect(() => T.number.validate('42')).toThrow('Expected number, got a string')
10
+ expect(() => T.number.validate(NaN)).toThrow('Expected a number, got NaN')
11
+ expect(() => T.number.validate(Infinity)).toThrow('Expected a finite number, got Infinity')
12
+ expect(() => T.number.validate(-Infinity)).toThrow('Expected a finite number, got -Infinity')
13
+ })
14
+
15
+ it('[N2] positiveNumber accepts zero and positive finite numbers', () => {
16
+ expect(T.positiveNumber.validate(29.99)).toBe(29.99)
17
+ expect(T.positiveNumber.validate(0)).toBe(0)
18
+
19
+ expect(() => T.positiveNumber.validate(-1)).toThrow('Expected a positive number, got -1')
20
+ expect(() => T.positiveNumber.validate(-Infinity)).toThrow(
21
+ 'Expected a positive number, got -Infinity'
22
+ )
23
+ expect(() => T.positiveNumber.validate(Infinity)).toThrow(
24
+ 'Expected a finite number, got Infinity'
25
+ )
26
+ expect(() => T.positiveNumber.validate(NaN)).toThrow('Expected a number, got NaN')
27
+ expect(() => T.positiveNumber.validate('1')).toThrow('Expected number, got a string')
28
+ })
29
+
30
+ it('[N3] nonZeroNumber accepts only positive finite numbers', () => {
31
+ expect(T.nonZeroNumber.validate(0.01)).toBe(0.01)
32
+
33
+ expect(() => T.nonZeroNumber.validate(0)).toThrow('Expected a non-zero positive number, got 0')
34
+ expect(() => T.nonZeroNumber.validate(-5)).toThrow(
35
+ 'Expected a non-zero positive number, got -5'
36
+ )
37
+ expect(() => T.nonZeroNumber.validate(-Infinity)).toThrow(
38
+ 'Expected a non-zero positive number, got -Infinity'
39
+ )
40
+ })
41
+
42
+ it('[N4] nonZeroFiniteNumber accepts non-zero finite numbers including negatives', () => {
43
+ expect(T.nonZeroFiniteNumber.validate(-1.5)).toBe(-1.5)
44
+ expect(T.nonZeroFiniteNumber.validate(2)).toBe(2)
45
+
46
+ expect(() => T.nonZeroFiniteNumber.validate(0)).toThrow('Expected a non-zero number, got 0')
47
+ expect(() => T.nonZeroFiniteNumber.validate(Infinity)).toThrow(
48
+ 'Expected a finite number, got Infinity'
49
+ )
50
+ })
51
+
52
+ it('[N5] unitInterval accepts finite numbers in [0, 1]', () => {
53
+ expect(T.unitInterval.validate(0)).toBe(0)
54
+ expect(T.unitInterval.validate(0.5)).toBe(0.5)
55
+ expect(T.unitInterval.validate(1)).toBe(1)
56
+
57
+ expect(() => T.unitInterval.validate(1.5)).toThrow('Expected a number between 0 and 1, got 1.5')
58
+ expect(() => T.unitInterval.validate(-0.1)).toThrow(
59
+ 'Expected a number between 0 and 1, got -0.1'
60
+ )
61
+ expect(() => T.unitInterval.validate(Infinity)).toThrow(
62
+ 'Expected a number between 0 and 1, got Infinity'
63
+ )
64
+ expect(() => T.unitInterval.validate(NaN)).toThrow('Expected a number, got NaN')
65
+ })
66
+
67
+ it('[N6] integer accepts whole finite numbers including negatives', () => {
68
+ expect(T.integer.validate(42)).toBe(42)
69
+ expect(T.integer.validate(-5)).toBe(-5)
70
+ expect(T.integer.validate(0)).toBe(0)
71
+
72
+ expect(() => T.integer.validate(3.14)).toThrow('Expected an integer, got 3.14')
73
+ expect(() => T.integer.validate(NaN)).toThrow('Expected a number, got NaN')
74
+ expect(() => T.integer.validate(Infinity)).toThrow('Expected a finite number, got Infinity')
75
+ expect(() => T.integer.validate('1')).toThrow('Expected number, got a string')
76
+ })
77
+
78
+ it('[N7] positiveInteger accepts zero and positive integers', () => {
79
+ expect(T.positiveInteger.validate(5)).toBe(5)
80
+ expect(T.positiveInteger.validate(0)).toBe(0)
81
+
82
+ expect(() => T.positiveInteger.validate(-1)).toThrow('Expected a positive integer, got -1')
83
+ expect(() => T.positiveInteger.validate(3.14)).toThrow('Expected an integer, got 3.14')
84
+ })
85
+
86
+ it('[N7] positiveInteger reports any negative number as not a positive integer, even fractions', () => {
87
+ expect(() => T.positiveInteger.validate(-1.5)).toThrow('Expected a positive integer, got -1.5')
88
+ })
89
+
90
+ it('[N8] nonZeroInteger accepts only positive integers', () => {
91
+ expect(T.nonZeroInteger.validate(1)).toBe(1)
92
+
93
+ expect(() => T.nonZeroInteger.validate(0)).toThrow(
94
+ 'Expected a non-zero positive integer, got 0'
95
+ )
96
+ expect(() => T.nonZeroInteger.validate(-5)).toThrow(
97
+ 'Expected a non-zero positive integer, got -5'
98
+ )
99
+ expect(() => T.nonZeroInteger.validate(0.5)).toThrow('Expected an integer, got 0.5')
100
+ expect(() => T.nonZeroInteger.validate(-0.5)).toThrow(
101
+ 'Expected a non-zero positive integer, got -0.5'
102
+ )
103
+ })
104
+
105
+ it('[N9] negative zero counts as zero and as a non-negative integer', () => {
106
+ expect(T.number.validate(-0)).toBe(-0)
107
+ expect(T.integer.validate(-0)).toBe(-0)
108
+ expect(T.positiveNumber.validate(-0)).toBe(-0)
109
+ expect(T.positiveInteger.validate(-0)).toBe(-0)
110
+
111
+ expect(() => T.nonZeroNumber.validate(-0)).toThrow('Expected a non-zero positive number, got 0')
112
+ expect(() => T.nonZeroFiniteNumber.validate(-0)).toThrow('Expected a non-zero number, got 0')
113
+ })
114
+ })