@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,705 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest"
2
+ import { postalCode, setLocale } from "../../src"
3
+
4
+ describe("postalCode() features", () => {
5
+ beforeEach(() => setLocale("en"))
6
+
7
+ describe("3-digit postal code validation", () => {
8
+ it("should accept valid 3-digit postal codes", () => {
9
+ const schema = postalCode({ format: "3" })
10
+ expect(schema.parse("100")).toBe("100") // Taipei
11
+ expect(schema.parse("200")).toBe("200") // Keelung
12
+ expect(schema.parse("300")).toBe("300") // Taoyuan/Hsinchu
13
+ expect(schema.parse("400")).toBe("400") // Taichung
14
+ expect(schema.parse("500")).toBe("500") // Changhua
15
+ expect(schema.parse("600")).toBe("600") // Chiayi
16
+ expect(schema.parse("700")).toBe("700") // Tainan
17
+ expect(schema.parse("800")).toBe("800") // Kaohsiung
18
+ expect(schema.parse("900")).toBe("900") // Pingtung
19
+ })
20
+
21
+ it("should reject invalid 3-digit postal codes", () => {
22
+ const schema = postalCode({ format: "3" })
23
+ expect(() => schema.parse("000")).toThrow("Invalid Taiwan postal code")
24
+ expect(() => schema.parse("099")).toThrow("Invalid Taiwan postal code")
25
+ expect(() => schema.parse("999")).toThrow("Invalid Taiwan postal code")
26
+ })
27
+
28
+ it("should reject non-3-digit formats when format is '3'", () => {
29
+ const schema = postalCode({ format: "3" })
30
+ expect(() => schema.parse("10001")).toThrow("Only 3-digit postal codes are allowed")
31
+ expect(() => schema.parse("100001")).toThrow("Only 3-digit postal codes are allowed")
32
+ })
33
+
34
+ it("should validate specific Taipei area postal codes", () => {
35
+ const schema = postalCode({ format: "3" })
36
+ expect(schema.parse("100")).toBe("100") // Zhongzheng
37
+ expect(schema.parse("103")).toBe("103") // Datong
38
+ expect(schema.parse("104")).toBe("104") // Zhongshan
39
+ expect(schema.parse("105")).toBe("105") // Songshan
40
+ expect(schema.parse("106")).toBe("106") // Da'an
41
+ expect(schema.parse("108")).toBe("108") // Wanhua
42
+ expect(schema.parse("110")).toBe("110") // Xinyi
43
+ expect(schema.parse("111")).toBe("111") // Shilin
44
+ expect(schema.parse("112")).toBe("112") // Beitou
45
+ expect(schema.parse("114")).toBe("114") // Neihu
46
+ expect(schema.parse("115")).toBe("115") // Nangang
47
+ expect(schema.parse("116")).toBe("116") // Wenshan
48
+ })
49
+ })
50
+
51
+ describe("5-digit postal code validation", () => {
52
+ it("should accept valid 5-digit postal codes", () => {
53
+ const schema = postalCode({ format: "5" })
54
+ expect(schema.parse("10001")).toBe("10001")
55
+ expect(schema.parse("20001")).toBe("20001")
56
+ expect(schema.parse("30001")).toBe("30001")
57
+ expect(schema.parse("40001")).toBe("40001")
58
+ expect(schema.parse("50001")).toBe("50001")
59
+ })
60
+
61
+ it("should reject invalid 5-digit postal codes", () => {
62
+ const schema = postalCode({ format: "5" })
63
+ expect(() => schema.parse("00001")).toThrow("Invalid Taiwan postal code")
64
+ expect(() => schema.parse("09901")).toThrow("Invalid Taiwan postal code")
65
+ expect(() => schema.parse("99901")).toThrow("Invalid Taiwan postal code")
66
+ })
67
+
68
+ it("should reject non-5-digit formats when format is '5'", () => {
69
+ const schema = postalCode({ format: "5" })
70
+ expect(() => schema.parse("100")).toThrow("Only 5-digit postal codes are allowed")
71
+ expect(() => schema.parse("100001")).toThrow("Only 5-digit postal codes are allowed")
72
+ })
73
+
74
+ it("should validate 5-digit postal codes with valid prefixes", () => {
75
+ const schema = postalCode({ format: "5" })
76
+ expect(schema.parse("10099")).toBe("10099") // Valid Taipei prefix
77
+ expect(schema.parse("20099")).toBe("20099") // Valid Keelung prefix
78
+ expect(schema.parse("88099")).toBe("88099") // Valid Penghu prefix
79
+ })
80
+ })
81
+
82
+ describe("6-digit postal code validation", () => {
83
+ it("should accept valid 6-digit postal codes", () => {
84
+ const schema = postalCode({ format: "6" })
85
+ expect(schema.parse("100001")).toBe("100001")
86
+ expect(schema.parse("200001")).toBe("200001")
87
+ expect(schema.parse("300001")).toBe("300001")
88
+ expect(schema.parse("400001")).toBe("400001")
89
+ expect(schema.parse("500001")).toBe("500001")
90
+ })
91
+
92
+ it("should reject invalid 6-digit postal codes", () => {
93
+ const schema = postalCode({ format: "6" })
94
+ expect(() => schema.parse("000001")).toThrow("Invalid Taiwan postal code")
95
+ expect(() => schema.parse("099001")).toThrow("Invalid Taiwan postal code")
96
+ expect(() => schema.parse("999001")).toThrow("Invalid Taiwan postal code")
97
+ })
98
+
99
+ it("should reject non-6-digit formats when format is '6'", () => {
100
+ const schema = postalCode({ format: "6" })
101
+ expect(() => schema.parse("100")).toThrow("Only 6-digit postal codes are allowed")
102
+ expect(() => schema.parse("10001")).toThrow("Only 6-digit postal codes are allowed")
103
+ })
104
+
105
+ it("should validate 6-digit postal codes with all valid prefixes", () => {
106
+ const schema = postalCode({ format: "6" })
107
+ expect(schema.parse("100999")).toBe("100999") // Taipei
108
+ expect(schema.parse("880999")).toBe("880999") // Penghu
109
+ expect(schema.parse("890999")).toBe("890999") // Kinmen
110
+ expect(schema.parse("209999")).toBe("209999") // Lienchiang (Matsu)
111
+ })
112
+ })
113
+
114
+ describe("combined format validation", () => {
115
+ it("should accept both 3 and 5 digit formats with '3+5'", () => {
116
+ const schema = postalCode({ format: "3+5" })
117
+ expect(schema.parse("100")).toBe("100")
118
+ expect(schema.parse("10001")).toBe("10001")
119
+ expect(() => schema.parse("100001")).toThrow("Invalid Taiwan postal code")
120
+ })
121
+
122
+ it("should accept both 3 and 6 digit formats with '3+6' (default)", () => {
123
+ const schema = postalCode() // Default format is "3+6"
124
+ expect(schema.parse("100")).toBe("100")
125
+ expect(schema.parse("100001")).toBe("100001")
126
+ expect(() => schema.parse("10001")).toThrow("Invalid Taiwan postal code")
127
+ })
128
+
129
+ it("should accept both 5 and 6 digit formats with '5+6'", () => {
130
+ const schema = postalCode({ format: "5+6" })
131
+ expect(schema.parse("10001")).toBe("10001")
132
+ expect(schema.parse("100001")).toBe("100001")
133
+ expect(() => schema.parse("100")).toThrow("Invalid Taiwan postal code")
134
+ })
135
+
136
+ it("should accept all formats with 'all'", () => {
137
+ const schema = postalCode({ format: "all" })
138
+ expect(schema.parse("100")).toBe("100")
139
+ expect(schema.parse("10001")).toBe("10001")
140
+ expect(schema.parse("100001")).toBe("100001")
141
+ })
142
+ })
143
+
144
+ describe("dash and space handling", () => {
145
+ it("should handle dashes in postal codes when allowDashes is true", () => {
146
+ const schema = postalCode({ format: "all", allowDashes: true })
147
+ expect(schema.parse("100")).toBe("100")
148
+ expect(schema.parse("100-01")).toBe("10001")
149
+ expect(schema.parse("100-001")).toBe("100001")
150
+ expect(schema.parse("100 001")).toBe("100001")
151
+ })
152
+
153
+ it("should reject dashes when allowDashes is false", () => {
154
+ const schema = postalCode({ format: "all", allowDashes: false })
155
+ expect(schema.parse("100")).toBe("100")
156
+ expect(() => schema.parse("100-01")).toThrow("Invalid Taiwan postal code")
157
+ expect(() => schema.parse("100-001")).toThrow("Invalid Taiwan postal code")
158
+ })
159
+
160
+ it("should normalize various dash and space formats", () => {
161
+ const schema = postalCode({ format: "6", allowDashes: true })
162
+ expect(schema.parse("100-001")).toBe("100001")
163
+ expect(schema.parse("100 001")).toBe("100001")
164
+ expect(schema.parse("100 001")).toBe("100001")
165
+ expect(schema.parse("100---001")).toBe("100001")
166
+ })
167
+ })
168
+
169
+ describe("prefix filtering", () => {
170
+ it("should only allow specified prefixes", () => {
171
+ const schema = postalCode({
172
+ format: "all",
173
+ allowedPrefixes: ["100", "200", "300"]
174
+ })
175
+ expect(schema.parse("100")).toBe("100")
176
+ expect(schema.parse("10001")).toBe("10001")
177
+ expect(schema.parse("100001")).toBe("100001")
178
+ expect(schema.parse("200")).toBe("200")
179
+ expect(() => schema.parse("400")).toThrow("Invalid Taiwan postal code")
180
+ expect(() => schema.parse("40001")).toThrow("Invalid Taiwan postal code")
181
+ })
182
+
183
+ it("should block specified prefixes", () => {
184
+ const schema = postalCode({
185
+ format: "all",
186
+ blockedPrefixes: ["999", "000"]
187
+ })
188
+ expect(schema.parse("100")).toBe("100")
189
+ expect(() => schema.parse("999")).toThrow("Invalid Taiwan postal code")
190
+ expect(() => schema.parse("99901")).toThrow("Invalid Taiwan postal code")
191
+ expect(() => schema.parse("000")).toThrow("Invalid Taiwan postal code")
192
+ })
193
+
194
+ it("should prioritize allowedPrefixes over strict validation", () => {
195
+ const schema = postalCode({
196
+ format: "3",
197
+ allowedPrefixes: ["999"], // Not in official list
198
+ strictValidation: true
199
+ })
200
+ expect(schema.parse("999")).toBe("999")
201
+ })
202
+
203
+ it("should respect blockedPrefixes even with allowedPrefixes", () => {
204
+ const schema = postalCode({
205
+ format: "3",
206
+ allowedPrefixes: ["100", "200", "999"],
207
+ blockedPrefixes: ["999"]
208
+ })
209
+ expect(schema.parse("100")).toBe("100")
210
+ expect(schema.parse("200")).toBe("200")
211
+ expect(() => schema.parse("999")).toThrow("Invalid Taiwan postal code")
212
+ })
213
+ })
214
+
215
+ describe("strict validation", () => {
216
+ it("should validate against official postal code list when strict", () => {
217
+ const schema = postalCode({ format: "3", strictValidation: true })
218
+ expect(schema.parse("100")).toBe("100") // Valid official code
219
+ expect(() => schema.parse("199")).toThrow("Invalid Taiwan postal code") // Not in official list
220
+ })
221
+
222
+ it("should allow broader range when not strict", () => {
223
+ const schema = postalCode({ format: "3", strictValidation: false })
224
+ expect(schema.parse("100")).toBe("100") // Valid official code
225
+ expect(schema.parse("199")).toBe("199") // Not in official list but in range 100-999
226
+ expect(() => schema.parse("099")).toThrow("Invalid Taiwan postal code") // Still below 100
227
+ })
228
+
229
+ it("should validate all Taiwan regions with strict validation", () => {
230
+ const schema = postalCode({ format: "3", strictValidation: true })
231
+
232
+ // Major cities
233
+ expect(schema.parse("100")).toBe("100") // Taipei City
234
+ expect(schema.parse("200")).toBe("200") // Keelung City
235
+ expect(schema.parse("300")).toBe("300") // Taoyuan City
236
+ expect(schema.parse("400")).toBe("400") // Taichung City
237
+ expect(schema.parse("500")).toBe("500") // Changhua County
238
+ expect(schema.parse("600")).toBe("600") // Chiayi City
239
+ expect(schema.parse("700")).toBe("700") // Tainan City
240
+ expect(schema.parse("800")).toBe("800") // Kaohsiung City
241
+ expect(schema.parse("900")).toBe("900") // Pingtung County
242
+
243
+ // Eastern Taiwan
244
+ expect(schema.parse("260")).toBe("260") // Yilan County
245
+ expect(schema.parse("970")).toBe("970") // Hualien County
246
+ expect(schema.parse("950")).toBe("950") // Taitung County
247
+
248
+ // Offshore islands
249
+ expect(schema.parse("880")).toBe("880") // Penghu County
250
+ expect(schema.parse("890")).toBe("890") // Kinmen County
251
+ expect(schema.parse("209")).toBe("209") // Lienchiang County (Matsu)
252
+ })
253
+ })
254
+
255
+ describe("required and optional validation", () => {
256
+ it("should handle required validation", () => {
257
+ const schema = postalCode()
258
+ expect(() => schema.parse(null)).toThrow("Required")
259
+ expect(() => schema.parse(undefined)).toThrow("Required")
260
+ expect(() => schema.parse("")).toThrow("Required")
261
+ })
262
+
263
+ it("should allow null when not required", () => {
264
+ const schema = postalCode({ required: false })
265
+ expect(schema.parse(null)).toBe(null)
266
+ expect(schema.parse(undefined)).toBe(null)
267
+ expect(schema.parse("")).toBe(null)
268
+ })
269
+
270
+ it("should use default value when provided", () => {
271
+ const schema = postalCode({ defaultValue: "100001" })
272
+ expect(schema.parse("")).toBe("100001")
273
+ expect(schema.parse(null)).toBe("100001")
274
+ expect(schema.parse(undefined)).toBe("100001")
275
+ })
276
+
277
+ it("should use default value for optional fields", () => {
278
+ const schema = postalCode({ required: false, defaultValue: "100001" })
279
+ expect(schema.parse("")).toBe("100001")
280
+ expect(schema.parse(null)).toBe("100001")
281
+ expect(schema.parse(undefined)).toBe("100001")
282
+ })
283
+ })
284
+
285
+ describe("transform functionality", () => {
286
+ it("should apply transform function", () => {
287
+ const schema = postalCode({
288
+ format: "6",
289
+ transform: (val) => val.replace(/\D/g, "") // Remove non-digits
290
+ })
291
+ expect(schema.parse("100-001")).toBe("100001")
292
+ expect(schema.parse("100.001")).toBe("100001")
293
+ expect(schema.parse("100abc001")).toBe("100001")
294
+ })
295
+
296
+ it("should apply transform after dash removal", () => {
297
+ const schema = postalCode({
298
+ format: "3",
299
+ allowDashes: true,
300
+ transform: (val) => val.padEnd(3, "0") // Pad to 3 digits
301
+ })
302
+ expect(schema.parse("10")).toBe("100")
303
+ expect(schema.parse("1")).toBe("100")
304
+ })
305
+ })
306
+
307
+ describe("legacy 5-digit warning", () => {
308
+ it("should emit warning for 5-digit codes when warn5Digit is true", () => {
309
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
310
+ const schema = postalCode({ format: "all", warn5Digit: true })
311
+
312
+ schema.parse("10001") // Should emit warning
313
+ expect(consoleSpy).toHaveBeenCalledWith("5-digit postal codes are legacy format, consider using 6-digit format")
314
+
315
+ schema.parse("100") // Should not emit warning
316
+ schema.parse("100001") // Should not emit warning
317
+
318
+ consoleSpy.mockRestore()
319
+ })
320
+
321
+ it("should not emit warning when warn5Digit is false", () => {
322
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
323
+ const schema = postalCode({ format: "all", warn5Digit: false })
324
+
325
+ schema.parse("10001") // Should not emit warning
326
+ expect(consoleSpy).not.toHaveBeenCalled()
327
+
328
+ consoleSpy.mockRestore()
329
+ })
330
+
331
+ it("should not emit warning for 5-digit only format", () => {
332
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
333
+ const schema = postalCode({ format: "5", warn5Digit: true })
334
+
335
+ schema.parse("10001") // Should not emit warning for 5-digit only format
336
+ expect(consoleSpy).not.toHaveBeenCalled()
337
+
338
+ consoleSpy.mockRestore()
339
+ })
340
+ })
341
+
342
+ describe("custom i18n messages", () => {
343
+ it("should use custom messages when provided", () => {
344
+ const schema = postalCode({
345
+ format: "3",
346
+ i18n: {
347
+ en: {
348
+ required: "Custom required message",
349
+ invalid: "Custom invalid message",
350
+ format3Only: "Custom 3-digit only message",
351
+ },
352
+ "zh-TW": {
353
+ required: "客製化必填訊息",
354
+ invalid: "客製化無效訊息",
355
+ format3Only: "客製化僅限3碼訊息",
356
+ },
357
+ },
358
+ })
359
+
360
+ expect(() => schema.parse("")).toThrow("Custom required message")
361
+ expect(() => schema.parse("999")).toThrow("Custom invalid message")
362
+ expect(() => schema.parse("10001")).toThrow("Custom 3-digit only message")
363
+ })
364
+
365
+ it("should fallback to default messages when custom not provided", () => {
366
+ const schema = postalCode({
367
+ format: "6",
368
+ i18n: {
369
+ en: {
370
+ required: "Custom required message",
371
+ },
372
+ "zh-TW": {
373
+ required: "客製化必填訊息",
374
+ },
375
+ },
376
+ })
377
+
378
+ expect(() => schema.parse("")).toThrow("Custom required message")
379
+ expect(() => schema.parse("100")).toThrow("Only 6-digit postal codes are allowed") // Default message
380
+ })
381
+
382
+ it("should use correct locale for custom messages", () => {
383
+ setLocale("en")
384
+ const schemaEn = postalCode({
385
+ format: "3",
386
+ i18n: {
387
+ en: {
388
+ invalid: "English invalid message",
389
+ },
390
+ "zh-TW": {
391
+ invalid: "繁體中文無效訊息",
392
+ },
393
+ },
394
+ })
395
+ expect(() => schemaEn.parse("999")).toThrow("English invalid message")
396
+
397
+ setLocale("zh-TW")
398
+ const schemaZh = postalCode({
399
+ format: "3",
400
+ i18n: {
401
+ en: {
402
+ invalid: "English invalid message",
403
+ },
404
+ "zh-TW": {
405
+ invalid: "繁體中文無效訊息",
406
+ },
407
+ },
408
+ })
409
+ expect(() => schemaZh.parse("999")).toThrow("繁體中文無效訊息")
410
+ })
411
+ })
412
+
413
+ describe("complex scenarios", () => {
414
+ it("should work with multiple validations", () => {
415
+ const schema = postalCode({
416
+ format: "6",
417
+ allowDashes: true,
418
+ strictValidation: true,
419
+ allowedPrefixes: ["100", "200", "300"],
420
+ transform: (val) => val.replace(/\D/g, "").padEnd(6, "0")
421
+ })
422
+
423
+ expect(schema.parse("100-001")).toBe("100001")
424
+ expect(schema.parse("200")).toBe("200000")
425
+ expect(schema.parse("300001")).toBe("300001")
426
+
427
+ expect(() => schema.parse("400-001")).toThrow("Invalid Taiwan postal code") // Not in allowedPrefixes
428
+ expect(schema.parse("100")).toBe("100000") // After transform and padding, becomes "100000" which is valid
429
+ })
430
+
431
+ it("should handle edge cases with transforms and dashes", () => {
432
+ const schema = postalCode({
433
+ format: "all",
434
+ allowDashes: true,
435
+ transform: (val) => val.toUpperCase().replace(/[^0-9-]/g, "")
436
+ })
437
+
438
+ expect(schema.parse("100ABC-001DEF")).toBe("100001")
439
+ expect(schema.parse("200XYZ")).toBe("200")
440
+ expect(schema.parse("300-01#$%")).toBe("30001")
441
+ })
442
+
443
+ it("should validate comprehensive Taiwan postal code coverage", () => {
444
+ const schema = postalCode({ format: "all", strictValidation: true })
445
+
446
+ // Test various regions
447
+ const validCodes = [
448
+ // Taipei area
449
+ "100", "103", "104", "105", "106", "108", "110", "111", "112", "114", "115", "116",
450
+ "10001", "10301", "100001", "103001",
451
+
452
+ // New Taipei area
453
+ "220", "221", "222", "223", "224", "226", "227", "228",
454
+ "22001", "221001",
455
+
456
+ // Taoyuan area
457
+ "320", "324", "325", "326", "327", "328", "330", "333",
458
+ "32001", "320001",
459
+
460
+ // Offshore islands
461
+ "880", "881", "882", "883", "884", "885", // Penghu
462
+ "890", "891", "892", "893", "894", "895", "896", // Kinmen
463
+ "209", "210", "211", "212", // Lienchiang (Matsu)
464
+ "88001", "890001", "209001"
465
+ ]
466
+
467
+ validCodes.forEach(code => {
468
+ expect(schema.parse(code)).toBe(code)
469
+ })
470
+ })
471
+
472
+ it("should work with real-world postal codes", () => {
473
+ const schema = postalCode({ format: "all", allowDashes: true })
474
+
475
+ // Real Taiwan postal codes
476
+ expect(schema.parse("100")).toBe("100") // Taipei Main Post Office
477
+ expect(schema.parse("110")).toBe("110") // Xinyi District, Taipei
478
+ expect(schema.parse("220")).toBe("220") // Banqiao District, New Taipei
479
+ expect(schema.parse("300")).toBe("300") // East District, Hsinchu City
480
+ expect(schema.parse("400")).toBe("400") // Central District, Taichung
481
+ expect(schema.parse("700")).toBe("700") // Central District, Tainan
482
+ expect(schema.parse("800")).toBe("800") // Xinxing District, Kaohsiung
483
+ expect(schema.parse("880")).toBe("880") // Magong City, Penghu
484
+ expect(schema.parse("890")).toBe("890") // Jincheng Township, Kinmen
485
+
486
+ // With dashes
487
+ expect(schema.parse("100-01")).toBe("10001") // 5-digit format
488
+ expect(schema.parse("100-001")).toBe("100001") // 6-digit format
489
+ })
490
+ })
491
+
492
+ describe("strict suffix validation with regional ranges", () => {
493
+ it("should validate 5-digit suffix ranges for major cities", () => {
494
+ const schema = postalCode({ format: "5", strictSuffixValidation: true })
495
+ // Taipei areas (full range 01-99)
496
+ expect(schema.parse("10001")).toBe("10001") // Valid suffix 01
497
+ expect(schema.parse("10099")).toBe("10099") // Valid suffix 99
498
+ expect(schema.parse("11050")).toBe("11050") // Xinyi District mid-range
499
+ expect(() => schema.parse("10000")).toThrow("Invalid postal code suffix") // Invalid suffix 00
500
+ })
501
+
502
+ it("should validate 6-digit suffix ranges for major cities", () => {
503
+ const schema = postalCode({ format: "6", strictSuffixValidation: true })
504
+ // Taipei areas (full range 001-999)
505
+ expect(schema.parse("100001")).toBe("100001") // Valid suffix 001
506
+ expect(schema.parse("100999")).toBe("100999") // Valid suffix 999
507
+ expect(schema.parse("110500")).toBe("110500") // Xinyi District mid-range
508
+ expect(() => schema.parse("100000")).toThrow("Invalid postal code suffix") // Invalid suffix 000
509
+ })
510
+
511
+ it("should validate restricted ranges for smaller areas", () => {
512
+ const schema = postalCode({ format: "all", strictSuffixValidation: true })
513
+
514
+ // Penghu (limited range)
515
+ expect(schema.parse("88001")).toBe("88001") // Valid for Penghu
516
+ expect(schema.parse("88050")).toBe("88050") // Within Penghu 5-digit range
517
+ expect(schema.parse("880001")).toBe("880001") // Valid for Penghu 6-digit
518
+ expect(schema.parse("880500")).toBe("880500") // Within Penghu 6-digit range
519
+ expect(() => schema.parse("88099")).toThrow("Invalid postal code suffix") // Beyond Penghu 5-digit range
520
+ expect(() => schema.parse("880999")).toThrow("Invalid postal code suffix") // Beyond Penghu 6-digit range
521
+
522
+ // Kinmen (more limited range)
523
+ expect(schema.parse("89001")).toBe("89001") // Valid for Kinmen
524
+ expect(schema.parse("89030")).toBe("89030") // Within Kinmen range
525
+ expect(() => schema.parse("89050")).toThrow("Invalid postal code suffix") // Beyond Kinmen range
526
+
527
+ // Matsu (most limited range)
528
+ expect(schema.parse("20901")).toBe("20901") // Valid for Matsu
529
+ expect(schema.parse("20920")).toBe("20920") // Within Matsu range
530
+ expect(() => schema.parse("20930")).toThrow("Invalid postal code suffix") // Beyond Matsu range
531
+ })
532
+
533
+ it("should allow any suffix when strictSuffixValidation is disabled", () => {
534
+ const schema = postalCode({ format: "all", strictSuffixValidation: false })
535
+ expect(schema.parse("10000")).toBe("10000") // Suffix 00 allowed
536
+ expect(schema.parse("100000")).toBe("100000") // Suffix 000 allowed
537
+ expect(schema.parse("10099")).toBe("10099") // Normal suffix
538
+ })
539
+ })
540
+
541
+ describe("5-digit deprecation", () => {
542
+ it("should reject 5-digit codes when deprecate5Digit is enabled", () => {
543
+ const schema = postalCode({ format: "all", deprecate5Digit: true })
544
+ expect(schema.parse("100")).toBe("100") // 3-digit still allowed
545
+ expect(schema.parse("100001")).toBe("100001") // 6-digit still allowed
546
+ expect(() => schema.parse("10001")).toThrow("5-digit postal codes are deprecated") // 5-digit rejected
547
+ })
548
+
549
+ it("should allow 5-digit codes when deprecate5Digit is disabled", () => {
550
+ const schema = postalCode({ format: "all", deprecate5Digit: false })
551
+ expect(schema.parse("10001")).toBe("10001") // 5-digit allowed
552
+ })
553
+ })
554
+
555
+ describe("combined strict validation scenarios", () => {
556
+ it("should work with both strictSuffixValidation and deprecate5Digit", () => {
557
+ const schema = postalCode({
558
+ format: "6",
559
+ strictSuffixValidation: true,
560
+ deprecate5Digit: true
561
+ })
562
+ expect(schema.parse("100001")).toBe("100001") // Valid 6-digit
563
+ expect(() => schema.parse("100000")).toThrow("Invalid postal code suffix") // Invalid suffix
564
+ expect(() => schema.parse("10001")).toThrow("Only 6-digit postal codes are allowed") // Wrong format
565
+ })
566
+
567
+ it("should provide specific error for real-world validation scenarios", () => {
568
+ const realWorldSchema = postalCode({
569
+ format: "6",
570
+ strictSuffixValidation: true,
571
+ strictValidation: true
572
+ })
573
+
574
+ // Valid real postal codes from major cities
575
+ expect(realWorldSchema.parse("100001")).toBe("100001") // Taipei Zhongzheng
576
+ expect(realWorldSchema.parse("110015")).toBe("110015") // Taipei Xinyi
577
+ expect(realWorldSchema.parse("800001")).toBe("800001") // Kaohsiung Xinxing
578
+ expect(realWorldSchema.parse("400500")).toBe("400500") // Taichung Central
579
+ expect(realWorldSchema.parse("700300")).toBe("700300") // Tainan Central
580
+
581
+ // Valid postal codes from smaller areas with restricted ranges
582
+ expect(realWorldSchema.parse("880025")).toBe("880025") // Penghu (within range)
583
+ expect(realWorldSchema.parse("890015")).toBe("890015") // Kinmen (within range)
584
+ expect(realWorldSchema.parse("209010")).toBe("209010") // Matsu (within range)
585
+
586
+ // Invalid due to suffix being 000
587
+ expect(() => realWorldSchema.parse("100000")).toThrow("Invalid postal code suffix")
588
+
589
+ // Invalid due to suffix exceeding area-specific ranges
590
+ expect(() => realWorldSchema.parse("880600")).toThrow("Invalid postal code suffix") // Penghu beyond range
591
+ expect(() => realWorldSchema.parse("890350")).toThrow("Invalid postal code suffix") // Kinmen beyond range
592
+ expect(() => realWorldSchema.parse("209250")).toThrow("Invalid postal code suffix") // Matsu beyond range
593
+
594
+ // Invalid due to prefix not in official list
595
+ expect(() => realWorldSchema.parse("999001")).toThrow("Invalid Taiwan postal code")
596
+ })
597
+ })
598
+
599
+ describe("regional-specific validation", () => {
600
+ it("should validate major cities with full ranges", () => {
601
+ const schema = postalCode({ format: "all", strictSuffixValidation: true })
602
+
603
+ // Taipei City areas - should have full 01-99 and 001-999 ranges
604
+ const taipeiAreas = ["100", "103", "104", "105", "106", "108", "110", "111", "112", "114", "115", "116"]
605
+ taipeiAreas.forEach(area => {
606
+ expect(schema.parse(`${area}01`)).toBe(`${area}01`) // Min 5-digit
607
+ expect(schema.parse(`${area}99`)).toBe(`${area}99`) // Max 5-digit
608
+ expect(schema.parse(`${area}001`)).toBe(`${area}001`) // Min 6-digit
609
+ expect(schema.parse(`${area}999`)).toBe(`${area}999`) // Max 6-digit
610
+ })
611
+
612
+ // Test major cities
613
+ expect(schema.parse("32050")).toBe("32050") // Taoyuan
614
+ expect(schema.parse("40050")).toBe("40050") // Taichung
615
+ expect(schema.parse("70050")).toBe("70050") // Tainan
616
+ expect(schema.parse("80050")).toBe("80050") // Kaohsiung
617
+ })
618
+
619
+ it("should enforce restricted ranges for offshore islands", () => {
620
+ const schema = postalCode({ format: "all", strictSuffixValidation: true })
621
+
622
+ // Penghu County (880) - limited to 01-50 and 001-500
623
+ expect(schema.parse("88001")).toBe("88001")
624
+ expect(schema.parse("88050")).toBe("88050")
625
+ expect(schema.parse("880001")).toBe("880001")
626
+ expect(schema.parse("880500")).toBe("880500")
627
+ expect(() => schema.parse("88051")).toThrow() // Beyond range
628
+ expect(() => schema.parse("880501")).toThrow() // Beyond range
629
+
630
+ // Kinmen County (890) - limited to 01-30 and 001-300
631
+ expect(schema.parse("89001")).toBe("89001")
632
+ expect(schema.parse("89030")).toBe("89030")
633
+ expect(schema.parse("890001")).toBe("890001")
634
+ expect(schema.parse("890300")).toBe("890300")
635
+ expect(() => schema.parse("89031")).toThrow() // Beyond range
636
+ expect(() => schema.parse("890301")).toThrow() // Beyond range
637
+
638
+ // Lienchiang County/Matsu (209) - limited to 01-20 and 001-200
639
+ expect(schema.parse("20901")).toBe("20901")
640
+ expect(schema.parse("20920")).toBe("20920")
641
+ expect(schema.parse("209001")).toBe("209001")
642
+ expect(schema.parse("209200")).toBe("209200")
643
+ expect(() => schema.parse("20921")).toThrow() // Beyond range
644
+ expect(() => schema.parse("209201")).toThrow() // Beyond range
645
+ })
646
+
647
+ it("should use default ranges for areas not in specific mapping", () => {
648
+ const schema = postalCode({ format: "all", strictSuffixValidation: true })
649
+
650
+ // Areas not specifically mapped should use default ranges (01-99, 001-999)
651
+ expect(schema.parse("26001")).toBe("26001") // Yilan - uses default
652
+ expect(schema.parse("26099")).toBe("26099")
653
+ expect(schema.parse("260001")).toBe("260001")
654
+ expect(schema.parse("260999")).toBe("260999")
655
+ expect(() => schema.parse("26000")).toThrow() // Invalid suffix 00
656
+ expect(() => schema.parse("260000")).toThrow() // Invalid suffix 000
657
+ })
658
+ })
659
+
660
+ describe("edge cases", () => {
661
+ it("should handle empty and whitespace inputs", () => {
662
+ const schema = postalCode({ required: false })
663
+ expect(schema.parse("")).toBe(null)
664
+ expect(schema.parse(" ")).toBe(null)
665
+ expect(schema.parse("\t")).toBe(null)
666
+ expect(schema.parse("\n")).toBe(null)
667
+ })
668
+
669
+ it("should handle numeric inputs", () => {
670
+ const schema = postalCode({ format: "3" })
671
+ expect(schema.parse(100)).toBe("100")
672
+ expect(schema.parse(200)).toBe("200")
673
+ })
674
+
675
+ it("should reject codes with letters when not using transform", () => {
676
+ const schema = postalCode({ format: "3", allowDashes: false })
677
+ expect(() => schema.parse("10A")).toThrow("Invalid Taiwan postal code")
678
+ expect(() => schema.parse("ABC")).toThrow("Invalid Taiwan postal code")
679
+ })
680
+
681
+ it("should handle very specific area restrictions", () => {
682
+ // Only allow Taipei city areas
683
+ const taipeiOnlySchema = postalCode({
684
+ format: "all",
685
+ allowedPrefixes: ["100", "103", "104", "105", "106", "108", "110", "111", "112", "114", "115", "116"]
686
+ })
687
+
688
+ expect(taipeiOnlySchema.parse("100")).toBe("100")
689
+ expect(taipeiOnlySchema.parse("110001")).toBe("110001")
690
+ expect(() => taipeiOnlySchema.parse("220")).toThrow("Invalid Taiwan postal code") // New Taipei, not Taipei
691
+ })
692
+
693
+ it("should handle format combinations correctly", () => {
694
+ const schema35 = postalCode({ format: "3+5" })
695
+ expect(schema35.parse("100")).toBe("100")
696
+ expect(schema35.parse("10001")).toBe("10001")
697
+ expect(() => schema35.parse("100001")).toThrow("Invalid Taiwan postal code")
698
+
699
+ const schema56 = postalCode({ format: "5+6" })
700
+ expect(schema56.parse("10001")).toBe("10001")
701
+ expect(schema56.parse("100001")).toBe("100001")
702
+ expect(() => schema56.parse("100")).toThrow("Invalid Taiwan postal code")
703
+ })
704
+ })
705
+ })