sparkdesign 0.0.1 → 0.1.10

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 (118) hide show
  1. package/README.md +188 -4
  2. package/dist/commands/add.js +93 -0
  3. package/dist/commands/diff.js +54 -0
  4. package/dist/commands/init.js +96 -0
  5. package/dist/commands/list.js +25 -0
  6. package/dist/index.js +37 -0
  7. package/dist/utils/config.js +53 -0
  8. package/dist/utils/registry.js +34 -0
  9. package/dist/utils/tokens.js +176 -0
  10. package/dist/utils/transform.js +19 -0
  11. package/package.json +33 -10
  12. package/registry/__tests__/basic/button.test.tsx +333 -0
  13. package/registry/__tests__/chat/markdown.test.tsx +387 -0
  14. package/registry/__tests__/chat/thinking-indicator.test.tsx +244 -0
  15. package/registry/__tests__/chat/tool-invocation-card.test.tsx +346 -0
  16. package/registry/basic/alert-dialog.tsx +180 -0
  17. package/registry/basic/avatar.tsx +120 -0
  18. package/registry/basic/button.tsx +100 -0
  19. package/registry/basic/collapse.tsx +94 -0
  20. package/registry/basic/collapsible-card.tsx +230 -0
  21. package/registry/basic/collapsible.tsx +21 -0
  22. package/registry/basic/dropdown-menu.tsx +254 -0
  23. package/registry/basic/icon-button.tsx +66 -0
  24. package/registry/basic/icons-inline.tsx +206 -0
  25. package/registry/basic/kbd.tsx +50 -0
  26. package/registry/basic/option-list.tsx +125 -0
  27. package/registry/basic/pagination.tsx +132 -0
  28. package/registry/basic/progress.tsx +42 -0
  29. package/registry/basic/radio-group.tsx +69 -0
  30. package/registry/basic/resizable.tsx +67 -0
  31. package/registry/basic/scrollbar.tsx +114 -0
  32. package/registry/basic/select.tsx +177 -0
  33. package/registry/basic/shimmering-text.tsx +115 -0
  34. package/registry/basic/sidebar-menu.tsx +177 -0
  35. package/registry/basic/skeleton.tsx +33 -0
  36. package/registry/basic/slider.tsx +55 -0
  37. package/registry/basic/sonner.tsx +104 -0
  38. package/registry/basic/spinner.tsx +17 -0
  39. package/registry/basic/switch.tsx +49 -0
  40. package/registry/basic/table.tsx +117 -0
  41. package/registry/basic/tabs.tsx +85 -0
  42. package/registry/basic/tag.tsx +161 -0
  43. package/registry/basic/theme-from-document.ts +10 -0
  44. package/registry/basic/toggle.tsx +223 -0
  45. package/registry/basic/tooltip.tsx +80 -0
  46. package/registry/basic/typography.tsx +201 -0
  47. package/registry/chat/ask-user-part.tsx +70 -0
  48. package/registry/chat/browser-action-part.tsx +166 -0
  49. package/registry/chat/chat-input/chat-input-folder-selector.tsx +185 -0
  50. package/registry/chat/chat-input/chat-input-model-switcher.tsx +131 -0
  51. package/registry/chat/chat-input/chat-input-textarea.tsx +67 -0
  52. package/registry/chat/chat-input/compound.tsx +334 -0
  53. package/registry/chat/chat-input/context.tsx +189 -0
  54. package/registry/chat/chat-input/folder-permission-dialog.tsx +61 -0
  55. package/registry/chat/chat-input/index.tsx +123 -0
  56. package/registry/chat/chat-input/types.ts +77 -0
  57. package/registry/chat/chat-input/useAutoResizeTextarea.ts +20 -0
  58. package/registry/chat/code-block-part.tsx +151 -0
  59. package/registry/chat/file-attachment.tsx +44 -0
  60. package/registry/chat/file-card.tsx +68 -0
  61. package/registry/chat/file-review-part.tsx +259 -0
  62. package/registry/chat/folder-button.tsx +169 -0
  63. package/registry/chat/generated-images-grid.tsx +56 -0
  64. package/registry/chat/generation-status-bar.tsx +72 -0
  65. package/registry/chat/hint-banner.tsx +165 -0
  66. package/registry/chat/image-attachment.tsx +166 -0
  67. package/registry/chat/image-generating.tsx +281 -0
  68. package/registry/chat/markdown.tsx +146 -0
  69. package/registry/chat/mermaid-part.tsx +90 -0
  70. package/registry/chat/permission-card.tsx +178 -0
  71. package/registry/chat/plan-part.tsx +168 -0
  72. package/registry/chat/queue-indicator.tsx +234 -0
  73. package/registry/chat/reasoning-step/compound.tsx +336 -0
  74. package/registry/chat/reasoning-step/context.tsx +114 -0
  75. package/registry/chat/reasoning-step/index.tsx +45 -0
  76. package/registry/chat/reasoning-step/types.ts +109 -0
  77. package/registry/chat/related-prompts.tsx +91 -0
  78. package/registry/chat/response/compound.tsx +210 -0
  79. package/registry/chat/response/context.tsx +200 -0
  80. package/registry/chat/response/index.tsx +87 -0
  81. package/registry/chat/response/types.ts +123 -0
  82. package/registry/chat/send-button.tsx +94 -0
  83. package/registry/chat/streaming-markdown-block.tsx +111 -0
  84. package/registry/chat/task-part.tsx +109 -0
  85. package/registry/chat/terminal-code-block-part.tsx +69 -0
  86. package/registry/chat/thinking-indicator.tsx +91 -0
  87. package/registry/chat/tool-invocation-card.tsx +132 -0
  88. package/registry/chat/user-message.tsx +38 -0
  89. package/registry/chat/user-question/UserQuestionCard.tsx +198 -0
  90. package/registry/chat/user-question/UserQuestionFooter.tsx +66 -0
  91. package/registry/chat/user-question/UserQuestionHeader.tsx +64 -0
  92. package/registry/chat/user-question/compound.tsx +324 -0
  93. package/registry/chat/user-question/context.tsx +456 -0
  94. package/registry/chat/user-question/index.tsx +95 -0
  95. package/registry/chat/user-question/types.ts +61 -0
  96. package/registry/chat/user-question/useUserQuestionKeyboard.ts +126 -0
  97. package/registry/chat/user-question/useUserQuestionState.ts +165 -0
  98. package/registry/chat/user-question-answer.tsx +62 -0
  99. package/registry/lib/file-icon-maps.ts +150 -0
  100. package/registry/lib/use-mermaid-render.ts +76 -0
  101. package/registry/lib/utils.ts +6 -0
  102. package/registry/meta.json +1 -0
  103. package/registry/tokens/index.css +31 -0
  104. package/registry/tokens/scale/computed.css +103 -0
  105. package/registry/tokens/scale/config.css +110 -0
  106. package/registry/tokens/scale/index.css +30 -0
  107. package/registry/tokens/scale/presets/compact.css +30 -0
  108. package/registry/tokens/scale/presets/dense.css +64 -0
  109. package/registry/tokens/scale/presets/sharp.css +40 -0
  110. package/registry/tokens/scale/presets/soft.css +16 -0
  111. package/registry/tokens/scale.css +13 -0
  112. package/registry/tokens/scrollbar-utility.css +35 -0
  113. package/registry/tokens/theme.css +633 -0
  114. package/registry/tokens/themes/dark-parchment.css +132 -0
  115. package/registry/tokens/themes/dark-qoder.css +132 -0
  116. package/registry/tokens/themes/light-parchment.css +123 -0
  117. package/registry/tokens/themes/light-qoder.css +131 -0
  118. package/index.js +0 -5
