@dsh-workbench/shared 0.2.9 → 0.2.11

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.
Files changed (89) hide show
  1. package/dist/api/dsh-client.d.ts +4 -1
  2. package/dist/components/Activity.css +1 -0
  3. package/dist/components/Card.css +4 -4
  4. package/dist/components/CardAnchor.d.ts +7 -0
  5. package/dist/components/CardAnchor.js +8 -0
  6. package/dist/components/CardAnchor.module.css +3 -0
  7. package/dist/components/CardHead.css +1 -5
  8. package/dist/components/ChoiceGroup.css +11 -2
  9. package/dist/components/Field.css +4 -0
  10. package/dist/components/MockStrip.css +6 -0
  11. package/dist/components/Note.css +4 -4
  12. package/dist/components/SelectField.css +156 -0
  13. package/dist/components/SelectField.d.ts +57 -0
  14. package/dist/components/SelectField.js +236 -0
  15. package/dist/components/SettingsSection.css +2 -2
  16. package/dist/components/StateDot.d.ts +5 -0
  17. package/dist/components/StateDot.js +10 -0
  18. package/dist/components/TextField.css +5 -0
  19. package/dist/components/icons.d.ts +1 -0
  20. package/dist/components/icons.js +13 -0
  21. package/dist/host/__tests__/fakes.d.ts +41 -0
  22. package/dist/host/__tests__/fakes.js +76 -0
  23. package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
  24. package/dist/host/__tests__/preflight-fixtures.js +47 -0
  25. package/dist/host/context-inject.d.ts +24 -0
  26. package/dist/host/context-inject.js +74 -0
  27. package/dist/host/events.d.ts +9 -0
  28. package/dist/host/events.js +70 -0
  29. package/dist/host/mount-units.d.ts +52 -0
  30. package/dist/host/mount-units.js +186 -0
  31. package/dist/host/preflight.d.ts +58 -0
  32. package/dist/host/preflight.js +233 -0
  33. package/dist/host/probe-cache.d.ts +6 -0
  34. package/dist/host/probe-cache.js +19 -0
  35. package/dist/host/probe-libs.d.ts +45 -0
  36. package/dist/host/probe-libs.js +61 -0
  37. package/dist/host/probe-units.d.ts +17 -0
  38. package/dist/host/probe-units.js +102 -0
  39. package/dist/host/probe-verdict.d.ts +7 -0
  40. package/dist/host/probe-verdict.js +143 -0
  41. package/dist/host/tool-approval.d.ts +3 -1
  42. package/dist/host/tool-approval.js +3 -3
  43. package/dist/host/tool-kit.js +2 -2
  44. package/dist/host/tool-scope.d.ts +32 -0
  45. package/dist/host/tool-scope.js +84 -0
  46. package/dist/host/unit-readiness.d.ts +7 -0
  47. package/dist/host/unit-readiness.js +5 -0
  48. package/dist/host/unit-registry.d.ts +9 -0
  49. package/dist/host/unit-registry.js +33 -0
  50. package/dist/host/version-range.d.ts +2 -0
  51. package/dist/host/version-range.js +73 -0
  52. package/dist/host.d.ts +1 -0
  53. package/dist/host.js +1 -0
  54. package/dist/i18n/en.js +1 -1
  55. package/dist/i18n/index.d.ts +1 -1
  56. package/dist/i18n/zh.d.ts +1 -1
  57. package/dist/i18n/zh.js +1 -1
  58. package/dist/index.d.ts +2 -0
  59. package/dist/index.js +2 -0
  60. package/dist/lib/react.d.ts +2 -0
  61. package/dist/lib/react.js +2 -0
  62. package/package.json +1 -1
  63. package/src/api/dsh-client.ts +5 -1
  64. package/src/components/Activity.css +1 -0
  65. package/src/components/Card.css +4 -4
  66. package/src/components/Card.test.tsx +46 -0
  67. package/src/components/CardAnchor.module.css +3 -0
  68. package/src/components/CardAnchor.test.tsx +36 -0
  69. package/src/components/CardAnchor.tsx +20 -0
  70. package/src/components/CardHead.css +1 -5
  71. package/src/components/ChoiceGroup.css +11 -2
  72. package/src/components/Field.css +4 -0
  73. package/src/components/MockStrip.css +6 -0
  74. package/src/components/Note.css +4 -4
  75. package/src/components/SelectField.css +156 -0
  76. package/src/components/SelectField.test.tsx +238 -0
  77. package/src/components/SelectField.tsx +486 -0
  78. package/src/components/SettingsSection.css +2 -2
  79. package/src/components/TextField.css +5 -0
  80. package/src/components/icons.tsx +19 -0
  81. package/src/host/tool-approval.ts +4 -3
  82. package/src/host/tool-kit.ts +2 -2
  83. package/src/host.ts +1 -0
  84. package/src/i18n/en.ts +1 -1
  85. package/src/i18n/zh.ts +1 -1
  86. package/src/index.ts +2 -0
  87. package/src/lib/react.ts +3 -0
  88. package/dist/lib/lint-probe.d.ts +0 -1
  89. package/dist/lib/lint-probe.js +0 -6
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('../lib/react.js', () => ({
4
+ h: (type: unknown, props: unknown, ...children: unknown[]) => ({ type, props, children }),
5
+ }));
6
+
7
+ import { Card, type CardProps } from './Card.js';
8
+
9
+ const t = (key: string): string => key;
10
+
11
+ interface AnyElement {
12
+ readonly type: unknown;
13
+ readonly props: Record<string, unknown>;
14
+ readonly children: unknown[];
15
+ }
16
+
17
+ function card(extra: Partial<CardProps> = {}): AnyElement {
18
+ return Card({
19
+ t,
20
+ title: '目录概览',
21
+ refreshing: false,
22
+ onRefresh: () => {},
23
+ children: null,
24
+ ...extra,
25
+ }) as unknown as AnyElement;
26
+ }
27
+
28
+ function headOf(root: AnyElement): AnyElement {
29
+ return root.children[0] as AnyElement;
30
+ }
31
+
32
+ describe('卡壳(契约类 wb-card,第三方模块自己写也认这套)', () => {
33
+ it('★ 卡壳就是 section.wb-card + .wb-cardBody,里面是卡头与内容', () => {
34
+ const root = card();
35
+
36
+ expect(root.type).toBe('section');
37
+ expect(root.props['className']).toBe('wb-card');
38
+ expect(headOf(root).type).not.toBe('section');
39
+ expect((root.children[1] as AnyElement).props['className']).toBe('wb-cardBody');
40
+ });
41
+
42
+ it('★ 卡自己不带 id:这张卡是谁由外壳的锚点盖(模块装的是哪一代构建都不影响)', () => {
43
+ expect(card().props['id']).toBeUndefined();
44
+ expect(card({ head: null }).props['id']).toBeUndefined();
45
+ });
46
+ });
@@ -0,0 +1,3 @@
1
+ .anchor {
2
+ display: contents;
3
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('../lib/react.js', () => ({
4
+ h: (type: unknown, props: unknown, ...children: unknown[]) => ({ type, props, children }),
5
+ }));
6
+
7
+ vi.mock('./CardAnchor.module.css', () => ({ default: { anchor: 'wb-anchor' } }));
8
+
9
+ import { CardAnchor, cardIdOf } from './CardAnchor.js';
10
+
11
+ interface AnyElement {
12
+ readonly type: unknown;
13
+ readonly props: Record<string, unknown>;
14
+ readonly children: unknown[];
15
+ }
16
+
17
+ function anchor(unit: string): AnyElement {
18
+ return CardAnchor({ unit, children: 'card' }) as unknown as AnyElement;
19
+ }
20
+
21
+ describe('外壳给每张卡盖的锚点', () => {
22
+ it('★★ 锚点就是 #wb-card-<unit>,卡在外面量不出盒子也不动布局(display: contents)', () => {
23
+ const root = anchor('demo');
24
+
25
+ expect(root.type).toBe('div');
26
+ expect(root.props['id']).toBe('wb-card-demo');
27
+ expect(root.props['className']).toBe('wb-anchor');
28
+ expect(root.children).toEqual(['card']);
29
+ });
30
+
31
+ it('★ 名字由 unit 拼出来:空 unit 不给 id(一个页面里 id 必须唯一)', () => {
32
+ expect(cardIdOf(null)).toBeUndefined();
33
+ expect(cardIdOf('')).toBeUndefined();
34
+ expect(cardIdOf('cmdb')).toBe('wb-card-cmdb');
35
+ });
36
+ });
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from '../lib/react.js';
2
+ import { h } from '../lib/react.js';
3
+ import styles from './CardAnchor.module.css';
4
+
5
+ export function cardIdOf(unit: string | null): string | undefined {
6
+ return unit === null || unit === '' ? undefined : `wb-card-${unit}`;
7
+ }
8
+
9
+ export interface CardAnchorProps {
10
+ readonly unit: string;
11
+ readonly children?: ReactNode | undefined;
12
+ }
13
+
14
+ export function CardAnchor({ unit, children }: CardAnchorProps) {
15
+ return (
16
+ <div id={cardIdOf(unit)} className={styles.anchor}>
17
+ {children}
18
+ </div>
19
+ );
20
+ }
@@ -4,7 +4,7 @@
4
4
  align-items: center;
5
5
  gap: var(--wb-len-6);
6
6
  flex: none;
7
- padding: var(--wb-len-8) var(--wb-len-10) var(--wb-len-8) var(--wb-len-20);
7
+ padding: var(--wb-len-8) var(--wb-len-10) var(--wb-len-8) var(--wb-card-gutter);
8
8
  border-bottom: 0.5px solid var(--dsw-alias-border-l2);
9
9
  background: var(--dsw-alias-bg-layer-2);
10
10
  }
