@kine-design/core 0.0.1-beta.6 → 0.0.1-beta.8
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/datePicker/__tests__/useDatePicker.test.ts +5 -0
- package/components/base/image/__tests__/useImage.test.ts +2 -0
- package/components/base/inputNumber/__tests__/useInputNumber.test.ts +36 -0
- package/components/base/timePicker/__tests__/useTimePicker.test.ts +6 -0
- package/components/base/tree/__tests__/tree.test.ts +9 -0
- package/components/template/table/__tests__/useTable.test.ts +4 -5
- package/compositions/common/__tests__/useEventListener.test.ts +27 -0
- package/compositions/common/__tests__/usePopover.test.ts +6 -1
- package/compositions/common/__tests__/useTeleport.test.ts +7 -0
- package/compositions/common/testAnchor.ts +211 -0
- package/compositions/modal/__tests__/useModal.test.ts +18 -0
- package/dist/compositions/common/testAnchor.d.ts +40 -0
- package/package.json +1 -1
- package/tools/__tests__/empty.test.ts +20 -1
|
@@ -27,6 +27,11 @@ describe('toDayjs', () => {
|
|
|
27
27
|
const d = toDayjs(undefined);
|
|
28
28
|
expect(d.isValid()).toBe(true);
|
|
29
29
|
});
|
|
30
|
+
|
|
31
|
+
it('无效日期字符串返回 invalid dayjs 对象', () => {
|
|
32
|
+
const d = toDayjs('not-a-date');
|
|
33
|
+
expect(d.isValid()).toBe(false);
|
|
34
|
+
});
|
|
30
35
|
});
|
|
31
36
|
|
|
32
37
|
describe('generateTimeColumn', () => {
|
|
@@ -33,6 +33,8 @@ describe('useImage', () => {
|
|
|
33
33
|
it('handleLoad 设置状态为 loaded 并触发 emit', () => {
|
|
34
34
|
const emit = createEmit();
|
|
35
35
|
const { status, handleLoad } = useImage(defaultProps, emit);
|
|
36
|
+
// Vitest node 环境下由 happy-dom/jsdom 提供 Event 构造函数,
|
|
37
|
+
// handleLoad 仅需接收 Event 实例作为参数传给 emit,不依赖 DOM 行为
|
|
36
38
|
const event = new Event('load');
|
|
37
39
|
|
|
38
40
|
handleLoad(event);
|
|
@@ -150,4 +150,40 @@ describe('useInputNumber 输入流程', () => {
|
|
|
150
150
|
const call = lastUpdate(emit);
|
|
151
151
|
expect(call[1]).toBe(3);
|
|
152
152
|
});
|
|
153
|
+
|
|
154
|
+
it('disabled 时 increment 和 decrement 为 no-op', () => {
|
|
155
|
+
const { increment, decrement, emit } = setup({ modelValue: 5, disabled: true });
|
|
156
|
+
emit.mockClear();
|
|
157
|
+
increment();
|
|
158
|
+
decrement();
|
|
159
|
+
// disabled 状态下不触发任何 emit
|
|
160
|
+
expect(emit).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('readonly 时 increment 和 decrement 为 no-op', () => {
|
|
164
|
+
const { increment, decrement, emit } = setup({ modelValue: 5, readonly: true });
|
|
165
|
+
emit.mockClear();
|
|
166
|
+
increment();
|
|
167
|
+
decrement();
|
|
168
|
+
// readonly 状态下不触发任何 emit
|
|
169
|
+
expect(emit).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('useInputNumber 中间态处理', () => {
|
|
174
|
+
it('validate(".") 为中间态 → emit null', () => {
|
|
175
|
+
const { validate, emit } = setup({ modelValue: null });
|
|
176
|
+
validate('.' as unknown as number);
|
|
177
|
+
// "." 经 handleInputChange 会被转为 "0.",但直接 validate('.') 是非法输入
|
|
178
|
+
// validate 里 isNaN(+'.') 为 true 且不是 '' 也不是 '-',所以不触发 emit
|
|
179
|
+
expect(emit.mock.calls.filter((c: unknown[]) => c[0] === 'update:modelValue')).toHaveLength(0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('validate("-.") 为非法输入 → 不触发 emit', () => {
|
|
183
|
+
const { validate, emit } = setup({ modelValue: null });
|
|
184
|
+
emit.mockClear();
|
|
185
|
+
validate('-.' as unknown as number);
|
|
186
|
+
// '-.' → isNaN(+'-.')=true, 不是 '' 不是 '-',走非法输入分支
|
|
187
|
+
expect(emit).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
153
189
|
});
|
|
@@ -17,6 +17,12 @@ describe('generateColumn', () => {
|
|
|
17
17
|
it('生成 60 分钟列,步进 5', () => {
|
|
18
18
|
expect(generateColumn(60, 5)).toEqual([0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]);
|
|
19
19
|
});
|
|
20
|
+
|
|
21
|
+
// step=0 会导致 for 循环条件 i += 0 永远不推进,造成死循环。
|
|
22
|
+
// 源码无守卫,此处跳过并记录风险,避免测试挂起。
|
|
23
|
+
it.skip('step=0 会导致死循环(源码未做守卫)', () => {
|
|
24
|
+
// generateColumn(24, 0) — 无限循环,不可执行
|
|
25
|
+
});
|
|
20
26
|
});
|
|
21
27
|
|
|
22
28
|
describe('useTimePicker', () => {
|
|
@@ -211,4 +211,13 @@ describe('Tree', () => {
|
|
|
211
211
|
const childKeys = tree.getChildrenKeys(roots[0]);
|
|
212
212
|
expect(childKeys).toEqual(['b']);
|
|
213
213
|
});
|
|
214
|
+
|
|
215
|
+
it('getTreeData 不存在的 key 返回 undefined 元素', () => {
|
|
216
|
+
// cacheMap.get(key) 对不存在的 key 返回 undefined,
|
|
217
|
+
// getNodesByKeys 不做过滤,数组中会包含 undefined
|
|
218
|
+
const tree = new Tree({ data: SAMPLE_DATA });
|
|
219
|
+
const nodes = tree.getTreeData(['nonexistent']);
|
|
220
|
+
expect(nodes).toHaveLength(1);
|
|
221
|
+
expect(nodes[0]).toBeUndefined();
|
|
222
|
+
});
|
|
214
223
|
});
|
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
8
|
*/
|
|
9
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
10
10
|
import { useTable } from '../useTable';
|
|
11
11
|
|
|
12
12
|
describe('useTable', () => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
});
|
|
13
16
|
const createRenders = () => ({
|
|
14
17
|
empty: 'EMPTY',
|
|
15
18
|
tbodyTr: (opt: any) => `TD:${opt.param}:${opt.data}`,
|
|
@@ -58,8 +61,6 @@ describe('useTable', () => {
|
|
|
58
61
|
const result = initTable(createRenders(), columns, data);
|
|
59
62
|
expect(result.thead).toBe('THEAD:[TH:name:姓名]');
|
|
60
63
|
expect(errorSpy).toHaveBeenCalled();
|
|
61
|
-
|
|
62
|
-
errorSpy.mockRestore();
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
it('列宽 number 转换为 px', () => {
|
|
@@ -133,7 +134,5 @@ describe('useTable', () => {
|
|
|
133
134
|
|
|
134
135
|
error('test error');
|
|
135
136
|
expect(spy).toHaveBeenCalledWith('[水墨UI表格组件] test error');
|
|
136
|
-
|
|
137
|
-
spy.mockRestore();
|
|
138
137
|
});
|
|
139
138
|
});
|
|
@@ -15,6 +15,7 @@ describe('useEventListener', () => {
|
|
|
15
15
|
const target = {
|
|
16
16
|
addEventListener: vi.fn(),
|
|
17
17
|
removeEventListener: vi.fn(),
|
|
18
|
+
dispatchEvent: vi.fn(),
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
const { add, remove } = useEventListener({
|
|
@@ -35,6 +36,7 @@ describe('useEventListener', () => {
|
|
|
35
36
|
const target = {
|
|
36
37
|
addEventListener: vi.fn(),
|
|
37
38
|
removeEventListener: vi.fn(),
|
|
39
|
+
dispatchEvent: vi.fn(),
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
const { onMounted, onBeforeUnmount } = useEventListener({
|
|
@@ -55,6 +57,7 @@ describe('useEventListener', () => {
|
|
|
55
57
|
const realTarget = {
|
|
56
58
|
addEventListener: vi.fn(),
|
|
57
59
|
removeEventListener: vi.fn(),
|
|
60
|
+
dispatchEvent: vi.fn(),
|
|
58
61
|
};
|
|
59
62
|
const targetFn = vi.fn(() => realTarget);
|
|
60
63
|
|
|
@@ -68,4 +71,28 @@ describe('useEventListener', () => {
|
|
|
68
71
|
expect(targetFn).toHaveBeenCalled();
|
|
69
72
|
expect(realTarget.addEventListener).toHaveBeenCalledWith('click', handler);
|
|
70
73
|
});
|
|
74
|
+
|
|
75
|
+
it('target 为 null 时 add 抛出异常(无防御性检查)', () => {
|
|
76
|
+
// useEventListener 不对 null/undefined target 做守卫,
|
|
77
|
+
// 调用 add() 时会对 null 调用 addEventListener,导致 TypeError
|
|
78
|
+
const handler = vi.fn();
|
|
79
|
+
const { add } = useEventListener({
|
|
80
|
+
target: null as unknown as EventTarget,
|
|
81
|
+
event: 'click',
|
|
82
|
+
handler,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(() => add()).toThrow();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('target 为返回 undefined 的函数时 add 抛出异常', () => {
|
|
89
|
+
const handler = vi.fn();
|
|
90
|
+
const { add } = useEventListener({
|
|
91
|
+
target: (() => undefined) as unknown as () => EventTarget,
|
|
92
|
+
event: 'click',
|
|
93
|
+
handler,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(() => add()).toThrow();
|
|
97
|
+
});
|
|
71
98
|
});
|
|
@@ -27,7 +27,12 @@ describe('usePopover', () => {
|
|
|
27
27
|
|
|
28
28
|
it('自定义 offset', () => {
|
|
29
29
|
const { popoverOptions } = usePopover(undefined, { offset: 10 });
|
|
30
|
-
|
|
30
|
+
const middleware = popoverOptions.popper?.middleware;
|
|
31
|
+
expect(middleware).toHaveLength(3);
|
|
32
|
+
// middleware[0] 是 offset(10),验证自定义 offset 值确实被应用
|
|
33
|
+
const offsetMiddleware = middleware![0] as { name: string; options: unknown };
|
|
34
|
+
expect(offsetMiddleware.name).toBe('offset');
|
|
35
|
+
expect(offsetMiddleware.options).toBe(10);
|
|
31
36
|
});
|
|
32
37
|
|
|
33
38
|
it('扩展 middleware', () => {
|
|
@@ -22,4 +22,11 @@ describe('initTeleportOptions', () => {
|
|
|
22
22
|
const custom = { to: '#app', disabled: false };
|
|
23
23
|
expect(initTeleportOptions(custom as any)).toBe(custom);
|
|
24
24
|
});
|
|
25
|
+
|
|
26
|
+
it('false 不命中 true/undefined 分支,原样返回', () => {
|
|
27
|
+
// initTeleportOptions 只对 true 和 undefined 返回 { to: 'body' },
|
|
28
|
+
// false 走 else 分支直接返回原值
|
|
29
|
+
const result = initTeleportOptions(false as unknown as import('vue').TeleportProps);
|
|
30
|
+
expect(result).toBe(false);
|
|
31
|
+
});
|
|
25
32
|
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Test anchor system: provide/inject keys, types, and kDefineComponent wrapper
|
|
3
|
+
* @author kine-design
|
|
4
|
+
* @date 2026/4/28
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* Renders business-semantic `data-k` attributes on DOM elements when testAnchor is enabled,
|
|
8
|
+
* allowing E2E tests to target components by stable identifiers.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
cloneVNode,
|
|
12
|
+
defineComponent,
|
|
13
|
+
inject,
|
|
14
|
+
type ComponentPropsOptions,
|
|
15
|
+
type EmitsOptions,
|
|
16
|
+
type InjectionKey,
|
|
17
|
+
type Ref,
|
|
18
|
+
type SetupContext,
|
|
19
|
+
type VNode,
|
|
20
|
+
} from 'vue';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Injection keys
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** Global switch provided by KConfigProvider to enable data-k rendering */
|
|
27
|
+
export const K_TEST_ANCHOR_KEY: InjectionKey<Ref<boolean>> = Symbol('k-test-anchor');
|
|
28
|
+
|
|
29
|
+
/** Field identity provided by KFormItem so child inputs inherit field name */
|
|
30
|
+
export const K_FIELD_KEY: InjectionKey<Ref<string | undefined>> = Symbol('k-field');
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Types
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export interface KAnchorConfig {
|
|
37
|
+
/** Semantic type: 'field' | 'action' | 'table' | 'col' | 'dialog' | 'drawer' | 'tab' */
|
|
38
|
+
type: string;
|
|
39
|
+
/** Which prop to derive the identity from (e.g., 'prop' for KFormItem, 'title' for KDialog) */
|
|
40
|
+
prop?: string;
|
|
41
|
+
/** Derive identity from default slot text content (for KButton) */
|
|
42
|
+
slotText?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface KDefineComponentOptions {
|
|
46
|
+
name: string;
|
|
47
|
+
props?: ComponentPropsOptions;
|
|
48
|
+
emits?: EmitsOptions;
|
|
49
|
+
kAnchor?: KAnchorConfig;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Helpers
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract text content from a VNode tree (for slotText mode).
|
|
58
|
+
* Walks children recursively, concatenating string segments.
|
|
59
|
+
*/
|
|
60
|
+
function extractSlotText(nodes: VNode[] | undefined): string {
|
|
61
|
+
if (!nodes) return '';
|
|
62
|
+
let text = '';
|
|
63
|
+
for (const node of nodes) {
|
|
64
|
+
if (typeof node.children === 'string') {
|
|
65
|
+
text += node.children;
|
|
66
|
+
} else if (Array.isArray(node.children)) {
|
|
67
|
+
text += extractSlotText(node.children as VNode[]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return text.trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the data-k value for a component instance.
|
|
75
|
+
* Returns undefined when testAnchor is off or no identity can be derived.
|
|
76
|
+
*/
|
|
77
|
+
function resolveDataK(
|
|
78
|
+
config: KAnchorConfig,
|
|
79
|
+
props: Record<string, unknown>,
|
|
80
|
+
attrs: Record<string, unknown>,
|
|
81
|
+
fieldFromParent: string | undefined,
|
|
82
|
+
slotNodes?: VNode[],
|
|
83
|
+
): string | undefined {
|
|
84
|
+
// Developer override via k-{type} attr (e.g., k-action="approve")
|
|
85
|
+
const overrideKey = `k-${config.type}`;
|
|
86
|
+
const override = attrs[overrideKey];
|
|
87
|
+
if (typeof override === 'string' && override) {
|
|
88
|
+
return `${config.type}:${override}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Derive from prop
|
|
92
|
+
if (config.prop) {
|
|
93
|
+
const val = props[config.prop];
|
|
94
|
+
if (typeof val === 'string' && val) {
|
|
95
|
+
return `${config.type}:${val}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Derive from slot text (e.g., KButton default slot)
|
|
100
|
+
if (config.slotText && slotNodes) {
|
|
101
|
+
const text = extractSlotText(slotNodes);
|
|
102
|
+
if (text) {
|
|
103
|
+
return `${config.type}:${text}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Fallback: check props.text (for KButton text prop)
|
|
108
|
+
if (config.slotText && typeof props.text === 'string' && props.text) {
|
|
109
|
+
return `${config.type}:${props.text}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Inherit from parent KFormItem for input-type components
|
|
113
|
+
if (config.type === 'field' && !config.prop && fieldFromParent) {
|
|
114
|
+
return `${config.type}:${fieldFromParent}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Inject data-k attribute into a VNode.
|
|
122
|
+
* Uses cloneVNode to avoid mutating the original.
|
|
123
|
+
*/
|
|
124
|
+
function injectDataK(vnode: VNode, dataK: string): VNode {
|
|
125
|
+
return cloneVNode(vnode, { 'data-k': dataK });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// kDefineComponent
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Wrapper around Vue's defineComponent that automatically injects `data-k`
|
|
134
|
+
* test anchor attributes when testAnchor is enabled via KConfigProvider.
|
|
135
|
+
*
|
|
136
|
+
* Usage:
|
|
137
|
+
* ```tsx
|
|
138
|
+
* export default kDefineComponent((_props, ctx) => {
|
|
139
|
+
* return () => <div class="k-form-item">...</div>;
|
|
140
|
+
* }, {
|
|
141
|
+
* name: 'KFormItem',
|
|
142
|
+
* props: FormCore.formItemProps,
|
|
143
|
+
* kAnchor: { type: 'field', prop: 'prop' },
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export function kDefineComponent<P extends Record<string, unknown>>(
|
|
148
|
+
setup: (props: P, ctx: SetupContext) => () => VNode | VNode[] | null | undefined,
|
|
149
|
+
options: KDefineComponentOptions,
|
|
150
|
+
) {
|
|
151
|
+
const { kAnchor, ...defineOptions } = options;
|
|
152
|
+
|
|
153
|
+
return defineComponent((rawProps: P, ctx: SetupContext) => {
|
|
154
|
+
// Call the original setup to get the render function
|
|
155
|
+
const render = setup(rawProps, ctx);
|
|
156
|
+
|
|
157
|
+
// If no kAnchor config, skip entirely
|
|
158
|
+
if (!kAnchor) {
|
|
159
|
+
return render;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Read the global testAnchor switch
|
|
163
|
+
const testAnchor = inject(K_TEST_ANCHOR_KEY, undefined);
|
|
164
|
+
|
|
165
|
+
// For field-type components without own prop, inject parent field identity
|
|
166
|
+
const parentField = (kAnchor.type === 'field' && !kAnchor.prop)
|
|
167
|
+
? inject(K_FIELD_KEY, undefined)
|
|
168
|
+
: undefined;
|
|
169
|
+
|
|
170
|
+
return () => {
|
|
171
|
+
const vnode = render();
|
|
172
|
+
|
|
173
|
+
// When testAnchor is off (default), render as-is
|
|
174
|
+
if (!testAnchor?.value) {
|
|
175
|
+
return vnode;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Resolve the data-k value
|
|
179
|
+
const props = rawProps as Record<string, unknown>;
|
|
180
|
+
const slotNodes = kAnchor.slotText ? (ctx.slots.default?.() ?? undefined) : undefined;
|
|
181
|
+
const dataK = resolveDataK(
|
|
182
|
+
kAnchor,
|
|
183
|
+
props,
|
|
184
|
+
ctx.attrs,
|
|
185
|
+
parentField?.value,
|
|
186
|
+
slotNodes,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (!dataK) {
|
|
190
|
+
return vnode;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Inject into root vnode
|
|
194
|
+
if (vnode === null || vnode === undefined) {
|
|
195
|
+
return vnode;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Handle array of vnodes (Fragment) — inject into first element
|
|
199
|
+
if (Array.isArray(vnode)) {
|
|
200
|
+
if (vnode.length === 0) return vnode;
|
|
201
|
+
const first = vnode[0];
|
|
202
|
+
if (first && typeof first === 'object') {
|
|
203
|
+
return [injectDataK(first, dataK), ...vnode.slice(1)];
|
|
204
|
+
}
|
|
205
|
+
return vnode;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return injectDataK(vnode, dataK);
|
|
209
|
+
};
|
|
210
|
+
}, defineOptions as Parameters<typeof defineComponent>[1]);
|
|
211
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
8
|
*/
|
|
9
9
|
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { nextTick, reactive } from 'vue';
|
|
10
11
|
import { useModal } from '../useModal';
|
|
11
12
|
|
|
12
13
|
describe('useModal', () => {
|
|
@@ -89,4 +90,21 @@ describe('useModal', () => {
|
|
|
89
90
|
onContentClick(event);
|
|
90
91
|
expect(event.stopPropagation).toHaveBeenCalled();
|
|
91
92
|
});
|
|
93
|
+
|
|
94
|
+
it('外部 props.visible 变化时 watcher 同步内部 visible', async () => {
|
|
95
|
+
const emit = vi.fn();
|
|
96
|
+
const props = reactive({ visible: false });
|
|
97
|
+
const { visible } = useModal(props, emit);
|
|
98
|
+
|
|
99
|
+
expect(visible.value).toBe(false);
|
|
100
|
+
|
|
101
|
+
// 外部修改 props.visible → watcher 同步 visible.value
|
|
102
|
+
props.visible = true;
|
|
103
|
+
await nextTick();
|
|
104
|
+
expect(visible.value).toBe(true);
|
|
105
|
+
|
|
106
|
+
props.visible = false;
|
|
107
|
+
await nextTick();
|
|
108
|
+
expect(visible.value).toBe(false);
|
|
109
|
+
});
|
|
92
110
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ComponentPropsOptions, EmitsOptions, InjectionKey, Ref, SetupContext, VNode } from 'vue';
|
|
2
|
+
/** Global switch provided by KConfigProvider to enable data-k rendering */
|
|
3
|
+
export declare const K_TEST_ANCHOR_KEY: InjectionKey<Ref<boolean>>;
|
|
4
|
+
/** Field identity provided by KFormItem so child inputs inherit field name */
|
|
5
|
+
export declare const K_FIELD_KEY: InjectionKey<Ref<string | undefined>>;
|
|
6
|
+
export interface KAnchorConfig {
|
|
7
|
+
/** Semantic type: 'field' | 'action' | 'table' | 'col' | 'dialog' | 'drawer' | 'tab' */
|
|
8
|
+
type: string;
|
|
9
|
+
/** Which prop to derive the identity from (e.g., 'prop' for KFormItem, 'title' for KDialog) */
|
|
10
|
+
prop?: string;
|
|
11
|
+
/** Derive identity from default slot text content (for KButton) */
|
|
12
|
+
slotText?: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface KDefineComponentOptions {
|
|
15
|
+
name: string;
|
|
16
|
+
props?: ComponentPropsOptions;
|
|
17
|
+
emits?: EmitsOptions;
|
|
18
|
+
kAnchor?: KAnchorConfig;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Wrapper around Vue's defineComponent that automatically injects `data-k`
|
|
22
|
+
* test anchor attributes when testAnchor is enabled via KConfigProvider.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* ```tsx
|
|
26
|
+
* export default kDefineComponent((_props, ctx) => {
|
|
27
|
+
* return () => <div class="k-form-item">...</div>;
|
|
28
|
+
* }, {
|
|
29
|
+
* name: 'KFormItem',
|
|
30
|
+
* props: FormCore.formItemProps,
|
|
31
|
+
* kAnchor: { type: 'field', prop: 'prop' },
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function kDefineComponent<P extends Record<string, unknown>>(setup: (props: P, ctx: SetupContext) => () => VNode | VNode[] | null | undefined, options: KDefineComponentOptions): import('vue').DefineSetupFnComponent<P, EmitsOptions, {}, P & ({
|
|
36
|
+
[x: `on${Capitalize<string>}`]: ((...args: never) => any) | undefined;
|
|
37
|
+
} | {
|
|
38
|
+
[x: `on${Capitalize<string>}`]: ((...args: any[]) => any) | undefined;
|
|
39
|
+
}), import('vue').PublicProps>;
|
|
40
|
+
export {};
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
8
|
*/
|
|
9
|
-
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
10
10
|
import { notEmpty, isEmpty } from '../empty';
|
|
11
11
|
|
|
12
12
|
describe('notEmpty', () => {
|
|
@@ -57,8 +57,27 @@ describe('notEmpty', () => {
|
|
|
57
57
|
it('非空正则非空', () => expect(notEmpty(/abc/)).toBe(true));
|
|
58
58
|
|
|
59
59
|
// TypedArray
|
|
60
|
+
// notEmpty 对 TypedArray 返回 value.length(number),0 为 falsy,>0 为 truthy
|
|
60
61
|
it('空 Uint8Array 为空', () => expect(notEmpty(new Uint8Array(0))).toBe(0));
|
|
61
62
|
it('非空 Float64Array 非空', () => expect(notEmpty(new Float64Array([1.5]))).toBe(1));
|
|
63
|
+
|
|
64
|
+
// WeakMap / WeakSet — 无法判断是否为空,始终返回 false 并 console.error
|
|
65
|
+
it('WeakMap 无法判断,返回 false', () => {
|
|
66
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
67
|
+
expect(notEmpty(new WeakMap())).toBe(false);
|
|
68
|
+
expect(spy).toHaveBeenCalledWith('WeakMap和WeakSet无法以通用的方式判断是否为空。');
|
|
69
|
+
spy.mockRestore();
|
|
70
|
+
});
|
|
71
|
+
it('WeakSet 无法判断,返回 false', () => {
|
|
72
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
73
|
+
expect(notEmpty(new WeakSet())).toBe(false);
|
|
74
|
+
expect(spy).toHaveBeenCalledWith('WeakMap和WeakSet无法以通用的方式判断是否为空。');
|
|
75
|
+
spy.mockRestore();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// BigInt — typeof bigint 分支,始终返回 true
|
|
79
|
+
it('BigInt(0) 非空', () => expect(notEmpty(BigInt(0))).toBe(true));
|
|
80
|
+
it('BigInt(42) 非空', () => expect(notEmpty(BigInt(42))).toBe(true));
|
|
62
81
|
});
|
|
63
82
|
|
|
64
83
|
describe('isEmpty', () => {
|