@@ -0,0 +1,387 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { MarkdownBody } from '../../chat/markdown'
4
+
5
+ describe('MarkdownBody', () => {
6
+ // ============================================================
7
+ // 基础渲染测试
8
+ // ============================================================
9
+ describe('基础渲染', () => {
10
+ it('应正确渲染组件', () => {
11
+ const { container } = render(<MarkdownBody>Hello World</MarkdownBody>)
12
+ expect(container.firstChild).toBeInTheDocument()
13
+ })
14
+
15
+ it('应设置正确的 displayName', () => {
16
+ expect(MarkdownBody.displayName).toBe('MarkdownBody')
17
+ })
18
+
19
+ it('应渲染普通文本', () => {
20
+ render(<MarkdownBody>Simple text content</MarkdownBody>)
21
+ expect(screen.getByText('Simple text content')).toBeInTheDocument()
22
+ })
23
+ })
24
+
25
+ // ============================================================
26
+ // 段落渲染测试
27
+ // ============================================================
28
+ describe('段落 (p)', () => {
29
+ it('应渲染段落为 p 元素', () => {
30
+ const { container } = render(<MarkdownBody>A paragraph</MarkdownBody>)
31
+ expect(container.querySelector('p')).toBeInTheDocument()
32
+ })
33
+
34
+ it('段落应有正确的样式类', () => {
35
+ const { container } = render(<MarkdownBody>Styled paragraph</MarkdownBody>)
36
+ const p = container.querySelector('p')
37
+ expect(p).toHaveClass('text-sm')
38
+ expect(p).toHaveClass('text-text')
39
+ })
40
+ })
41
+
42
+ // ============================================================
43
+ // 标题渲染测试
44
+ // ============================================================
45
+ describe('标题 (h1-h6)', () => {
46
+ it('应渲染 h1 标题', () => {
47
+ const { container } = render(<MarkdownBody># Heading 1</MarkdownBody>)
48
+ const h1 = container.querySelector('h1')
49
+ expect(h1).toBeInTheDocument()
50
+ expect(h1).toHaveTextContent('Heading 1')
51
+ })
52
+
53
+ it('应渲染 h2 标题', () => {
54
+ const { container } = render(<MarkdownBody>## Heading 2</MarkdownBody>)
55
+ const h2 = container.querySelector('h2')
56
+ expect(h2).toBeInTheDocument()
57
+ expect(h2).toHaveTextContent('Heading 2')
58
+ })
59
+
60
+ it('应渲染 h3 标题', () => {
61
+ const { container } = render(<MarkdownBody>### Heading 3</MarkdownBody>)
62
+ const h3 = container.querySelector('h3')
63
+ expect(h3).toBeInTheDocument()
64
+ })
65
+
66
+ it('h1 应有正确的样式', () => {
67
+ const { container } = render(<MarkdownBody># Title</MarkdownBody>)
68
+ const h1 = container.querySelector('h1')
69
+ expect(h1).toHaveClass('text-4xl')
70
+ expect(h1).toHaveClass('font-medium')
71
+ })
72
+
73
+ it('h2 应有边框样式', () => {
74
+ const { container } = render(<MarkdownBody>## Section</MarkdownBody>)
75
+ const h2 = container.querySelector('h2')
76
+ expect(h2).toHaveClass('border-b')
77
+ })
78
+ })
79
+
80
+ // ============================================================
81
+ // 列表渲染测试
82
+ // ============================================================
83
+ describe('列表 (ul/ol)', () => {
84
+ it('应渲染无序列表', () => {
85
+ const { container } = render(
86
+ <MarkdownBody>{`- Item 1\n- Item 2\n- Item 3`}</MarkdownBody>
87
+ )
88
+ const ul = container.querySelector('ul')
89
+ expect(ul).toBeInTheDocument()
90
+ expect(ul).toHaveClass('list-disc')
91
+ })
92
+
93
+ it('应渲染有序列表', () => {
94
+ const { container } = render(
95
+ <MarkdownBody>{`1. First\n2. Second\n3. Third`}</MarkdownBody>
96
+ )
97
+ const ol = container.querySelector('ol')
98
+ expect(ol).toBeInTheDocument()
99
+ expect(ol).toHaveClass('list-decimal')
100
+ })
101
+
102
+ it('列表项应正确渲染', () => {
103
+ const { container } = render(
104
+ <MarkdownBody>{`- Apple\n- Banana`}</MarkdownBody>
105
+ )
106
+ const items = container.querySelectorAll('li')
107
+ expect(items).toHaveLength(2)
108
+ })
109
+ })
110
+
111
+ // ============================================================
112
+ // 代码渲染测试
113
+ // ============================================================
114
+ describe('代码 (code/pre)', () => {
115
+ it('应渲染行内代码', () => {
116
+ const { container } = render(
117
+ <MarkdownBody>Use `npm install` to install</MarkdownBody>
118
+ )
119
+ const code = container.querySelector('code')
120
+ expect(code).toBeInTheDocument()
121
+ expect(code).toHaveTextContent('npm install')
122
+ })
123
+
124
+ it('行内代码应有背景样式', () => {
125
+ const { container } = render(
126
+ <MarkdownBody>Run `test` command</MarkdownBody>
127
+ )
128
+ const code = container.querySelector('code')
129
+ expect(code).toHaveClass('bg-fill-tertiary')
130
+ expect(code).toHaveClass('rounded')
131
+ })
132
+
133
+ it('应渲染代码块', () => {
134
+ const { container } = render(
135
+ <MarkdownBody>{`\`\`\`javascript\nconst x = 1;\n\`\`\``}</MarkdownBody>
136
+ )
137
+ const pre = container.querySelector('pre')
138
+ expect(pre).toBeInTheDocument()
139
+ })
140
+
141
+ it('代码块应包含在滚动容器中', () => {
142
+ const { container } = render(
143
+ <MarkdownBody>{`\`\`\`js\ncode\n\`\`\``}</MarkdownBody>
144
+ )
145
+ // Scrollbar 组件包裹
146
+ const scrollContainer = container.querySelector('[class*="overflow"]')
147
+ expect(scrollContainer).toBeInTheDocument()
148
+ })
149
+ })
150
+
151
+ // ============================================================
152
+ // 引用渲染测试
153
+ // ============================================================
154
+ describe('引用 (blockquote)', () => {
155
+ it('应渲染引用块', () => {
156
+ const { container } = render(
157
+ <MarkdownBody>{'> This is a quote'}</MarkdownBody>
158
+ )
159
+ const blockquote = container.querySelector('blockquote')
160
+ expect(blockquote).toBeInTheDocument()
161
+ })
162
+
163
+ it('引用块应有边框样式', () => {
164
+ const { container } = render(
165
+ <MarkdownBody>{'> Quote text'}</MarkdownBody>
166
+ )
167
+ const blockquote = container.querySelector('blockquote')
168
+ expect(blockquote).toHaveClass('border-l-2')
169
+ expect(blockquote).toHaveClass('italic')
170
+ })
171
+ })
172
+
173
+ // ============================================================
174
+ // 链接渲染测试
175
+ // ============================================================
176
+ describe('链接 (a)', () => {
177
+ it('应渲染链接', () => {
178
+ render(<MarkdownBody>[Click me](https://example.com)</MarkdownBody>)
179
+ const link = screen.getByRole('link')
180
+ expect(link).toBeInTheDocument()
181
+ expect(link).toHaveTextContent('Click me')
182
+ })
183
+
184
+ it('链接应在新标签页打开', () => {
185
+ render(<MarkdownBody>[Link](https://example.com)</MarkdownBody>)
186
+ const link = screen.getByRole('link')
187
+ expect(link).toHaveAttribute('target', '_blank')
188
+ expect(link).toHaveAttribute('rel', 'noopener noreferrer')
189
+ })
190
+
191
+ it('链接应有正确的 href', () => {
192
+ render(<MarkdownBody>[Test](https://test.com)</MarkdownBody>)
193
+ const link = screen.getByRole('link')
194
+ expect(link).toHaveAttribute('href', 'https://test.com')
195
+ })
196
+ })
197
+
198
+ // ============================================================
199
+ // 强调渲染测试
200
+ // ============================================================
201
+ describe('强调 (strong/em)', () => {
202
+ it('应渲染粗体', () => {
203
+ const { container } = render(
204
+ <MarkdownBody>**Bold text**</MarkdownBody>
205
+ )
206
+ const strong = container.querySelector('strong')
207
+ expect(strong).toBeInTheDocument()
208
+ expect(strong).toHaveTextContent('Bold text')
209
+ })
210
+
211
+ it('应渲染斜体', () => {
212
+ const { container } = render(
213
+ <MarkdownBody>*Italic text*</MarkdownBody>
214
+ )
215
+ const em = container.querySelector('em')
216
+ expect(em).toBeInTheDocument()
217
+ expect(em).toHaveTextContent('Italic text')
218
+ })
219
+
220
+ it('粗体应有正确样式', () => {
221
+ const { container } = render(
222
+ <MarkdownBody>**Strong**</MarkdownBody>
223
+ )
224
+ const strong = container.querySelector('strong')
225
+ expect(strong).toHaveClass('font-semibold')
226
+ })
227
+
228
+ it('斜体应有正确样式', () => {
229
+ const { container } = render(
230
+ <MarkdownBody>*Emphasis*</MarkdownBody>
231
+ )
232
+ const em = container.querySelector('em')
233
+ expect(em).toHaveClass('italic')
234
+ })
235
+ })
236
+
237
+ // ============================================================
238
+ // 水平线渲染测试
239
+ // ============================================================
240
+ describe('水平线 (hr)', () => {
241
+ it('应渲染水平线', () => {
242
+ const { container } = render(
243
+ <MarkdownBody>{`Above\n\n---\n\nBelow`}</MarkdownBody>
244
+ )
245
+ const hr = container.querySelector('hr')
246
+ expect(hr).toBeInTheDocument()
247
+ })
248
+ })
249
+
250
+ // ============================================================
251
+ // 表格渲染测试 (GFM)
252
+ // ============================================================
253
+ describe('表格 (GFM)', () => {
254
+ it('应渲染表格', () => {
255
+ const tableMarkdown = `
256
+ | Name | Age |
257
+ |------|-----|
258
+ | Alice | 25 |
259
+ | Bob | 30 |
260
+ `
261
+ const { container } = render(<MarkdownBody>{tableMarkdown}</MarkdownBody>)
262
+ expect(container.querySelector('table')).toBeInTheDocument()
263
+ })
264
+
265
+ it('应渲染表头', () => {
266
+ const tableMarkdown = `
267
+ | Header1 | Header2 |
268
+ |---------|---------|
269
+ | Cell1 | Cell2 |
270
+ `
271
+ const { container } = render(<MarkdownBody>{tableMarkdown}</MarkdownBody>)
272
+ const thead = container.querySelector('thead')
273
+ expect(thead).toBeInTheDocument()
274
+ })
275
+
276
+ it('应渲染表体', () => {
277
+ const tableMarkdown = `
278
+ | A | B |
279
+ |---|---|
280
+ | 1 | 2 |
281
+ `
282
+ const { container } = render(<MarkdownBody>{tableMarkdown}</MarkdownBody>)
283
+ const tbody = container.querySelector('tbody')
284
+ expect(tbody).toBeInTheDocument()
285
+ })
286
+
287
+ it('表格应在滚动容器中', () => {
288
+ const tableMarkdown = `
289
+ | Col1 | Col2 |
290
+ |------|------|
291
+ | Data | Data |
292
+ `
293
+ const { container } = render(<MarkdownBody>{tableMarkdown}</MarkdownBody>)
294
+ // Scrollbar 组件包裹
295
+ const scrollContainer = container.querySelector('[class*="border"]')
296
+ expect(scrollContainer).toBeInTheDocument()
297
+ })
298
+ })
299
+
300
+ // ============================================================
301
+ // 任务列表测试 (GFM)
302
+ // ============================================================
303
+ describe('任务列表 (GFM)', () => {
304
+ it('应渲染任务列表', () => {
305
+ const taskList = `
306
+ - [ ] Unchecked
307
+ - [x] Checked
308
+ `
309
+ const { container } = render(<MarkdownBody>{taskList}</MarkdownBody>)
310
+ const checkboxes = container.querySelectorAll('input[type="checkbox"]')
311
+ expect(checkboxes.length).toBeGreaterThan(0)
312
+ })
313
+
314
+ it('任务列表应无圆点样式', () => {
315
+ const taskList = `
316
+ - [ ] Task 1
317
+ - [x] Task 2
318
+ `
319
+ const { container } = render(<MarkdownBody>{taskList}</MarkdownBody>)
320
+ const ul = container.querySelector('ul')
321
+ expect(ul).toHaveClass('list-none')
322
+ })
323
+ })
324
+
325
+ // ============================================================
326
+ // className 测试
327
+ // ============================================================
328
+ describe('className', () => {
329
+ it('应合并自定义 className', () => {
330
+ const { container } = render(
331
+ <MarkdownBody className="custom-markdown">Content</MarkdownBody>
332
+ )
333
+ expect(container.firstChild).toHaveClass('custom-markdown')
334
+ })
335
+
336
+ it('根元素应有 markdown-body 类', () => {
337
+ const { container } = render(
338
+ <MarkdownBody>Content</MarkdownBody>
339
+ )
340
+ expect(container.firstChild).toHaveClass('markdown-body')
341
+ })
342
+ })
343
+
344
+ // ============================================================
345
+ // 复杂内容测试
346
+ // ============================================================
347
+ describe('复杂内容', () => {
348
+ it('应正确渲染混合内容', () => {
349
+ const complexMarkdown = `
350
+ # Title
351
+
352
+ A paragraph with **bold** and *italic* text.
353
+
354
+ - List item 1
355
+ - List item 2
356
+
357
+ \`\`\`js
358
+ const x = 1;
359
+ \`\`\`
360
+
361
+ > A quote
362
+
363
+ [A link](https://example.com)
364
+ `
365
+ const { container } = render(<MarkdownBody>{complexMarkdown}</MarkdownBody>)
366
+
367
+ expect(container.querySelector('h1')).toBeInTheDocument()
368
+ expect(container.querySelector('p')).toBeInTheDocument()
369
+ expect(container.querySelector('strong')).toBeInTheDocument()
370
+ expect(container.querySelector('em')).toBeInTheDocument()
371
+ expect(container.querySelector('ul')).toBeInTheDocument()
372
+ expect(container.querySelector('pre')).toBeInTheDocument()
373
+ expect(container.querySelector('blockquote')).toBeInTheDocument()
374
+ expect(container.querySelector('a')).toBeInTheDocument()
375
+ })
376
+
377
+ it('应处理空字符串', () => {
378
+ const { container } = render(<MarkdownBody>{''}</MarkdownBody>)
379
+ expect(container.firstChild).toBeInTheDocument()
380
+ })
381
+
382
+ it('应处理纯空白字符串', () => {
383
+ const { container } = render(<MarkdownBody>{' '}</MarkdownBody>)
384
+ expect(container.firstChild).toBeInTheDocument()
385
+ })
386
+ })
387
+ })
@@ -0,0 +1,244 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { ThinkingIndicator } from '../../chat/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
+ })