Skip to content

Commit 5f8673c

Browse files
committed
✨ ♻️ vuetify input checker #260
1 parent de1e1c7 commit 5f8673c

File tree

14 files changed

+654
-46
lines changed

14 files changed

+654
-46
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
describe('ClassError', () => {
4+
it('ApiResultError - error result', () => {
5+
const result: ErrorResult = {
6+
success: false,
7+
errors: [{
8+
message: 'test error',
9+
}],
10+
};
11+
const error = new ApiResultError(result);
12+
expect(error).toBeInstanceOf(ApiResultError);
13+
expect(error).toBeInstanceOf(Error);
14+
expect(error.message).toBe('test error');
15+
expect(error.errorResult).toBe(result);
16+
expect(error.falseResult).toBeUndefined();
17+
expect(error.name).toBe('Error');
18+
});
19+
20+
it('ApiResultError - false result', () => {
21+
const result: DataResult = {
22+
success: false,
23+
message: 'test false',
24+
data: null,
25+
};
26+
const error = new ApiResultError(result);
27+
expect(error).toBeInstanceOf(ApiResultError);
28+
expect(error).toBeInstanceOf(Error);
29+
expect(error.message).toBe('test false');
30+
expect(error.falseResult).toBe(result);
31+
expect(error.errorResult).toBeUndefined();
32+
expect(error.name).toBe('Error');
33+
});
34+
35+
it('ApiResultError - error result with empty message', () => {
36+
const result: ErrorResult = {
37+
success: false,
38+
errors: [{
39+
message: '',
40+
}],
41+
};
42+
const error = new ApiResultError(result);
43+
expect(error).toBeInstanceOf(ApiResultError);
44+
expect(error).toBeInstanceOf(Error);
45+
expect(error.message).toBe(TypeApiError);
46+
expect(error.errorResult).toBe(result);
47+
expect(error.falseResult).toBeUndefined();
48+
expect(error.name).toBe('Error');
49+
});
50+
51+
it('ApiResultError - false result with empty message', () => {
52+
const result: DataResult = {
53+
success: false,
54+
message: '',
55+
data: null,
56+
};
57+
const error = new ApiResultError(result);
58+
expect(error).toBeInstanceOf(ApiResultError);
59+
expect(error).toBeInstanceOf(Error);
60+
expect(error.message).toBe(TypeApiFalse);
61+
expect(error.falseResult).toBe(result);
62+
expect(error.errorResult).toBeUndefined();
63+
expect(error.name).toBe('Error');
64+
});
65+
66+
it('SystemError', () => {
67+
const error = new SystemError('test system error', { test: 'test' });
68+
expect(error).toBeInstanceOf(SystemError);
69+
expect(error).toBeInstanceOf(Error);
70+
expect(error.message).toBe('test system error');
71+
expect(error.attachment).toEqual({ test: 'test' });
72+
expect(error.name).toBe('Error');
73+
});
74+
75+
it('SystemError - no attachment', () => {
76+
const error = new SystemError('test system error');
77+
expect(error).toBeInstanceOf(SystemError);
78+
expect(error).toBeInstanceOf(Error);
79+
expect(error.message).toBe('test system error');
80+
expect(error.attachment).toBeUndefined();
81+
expect(error.name).toBe('Error');
82+
});
83+
});

layers/common/tests/common-util.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect } from 'vitest';
1+
import { describe, it, expect, vi } from 'vitest';
22

