sparkdesign 0.3.1 → 0.3.3
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/README.md +7 -5
- package/cli/dist/index.js +0 -0
- package/cli/dist/utils/tokens.js +103 -17
- package/cli/registry/basic/button.test.tsx +333 -0
- package/cli/registry/chat/{question-part.tsx → ask-user-part.tsx} +4 -4
- package/cli/registry/chat/{browser-use-part.tsx → browser-action-part.tsx} +6 -6
- package/cli/registry/chat/{suggestion-part.tsx → hint-banner.tsx} +4 -4
- package/cli/registry/chat/markdown.test.tsx +387 -0
- package/cli/registry/chat/{reasoning-step.tsx → reasoning-step/compound.tsx} +163 -185
- package/cli/registry/chat/reasoning-step/context.tsx +114 -0
- package/cli/registry/chat/reasoning-step/index.tsx +45 -0
- package/cli/registry/chat/reasoning-step/types.ts +109 -0
- package/cli/registry/chat/response/compound.tsx +210 -0
- package/cli/registry/chat/{response.tsx → response/context.tsx} +65 -136
- package/cli/registry/chat/response/index.tsx +87 -0
- package/cli/registry/chat/response/types.ts +123 -0
- package/cli/registry/chat/thinking-indicator.test.tsx +244 -0
- package/cli/registry/chat/tool-invocation-card.test.tsx +346 -0
- package/cli/registry/chat/{request.tsx → user-message.tsx} +3 -3
- package/cli/registry/chat/user-question/compound.tsx +324 -0
- package/cli/registry/chat/user-question/context.tsx +456 -0
- package/cli/registry/chat/user-question/index.tsx +71 -316
- package/cli/registry/chat/user-question/useUserQuestionKeyboard.ts +5 -6
- package/cli/registry/tokens/index.css +31 -0
- package/cli/registry/tokens/scale/computed.css +103 -0
- package/cli/registry/tokens/scale/config.css +110 -0
- package/cli/registry/tokens/scale/index.css +30 -0
- package/cli/registry/tokens/scale/presets/compact.css +30 -0
- package/cli/registry/tokens/scale/presets/dense.css +64 -0
- package/cli/registry/tokens/scale/presets/sharp.css +40 -0
- package/cli/registry/tokens/scale/presets/soft.css +16 -0
- package/cli/registry/tokens/scale.css +12 -298
- package/cli/registry/tokens/scrollbar-utility.css +35 -0
- package/cli/registry/tokens/themes/dark-parchment.css +132 -0
- package/cli/registry/tokens/themes/dark-qoder.css +132 -0
- package/cli/registry/tokens/themes/light-parchment.css +123 -0
- package/cli/registry/tokens/themes/light-qoder.css +131 -0
- package/dist/qoder-design.css +1 -1
- package/dist/registry/chat/ask-user-part.d.ts +24 -0
- package/dist/registry/chat/browser-action-part.d.ts +28 -0
- package/dist/registry/chat/{suggestion-part.d.ts → hint-banner.d.ts} +4 -4
- package/dist/registry/chat/reasoning-step/compound.d.ts +17 -0
- package/dist/registry/chat/reasoning-step/context.d.ts +10 -0
- package/dist/registry/chat/reasoning-step/index.d.ts +14 -0
- package/dist/registry/chat/reasoning-step/types.d.ts +95 -0
- package/dist/registry/chat/response/compound.d.ts +25 -0
- package/dist/registry/chat/response/context.d.ts +9 -0
- package/dist/registry/chat/response/index.d.ts +15 -0
- package/dist/registry/chat/response/types.d.ts +99 -0
- package/dist/registry/chat/user-message.d.ts +6 -0
- package/dist/registry/chat/user-question/compound.d.ts +37 -0
- package/dist/registry/chat/user-question/context.d.ts +55 -0
- package/dist/registry/chat/user-question/index.d.ts +13 -5
- package/dist/registry/chat/user-question/useUserQuestionKeyboard.d.ts +2 -3
- package/dist/scale.css +9 -303
- package/dist/spark-design.cjs.js +62 -62
- package/dist/spark-design.es.js +3992 -3826
- package/dist/src/components/chat/AskUserPart/index.d.ts +6 -0
- package/dist/src/components/chat/BrowserActionPart/index.d.ts +7 -0
- package/dist/src/components/chat/HintBanner/index.d.ts +6 -0
- package/dist/src/components/chat/ReasoningStep/index.d.ts +11 -5
- package/dist/src/components/chat/Response/index.d.ts +16 -6
- package/dist/src/components/chat/UserMessage/index.d.ts +7 -0
- package/dist/src/components/chat/UserQuestion/index.d.ts +18 -4
- package/dist/src/components/index.d.ts +63 -63
- package/dist/theme.css +13 -800
- package/package.json +27 -3
- package/dist/registry/chat/browser-use-part.d.ts +0 -28
- package/dist/registry/chat/question-part.d.ts +0 -24
- package/dist/registry/chat/reasoning-step.d.ts +0 -35
- package/dist/registry/chat/request.d.ts +0 -6
- package/dist/registry/chat/response.d.ts +0 -28
- package/dist/src/components/chat/BrowserUsePart/index.d.ts +0 -7
- package/dist/src/components/chat/QuestionPart/index.d.ts +0 -6
- package/dist/src/components/chat/Request/index.d.ts +0 -7
- package/dist/src/components/chat/SuggestionPart/index.d.ts +0 -6
- /package/dist/src/components/{foundation → basic}/AlertDialog/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Avatar/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Button/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Collapse/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Collapsible/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/CollapsibleSection/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/DropdownMenu/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/EllipsisText/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/IconButton/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Kbd/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/OptionList/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Pagination/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Progress/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/RadioGroup/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Resizable/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Scrollbar/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Select/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Skeleton/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Slider/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Spinner/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Switch/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Table/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Tabs/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Tag/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Toast/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Toggle/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Tooltip/index.d.ts +0 -0
- /package/dist/src/components/{foundation → basic}/Typography/index.d.ts +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { ThinkingIndicator } from './thinking-indicator'
|
|
4
|
+
|
|
5
|
+
describe('ThinkingIndicator', () => {
|
|
6
|
+
// ============================================================
|
|
7
|
+
// 渲染测试
|
|
8
|
+
// ============================================================
|
|
9
|
+
describe('渲染', () => {
|
|
10
|
+
it('应正确渲染组件', () => {
|
|
11
|
+
const { container } = render(<ThinkingIndicator />)
|
|
12
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('应设置正确的 displayName', () => {
|
|
16
|
+
expect(ThinkingIndicator.displayName).toBe('ThinkingIndicator')
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// ============================================================
|
|
21
|
+
// 文案测试
|
|
22
|
+
// ============================================================
|
|
23
|
+
describe('text 文案', () => {
|
|
24
|
+
it('应显示默认文案 "Qoder is Thinking..."', () => {
|
|
25
|
+
render(<ThinkingIndicator />)
|
|
26
|
+
expect(screen.getByText('Qoder is Thinking...')).toBeInTheDocument()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('应显示自定义文案', () => {
|
|
30
|
+
render(<ThinkingIndicator text="正在思考..." />)
|
|
31
|
+
expect(screen.getByText('正在思考...')).toBeInTheDocument()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('空文案应正常渲染', () => {
|
|
35
|
+
const { container } = render(<ThinkingIndicator text="" />)
|
|
36
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('长文案应正常渲染', () => {
|
|
40
|
+
const longText = 'AI is analyzing your complex multi-step request with deep reasoning...'
|
|
41
|
+
render(<ThinkingIndicator text={longText} />)
|
|
42
|
+
expect(screen.getByText(longText)).toBeInTheDocument()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// ============================================================
|
|
47
|
+
// Loader Slot 测试
|
|
48
|
+
// ============================================================
|
|
49
|
+
describe('loader slot', () => {
|
|
50
|
+
it('无 loader prop 时应渲染默认三点动画', () => {
|
|
51
|
+
const { container } = render(<ThinkingIndicator />)
|
|
52
|
+
expect(container.querySelector('.thinking-dots')).toBeInTheDocument()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('默认动画应包含三个点', () => {
|
|
56
|
+
const { container } = render(<ThinkingIndicator />)
|
|
57
|
+
const dots = container.querySelectorAll('.thinking-dots span')
|
|
58
|
+
expect(dots).toHaveLength(3)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('有 loader prop 时应渲染自定义 loader', () => {
|
|
62
|
+
render(
|
|
63
|
+
<ThinkingIndicator
|
|
64
|
+
loader={<div data-testid="custom-loader">Custom Loader</div>}
|
|
65
|
+
/>
|
|
66
|
+
)
|
|
67
|
+
expect(screen.getByTestId('custom-loader')).toBeInTheDocument()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('自定义 loader 时不应渲染默认动画', () => {
|
|
71
|
+
const { container } = render(
|
|
72
|
+
<ThinkingIndicator
|
|
73
|
+
loader={<div data-testid="custom-loader">Custom</div>}
|
|
74
|
+
/>
|
|
75
|
+
)
|
|
76
|
+
expect(container.querySelector('.thinking-dots')).not.toBeInTheDocument()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('loader 可以是复杂 React 节点', () => {
|
|
80
|
+
render(
|
|
81
|
+
<ThinkingIndicator
|
|
82
|
+
loader={
|
|
83
|
+
<div data-testid="complex-loader">
|
|
84
|
+
<span>Loading</span>
|
|
85
|
+
<span>...</span>
|
|
86
|
+
</div>
|
|
87
|
+
}
|
|
88
|
+
/>
|
|
89
|
+
)
|
|
90
|
+
expect(screen.getByTestId('complex-loader')).toBeInTheDocument()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// ============================================================
|
|
95
|
+
// Shimmer 效果测试
|
|
96
|
+
// ============================================================
|
|
97
|
+
describe('shimmer 效果', () => {
|
|
98
|
+
it('默认应启用 shimmer 效果', () => {
|
|
99
|
+
render(<ThinkingIndicator text="Test" />)
|
|
100
|
+
// shimmer 使用 motion.span,在测试环境被 mock 为普通 span
|
|
101
|
+
expect(screen.getByText('Test')).toBeInTheDocument()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('shimmer=true 时文本应有渐变样式类', () => {
|
|
105
|
+
render(<ThinkingIndicator shimmer text="Shimmer Text" />)
|
|
106
|
+
const textElement = screen.getByText('Shimmer Text')
|
|
107
|
+
expect(textElement).toHaveClass('bg-clip-text')
|
|
108
|
+
expect(textElement).toHaveClass('text-transparent')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('shimmer=false 时应显示静态文本', () => {
|
|
112
|
+
render(<ThinkingIndicator shimmer={false} text="Static Text" />)
|
|
113
|
+
const textElement = screen.getByText('Static Text')
|
|
114
|
+
expect(textElement).toHaveClass('text-text-secondary')
|
|
115
|
+
expect(textElement).not.toHaveClass('text-transparent')
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// ============================================================
|
|
120
|
+
// Shimmer 配置测试
|
|
121
|
+
// ============================================================
|
|
122
|
+
describe('shimmer 配置', () => {
|
|
123
|
+
it('应接受自定义 shimmerDuration', () => {
|
|
124
|
+
// shimmerDuration 用于动画,测试环境动画被 mock,仅验证 prop 接受
|
|
125
|
+
const { container } = render(
|
|
126
|
+
<ThinkingIndicator shimmerDuration={5} text="Custom Duration" />
|
|
127
|
+
)
|
|
128
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('应接受自定义 shimmerSpread', () => {
|
|
132
|
+
const { container } = render(
|
|
133
|
+
<ThinkingIndicator shimmerSpread={4} text="Custom Spread" />
|
|
134
|
+
)
|
|
135
|
+
expect(container.firstChild).toBeInTheDocument()
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// ============================================================
|
|
140
|
+
// textClassName 测试
|
|
141
|
+
// ============================================================
|
|
142
|
+
describe('textClassName', () => {
|
|
143
|
+
it('应将 textClassName 应用到文本元素', () => {
|
|
144
|
+
render(
|
|
145
|
+
<ThinkingIndicator text="Styled Text" textClassName="custom-text-class" />
|
|
146
|
+
)
|
|
147
|
+
const textElement = screen.getByText('Styled Text')
|
|
148
|
+
expect(textElement).toHaveClass('custom-text-class')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('textClassName 应与默认类名共存', () => {
|
|
152
|
+
render(
|
|
153
|
+
<ThinkingIndicator
|
|
154
|
+
shimmer={false}
|
|
155
|
+
text="Mixed Classes"
|
|
156
|
+
textClassName="my-class"
|
|
157
|
+
/>
|
|
158
|
+
)
|
|
159
|
+
const textElement = screen.getByText('Mixed Classes')
|
|
160
|
+
expect(textElement).toHaveClass('my-class')
|
|
161
|
+
expect(textElement).toHaveClass('text-sm')
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// ============================================================
|
|
166
|
+
// className 覆盖测试
|
|
167
|
+
// ============================================================
|
|
168
|
+
describe('className', () => {
|
|
169
|
+
it('应合并自定义 className 到根元素', () => {
|
|
170
|
+
const { container } = render(
|
|
171
|
+
<ThinkingIndicator className="custom-root-class" />
|
|
172
|
+
)
|
|
173
|
+
expect(container.firstChild).toHaveClass('custom-root-class')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('自定义 className 应与默认布局类共存', () => {
|
|
177
|
+
const { container } = render(
|
|
178
|
+
<ThinkingIndicator className="my-indicator" />
|
|
179
|
+
)
|
|
180
|
+
expect(container.firstChild).toHaveClass('my-indicator')
|
|
181
|
+
expect(container.firstChild).toHaveClass('inline-flex')
|
|
182
|
+
expect(container.firstChild).toHaveClass('items-center')
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// ============================================================
|
|
187
|
+
// 原生属性透传测试
|
|
188
|
+
// ============================================================
|
|
189
|
+
describe('原生属性透传', () => {
|
|
190
|
+
it('应透传 data-* 属性', () => {
|
|
191
|
+
render(<ThinkingIndicator data-testid="thinking" />)
|
|
192
|
+
expect(screen.getByTestId('thinking')).toBeInTheDocument()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('应透传 id 属性', () => {
|
|
196
|
+
const { container } = render(<ThinkingIndicator id="my-indicator" />)
|
|
197
|
+
expect(container.querySelector('#my-indicator')).toBeInTheDocument()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('应透传 aria-* 属性', () => {
|
|
201
|
+
const { container } = render(
|
|
202
|
+
<ThinkingIndicator aria-label="AI is thinking" />
|
|
203
|
+
)
|
|
204
|
+
expect(container.firstChild).toHaveAttribute('aria-label', 'AI is thinking')
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// ============================================================
|
|
209
|
+
// 组合场景测试
|
|
210
|
+
// ============================================================
|
|
211
|
+
describe('组合场景', () => {
|
|
212
|
+
it('应支持所有 props 组合', () => {
|
|
213
|
+
render(
|
|
214
|
+
<ThinkingIndicator
|
|
215
|
+
text="完整配置测试"
|
|
216
|
+
shimmer={true}
|
|
217
|
+
shimmerDuration={3}
|
|
218
|
+
shimmerSpread={2}
|
|
219
|
+
textClassName="text-class"
|
|
220
|
+
className="root-class"
|
|
221
|
+
data-testid="full-config"
|
|
222
|
+
loader={<span data-testid="custom-spinner">⟳</span>}
|
|
223
|
+
/>
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
const root = screen.getByTestId('full-config')
|
|
227
|
+
expect(root).toHaveClass('root-class')
|
|
228
|
+
expect(screen.getByText('完整配置测试')).toHaveClass('text-class')
|
|
229
|
+
expect(screen.getByTestId('custom-spinner')).toBeInTheDocument()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('shimmer=false 时 loader 仍可自定义', () => {
|
|
233
|
+
render(
|
|
234
|
+
<ThinkingIndicator
|
|
235
|
+
shimmer={false}
|
|
236
|
+
text="No Shimmer"
|
|
237
|
+
loader={<span data-testid="static-loader">●</span>}
|
|
238
|
+
/>
|
|
239
|
+
)
|
|
240
|
+
expect(screen.getByTestId('static-loader')).toBeInTheDocument()
|
|
241
|
+
expect(screen.getByText('No Shimmer')).toHaveClass('text-text-secondary')
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
})
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { ToolInvocationCard } from './tool-invocation-card'
|
|
4
|
+
|
|
5
|
+
describe('ToolInvocationCard', () => {
|
|
6
|
+
// ============================================================
|
|
7
|
+
// 基础渲染测试
|
|
8
|
+
// ============================================================
|
|
9
|
+
describe('基础渲染', () => {
|
|
10
|
+
it('应正确渲染工具名称', () => {
|
|
11
|
+
render(<ToolInvocationCard toolName="read_file" status="success" />)
|
|
12
|
+
expect(screen.getByText('read_file')).toBeInTheDocument()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('应设置正确的 displayName', () => {
|
|
16
|
+
expect(ToolInvocationCard.displayName).toBe('ToolInvocationCard')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('应渲染参数文本', () => {
|
|
20
|
+
render(
|
|
21
|
+
<ToolInvocationCard
|
|
22
|
+
toolName="search"
|
|
23
|
+
params="query: hello world"
|
|
24
|
+
status="success"
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
expect(screen.getByText('query: hello world')).toBeInTheDocument()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('无参数时不应渲染参数区域', () => {
|
|
31
|
+
const { container } = render(
|
|
32
|
+
<ToolInvocationCard toolName="list_dir" status="success" />
|
|
33
|
+
)
|
|
34
|
+
// 只有工具名和状态文本
|
|
35
|
+
expect(container.textContent).toContain('list_dir')
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// ============================================================
|
|
40
|
+
// 状态图标测试
|
|
41
|
+
// ============================================================
|
|
42
|
+
describe('状态图标', () => {
|
|
43
|
+
it('running 状态应显示时间图标', () => {
|
|
44
|
+
const { container } = render(
|
|
45
|
+
<ToolInvocationCard toolName="test" status="running" />
|
|
46
|
+
)
|
|
47
|
+
// 默认 timeIcon 使用 TimeLine 组件
|
|
48
|
+
expect(container.querySelector('svg')).toBeInTheDocument()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('success 状态应显示勾选图标', () => {
|
|
52
|
+
const { container } = render(
|
|
53
|
+
<ToolInvocationCard toolName="test" status="success" />
|
|
54
|
+
)
|
|
55
|
+
expect(container.querySelector('svg')).toBeInTheDocument()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('error 状态应显示关闭图标', () => {
|
|
59
|
+
const { container } = render(
|
|
60
|
+
<ToolInvocationCard toolName="test" status="error" />
|
|
61
|
+
)
|
|
62
|
+
expect(container.querySelector('svg')).toBeInTheDocument()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('应接受自定义图标', () => {
|
|
66
|
+
render(
|
|
67
|
+
<ToolInvocationCard
|
|
68
|
+
toolName="test"
|
|
69
|
+
status="success"
|
|
70
|
+
checkboxCircleIcon={<span data-testid="custom-check">✓</span>}
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
expect(screen.getByTestId('custom-check')).toBeInTheDocument()
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// ============================================================
|
|
78
|
+
// 状态文本测试
|
|
79
|
+
// ============================================================
|
|
80
|
+
describe('状态文本', () => {
|
|
81
|
+
it('running 状态应显示默认文本 "Running"', () => {
|
|
82
|
+
render(<ToolInvocationCard toolName="test" status="running" />)
|
|
83
|
+
expect(screen.getByText('Running')).toBeInTheDocument()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('success 状态应显示默认文本 "Ran"', () => {
|
|
87
|
+
render(<ToolInvocationCard toolName="test" status="success" />)
|
|
88
|
+
expect(screen.getByText('Ran')).toBeInTheDocument()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('error 状态应显示默认文本 "Failed"', () => {
|
|
92
|
+
render(<ToolInvocationCard toolName="test" status="error" />)
|
|
93
|
+
expect(screen.getByText('Failed')).toBeInTheDocument()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('应接受自定义 statusText', () => {
|
|
97
|
+
render(
|
|
98
|
+
<ToolInvocationCard
|
|
99
|
+
toolName="test"
|
|
100
|
+
status="success"
|
|
101
|
+
statusText="执行完成"
|
|
102
|
+
/>
|
|
103
|
+
)
|
|
104
|
+
expect(screen.getByText('执行完成')).toBeInTheDocument()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('statusText 为空字符串时不应显示状态文本', () => {
|
|
108
|
+
render(
|
|
109
|
+
<ToolInvocationCard
|
|
110
|
+
toolName="test"
|
|
111
|
+
status="success"
|
|
112
|
+
statusText=""
|
|
113
|
+
/>
|
|
114
|
+
)
|
|
115
|
+
expect(screen.queryByText('Ran')).not.toBeInTheDocument()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('应接受自定义状态文本 props', () => {
|
|
119
|
+
render(
|
|
120
|
+
<ToolInvocationCard
|
|
121
|
+
toolName="test"
|
|
122
|
+
status="running"
|
|
123
|
+
runningText="执行中"
|
|
124
|
+
/>
|
|
125
|
+
)
|
|
126
|
+
expect(screen.getByText('执行中')).toBeInTheDocument()
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// ============================================================
|
|
131
|
+
// 响应展示测试
|
|
132
|
+
// ============================================================
|
|
133
|
+
describe('响应展示', () => {
|
|
134
|
+
it('有响应时应可展开', () => {
|
|
135
|
+
render(
|
|
136
|
+
<ToolInvocationCard
|
|
137
|
+
toolName="test"
|
|
138
|
+
status="success"
|
|
139
|
+
response="Tool output result"
|
|
140
|
+
/>
|
|
141
|
+
)
|
|
142
|
+
// 点击展开
|
|
143
|
+
fireEvent.click(screen.getByText('test'))
|
|
144
|
+
expect(screen.getByText('Tool output result')).toBeInTheDocument()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('defaultExpanded=true 时应默认展开', () => {
|
|
148
|
+
render(
|
|
149
|
+
<ToolInvocationCard
|
|
150
|
+
toolName="test"
|
|
151
|
+
status="success"
|
|
152
|
+
response="Already expanded"
|
|
153
|
+
defaultExpanded={true}
|
|
154
|
+
/>
|
|
155
|
+
)
|
|
156
|
+
expect(screen.getByText('Already expanded')).toBeInTheDocument()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('无响应时不应显示展开箭头', () => {
|
|
160
|
+
const { container } = render(
|
|
161
|
+
<ToolInvocationCard toolName="test" status="success" />
|
|
162
|
+
)
|
|
163
|
+
// 没有箭头图标(ArrowRightLine 的 SVG)
|
|
164
|
+
const arrows = container.querySelectorAll('[class*="rotate"]')
|
|
165
|
+
expect(arrows).toHaveLength(0)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('running 状态时不应可展开', () => {
|
|
169
|
+
render(
|
|
170
|
+
<ToolInvocationCard
|
|
171
|
+
toolName="test"
|
|
172
|
+
status="running"
|
|
173
|
+
response="Some response"
|
|
174
|
+
/>
|
|
175
|
+
)
|
|
176
|
+
// running 状态下点击不应展开
|
|
177
|
+
fireEvent.click(screen.getByText('test'))
|
|
178
|
+
expect(screen.queryByText('Some response')).not.toBeInTheDocument()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('点击应切换展开/收起状态', () => {
|
|
182
|
+
render(
|
|
183
|
+
<ToolInvocationCard
|
|
184
|
+
toolName="toggle-test"
|
|
185
|
+
status="success"
|
|
186
|
+
response="Toggle content"
|
|
187
|
+
/>
|
|
188
|
+
)
|
|
189
|
+
const trigger = screen.getByText('toggle-test')
|
|
190
|
+
|
|
191
|
+
// 第一次点击展开
|
|
192
|
+
fireEvent.click(trigger)
|
|
193
|
+
expect(screen.getByText('Toggle content')).toBeInTheDocument()
|
|
194
|
+
|
|
195
|
+
// 第二次点击收起
|
|
196
|
+
fireEvent.click(trigger)
|
|
197
|
+
expect(screen.queryByText('Toggle content')).not.toBeInTheDocument()
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// ============================================================
|
|
202
|
+
// 响应标签测试
|
|
203
|
+
// ============================================================
|
|
204
|
+
describe('响应标签', () => {
|
|
205
|
+
it('展开时应显示默认标签 "Response"', () => {
|
|
206
|
+
render(
|
|
207
|
+
<ToolInvocationCard
|
|
208
|
+
toolName="test"
|
|
209
|
+
status="success"
|
|
210
|
+
response="Content"
|
|
211
|
+
defaultExpanded
|
|
212
|
+
/>
|
|
213
|
+
)
|
|
214
|
+
expect(screen.getByText('Response')).toBeInTheDocument()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('应接受自定义 responseLabel', () => {
|
|
218
|
+
render(
|
|
219
|
+
<ToolInvocationCard
|
|
220
|
+
toolName="test"
|
|
221
|
+
status="success"
|
|
222
|
+
response="Content"
|
|
223
|
+
responseLabel="输出结果"
|
|
224
|
+
defaultExpanded
|
|
225
|
+
/>
|
|
226
|
+
)
|
|
227
|
+
expect(screen.getByText('输出结果')).toBeInTheDocument()
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// ============================================================
|
|
232
|
+
// 错误状态样式测试
|
|
233
|
+
// ============================================================
|
|
234
|
+
describe('错误状态', () => {
|
|
235
|
+
it('error 状态响应应应用错误样式', () => {
|
|
236
|
+
render(
|
|
237
|
+
<ToolInvocationCard
|
|
238
|
+
toolName="test"
|
|
239
|
+
status="error"
|
|
240
|
+
response="Error message"
|
|
241
|
+
defaultExpanded
|
|
242
|
+
/>
|
|
243
|
+
)
|
|
244
|
+
const errorText = screen.getByText('Error message')
|
|
245
|
+
expect(errorText).toHaveClass('text-error')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('success 状态响应不应有错误样式', () => {
|
|
249
|
+
render(
|
|
250
|
+
<ToolInvocationCard
|
|
251
|
+
toolName="test"
|
|
252
|
+
status="success"
|
|
253
|
+
response="Success message"
|
|
254
|
+
defaultExpanded
|
|
255
|
+
/>
|
|
256
|
+
)
|
|
257
|
+
const successText = screen.getByText('Success message')
|
|
258
|
+
expect(successText).not.toHaveClass('text-error')
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// ============================================================
|
|
263
|
+
// className 测试
|
|
264
|
+
// ============================================================
|
|
265
|
+
describe('className', () => {
|
|
266
|
+
it('应合并自定义 className', () => {
|
|
267
|
+
const { container } = render(
|
|
268
|
+
<ToolInvocationCard
|
|
269
|
+
toolName="test"
|
|
270
|
+
status="success"
|
|
271
|
+
className="custom-card"
|
|
272
|
+
/>
|
|
273
|
+
)
|
|
274
|
+
expect(container.firstChild).toHaveClass('custom-card')
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
// ============================================================
|
|
279
|
+
// 自定义图标 props 测试
|
|
280
|
+
// ============================================================
|
|
281
|
+
describe('自定义图标 props', () => {
|
|
282
|
+
it('应接受自定义 timeIcon', () => {
|
|
283
|
+
render(
|
|
284
|
+
<ToolInvocationCard
|
|
285
|
+
toolName="test"
|
|
286
|
+
status="running"
|
|
287
|
+
timeIcon={<span data-testid="time-icon">⏱</span>}
|
|
288
|
+
/>
|
|
289
|
+
)
|
|
290
|
+
expect(screen.getByTestId('time-icon')).toBeInTheDocument()
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('应接受自定义 closeCircleIcon', () => {
|
|
294
|
+
render(
|
|
295
|
+
<ToolInvocationCard
|
|
296
|
+
toolName="test"
|
|
297
|
+
status="error"
|
|
298
|
+
closeCircleIcon={<span data-testid="close-icon">✗</span>}
|
|
299
|
+
/>
|
|
300
|
+
)
|
|
301
|
+
expect(screen.getByTestId('close-icon')).toBeInTheDocument()
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('应接受自定义 arrowRightIcon', () => {
|
|
305
|
+
render(
|
|
306
|
+
<ToolInvocationCard
|
|
307
|
+
toolName="test"
|
|
308
|
+
status="success"
|
|
309
|
+
response="Content"
|
|
310
|
+
arrowRightIcon={<span data-testid="arrow-icon">→</span>}
|
|
311
|
+
/>
|
|
312
|
+
)
|
|
313
|
+
expect(screen.getByTestId('arrow-icon')).toBeInTheDocument()
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// ============================================================
|
|
318
|
+
// 组合场景测试
|
|
319
|
+
// ============================================================
|
|
320
|
+
describe('组合场景', () => {
|
|
321
|
+
it('应支持所有 props 组合', () => {
|
|
322
|
+
render(
|
|
323
|
+
<ToolInvocationCard
|
|
324
|
+
toolName="full_test"
|
|
325
|
+
params="/path/to/file.ts"
|
|
326
|
+
status="success"
|
|
327
|
+
response="File content here..."
|
|
328
|
+
defaultExpanded
|
|
329
|
+
statusText="已完成"
|
|
330
|
+
responseLabel="文件内容"
|
|
331
|
+
className="test-class"
|
|
332
|
+
checkboxCircleIcon={<span data-testid="check">✓</span>}
|
|
333
|
+
arrowRightIcon={<span data-testid="arrow">→</span>}
|
|
334
|
+
/>
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
expect(screen.getByText('full_test')).toBeInTheDocument()
|
|
338
|
+
expect(screen.getByText('/path/to/file.ts')).toBeInTheDocument()
|
|
339
|
+
expect(screen.getByText('已完成')).toBeInTheDocument()
|
|
340
|
+
expect(screen.getByText('文件内容')).toBeInTheDocument()
|
|
341
|
+
expect(screen.getByText('File content here...')).toBeInTheDocument()
|
|
342
|
+
expect(screen.getByTestId('check')).toBeInTheDocument()
|
|
343
|
+
expect(screen.getByTestId('arrow')).toBeInTheDocument()
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
})
|
|
@@ -2,12 +2,12 @@ import { forwardRef } from 'react'
|
|
|
2
2
|
import type { HTMLAttributes, ReactNode } from 'react'
|
|
3
3
|
import { cn } from '@/lib/utils'
|
|
4
4
|
|
|
5
|
-
export interface
|
|
5
|
+
export interface UserMessageProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
6
6
|
children: ReactNode
|
|
7
7
|
maxWidth?: string | number
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export const
|
|
10
|
+
export const UserMessage = forwardRef<HTMLDivElement, UserMessageProps>(
|
|
11
11
|
({ children, className, maxWidth = '752px', ...props }, ref) => {
|
|
12
12
|
const containerStyles = cn(
|
|
13
13
|
'w-full',
|
|
@@ -35,4 +35,4 @@ export const Request = forwardRef<HTMLDivElement, RequestProps>(
|
|
|
35
35
|
}
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
UserMessage.displayName = 'UserMessage'
|