@hy_ong/zod-kit 0.0.5 → 0.0.6

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,528 @@
1
+ import { describe, it, expect, beforeEach } from "vitest"
2
+ import { time, setLocale, validateTimeFormat, parseTimeToMinutes, normalizeTime } from "../../src"
3
+
4
+ describe("Taiwan time() validator", () => {
5
+ beforeEach(() => setLocale("en"))
6
+
7
+ describe("basic functionality", () => {
8
+ it("should validate correct time formats", () => {
9
+ const schema24 = time({ format: "HH:mm" })
10
+ const schema12 = time({ format: "hh:mm A" })
11
+ const schemaWithSeconds = time({ format: "HH:mm:ss" })
12
+
13
+ // 24-hour format
14
+ expect(schema24.parse("09:30")).toBe("09:30")
15
+ expect(schema24.parse("14:45")).toBe("14:45")
16
+ expect(schema24.parse("00:00")).toBe("00:00")
17
+ expect(schema24.parse("23:59")).toBe("23:59")
18
+
19
+ // 12-hour format
20
+ expect(schema12.parse("09:30 AM")).toBe("09:30 AM")
21
+ expect(schema12.parse("02:45 PM")).toBe("02:45 PM")
22
+ expect(schema12.parse("12:00 AM")).toBe("12:00 AM")
23
+ expect(schema12.parse("12:00 PM")).toBe("12:00 PM")
24
+
25
+ // With seconds
26
+ expect(schemaWithSeconds.parse("14:30:45")).toBe("14:30:45")
27
+ expect(schemaWithSeconds.parse("00:00:00")).toBe("00:00:00")
28
+ expect(schemaWithSeconds.parse("23:59:59")).toBe("23:59:59")
29
+ })
30
+
31
+ it("should validate single digit hour format", () => {
32
+ const schemaH = time({ format: "H:mm" })
33
+ const schemah = time({ format: "h:mm A" })
34
+
35
+ // Single digit 24-hour
36
+ expect(schemaH.parse("9:30")).toBe("9:30")
37
+ expect(schemaH.parse("14:45")).toBe("14:45")
38
+
39
+ // Single digit 12-hour
40
+ expect(schemah.parse("9:30 AM")).toBe("9:30 AM")
41
+ expect(schemah.parse("2:45 PM")).toBe("2:45 PM")
42
+ })
43
+
44
+ it("should reject invalid time formats", () => {
45
+ const schema = time({ format: "HH:mm" })
46
+
47
+ // Invalid formats
48
+ expect(() => schema.parse("25:30")).toThrow("Must be in HH:mm format")
49
+ expect(() => schema.parse("14:70")).toThrow("Must be in HH:mm format")
50
+ expect(() => schema.parse("abc")).toThrow("Must be in HH:mm format")
51
+ expect(() => schema.parse("14")).toThrow("Must be in HH:mm format")
52
+ expect(() => schema.parse("14:30:45")).toThrow("Must be in HH:mm format") // seconds not allowed
53
+ expect(() => schema.parse("2:30 PM")).toThrow("Must be in HH:mm format") // 12-hour in 24-hour format
54
+ })
55
+
56
+ it("should handle whitespace trimming", () => {
57
+ const schema = time({ format: "HH:mm" })
58
+
59
+ expect(schema.parse(" 14:30 ")).toBe("14:30")
60
+ expect(schema.parse("\t09:15\n")).toBe("09:15")
61
+ })
62
+ })
63
+
64
+ describe("whitelist functionality", () => {
65
+ it("should accept any string in whitelist regardless of format", () => {
66
+ const schema = time({
67
+ format: "HH:mm",
68
+ whitelist: ["anytime", "flexible", "TBD"]
69
+ })
70
+
71
+ expect(schema.parse("anytime")).toBe("anytime")
72
+ expect(schema.parse("flexible")).toBe("flexible")
73
+ expect(schema.parse("TBD")).toBe("TBD")
74
+ expect(schema.parse("14:30")).toBe("14:30") // Valid time still works
75
+ })
76
+
77
+ it("should reject values not in whitelist when whitelistOnly is true", () => {
78
+ const schema = time({
79
+ format: "HH:mm",
80
+ whitelist: ["morning", "14:30"],
81
+ whitelistOnly: true
82
+ })
83
+
84
+ expect(schema.parse("morning")).toBe("morning")
85
+ expect(schema.parse("14:30")).toBe("14:30")
86
+
87
+ // Invalid times not in whitelist should be rejected
88
+ expect(() => schema.parse("15:30")).toThrow("Time is not in the allowed list")
89
+ expect(() => schema.parse("evening")).toThrow("Time is not in the allowed list")
90
+ })
91
+
92
+ it("should work with empty whitelist", () => {
93
+ const schema = time({
94
+ format: "HH:mm",
95
+ whitelist: []
96
+ })
97
+
98
+ // With empty whitelist, should still validate time format
99
+ expect(schema.parse("14:30")).toBe("14:30")
100
+ expect(() => schema.parse("invalid")).toThrow("Must be in HH:mm format")
101
+ })
102
+
103
+ it("should prioritize whitelist over format validation", () => {
104
+ const schema = time({
105
+ required: false,
106
+ format: "HH:mm",
107
+ whitelist: ["not-a-time", "123", ""]
108
+ })
109
+
110
+ expect(schema.parse("not-a-time")).toBe("not-a-time")
111
+ expect(schema.parse("123")).toBe("123")
112
+ expect(schema.parse("")).toBe("")
113
+ })
114
+ })
115
+
116
+ describe("required/optional behavior", () => {
117
+ it("should handle required=true (default)", () => {
118
+ const schema = time({ format: "HH:mm" })
119
+
120
+ expect(() => schema.parse("")).toThrow("Required")
121
+ expect(() => schema.parse(null)).toThrow("Required")
122
+ expect(() => schema.parse(undefined)).toThrow("Required")
123
+ })
124
+
125
+ it("should handle required=false", () => {
126
+ const schema = time({ format: "HH:mm", required: false })
127
+
128
+ expect(schema.parse("")).toBe(null)
129
+ expect(schema.parse(null)).toBe(null)
130
+ expect(schema.parse(undefined)).toBe(null)
131
+ expect(schema.parse("14:30")).toBe("14:30")
132
+ })
133
+
134
+ it("should use default values", () => {
135
+ const requiredSchema = time({ format: "HH:mm", defaultValue: "09:00" })
136
+ const optionalSchema = time({ format: "HH:mm", required: false, defaultValue: "12:00" })
137
+
138
+ expect(requiredSchema.parse("")).toBe("09:00")
139
+ expect(requiredSchema.parse(null)).toBe("09:00")
140
+ expect(optionalSchema.parse("")).toBe("12:00")
141
+ expect(optionalSchema.parse(null)).toBe("12:00")
142
+ })
143
+
144
+ it("should handle whitelist with optional fields", () => {
145
+ const schema = time({
146
+ format: "HH:mm",
147
+ required: false,
148
+ whitelist: ["flexible", "14:30"],
149
+ whitelistOnly: true
150
+ })
151
+
152
+ expect(schema.parse("")).toBe(null)
153
+ expect(schema.parse("flexible")).toBe("flexible")
154
+ expect(schema.parse("14:30")).toBe("14:30")
155
+ expect(() => schema.parse("15:30")).toThrow("Time is not in the allowed list")
156
+ })
157
+ })
158
+
159
+ describe("time range validation", () => {
160
+ it("should validate minimum and maximum times", () => {
161
+ const schema = time({
162
+ format: "HH:mm",
163
+ min: "09:00",
164
+ max: "17:00"
165
+ })
166
+
167
+ // Valid times within range
168
+ expect(schema.parse("09:00")).toBe("09:00")
169
+ expect(schema.parse("12:30")).toBe("12:30")
170
+ expect(schema.parse("17:00")).toBe("17:00")
171
+
172
+ // Invalid times outside range
173
+ expect(() => schema.parse("08:59")).toThrow("Time must be after")
174
+ expect(() => schema.parse("17:01")).toThrow("Time must be before")
175
+ })
176
+
177
+ it("should validate hour ranges", () => {
178
+ const schema = time({
179
+ format: "HH:mm",
180
+ minHour: 9,
181
+ maxHour: 17
182
+ })
183
+
184
+ // Valid hours
185
+ expect(schema.parse("09:30")).toBe("09:30")
186
+ expect(schema.parse("17:45")).toBe("17:45")
187
+
188
+ // Invalid hours
189
+ expect(() => schema.parse("08:30")).toThrow("Hour must be between")
190
+ expect(() => schema.parse("18:30")).toThrow("Hour must be between")
191
+ })
192
+
193
+ it("should validate allowed hours", () => {
194
+ const schema = time({
195
+ format: "HH:mm",
196
+ allowedHours: [9, 12, 15, 18]
197
+ })
198
+
199
+ // Valid hours
200
+ expect(schema.parse("09:30")).toBe("09:30")
201
+ expect(schema.parse("12:00")).toBe("12:00")
202
+ expect(schema.parse("15:45")).toBe("15:45")
203
+ expect(schema.parse("18:00")).toBe("18:00")
204
+
205
+ // Invalid hours
206
+ expect(() => schema.parse("10:30")).toThrow("Hour must be between")
207
+ expect(() => schema.parse("16:30")).toThrow("Hour must be between")
208
+ })
209
+
210
+ it("should validate minute steps", () => {
211
+ const schema = time({
212
+ format: "HH:mm",
213
+ minuteStep: 15
214
+ })
215
+
216
+ // Valid minute steps (0, 15, 30, 45)
217
+ expect(schema.parse("14:00")).toBe("14:00")
218
+ expect(schema.parse("14:15")).toBe("14:15")
219
+ expect(schema.parse("14:30")).toBe("14:30")
220
+ expect(schema.parse("14:45")).toBe("14:45")
221
+
222
+ // Invalid minute steps
223
+ expect(() => schema.parse("14:05")).toThrow("minute")
224
+ expect(() => schema.parse("14:37")).toThrow("minute")
225
+ })
226
+ })
227
+
228
+ describe("transform function", () => {
229
+ it("should apply custom transform", () => {
230
+ const schema = time({
231
+ format: "HH:mm",
232
+ transform: (val) => val.toUpperCase()
233
+ })
234
+
235
+ expect(schema.parse("14:30")).toBe("14:30")
236
+ })
237
+
238
+ it("should apply transform before validation", () => {
239
+ const schema = time({
240
+ format: "HH:mm",
241
+ transform: (val) => val.replace(/\s+/g, "")
242
+ })
243
+
244
+ expect(schema.parse("1 4 : 3 0")).toBe("14:30")
245
+ })
246
+
247
+ it("should work with whitelist after transform", () => {
248
+ const schema = time({
249
+ format: "HH:mm",
250
+ transform: (val) => val.toLowerCase(),
251
+ whitelist: ["morning", "14:30"]
252
+ })
253
+
254
+ expect(schema.parse("MORNING")).toBe("morning")
255
+ expect(schema.parse("14:30")).toBe("14:30")
256
+ })
257
+ })
258
+
259
+ describe("input preprocessing", () => {
260
+ it("should handle string conversion", () => {
261
+ const schema = time({ format: "HH:mm" })
262
+
263
+ // Test string conversion of numbers
264
+ expect(() => schema.parse(1430)).toThrow("Must be in HH:mm format") // Invalid because not in HH:mm format
265
+ })
266
+
267
+ it("should trim whitespace", () => {
268
+ const schema = time({ format: "HH:mm" })
269
+
270
+ expect(schema.parse(" 14:30 ")).toBe("14:30")
271
+ expect(schema.parse("\t09:15\n")).toBe("09:15")
272
+ })
273
+ })
274
+
275
+ describe("utility function", () => {
276
+ describe("validateTimeFormat", () => {
277
+ it("should correctly validate time formats", () => {
278
+ // 24-hour format
279
+ expect(validateTimeFormat("14:30", "HH:mm")).toBe(true)
280
+ expect(validateTimeFormat("09:00", "HH:mm")).toBe(true)
281
+ expect(validateTimeFormat("25:30", "HH:mm")).toBe(false)
282
+ expect(validateTimeFormat("14:70", "HH:mm")).toBe(false)
283
+
284
+ // 12-hour format
285
+ expect(validateTimeFormat("02:30 PM", "hh:mm A")).toBe(true)
286
+ expect(validateTimeFormat("12:00 AM", "hh:mm A")).toBe(true)
287
+ expect(validateTimeFormat("13:30 PM", "hh:mm A")).toBe(false)
288
+ expect(validateTimeFormat("02:30", "hh:mm A")).toBe(false)
289
+
290
+ // With seconds
291
+ expect(validateTimeFormat("14:30:45", "HH:mm:ss")).toBe(true)
292
+ expect(validateTimeFormat("14:30:70", "HH:mm:ss")).toBe(false)
293
+
294
+ // Single digit hours
295
+ expect(validateTimeFormat("9:30", "H:mm")).toBe(true)
296
+ expect(validateTimeFormat("9:30 AM", "h:mm A")).toBe(true)
297
+ })
298
+ })
299
+
300
+ describe("parseTimeToMinutes", () => {
301
+ it("should parse 24-hour format correctly", () => {
302
+ expect(parseTimeToMinutes("00:00", "HH:mm")).toBe(0)
303
+ expect(parseTimeToMinutes("12:30", "HH:mm")).toBe(750) // 12*60 + 30
304
+ expect(parseTimeToMinutes("23:59", "HH:mm")).toBe(1439) // 23*60 + 59
305
+ })
306
+
307
+ it("should parse 12-hour format correctly", () => {
308
+ expect(parseTimeToMinutes("12:00 AM", "hh:mm A")).toBe(0)
309
+ expect(parseTimeToMinutes("12:30 PM", "hh:mm A")).toBe(750)
310
+ expect(parseTimeToMinutes("11:59 PM", "hh:mm A")).toBe(1439)
311
+ expect(parseTimeToMinutes("01:30 PM", "hh:mm A")).toBe(810) // 13*60 + 30
312
+ })
313
+
314
+ it("should handle invalid times", () => {
315
+ expect(parseTimeToMinutes("25:30", "HH:mm")).toBe(null)
316
+ expect(parseTimeToMinutes("14:70", "HH:mm")).toBe(null)
317
+ expect(parseTimeToMinutes("invalid", "HH:mm")).toBe(null)
318
+ })
319
+ })
320
+
321
+ describe("normalizeTime", () => {
322
+ it("should normalize 12-hour to 24-hour format", () => {
323
+ expect(normalizeTime("12:00 AM", "hh:mm A")).toBe("00:00")
324
+ expect(normalizeTime("12:30 PM", "hh:mm A")).toBe("12:30")
325
+ expect(normalizeTime("01:30 PM", "hh:mm A")).toBe("13:30")
326
+ expect(normalizeTime("11:59 PM", "hh:mm A")).toBe("23:59")
327
+ })
328
+
329
+ it("should normalize single digit hours", () => {
330
+ expect(normalizeTime("9:30", "H:mm")).toBe("09:30")
331
+ expect(normalizeTime("14:30", "H:mm")).toBe("14:30")
332
+ })
333
+
334
+ it("should handle seconds format", () => {
335
+ expect(normalizeTime("01:30:45 PM", "hh:mm:ss A")).toBe("13:30:45")
336
+ })
337
+ })
338
+ })
339
+
340
+ describe("i18n support", () => {
341
+ it("should use English messages by default", () => {
342
+ setLocale("en")
343
+ const schema = time({ format: "HH:mm" })
344
+
345
+ expect(() => schema.parse("")).toThrow("Required")
346
+ expect(() => schema.parse("invalid")).toThrow("Must be in HH:mm format")
347
+ })
348
+
349
+ it("should use Chinese messages when locale is zh-TW", () => {
350
+ setLocale("zh-TW")
351
+ const schema = time({ format: "HH:mm" })
352
+
353
+ expect(() => schema.parse("")).toThrow("必填")
354
+ expect(() => schema.parse("invalid")).toThrow("必須為 HH:mm 格式")
355
+ })
356
+
357
+ it("should support whitelist error messages", () => {
358
+ setLocale("en")
359
+ const schema = time({
360
+ format: "HH:mm",
361
+ whitelist: ["morning"],
362
+ whitelistOnly: true
363
+ })
364
+
365
+ expect(() => schema.parse("14:30")).toThrow("Time is not in the allowed list")
366
+ })
367
+
368
+ it("should support custom i18n messages", () => {
369
+ const schema = time({
370
+ format: "HH:mm",
371
+ i18n: {
372
+ en: {
373
+ required: "Time is required",
374
+ invalid: "Please enter a valid time"
375
+ },
376
+ "zh-TW": {
377
+ required: "請輸入時間",
378
+ invalid: "請輸入有效的時間格式"
379
+ }
380
+ }
381
+ })
382
+
383
+ setLocale("en")
384
+ expect(() => schema.parse("")).toThrow("Time is required")
385
+
386
+ setLocale("zh-TW")
387
+ expect(() => schema.parse("")).toThrow("請輸入時間")
388
+ })
389
+
390
+ it("should support custom whitelist messages", () => {
391
+ const schema = time({
392
+ format: "HH:mm",
393
+ whitelist: ["morning"],
394
+ whitelistOnly: true,
395
+ i18n: {
396
+ en: {
397
+ notInWhitelist: "This time is not allowed"
398
+ },
399
+ "zh-TW": {
400
+ notInWhitelist: "此時間不被允許"
401
+ }
402
+ }
403
+ })
404
+
405
+ setLocale("en")
406
+ expect(() => schema.parse("14:30")).toThrow("This time is not allowed")
407
+
408
+ setLocale("zh-TW")
409
+ expect(() => schema.parse("14:30")).toThrow("此時間不被允許")
410
+ })
411
+ })
412
+
413
+ describe("real world time scenarios", () => {
414
+ it("should validate business hours", () => {
415
+ const businessHours = time({
416
+ format: "HH:mm",
417
+ min: "09:00",
418
+ max: "17:00",
419
+ minuteStep: 30
420
+ })
421
+
422
+ expect(businessHours.parse("09:00")).toBe("09:00")
423
+ expect(businessHours.parse("12:30")).toBe("12:30")
424
+ expect(businessHours.parse("17:00")).toBe("17:00")
425
+
426
+ expect(() => businessHours.parse("08:30")).toThrow("Time must be after")
427
+ expect(() => businessHours.parse("17:30")).toThrow("Time must be before")
428
+ expect(() => businessHours.parse("12:15")).toThrow("minute")
429
+ })
430
+
431
+ it("should validate appointment slots", () => {
432
+ const appointmentSlots = time({
433
+ format: "hh:mm A",
434
+ allowedHours: [9, 10, 11, 14, 15, 16],
435
+ minuteStep: 15
436
+ })
437
+
438
+ expect(appointmentSlots.parse("09:00 AM")).toBe("09:00 AM")
439
+ expect(appointmentSlots.parse("02:15 PM")).toBe("02:15 PM")
440
+ expect(appointmentSlots.parse("04:45 PM")).toBe("04:45 PM")
441
+
442
+ expect(() => appointmentSlots.parse("12:00 PM")).toThrow("Hour must be between")
443
+ expect(() => appointmentSlots.parse("09:05 AM")).toThrow("minute")
444
+ })
445
+
446
+ it("should handle flexible time input", () => {
447
+ const flexibleTime = time({
448
+ format: "HH:mm",
449
+ whitelist: ["morning", "afternoon", "evening", "anytime"],
450
+ required: false
451
+ })
452
+
453
+ expect(flexibleTime.parse("morning")).toBe("morning")
454
+ expect(flexibleTime.parse("14:30")).toBe("14:30")
455
+ expect(flexibleTime.parse("")).toBe(null)
456
+ expect(() => flexibleTime.parse("invalid")).toThrow("Must be in HH:mm format")
457
+ })
458
+ })
459
+
460
+ describe("edge cases", () => {
461
+ it("should handle various input types", () => {
462
+ const schema = time({ format: "HH:mm" })
463
+
464
+ // Test different input types that should be converted to string
465
+ expect(schema.parse("14:30")).toBe("14:30")
466
+ })
467
+
468
+ it("should handle empty and whitespace inputs", () => {
469
+ const requiredSchema = time({ format: "HH:mm", required: true })
470
+ const optionalSchema = time({ format: "HH:mm", required: false })
471
+
472
+ expect(() => requiredSchema.parse("")).toThrow("Required")
473
+ expect(() => requiredSchema.parse(" ")).toThrow("Required")
474
+
475
+ expect(optionalSchema.parse("")).toBe(null)
476
+ expect(optionalSchema.parse(" ")).toBe(null)
477
+ })
478
+
479
+ it("should preserve valid format after transformation", () => {
480
+ const schema = time({
481
+ format: "HH:mm",
482
+ transform: (val) => val.replace(/[^0-9:]/g, "").replace(/^(\d{2})(\d{2})$/, "$1:$2")
483
+ })
484
+
485
+ expect(schema.parse("14abc:30def")).toBe("14:30")
486
+ expect(schema.parse("0915")).toBe("09:15")
487
+ })
488
+
489
+ it("should work with complex whitelist scenarios", () => {
490
+ const schema = time({
491
+ format: "HH:mm",
492
+ whitelist: ["14:30", "TBD", "flexible", ""],
493
+ whitelistOnly: true,
494
+ required: false
495
+ })
496
+
497
+ // Whitelist scenarios
498
+ expect(schema.parse("14:30")).toBe("14:30")
499
+ expect(schema.parse("TBD")).toBe("TBD")
500
+ expect(schema.parse("flexible")).toBe("flexible")
501
+ expect(schema.parse("")).toBe("")
502
+
503
+ // Not in the whitelist
504
+ expect(() => schema.parse("15:30")).toThrow("Time is not in the allowed list")
505
+ expect(() => schema.parse("other-value")).toThrow("Time is not in the allowed list")
506
+ })
507
+
508
+ it("should handle boundary cases for different formats", () => {
509
+ const schema24 = time({ format: "HH:mm" })
510
+ const schema12 = time({ format: "hh:mm A" })
511
+
512
+ // 24-hour boundary cases
513
+ expect(schema24.parse("00:00")).toBe("00:00")
514
+ expect(schema24.parse("23:59")).toBe("23:59")
515
+
516
+ // 12-hour boundary cases
517
+ expect(schema12.parse("12:00 AM")).toBe("12:00 AM") // Midnight
518
+ expect(schema12.parse("12:00 PM")).toBe("12:00 PM") // Noon
519
+ expect(schema12.parse("01:00 AM")).toBe("01:00 AM")
520
+ expect(schema12.parse("11:59 PM")).toBe("11:59 PM")
521
+
522
+ // Invalid boundary cases
523
+ expect(() => schema24.parse("24:00")).toThrow("Must be in")
524
+ expect(() => schema12.parse("00:00 AM")).toThrow("Must be in")
525
+ expect(() => schema12.parse("13:00 PM")).toThrow("Must be in")
526
+ })
527
+ })
528
+ })