@hy_ong/zod-kit 0.0.4 → 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 +28 -0
- package/LICENSE +21 -0
- package/README.md +465 -97
- package/debug.js +21 -0
- package/debug.ts +16 -0
- package/dist/index.cjs +3127 -146
- package/dist/index.d.cts +3021 -25
- package/dist/index.d.ts +3021 -25
- package/dist/index.js +3081 -144
- package/eslint.config.mts +8 -0
- package/package.json +10 -9
- package/src/config.ts +1 -1
- package/src/i18n/locales/en.json +161 -25
- package/src/i18n/locales/zh-TW.json +165 -26
- package/src/index.ts +17 -7
- package/src/validators/common/boolean.ts +191 -0
- package/src/validators/common/date.ts +299 -0
- package/src/validators/common/datetime.ts +673 -0
- package/src/validators/common/email.ts +313 -0
- package/src/validators/common/file.ts +384 -0
- package/src/validators/common/id.ts +471 -0
- package/src/validators/common/number.ts +319 -0
- package/src/validators/common/password.ts +386 -0
- package/src/validators/common/text.ts +271 -0
- package/src/validators/common/time.ts +600 -0
- package/src/validators/common/url.ts +347 -0
- package/src/validators/taiwan/business-id.ts +262 -0
- package/src/validators/taiwan/fax.ts +327 -0
- package/src/validators/taiwan/mobile.ts +242 -0
- package/src/validators/taiwan/national-id.ts +425 -0
- package/src/validators/taiwan/postal-code.ts +1049 -0
- package/src/validators/taiwan/tel.ts +330 -0
- package/tests/common/boolean.test.ts +340 -92
- package/tests/common/date.test.ts +458 -0
- package/tests/common/datetime.test.ts +693 -0
- package/tests/common/email.test.ts +232 -60
- package/tests/common/file.test.ts +479 -0
- package/tests/common/id.test.ts +535 -0
- package/tests/common/number.test.ts +230 -60
- package/tests/common/password.test.ts +271 -44
- package/tests/common/text.test.ts +210 -13
- package/tests/common/time.test.ts +528 -0
- package/tests/common/url.test.ts +492 -67
- package/tests/taiwan/business-id.test.ts +240 -0
- package/tests/taiwan/fax.test.ts +463 -0
- package/tests/taiwan/mobile.test.ts +373 -0
- package/tests/taiwan/national-id.test.ts +435 -0
- package/tests/taiwan/postal-code.test.ts +705 -0
- package/tests/taiwan/tel.test.ts +467 -0
- package/eslint.config.mjs +0 -10
- package/src/common/boolean.ts +0 -36
- package/src/common/date.ts +0 -43
- package/src/common/email.ts +0 -44
- package/src/common/integer.ts +0 -46
- package/src/common/number.ts +0 -37
- package/src/common/password.ts +0 -33
- package/src/common/text.ts +0 -34
- package/src/common/url.ts +0 -37
- package/tests/common/integer.test.ts +0 -90
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest"
|
|
2
|
+
import { datetime, setLocale, validateDateTimeFormat, parseDateTimeValue, normalizeDateTimeValue } from "../../src"
|
|
3
|
+
import dayjs from "dayjs"
|
|
4
|
+
|
|
5
|
+
describe("Taiwan datetime() validator", () => {
|
|
6
|
+
beforeEach(() => setLocale("en"))
|
|
7
|
+
|
|
8
|
+
describe("basic functionality", () => {
|
|
9
|
+
it("should validate correct datetime formats", () => {
|
|
10
|
+
const schemaISO = datetime({ format: "YYYY-MM-DD HH:mm" })
|
|
11
|
+
const schemaUS = datetime({ format: "MM/DD/YYYY HH:mm" })
|
|
12
|
+
const schemaEU = datetime({ format: "DD/MM/YYYY HH:mm" })
|
|
13
|
+
const schemaWithSeconds = datetime({ format: "YYYY-MM-DD HH:mm:ss" })
|
|
14
|
+
const schema12Hour = datetime({ format: "YYYY-MM-DD hh:mm A" })
|
|
15
|
+
|
|
16
|
+
// ISO format
|
|
17
|
+
expect(schemaISO.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
|
|
18
|
+
expect(schemaISO.parse("2024-12-31 23:59")).toBe("2024-12-31 23:59")
|
|
19
|
+
expect(schemaISO.parse("2024-01-01 00:00")).toBe("2024-01-01 00:00")
|
|
20
|
+
|
|
21
|
+
// US format
|
|
22
|
+
expect(schemaUS.parse("03/15/2024 14:30")).toBe("03/15/2024 14:30")
|
|
23
|
+
expect(schemaUS.parse("12/31/2024 23:59")).toBe("12/31/2024 23:59")
|
|
24
|
+
|
|
25
|
+
// EU format
|
|
26
|
+
expect(schemaEU.parse("15/03/2024 14:30")).toBe("15/03/2024 14:30")
|
|
27
|
+
expect(schemaEU.parse("31/12/2024 23:59")).toBe("31/12/2024 23:59")
|
|
28
|
+
|
|
29
|
+
// With seconds
|
|
30
|
+
expect(schemaWithSeconds.parse("2024-03-15 14:30:45")).toBe("2024-03-15 14:30:45")
|
|
31
|
+
expect(schemaWithSeconds.parse("2024-01-01 00:00:00")).toBe("2024-01-01 00:00:00")
|
|
32
|
+
|
|
33
|
+
// 12-hour format
|
|
34
|
+
expect(schema12Hour.parse("2024-03-15 02:30 PM")).toBe("2024-03-15 02:30 PM")
|
|
35
|
+
expect(schema12Hour.parse("2024-03-15 12:00 AM")).toBe("2024-03-15 12:00 AM")
|
|
36
|
+
expect(schema12Hour.parse("2024-03-15 12:00 PM")).toBe("2024-03-15 12:00 PM")
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("should validate special formats", () => {
|
|
40
|
+
const isoSchema = datetime({ format: "ISO" })
|
|
41
|
+
const rfcSchema = datetime({ format: "RFC" })
|
|
42
|
+
const unixSchema = datetime({ format: "UNIX" })
|
|
43
|
+
|
|
44
|
+
// ISO 8601
|
|
45
|
+
expect(isoSchema.parse("2024-03-15T14:30:45.000Z")).toBe("2024-03-15T14:30:45.000Z")
|
|
46
|
+
expect(isoSchema.parse("2024-03-15T14:30:45Z")).toBe("2024-03-15T14:30:45Z")
|
|
47
|
+
|
|
48
|
+
// RFC 2822
|
|
49
|
+
expect(rfcSchema.parse("Fri, 15 Mar 2024 14:30:45 GMT")).toBe("Fri, 15 Mar 2024 14:30:45 GMT")
|
|
50
|
+
|
|
51
|
+
// Unix timestamp
|
|
52
|
+
expect(unixSchema.parse("1710508245")).toBe("1710508245")
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("should reject invalid datetime formats", () => {
|
|
56
|
+
const schema = datetime({ format: "YYYY-MM-DD HH:mm" })
|
|
57
|
+
|
|
58
|
+
// Invalid date formats
|
|
59
|
+
expect(() => schema.parse("2024-13-15 14:30")).toThrow("Must be in YYYY-MM-DD HH:mm format")
|
|
60
|
+
expect(() => schema.parse("2024-03-32 14:30")).toThrow("Must be in YYYY-MM-DD HH:mm format")
|
|
61
|
+
expect(() => schema.parse("2024-03-15 25:30")).toThrow("Must be in YYYY-MM-DD HH:mm format")
|
|
62
|
+
expect(() => schema.parse("2024-03-15 14:70")).toThrow("Must be in YYYY-MM-DD HH:mm format")
|
|
63
|
+
expect(() => schema.parse("abc")).toThrow("Must be in YYYY-MM-DD HH:mm format")
|
|
64
|
+
expect(() => schema.parse("2024/03/15 14:30")).toThrow("Must be in YYYY-MM-DD HH:mm format") // Wrong format
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("should handle whitespace trimming", () => {
|
|
68
|
+
const schema = datetime({ format: "YYYY-MM-DD HH:mm" })
|
|
69
|
+
|
|
70
|
+
expect(schema.parse(" 2024-03-15 14:30 ")).toBe("2024-03-15 14:30")
|
|
71
|
+
expect(schema.parse("\t2024-03-15 09:15\n")).toBe("2024-03-15 09:15")
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe("whitelist functionality", () => {
|
|
76
|
+
it("should accept any string in whitelist regardless of format", () => {
|
|
77
|
+
const schema = datetime({
|
|
78
|
+
format: "YYYY-MM-DD HH:mm",
|
|
79
|
+
whitelist: ["now", "tomorrow", "TBD"]
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
expect(schema.parse("now")).toBe("now")
|
|
83
|
+
expect(schema.parse("tomorrow")).toBe("tomorrow")
|
|
84
|
+
expect(schema.parse("TBD")).toBe("TBD")
|
|
85
|
+
expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30") // Valid datetime still works
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("should reject values not in whitelist when whitelistOnly is true", () => {
|
|
89
|
+
const schema = datetime({
|
|
90
|
+
format: "YYYY-MM-DD HH:mm",
|
|
91
|
+
whitelist: ["now", "2024-03-15 14:30"],
|
|
92
|
+
whitelistOnly: true
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
expect(schema.parse("now")).toBe("now")
|
|
96
|
+
expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
|
|
97
|
+
|
|
98
|
+
// Invalid datetimes not in whitelist should be rejected
|
|
99
|
+
expect(() => schema.parse("2024-03-15 15:30")).toThrow("DateTime is not in the allowed list")
|
|
100
|
+
expect(() => schema.parse("later")).toThrow("DateTime is not in the allowed list")
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("should work with empty whitelist", () => {
|
|
104
|
+
const schema = datetime({
|
|
105
|
+
format: "YYYY-MM-DD HH:mm",
|
|
106
|
+
whitelist: []
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// With empty whitelist, should still validate datetime format
|
|
110
|
+
expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
|
|
111
|
+
expect(() => schema.parse("invalid")).toThrow("Must be in YYYY-MM-DD HH:mm format")
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("should prioritize whitelist over format validation", () => {
|
|
115
|
+
const schema = datetime({
|
|
116
|
+
format: "YYYY-MM-DD HH:mm",
|
|
117
|
+
whitelist: ["anytime", "flexible", "TBD"]
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
expect(schema.parse("anytime")).toBe("anytime")
|
|
121
|
+
expect(schema.parse("flexible")).toBe("flexible")
|
|
122
|
+
expect(schema.parse("TBD")).toBe("TBD")
|
|
123
|
+
expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30") // Valid datetime still works
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe("required/optional behavior", () => {
|
|
128
|
+
it("should handle required=true (default)", () => {
|
|
129
|
+
const schema = datetime({ format: "YYYY-MM-DD HH:mm" })
|
|
130
|
+
|
|
131
|
+
expect(() => schema.parse("")).toThrow("Required")
|
|
132
|
+
expect(() => schema.parse(null)).toThrow("Required")
|
|
133
|
+
expect(() => schema.parse(undefined)).toThrow("Required")
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("should handle required=false", () => {
|
|
137
|
+
const schema = datetime({ format: "YYYY-MM-DD HH:mm", required: false })
|
|
138
|
+
|
|
139
|
+
expect(schema.parse("")).toBe(null)
|
|
140
|
+
expect(schema.parse(null)).toBe(null)
|
|
141
|
+
expect(schema.parse(undefined)).toBe(null)
|
|
142
|
+
expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("should use default values", () => {
|
|
146
|
+
const requiredSchema = datetime({
|
|
147
|
+
format: "YYYY-MM-DD HH:mm",
|
|
148
|
+
defaultValue: "2024-01-01 12:00"
|
|
149
|
+
})
|
|
150
|
+
const optionalSchema = datetime({
|
|
151
|
+
format: "YYYY-MM-DD HH:mm",
|
|
152
|
+
required: false,
|
|
153
|
+
defaultValue: "2024-01-01 12:00"
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
expect(requiredSchema.parse("")).toBe("2024-01-01 12:00")
|
|
157
|
+
expect(requiredSchema.parse(null)).toBe("2024-01-01 12:00")
|
|
158
|
+
|
|
159
|
+
expect(optionalSchema.parse("")).toBe("2024-01-01 12:00")
|
|
160
|
+
expect(optionalSchema.parse(null)).toBe("2024-01-01 12:00")
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it("should handle whitelist with optional fields", () => {
|
|
164
|
+
const schema = datetime({
|
|
165
|
+
format: "YYYY-MM-DD HH:mm",
|
|
166
|
+
required: false,
|
|
167
|
+
whitelist: ["flexible", "2024-03-15 14:30"],
|
|
168
|
+
whitelistOnly: true
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
expect(schema.parse("")).toBe(null)
|
|
172
|
+
expect(schema.parse("flexible")).toBe("flexible")
|
|
173
|
+
expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
|
|
174
|
+
expect(() => schema.parse("2024-03-15 15:30")).toThrow("DateTime is not in the allowed list")
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe("datetime range validation", () => {
|
|
179
|
+
it("should validate minimum and maximum datetimes", () => {
|
|
180
|
+
const schema = datetime({
|
|
181
|
+
format: "YYYY-MM-DD HH:mm",
|
|
182
|
+
min: "2024-03-15 09:00",
|
|
183
|
+
max: "2024-03-15 17:00"
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// Valid datetimes within range
|
|
187
|
+
expect(schema.parse("2024-03-15 09:00")).toBe("2024-03-15 09:00")
|
|
188
|
+
expect(schema.parse("2024-03-15 12:30")).toBe("2024-03-15 12:30")
|
|
189
|
+
expect(schema.parse("2024-03-15 17:00")).toBe("2024-03-15 17:00")
|
|
190
|
+
|
|
191
|
+
// Invalid datetimes outside range
|
|
192
|
+
expect(() => schema.parse("2024-03-15 08:59")).toThrow("DateTime must be after")
|
|
193
|
+
expect(() => schema.parse("2024-03-15 17:01")).toThrow("DateTime must be before")
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("should validate hour ranges", () => {
|
|
197
|
+
const schema = datetime({
|
|
198
|
+
format: "YYYY-MM-DD HH:mm",
|
|
199
|
+
minHour: 9,
|
|
200
|
+
maxHour: 17
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
expect(schema.parse("2024-03-15 09:30")).toBe("2024-03-15 09:30")
|
|
204
|
+
expect(schema.parse("2024-03-15 17:30")).toBe("2024-03-15 17:30")
|
|
205
|
+
|
|
206
|
+
expect(() => schema.parse("2024-03-15 08:30")).toThrow("Hour must be between")
|
|
207
|
+
expect(() => schema.parse("2024-03-15 18:30")).toThrow("Hour must be between")
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it("should validate allowed hours", () => {
|
|
211
|
+
const schema = datetime({
|
|
212
|
+
format: "YYYY-MM-DD HH:mm",
|
|
213
|
+
allowedHours: [9, 10, 14, 15, 16]
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
expect(schema.parse("2024-03-15 09:30")).toBe("2024-03-15 09:30")
|
|
217
|
+
expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
|
|
218
|
+
|
|
219
|
+
expect(() => schema.parse("2024-03-15 08:30")).toThrow("Hour must be between")
|
|
220
|
+
expect(() => schema.parse("2024-03-15 11:30")).toThrow("Hour must be between")
|
|
221
|
+
expect(() => schema.parse("2024-03-15 17:30")).toThrow("Hour must be between")
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it("should validate minute steps", () => {
|
|
225
|
+
const schema = datetime({
|
|
226
|
+
format: "YYYY-MM-DD HH:mm",
|
|
227
|
+
minuteStep: 15
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
expect(schema.parse("2024-03-15 14:00")).toBe("2024-03-15 14:00")
|
|
231
|
+
expect(schema.parse("2024-03-15 14:15")).toBe("2024-03-15 14:15")
|
|
232
|
+
expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
|
|
233
|
+
expect(schema.parse("2024-03-15 14:45")).toBe("2024-03-15 14:45")
|
|
234
|
+
|
|
235
|
+
expect(() => schema.parse("2024-03-15 14:05")).toThrow("Minutes must be in 15-minute intervals")
|
|
236
|
+
expect(() => schema.parse("2024-03-15 14:37")).toThrow("Minutes must be in 15-minute intervals")
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe("temporal validation", () => {
|
|
241
|
+
it("should validate past dates", () => {
|
|
242
|
+
const schema = datetime({
|
|
243
|
+
format: "YYYY-MM-DD HH:mm",
|
|
244
|
+
mustBePast: true
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
const pastDate = dayjs().subtract(1, "day").format("YYYY-MM-DD HH:mm")
|
|
248
|
+
const futureDate = dayjs().add(1, "day").format("YYYY-MM-DD HH:mm")
|
|
249
|
+
|
|
250
|
+
expect(schema.parse(pastDate)).toBe(pastDate)
|
|
251
|
+
expect(() => schema.parse(futureDate)).toThrow("DateTime must be in the past")
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it("should validate future dates", () => {
|
|
255
|
+
const schema = datetime({
|
|
256
|
+
format: "YYYY-MM-DD HH:mm",
|
|
257
|
+
mustBeFuture: true
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const pastDate = dayjs().subtract(1, "day").format("YYYY-MM-DD HH:mm")
|
|
261
|
+
const futureDate = dayjs().add(1, "day").format("YYYY-MM-DD HH:mm")
|
|
262
|
+
|
|
263
|
+
expect(schema.parse(futureDate)).toBe(futureDate)
|
|
264
|
+
expect(() => schema.parse(pastDate)).toThrow("DateTime must be in the future")
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it("should validate today", () => {
|
|
268
|
+
const schema = datetime({
|
|
269
|
+
format: "YYYY-MM-DD HH:mm",
|
|
270
|
+
mustBeToday: true
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const todayDate = dayjs().format("YYYY-MM-DD") + " 14:30"
|
|
274
|
+
const yesterdayDate = dayjs().subtract(1, "day").format("YYYY-MM-DD") + " 14:30"
|
|
275
|
+
|
|
276
|
+
expect(schema.parse(todayDate)).toBe(todayDate)
|
|
277
|
+
expect(() => schema.parse(yesterdayDate)).toThrow("DateTime must be today")
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it("should validate weekdays and weekends", () => {
|
|
281
|
+
const weekdaySchema = datetime({
|
|
282
|
+
format: "YYYY-MM-DD HH:mm",
|
|
283
|
+
weekdaysOnly: true
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
const weekendSchema = datetime({
|
|
287
|
+
format: "YYYY-MM-DD HH:mm",
|
|
288
|
+
weekendsOnly: true
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// Find a Monday and a Saturday
|
|
292
|
+
const monday = "2024-03-18 14:30" // This is a Monday
|
|
293
|
+
const saturday = "2024-03-16 14:30" // This is a Saturday
|
|
294
|
+
|
|
295
|
+
expect(weekdaySchema.parse(monday)).toBe(monday)
|
|
296
|
+
expect(() => weekdaySchema.parse(saturday)).toThrow("DateTime must be a weekday")
|
|
297
|
+
|
|
298
|
+
expect(weekendSchema.parse(saturday)).toBe(saturday)
|
|
299
|
+
expect(() => weekendSchema.parse(monday)).toThrow("DateTime must be a weekend")
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
describe("timezone support", () => {
|
|
304
|
+
it("should handle timezone parsing", () => {
|
|
305
|
+
const schema = datetime({
|
|
306
|
+
format: "YYYY-MM-DD HH:mm",
|
|
307
|
+
timezone: "Asia/Taipei"
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it("should validate with timezone-aware ranges", () => {
|
|
314
|
+
const schema = datetime({
|
|
315
|
+
format: "YYYY-MM-DD HH:mm",
|
|
316
|
+
timezone: "Asia/Taipei",
|
|
317
|
+
min: "2024-03-15 09:00",
|
|
318
|
+
max: "2024-03-15 17:00"
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
expect(schema.parse("2024-03-15 12:30")).toBe("2024-03-15 12:30")
|
|
322
|
+
expect(() => schema.parse("2024-03-15 08:30")).toThrow("DateTime must be after")
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
describe("transform function", () => {
|
|
327
|
+
it("should apply custom transform", () => {
|
|
328
|
+
const schema = datetime({
|
|
329
|
+
format: "YYYY-MM-DD HH:mm",
|
|
330
|
+
transform: (val) => val.replace(/\//g, "-")
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
expect(schema.parse("2024/03/15 14:30")).toBe("2024-03-15 14:30")
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it("should apply transform before validation", () => {
|
|
337
|
+
const schema = datetime({
|
|
338
|
+
format: "YYYY-MM-DD HH:mm",
|
|
339
|
+
transform: (val) => val.replace(/T/g, " ").replace(/Z$/, "")
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
expect(schema.parse("2024-03-15T14:30Z")).toBe("2024-03-15 14:30")
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it("should work with whitelist after transform", () => {
|
|
346
|
+
const schema = datetime({
|
|
347
|
+
format: "YYYY-MM-DD HH:mm",
|
|
348
|
+
whitelist: ["NOW", "2024-03-15 14:30"],
|
|
349
|
+
transform: (val) => val.toUpperCase()
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
expect(schema.parse("now")).toBe("NOW")
|
|
353
|
+
expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
describe("input preprocessing", () => {
|
|
358
|
+
it("should handle string conversion", () => {
|
|
359
|
+
const schema = datetime({ format: "UNIX" })
|
|
360
|
+
|
|
361
|
+
// Test string conversion of numbers
|
|
362
|
+
expect(schema.parse(1710508245)).toBe("1710508245")
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it("should trim whitespace", () => {
|
|
366
|
+
const schema = datetime({ format: "YYYY-MM-DD HH:mm" })
|
|
367
|
+
|
|
368
|
+
expect(schema.parse(" 2024-03-15 14:30 ")).toBe("2024-03-15 14:30")
|
|
369
|
+
expect(schema.parse("\t2024-03-15 09:15\n")).toBe("2024-03-15 09:15")
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it("should handle different trim modes", () => {
|
|
373
|
+
const trimStartSchema = datetime({
|
|
374
|
+
format: "YYYY-MM-DD HH:mm",
|
|
375
|
+
trimMode: "trimStart"
|
|
376
|
+
})
|
|
377
|
+
const trimEndSchema = datetime({
|
|
378
|
+
format: "YYYY-MM-DD HH:mm",
|
|
379
|
+
trimMode: "trimEnd"
|
|
380
|
+
})
|
|
381
|
+
const noTrimSchema = datetime({
|
|
382
|
+
format: "YYYY-MM-DD HH:mm",
|
|
383
|
+
trimMode: "none"
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
expect(trimStartSchema.parse(" 2024-03-15 14:30 ")).toBe("2024-03-15 14:30 ")
|
|
387
|
+
expect(trimEndSchema.parse(" 2024-03-15 14:30 ")).toBe(" 2024-03-15 14:30")
|
|
388
|
+
expect(noTrimSchema.parse(" 2024-03-15 14:30 ")).toBe(" 2024-03-15 14:30 ")
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it("should handle case transformations", () => {
|
|
392
|
+
const upperSchema = datetime({
|
|
393
|
+
format: "YYYY-MM-DD HH:mm",
|
|
394
|
+
whitelist: ["NOW", "TOMORROW"],
|
|
395
|
+
casing: "upper"
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
expect(upperSchema.parse("now")).toBe("NOW")
|
|
399
|
+
expect(upperSchema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
|
|
400
|
+
})
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
describe("custom regex validation", () => {
|
|
404
|
+
it("should use custom regex instead of format validation", () => {
|
|
405
|
+
const schema = datetime({
|
|
406
|
+
regex: /^(morning|afternoon|evening|night)$/
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
expect(schema.parse("morning")).toBe("morning")
|
|
410
|
+
expect(schema.parse("evening")).toBe("evening")
|
|
411
|
+
expect(() => schema.parse("invalid")).toThrow("Invalid datetime format")
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it("should skip datetime parsing with custom regex", () => {
|
|
415
|
+
const schema = datetime({
|
|
416
|
+
regex: /^(now|later|asap)$/,
|
|
417
|
+
mustBePast: true // This should be ignored when using regex
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
expect(schema.parse("now")).toBe("now")
|
|
421
|
+
expect(schema.parse("later")).toBe("later")
|
|
422
|
+
expect(() => schema.parse("invalid")).toThrow("Invalid datetime format")
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
describe("includes/excludes validation", () => {
|
|
427
|
+
it("should validate includes", () => {
|
|
428
|
+
const schema = datetime({
|
|
429
|
+
format: "YYYY-MM-DD HH:mm",
|
|
430
|
+
includes: "2024"
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
|
|
434
|
+
expect(() => schema.parse("2023-03-15 14:30")).toThrow("Must include 2024")
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it("should validate excludes", () => {
|
|
438
|
+
const schema = datetime({
|
|
439
|
+
regex: /^[\d\-\s:test]+$/, // Custom regex to allow "test" in the string
|
|
440
|
+
excludes: ["test", "invalid"]
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
|
|
444
|
+
expect(() => schema.parse("2024-test-15 14:30")).toThrow("Must not contain test")
|
|
445
|
+
})
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
describe("utility functions", () => {
|
|
449
|
+
describe("validateDateTimeFormat", () => {
|
|
450
|
+
it("should correctly validate datetime formats", () => {
|
|
451
|
+
expect(validateDateTimeFormat("2024-03-15 14:30", "YYYY-MM-DD HH:mm")).toBe(true)
|
|
452
|
+
expect(validateDateTimeFormat("2024-03-15 14:30:45", "YYYY-MM-DD HH:mm:ss")).toBe(true)
|
|
453
|
+
expect(validateDateTimeFormat("15/03/2024 14:30", "DD/MM/YYYY HH:mm")).toBe(true)
|
|
454
|
+
expect(validateDateTimeFormat("2024-03-15T14:30:45.000Z", "ISO")).toBe(true)
|
|
455
|
+
expect(validateDateTimeFormat("1710508245", "UNIX")).toBe(true)
|
|
456
|
+
|
|
457
|
+
// Invalid formats
|
|
458
|
+
expect(validateDateTimeFormat("2024/03/15 14:30", "YYYY-MM-DD HH:mm")).toBe(false)
|
|
459
|
+
expect(validateDateTimeFormat("invalid", "YYYY-MM-DD HH:mm")).toBe(false)
|
|
460
|
+
expect(validateDateTimeFormat("2024-13-15 14:30", "YYYY-MM-DD HH:mm")).toBe(false)
|
|
461
|
+
})
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
describe("parseDateTimeValue", () => {
|
|
465
|
+
it("should parse different datetime formats correctly", () => {
|
|
466
|
+
const result1 = parseDateTimeValue("2024-03-15 14:30", "YYYY-MM-DD HH:mm")
|
|
467
|
+
expect(result1?.isValid()).toBe(true)
|
|
468
|
+
expect(result1?.year()).toBe(2024)
|
|
469
|
+
expect(result1?.month()).toBe(2) // 0-indexed
|
|
470
|
+
expect(result1?.date()).toBe(15)
|
|
471
|
+
expect(result1?.hour()).toBe(14)
|
|
472
|
+
expect(result1?.minute()).toBe(30)
|
|
473
|
+
|
|
474
|
+
const result2 = parseDateTimeValue("1710508245", "UNIX")
|
|
475
|
+
expect(result2?.isValid()).toBe(true)
|
|
476
|
+
|
|
477
|
+
const result3 = parseDateTimeValue("2024-03-15T14:30:45.000Z", "ISO")
|
|
478
|
+
expect(result3?.isValid()).toBe(true)
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
it("should handle invalid datetimes", () => {
|
|
482
|
+
expect(parseDateTimeValue("invalid", "YYYY-MM-DD HH:mm")).toBe(null)
|
|
483
|
+
expect(parseDateTimeValue("2024-13-15 14:30", "YYYY-MM-DD HH:mm")).toBe(null)
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
it("should handle timezone", () => {
|
|
487
|
+
const result = parseDateTimeValue("2024-03-15 14:30", "YYYY-MM-DD HH:mm", "Asia/Taipei")
|
|
488
|
+
expect(result?.isValid()).toBe(true)
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
describe("normalizeDateTimeValue", () => {
|
|
493
|
+
it("should normalize datetime to specified format", () => {
|
|
494
|
+
const result1 = normalizeDateTimeValue("2024-03-15 14:30", "YYYY-MM-DD HH:mm")
|
|
495
|
+
expect(result1).toBe("2024-03-15 14:30")
|
|
496
|
+
|
|
497
|
+
const result2 = normalizeDateTimeValue("15/03/2024 14:30", "DD/MM/YYYY HH:mm")
|
|
498
|
+
expect(result2).toBe("15/03/2024 14:30")
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it("should handle different format conversions", () => {
|
|
502
|
+
// Note: These would need actual datetime parsing logic to work correctly
|
|
503
|
+
const result = normalizeDateTimeValue("1710508245", "UNIX")
|
|
504
|
+
expect(result).toBe("1710508245")
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it("should handle invalid dates", () => {
|
|
508
|
+
expect(normalizeDateTimeValue("invalid", "YYYY-MM-DD HH:mm")).toBe(null)
|
|
509
|
+
})
|
|
510
|
+
})
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
describe("i18n support", () => {
|
|
514
|
+
it("should use English messages by default", () => {
|
|
515
|
+
setLocale("en")
|
|
516
|
+
expect(() => datetime().parse("")).toThrow("Required")
|
|
517
|
+
expect(() => datetime().parse("invalid")).toThrow("Must be in YYYY-MM-DD HH:mm format")
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it("should use Chinese messages when locale is zh-TW", () => {
|
|
521
|
+
setLocale("zh-TW")
|
|
522
|
+
const schema = datetime({ format: "YYYY-MM-DD HH:mm" })
|
|
523
|
+
|
|
524
|
+
expect(() => schema.parse("")).toThrow("必填")
|
|
525
|
+
expect(() => schema.parse("invalid")).toThrow("必須為 YYYY-MM-DD HH:mm 格式")
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
it("should support whitelist error messages", () => {
|
|
529
|
+
setLocale("en")
|
|
530
|
+
const schema = datetime({
|
|
531
|
+
format: "YYYY-MM-DD HH:mm",
|
|
532
|
+
whitelist: ["now"],
|
|
533
|
+
whitelistOnly: true
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
expect(() => schema.parse("2024-03-15 14:30")).toThrow("DateTime is not in the allowed list")
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
it("should support custom i18n messages", () => {
|
|
540
|
+
const schema = datetime({
|
|
541
|
+
format: "YYYY-MM-DD HH:mm",
|
|
542
|
+
i18n: {
|
|
543
|
+
en: {
|
|
544
|
+
required: "DateTime is required",
|
|
545
|
+
invalid: "Invalid datetime value"
|
|
546
|
+
},
|
|
547
|
+
"zh-TW": {
|
|
548
|
+
required: "請輸入日期時間",
|
|
549
|
+
invalid: "無效的日期時間值"
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
setLocale("en")
|
|
555
|
+
expect(() => schema.parse("")).toThrow("DateTime is required")
|
|
556
|
+
|
|
557
|
+
setLocale("zh-TW")
|
|
558
|
+
expect(() => schema.parse("")).toThrow("請輸入日期時間")
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
it("should support custom whitelist messages", () => {
|
|
562
|
+
const schema = datetime({
|
|
563
|
+
format: "YYYY-MM-DD HH:mm",
|
|
564
|
+
whitelist: ["now"],
|
|
565
|
+
whitelistOnly: true,
|
|
566
|
+
i18n: {
|
|
567
|
+
en: {
|
|
568
|
+
notInWhitelist: "This datetime is not allowed"
|
|
569
|
+
},
|
|
570
|
+
"zh-TW": {
|
|
571
|
+
notInWhitelist: "此日期時間不被允許"
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
setLocale("en")
|
|
577
|
+
expect(() => schema.parse("2024-03-15 14:30")).toThrow("This datetime is not allowed")
|
|
578
|
+
|
|
579
|
+
setLocale("zh-TW")
|
|
580
|
+
expect(() => schema.parse("2024-03-15 14:30")).toThrow("此日期時間不被允許")
|
|
581
|
+
})
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
describe("real world datetime scenarios", () => {
|
|
585
|
+
it("should validate business hours", () => {
|
|
586
|
+
const businessHours = datetime({
|
|
587
|
+
format: "YYYY-MM-DD HH:mm",
|
|
588
|
+
minHour: 9,
|
|
589
|
+
maxHour: 17,
|
|
590
|
+
weekdaysOnly: true,
|
|
591
|
+
minuteStep: 30
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
expect(businessHours.parse("2024-03-18 09:00")).toBe("2024-03-18 09:00") // Monday
|
|
595
|
+
expect(businessHours.parse("2024-03-18 12:30")).toBe("2024-03-18 12:30")
|
|
596
|
+
expect(businessHours.parse("2024-03-18 17:00")).toBe("2024-03-18 17:00")
|
|
597
|
+
|
|
598
|
+
expect(() => businessHours.parse("2024-03-16 14:30")).toThrow("DateTime must be a weekday") // Saturday
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it("should validate appointment slots", () => {
|
|
602
|
+
const appointmentSlots = datetime({
|
|
603
|
+
format: "YYYY-MM-DD HH:mm",
|
|
604
|
+
allowedHours: [9, 10, 11, 14, 15, 16],
|
|
605
|
+
minuteStep: 30,
|
|
606
|
+
weekdaysOnly: true
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
expect(appointmentSlots.parse("2024-03-18 09:00")).toBe("2024-03-18 09:00")
|
|
610
|
+
expect(appointmentSlots.parse("2024-03-18 15:30")).toBe("2024-03-18 15:30")
|
|
611
|
+
|
|
612
|
+
expect(() => appointmentSlots.parse("2024-03-18 12:00")).toThrow("Hour must be between")
|
|
613
|
+
expect(() => appointmentSlots.parse("2024-03-18 09:15")).toThrow("Minutes must be in 30-minute intervals")
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
it("should handle flexible datetime input", () => {
|
|
617
|
+
const flexibleDateTime = datetime({
|
|
618
|
+
format: "YYYY-MM-DD HH:mm",
|
|
619
|
+
whitelist: ["now", "tomorrow", "next week", "asap"],
|
|
620
|
+
required: false
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
expect(flexibleDateTime.parse("now")).toBe("now")
|
|
624
|
+
expect(flexibleDateTime.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
|
|
625
|
+
expect(flexibleDateTime.parse("")).toBe(null)
|
|
626
|
+
expect(() => flexibleDateTime.parse("invalid")).toThrow("Must be in YYYY-MM-DD HH:mm format")
|
|
627
|
+
})
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
describe("edge cases", () => {
|
|
631
|
+
it("should handle various input types", () => {
|
|
632
|
+
const schema = datetime({ format: "UNIX" })
|
|
633
|
+
|
|
634
|
+
expect(schema.parse("1710508245")).toBe("1710508245")
|
|
635
|
+
expect(schema.parse(1710508245)).toBe("1710508245")
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
it("should handle empty and whitespace inputs", () => {
|
|
639
|
+
const requiredSchema = datetime({ format: "YYYY-MM-DD HH:mm", required: true })
|
|
640
|
+
const optionalSchema = datetime({ format: "YYYY-MM-DD HH:mm", required: false })
|
|
641
|
+
|
|
642
|
+
expect(() => requiredSchema.parse("")).toThrow("Required")
|
|
643
|
+
expect(() => requiredSchema.parse(" ")).toThrow("Required")
|
|
644
|
+
|
|
645
|
+
expect(optionalSchema.parse("")).toBe(null)
|
|
646
|
+
expect(optionalSchema.parse(" ")).toBe(null)
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
it("should preserve valid format after transformation", () => {
|
|
650
|
+
const schema = datetime({
|
|
651
|
+
format: "YYYY-MM-DD HH:mm",
|
|
652
|
+
transform: (val) => val.replace(/[^0-9:\-\s]/g, "")
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
expect(schema.parse("2024abc-03def-15 14:30")).toBe("2024-03-15 14:30")
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
it("should work with complex whitelist scenarios", () => {
|
|
659
|
+
const schema = datetime({
|
|
660
|
+
format: "YYYY-MM-DD HH:mm",
|
|
661
|
+
whitelist: ["2024-03-15 14:30", "TBD", "flexible", ""],
|
|
662
|
+
whitelistOnly: true,
|
|
663
|
+
required: false
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
// Whitelist scenarios
|
|
667
|
+
expect(schema.parse("2024-03-15 14:30")).toBe("2024-03-15 14:30")
|
|
668
|
+
expect(schema.parse("TBD")).toBe("TBD")
|
|
669
|
+
expect(schema.parse("flexible")).toBe("flexible")
|
|
670
|
+
expect(schema.parse("")).toBe("")
|
|
671
|
+
// Not in the whitelist
|
|
672
|
+
expect(() => schema.parse("2024-03-15 15:30")).toThrow("DateTime is not in the allowed list")
|
|
673
|
+
expect(() => schema.parse("other-value")).toThrow("DateTime is not in the allowed list")
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
it("should handle boundary cases for different formats", () => {
|
|
677
|
+
const schema24 = datetime({ format: "YYYY-MM-DD HH:mm" })
|
|
678
|
+
const schema12 = datetime({ format: "YYYY-MM-DD hh:mm A" })
|
|
679
|
+
|
|
680
|
+
// Valid boundary cases
|
|
681
|
+
expect(schema24.parse("2024-01-01 00:00")).toBe("2024-01-01 00:00")
|
|
682
|
+
expect(schema24.parse("2024-12-31 23:59")).toBe("2024-12-31 23:59")
|
|
683
|
+
expect(schema12.parse("2024-01-01 12:00 AM")).toBe("2024-01-01 12:00 AM")
|
|
684
|
+
expect(schema12.parse("2024-12-31 11:59 PM")).toBe("2024-12-31 11:59 PM")
|
|
685
|
+
|
|
686
|
+
// Invalid boundary cases
|
|
687
|
+
expect(() => schema24.parse("2024-01-01 24:00")).toThrow("Must be in YYYY-MM-DD HH:mm format")
|
|
688
|
+
expect(() => schema24.parse("2024-01-01 14:60")).toThrow("Must be in YYYY-MM-DD HH:mm format")
|
|
689
|
+
expect(() => schema12.parse("2024-01-01 00:00 AM")).toThrow("Must be in YYYY-MM-DD hh:mm A format")
|
|
690
|
+
expect(() => schema12.parse("2024-01-01 13:00 PM")).toThrow("Must be in YYYY-MM-DD hh:mm A format")
|
|
691
|
+
})
|
|
692
|
+
})
|
|
693
|
+
})
|