@lobehub/lobehub 2.0.0-next.353 → 2.0.0-next.354
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/.agents/skills/add-provider-doc/SKILL.md +90 -0
- package/.agents/skills/add-setting-env/SKILL.md +106 -0
- package/.agents/skills/debug/SKILL.md +66 -0
- package/.agents/skills/desktop/SKILL.md +78 -0
- package/.agents/skills/desktop/references/feature-implementation.md +99 -0
- package/.agents/skills/desktop/references/local-tools.md +133 -0
- package/.agents/skills/desktop/references/menu-config.md +103 -0
- package/.agents/skills/desktop/references/window-management.md +143 -0
- package/.agents/skills/drizzle/SKILL.md +129 -0
- package/.agents/skills/drizzle/references/db-migrations.md +50 -0
- package/.agents/skills/hotkey/SKILL.md +90 -0
- package/{.cursor/rules/i18n.mdc → .agents/skills/i18n/SKILL.md} +14 -23
- package/.agents/skills/linear/SKILL.md +51 -0
- package/.agents/skills/microcopy/SKILL.md +83 -0
- package/.agents/skills/modal/SKILL.md +102 -0
- package/{.cursor/rules/project-structure.mdc → .agents/skills/project-overview/SKILL.md} +65 -37
- package/.agents/skills/react/SKILL.md +73 -0
- package/.agents/skills/react/references/layout-kit.md +100 -0
- package/.agents/skills/recent-data/SKILL.md +108 -0
- package/.agents/skills/testing/SKILL.md +89 -0
- package/.agents/skills/testing/references/agent-runtime-e2e.md +126 -0
- package/.agents/skills/testing/references/db-model-test.md +124 -0
- package/.agents/skills/testing/references/desktop-controller-test.md +124 -0
- package/.agents/skills/testing/references/electron-ipc-test.md +63 -0
- package/.agents/skills/testing/references/zustand-store-action-test.md +150 -0
- package/.agents/skills/typescript/SKILL.md +52 -0
- package/.agents/skills/zustand/SKILL.md +78 -0
- package/.agents/skills/zustand/references/action-patterns.md +125 -0
- package/.agents/skills/zustand/references/slice-organization.md +125 -0
- package/AGENTS.md +42 -55
- package/CHANGELOG.md +33 -0
- package/CLAUDE.md +57 -46
- package/GEMINI.md +47 -39
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/src/features/FileViewer/Renderer/PDF/index.tsx +2 -3
- package/src/features/ShareModal/SharePdf/PdfPreview.tsx +1 -2
- package/src/libs/pdfjs/index.tsx +25 -0
- package/src/store/test-coverage.md +5 -5
- package/.cursor/rules/add-provider-doc.mdc +0 -183
- package/.cursor/rules/add-setting-env.mdc +0 -175
- package/.cursor/rules/cursor-rules.mdc +0 -28
- package/.cursor/rules/db-migrations.mdc +0 -46
- package/.cursor/rules/debug-usage.mdc +0 -86
- package/.cursor/rules/desktop-controller-tests.mdc +0 -189
- package/.cursor/rules/desktop-feature-implementation.mdc +0 -155
- package/.cursor/rules/desktop-local-tools-implement.mdc +0 -81
- package/.cursor/rules/desktop-menu-configuration.mdc +0 -209
- package/.cursor/rules/desktop-window-management.mdc +0 -301
- package/.cursor/rules/drizzle-schema-style-guide.mdc +0 -218
- package/.cursor/rules/hotkey.mdc +0 -162
- package/.cursor/rules/linear.mdc +0 -53
- package/.cursor/rules/microcopy-cn.mdc +0 -158
- package/.cursor/rules/microcopy-en.mdc +0 -148
- package/.cursor/rules/modal-imperative.mdc +0 -162
- package/.cursor/rules/packages/react-layout-kit.mdc +0 -122
- package/.cursor/rules/project-introduce.mdc +0 -36
- package/.cursor/rules/react.mdc +0 -169
- package/.cursor/rules/recent-data-usage.mdc +0 -139
- package/.cursor/rules/rules-index.mdc +0 -44
- package/.cursor/rules/testing-guide/agent-runtime-e2e.mdc +0 -285
- package/.cursor/rules/testing-guide/db-model-test.mdc +0 -455
- package/.cursor/rules/testing-guide/electron-ipc-test.mdc +0 -80
- package/.cursor/rules/testing-guide/testing-guide.mdc +0 -534
- package/.cursor/rules/testing-guide/zustand-store-action-test.mdc +0 -574
- package/.cursor/rules/typescript.mdc +0 -55
- package/.cursor/rules/zustand-action-patterns.mdc +0 -328
- package/.cursor/rules/zustand-slice-organization.mdc +0 -308
- package/src/libs/pdfjs/pdf.worker.ts +0 -1
- package/src/libs/pdfjs/worker.ts +0 -12
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/AGENTS.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/SKILL.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/advanced-event-handler-refs.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/advanced-use-latest.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-api-routes.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-defer-await.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-dependencies.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-parallel.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-suspense-boundaries.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-barrel-imports.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-conditional.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-defer-third-party.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-dynamic-imports.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-preload.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-event-listeners.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-localstorage-schema.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-passive-event-listeners.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-swr-dedup.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-batch-dom-css.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-function-results.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-property-access.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-storage.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-combine-iterations.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-early-exit.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-hoist-regexp.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-index-maps.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-length-check-first.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-min-max-loop.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-set-map-lookups.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-tosorted-immutable.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-activity.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-animate-svg-wrapper.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-conditional-render.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-content-visibility.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-hoist-jsx.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-hydration-no-flicker.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-svg-precision.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-defer-reads.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-dependencies.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-derived-state.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-functional-setstate.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-lazy-state-init.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-memo.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-transitions.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-after-nonblocking.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-cache-lru.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-cache-react.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-parallel-fetching.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-serialization.md +0 -0
|
@@ -1,574 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Best practices for testing Zustand store actions
|
|
3
|
-
globs: src/store/**/*.test.ts
|
|
4
|
-
alwaysApply: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Zustand Store Action Testing Guide
|
|
8
|
-
|
|
9
|
-
This guide provides best practices for testing Zustand store actions, based on our proven testing patterns.
|
|
10
|
-
|
|
11
|
-
## Basic Test Structure
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
import { act, renderHook } from '@testing-library/react';
|
|
15
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
16
|
-
|
|
17
|
-
import { messageService } from '@/services/message';
|
|
18
|
-
|
|
19
|
-
import { useChatStore } from '../../store';
|
|
20
|
-
|
|
21
|
-
// Keep zustand mock as it's needed globally
|
|
22
|
-
vi.mock('zustand/traditional');
|
|
23
|
-
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
// Reset store state
|
|
26
|
-
vi.clearAllMocks();
|
|
27
|
-
useChatStore.setState(
|
|
28
|
-
{
|
|
29
|
-
activeId: 'test-session-id',
|
|
30
|
-
messagesMap: {},
|
|
31
|
-
loadingIds: [],
|
|
32
|
-
},
|
|
33
|
-
false,
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
// ✅ Setup only spies that MOST tests need
|
|
37
|
-
vi.spyOn(messageService, 'createMessage').mockResolvedValue('new-message-id');
|
|
38
|
-
// ❌ Don't setup spies that only few tests need - spy only when needed
|
|
39
|
-
|
|
40
|
-
// Setup common mock methods
|
|
41
|
-
act(() => {
|
|
42
|
-
useChatStore.setState({
|
|
43
|
-
refreshMessages: vi.fn(),
|
|
44
|
-
internal_coreProcessMessage: vi.fn(),
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
afterEach(() => {
|
|
50
|
-
vi.restoreAllMocks();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('action name', () => {
|
|
54
|
-
describe('validation', () => {
|
|
55
|
-
// Validation tests
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe('normal flow', () => {
|
|
59
|
-
// Happy path tests
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe('error handling', () => {
|
|
63
|
-
// Error case tests
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Testing Best Practices
|
|
69
|
-
|
|
70
|
-
### 1. Test Layering - Spy Direct Dependencies Only
|
|
71
|
-
|
|
72
|
-
✅ **Good**: Spy on the direct dependency
|
|
73
|
-
|
|
74
|
-
```typescript
|
|
75
|
-
// When testing internal_coreProcessMessage, spy its direct dependency
|
|
76
|
-
const fetchAIChatSpy = vi
|
|
77
|
-
.spyOn(result.current, 'internal_fetchAIChatMessage')
|
|
78
|
-
.mockResolvedValue({ isFunctionCall: false, content: 'AI response' });
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
❌ **Bad**: Spy on lower-level implementation details
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
// Don't spy on services that internal_fetchAIChatMessage uses
|
|
85
|
-
const streamSpy = vi
|
|
86
|
-
.spyOn(chatService, 'createAssistantMessageStream')
|
|
87
|
-
.mockImplementation(...);
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**Why**: Each test should only mock its direct dependencies, not the entire call chain. This makes tests more maintainable and less brittle.
|
|
91
|
-
|
|
92
|
-
### 2. Mock Management - Minimize Global Spies
|
|
93
|
-
|
|
94
|
-
✅ **Good**: Spy only when needed
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
beforeEach(() => {
|
|
98
|
-
// ✅ Only spy services that most tests need
|
|
99
|
-
vi.spyOn(messageService, 'createMessage').mockResolvedValue('new-message-id');
|
|
100
|
-
// ✅ Don't spy chatService globally
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should process message', async () => {
|
|
104
|
-
// ✅ Spy chatService only in tests that need it
|
|
105
|
-
const streamSpy = vi.spyOn(chatService, 'createAssistantMessageStream')
|
|
106
|
-
.mockImplementation(...);
|
|
107
|
-
|
|
108
|
-
// test logic
|
|
109
|
-
|
|
110
|
-
streamSpy.mockRestore();
|
|
111
|
-
});
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
❌ **Bad**: Setup all spies globally
|
|
115
|
-
|
|
116
|
-
```typescript
|
|
117
|
-
beforeEach(() => {
|
|
118
|
-
vi.spyOn(messageService, 'createMessage').mockResolvedValue('id');
|
|
119
|
-
vi.spyOn(chatService, 'createAssistantMessageStream').mockResolvedValue({}); // ❌ Not all tests need this
|
|
120
|
-
vi.spyOn(fileService, 'uploadFile').mockResolvedValue({}); // ❌ Creates implicit coupling
|
|
121
|
-
});
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### 3. Service Mocking - Mock the Correct Layer
|
|
125
|
-
|
|
126
|
-
✅ **Good**: Mock the service method
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
it('should fetch AI chat response', async () => {
|
|
130
|
-
const streamSpy = vi
|
|
131
|
-
.spyOn(chatService, 'createAssistantMessageStream')
|
|
132
|
-
.mockImplementation(async ({ onMessageHandle, onFinish }) => {
|
|
133
|
-
await onMessageHandle?.({ type: 'text', text: 'Hello' } as any);
|
|
134
|
-
await onFinish?.('Hello', {});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// test logic
|
|
138
|
-
});
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
❌ **Bad**: Mock global fetch
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
it('should fetch AI chat response', async () => {
|
|
145
|
-
global.fetch = vi.fn().mockResolvedValue(...); // ❌ Too low level
|
|
146
|
-
});
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### 4. Test Organization - Use Descriptive Nesting
|
|
150
|
-
|
|
151
|
-
✅ **Good**: Clear nested structure
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
describe('sendMessage', () => {
|
|
155
|
-
describe('validation', () => {
|
|
156
|
-
it('should not send when session is inactive', async () => {});
|
|
157
|
-
it('should not send when message is empty', async () => {});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe('message creation', () => {
|
|
161
|
-
it('should create user message and trigger AI processing', async () => {});
|
|
162
|
-
it('should send message with files attached', async () => {});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe('error handling', () => {
|
|
166
|
-
it('should handle message creation errors gracefully', async () => {});
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
❌ **Bad**: Flat structure
|
|
172
|
-
|
|
173
|
-
```typescript
|
|
174
|
-
describe('sendMessage', () => {
|
|
175
|
-
it('test 1', async () => {});
|
|
176
|
-
it('test 2', async () => {});
|
|
177
|
-
it('test 3', async () => {});
|
|
178
|
-
});
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### 5. Testing Async Actions
|
|
182
|
-
|
|
183
|
-
Always wrap async operations in `act()`:
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
it('should send message', async () => {
|
|
187
|
-
const { result } = renderHook(() => useChatStore());
|
|
188
|
-
|
|
189
|
-
await act(async () => {
|
|
190
|
-
await result.current.sendMessage({ message: 'Hello' });
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
expect(messageService.createMessage).toHaveBeenCalled();
|
|
194
|
-
});
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
### 6. State Setup - Use act() for setState
|
|
198
|
-
|
|
199
|
-
```typescript
|
|
200
|
-
it('should handle disabled state', async () => {
|
|
201
|
-
act(() => {
|
|
202
|
-
useChatStore.setState({ activeId: undefined });
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const { result } = renderHook(() => useChatStore());
|
|
206
|
-
// test logic
|
|
207
|
-
});
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### 7. Testing Complex Flows
|
|
211
|
-
|
|
212
|
-
For complex flows with multiple steps, use clear spy setup:
|
|
213
|
-
|
|
214
|
-
```typescript
|
|
215
|
-
it('should handle topic creation flow', async () => {
|
|
216
|
-
// Setup store state
|
|
217
|
-
act(() => {
|
|
218
|
-
useChatStore.setState({
|
|
219
|
-
activeTopicId: undefined,
|
|
220
|
-
messagesMap: {
|
|
221
|
-
'test-session-id': [
|
|
222
|
-
{ id: 'msg-1', role: 'user', content: 'Message 1' },
|
|
223
|
-
{ id: 'msg-2', role: 'assistant', content: 'Response 1' },
|
|
224
|
-
{ id: 'msg-3', role: 'user', content: 'Message 2' },
|
|
225
|
-
],
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
const { result } = renderHook(() => useChatStore());
|
|
231
|
-
|
|
232
|
-
// Spy on action dependencies
|
|
233
|
-
const createTopicSpy = vi.spyOn(result.current, 'createTopic').mockResolvedValue('new-topic-id');
|
|
234
|
-
const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleMessageLoading');
|
|
235
|
-
|
|
236
|
-
// Execute
|
|
237
|
-
await act(async () => {
|
|
238
|
-
await result.current.sendMessage({ message: 'Test message' });
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// Assert
|
|
242
|
-
expect(createTopicSpy).toHaveBeenCalled();
|
|
243
|
-
expect(toggleLoadingSpy).toHaveBeenCalledWith(true, expect.any(String));
|
|
244
|
-
});
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
### 8. Streaming Response Mocking
|
|
248
|
-
|
|
249
|
-
When testing streaming responses, simulate the flow properly:
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
it('should handle streaming chunks', async () => {
|
|
253
|
-
const { result } = renderHook(() => useChatStore());
|
|
254
|
-
const messages = [{ id: 'msg-1', role: 'user', content: 'Hello', sessionId: 'test-session' }];
|
|
255
|
-
|
|
256
|
-
const streamSpy = vi
|
|
257
|
-
.spyOn(chatService, 'createAssistantMessageStream')
|
|
258
|
-
.mockImplementation(async ({ onMessageHandle, onFinish }) => {
|
|
259
|
-
// Simulate streaming chunks
|
|
260
|
-
await onMessageHandle?.({ type: 'text', text: 'Hello' } as any);
|
|
261
|
-
await onMessageHandle?.({ type: 'text', text: ' World' } as any);
|
|
262
|
-
await onFinish?.('Hello World', {});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
await act(async () => {
|
|
266
|
-
await result.current.internal_fetchAIChatMessage({
|
|
267
|
-
messages,
|
|
268
|
-
messageId: 'test-message-id',
|
|
269
|
-
model: 'gpt-4o-mini',
|
|
270
|
-
provider: 'openai',
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
expect(result.current.internal_dispatchMessage).toHaveBeenCalled();
|
|
275
|
-
|
|
276
|
-
streamSpy.mockRestore();
|
|
277
|
-
});
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
### 9. Error Handling Tests
|
|
281
|
-
|
|
282
|
-
Always test error scenarios:
|
|
283
|
-
|
|
284
|
-
```typescript
|
|
285
|
-
it('should handle errors gracefully', async () => {
|
|
286
|
-
const { result } = renderHook(() => useChatStore());
|
|
287
|
-
|
|
288
|
-
vi.spyOn(messageService, 'createMessage').mockRejectedValue(new Error('create message error'));
|
|
289
|
-
|
|
290
|
-
await act(async () => {
|
|
291
|
-
try {
|
|
292
|
-
await result.current.sendMessage({ message: 'Test message' });
|
|
293
|
-
} catch {
|
|
294
|
-
// Expected to throw
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
|
299
|
-
});
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
### 10. Cleanup After Tests
|
|
303
|
-
|
|
304
|
-
Always restore mocks after each test:
|
|
305
|
-
|
|
306
|
-
```typescript
|
|
307
|
-
afterEach(() => {
|
|
308
|
-
vi.restoreAllMocks();
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
// For individual test cleanup:
|
|
312
|
-
it('should test something', async () => {
|
|
313
|
-
const spy = vi.spyOn(service, 'method').mockImplementation(...);
|
|
314
|
-
|
|
315
|
-
// test logic
|
|
316
|
-
|
|
317
|
-
spy.mockRestore(); // Optional: cleanup immediately after test
|
|
318
|
-
});
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
## Common Patterns
|
|
322
|
-
|
|
323
|
-
### Testing Store Methods That Call Other Store Methods
|
|
324
|
-
|
|
325
|
-
```typescript
|
|
326
|
-
it('should call internal methods', async () => {
|
|
327
|
-
const { result } = renderHook(() => useChatStore());
|
|
328
|
-
|
|
329
|
-
const internalMethodSpy = vi.spyOn(result.current, 'internal_method').mockResolvedValue();
|
|
330
|
-
|
|
331
|
-
await act(async () => {
|
|
332
|
-
await result.current.publicMethod();
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
expect(internalMethodSpy).toHaveBeenCalledWith(
|
|
336
|
-
expect.any(String),
|
|
337
|
-
expect.objectContaining({ key: 'value' }),
|
|
338
|
-
);
|
|
339
|
-
});
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
### Testing Conditional Logic
|
|
343
|
-
|
|
344
|
-
```typescript
|
|
345
|
-
describe('conditional behavior', () => {
|
|
346
|
-
it('should execute when condition is true', async () => {
|
|
347
|
-
const { result } = renderHook(() => useChatStore());
|
|
348
|
-
vi.spyOn(result.current, 'internal_shouldUseRAG').mockReturnValue(true);
|
|
349
|
-
|
|
350
|
-
await act(async () => {
|
|
351
|
-
await result.current.sendMessage({ message: 'test' });
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
expect(result.current.internal_retrieveChunks).toHaveBeenCalled();
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it('should not execute when condition is false', async () => {
|
|
358
|
-
const { result } = renderHook(() => useChatStore());
|
|
359
|
-
vi.spyOn(result.current, 'internal_shouldUseRAG').mockReturnValue(false);
|
|
360
|
-
|
|
361
|
-
await act(async () => {
|
|
362
|
-
await result.current.sendMessage({ message: 'test' });
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
expect(result.current.internal_retrieveChunks).not.toHaveBeenCalled();
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
### Testing AbortController
|
|
371
|
-
|
|
372
|
-
```typescript
|
|
373
|
-
it('should abort generation and clear loading state', () => {
|
|
374
|
-
const abortController = new AbortController();
|
|
375
|
-
|
|
376
|
-
act(() => {
|
|
377
|
-
useChatStore.setState({ chatLoadingIdsAbortController: abortController });
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
const { result } = renderHook(() => useChatStore());
|
|
381
|
-
const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleChatLoading');
|
|
382
|
-
|
|
383
|
-
act(() => {
|
|
384
|
-
result.current.stopGenerateMessage();
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
expect(abortController.signal.aborted).toBe(true);
|
|
388
|
-
expect(toggleLoadingSpy).toHaveBeenCalledWith(false, undefined, expect.any(String));
|
|
389
|
-
});
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
## Anti-Patterns to Avoid
|
|
393
|
-
|
|
394
|
-
❌ **Don't**: Mock the entire store
|
|
395
|
-
|
|
396
|
-
```typescript
|
|
397
|
-
vi.mock('../../store', () => ({
|
|
398
|
-
useChatStore: vi.fn(() => ({
|
|
399
|
-
sendMessage: vi.fn(),
|
|
400
|
-
})),
|
|
401
|
-
}));
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
❌ **Don't**: Test implementation details
|
|
405
|
-
|
|
406
|
-
```typescript
|
|
407
|
-
// Bad: testing internal state structure
|
|
408
|
-
expect(result.current.messagesMap).toHaveProperty('test-session');
|
|
409
|
-
|
|
410
|
-
// Good: testing behavior
|
|
411
|
-
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
❌ **Don't**: Create tight coupling between tests
|
|
415
|
-
|
|
416
|
-
```typescript
|
|
417
|
-
// Bad: Tests depend on order
|
|
418
|
-
let messageId: string;
|
|
419
|
-
|
|
420
|
-
it('test 1', () => {
|
|
421
|
-
messageId = 'some-id'; // Side effect
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it('test 2', () => {
|
|
425
|
-
expect(messageId).toBeDefined(); // Depends on test 1
|
|
426
|
-
});
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
❌ **Don't**: Over-mock services
|
|
430
|
-
|
|
431
|
-
```typescript
|
|
432
|
-
// Bad: Mocking everything
|
|
433
|
-
beforeEach(() => {
|
|
434
|
-
vi.mock('@/services/chat');
|
|
435
|
-
vi.mock('@/services/message');
|
|
436
|
-
vi.mock('@/services/file');
|
|
437
|
-
vi.mock('@/services/agent');
|
|
438
|
-
// ... too many global mocks
|
|
439
|
-
});
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
## Testing SWR Hooks in Zustand Stores
|
|
443
|
-
|
|
444
|
-
Some Zustand store slices use SWR hooks for data fetching. These require a different testing approach.
|
|
445
|
-
|
|
446
|
-
### Basic SWR Hook Test Structure
|
|
447
|
-
|
|
448
|
-
```typescript
|
|
449
|
-
import { renderHook, waitFor } from '@testing-library/react';
|
|
450
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
451
|
-
|
|
452
|
-
import { discoverService } from '@/services/discover';
|
|
453
|
-
import { globalHelpers } from '@/store/global/helpers';
|
|
454
|
-
|
|
455
|
-
import { useDiscoverStore as useStore } from '../../store';
|
|
456
|
-
|
|
457
|
-
vi.mock('zustand/traditional');
|
|
458
|
-
|
|
459
|
-
beforeEach(() => {
|
|
460
|
-
vi.clearAllMocks();
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
describe('SWR Hook Actions', () => {
|
|
464
|
-
it('should fetch data and return correct response', async () => {
|
|
465
|
-
const mockData = [{ id: '1', name: 'Item 1' }];
|
|
466
|
-
|
|
467
|
-
// Mock the service call (the fetcher)
|
|
468
|
-
vi.spyOn(discoverService, 'getPluginCategories').mockResolvedValue(mockData as any);
|
|
469
|
-
vi.spyOn(globalHelpers, 'getCurrentLanguage').mockReturnValue('en-US');
|
|
470
|
-
|
|
471
|
-
const params = {} as any;
|
|
472
|
-
const { result } = renderHook(() => useStore.getState().usePluginCategories(params));
|
|
473
|
-
|
|
474
|
-
// Use waitFor to wait for async data loading
|
|
475
|
-
await waitFor(() => {
|
|
476
|
-
expect(result.current.data).toEqual(mockData);
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
expect(discoverService.getPluginCategories).toHaveBeenCalledWith(params);
|
|
480
|
-
});
|
|
481
|
-
});
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
**Key points**:
|
|
485
|
-
|
|
486
|
-
- **DO NOT mock useSWR** - let it use the real implementation
|
|
487
|
-
- Only mock the **service methods** (fetchers)
|
|
488
|
-
- Use `waitFor` from `@testing-library/react` to wait for async operations
|
|
489
|
-
- Check `result.current.data` directly after waitFor completes
|
|
490
|
-
|
|
491
|
-
### Testing SWR Key Generation
|
|
492
|
-
|
|
493
|
-
```typescript
|
|
494
|
-
it('should generate correct SWR key with locale and params', () => {
|
|
495
|
-
vi.spyOn(globalHelpers, 'getCurrentLanguage').mockReturnValue('zh-CN');
|
|
496
|
-
|
|
497
|
-
const useSWRMock = vi.mocked(useSWR);
|
|
498
|
-
let capturedKey: string | null = null;
|
|
499
|
-
useSWRMock.mockImplementation(((key: string) => {
|
|
500
|
-
capturedKey = key;
|
|
501
|
-
return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
|
|
502
|
-
}) as any);
|
|
503
|
-
|
|
504
|
-
const params = { page: 2, category: 'tools' } as any;
|
|
505
|
-
renderHook(() => useStore.getState().usePluginList(params));
|
|
506
|
-
|
|
507
|
-
expect(capturedKey).toBe('plugin-list-zh-CN-2-tools');
|
|
508
|
-
});
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
### Testing SWR Configuration
|
|
512
|
-
|
|
513
|
-
```typescript
|
|
514
|
-
it('should have correct SWR configuration', () => {
|
|
515
|
-
const useSWRMock = vi.mocked(useSWR);
|
|
516
|
-
let capturedOptions: any = null;
|
|
517
|
-
useSWRMock.mockImplementation(((key: string, fetcher: any, options: any) => {
|
|
518
|
-
capturedOptions = options;
|
|
519
|
-
return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
|
|
520
|
-
}) as any);
|
|
521
|
-
|
|
522
|
-
renderHook(() => useStore.getState().usePluginIdentifiers());
|
|
523
|
-
|
|
524
|
-
expect(capturedOptions).toMatchObject({ revalidateOnFocus: false });
|
|
525
|
-
});
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
### Testing Conditional Fetching
|
|
529
|
-
|
|
530
|
-
```typescript
|
|
531
|
-
it('should not fetch when required parameter is missing', () => {
|
|
532
|
-
const useSWRMock = vi.mocked(useSWR);
|
|
533
|
-
let capturedKey: string | null = null;
|
|
534
|
-
useSWRMock.mockImplementation(((key: string | null) => {
|
|
535
|
-
capturedKey = key;
|
|
536
|
-
return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
|
|
537
|
-
}) as any);
|
|
538
|
-
|
|
539
|
-
// When identifier is undefined, SWR key should be null
|
|
540
|
-
renderHook(() => useStore.getState().usePluginDetail({ identifier: undefined }));
|
|
541
|
-
|
|
542
|
-
expect(capturedKey).toBeNull();
|
|
543
|
-
});
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
### Key Differences from Regular Action Tests
|
|
547
|
-
|
|
548
|
-
1. **Mock useSWR globally**: Use `vi.mock('swr')` at the top level
|
|
549
|
-
2. **Mock the fetcher, not the result**:
|
|
550
|
-
- ✅ **Correct**: `const data = fetcher?.()` - call fetcher and return its Promise
|
|
551
|
-
- ❌ **Wrong**: `return { data: mockData }` - hardcode the result
|
|
552
|
-
3. **Await Promise results**: The `data` field is a Promise, use `await result.current.data`
|
|
553
|
-
4. **No act() wrapper needed**: SWR hooks don't trigger React state updates in these tests
|
|
554
|
-
5. **Test SWR key generation**: Verify keys include locale and parameters
|
|
555
|
-
6. **Test configuration**: Verify revalidation and other SWR options
|
|
556
|
-
7. **Type assertions**: Use `as any` for test mock data where type definitions are strict
|
|
557
|
-
|
|
558
|
-
**Why this matters**:
|
|
559
|
-
|
|
560
|
-
- The fetcher (service method) is what we're testing - it must be called
|
|
561
|
-
- Hardcoding the return value bypasses the actual fetcher logic
|
|
562
|
-
- SWR returns Promises in real usage, tests should mirror this behavior
|
|
563
|
-
|
|
564
|
-
## Benefits of This Approach
|
|
565
|
-
|
|
566
|
-
✅ **Clear test layers** - Each test only spies on direct dependencies ✅ **Correct mocks** - Mocks match actual implementation ✅ **Better maintainability** - Changes to implementation require fewer test updates ✅ **Improved coverage** - Structured approach ensures all branches are tested ✅ **Reduced coupling** - Tests are independent and can run in any order
|
|
567
|
-
|
|
568
|
-
## Reference
|
|
569
|
-
|
|
570
|
-
See example implementation in:
|
|
571
|
-
|
|
572
|
-
- `src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts` (Regular actions)
|
|
573
|
-
- `src/store/discover/slices/plugin/action.test.ts` (SWR hooks)
|
|
574
|
-
- `src/store/discover/slices/mcp/action.test.ts` (SWR hooks)
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: TypeScript code style and optimization guidelines
|
|
3
|
-
globs: *.ts,*.tsx,*.mts
|
|
4
|
-
alwaysApply: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# TypeScript Code Style Guide
|
|
8
|
-
|
|
9
|
-
## Types and Type Safety
|
|
10
|
-
|
|
11
|
-
- avoid explicit type annotations when TypeScript can infer types.
|
|
12
|
-
- avoid implicitly `any` variables; explicitly type when necessary (e.g., `let a: number` instead of `let a`).
|
|
13
|
-
- use the most accurate type possible (e.g., prefer `Record<PropertyKey, unknown>` over `object` and `any`).
|
|
14
|
-
- prefer `interface` over `type` for object shapes (e.g., React component props). Keep `type` for unions, intersections, and utility types.
|
|
15
|
-
- prefer `as const satisfies XyzInterface` over plain `as const` when suitable.
|
|
16
|
-
- prefer `@ts-expect-error` over `@ts-ignore` over `as any`
|
|
17
|
-
- Avoid meaningless null/undefined parameters; design strict function contracts.
|
|
18
|
-
|
|
19
|
-
## Asynchronous Patterns and Concurrency
|
|
20
|
-
|
|
21
|
-
- Prefer `async`/`await` over callbacks or chained `.then` promises.
|
|
22
|
-
- Prefer async APIs over sync ones (avoid `*Sync`).
|
|
23
|
-
- Prefer promise-based variants (e.g., `import { readFile } from 'fs/promises'`) over callback-based APIs from `fs`.
|
|
24
|
-
- Where safe, convert sequential async flows to concurrent ones with `Promise.all`, `Promise.race`, etc.
|
|
25
|
-
|
|
26
|
-
## Code Structure and Readability
|
|
27
|
-
|
|
28
|
-
- Prefer object destructuring when accessing and using properties.
|
|
29
|
-
- Use consistent, descriptive naming; avoid obscure abbreviations.
|
|
30
|
-
- Use semantically meaningful variable, function, and class names.
|
|
31
|
-
- Replace magic numbers or strings with well-named constants.
|
|
32
|
-
- Defer formatting to tooling; ignore purely formatting-only issues and autofixable lint problems.
|
|
33
|
-
|
|
34
|
-
## UI and Theming
|
|
35
|
-
|
|
36
|
-
- Use components from `@lobehub/ui`, Ant Design, or existing design system components instead of raw HTML tags (e.g., `Button` vs. `button`).
|
|
37
|
-
- Design for dark mode and mobile responsiveness:
|
|
38
|
-
- Use the `antd-style` token system instead of hard-coded colors.
|
|
39
|
-
- Select appropriate component variants.
|
|
40
|
-
|
|
41
|
-
## Performance
|
|
42
|
-
|
|
43
|
-
- Prefer `for…of` loops to index-based `for` loops when feasible.
|
|
44
|
-
- Reuse existing utils inside `packages/utils` or installed npm packages rather than reinventing the wheel.
|
|
45
|
-
- Query only the required columns from a database rather than selecting entire rows.
|
|
46
|
-
|
|
47
|
-
## Time and Consistency
|
|
48
|
-
|
|
49
|
-
- Instead of calling `Date.now()` multiple times, assign it to a constant once and reuse it to ensure consistency and improve readability.
|
|
50
|
-
|
|
51
|
-
## Logging
|
|
52
|
-
|
|
53
|
-
- Never log user private information like api key, etc
|
|
54
|
-
- Don't use `import { log } from 'debug'` to log messages, because it will directly log the message to the console.
|
|
55
|
-
- Use console.error instead of debug package to log error message in catch block.
|