@kine-design/core 0.0.1-beta.4 → 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/.vlaude/last-session-id +1 -0
- 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/button/api.ts +1 -1
- package/components/base/button/props.d.ts +3 -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/datePicker/api.ts +4 -0
- package/components/base/datePicker/props.d.ts +26 -0
- package/components/base/dropdown/useDropdown.ts +2 -1
- package/components/base/image/__tests__/useImage.test.ts +174 -0
- package/components/base/input/api.ts +5 -1
- package/components/base/input/props.d.ts +26 -1
- package/components/base/input/useInput.ts +12 -2
- package/components/base/inputNumber/__tests__/useInputNumber.test.ts +153 -0
- package/components/base/inputNumber/api.ts +2 -1
- package/components/base/inputNumber/props.d.ts +9 -1
- package/components/base/inputNumber/useInputNumber.ts +53 -11
- package/components/base/loading/api.ts +1 -1
- package/components/base/loading/props.d.ts +3 -1
- package/components/base/popover/usePopover.ts +4 -3
- package/components/base/progress/api.ts +3 -1
- package/components/base/progress/props.d.ts +15 -0
- package/components/base/rate/useRate.ts +2 -1
- package/components/base/select/api.ts +3 -0
- package/components/base/select/props.d.ts +21 -1
- package/components/base/select/useSelect.ts +2 -1
- package/components/base/slider/api.ts +3 -1
- package/components/base/slider/props.d.ts +14 -0
- package/components/base/slider/useSlider.ts +6 -2
- 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/tag/api.ts +1 -1
- package/components/base/tag/props.d.ts +3 -1
- package/components/base/timePicker/__tests__/useTimePicker.test.ts +118 -0
- package/components/base/timePicker/api.ts +4 -1
- package/components/base/timePicker/props.d.ts +20 -0
- package/components/base/tooltip/api.ts +1 -0
- package/components/base/tooltip/props.d.ts +6 -0
- package/components/base/transfer/useTransfer.ts +2 -1
- package/components/base/tree/__tests__/tree.test.ts +214 -0
- package/components/message/dialog/api.ts +2 -1
- package/components/message/dialog/props.d.ts +7 -0
- package/components/message/drawer/api.ts +1 -0
- package/components/message/drawer/props.d.ts +7 -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/components/types/props.d.ts +5 -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/useComponentSize.ts +17 -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 +4 -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/useComponentSize.d.ts +6 -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 +153 -23
- 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,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
|
|
|
@@ -50,3 +50,8 @@ type MPropMethod<T, TConstructor = any> = [T] extends [
|
|
|
50
50
|
: never
|
|
51
51
|
|
|
52
52
|
export type MPropType<T> = MPropConstructor<T> | MPropConstructor<T>[];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @description unified component size variants
|
|
56
|
+
*/
|
|
57
|
+
export type KineSize = 'large' | 'medium' | 'small';
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description composable for unified component size with global injection support
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/4/7
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { computed, inject, type InjectionKey, type Ref } from 'vue';
|
|
10
|
+
import { KineSize } from '../../components/types/props';
|
|
11
|
+
|
|
12
|
+
export const KINE_SIZE_KEY: InjectionKey<Ref<KineSize>> = Symbol('kine-size');
|
|
13
|
+
|
|
14
|
+
export function useComponentSize(props: { size?: KineSize }): Ref<KineSize> {
|
|
15
|
+
const globalSize = inject(KINE_SIZE_KEY, undefined);
|
|
16
|
+
return computed(() => props.size ?? globalSize?.value ?? 'medium');
|
|
17
|
+
}
|