@kine-design/core 0.0.1-beta.6 → 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.
@@ -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
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kine-design/core",
3
- "version": "0.0.1-beta.6",
3
+ "version": "0.0.1-beta.7",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/core.js",