@wwog/react 1.3.6 → 1.3.9
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/README.md +102 -1
- package/dist/index.d.mts +54 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/algorithm/date/zellersKongruenz.ts +80 -0
- package/src/algorithm/date.ts +5 -0
- package/src/components/Struct/ArrayRender.tsx +31 -1
- package/src/utils/index.ts +2 -1
- package/src/utils/ruleChecker.test.ts +662 -0
- package/src/utils/ruleChecker.ts +340 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ruleChecker } from './ruleChecker';
|
|
3
|
+
import type { RuleDescription } from './ruleChecker';
|
|
4
|
+
|
|
5
|
+
describe('ruleChecker', () => {
|
|
6
|
+
// ==== 基础功能测试 ====
|
|
7
|
+
describe('基础功能', () => {
|
|
8
|
+
it('应该通过空规则验证', () => {
|
|
9
|
+
const result = ruleChecker({}, {});
|
|
10
|
+
expect(result.valid).toBe(true);
|
|
11
|
+
if (result.valid) {
|
|
12
|
+
expect(result.data).toEqual({});
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('应该验证 required 规则 - 成功', () => {
|
|
17
|
+
const data = { name: 'John' };
|
|
18
|
+
const rules: RuleDescription<typeof data> = {
|
|
19
|
+
name: { required: true },
|
|
20
|
+
};
|
|
21
|
+
const result = ruleChecker(data, rules);
|
|
22
|
+
expect(result.valid).toBe(true);
|
|
23
|
+
if (result.valid) {
|
|
24
|
+
expect(result.data.name).toBe('John');
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('应该验证 required 规则 - 失败', () => {
|
|
29
|
+
const data = { name: '' };
|
|
30
|
+
const rules: RuleDescription<typeof data> = {
|
|
31
|
+
name: { required: true },
|
|
32
|
+
};
|
|
33
|
+
const result = ruleChecker(data, rules);
|
|
34
|
+
expect(result.valid).toBe(false);
|
|
35
|
+
if (!result.valid) {
|
|
36
|
+
expect(result.errors).toContain('name 为必填项');
|
|
37
|
+
expect(result.fieldErrors.name).toContain('name 为必填项');
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('应该使用自定义错误消息', () => {
|
|
42
|
+
const data = { email: '' };
|
|
43
|
+
const rules: RuleDescription<typeof data> = {
|
|
44
|
+
email: { required: true, message: '邮箱不能为空' },
|
|
45
|
+
};
|
|
46
|
+
const result = ruleChecker(data, rules);
|
|
47
|
+
expect(result.valid).toBe(false);
|
|
48
|
+
if (!result.valid) {
|
|
49
|
+
expect(result.errors).toContain('邮箱不能为空');
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('应该处理 null 和 undefined 值', () => {
|
|
54
|
+
type DataType = { name: string | null; age?: number };
|
|
55
|
+
const data: Partial<DataType> = { name: null, age: undefined };
|
|
56
|
+
const rules: RuleDescription<DataType> = {
|
|
57
|
+
name: { required: true },
|
|
58
|
+
age: { required: false },
|
|
59
|
+
};
|
|
60
|
+
const result = ruleChecker(data, rules);
|
|
61
|
+
expect(result.valid).toBe(false);
|
|
62
|
+
if (!result.valid) {
|
|
63
|
+
expect(result.fieldErrors.name).toContain('name 为必填项');
|
|
64
|
+
expect(result.fieldErrors.age).toBeUndefined();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('应该验证自定义 validator - 返回 boolean', () => {
|
|
69
|
+
const data = { password: '123' };
|
|
70
|
+
const rules: RuleDescription<typeof data> = {
|
|
71
|
+
password: {
|
|
72
|
+
validator: (value) => value.length >= 6,
|
|
73
|
+
message: '密码至少6位',
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const result = ruleChecker(data, rules);
|
|
77
|
+
expect(result.valid).toBe(false);
|
|
78
|
+
if (!result.valid) {
|
|
79
|
+
expect(result.errors).toContain('密码至少6位');
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('应该验证自定义 validator - 返回 string', () => {
|
|
84
|
+
const data = { username: 'admin' };
|
|
85
|
+
const rules: RuleDescription<typeof data> = {
|
|
86
|
+
username: {
|
|
87
|
+
validator: (value) =>
|
|
88
|
+
value === 'admin' ? '用户名不能是admin' : true,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const result = ruleChecker(data, rules);
|
|
92
|
+
expect(result.valid).toBe(false);
|
|
93
|
+
if (!result.valid) {
|
|
94
|
+
expect(result.errors).toContain('用户名不能是admin');
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ==== 字符串规则测试 ====
|
|
100
|
+
describe('字符串规则', () => {
|
|
101
|
+
it('应该验证字符串长度 len', () => {
|
|
102
|
+
const data = { code: '123' };
|
|
103
|
+
const rules: RuleDescription<typeof data> = {
|
|
104
|
+
code: { len: 4 },
|
|
105
|
+
};
|
|
106
|
+
const result = ruleChecker(data, rules);
|
|
107
|
+
expect(result.valid).toBe(false);
|
|
108
|
+
if (!result.valid) {
|
|
109
|
+
expect(result.errors).toContain('code 长度必须为 4');
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('应该验证字符串最小长度 min', () => {
|
|
114
|
+
const data = { username: 'ab' };
|
|
115
|
+
const rules: RuleDescription<typeof data> = {
|
|
116
|
+
username: { min: 3 },
|
|
117
|
+
};
|
|
118
|
+
const result = ruleChecker(data, rules);
|
|
119
|
+
expect(result.valid).toBe(false);
|
|
120
|
+
if (!result.valid) {
|
|
121
|
+
expect(result.errors).toContain('username 长度不能少于 3');
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('应该验证字符串最大长度 max', () => {
|
|
126
|
+
const data = { comment: 'a'.repeat(101) };
|
|
127
|
+
const rules: RuleDescription<typeof data> = {
|
|
128
|
+
comment: { max: 100 },
|
|
129
|
+
};
|
|
130
|
+
const result = ruleChecker(data, rules);
|
|
131
|
+
expect(result.valid).toBe(false);
|
|
132
|
+
if (!result.valid) {
|
|
133
|
+
expect(result.errors).toContain('comment 长度不能超过 100');
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('应该验证正则表达式', () => {
|
|
138
|
+
const data = { phone: '123456' };
|
|
139
|
+
const rules: RuleDescription<typeof data> = {
|
|
140
|
+
phone: { regex: /^\d{11}$/ },
|
|
141
|
+
};
|
|
142
|
+
const result = ruleChecker(data, rules);
|
|
143
|
+
expect(result.valid).toBe(false);
|
|
144
|
+
if (!result.valid) {
|
|
145
|
+
expect(result.errors).toContain('phone 格式不正确');
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('应该验证邮箱格式', () => {
|
|
150
|
+
const data = { email: 'invalid-email' };
|
|
151
|
+
const rules: RuleDescription<typeof data> = {
|
|
152
|
+
email: { email: true },
|
|
153
|
+
};
|
|
154
|
+
const result = ruleChecker(data, rules);
|
|
155
|
+
expect(result.valid).toBe(false);
|
|
156
|
+
if (!result.valid) {
|
|
157
|
+
expect(result.errors).toContain('email 不是有效的邮箱');
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('应该通过有效的邮箱验证', () => {
|
|
162
|
+
const data = { email: 'test@example.com' };
|
|
163
|
+
const rules: RuleDescription<typeof data> = {
|
|
164
|
+
email: { email: true },
|
|
165
|
+
};
|
|
166
|
+
const result = ruleChecker(data, rules);
|
|
167
|
+
expect(result.valid).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('应该验证URL格式', () => {
|
|
171
|
+
const data = { website: 'invalid-url' };
|
|
172
|
+
const rules: RuleDescription<typeof data> = {
|
|
173
|
+
website: { url: true },
|
|
174
|
+
};
|
|
175
|
+
const result = ruleChecker(data, rules);
|
|
176
|
+
expect(result.valid).toBe(false);
|
|
177
|
+
if (!result.valid) {
|
|
178
|
+
expect(result.errors).toContain('website 不是有效的URL');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('应该通过有效的URL验证', () => {
|
|
183
|
+
const data = { website: 'https://example.com' };
|
|
184
|
+
const rules: RuleDescription<typeof data> = {
|
|
185
|
+
website: { url: true },
|
|
186
|
+
};
|
|
187
|
+
const result = ruleChecker(data, rules);
|
|
188
|
+
expect(result.valid).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('应该验证手机号格式', () => {
|
|
192
|
+
const data = { mobile: '123456789' };
|
|
193
|
+
const rules: RuleDescription<typeof data> = {
|
|
194
|
+
mobile: { phone: true },
|
|
195
|
+
};
|
|
196
|
+
const result = ruleChecker(data, rules);
|
|
197
|
+
expect(result.valid).toBe(false);
|
|
198
|
+
if (!result.valid) {
|
|
199
|
+
expect(result.errors).toContain('mobile 不是有效的手机号');
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('应该通过有效的手机号验证', () => {
|
|
204
|
+
const data = { mobile: '13812345678' };
|
|
205
|
+
const rules: RuleDescription<typeof data> = {
|
|
206
|
+
mobile: { phone: true },
|
|
207
|
+
};
|
|
208
|
+
const result = ruleChecker(data, rules);
|
|
209
|
+
expect(result.valid).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ==== 数字规则测试 ====
|
|
214
|
+
describe('数字规则', () => {
|
|
215
|
+
it('应该验证数字最小值', () => {
|
|
216
|
+
const data = { age: 5 };
|
|
217
|
+
const rules: RuleDescription<typeof data> = {
|
|
218
|
+
age: { min: 18 },
|
|
219
|
+
};
|
|
220
|
+
const result = ruleChecker(data, rules);
|
|
221
|
+
expect(result.valid).toBe(false);
|
|
222
|
+
if (!result.valid) {
|
|
223
|
+
expect(result.errors).toContain('age 不能小于 18');
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('应该验证数字最大值', () => {
|
|
228
|
+
const data = { score: 105 };
|
|
229
|
+
const rules: RuleDescription<typeof data> = {
|
|
230
|
+
score: { max: 100 },
|
|
231
|
+
};
|
|
232
|
+
const result = ruleChecker(data, rules);
|
|
233
|
+
expect(result.valid).toBe(false);
|
|
234
|
+
if (!result.valid) {
|
|
235
|
+
expect(result.errors).toContain('score 不能大于 100');
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('应该通过数字范围验证', () => {
|
|
240
|
+
const data = { percentage: 85 };
|
|
241
|
+
const rules: RuleDescription<typeof data> = {
|
|
242
|
+
percentage: { min: 0, max: 100 },
|
|
243
|
+
};
|
|
244
|
+
const result = ruleChecker(data, rules);
|
|
245
|
+
expect(result.valid).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// ==== 数组规则测试 ====
|
|
250
|
+
describe('数组规则', () => {
|
|
251
|
+
it('应该验证数组长度 len', () => {
|
|
252
|
+
const data = { items: [1, 2, 3] };
|
|
253
|
+
const rules: RuleDescription<typeof data> = {
|
|
254
|
+
items: { len: 5 },
|
|
255
|
+
};
|
|
256
|
+
const result = ruleChecker(data, rules);
|
|
257
|
+
expect(result.valid).toBe(false);
|
|
258
|
+
if (!result.valid) {
|
|
259
|
+
expect(result.errors).toContain('items 长度必须为 5');
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('应该验证数组最小长度', () => {
|
|
264
|
+
const data = { tags: ['one'] };
|
|
265
|
+
const rules: RuleDescription<typeof data> = {
|
|
266
|
+
tags: { min: 2 },
|
|
267
|
+
};
|
|
268
|
+
const result = ruleChecker(data, rules);
|
|
269
|
+
expect(result.valid).toBe(false);
|
|
270
|
+
if (!result.valid) {
|
|
271
|
+
expect(result.errors).toContain('tags 长度不能小于 2');
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('应该验证数组最大长度', () => {
|
|
276
|
+
const data = { categories: [1, 2, 3, 4, 5, 6] };
|
|
277
|
+
const rules: RuleDescription<typeof data> = {
|
|
278
|
+
categories: { max: 5 },
|
|
279
|
+
};
|
|
280
|
+
const result = ruleChecker(data, rules);
|
|
281
|
+
expect(result.valid).toBe(false);
|
|
282
|
+
if (!result.valid) {
|
|
283
|
+
expect(result.errors).toContain('categories 长度不能大于 5');
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('应该验证数组元素唯一性', () => {
|
|
288
|
+
const data = { ids: [1, 2, 2, 3] };
|
|
289
|
+
const rules: RuleDescription<typeof data> = {
|
|
290
|
+
ids: { unique: true },
|
|
291
|
+
};
|
|
292
|
+
const result = ruleChecker(data, rules);
|
|
293
|
+
expect(result.valid).toBe(false);
|
|
294
|
+
if (!result.valid) {
|
|
295
|
+
expect(result.errors).toContain('ids 元素必须唯一');
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('应该通过数组元素唯一性验证', () => {
|
|
300
|
+
const data = { ids: [1, 2, 3, 4] };
|
|
301
|
+
const rules: RuleDescription<typeof data> = {
|
|
302
|
+
ids: { unique: true },
|
|
303
|
+
};
|
|
304
|
+
const result = ruleChecker(data, rules);
|
|
305
|
+
expect(result.valid).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('应该验证数组元素规则', () => {
|
|
309
|
+
const data = { emails: ['valid@email.com', 'invalid-email'] };
|
|
310
|
+
const rules: RuleDescription<typeof data> = {
|
|
311
|
+
emails: {
|
|
312
|
+
elementRule: { email: true },
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
const result = ruleChecker(data, rules);
|
|
316
|
+
expect(result.valid).toBe(false);
|
|
317
|
+
if (!result.valid) {
|
|
318
|
+
// 检查错误信息包含了数组元素索引的错误
|
|
319
|
+
expect(result.errors.some((error) => error.includes('emails[1]'))).toBe(
|
|
320
|
+
true
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ==== 依赖验证测试 ====
|
|
327
|
+
describe('依赖验证', () => {
|
|
328
|
+
it('应该验证 dependsOn - 返回 boolean', () => {
|
|
329
|
+
const data = { type: 'premium', discount: 0.9 };
|
|
330
|
+
const rules: RuleDescription<typeof data> = {
|
|
331
|
+
discount: {
|
|
332
|
+
dependsOn: (data) => data.type === 'basic', // 当类型不是basic时会失败
|
|
333
|
+
message: '只有基础用户才能设置折扣',
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
const result = ruleChecker(data, rules);
|
|
337
|
+
expect(result.valid).toBe(false);
|
|
338
|
+
if (!result.valid) {
|
|
339
|
+
expect(result.errors).toContain('只有基础用户才能设置折扣');
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('应该验证 dependsOn - 返回 string', () => {
|
|
344
|
+
const data = { age: 16, hasLicense: true };
|
|
345
|
+
const rules: RuleDescription<typeof data> = {
|
|
346
|
+
hasLicense: {
|
|
347
|
+
dependsOn: (data) =>
|
|
348
|
+
(data.age || 0) < 18 ? '未成年人不能有驾驶证' : true,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
const result = ruleChecker(data, rules);
|
|
352
|
+
expect(result.valid).toBe(false);
|
|
353
|
+
if (!result.valid) {
|
|
354
|
+
expect(result.errors).toContain('未成年人不能有驾驶证');
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('应该通过 dependsOn 验证', () => {
|
|
359
|
+
const data = { type: 'basic', discount: undefined };
|
|
360
|
+
const rules: RuleDescription<typeof data> = {
|
|
361
|
+
discount: {
|
|
362
|
+
dependsOn: (data) => data.type === 'premium',
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
const result = ruleChecker(data, rules);
|
|
366
|
+
expect(result.valid).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// ==== 复杂场景测试 ====
|
|
371
|
+
describe('复杂场景', () => {
|
|
372
|
+
it('应该处理多个规则数组', () => {
|
|
373
|
+
const data = { password: '123' };
|
|
374
|
+
const rules: RuleDescription<typeof data> = {
|
|
375
|
+
password: [
|
|
376
|
+
{ required: true },
|
|
377
|
+
{ min: 6, message: '密码至少6位' },
|
|
378
|
+
{ regex: /[A-Z]/, message: '密码必须包含大写字母' },
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
const result = ruleChecker(data, rules);
|
|
382
|
+
expect(result.valid).toBe(false);
|
|
383
|
+
if (!result.valid) {
|
|
384
|
+
expect(result.errors).toContain('密码至少6位');
|
|
385
|
+
expect(result.errors).toContain('密码必须包含大写字母');
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('应该收集多个字段的错误', () => {
|
|
390
|
+
const data = { name: '', email: 'invalid', age: 5 };
|
|
391
|
+
const rules: RuleDescription<typeof data> = {
|
|
392
|
+
name: { required: true },
|
|
393
|
+
email: { email: true },
|
|
394
|
+
age: { min: 18 },
|
|
395
|
+
};
|
|
396
|
+
const result = ruleChecker(data, rules);
|
|
397
|
+
expect(result.valid).toBe(false);
|
|
398
|
+
if (!result.valid) {
|
|
399
|
+
expect(result.errors).toHaveLength(3);
|
|
400
|
+
expect(result.fieldErrors.name).toContain('name 为必填项');
|
|
401
|
+
expect(result.fieldErrors.email).toContain('email 不是有效的邮箱');
|
|
402
|
+
expect(result.fieldErrors.age).toContain('age 不能小于 18');
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('应该处理复杂的用户注册场景', () => {
|
|
407
|
+
const data = {
|
|
408
|
+
username: 'ab',
|
|
409
|
+
email: 'test@example.com',
|
|
410
|
+
password: '123',
|
|
411
|
+
confirmPassword: '456',
|
|
412
|
+
age: 25,
|
|
413
|
+
hobbies: ['reading'],
|
|
414
|
+
terms: false,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const rules: RuleDescription<typeof data> = {
|
|
418
|
+
username: { required: true, min: 3, max: 20 },
|
|
419
|
+
email: { required: true, email: true },
|
|
420
|
+
password: [
|
|
421
|
+
{ required: true },
|
|
422
|
+
{ min: 6 },
|
|
423
|
+
{ regex: /[A-Z]/, message: '密码必须包含大写字母' },
|
|
424
|
+
],
|
|
425
|
+
confirmPassword: {
|
|
426
|
+
required: true,
|
|
427
|
+
validator: (value, data) =>
|
|
428
|
+
value === data.password || '两次密码不一致',
|
|
429
|
+
},
|
|
430
|
+
age: { required: true, min: 18, max: 120 },
|
|
431
|
+
hobbies: { min: 2, max: 5 },
|
|
432
|
+
terms: {
|
|
433
|
+
required: true,
|
|
434
|
+
validator: (value) => value === true || '必须同意用户协议',
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const result = ruleChecker(data, rules);
|
|
439
|
+
expect(result.valid).toBe(false);
|
|
440
|
+
if (!result.valid) {
|
|
441
|
+
expect(result.fieldErrors.username).toContain(
|
|
442
|
+
'username 长度不能少于 3'
|
|
443
|
+
);
|
|
444
|
+
expect(result.fieldErrors.password).toContain(
|
|
445
|
+
'password 长度不能少于 6'
|
|
446
|
+
);
|
|
447
|
+
expect(result.fieldErrors.confirmPassword).toContain('两次密码不一致');
|
|
448
|
+
expect(result.fieldErrors.hobbies).toContain('hobbies 长度不能小于 2');
|
|
449
|
+
expect(result.fieldErrors.terms).toContain('必须同意用户协议');
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('应该在所有验证通过时返回正确的数据', () => {
|
|
454
|
+
const data = {
|
|
455
|
+
name: 'John Doe',
|
|
456
|
+
email: 'john@example.com',
|
|
457
|
+
age: 30,
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const rules: RuleDescription<typeof data> = {
|
|
461
|
+
name: { required: true, min: 2 },
|
|
462
|
+
email: { required: true, email: true },
|
|
463
|
+
age: { required: true, min: 18, max: 120 },
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const result = ruleChecker(data, rules);
|
|
467
|
+
expect(result.valid).toBe(true);
|
|
468
|
+
if (result.valid) {
|
|
469
|
+
expect(result.data).toEqual(data);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// ==== 类型安全测试 ====
|
|
475
|
+
describe('类型安全验证', () => {
|
|
476
|
+
it('数字字段应该只支持数字相关规则', () => {
|
|
477
|
+
const data = { age: 25 };
|
|
478
|
+
|
|
479
|
+
// 这些应该是有效的数字规则
|
|
480
|
+
const validRules: RuleDescription<typeof data> = {
|
|
481
|
+
age: { min: 0, max: 120, required: true },
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const result = ruleChecker(data, validRules);
|
|
485
|
+
expect(result.valid).toBe(true);
|
|
486
|
+
|
|
487
|
+
// 注意:TypeScript 会阻止以下无效的数字规则
|
|
488
|
+
// age: { email: true }, // ❌ 编译错误
|
|
489
|
+
// age: { regex: /test/ }, // ❌ 编译错误
|
|
490
|
+
// age: { len: 10 }, // ❌ 编译错误
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('字符串字段应该支持长度和格式规则', () => {
|
|
494
|
+
const data = { email: 'test@example.com', name: 'John' };
|
|
495
|
+
|
|
496
|
+
const rules: RuleDescription<typeof data> = {
|
|
497
|
+
email: {
|
|
498
|
+
required: true,
|
|
499
|
+
email: true,
|
|
500
|
+
min: 5,
|
|
501
|
+
max: 100,
|
|
502
|
+
},
|
|
503
|
+
name: {
|
|
504
|
+
required: true,
|
|
505
|
+
min: 2,
|
|
506
|
+
max: 50,
|
|
507
|
+
regex: /^[a-zA-Z]+$/,
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const result = ruleChecker(data, rules);
|
|
512
|
+
expect(result.valid).toBe(true);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('数组字段应该支持长度和唯一性规则', () => {
|
|
516
|
+
const data = { tags: ['react', 'typescript'], ids: [1, 2, 3] };
|
|
517
|
+
|
|
518
|
+
const rules: RuleDescription<typeof data> = {
|
|
519
|
+
tags: {
|
|
520
|
+
min: 1,
|
|
521
|
+
max: 5,
|
|
522
|
+
unique: true,
|
|
523
|
+
elementRule: { min: 2, max: 20 }, // 元素是字符串
|
|
524
|
+
},
|
|
525
|
+
ids: {
|
|
526
|
+
len: 3,
|
|
527
|
+
unique: true,
|
|
528
|
+
elementRule: { min: 1, max: 1000 }, // 元素是数字
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const result = ruleChecker(data, rules);
|
|
533
|
+
expect(result.valid).toBe(true);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('布尔字段应该只支持基础规则', () => {
|
|
537
|
+
const data = { isActive: false, hasPermission: false };
|
|
538
|
+
|
|
539
|
+
const rules: RuleDescription<typeof data> = {
|
|
540
|
+
isActive: {
|
|
541
|
+
required: true,
|
|
542
|
+
validator: (value) => value === true || '必须激活账户',
|
|
543
|
+
},
|
|
544
|
+
hasPermission: {
|
|
545
|
+
required: true,
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const result = ruleChecker(data, rules);
|
|
550
|
+
expect(result.valid).toBe(false);
|
|
551
|
+
if (!result.valid) {
|
|
552
|
+
expect(result.errors).toContain('必须激活账户');
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('应该正确处理混合类型的复杂对象', () => {
|
|
557
|
+
const data = {
|
|
558
|
+
id: 123,
|
|
559
|
+
name: 'Product Name',
|
|
560
|
+
price: 99.99,
|
|
561
|
+
tags: ['electronics', 'mobile'],
|
|
562
|
+
isActive: true,
|
|
563
|
+
description: 'A great product',
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const rules: RuleDescription<typeof data> = {
|
|
567
|
+
id: { required: true, min: 1 }, // 数字规则
|
|
568
|
+
name: { required: true, min: 3, max: 50 }, // 字符串长度规则
|
|
569
|
+
price: { required: true, min: 0, max: 10000 }, // 数字范围规则
|
|
570
|
+
tags: { min: 1, max: 10, unique: true }, // 数组规则
|
|
571
|
+
isActive: { required: true }, // 布尔基础规则
|
|
572
|
+
description: { min: 10, max: 500, regex: /^[a-zA-Z\s]+$/ }, // 字符串格式规则
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const result = ruleChecker(data, rules);
|
|
576
|
+
expect(result.valid).toBe(true);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// ==== 边缘情况测试 ====
|
|
581
|
+
describe('边缘情况', () => {
|
|
582
|
+
it('应该正确处理空字符串的各种验证', () => {
|
|
583
|
+
const data = { text: '' };
|
|
584
|
+
const rules: RuleDescription<typeof data> = {
|
|
585
|
+
text: { min: 1 }, // 空字符串长度为0,应该失败
|
|
586
|
+
};
|
|
587
|
+
const result = ruleChecker(data, rules);
|
|
588
|
+
expect(result.valid).toBe(false);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('应该正确处理空数组的各种验证', () => {
|
|
592
|
+
const data = { items: [] as string[] };
|
|
593
|
+
const rules: RuleDescription<typeof data> = {
|
|
594
|
+
items: { min: 1 }, // 空数组长度为0,应该失败
|
|
595
|
+
};
|
|
596
|
+
const result = ruleChecker(data, rules);
|
|
597
|
+
expect(result.valid).toBe(false);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('应该正确处理数字 0 的验证', () => {
|
|
601
|
+
const data = { count: 0 };
|
|
602
|
+
const rules: RuleDescription<typeof data> = {
|
|
603
|
+
count: { required: true, min: 0 }, // 0 是有效值,应该通过
|
|
604
|
+
};
|
|
605
|
+
const result = ruleChecker(data, rules);
|
|
606
|
+
expect(result.valid).toBe(true);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('应该正确处理 false 值的验证', () => {
|
|
610
|
+
const data = { isEnabled: false };
|
|
611
|
+
const rules: RuleDescription<typeof data> = {
|
|
612
|
+
isEnabled: { required: true }, // false 是有效值,应该通过
|
|
613
|
+
};
|
|
614
|
+
const result = ruleChecker(data, rules);
|
|
615
|
+
expect(result.valid).toBe(true);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('应该正确处理只有空格的字符串', () => {
|
|
619
|
+
const data = { text: ' ' };
|
|
620
|
+
const rules: RuleDescription<typeof data> = {
|
|
621
|
+
text: { required: true }, // 只有空格被视为空值,应该失败
|
|
622
|
+
};
|
|
623
|
+
const result = ruleChecker(data, rules);
|
|
624
|
+
expect(result.valid).toBe(false);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('应该处理非常长的字符串', () => {
|
|
628
|
+
const longText = 'a'.repeat(1000);
|
|
629
|
+
const data = { content: longText };
|
|
630
|
+
const rules: RuleDescription<typeof data> = {
|
|
631
|
+
content: { max: 500 }, // 超过最大长度,应该失败
|
|
632
|
+
};
|
|
633
|
+
const result = ruleChecker(data, rules);
|
|
634
|
+
expect(result.valid).toBe(false);
|
|
635
|
+
if (!result.valid) {
|
|
636
|
+
expect(result.errors).toContain('content 长度不能超过 500');
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('应该处理复杂的嵌套数组元素验证', () => {
|
|
641
|
+
const data = {
|
|
642
|
+
users: [
|
|
643
|
+
{ name: 'Alice', email: 'alice@test.com' },
|
|
644
|
+
{ name: 'B', email: 'invalid-email' }, // 名字太短,邮箱无效
|
|
645
|
+
],
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// 注意:这个例子展示了当前系统的限制
|
|
649
|
+
// 对于复杂嵌套对象,需要扩展 elementRule 支持
|
|
650
|
+
const rules: RuleDescription<typeof data> = {
|
|
651
|
+
users: {
|
|
652
|
+
min: 1,
|
|
653
|
+
max: 10,
|
|
654
|
+
// elementRule 目前不支持对象验证
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const result = ruleChecker(data, rules);
|
|
659
|
+
expect(result.valid).toBe(true); // 基本数组规则通过
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
});
|