33
describe('flatArray', () => {
44
it('should flatten mixed values and arrays', () => {
@@ -44,3 +44,30 @@ describe('attachId', () => {
4444
expect(result).toHaveProperty('id', '123');
4545
});
4646
});
47+
48+
describe('refToFunction', () => {
49+
it('should return DummyFunction when vr is undefined', () => {
50+
const result = refToFunction<number>(undefined);
51+
expect(result).toBe(DummyFunction);
52+
});
53+
54+
it('should return DummyFunction when vr is null', () => {
55+
const result = refToFunction<string>(null);
56+
expect(result).toBe(DummyFunction);
57+
});
58+
59+
it('should return the provided function when vr is a function', () => {
60+
const mockFn = vi.fn();
61+
const result = refToFunction<number>(mockFn);
62+
expect(result).toBe(mockFn);
63+
});
64+
65+
it('should return a function that sets value on a Ref object', () => {
66+
const r1 = ref(1);
67+
const result = refToFunction(r1);
68+
69+
const testValue = 42;
70+
result(testValue);
71+
expect(r1.value).toBe(testValue);
72+
});
73+
});

layers/common/utils/common-util.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,13 @@ export function lazyNonnull<T>(_default?: T) {
8787
},
8888
};
8989
}
90+
91+
export const DummyFunction = () => {};
92+
93+
export function refToFunction<T>(vr?: null | Ref<T> | ((v: T) => void)): (v: T) => void {
94+
if (vr == null) return DummyFunction;
95+
if (typeof vr === 'function') return vr;
96+
return (v: T) => {
97+
vr.value = v;
98+
};
99+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* ```tsx
3+
* <template>
4+
* <VTextField
5+
* v-model="firstNameModel"
6+
* label="First Name"
7+
* :rules="[firstNameCheck]"
8+
* clearable
9+
* placeholder="Enter text"
10+
* :error-messages="firstNameError"
11+
* />
12+
* </template>
13+
* <script setup lang="ts">
14+
* const firstNameModel = shallowRef('');
15+
* const firstNameError = shallowRef('');
16+
* const firstNameCheck = useInputChecker({
17+
* check: /^[\x20-\x7E]{3,}$/,
18+
* model: firstNameModel,
19+
* output: firstNameError,
20+
* });
21+
* </script>
22+
* ```
23+
*
24+
* generate a validator function for vuetify input.
25+
*/
26+
27+
let counter = 0;
28+
export function useInputChecker(opt: {
29+
check: MayArray<RegExp | ((value: string | Ref<string>) => boolean | string)>;
30+
model?: Ref<string> | (() => string);
31+
output?: Ref<string> | ((err: string) => void);
32+
notify?: {
33+
handle: InstanceType<typeof NoticeCapturer>;
34+
accept: string | ((ntc: I18nNotice) => boolean | undefined);
35+
id?: string;
36+
order?: number;
37+
};
38+
},
39+
): (ev?: Maybe<I18nNotice | Ref<string> | string>) => boolean | string {
40+
const target = typeof opt.notify?.accept === 'string' ? opt.notify.accept : undefined;
41+
const output = refToFunction(opt.output);
42+
43+
// regitster notice handler
44+
if (opt.notify != null) {
45+
const tg = opt.notify.accept;
46+
let acc;
47+
let id = opt.notify.id;
48+
if (typeof tg === 'string') {
49+
acc = (n: I18nNotice) => n.target === tg;
50+
id ??= tg;
51+
}
52+
else {
53+
acc = tg;
54+
}
55+
56+
if (id == null) {
57+
id = `input-checker-${counter++}`;
58+
logger.info('no id for notice handler, use counter %s', id);
59+
}
60+
61+
const scope = getCurrentScope();
62+
const localize = scope ? useLocalizeMessage() : (ntc: I18nNotice, _: boolean) => ntc.message ?? '';
63+
64+
opt.notify.handle.put({
65+
id,
66+
order: opt.notify.order || 100,
67+
hook: (ntc: I18nNotice) => {
68+
if (acc(ntc)) {
69+
const msg = localize(ntc, true);
70+
output(msg);
71+
return false;
72+
}
73+
},
74+
});
75+
// remove on unmount
76+
if (scope) {
77+
onScopeDispose(() => {
78+
opt.notify?.handle.del(id);
79+
});
80+
}
81+
}
82+
83+
const checks = (Array.isArray(opt.check) ? [...opt.check] : [opt.check]).map((it) => {
84+
return typeof it === 'function' ? it : (v: string) => it.test(v);
85+
});
86+
87+
return (ev) => {
88+
let value: string | undefined;
89+
90+
if (ev == null) {
91+
value = undefined;
92+
}
93+
else if (typeof ev === 'string') {
94+
value = ev;
95+
}
96+
else if (isRef(ev)) {
97+
value = ev.value;
98+
}
99+
else if (ev instanceof Event) {
100+
// ignore;
101+
}
102+
// must notice
103+
else {
104+
if (opt.notify?.handle == null) {
105+
logger.warn('no notice handler, ignored', ev);
106+
}
107+
else {
108+
if (target != null && ev.target == null) {
109+
ev.target = target;
110+
}
111+
opt.notify.handle.emit(ev);
112+
}
113+
return true;
114+
}
115+
116+
if (value == null) {
117+
value = typeof opt.model === 'function' ? opt.model() : opt.model?.value;
118+
value ??= '';
119+
}
120+
121+
let valid: string | boolean = true;
122+
for (const chk of checks) {
123+
const r = chk(value);
124+
if (r != null && r !== true) {
125+
valid = r;
126+
break;
127+
}
128+
}
129+
130+
output(valid === true ? '' : valid === false ? 'invalid' : valid);
131+
return valid;
132+
};
133+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { shallowRef } from 'vue';
3+
4+
describe('useInputChecker', () => {
5+
// Mock NoticeCapturer
6+
const mockNoticeCapturer = {
7+
put: vi.fn(),
8+
del: vi.fn(),
9+
emit: vi.fn(),
10+
handle: vi.fn(),
11+
};
12+
13+
beforeEach(() => {
14+
vi.clearAllMocks();
15+
});
16+
17+
it('should validate input against regex correctly', () => {
18+
const model = shallowRef('');
19+
const validator = useInputChecker({
20+
check: /^[0-9]{6,}$/,
21+
model,
22+
notify: undefined,
23+
});
24+
25+
model.value = '12345'; // Too short
26+
expect(validator()).toBe(false);
27+
28+
model.value = '123456'; // Valid
29+
expect(validator()).toBe(true);
30+
});
31+
32+
it('should call notify.output when validation fails', () => {
33+
const errorOutput = shallowRef('');
34+
const validator = useInputChecker({
35+
check: /^[0-9]{6,}$/,
36+
model: shallowRef(''),
37+
output: errorOutput,
38+
notify: {
39+
handle: mockNoticeCapturer as SafeAny,
40+
accept: 'testField',
41+
},
42+
});
43+
44+
validator(); // No value in model
45+
expect(errorOutput.value).toBeTruthy();
46+
47+
// Test with invalid value
48+
validator('abc'); // Direct input
49+
expect(errorOutput.value).toBeTruthy();
50+
});
51+
52+
it('should emit notice when event is passed', () => {
53+
const validator = useInputChecker({
54+
check: /^[0-9]{6,}$/,
55+
output: () => {},
56+
notify: {
57+
handle: mockNoticeCapturer as SafeAny,
58+
accept: 'noticeTarget',
59+
},
60+
});
61+
62+
const mockEvent = { target: 'noticeTarget', message: 'Invalid input' };
63+
validator(mockEvent as SafeAny);
64+
65+
expect(mockNoticeCapturer.emit).toHaveBeenCalledWith(mockEvent);
66+
});
67+
68+
it('should register a notice handler on initialization', () => {
69+
useInputChecker({
70+
check: /^[0-9]{6,}$/,
71+
output: () => {},
72+
notify: {
73+
handle: mockNoticeCapturer as SafeAny,
74+
accept: 'noticeTarget',
75+
id: 'custom-id',
76+
order: 50,
77+
},
78+
});
79+
80+
expect(mockNoticeCapturer.put).toHaveBeenCalled();
81+
});
82+
});

0 commit comments

Comments
 (0)