@kine-design/core 0.0.1-beta.5 → 0.0.1-beta.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/components/base/affix/useAffix.ts +2 -1
- package/components/base/anchor/useAnchor.ts +2 -1
- package/components/base/autoComplete/useAutoComplete.ts +2 -1
- package/components/base/carousel/useCarousel.ts +2 -1
- package/components/base/cascader/useCascader.ts +2 -1
- package/components/base/checkbox/useCheckbox.ts +2 -1
- package/components/base/collapse/useCollapse.ts +2 -1
- package/components/base/datePicker/__tests__/useDatePicker.test.ts +239 -0
- package/components/base/dropdown/useDropdown.ts +2 -1
- package/components/base/image/__tests__/useImage.test.ts +174 -0
- package/components/base/input/useInput.ts +3 -1
- package/components/base/inputNumber/__tests__/useInputNumber.test.ts +153 -0
- package/components/base/inputNumber/useInputNumber.ts +53 -11
- package/components/base/popover/usePopover.ts +4 -3
- package/components/base/rate/useRate.ts +2 -1
- package/components/base/select/useSelect.ts +2 -1
- package/components/base/slider/useSlider.ts +2 -1
- package/components/base/steps/__tests__/useSteps.test.ts +46 -0
- package/components/base/switch/useSwitch.tsx +2 -1
- package/components/base/tabs/useTabs.ts +2 -1
- package/components/base/timePicker/__tests__/useTimePicker.test.ts +118 -0
- package/components/base/transfer/useTransfer.ts +2 -1
- package/components/base/tree/__tests__/tree.test.ts +214 -0
- package/components/message/notification/__tests__/useNotification.test.ts +129 -0
- package/components/message/popover/usePopover.ts +4 -4
- package/components/other/darkMode/useDarkMode.ts +2 -2
- package/components/template/menu/__tests__/useMenu.test.ts +157 -0
- package/components/template/pagination/__tests__/usePagination.test.ts +138 -0
- package/components/template/table/__tests__/useTable.test.ts +139 -0
- package/components/template/table/useTable.ts +2 -4
- package/components/types/hook.d.ts +11 -0
- package/compositions/common/__tests__/useDebounceFn.test.ts +62 -0
- package/compositions/common/__tests__/useEventListener.test.ts +71 -0
- package/compositions/common/__tests__/usePopover.test.ts +38 -0
- package/compositions/common/__tests__/useTeleport.test.ts +25 -0
- package/compositions/common/useEventListener.ts +3 -3
- package/compositions/common/useResizeObserver.ts +6 -2
- package/compositions/input/__tests__/useBooleanInput.test.ts +73 -0
- package/compositions/modal/__tests__/useModal.test.ts +92 -0
- package/compositions/modal/useModal.ts +2 -2
- package/compositions/popper/useClickAway.ts +4 -4
- package/compositions/utils/__tests__/filters.test.ts +136 -0
- package/compositions/virtualList/__tests__/useHeightCache.test.ts +97 -0
- package/dist/components/base/affix/useAffix.d.ts +2 -1
- package/dist/components/base/anchor/useAnchor.d.ts +2 -1
- package/dist/components/base/autoComplete/useAutoComplete.d.ts +2 -1
- package/dist/components/base/carousel/useCarousel.d.ts +2 -1
- package/dist/components/base/cascader/useCascader.d.ts +2 -1
- package/dist/components/base/checkbox/useCheckbox.d.ts +2 -1
- package/dist/components/base/collapse/useCollapse.d.ts +2 -1
- package/dist/components/base/datePicker/__tests__/useDatePicker.test.d.ts +1 -0
- package/dist/components/base/dropdown/useDropdown.d.ts +2 -1
- package/dist/components/base/image/__tests__/useImage.test.d.ts +1 -0
- package/dist/components/base/input/useInput.d.ts +2 -1
- package/dist/components/base/inputNumber/__tests__/useInputNumber.test.d.ts +1 -0
- package/dist/components/base/inputNumber/useInputNumber.d.ts +2 -1
- package/dist/components/base/popover/usePopover.d.ts +2 -1
- package/dist/components/base/rate/useRate.d.ts +2 -1
- package/dist/components/base/select/useSelect.d.ts +2 -1
- package/dist/components/base/slider/useSlider.d.ts +2 -1
- package/dist/components/base/steps/__tests__/useSteps.test.d.ts +1 -0
- package/dist/components/base/switch/useSwitch.d.ts +2 -1
- package/dist/components/base/tabs/useTabs.d.ts +2 -1
- package/dist/components/base/timePicker/__tests__/useTimePicker.test.d.ts +1 -0
- package/dist/components/base/transfer/useTransfer.d.ts +2 -1
- package/dist/components/base/tree/__tests__/tree.test.d.ts +1 -0
- package/dist/components/message/notification/__tests__/useNotification.test.d.ts +1 -0
- package/dist/components/message/popover/usePopover.d.ts +1 -1
- package/dist/components/other/darkMode/useDarkMode.d.ts +2 -3
- package/dist/components/template/menu/__tests__/useMenu.test.d.ts +1 -0
- package/dist/components/template/pagination/__tests__/usePagination.test.d.ts +1 -0
- package/dist/components/template/table/__tests__/useTable.test.d.ts +1 -0
- package/dist/compositions/common/__tests__/useDebounceFn.test.d.ts +1 -0
- package/dist/compositions/common/__tests__/useEventListener.test.d.ts +1 -0
- package/dist/compositions/common/__tests__/usePopover.test.d.ts +1 -0
- package/dist/compositions/common/__tests__/useTeleport.test.d.ts +1 -0
- package/dist/compositions/common/useEventListener.d.ts +2 -2
- package/dist/compositions/input/__tests__/useBooleanInput.test.d.ts +1 -0
- package/dist/compositions/modal/__tests__/useModal.test.d.ts +1 -0
- package/dist/compositions/modal/useModal.d.ts +2 -1
- package/dist/compositions/popper/useClickAway.d.ts +3 -3
- package/dist/compositions/utils/__tests__/filters.test.d.ts +1 -0
- package/dist/compositions/virtualList/__tests__/useHeightCache.test.d.ts +1 -0
- package/dist/core.js +64 -18
- package/dist/tools/__tests__/empty.test.d.ts +1 -0
- package/dist/tools/empty.d.ts +2 -2
- package/dist/tools/types.d.ts +1 -1
- package/dist/vitest.config.d.ts +10 -0
- package/package.json +6 -2
- package/tools/__tests__/empty.test.ts +72 -0
- package/tools/empty.ts +2 -2
- package/tools/types.ts +1 -1
- package/vitest.config.ts +17 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description useDebounceFn 测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
import useDebounceFn from '../useDebounceFn';
|
|
11
|
+
|
|
12
|
+
beforeEach(() => { vi.useFakeTimers(); });
|
|
13
|
+
afterEach(() => { vi.useRealTimers(); });
|
|
14
|
+
|
|
15
|
+
describe('useDebounceFn', () => {
|
|
16
|
+
it('返回防抖后的函数', () => {
|
|
17
|
+
const fn = vi.fn();
|
|
18
|
+
const debounced = useDebounceFn(fn, 100);
|
|
19
|
+
|
|
20
|
+
debounced();
|
|
21
|
+
expect(fn).not.toHaveBeenCalled();
|
|
22
|
+
|
|
23
|
+
vi.advanceTimersByTime(100);
|
|
24
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('默认延迟 200ms', () => {
|
|
28
|
+
const fn = vi.fn();
|
|
29
|
+
const debounced = useDebounceFn(fn);
|
|
30
|
+
|
|
31
|
+
debounced();
|
|
32
|
+
vi.advanceTimersByTime(199);
|
|
33
|
+
expect(fn).not.toHaveBeenCalled();
|
|
34
|
+
|
|
35
|
+
vi.advanceTimersByTime(1);
|
|
36
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('支持 maxWait 选项', () => {
|
|
40
|
+
const fn = vi.fn();
|
|
41
|
+
const debounced = useDebounceFn(fn, 100, { maxWait: 150 });
|
|
42
|
+
|
|
43
|
+
debounced();
|
|
44
|
+
vi.advanceTimersByTime(80);
|
|
45
|
+
debounced();
|
|
46
|
+
vi.advanceTimersByTime(80);
|
|
47
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('多次调用只执行最后一次', () => {
|
|
51
|
+
const fn = vi.fn();
|
|
52
|
+
const debounced = useDebounceFn(fn, 50);
|
|
53
|
+
|
|
54
|
+
debounced('a');
|
|
55
|
+
debounced('b');
|
|
56
|
+
debounced('c');
|
|
57
|
+
|
|
58
|
+
vi.advanceTimersByTime(50);
|
|
59
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
60
|
+
expect(fn).toHaveBeenCalledWith('c');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description useEventListener 测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import useEventListener from '../useEventListener';
|
|
11
|
+
|
|
12
|
+
describe('useEventListener', () => {
|
|
13
|
+
it('add 注册事件,remove 移除事件', () => {
|
|
14
|
+
const handler = vi.fn();
|
|
15
|
+
const target = {
|
|
16
|
+
addEventListener: vi.fn(),
|
|
17
|
+
removeEventListener: vi.fn(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const { add, remove } = useEventListener({
|
|
21
|
+
target,
|
|
22
|
+
event: 'click',
|
|
23
|
+
handler,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
add();
|
|
27
|
+
expect(target.addEventListener).toHaveBeenCalledWith('click', handler);
|
|
28
|
+
|
|
29
|
+
remove();
|
|
30
|
+
expect(target.removeEventListener).toHaveBeenCalledWith('click', handler);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('onMounted 调用 add,onBeforeUnmount 调用 remove', () => {
|
|
34
|
+
const handler = vi.fn();
|
|
35
|
+
const target = {
|
|
36
|
+
addEventListener: vi.fn(),
|
|
37
|
+
removeEventListener: vi.fn(),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const { onMounted, onBeforeUnmount } = useEventListener({
|
|
41
|
+
target,
|
|
42
|
+
event: 'scroll',
|
|
43
|
+
handler,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
onMounted();
|
|
47
|
+
expect(target.addEventListener).toHaveBeenCalledOnce();
|
|
48
|
+
|
|
49
|
+
onBeforeUnmount();
|
|
50
|
+
expect(target.removeEventListener).toHaveBeenCalledOnce();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('target 为函数时延迟求值', () => {
|
|
54
|
+
const handler = vi.fn();
|
|
55
|
+
const realTarget = {
|
|
56
|
+
addEventListener: vi.fn(),
|
|
57
|
+
removeEventListener: vi.fn(),
|
|
58
|
+
};
|
|
59
|
+
const targetFn = vi.fn(() => realTarget);
|
|
60
|
+
|
|
61
|
+
const { add } = useEventListener({
|
|
62
|
+
target: targetFn,
|
|
63
|
+
event: 'click',
|
|
64
|
+
handler,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
add();
|
|
68
|
+
expect(targetFn).toHaveBeenCalled();
|
|
69
|
+
expect(realTarget.addEventListener).toHaveBeenCalledWith('click', handler);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description usePopover(common)测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import usePopover from '../usePopover';
|
|
11
|
+
|
|
12
|
+
describe('usePopover', () => {
|
|
13
|
+
it('默认 placement 为 bottom-start', () => {
|
|
14
|
+
const { popoverOptions } = usePopover();
|
|
15
|
+
expect(popoverOptions.placement).toBe('bottom-start');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('自定义 placement', () => {
|
|
19
|
+
const { popoverOptions } = usePopover({ placement: 'top-end' });
|
|
20
|
+
expect(popoverOptions.placement).toBe('top-end');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('默认 middleware 包含 offset、flip、shift', () => {
|
|
24
|
+
const { popoverOptions } = usePopover();
|
|
25
|
+
expect(popoverOptions.popper?.middleware).toHaveLength(3);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('自定义 offset', () => {
|
|
29
|
+
const { popoverOptions } = usePopover(undefined, { offset: 10 });
|
|
30
|
+
expect(popoverOptions.popper?.middleware).toHaveLength(3);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('扩展 middleware', () => {
|
|
34
|
+
const customMiddleware = { name: 'custom', fn: () => ({}) };
|
|
35
|
+
const { popoverOptions } = usePopover(undefined, { extends: [customMiddleware as any] });
|
|
36
|
+
expect(popoverOptions.popper?.middleware).toHaveLength(4);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description useTeleport / initTeleportOptions 测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { initTeleportOptions } from '../useTeleport';
|
|
11
|
+
|
|
12
|
+
describe('initTeleportOptions', () => {
|
|
13
|
+
it('true 默认 teleport 到 body', () => {
|
|
14
|
+
expect(initTeleportOptions(true)).toEqual({ to: 'body' });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('undefined 默认 teleport 到 body', () => {
|
|
18
|
+
expect(initTeleportOptions(undefined)).toEqual({ to: 'body' });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('自定义配置直接返回', () => {
|
|
22
|
+
const custom = { to: '#app', disabled: false };
|
|
23
|
+
expect(initTeleportOptions(custom as any)).toBe(custom);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
export type EventListenerOptions = {
|
|
12
|
-
target:
|
|
12
|
+
target: EventTarget | (() => EventTarget),
|
|
13
13
|
event: string,
|
|
14
14
|
handler: EventListenerOrEventListenerObject,
|
|
15
15
|
};
|
|
@@ -35,14 +35,14 @@ export default function useEventListener(options: EventListenerOptions) {
|
|
|
35
35
|
add();
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
const
|
|
38
|
+
const onBeforeUnmount = () => {
|
|
39
39
|
remove();
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
return {
|
|
43
43
|
add, remove,
|
|
44
44
|
onMounted,
|
|
45
|
-
|
|
45
|
+
onBeforeUnmount,
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Learn from vueuse useResizeObserver
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { Ref, watch } from 'vue';
|
|
12
|
+
import { onBeforeUnmount, Ref, watch } from 'vue';
|
|
13
13
|
|
|
14
14
|
export function useResizeObserver(
|
|
15
15
|
target: Ref<HTMLElement | null>,
|
|
@@ -27,7 +27,7 @@ export function useResizeObserver(
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
// vueuse used watch
|
|
30
|
-
watch(target, () => {
|
|
30
|
+
const stopWatch = watch(target, () => {
|
|
31
31
|
if (target.value) {
|
|
32
32
|
cleanup();
|
|
33
33
|
observer = new ResizeObserver(callback);
|
|
@@ -35,6 +35,10 @@ export function useResizeObserver(
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
onBeforeUnmount(() => {
|
|
39
|
+
cleanup();
|
|
40
|
+
stopWatch();
|
|
41
|
+
});
|
|
38
42
|
|
|
39
43
|
return {
|
|
40
44
|
cleanup,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description initChecked / getNewModelValue 测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { initChecked, getNewModelValue } from '../useBooleanInput';
|
|
11
|
+
|
|
12
|
+
describe('initChecked', () => {
|
|
13
|
+
it('checked 为 true 时直接返回 true', () => {
|
|
14
|
+
expect(initChecked({ checked: true, modelValue: undefined, value: undefined })).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('modelValue 为 boolean 时返回该值', () => {
|
|
18
|
+
expect(initChecked({ checked: undefined, modelValue: true, value: undefined })).toBe(true);
|
|
19
|
+
expect(initChecked({ checked: undefined, modelValue: false, value: undefined })).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('modelValue 和 value 相等时返回 true', () => {
|
|
23
|
+
expect(initChecked({ checked: undefined, modelValue: 'a', value: 'a' })).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('modelValue 和 value 不等时返回 false', () => {
|
|
27
|
+
expect(initChecked({ checked: undefined, modelValue: 'a', value: 'b' })).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('modelValue 为数组时,检查 value 是否在数组中', () => {
|
|
31
|
+
expect(initChecked({ checked: undefined, modelValue: ['a', 'b'], value: 'a' })).toBe(true);
|
|
32
|
+
expect(initChecked({ checked: undefined, modelValue: ['a', 'b'], value: 'c' })).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('所有值为空时返回 false', () => {
|
|
36
|
+
expect(initChecked({ checked: undefined, modelValue: undefined, value: undefined })).toBe(false);
|
|
37
|
+
expect(initChecked({ checked: null, modelValue: null, value: null })).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('getNewModelValue', () => {
|
|
42
|
+
// 有 value 且 modelValue 为数组
|
|
43
|
+
it('checked=true 且 modelValue 为数组时添加 value', () => {
|
|
44
|
+
expect(getNewModelValue({ value: 'c', modelValue: ['a', 'b'] }, true)).toEqual(['a', 'b', 'c']);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('checked=false 且 modelValue 为数组时移除 value', () => {
|
|
48
|
+
expect(getNewModelValue({ value: 'b', modelValue: ['a', 'b'] }, false)).toEqual(['a']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('不重复添加已存在的 value', () => {
|
|
52
|
+
expect(getNewModelValue({ value: 'a', modelValue: ['a', 'b'] }, true)).toEqual(['a', 'b']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('移除不存在的 value 不报错', () => {
|
|
56
|
+
expect(getNewModelValue({ value: 'c', modelValue: ['a', 'b'] }, false)).toEqual(['a', 'b']);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// 有 value 且 modelValue 非数组
|
|
60
|
+
it('checked=true 返回 value', () => {
|
|
61
|
+
expect(getNewModelValue({ value: 'a', modelValue: undefined }, true)).toBe('a');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('checked=false 返回 undefined', () => {
|
|
65
|
+
expect(getNewModelValue({ value: 'a', modelValue: undefined }, false)).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// 无 value
|
|
69
|
+
it('无 value 时直接返回 checked 布尔值', () => {
|
|
70
|
+
expect(getNewModelValue({ value: undefined, modelValue: undefined }, true)).toBe(true);
|
|
71
|
+
expect(getNewModelValue({ value: undefined, modelValue: undefined }, false)).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description useModal 测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { useModal } from '../useModal';
|
|
11
|
+
|
|
12
|
+
describe('useModal', () => {
|
|
13
|
+
it('初始 visible 默认为 false', () => {
|
|
14
|
+
const emit = vi.fn();
|
|
15
|
+
const { visible } = useModal({}, emit);
|
|
16
|
+
expect(visible.value).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('初始 visible 跟随 props', () => {
|
|
20
|
+
const emit = vi.fn();
|
|
21
|
+
const { visible } = useModal({ visible: true }, emit);
|
|
22
|
+
expect(visible.value).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('open 设置 visible 为 true 并触发 emit', () => {
|
|
26
|
+
const emit = vi.fn();
|
|
27
|
+
const { visible, open } = useModal({}, emit);
|
|
28
|
+
|
|
29
|
+
open();
|
|
30
|
+
expect(visible.value).toBe(true);
|
|
31
|
+
expect(emit).toHaveBeenCalledWith('update:visible', true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('close 设置 visible 为 false 并触发 emit', () => {
|
|
35
|
+
const emit = vi.fn();
|
|
36
|
+
const { visible, close } = useModal({ visible: true }, emit);
|
|
37
|
+
|
|
38
|
+
close();
|
|
39
|
+
expect(visible.value).toBe(false);
|
|
40
|
+
expect(emit).toHaveBeenCalledWith('update:visible', false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('toggle 切换 visible 状态', () => {
|
|
44
|
+
const emit = vi.fn();
|
|
45
|
+
const { visible, toggle } = useModal({}, emit);
|
|
46
|
+
|
|
47
|
+
toggle(); // false → true
|
|
48
|
+
expect(visible.value).toBe(true);
|
|
49
|
+
|
|
50
|
+
toggle(); // true → false
|
|
51
|
+
expect(visible.value).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('onMaskClick 默认关闭', () => {
|
|
55
|
+
const emit = vi.fn();
|
|
56
|
+
const { visible, open, onMaskClick } = useModal({}, emit);
|
|
57
|
+
|
|
58
|
+
open();
|
|
59
|
+
onMaskClick();
|
|
60
|
+
expect(visible.value).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('onMaskClick 当 mask.clickClose=false 时不关闭', () => {
|
|
64
|
+
const emit = vi.fn();
|
|
65
|
+
const { visible, open, onMaskClick } = useModal({ mask: { clickClose: false } }, emit);
|
|
66
|
+
|
|
67
|
+
open();
|
|
68
|
+
onMaskClick();
|
|
69
|
+
expect(visible.value).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('showMask 默认返回 true', () => {
|
|
73
|
+
const emit = vi.fn();
|
|
74
|
+
const { showMask } = useModal({}, emit);
|
|
75
|
+
expect(showMask()).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('showMask 当 mask.show=false 时返回 false', () => {
|
|
79
|
+
const emit = vi.fn();
|
|
80
|
+
const { showMask } = useModal({ mask: { show: false } }, emit);
|
|
81
|
+
expect(showMask()).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('onContentClick 阻止冒泡', () => {
|
|
85
|
+
const emit = vi.fn();
|
|
86
|
+
const { onContentClick } = useModal({}, emit);
|
|
87
|
+
const event = { stopPropagation: vi.fn() } as unknown as MouseEvent;
|
|
88
|
+
|
|
89
|
+
onContentClick(event);
|
|
90
|
+
expect(event.stopPropagation).toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* 处理 visible 状态、遮罩层点击关闭、emit 同步
|
|
10
10
|
*/
|
|
11
11
|
import { ref, watch } from 'vue';
|
|
12
|
+
import { type HookContext } from '../../components/types/hook';
|
|
12
13
|
import { ModelMask } from '../../types/common/model';
|
|
13
14
|
|
|
14
15
|
export interface UseModalProps {
|
|
@@ -16,8 +17,7 @@ export interface UseModalProps {
|
|
|
16
17
|
mask?: ModelMask;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
export function useModal(props: UseModalProps, emit: (...args: any[]) => void) {
|
|
20
|
+
export function useModal(props: UseModalProps, emit: HookContext['emit']) {
|
|
21
21
|
const visible = ref(props.visible ?? false);
|
|
22
22
|
|
|
23
23
|
watch(() => props.visible, (val) => {
|
|
@@ -11,14 +11,14 @@ import useEventListener from '../common/useEventListener';
|
|
|
11
11
|
|
|
12
12
|
const event = 'pointerdown';
|
|
13
13
|
export default function useClickAway(options: {
|
|
14
|
-
target:
|
|
15
|
-
handler: (event:
|
|
14
|
+
target: EventTarget | (() => EventTarget | null | undefined),
|
|
15
|
+
handler: (event: PointerEvent) => void
|
|
16
16
|
}) {
|
|
17
17
|
if (typeof window === 'undefined' || !window) {
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const listener = (event:
|
|
21
|
+
const listener = (event: Event) => {
|
|
22
22
|
const el = typeof options.target === 'function' ? options.target() : options.target;
|
|
23
23
|
if (!el) {
|
|
24
24
|
return;
|
|
@@ -28,7 +28,7 @@ export default function useClickAway(options: {
|
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
options.handler(event);
|
|
31
|
+
options.handler(event as PointerEvent);
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
return useEventListener({
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description debounceFilter / throttleFilter / createFilterWrapper 测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
import { createFilterWrapper, debounceFilter, throttleFilter } from '../filters';
|
|
11
|
+
|
|
12
|
+
beforeEach(() => { vi.useFakeTimers(); });
|
|
13
|
+
afterEach(() => { vi.useRealTimers(); });
|
|
14
|
+
|
|
15
|
+
describe('debounceFilter', () => {
|
|
16
|
+
it('延迟执行函数', () => {
|
|
17
|
+
const fn = vi.fn();
|
|
18
|
+
const debounced = createFilterWrapper(debounceFilter(100), fn);
|
|
19
|
+
|
|
20
|
+
debounced();
|
|
21
|
+
expect(fn).not.toHaveBeenCalled();
|
|
22
|
+
|
|
23
|
+
vi.advanceTimersByTime(100);
|
|
24
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('多次调用只执行最后一次', () => {
|
|
28
|
+
const fn = vi.fn();
|
|
29
|
+
const debounced = createFilterWrapper(debounceFilter(100), fn);
|
|
30
|
+
|
|
31
|
+
debounced();
|
|
32
|
+
debounced();
|
|
33
|
+
debounced();
|
|
34
|
+
|
|
35
|
+
vi.advanceTimersByTime(100);
|
|
36
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('ms <= 0 时立即执行', () => {
|
|
40
|
+
const fn = vi.fn();
|
|
41
|
+
const debounced = createFilterWrapper(debounceFilter(0), fn);
|
|
42
|
+
|
|
43
|
+
debounced();
|
|
44
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('maxWait 限制最大等待时间', () => {
|
|
48
|
+
const fn = vi.fn();
|
|
49
|
+
const debounced = createFilterWrapper(debounceFilter(100, { maxWait: 150 }), fn);
|
|
50
|
+
|
|
51
|
+
debounced();
|
|
52
|
+
vi.advanceTimersByTime(80);
|
|
53
|
+
debounced(); // 重置 debounce timer
|
|
54
|
+
vi.advanceTimersByTime(80); // 总共 160ms,超过 maxWait
|
|
55
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('maxWait <= 0 时立即执行', () => {
|
|
59
|
+
const fn = vi.fn();
|
|
60
|
+
const debounced = createFilterWrapper(debounceFilter(100, { maxWait: 0 }), fn);
|
|
61
|
+
|
|
62
|
+
debounced();
|
|
63
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('保留参数传递', () => {
|
|
67
|
+
const fn = vi.fn();
|
|
68
|
+
const debounced = createFilterWrapper(debounceFilter(50), fn);
|
|
69
|
+
|
|
70
|
+
debounced('a', 'b');
|
|
71
|
+
vi.advanceTimersByTime(50);
|
|
72
|
+
expect(fn).toHaveBeenCalledWith('a', 'b');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('throttleFilter', () => {
|
|
77
|
+
it('首次立即执行(leading=true 默认)', () => {
|
|
78
|
+
const fn = vi.fn();
|
|
79
|
+
const throttled = createFilterWrapper(throttleFilter(100), fn);
|
|
80
|
+
|
|
81
|
+
throttled();
|
|
82
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('间隔内不重复执行', () => {
|
|
86
|
+
const fn = vi.fn();
|
|
87
|
+
const throttled = createFilterWrapper(throttleFilter(100), fn);
|
|
88
|
+
|
|
89
|
+
throttled();
|
|
90
|
+
throttled();
|
|
91
|
+
throttled();
|
|
92
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('trailing=true 时间隔结束后执行尾部调用', () => {
|
|
96
|
+
const fn = vi.fn();
|
|
97
|
+
const throttled = createFilterWrapper(throttleFilter(100, true, true), fn);
|
|
98
|
+
|
|
99
|
+
throttled(); // leading 执行
|
|
100
|
+
throttled(); // 排队 trailing
|
|
101
|
+
|
|
102
|
+
vi.advanceTimersByTime(100);
|
|
103
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('trailing=false 不执行尾部调用', () => {
|
|
107
|
+
const fn = vi.fn();
|
|
108
|
+
const throttled = createFilterWrapper(throttleFilter(100, false, true), fn);
|
|
109
|
+
|
|
110
|
+
throttled(); // leading 执行
|
|
111
|
+
throttled(); // 不排队
|
|
112
|
+
|
|
113
|
+
vi.advanceTimersByTime(100);
|
|
114
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('leading=false 首次不执行', () => {
|
|
118
|
+
const fn = vi.fn();
|
|
119
|
+
const throttled = createFilterWrapper(throttleFilter(100, true, false), fn);
|
|
120
|
+
|
|
121
|
+
throttled();
|
|
122
|
+
expect(fn).not.toHaveBeenCalled();
|
|
123
|
+
|
|
124
|
+
vi.advanceTimersByTime(100);
|
|
125
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('ms <= 0 时立即执行', () => {
|
|
129
|
+
const fn = vi.fn();
|
|
130
|
+
const throttled = createFilterWrapper(throttleFilter(0), fn);
|
|
131
|
+
|
|
132
|
+
throttled();
|
|
133
|
+
throttled();
|
|
134
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description createHeightCache 测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { createHeightCache } from '../useHeightCache';
|
|
11
|
+
|
|
12
|
+
describe('createHeightCache', () => {
|
|
13
|
+
it('初始化后 getHeight 返回预估高度', () => {
|
|
14
|
+
const cache = createHeightCache(10, 40);
|
|
15
|
+
expect(cache.getHeight(0)).toBe(40);
|
|
16
|
+
expect(cache.getHeight(5)).toBe(40);
|
|
17
|
+
expect(cache.getHeight(9)).toBe(40);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('update 后 getHeight 返回实际高度', () => {
|
|
21
|
+
const cache = createHeightCache(10, 40);
|
|
22
|
+
cache.update(3, 60);
|
|
23
|
+
expect(cache.getHeight(3)).toBe(60);
|
|
24
|
+
// 其他未更新的仍为预估值
|
|
25
|
+
expect(cache.getHeight(4)).toBe(40);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('update 越界不报错', () => {
|
|
29
|
+
const cache = createHeightCache(5, 40);
|
|
30
|
+
cache.update(-1, 50); // 越界
|
|
31
|
+
cache.update(10, 50); // 越界
|
|
32
|
+
expect(cache.getHeight(0)).toBe(40); // 不受影响
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('getOffset 返回累计高度', () => {
|
|
36
|
+
const cache = createHeightCache(5, 40);
|
|
37
|
+
// 全部预估:0*40=0, 1*40=40, ...
|
|
38
|
+
expect(cache.getOffset(0)).toBe(0);
|
|
39
|
+
expect(cache.getOffset(1)).toBe(40);
|
|
40
|
+
expect(cache.getOffset(3)).toBe(120);
|
|
41
|
+
|
|
42
|
+
// 更新某项后
|
|
43
|
+
cache.update(1, 60);
|
|
44
|
+
expect(cache.getOffset(2)).toBe(40 + 60); // 100
|
|
45
|
+
expect(cache.getOffset(3)).toBe(40 + 60 + 40); // 140
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('getTotalHeight 返回所有项高度之和', () => {
|
|
49
|
+
const cache = createHeightCache(5, 40);
|
|
50
|
+
expect(cache.getTotalHeight()).toBe(200);
|
|
51
|
+
|
|
52
|
+
cache.update(0, 50);
|
|
53
|
+
expect(cache.getTotalHeight()).toBe(210);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('findIndex 二分查找定位正确', () => {
|
|
57
|
+
const cache = createHeightCache(100, 40);
|
|
58
|
+
|
|
59
|
+
// scrollTop=0 → index 0
|
|
60
|
+
expect(cache.findIndex(0)).toBe(0);
|
|
61
|
+
|
|
62
|
+
// scrollTop=40 → index 1
|
|
63
|
+
expect(cache.findIndex(40)).toBe(1);
|
|
64
|
+
|
|
65
|
+
// scrollTop=39 → 仍在 index 0 区间内
|
|
66
|
+
expect(cache.findIndex(39)).toBe(0);
|
|
67
|
+
|
|
68
|
+
// scrollTop=80 → index 2
|
|
69
|
+
expect(cache.findIndex(80)).toBe(2);
|
|
70
|
+
|
|
71
|
+
// scrollTop 超过总高度 → 最后一项
|
|
72
|
+
expect(cache.findIndex(99999)).toBe(99);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('findIndex 在实际高度不等时也能正确定位', () => {
|
|
76
|
+
const cache = createHeightCache(5, 40);
|
|
77
|
+
cache.update(0, 100); // [100, 40, 40, 40, 40]
|
|
78
|
+
|
|
79
|
+
// scrollTop=50 → 还在 index 0(0-100)
|
|
80
|
+
expect(cache.findIndex(50)).toBe(0);
|
|
81
|
+
|
|
82
|
+
// scrollTop=100 → index 1(100-140)
|
|
83
|
+
expect(cache.findIndex(100)).toBe(1);
|
|
84
|
+
|
|
85
|
+
// scrollTop=140 → index 2(140-180)
|
|
86
|
+
expect(cache.findIndex(140)).toBe(2);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('reset 重置缓存', () => {
|
|
90
|
+
const cache = createHeightCache(5, 40);
|
|
91
|
+
cache.update(0, 100);
|
|
92
|
+
|
|
93
|
+
cache.reset(3);
|
|
94
|
+
expect(cache.getHeight(0)).toBe(40); // 重置为预估值
|
|
95
|
+
expect(cache.getTotalHeight()).toBe(120); // 3 * 40
|
|
96
|
+
});
|
|
97
|
+
});
|