@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.
- package/.claude/settings.local.json +6 -1
- package/README.md +465 -97
- package/dist/index.cjs +1628 -121
- package/dist/index.d.cts +2699 -2
- package/dist/index.d.ts +2699 -2
- package/dist/index.js +1610 -120
- package/package.json +1 -1
- package/src/i18n/locales/en.json +62 -0
- package/src/i18n/locales/zh-TW.json +62 -0
- package/src/index.ts +4 -0
- package/src/validators/common/boolean.ts +94 -0
- package/src/validators/common/date.ts +128 -0
- package/src/validators/common/datetime.ts +673 -0
- package/src/validators/common/email.ts +113 -0
- package/src/validators/common/file.ts +384 -0
- package/src/validators/common/id.ts +224 -12
- package/src/validators/common/number.ts +125 -0
- package/src/validators/common/password.ts +174 -2
- package/src/validators/common/text.ts +120 -0
- package/src/validators/common/time.ts +600 -0
- package/src/validators/common/url.ts +140 -0
- package/src/validators/taiwan/business-id.ts +124 -2
- package/src/validators/taiwan/fax.ts +147 -2
- package/src/validators/taiwan/mobile.ts +134 -2
- package/src/validators/taiwan/national-id.ts +227 -10
- package/src/validators/taiwan/postal-code.ts +1049 -0
- package/src/validators/taiwan/tel.ts +150 -2
- package/tests/common/datetime.test.ts +693 -0
- package/tests/common/file.test.ts +479 -0
- package/tests/common/time.test.ts +528 -0
- package/tests/taiwan/postal-code.test.ts +705 -0
|
@@ -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
|
+
})
|