@dsh-workbench/shared 0.2.10 → 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.
- package/dist/api/dsh-client.d.ts +4 -1
- package/dist/components/Activity.css +1 -0
- package/dist/components/Card.css +4 -4
- package/dist/components/CardAnchor.d.ts +7 -0
- package/dist/components/CardAnchor.js +8 -0
- package/dist/components/CardAnchor.module.css +3 -0
- package/dist/components/CardHead.css +1 -5
- package/dist/components/ChoiceGroup.css +11 -2
- package/dist/components/Field.css +4 -0
- package/dist/components/MockStrip.css +6 -0
- package/dist/components/Note.css +4 -4
- package/dist/components/SelectField.css +156 -0
- package/dist/components/SelectField.d.ts +57 -0
- package/dist/components/SelectField.js +236 -0
- package/dist/components/SettingsSection.css +2 -2
- package/dist/components/StateDot.d.ts +5 -0
- package/dist/components/StateDot.js +10 -0
- package/dist/components/TextField.css +5 -0
- package/dist/components/icons.d.ts +1 -0
- package/dist/components/icons.js +13 -0
- package/dist/host/__tests__/fakes.d.ts +41 -0
- package/dist/host/__tests__/fakes.js +76 -0
- package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
- package/dist/host/__tests__/preflight-fixtures.js +47 -0
- package/dist/host/context-inject.d.ts +24 -0
- package/dist/host/context-inject.js +74 -0
- package/dist/host/events.d.ts +9 -0
- package/dist/host/events.js +70 -0
- package/dist/host/mount-units.d.ts +52 -0
- package/dist/host/mount-units.js +186 -0
- package/dist/host/preflight.d.ts +58 -0
- package/dist/host/preflight.js +233 -0
- package/dist/host/probe-cache.d.ts +6 -0
- package/dist/host/probe-cache.js +19 -0
- package/dist/host/probe-libs.d.ts +45 -0
- package/dist/host/probe-libs.js +61 -0
- package/dist/host/probe-units.d.ts +17 -0
- package/dist/host/probe-units.js +102 -0
- package/dist/host/probe-verdict.d.ts +7 -0
- package/dist/host/probe-verdict.js +143 -0
- package/dist/host/tool-approval.d.ts +2 -0
- package/dist/host/tool-approval.js +1 -1
- package/dist/host/tool-kit.js +2 -2
- package/dist/host/tool-scope.d.ts +32 -0
- package/dist/host/tool-scope.js +84 -0
- package/dist/host/unit-readiness.d.ts +7 -0
- package/dist/host/unit-readiness.js +5 -0
- package/dist/host/unit-registry.d.ts +9 -0
- package/dist/host/unit-registry.js +33 -0
- package/dist/host/version-range.d.ts +2 -0
- package/dist/host/version-range.js +73 -0
- package/dist/host.d.ts +1 -0
- package/dist/host.js +1 -0
- package/dist/i18n/en.js +1 -1
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/zh.d.ts +1 -1
- package/dist/i18n/zh.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/react.d.ts +2 -0
- package/dist/lib/react.js +2 -0
- package/package.json +1 -1
- package/src/api/dsh-client.ts +5 -1
- package/src/components/Activity.css +1 -0
- package/src/components/Card.css +4 -4
- package/src/components/Card.test.tsx +46 -0
- package/src/components/CardAnchor.module.css +3 -0
- package/src/components/CardAnchor.test.tsx +36 -0
- package/src/components/CardAnchor.tsx +20 -0
- package/src/components/CardHead.css +1 -5
- package/src/components/ChoiceGroup.css +11 -2
- package/src/components/Field.css +4 -0
- package/src/components/MockStrip.css +6 -0
- package/src/components/Note.css +4 -4
- package/src/components/SelectField.css +156 -0
- package/src/components/SelectField.test.tsx +238 -0
- package/src/components/SelectField.tsx +486 -0
- package/src/components/SettingsSection.css +2 -2
- package/src/components/TextField.css +5 -0
- package/src/components/icons.tsx +19 -0
- package/src/host/tool-approval.ts +2 -1
- package/src/host/tool-kit.ts +2 -2
- package/src/host.ts +1 -0
- package/src/i18n/en.ts +1 -1
- package/src/i18n/zh.ts +1 -1
- package/src/index.ts +2 -0
- package/src/lib/react.ts +3 -0
- package/dist/lib/lint-probe.d.ts +0 -1
- package/dist/lib/lint-probe.js +0 -6
|
@@ -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-
|
|
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
|
-
|
|
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 {
|
package/src/components/Field.css
CHANGED
|
@@ -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
|
}
|
package/src/components/Note.css
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
|
|
2
2
|
.wb-note {
|
|
3
3
|
margin: 0;
|
|
4
|
-
font-size: var(--dsw-font-
|
|
5
|
-
line-height: var(--wb-line-
|
|
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-
|
|
11
|
-
line-height: var(--wb-line-
|
|
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
|
+
});
|