@@ -86,7 +86,3 @@
86
86
  .wb-cardGear:hover:not(:disabled) {
87
87
  color: var(--dsw-alias-label-primary);
88
88
  }
89
-
90
- .wb-cardRefresh.wb-spinning svg {
91
- animation: wb-spin var(--wb-dur-0-9) linear infinite;
92
- }
@@ -62,7 +62,7 @@
62
62
  }
63
63
 
64
64
  .wb-checkbox:disabled + .wb-checkboxBox {
65
- opacity: 0.5;
65
+ cursor: default;
66
66
  }
67
67
 
68
68
  .wb-checkbox:focus-visible + .wb-checkboxBox {
@@ -75,6 +75,16 @@
75
75
  background: var(--dsw-alias-interactive-bg-hover);
76
76
  }
77
77
 
78
+ .wb-choiceOption:has(:disabled) {
79
+ cursor: default;
80
+ opacity: 0.5;
81
+ }
82
+
83
+ .wb-choiceOption:has(:disabled),
84
+ .wb-choiceOption:has(:disabled):hover {
85
+ background: transparent;
86
+ }
87
+
78
88
  .wb-choiceLabel {
79
89
  flex: 1;
80
90
  min-width: 0;
@@ -115,7 +125,6 @@
115
125
 
116
126
  .wb-radio:disabled {
117
127
  cursor: default;
118
- opacity: 0.5;
119
128
  }
120
129
 
121
130
  .wb-radio:focus-visible {
@@ -14,3 +14,7 @@
14
14
  line-height: var(--wb-line-18);
15
15
  color: var(--dsw-alias-label-tertiary);
16
16
  }
17
+
18
+ .wb-field:has(:disabled) .wb-fieldLabel {
19
+ opacity: 0.4;
20
+ }
@@ -36,4 +36,10 @@
36
36
  border-color: transparent;
37
37
  background: var(--dsw-alias-brand-primary);
38
38
  color: var(--dsw-alias-label-primary-foreground);
39
+ }
40
+
41
+ .wb-mockChipOn:hover {
42
+ border-color: transparent;
43
+ background: var(--dsw-alias-brand-primary);
44
+ color: var(--dsw-alias-label-primary-foreground);
39
45
  }
