@kine-design/core 0.0.1-beta.5 → 0.0.1-beta.7
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/testAnchor.ts +211 -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/testAnchor.d.ts +40 -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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description useNotificationQueue 测试
|
|
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 { useNotificationQueue } from '../useNotification';
|
|
11
|
+
|
|
12
|
+
beforeEach(() => { vi.useFakeTimers(); });
|
|
13
|
+
afterEach(() => { vi.useRealTimers(); });
|
|
14
|
+
|
|
15
|
+
describe('useNotificationQueue', () => {
|
|
16
|
+
it('初始通知列表为空', () => {
|
|
17
|
+
const { notifications } = useNotificationQueue();
|
|
18
|
+
expect(notifications.value).toHaveLength(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('add 添加通知并返回 id', () => {
|
|
22
|
+
const { notifications, add } = useNotificationQueue();
|
|
23
|
+
|
|
24
|
+
const id = add({ title: '测试通知' });
|
|
25
|
+
expect(typeof id).toBe('number');
|
|
26
|
+
expect(notifications.value).toHaveLength(1);
|
|
27
|
+
expect(notifications.value[0].title).toBe('测试通知');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('add 使用默认值', () => {
|
|
31
|
+
const { notifications, add } = useNotificationQueue();
|
|
32
|
+
|
|
33
|
+
add({ title: 'test' });
|
|
34
|
+
const item = notifications.value[0];
|
|
35
|
+
|
|
36
|
+
expect(item.message).toBe('');
|
|
37
|
+
expect(item.type).toBe('info');
|
|
38
|
+
expect(item.duration).toBe(4500);
|
|
39
|
+
expect(item.position).toBe('top-right');
|
|
40
|
+
expect(item.closable).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('add 自定义所有字段', () => {
|
|
44
|
+
const { notifications, add } = useNotificationQueue();
|
|
45
|
+
|
|
46
|
+
add({
|
|
47
|
+
title: '错误',
|
|
48
|
+
message: '操作失败',
|
|
49
|
+
type: 'error',
|
|
50
|
+
duration: 3000,
|
|
51
|
+
position: 'bottom-left',
|
|
52
|
+
closable: false,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const item = notifications.value[0];
|
|
56
|
+
expect(item.type).toBe('error');
|
|
57
|
+
expect(item.message).toBe('操作失败');
|
|
58
|
+
expect(item.duration).toBe(3000);
|
|
59
|
+
expect(item.position).toBe('bottom-left');
|
|
60
|
+
expect(item.closable).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('remove 按 id 移除通知', () => {
|
|
64
|
+
const { notifications, add, remove } = useNotificationQueue();
|
|
65
|
+
|
|
66
|
+
const id1 = add({ title: '通知1' });
|
|
67
|
+
const id2 = add({ title: '通知2' });
|
|
68
|
+
|
|
69
|
+
remove(id1);
|
|
70
|
+
expect(notifications.value).toHaveLength(1);
|
|
71
|
+
expect(notifications.value[0].id).toBe(id2);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('remove 不存在的 id 不报错', () => {
|
|
75
|
+
const { notifications, add, remove } = useNotificationQueue();
|
|
76
|
+
|
|
77
|
+
add({ title: '通知' });
|
|
78
|
+
remove(999);
|
|
79
|
+
expect(notifications.value).toHaveLength(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('duration > 0 时自动移除', () => {
|
|
83
|
+
const { notifications, add } = useNotificationQueue();
|
|
84
|
+
|
|
85
|
+
add({ title: '自动关闭', duration: 1000 });
|
|
86
|
+
expect(notifications.value).toHaveLength(1);
|
|
87
|
+
|
|
88
|
+
vi.advanceTimersByTime(1000);
|
|
89
|
+
expect(notifications.value).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('duration = 0 时不自动移除', () => {
|
|
93
|
+
const { notifications, add } = useNotificationQueue();
|
|
94
|
+
|
|
95
|
+
add({ title: '手动关闭', duration: 0 });
|
|
96
|
+
|
|
97
|
+
vi.advanceTimersByTime(10000);
|
|
98
|
+
expect(notifications.value).toHaveLength(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('多条通知独立管理', () => {
|
|
102
|
+
const { notifications, add } = useNotificationQueue();
|
|
103
|
+
|
|
104
|
+
add({ title: '快速消失', duration: 1000 });
|
|
105
|
+
add({ title: '慢速消失', duration: 5000 });
|
|
106
|
+
add({ title: '不消失', duration: 0 });
|
|
107
|
+
|
|
108
|
+
expect(notifications.value).toHaveLength(3);
|
|
109
|
+
|
|
110
|
+
vi.advanceTimersByTime(1000);
|
|
111
|
+
expect(notifications.value).toHaveLength(2);
|
|
112
|
+
|
|
113
|
+
vi.advanceTimersByTime(4000);
|
|
114
|
+
expect(notifications.value).toHaveLength(1);
|
|
115
|
+
expect(notifications.value[0].title).toBe('不消失');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('id 自增不重复', () => {
|
|
119
|
+
const { add } = useNotificationQueue();
|
|
120
|
+
|
|
121
|
+
const id1 = add({ title: '1' });
|
|
122
|
+
const id2 = add({ title: '2' });
|
|
123
|
+
const id3 = add({ title: '3' });
|
|
124
|
+
|
|
125
|
+
expect(id1).not.toBe(id2);
|
|
126
|
+
expect(id2).not.toBe(id3);
|
|
127
|
+
expect(id3 - id1).toBe(2);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -162,7 +162,7 @@ export function usePopover(
|
|
|
162
162
|
}
|
|
163
163
|
};
|
|
164
164
|
|
|
165
|
-
const
|
|
165
|
+
const onBeforeUnmountEvents: Function[] = [];
|
|
166
166
|
|
|
167
167
|
onMounted(() => {
|
|
168
168
|
if (!popoverRef.value || !contentRef.value) {
|
|
@@ -188,8 +188,8 @@ export function usePopover(
|
|
|
188
188
|
|
|
189
189
|
onBeforeUnmount(() => {
|
|
190
190
|
if (clickAwayInstance) {
|
|
191
|
-
const {
|
|
192
|
-
|
|
191
|
+
const { onBeforeUnmount } = clickAwayInstance;
|
|
192
|
+
onBeforeUnmount();
|
|
193
193
|
}
|
|
194
194
|
instance?.destroy();
|
|
195
195
|
});
|
|
@@ -228,7 +228,7 @@ export function usePopover(
|
|
|
228
228
|
popperInstance,
|
|
229
229
|
style, arrowStyle,
|
|
230
230
|
lifecycle: {
|
|
231
|
-
|
|
231
|
+
onBeforeUnmountEvents,
|
|
232
232
|
},
|
|
233
233
|
};
|
|
234
234
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - autoMode 时监听系统媒体查询变化,并在卸载时清理监听器
|
|
13
13
|
*/
|
|
14
14
|
import { ref, watch, onMounted, onUnmounted } from 'vue';
|
|
15
|
+
import { type HookContext } from '../../types/hook';
|
|
15
16
|
import { DarkModeProps } from './props';
|
|
16
17
|
|
|
17
18
|
/** 将暗色状态同步到 html[dark] attribute */
|
|
@@ -42,8 +43,7 @@ function writeStorage(storageKey: string, isDark: boolean) {
|
|
|
42
43
|
|
|
43
44
|
export function useDarkMode(
|
|
44
45
|
props: DarkModeProps,
|
|
45
|
-
|
|
46
|
-
ctx: { emit: (event: string, ...args: any[]) => void },
|
|
46
|
+
ctx: Pick<HookContext, 'emit'>,
|
|
47
47
|
) {
|
|
48
48
|
const isDark = ref(props.modelValue ?? false);
|
|
49
49
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description useMenu 测试
|
|
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 { useMenu } from '../useMenu';
|
|
11
|
+
|
|
12
|
+
const SAMPLE_DATA = [
|
|
13
|
+
{
|
|
14
|
+
key: 'home', label: '首页',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: 'system', label: '系统管理', children: [
|
|
18
|
+
{ key: 'user', label: '用户管理' },
|
|
19
|
+
{ key: 'role', label: '角色管理' },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'about', label: '关于',
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
describe('useMenu', () => {
|
|
28
|
+
it('initNodes 构建节点树', () => {
|
|
29
|
+
const { nodesRef, nodeMap } = useMenu({ data: SAMPLE_DATA });
|
|
30
|
+
|
|
31
|
+
expect(nodesRef.value).toHaveLength(3);
|
|
32
|
+
expect(nodeMap.size).toBe(5); // 3 + 2 children
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('根节点 isRoot=true,子节点有 parent', () => {
|
|
36
|
+
const { nodeMap } = useMenu({ data: SAMPLE_DATA });
|
|
37
|
+
|
|
38
|
+
expect(nodeMap.get('home')!.isRoot).toBe(true);
|
|
39
|
+
expect(nodeMap.get('user')!.isRoot).toBe(false);
|
|
40
|
+
expect(nodeMap.get('user')!.parent).toBe(nodeMap.get('system'));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('defaultExpandAll 展开所有节点', () => {
|
|
44
|
+
const { nodeMap } = useMenu({ data: SAMPLE_DATA, defaultExpandAll: true });
|
|
45
|
+
expect(nodeMap.get('system')!.expand).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('默认不展开', () => {
|
|
49
|
+
const { nodeMap } = useMenu({ data: SAMPLE_DATA });
|
|
50
|
+
expect(nodeMap.get('system')!.expand).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('toggleExpand 切换展开', () => {
|
|
54
|
+
const { nodeMap, toggleExpand } = useMenu({ data: SAMPLE_DATA });
|
|
55
|
+
const node = nodeMap.get('system')!;
|
|
56
|
+
|
|
57
|
+
expect(node.expand).toBe(false);
|
|
58
|
+
toggleExpand(node);
|
|
59
|
+
expect(node.expand).toBe(true);
|
|
60
|
+
toggleExpand(node);
|
|
61
|
+
expect(node.expand).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('setActive 激活节点并取消其他激活', () => {
|
|
65
|
+
const emit = vi.fn();
|
|
66
|
+
const { nodeMap, setActive } = useMenu({ data: SAMPLE_DATA }, emit);
|
|
67
|
+
|
|
68
|
+
setActive(nodeMap.get('home')!);
|
|
69
|
+
expect(nodeMap.get('home')!.isActive).toBe(true);
|
|
70
|
+
expect(emit).toHaveBeenCalledWith('update:active', 'home');
|
|
71
|
+
|
|
72
|
+
setActive(nodeMap.get('about')!);
|
|
73
|
+
expect(nodeMap.get('home')!.isActive).toBe(false);
|
|
74
|
+
expect(nodeMap.get('about')!.isActive).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('受控 active 初始化', () => {
|
|
78
|
+
const { nodeMap } = useMenu({ data: SAMPLE_DATA, active: 'about' });
|
|
79
|
+
expect(nodeMap.get('about')!.isActive).toBe(true);
|
|
80
|
+
expect(nodeMap.get('home')!.isActive).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('toggleChecked 切换选中', () => {
|
|
84
|
+
const { nodeMap, toggleChecked } = useMenu({ data: SAMPLE_DATA });
|
|
85
|
+
const node = nodeMap.get('user')!;
|
|
86
|
+
|
|
87
|
+
toggleChecked(node, true);
|
|
88
|
+
expect(node.checked).toBe(true);
|
|
89
|
+
expect(node.indeterminate).toBe(false);
|
|
90
|
+
|
|
91
|
+
toggleChecked(node, false);
|
|
92
|
+
expect(node.checked).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('getCheckedKeys 获取选中 key', () => {
|
|
96
|
+
const { nodeMap, toggleChecked, getCheckedKeys } = useMenu({ data: SAMPLE_DATA });
|
|
97
|
+
|
|
98
|
+
toggleChecked(nodeMap.get('user')!, true);
|
|
99
|
+
toggleChecked(nodeMap.get('about')!, true);
|
|
100
|
+
|
|
101
|
+
const keys = getCheckedKeys();
|
|
102
|
+
expect(keys).toContain('user');
|
|
103
|
+
expect(keys).toContain('about');
|
|
104
|
+
expect(keys).toHaveLength(2);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('半选节点不计入 getCheckedKeys', () => {
|
|
108
|
+
const { nodeMap, getCheckedKeys } = useMenu({ data: SAMPLE_DATA });
|
|
109
|
+
const node = nodeMap.get('system')!;
|
|
110
|
+
node.indeterminate = true;
|
|
111
|
+
node.checked = false;
|
|
112
|
+
|
|
113
|
+
expect(getCheckedKeys()).not.toContain('system');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('getNodesByKeys 批量获取节点', () => {
|
|
117
|
+
const { getNodesByKeys } = useMenu({ data: SAMPLE_DATA });
|
|
118
|
+
const nodes = getNodesByKeys(['home', 'user']);
|
|
119
|
+
expect(nodes).toHaveLength(2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('getNodesByKeys 过滤不存在的 key', () => {
|
|
123
|
+
const { getNodesByKeys } = useMenu({ data: SAMPLE_DATA });
|
|
124
|
+
const nodes = getNodesByKeys(['home', 'nonexistent']);
|
|
125
|
+
expect(nodes).toHaveLength(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('checkedKeys 初始化选中', () => {
|
|
129
|
+
const { nodeMap } = useMenu({ data: SAMPLE_DATA, checkedKeys: ['user', 'about'] });
|
|
130
|
+
expect(nodeMap.get('user')!.checked).toBe(true);
|
|
131
|
+
expect(nodeMap.get('about')!.checked).toBe(true);
|
|
132
|
+
expect(nodeMap.get('home')!.checked).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('initNodes 重新初始化', () => {
|
|
136
|
+
const { initNodes, nodesRef, nodeMap } = useMenu({ data: SAMPLE_DATA });
|
|
137
|
+
|
|
138
|
+
initNodes([{ key: 'new', label: '新节点' }]);
|
|
139
|
+
expect(nodesRef.value).toHaveLength(1);
|
|
140
|
+
expect(nodeMap.size).toBe(1);
|
|
141
|
+
expect(nodeMap.get('new')!.label).toBe('新节点');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('自定义 config 字段映射', () => {
|
|
145
|
+
const data = [
|
|
146
|
+
{ id: 'a', name: '菜单A', items: [{ id: 'b', name: '子菜单B' }] },
|
|
147
|
+
];
|
|
148
|
+
const { nodeMap } = useMenu({
|
|
149
|
+
data: data as any,
|
|
150
|
+
config: { key: 'id', label: 'name', children: 'items' },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(nodeMap.size).toBe(2);
|
|
154
|
+
expect(nodeMap.get('a')).toBeDefined();
|
|
155
|
+
expect(nodeMap.get('b')).toBeDefined();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description usePagination 测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { ref } from 'vue';
|
|
11
|
+
import { usePagination, type Pager } from '../usePagination';
|
|
12
|
+
|
|
13
|
+
/** 提取页码列表中的值 */
|
|
14
|
+
const values = (list: Pager[]) => list.map(p => p.value);
|
|
15
|
+
/** 提取页码列表中当前页标记 */
|
|
16
|
+
const currentFlags = (list: Pager[]) => list.filter(p => p.isCurrent).map(p => p.value);
|
|
17
|
+
|
|
18
|
+
describe('usePagination', () => {
|
|
19
|
+
describe('getPageBtnLength', () => {
|
|
20
|
+
it('计算总页数', () => {
|
|
21
|
+
const current = ref(1);
|
|
22
|
+
const { getPageBtnLength } = usePagination({ total: 100, pageSize: 10 }, current);
|
|
23
|
+
expect(getPageBtnLength()).toBe(10);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('向上取整', () => {
|
|
27
|
+
const current = ref(1);
|
|
28
|
+
const { getPageBtnLength } = usePagination({ total: 101, pageSize: 10 }, current);
|
|
29
|
+
expect(getPageBtnLength()).toBe(11);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('默认 total=0, pageSize=10', () => {
|
|
33
|
+
const current = ref(1);
|
|
34
|
+
const { getPageBtnLength } = usePagination({}, current);
|
|
35
|
+
expect(getPageBtnLength()).toBe(0);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('getPageNumList', () => {
|
|
40
|
+
it('只有一页时返回 [1]', () => {
|
|
41
|
+
const current = ref(1);
|
|
42
|
+
const { getPageNumList } = usePagination({ total: 5, pageSize: 10 }, current);
|
|
43
|
+
const list = getPageNumList();
|
|
44
|
+
expect(values(list)).toEqual([1]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('总页数 <= maxPageBtn 时不折叠', () => {
|
|
48
|
+
const current = ref(1);
|
|
49
|
+
const { getPageNumList } = usePagination({
|
|
50
|
+
total: 80, pageSize: 10, maxPageBtn: 10,
|
|
51
|
+
}, current);
|
|
52
|
+
const list = getPageNumList();
|
|
53
|
+
expect(values(list)).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('当前页靠左时右侧折叠', () => {
|
|
57
|
+
const current = ref(1);
|
|
58
|
+
const { getPageNumList } = usePagination({
|
|
59
|
+
total: 200, pageSize: 10, foldedMaxPageBtn: 5, maxPageBtn: 10,
|
|
60
|
+
}, current);
|
|
61
|
+
const list = getPageNumList();
|
|
62
|
+
|
|
63
|
+
// 应包含折叠符 "..."
|
|
64
|
+
expect(list.some(p => p.type === 'folded')).toBe(true);
|
|
65
|
+
// 最后一个应是尾页 20
|
|
66
|
+
expect(list[list.length - 1].value).toBe(20);
|
|
67
|
+
// isCurrent 标记正确
|
|
68
|
+
expect(currentFlags(list)).toEqual([1]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('当前页靠右时左侧折叠', () => {
|
|
72
|
+
const current = ref(20);
|
|
73
|
+
const { getPageNumList } = usePagination({
|
|
74
|
+
total: 200, pageSize: 10, foldedMaxPageBtn: 5, maxPageBtn: 10,
|
|
75
|
+
}, current);
|
|
76
|
+
const list = getPageNumList();
|
|
77
|
+
|
|
78
|
+
expect(list[0].value).toBe(1); // 首页
|
|
79
|
+
expect(list[1].type).toBe('folded'); // 左折叠
|
|
80
|
+
expect(currentFlags(list)).toEqual([20]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('当前页居中时两侧折叠', () => {
|
|
84
|
+
const current = ref(10);
|
|
85
|
+
const { getPageNumList } = usePagination({
|
|
86
|
+
total: 200, pageSize: 10, foldedMaxPageBtn: 5, maxPageBtn: 10,
|
|
87
|
+
}, current);
|
|
88
|
+
const list = getPageNumList();
|
|
89
|
+
|
|
90
|
+
expect(list[0].value).toBe(1); // 首页
|
|
91
|
+
expect(list[list.length - 1].value).toBe(20); // 尾页
|
|
92
|
+
// 两个折叠符
|
|
93
|
+
const foldedCount = list.filter(p => p.type === 'folded').length;
|
|
94
|
+
expect(foldedCount).toBe(2);
|
|
95
|
+
expect(currentFlags(list)).toEqual([10]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('showEdgePageNum=false 不显示首尾页', () => {
|
|
99
|
+
const current = ref(10);
|
|
100
|
+
const { getPageNumList } = usePagination({
|
|
101
|
+
total: 200, pageSize: 10, foldedMaxPageBtn: 5, maxPageBtn: 10, showEdgePageNum: false,
|
|
102
|
+
}, current);
|
|
103
|
+
const list = getPageNumList();
|
|
104
|
+
|
|
105
|
+
// 不应以 1 开头
|
|
106
|
+
expect(list[0].value).not.toBe(1);
|
|
107
|
+
expect(list[list.length - 1].value).not.toBe(20);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('折叠符的 jump 值正确', () => {
|
|
111
|
+
const current = ref(10);
|
|
112
|
+
const { getPageNumList } = usePagination({
|
|
113
|
+
total: 200, pageSize: 10, foldedMaxPageBtn: 5, maxPageBtn: 10,
|
|
114
|
+
}, current);
|
|
115
|
+
const list = getPageNumList();
|
|
116
|
+
|
|
117
|
+
const foldedPagers = list.filter(p => p.type === 'folded');
|
|
118
|
+
// 每个折叠符都应有 jump 值
|
|
119
|
+
foldedPagers.forEach(p => {
|
|
120
|
+
expect(p.jump).toBeGreaterThan(0);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('isCurrent 标记正确跟随 currentValue', () => {
|
|
125
|
+
const current = ref(5);
|
|
126
|
+
const { getPageNumList } = usePagination({
|
|
127
|
+
total: 80, pageSize: 10,
|
|
128
|
+
}, current);
|
|
129
|
+
const list = getPageNumList();
|
|
130
|
+
|
|
131
|
+
expect(currentFlags(list)).toEqual([5]);
|
|
132
|
+
|
|
133
|
+
current.value = 3;
|
|
134
|
+
const list2 = getPageNumList();
|
|
135
|
+
expect(currentFlags(list2)).toEqual([3]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description useTable 测试
|
|
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 { useTable } from '../useTable';
|
|
11
|
+
|
|
12
|
+
describe('useTable', () => {
|
|
13
|
+
const createRenders = () => ({
|
|
14
|
+
empty: 'EMPTY',
|
|
15
|
+
tbodyTr: (opt: any) => `TD:${opt.param}:${opt.data}`,
|
|
16
|
+
theadTh: (opt: any) => `TH:${opt.param}:${opt.label}`,
|
|
17
|
+
thead: (ths: string[]) => `THEAD:[${ths.join(',')}]`,
|
|
18
|
+
tbody: (trs: string[]) => `TBODY:[${trs.join(',')}]`,
|
|
19
|
+
tbodyTrs: (tds: string[], i: number) => `TR${i}:[${tds.join(',')}]`,
|
|
20
|
+
initSlot: () => undefined,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('initTable 正确构建表头和表体', () => {
|
|
24
|
+
const { initTable } = useTable();
|
|
25
|
+
const columns = [
|
|
26
|
+
{ props: { param: 'name', label: '姓名' } },
|
|
27
|
+
{ props: { param: 'age', label: '年龄' } },
|
|
28
|
+
];
|
|
29
|
+
const data = [
|
|
30
|
+
{ name: '张三', age: 25 },
|
|
31
|
+
{ name: '李四', age: 30 },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const result = initTable(createRenders(), columns, data);
|
|
35
|
+
|
|
36
|
+
expect(result.thead).toBe('THEAD:[TH:name:姓名,TH:age:年龄]');
|
|
37
|
+
expect(result.tbody).toContain('TD:name:张三');
|
|
38
|
+
expect(result.tbody).toContain('TD:age:30');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('数据为空时渲染 empty', () => {
|
|
42
|
+
const { initTable } = useTable();
|
|
43
|
+
const columns = [{ props: { param: 'name', label: '姓名' } }];
|
|
44
|
+
|
|
45
|
+
const result = initTable(createRenders(), columns, []);
|
|
46
|
+
expect(result.tbody).toBe('EMPTY');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('列没有 props 时被过滤', () => {
|
|
50
|
+
const { initTable } = useTable();
|
|
51
|
+
const errorSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
52
|
+
const columns = [
|
|
53
|
+
{ props: { param: 'name', label: '姓名' } },
|
|
54
|
+
{}, // 无 props
|
|
55
|
+
];
|
|
56
|
+
const data = [{ name: '张三' }];
|
|
57
|
+
|
|
58
|
+
const result = initTable(createRenders(), columns, data);
|
|
59
|
+
expect(result.thead).toBe('THEAD:[TH:name:姓名]');
|
|
60
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
61
|
+
|
|
62
|
+
errorSpy.mockRestore();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('列宽 number 转换为 px', () => {
|
|
66
|
+
const { initTable } = useTable();
|
|
67
|
+
const renders = {
|
|
68
|
+
...createRenders(),
|
|
69
|
+
theadTh: (opt: any) => `TH:${opt.style?.width}`,
|
|
70
|
+
};
|
|
71
|
+
const columns = [{ props: { param: 'name', label: '姓名', width: 200 } }];
|
|
72
|
+
|
|
73
|
+
const result = initTable(renders, columns, [{ name: 'test' }]);
|
|
74
|
+
expect(result.thead).toContain('200px');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('列宽 string 直接使用', () => {
|
|
78
|
+
const { initTable } = useTable();
|
|
79
|
+
const renders = {
|
|
80
|
+
...createRenders(),
|
|
81
|
+
theadTh: (opt: any) => `TH:${opt.style?.width}`,
|
|
82
|
+
};
|
|
83
|
+
const columns = [{ props: { param: 'name', label: '姓名', width: '50%' } }];
|
|
84
|
+
|
|
85
|
+
const result = initTable(renders, columns, [{ name: 'test' }]);
|
|
86
|
+
expect(result.thead).toContain('50%');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('data 中缺少字段时返回空字符串', () => {
|
|
90
|
+
const { initTable } = useTable();
|
|
91
|
+
const columns = [{ props: { param: 'missing', label: '缺失' } }];
|
|
92
|
+
const data = [{ name: 'test' }];
|
|
93
|
+
|
|
94
|
+
const result = initTable(createRenders(), columns, data);
|
|
95
|
+
expect(result.tbody).toContain('TD:missing:');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('initSlot 返回 slot 时传递给 tbodyTr', () => {
|
|
99
|
+
const { initTable } = useTable();
|
|
100
|
+
const bodySlot = () => 'custom';
|
|
101
|
+
const renders = {
|
|
102
|
+
...createRenders(),
|
|
103
|
+
initSlot: () => ({ body: bodySlot, head: undefined }),
|
|
104
|
+
tbodyTr: (opt: any) => `TD:${opt.slot ? 'has-slot' : 'no-slot'}`,
|
|
105
|
+
};
|
|
106
|
+
const columns = [{ props: { param: 'name', label: '姓名' } }];
|
|
107
|
+
const data = [{ name: 'test' }];
|
|
108
|
+
|
|
109
|
+
const result = initTable(renders, columns, data);
|
|
110
|
+
expect(result.tbody).toContain('has-slot');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('slotInfo 包含行数据和索引', () => {
|
|
114
|
+
const { initTable } = useTable();
|
|
115
|
+
let capturedSlotInfo: any;
|
|
116
|
+
const renders = {
|
|
117
|
+
...createRenders(),
|
|
118
|
+
tbodyTr: (opt: any) => {
|
|
119
|
+
capturedSlotInfo = opt.slotInfo;
|
|
120
|
+
return 'TD';
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
const columns = [{ props: { param: 'name', label: '姓名' } }];
|
|
124
|
+
const data = [{ name: '张三' }];
|
|
125
|
+
|
|
126
|
+
initTable(renders, columns, data);
|
|
127
|
+
expect(capturedSlotInfo).toEqual({ data: { name: '张三' }, index: 0 });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('error 输出带前缀的警告', () => {
|
|
131
|
+
const { error } = useTable();
|
|
132
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
133
|
+
|
|
134
|
+
error('test error');
|
|
135
|
+
expect(spy).toHaveBeenCalledWith('[水墨UI表格组件] test error');
|
|
136
|
+
|
|
137
|
+
spy.mockRestore();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -49,10 +49,8 @@ export function useTable() {
|
|
|
49
49
|
|
|
50
50
|
/** 从 data[i] 中安全取值 */
|
|
51
51
|
const getData = (i: number, param: string) => {
|
|
52
|
-
if (data[i]
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
return '';
|
|
52
|
+
if (data[i] == null) return '';
|
|
53
|
+
return data[i][param] ?? '';
|
|
56
54
|
};
|
|
57
55
|
|
|
58
56
|
/** 向每行的 td 列表中追加一列 */
|
|
@@ -10,4 +10,15 @@ import { SlotsType } from '@vue/runtime-core';
|
|
|
10
10
|
|
|
11
11
|
export type UseHookResult<Props, S extends SlotsType, Return> = Return;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Core hook 所需的最小 context 类型。
|
|
15
|
+
* 使用 `(...args: any[]) => void` 而非 `(event: string, ...) => void`,
|
|
16
|
+
* 以兼容 Vue defineComponent typed emits 产生的窄化 emit 签名。
|
|
17
|
+
*/
|
|
18
|
+
export type HookContext = {
|
|
19
|
+
emit: (...args: any[]) => void;
|
|
20
|
+
slots: Readonly<Record<string, ((...args: any[]) => any) | undefined>>;
|
|
21
|
+
expose?: (exposed?: Record<string, any>) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
13
24
|
|