@@ -1,14 +1,14 @@
1
1
 
2
2
  .wb-note {
3
3
  margin: 0;
4
- font-size: var(--dsw-font-xxxs-11-font-size);
5
- line-height: var(--wb-line-16);
4
+ font-size: var(--dsw-font-xxs-12-font-size);
5
+ line-height: var(--wb-line-18);
6
6
  color: var(--dsw-alias-label-tertiary);
7
7
  }
8
8
  .wb-problem {
9
9
  margin: 0;
10
- font-size: var(--dsw-font-xxxs-11-font-size);
11
- line-height: var(--wb-line-16);
10
+ font-size: var(--dsw-font-xxs-12-font-size);
11
+ line-height: var(--wb-line-18);
12
12
  color: var(--dsw-alias-state-error-primary);
13
13
  overflow-wrap: anywhere;
14
14
  }
@@ -0,0 +1,156 @@
1
+ :where(.wb-selectTrigger) {
2
+ box-sizing: border-box;
3
+ display: flex;
4
+ align-items: center;
5
+ gap: var(--wb-len-8);
6
+ width: 100%;
7
+ min-height: var(--wb-len-32);
8
+ padding: var(--wb-len-6) var(--wb-len-12);
9
+ border: 0.5px solid var(--dsw-alias-border-l4);
10
+ border-radius: var(--dsw-radius-md);
11
+ background: var(--dsw-alias-bg-layer-3);
12
+ font: var(--dsw-font-xs-13);
13
+ color: var(--dsw-alias-label-primary);
14
+ text-align: left;
15
+ cursor: pointer;
16
+ }
17
+
18
+ .wb-selectTrigger:focus-visible {
19
+ outline: none;
20
+ border-color: var(--dsw-alias-state-business-primary);
21
+ box-shadow: inset 0 0 0 0.5px var(--dsw-alias-state-business-primary);
22
+ }
23
+
24
+ .wb-selectTrigger:disabled {
25
+ cursor: not-allowed;
26
+ opacity: 0.4;
27
+ }
28
+
29
+ .wb-selectLoading:disabled {
30
+ cursor: default;
31
+ opacity: 1;
32
+ }
33
+
34
+ .wb-selectGhost {
35
+ gap: var(--wb-gap-xs);
36
+ min-height: 0;
37
+ height: var(--wb-len-34);
38
+ padding: 0 var(--wb-len-8);
39
+ border: none;
40
+ background: transparent;
41
+ font-size: var(--wb-font-row);
42
+ }
43
+
44
+ .wb-selectGhost:hover:not(:disabled) {
45
+ background: var(--dsw-alias-interactive-bg-hover);
46
+ }
47
+
48
+ .wb-selectGhost:focus-visible {
49
+ outline: var(--dsw-focus-ring-width) solid
50
+ var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
51
+ outline-offset: -2px;
52
+ border-color: transparent;
53
+ box-shadow: none;
54
+ }
55
+
56
+ .wb-selectAnchor {
57
+ display: block;
58
+ width: 100%;
59
+ }
60
+
61
+ .wb-selectIcon {
62
+ flex: none;
63
+ display: inline-flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ width: var(--wb-len-16);
67
+ height: var(--wb-len-20);
68
+ color: var(--dsw-alias-label-tertiary);
69
+ }
70
+
71
+ .wb-selectValue {
72
+ flex: 1;
73
+ min-width: 0;
74
+ display: flex;
75
+ align-items: baseline;
76
+ gap: var(--wb-len-6);
77
+ line-height: var(--wb-line-20);
78
+ overflow: hidden;
79
+ }
80
+
81
+ .wb-selectPlaceholder {
82
+ color: var(--dsw-alias-label-tertiary);
83
+ }
84
+
85
+ .wb-selectChevron {
86
+ flex: none;
87
+ display: inline-flex;
88
+ color: var(--dsw-alias-label-caption);
89
+ }
90
+
91
+ .wb-selectMenu {
92
+ min-width: var(--wb-len-240);
93
+ max-width: var(--wb-len-360);
94
+ max-height: min(var(--wb-len-320), calc(100vh - var(--wb-len-24)));
95
+ }
96
+
97
+ :where(.wb-selectSearch) {
98
+ box-sizing: border-box;
99
+ display: flex;
100
+ align-items: center;
101
+ gap: var(--wb-len-8);
102
+ width: 100%;
103
+ min-height: var(--wb-len-32);
104
+ padding: var(--wb-len-6) var(--wb-len-12);
105
+ border: 0.5px solid var(--dsw-alias-border-l4);
106
+ border-radius: var(--dsw-radius-md);
107
+ background: var(--dsw-alias-bg-layer-3);
108
+ }
109
+
110
+ .wb-selectSearch:focus-within {
111
+ border-color: var(--dsw-alias-state-business-primary);
112
+ box-shadow: inset 0 0 0 0.5px var(--dsw-alias-state-business-primary);
113
+ }
114
+
115
+ .wb-selectSearch.wb-selectGhost {
116
+ gap: var(--wb-gap-xs);
117
+ min-height: 0;
118
+ height: var(--wb-len-34);
119
+ padding: 0 var(--wb-len-8);
120
+ border: 0.5px solid var(--dsw-alias-border-l3);
121
+ background: var(--dsw-alias-bg-layer-2);
122
+ }
123
+
124
+ .wb-selectSearch.wb-selectGhost:focus-within {
125
+ border-color: var(--dsw-alias-state-business-primary);
126
+ box-shadow: none;
127
+ }
128
+
129
+ .wb-selectSearchIcon {
130
+ flex: none;
131
+ display: inline-flex;
132
+ color: var(--dsw-alias-label-tertiary);
133
+ }
134
+
135
+ .wb-textField.wb-selectSearchInput {
136
+ flex: 1;
137
+ min-width: 0;
138
+ min-height: 0;
139
+ padding: 0;
140
+ border: none;
141
+ background: transparent;
142
+ }
143
+
144
+ .wb-textField.wb-selectSearchInput:focus {
145
+ box-shadow: none;
146
+ }
147
+
148
+ .wb-selectFallback {
149
+ width: 100%;
150
+ }
151
+
152
+ .wb-selectHint {
153
+ font-size: var(--dsw-font-xxs-12-font-size);
154
+ line-height: var(--wb-line-18);
155
+ color: var(--dsw-alias-label-tertiary);
156
+ }
@@ -0,0 +1,238 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ let states: unknown[] = [];
4
+ vi.mock('../lib/react.js', () => ({
5
+ h: (type: unknown, props: unknown, ...children: unknown[]) => ({ type, props, children }),
6
+ useEffect: () => {},
7
+ useMemo: (compute: () => unknown) => compute(),
8
+ useRef: () => ({ current: null }),
9
+ useState: (initial: unknown) => [states.length > 0 ? states.shift() : initial, () => {}],
10
+ }));
11
+
12
+ vi.mock('./SelectField.css', () => ({ default: {} }));
13
+
14
+ import { Menu, StateDot, TextShimmer } from '@deepseek-ai/dsh-client-ui-primitives';
15
+ import type { MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives';
16
+ import { h } from '../lib/react.js';
17
+ import { Field } from './Field.js';
18
+ import { TextField } from './TextField.js';
19
+ import {
20
+ SelectField,
21
+ defaultSelectFilter,
22
+ selectCursorValue,
23
+ selectEntries,
24
+ stepSelectCursor,
25
+ } from './SelectField.js';
26
+
27
+ interface AnyElement {
28
+ readonly type: unknown;
29
+ readonly props: Record<string, unknown>;
30
+ readonly children?: unknown;
31
+ }
32
+
33
+ function childrenOf(element: AnyElement): unknown {
34
+ return element.children ?? element.props['children'];
35
+ }
36
+
37
+ function collect(node: unknown, out: AnyElement[] = []): AnyElement[] {
38
+ if (node === null || node === void 0 || typeof node === 'boolean') return out;
39
+ if (typeof node === 'string' || typeof node === 'number') return out;
40
+ if (Array.isArray(node)) {
41
+ for (const one of node) collect(one, out);
42
+ return out;
43
+ }
44
+ if (typeof node !== 'object') return out;
45
+ const element = node as AnyElement;
46
+ out.push(element);
47
+ collect(childrenOf(element), out);
48
+ return out;
49
+ }
50
+
51
+ function elementsOf(root: unknown): readonly AnyElement[] {
52
+ return collect(root);
53
+ }
54
+
55
+ function menuOf(root: unknown): AnyElement {
56
+ const menu = elementsOf(root).find((one) => one.type === Menu);
57
+ if (menu === undefined) throw new Error('没有渲染出 Menu');
58
+ return menu;
59
+ }
60
+
61
+ function inputOf(root: unknown): AnyElement | undefined {
62
+ return elementsOf(root).find((one) => one.type === TextField);
63
+ }
64
+
65
+ function render(props: Record<string, unknown>, state: readonly unknown[] = []): unknown {
66
+ states = [...state];
67
+ return SelectField({
68
+ value: 'b',
69
+ options: OPTIONS,
70
+ onChange: () => {},
71
+ ...props,
72
+ } as never);
73
+ }
74
+
75
+ const OPTIONS = [
76
+ { value: 'a', label: '工作区甲', group: '工作区' },
77
+ { value: 'b', label: '工作区乙', group: '工作区' },
78
+ { value: 'c', label: '默认工作区', group: '默认' },
79
+ ];
80
+
81
+ describe('SelectField:工作区那套下拉的公共形状', () => {
82
+ it('关着:触发钮上是当前选项,菜单条目已经按分组排好', () => {
83
+ const root = render({});
84
+ const menu = menuOf(root);
85
+
86
+ expect(menu.props['open']).toBe(false);
87
+ expect(menu.props['listClassName']).toContain('wb-selectMenu');
88
+ const trigger = elementsOf(menu.props['anchor']).find((one) => one.type === 'button');
89
+ expect(trigger?.props['aria-haspopup']).toBe('listbox');
90
+ expect(trigger?.props['aria-expanded']).toBe(false);
91
+ expect(JSON.stringify(trigger?.children)).toContain('工作区乙');
92
+
93
+ const items = menu.props['items'] as readonly MenuEntry[];
94
+ expect(items).toEqual([
95
+ { type: 'label', id: '__wb-select-group-0', text: '工作区' },
96
+ { id: 'a', label: '工作区甲' },
97
+ { id: 'b', label: '工作区乙' },
98
+ { type: 'label', id: '__wb-select-group-3', text: '默认' },
99
+ { id: 'c', label: '默认工作区' },
100
+ ]);
101
+ });
102
+
103
+ it('开着:锚点换成搜索框(这是这套交互的要点)', () => {
104
+ const root = render({}, [true, '', null]);
105
+ const menu = menuOf(root);
106
+
107
+ expect(menu.props['open']).toBe(true);
108
+ const anchor = elementsOf(menu.props['anchor']);
109
+ const box = anchor.find((one) => one.props['className'] === 'wb-selectSearch');
110
+ expect(box).toBeDefined();
111
+ expect(anchor.some((one) => one.type === 'button')).toBe(false);
112
+ expect(inputOf(menu.props['anchor'])?.props['value']).toBe('');
113
+ });
114
+
115
+ it('禁用:触发钮真的 disabled(压暗交给样式,别只在视觉上假装)', () => {
116
+ const root = render({ disabled: true });
117
+ const trigger = elementsOf(menuOf(root).props['anchor']).find((one) => one.type === 'button');
118
+
119
+ expect(trigger?.props['disabled']).toBe(true);
120
+ });
121
+
122
+ it('默认是常规下拉框,无边框那一档要显式选(工作区切换器走的是它)', () => {
123
+ const boxed = elementsOf(menuOf(render({})).props['anchor']).find(
124
+ (one) => one.type === 'button',
125
+ );
126
+ expect(boxed?.props['className']).toBe('wb-selectTrigger');
127
+
128
+ const ghost = elementsOf(menuOf(render({ variant: 'ghost' })).props['anchor']).find(
129
+ (one) => one.type === 'button',
130
+ );
131
+ expect(ghost?.props['className']).toBe('wb-selectTrigger wb-selectGhost');
132
+
133
+ const ghostSearch = elementsOf(
134
+ menuOf(render({ variant: 'ghost' }, [true, '', null])).props['anchor'],
135
+ ).find((one) => one.props['className'] === 'wb-selectSearch wb-selectGhost');
136
+ expect(ghostSearch).toBeDefined();
137
+ });
138
+
139
+ it('搜索:按纯文本过滤(label 不是字符串时用 searchText)', () => {
140
+ const root = render({}, [true, '默认', null]);
141
+ const items = menuOf(root).props['items'] as readonly MenuEntry[];
142
+ expect(items).toEqual([
143
+ { type: 'label', id: '__wb-select-group-0', text: '默认' },
144
+ { id: 'c', label: '默认工作区' },
145
+ ]);
146
+
147
+ states = [true, '甲', null];
148
+ const byText = menuOf(
149
+ SelectField({
150
+ value: 'b',
151
+ options: [{ value: 'a', label: <span>看不见的字</span>, searchText: '工作区甲' }],
152
+ onChange: () => {},
153
+ } as never),
154
+ ).props['items'] as readonly MenuEntry[];
155
+ expect(byText).toEqual([{ id: 'a', label: expect.anything() }]);
156
+ });
157
+
158
+ it('搜不到 / 加载中 / 空名单退化成手填,三种态各有明确落点', () => {
159
+ const empty = render({ emptyText: '没有匹配的工作区' }, [true, 'zzz', null]);
160
+ expect(menuOf(empty).props['items']).toEqual([
161
+ { type: 'label', id: '__wb-select-empty__', text: '没有匹配的工作区' },
162
+ ]);
163
+
164
+ const withoutEmpty = render({}, [true, 'zzz', null]);
165
+ expect(menuOf(withoutEmpty).props['items']).toEqual([]);
166
+
167
+ const loading = render({ loading: true, loadingText: '正在读工作区' });
168
+ const busy = elementsOf(loading).find((one) => one.type === 'button');
169
+ expect(busy?.props['aria-busy']).toBe('true');
170
+ expect(busy?.props['disabled']).toBe(true);
171
+ expect(elementsOf(busy?.children).some((one) => one.type === TextShimmer)).toBe(true);
172
+ expect(elementsOf(busy?.children).some((one) => one.type === StateDot)).toBe(true);
173
+
174
+ const fallback = render({
175
+ options: [],
176
+ fallback: { value: '手填的', onChange: () => {} },
177
+ });
178
+ expect(inputOf(fallback)?.props['value']).toBe('手填的');
179
+ });
180
+
181
+ it('label / hint 走 Field,footer 那条动作独立成一行', () => {
182
+ const root = render({ label: '工作区', hint: '三选一' });
183
+ expect(elementsOf(root).some((one) => one.type === Field)).toBe(true);
184
+
185
+ const footer = render({ footer: { label: '添加工作区…' }, onFooter: () => {} });
186
+ expect(menuOf(footer).props['footer']).toEqual([
187
+ { id: '__wb-select-footer__', label: '添加工作区…' },
188
+ ]);
189
+ });
190
+
191
+ it('选中与页脚动作各给各的出口,选中之后菜单关掉', () => {
192
+ const seen: string[] = [];
193
+ const root = render({ onChange: (next: string) => seen.push(next) });
194
+ (menuOf(root).props['onSelect'] as (id: string) => void)('c');
195
+ expect(seen).toEqual(['c']);
196
+
197
+ let added = 0;
198
+ const withFooter = render({ footer: { label: '添加工作区…' }, onFooter: () => (added += 1) });
199
+ (menuOf(withFooter).props['onSelect'] as (id: string) => void)('__wb-select-footer__');
200
+ expect(added).toBe(1);
201
+ });
202
+
203
+ it('游标:从 null 起步、两头回绕、空名单不动', () => {
204
+ expect(stepSelectCursor(OPTIONS, null, 1)).toBe(0);
205
+ expect(stepSelectCursor(OPTIONS, null, -1)).toBe(2);
206
+ expect(stepSelectCursor(OPTIONS, 2, 1)).toBe(0);
207
+ expect(stepSelectCursor(OPTIONS, 0, -1)).toBe(2);
208
+ expect(stepSelectCursor([], null, 1)).toBeNull();
209
+ expect(selectCursorValue(OPTIONS, 9)).toBe('c');
210
+ expect(selectCursorValue(OPTIONS, null)).toBeNull();
211
+ expect(selectCursorValue([], 0)).toBeNull();
212
+ });
213
+
214
+ it('默认过滤:空查询全过、大小写不敏感、label 与 searchText 都认', () => {
215
+ const option = { value: 'x', label: '腾讯云 · 生产' };
216
+ expect(defaultSelectFilter(option, ' ')).toBe(true);
217
+ expect(defaultSelectFilter(option, '生产')).toBe(true);
218
+ expect(defaultSelectFilter({ value: 'x', label: <span>生产</span>, searchText: 'tencent-prod' }, 'TENCENT')).toBe(
219
+ true,
220
+ );
221
+ expect(defaultSelectFilter(option, 'nope')).toBe(false);
222
+ });
223
+
224
+ it('条目构造:图标与禁用照传,分组标题只在换组时出现一次', () => {
225
+ const items = selectEntries(
226
+ [
227
+ { value: 'a', label: '甲', group: 'G', disabled: true },
228
+ { value: 'b', label: '乙', group: 'G', icon: <span>ico</span> },
229
+ ],
230
+ undefined,
231
+ );
232
+ expect(items).toEqual([
233
+ { type: 'label', id: '__wb-select-group-0', text: 'G' },
234
+ { id: 'a', label: '甲', disabled: true },
235
+ { id: 'b', label: '乙', icon: expect.anything() },
236
+ ]);
237
+ });
238
+ });