@lobehub/chat 1.124.0 → 1.124.2

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 (145) hide show
  1. package/.env.example +5 -0
  2. package/.github/scripts/pr-comment.js +11 -2
  3. package/.github/workflows/desktop-pr-build.yml +86 -12
  4. package/.github/workflows/release-desktop-beta.yml +91 -20
  5. package/CHANGELOG.md +58 -0
  6. package/Dockerfile +2 -0
  7. package/Dockerfile.database +2 -0
  8. package/Dockerfile.pglite +2 -0
  9. package/apps/desktop/electron-builder.js +8 -4
  10. package/changelog/v1.json +21 -0
  11. package/docs/self-hosting/environment-variables/model-provider.mdx +18 -0
  12. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +20 -0
  13. package/locales/ar/chat.json +2 -0
  14. package/locales/bg-BG/chat.json +2 -0
  15. package/locales/de-DE/chat.json +2 -0
  16. package/locales/en-US/chat.json +2 -0
  17. package/locales/es-ES/chat.json +2 -0
  18. package/locales/fa-IR/chat.json +2 -0
  19. package/locales/fr-FR/chat.json +2 -0
  20. package/locales/it-IT/chat.json +2 -0
  21. package/locales/ja-JP/chat.json +2 -0
  22. package/locales/ko-KR/chat.json +2 -0
  23. package/locales/nl-NL/chat.json +2 -0
  24. package/locales/pl-PL/chat.json +2 -0
  25. package/locales/pt-BR/chat.json +2 -0
  26. package/locales/ru-RU/chat.json +2 -0
  27. package/locales/tr-TR/chat.json +2 -0
  28. package/locales/vi-VN/chat.json +2 -0
  29. package/locales/zh-CN/chat.json +2 -0
  30. package/locales/zh-CN/modelProvider.json +1 -1
  31. package/locales/zh-TW/chat.json +2 -0
  32. package/package.json +1 -1
  33. package/packages/const/src/hotkeys.ts +1 -1
  34. package/packages/const/src/index.ts +1 -0
  35. package/packages/const/src/settings/hotkey.ts +3 -2
  36. package/packages/const/src/trace.ts +1 -1
  37. package/packages/const/src/user.ts +1 -2
  38. package/packages/database/src/client/db.test.ts +19 -13
  39. package/packages/electron-server-ipc/src/ipcClient.test.ts +783 -1
  40. package/packages/file-loaders/src/loadFile.test.ts +61 -0
  41. package/packages/file-loaders/src/utils/isTextReadableFile.test.ts +43 -0
  42. package/packages/file-loaders/src/utils/parser-utils.test.ts +155 -0
  43. package/packages/model-bank/src/aiModels/aihubmix.ts +38 -4
  44. package/packages/model-bank/src/aiModels/groq.ts +26 -8
  45. package/packages/model-bank/src/aiModels/hunyuan.ts +3 -3
  46. package/packages/model-bank/src/aiModels/modelscope.ts +13 -2
  47. package/packages/model-bank/src/aiModels/moonshot.ts +25 -5
  48. package/packages/model-bank/src/aiModels/novita.ts +40 -9
  49. package/packages/model-bank/src/aiModels/openrouter.ts +0 -13
  50. package/packages/model-bank/src/aiModels/qwen.ts +62 -1
  51. package/packages/model-bank/src/aiModels/siliconcloud.ts +20 -0
  52. package/packages/model-bank/src/aiModels/volcengine.ts +141 -15
  53. package/packages/model-runtime/package.json +2 -1
  54. package/packages/model-runtime/src/ai21/index.test.ts +2 -2
  55. package/packages/model-runtime/src/ai360/index.test.ts +2 -2
  56. package/packages/model-runtime/src/akashchat/index.test.ts +19 -0
  57. package/packages/model-runtime/src/anthropic/index.test.ts +1 -2
  58. package/packages/model-runtime/src/baichuan/index.test.ts +1 -2
  59. package/packages/model-runtime/src/bedrock/index.test.ts +1 -2
  60. package/packages/model-runtime/src/bfl/createImage.test.ts +1 -2
  61. package/packages/model-runtime/src/bfl/index.test.ts +1 -2
  62. package/packages/model-runtime/src/cloudflare/index.test.ts +1 -2
  63. package/packages/model-runtime/src/cohere/index.test.ts +19 -0
  64. package/packages/model-runtime/src/deepseek/index.test.ts +2 -2
  65. package/packages/model-runtime/src/fireworksai/index.test.ts +2 -2
  66. package/packages/model-runtime/src/giteeai/index.test.ts +2 -2
  67. package/packages/model-runtime/src/github/index.test.ts +2 -2
  68. package/packages/model-runtime/src/google/createImage.test.ts +1 -2
  69. package/packages/model-runtime/src/google/index.test.ts +1 -1
  70. package/packages/model-runtime/src/groq/index.test.ts +2 -3
  71. package/packages/model-runtime/src/huggingface/index.test.ts +40 -0
  72. package/packages/model-runtime/src/hunyuan/index.test.ts +2 -3
  73. package/packages/model-runtime/src/internlm/index.test.ts +2 -2
  74. package/packages/model-runtime/src/jina/index.test.ts +19 -0
  75. package/packages/model-runtime/src/lmstudio/index.test.ts +2 -2
  76. package/packages/model-runtime/src/minimax/index.test.ts +19 -0
  77. package/packages/model-runtime/src/mistral/index.test.ts +2 -3
  78. package/packages/model-runtime/src/modelscope/index.test.ts +19 -0
  79. package/packages/model-runtime/src/moonshot/index.test.ts +1 -2
  80. package/packages/model-runtime/src/nebius/index.test.ts +19 -0
  81. package/packages/model-runtime/src/newapi/index.test.ts +49 -42
  82. package/packages/model-runtime/src/newapi/index.ts +124 -143
  83. package/packages/model-runtime/src/novita/index.test.ts +3 -4
  84. package/packages/model-runtime/src/nvidia/index.test.ts +19 -0
  85. package/packages/model-runtime/src/openrouter/index.test.ts +2 -3
  86. package/packages/model-runtime/src/perplexity/index.test.ts +2 -3
  87. package/packages/model-runtime/src/ppio/index.test.ts +3 -4
  88. package/packages/model-runtime/src/qwen/index.test.ts +2 -2
  89. package/packages/model-runtime/src/sambanova/index.test.ts +19 -0
  90. package/packages/model-runtime/src/search1api/index.test.ts +19 -0
  91. package/packages/model-runtime/src/sensenova/index.test.ts +2 -2
  92. package/packages/model-runtime/src/spark/index.test.ts +2 -2
  93. package/packages/model-runtime/src/stepfun/index.test.ts +2 -2
  94. package/packages/model-runtime/src/taichu/index.test.ts +4 -5
  95. package/packages/model-runtime/src/tencentcloud/index.test.ts +1 -1
  96. package/packages/model-runtime/src/togetherai/index.test.ts +1 -2
  97. package/packages/model-runtime/src/upstage/index.test.ts +1 -2
  98. package/packages/model-runtime/src/utils/openaiCompatibleFactory/index.test.ts +9 -7
  99. package/packages/model-runtime/src/utils/streams/anthropic.ts +2 -2
  100. package/packages/model-runtime/src/utils/streams/openai/openai.ts +20 -13
  101. package/packages/model-runtime/src/utils/streams/openai/responsesStream.test.ts +1 -2
  102. package/packages/model-runtime/src/utils/streams/openai/responsesStream.ts +2 -2
  103. package/packages/model-runtime/src/utils/streams/protocol.ts +2 -2
  104. package/packages/model-runtime/src/wenxin/index.test.ts +2 -3
  105. package/packages/model-runtime/src/xai/index.test.ts +2 -2
  106. package/packages/model-runtime/src/zeroone/index.test.ts +1 -2
  107. package/packages/model-runtime/src/zhipu/index.test.ts +2 -3
  108. package/packages/model-runtime/vitest.config.mts +0 -7
  109. package/packages/types/src/index.ts +2 -0
  110. package/packages/types/src/message/base.ts +1 -1
  111. package/packages/types/src/openai/chat.ts +2 -3
  112. package/packages/utils/package.json +2 -1
  113. package/packages/utils/src/_deprecated/parseModels.test.ts +1 -1
  114. package/packages/utils/src/_deprecated/parseModels.ts +1 -1
  115. package/packages/utils/src/client/topic.test.ts +1 -2
  116. package/packages/utils/src/client/topic.ts +1 -2
  117. package/packages/utils/src/electron/desktopRemoteRPCFetch.ts +1 -1
  118. package/packages/utils/src/fetch/fetchSSE.ts +7 -8
  119. package/packages/utils/src/fetch/parseError.ts +1 -3
  120. package/packages/utils/src/format.test.ts +1 -2
  121. package/packages/utils/src/index.ts +1 -0
  122. package/packages/utils/src/toolManifest.ts +1 -2
  123. package/packages/utils/src/trace.ts +1 -1
  124. package/packages/utils/vitest.config.mts +1 -1
  125. package/packages/web-crawler/src/__tests__/urlRules.test.ts +275 -0
  126. package/packages/web-crawler/src/crawImpl/__tests__/exa.test.ts +269 -0
  127. package/packages/web-crawler/src/crawImpl/__tests__/firecrawl.test.ts +284 -0
  128. package/packages/web-crawler/src/crawImpl/__tests__/naive.test.ts +234 -0
  129. package/packages/web-crawler/src/crawImpl/__tests__/tavily.test.ts +359 -0
  130. package/packages/web-crawler/src/utils/__tests__/errorType.test.ts +217 -0
  131. package/packages/web-crawler/vitest.config.mts +3 -0
  132. package/scripts/electronWorkflow/mergeMacReleaseFiles.ts +207 -0
  133. package/src/app/[variants]/(main)/settings/provider/(detail)/newapi/page.tsx +1 -1
  134. package/src/components/Thinking/index.tsx +2 -3
  135. package/src/config/llm.ts +8 -0
  136. package/src/features/ChatInput/Desktop/index.tsx +16 -4
  137. package/src/features/ChatInput/StoreUpdater.tsx +2 -0
  138. package/src/libs/traces/index.ts +1 -1
  139. package/src/locales/default/chat.ts +1 -0
  140. package/src/locales/default/modelProvider.ts +1 -1
  141. package/src/server/modules/ModelRuntime/trace.ts +1 -2
  142. package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +107 -0
  143. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +352 -7
  144. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +2 -1
  145. package/packages/model-runtime/src/openrouter/__snapshots__/index.test.ts.snap +0 -113
@@ -0,0 +1,359 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { NetworkConnectionError, PageNotFoundError, TimeoutError } from '../../utils/errorType';
4
+ import { tavily } from '../tavily';
5
+
6
+ // Mock dependencies
7
+ vi.mock('../../utils/withTimeout', () => ({
8
+ DEFAULT_TIMEOUT: 30000,
9
+ withTimeout: vi.fn(),
10
+ }));
11
+
12
+ describe('tavily crawler', () => {
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ delete process.env.TAVILY_API_KEY;
16
+ delete process.env.TAVILY_EXTRACT_DEPTH;
17
+ });
18
+
19
+ it('should successfully crawl content with API key', async () => {
20
+ process.env.TAVILY_API_KEY = 'test-api-key';
21
+
22
+ const mockResponse = {
23
+ ok: true,
24
+ json: vi.fn().mockResolvedValue({
25
+ base_url: 'https://api.tavily.com',
26
+ response_time: 1.5,
27
+ results: [
28
+ {
29
+ url: 'https://example.com',
30
+ raw_content:
31
+ 'This is a test raw content with sufficient length to pass validation. '.repeat(3),
32
+ images: ['https://example.com/image1.jpg', 'https://example.com/image2.jpg'],
33
+ },
34
+ ],
35
+ }),
36
+ };
37
+
38
+ const { withTimeout } = await import('../../utils/withTimeout');
39
+ vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
40
+
41
+ const result = await tavily('https://example.com', { filterOptions: {} });
42
+
43
+ expect(result).toEqual({
44
+ content: 'This is a test raw content with sufficient length to pass validation. '.repeat(3),
45
+ contentType: 'text',
46
+ length: 'This is a test raw content with sufficient length to pass validation. '.repeat(3)
47
+ .length,
48
+ siteName: 'example.com',
49
+ title: 'example.com',
50
+ url: 'https://example.com',
51
+ });
52
+
53
+ expect(withTimeout).toHaveBeenCalledWith(expect.any(Promise), 30000);
54
+ });
55
+
56
+ it('should use custom extract depth when provided', async () => {
57
+ process.env.TAVILY_API_KEY = 'test-api-key';
58
+ process.env.TAVILY_EXTRACT_DEPTH = 'advanced';
59
+
60
+ const mockResponse = {
61
+ ok: true,
62
+ json: vi.fn().mockResolvedValue({
63
+ base_url: 'https://api.tavily.com',
64
+ response_time: 2.1,
65
+ results: [
66
+ {
67
+ url: 'https://example.com',
68
+ raw_content: 'Advanced extraction content with more details. '.repeat(5),
69
+ },
70
+ ],
71
+ }),
72
+ };
73
+
74
+ const { withTimeout } = await import('../../utils/withTimeout');
75
+ vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
76
+
77
+ await tavily('https://example.com', { filterOptions: {} });
78
+
79
+ expect(withTimeout).toHaveBeenCalledWith(expect.any(Promise), 30000);
80
+ });
81
+
82
+ it('should handle missing API key', async () => {
83
+ const mockResponse = {
84
+ ok: true,
85
+ json: vi.fn().mockResolvedValue({
86
+ base_url: 'https://api.tavily.com',
87
+ response_time: 1.2,
88
+ results: [
89
+ {
90
+ url: 'https://example.com',
91
+ raw_content: 'Test content with sufficient length. '.repeat(5),
92
+ },
93
+ ],
94
+ }),
95
+ };
96
+
97
+ const { withTimeout } = await import('../../utils/withTimeout');
98
+ vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
99
+
100
+ await tavily('https://example.com', { filterOptions: {} });
101
+
102
+ expect(withTimeout).toHaveBeenCalledWith(expect.any(Promise), 30000);
103
+ });
104
+
105
+ it('should return undefined when no results are returned', async () => {
106
+ process.env.TAVILY_API_KEY = 'test-api-key';
107
+
108
+ const mockResponse = {
109
+ ok: true,
110
+ json: vi.fn().mockResolvedValue({
111
+ base_url: 'https://api.tavily.com',
112
+ response_time: 0.8,
113
+ results: [],
114
+ }),
115
+ };
116
+
117
+ const { withTimeout } = await import('../../utils/withTimeout');
118
+ vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
119
+
120
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
121
+
122
+ const result = await tavily('https://example.com', { filterOptions: {} });
123
+
124
+ expect(result).toBeUndefined();
125
+ expect(consoleSpy).toHaveBeenCalledWith(
126
+ 'Tavily API returned no results for URL:',
127
+ 'https://example.com',
128
+ );
129
+
130
+ consoleSpy.mockRestore();
131
+ });
132
+
133
+ it('should return undefined for short content', async () => {
134
+ process.env.TAVILY_API_KEY = 'test-api-key';
135
+
136
+ const mockResponse = {
137
+ ok: true,
138
+ json: vi.fn().mockResolvedValue({
139
+ base_url: 'https://api.tavily.com',
140
+ response_time: 1.1,
141
+ results: [
142
+ {
143
+ url: 'https://example.com',
144
+ raw_content: 'Short', // Content too short
145
+ },
146
+ ],
147
+ }),
148
+ };
149
+
150
+ const { withTimeout } = await import('../../utils/withTimeout');
151
+ vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
152
+
153
+ const result = await tavily('https://example.com', { filterOptions: {} });
154
+
155
+ expect(result).toBeUndefined();
156
+ });
157
+
158
+ it('should return undefined when raw_content is missing', async () => {
159
+ process.env.TAVILY_API_KEY = 'test-api-key';
160
+
161
+ const mockResponse = {
162
+ ok: true,
163
+ json: vi.fn().mockResolvedValue({
164
+ base_url: 'https://api.tavily.com',
165
+ response_time: 1.0,
166
+ results: [
167
+ {
168
+ url: 'https://example.com',
169
+ // raw_content is missing
170
+ images: ['https://example.com/image.jpg'],
171
+ },
172
+ ],
173
+ }),
174
+ };
175
+
176
+ const { withTimeout } = await import('../../utils/withTimeout');
177
+ vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
178
+
179
+ const result = await tavily('https://example.com', { filterOptions: {} });
180
+
181
+ expect(result).toBeUndefined();
182
+ });
183
+
184
+ it('should throw PageNotFoundError for 404 status', async () => {
185
+ process.env.TAVILY_API_KEY = 'test-api-key';
186
+
187
+ const mockResponse = {
188
+ ok: false,
189
+ status: 404,
190
+ statusText: 'Not Found',
191
+ };
192
+
193
+ const { withTimeout } = await import('../../utils/withTimeout');
194
+ vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
195
+
196
+ await expect(tavily('https://example.com', { filterOptions: {} })).rejects.toThrow(
197
+ PageNotFoundError,
198
+ );
199
+ });
200
+
201
+ it('should throw error for other HTTP errors', async () => {
202
+ process.env.TAVILY_API_KEY = 'test-api-key';
203
+
204
+ const mockResponse = {
205
+ ok: false,
206
+ status: 500,
207
+ statusText: 'Internal Server Error',
208
+ };
209
+
210
+ const { withTimeout } = await import('../../utils/withTimeout');
211
+ vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
212
+
213
+ await expect(tavily('https://example.com', { filterOptions: {} })).rejects.toThrow(
214
+ 'Tavily request failed with status 500: Internal Server Error',
215
+ );
216
+ });
217
+
218
+ it('should throw NetworkConnectionError for fetch failures', async () => {
219
+ process.env.TAVILY_API_KEY = 'test-api-key';
220
+
221
+ const { withTimeout } = await import('../../utils/withTimeout');
222
+ vi.mocked(withTimeout).mockRejectedValue(new Error('fetch failed'));
223
+
224
+ await expect(tavily('https://example.com', { filterOptions: {} })).rejects.toThrow(
225
+ NetworkConnectionError,
226
+ );
227
+ });
228
+
229
+ it('should throw TimeoutError when request times out', async () => {
230
+ process.env.TAVILY_API_KEY = 'test-api-key';
231
+
232
+ const timeoutError = new TimeoutError('Request timeout');
233
+
234
+ const { withTimeout } = await import('../../utils/withTimeout');
235
+ vi.mocked(withTimeout).mockRejectedValue(timeoutError);
236
+
237
+ await expect(tavily('https://example.com', { filterOptions: {} })).rejects.toThrow(
238
+ TimeoutError,
239
+ );
240
+ });
241
+
242
+ it('should rethrow unknown errors', async () => {
243
+ process.env.TAVILY_API_KEY = 'test-api-key';
244
+
245
+ const unknownError = new Error('Unknown error');
246
+
247
+ const { withTimeout } = await import('../../utils/withTimeout');
248
+ vi.mocked(withTimeout).mockRejectedValue(unknownError);
249
+
250
+ await expect(tavily('https://example.com', { filterOptions: {} })).rejects.toThrow(
251
+ 'Unknown error',
252
+ );
253
+ });
254
+
255
+ it('should return undefined when JSON parsing fails', async () => {
256
+ process.env.TAVILY_API_KEY = 'test-api-key';
257
+
258
+ const mockResponse = {
259
+ ok: true,
260
+ json: vi.fn().mockRejectedValue(new Error('Invalid JSON')),
261
+ };
262
+
263
+ const { withTimeout } = await import('../../utils/withTimeout');
264
+ vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
265
+
266
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
267
+
268
+ const result = await tavily('https://example.com', { filterOptions: {} });
269
+
270
+ expect(result).toBeUndefined();
271
+ expect(consoleSpy).toHaveBeenCalled();
272
+
273
+ consoleSpy.mockRestore();
274
+ });
275
+
276
+ it('should use result URL when available', async () => {
277
+ process.env.TAVILY_API_KEY = 'test-api-key';
278
+
279
+ const mockResponse = {
280
+ ok: true,
281
+ json: vi.fn().mockResolvedValue({
282
+ base_url: 'https://api.tavily.com',
283
+ response_time: 1.3,
284
+ results: [
285
+ {
286
+ url: 'https://redirected.example.com',
287
+ raw_content: 'Test content with sufficient length. '.repeat(5),
288
+ },
289
+ ],
290
+ }),
291
+ };
292
+
293
+ const { withTimeout } = await import('../../utils/withTimeout');
294
+ vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
295
+
296
+ const result = await tavily('https://example.com', { filterOptions: {} });
297
+
298
+ expect(result?.url).toBe('https://redirected.example.com');
299
+ });
300
+
301
+ it('should fallback to original URL when result URL is missing', async () => {
302
+ process.env.TAVILY_API_KEY = 'test-api-key';
303
+
304
+ const mockResponse = {
305
+ ok: true,
306
+ json: vi.fn().mockResolvedValue({
307
+ base_url: 'https://api.tavily.com',
308
+ response_time: 1.4,
309
+ results: [
310
+ {
311
+ raw_content: 'Test content with sufficient length. '.repeat(5),
312
+ // url is missing
313
+ },
314
+ ],
315
+ }),
316
+ };
317
+
318
+ const { withTimeout } = await import('../../utils/withTimeout');
319
+ vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
320
+
321
+ const result = await tavily('https://example.com', { filterOptions: {} });
322
+
323
+ expect(result?.url).toBe('https://example.com');
324
+ });
325
+
326
+ it('should handle failed results in response', async () => {
327
+ process.env.TAVILY_API_KEY = 'test-api-key';
328
+
329
+ const mockResponse = {
330
+ ok: true,
331
+ json: vi.fn().mockResolvedValue({
332
+ base_url: 'https://api.tavily.com',
333
+ response_time: 1.6,
334
+ results: [],
335
+ failed_results: [
336
+ {
337
+ url: 'https://example.com',
338
+ error: 'Page not accessible',
339
+ },
340
+ ],
341
+ }),
342
+ };
343
+
344
+ const { withTimeout } = await import('../../utils/withTimeout');
345
+ vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
346
+
347
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
348
+
349
+ const result = await tavily('https://example.com', { filterOptions: {} });
350
+
351
+ expect(result).toBeUndefined();
352
+ expect(consoleSpy).toHaveBeenCalledWith(
353
+ 'Tavily API returned no results for URL:',
354
+ 'https://example.com',
355
+ );
356
+
357
+ consoleSpy.mockRestore();
358
+ });
359
+ });
@@ -0,0 +1,217 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { NetworkConnectionError, PageNotFoundError, TimeoutError } from '../errorType';
4
+
5
+ describe('errorType', () => {
6
+ describe('PageNotFoundError', () => {
7
+ it('should create error with correct message and name', () => {
8
+ const message = 'Page not found';
9
+ const error = new PageNotFoundError(message);
10
+
11
+ expect(error).toBeInstanceOf(Error);
12
+ expect(error).toBeInstanceOf(PageNotFoundError);
13
+ expect(error.message).toBe(message);
14
+ expect(error.name).toBe('PageNotFoundError');
15
+ });
16
+
17
+ it('should preserve stack trace', () => {
18
+ const error = new PageNotFoundError('Test message');
19
+ expect(error.stack).toBeDefined();
20
+ expect(typeof error.stack).toBe('string');
21
+ });
22
+
23
+ it('should be throwable and catchable', () => {
24
+ const message = 'Custom 404 message';
25
+
26
+ expect(() => {
27
+ throw new PageNotFoundError(message);
28
+ }).toThrow(PageNotFoundError);
29
+
30
+ expect(() => {
31
+ throw new PageNotFoundError(message);
32
+ }).toThrow(message);
33
+ });
34
+
35
+ it('should handle empty message', () => {
36
+ const error = new PageNotFoundError('');
37
+ expect(error.message).toBe('');
38
+ expect(error.name).toBe('PageNotFoundError');
39
+ });
40
+
41
+ it('should handle special characters in message', () => {
42
+ const specialMessage = 'Page not found: ëñçødéd tëxt & symbols!@#$%^&*()';
43
+ const error = new PageNotFoundError(specialMessage);
44
+ expect(error.message).toBe(specialMessage);
45
+ expect(error.name).toBe('PageNotFoundError');
46
+ });
47
+ });
48
+
49
+ describe('NetworkConnectionError', () => {
50
+ it('should create error with default message and correct name', () => {
51
+ const error = new NetworkConnectionError();
52
+
53
+ expect(error).toBeInstanceOf(Error);
54
+ expect(error).toBeInstanceOf(NetworkConnectionError);
55
+ expect(error.message).toBe('Network connection error');
56
+ expect(error.name).toBe('NetworkConnectionError');
57
+ });
58
+
59
+ it('should preserve stack trace', () => {
60
+ const error = new NetworkConnectionError();
61
+ expect(error.stack).toBeDefined();
62
+ expect(typeof error.stack).toBe('string');
63
+ });
64
+
65
+ it('should be throwable and catchable', () => {
66
+ expect(() => {
67
+ throw new NetworkConnectionError();
68
+ }).toThrow(NetworkConnectionError);
69
+
70
+ expect(() => {
71
+ throw new NetworkConnectionError();
72
+ }).toThrow('Network connection error');
73
+ });
74
+
75
+ it('should always have the same message regardless of parameters', () => {
76
+ const error1 = new NetworkConnectionError();
77
+ const error2 = new NetworkConnectionError();
78
+
79
+ expect(error1.message).toBe(error2.message);
80
+ expect(error1.name).toBe(error2.name);
81
+ expect(error1.message).toBe('Network connection error');
82
+ });
83
+ });
84
+
85
+ describe('TimeoutError', () => {
86
+ it('should create error with correct message and name', () => {
87
+ const message = 'Request timeout after 30 seconds';
88
+ const error = new TimeoutError(message);
89
+
90
+ expect(error).toBeInstanceOf(Error);
91
+ expect(error).toBeInstanceOf(TimeoutError);
92
+ expect(error.message).toBe(message);
93
+ expect(error.name).toBe('TimeoutError');
94
+ });
95
+
96
+ it('should preserve stack trace', () => {
97
+ const error = new TimeoutError('Test timeout');
98
+ expect(error.stack).toBeDefined();
99
+ expect(typeof error.stack).toBe('string');
100
+ });
101
+
102
+ it('should be throwable and catchable', () => {
103
+ const message = 'Custom timeout message';
104
+
105
+ expect(() => {
106
+ throw new TimeoutError(message);
107
+ }).toThrow(TimeoutError);
108
+
109
+ expect(() => {
110
+ throw new TimeoutError(message);
111
+ }).toThrow(message);
112
+ });
113
+
114
+ it('should handle empty message', () => {
115
+ const error = new TimeoutError('');
116
+ expect(error.message).toBe('');
117
+ expect(error.name).toBe('TimeoutError');
118
+ });
119
+
120
+ it('should handle numeric timeout values in message', () => {
121
+ const timeoutMessage = 'Request timed out after 5000ms';
122
+ const error = new TimeoutError(timeoutMessage);
123
+ expect(error.message).toBe(timeoutMessage);
124
+ expect(error.name).toBe('TimeoutError');
125
+ });
126
+ });
127
+
128
+ describe('error inheritance', () => {
129
+ it('should all extend from base Error class', () => {
130
+ const pageError = new PageNotFoundError('404');
131
+ const networkError = new NetworkConnectionError();
132
+ const timeoutError = new TimeoutError('timeout');
133
+
134
+ expect(pageError).toBeInstanceOf(Error);
135
+ expect(networkError).toBeInstanceOf(Error);
136
+ expect(timeoutError).toBeInstanceOf(Error);
137
+ });
138
+
139
+ it('should be distinguishable by instanceof', () => {
140
+ const pageError = new PageNotFoundError('404');
141
+ const networkError = new NetworkConnectionError();
142
+ const timeoutError = new TimeoutError('timeout');
143
+
144
+ expect(pageError).toBeInstanceOf(PageNotFoundError);
145
+ expect(pageError).not.toBeInstanceOf(NetworkConnectionError);
146
+ expect(pageError).not.toBeInstanceOf(TimeoutError);
147
+
148
+ expect(networkError).toBeInstanceOf(NetworkConnectionError);
149
+ expect(networkError).not.toBeInstanceOf(PageNotFoundError);
150
+ expect(networkError).not.toBeInstanceOf(TimeoutError);
151
+
152
+ expect(timeoutError).toBeInstanceOf(TimeoutError);
153
+ expect(timeoutError).not.toBeInstanceOf(PageNotFoundError);
154
+ expect(timeoutError).not.toBeInstanceOf(NetworkConnectionError);
155
+ });
156
+
157
+ it('should be distinguishable by name property', () => {
158
+ const pageError = new PageNotFoundError('404');
159
+ const networkError = new NetworkConnectionError();
160
+ const timeoutError = new TimeoutError('timeout');
161
+
162
+ expect(pageError.name).toBe('PageNotFoundError');
163
+ expect(networkError.name).toBe('NetworkConnectionError');
164
+ expect(timeoutError.name).toBe('TimeoutError');
165
+
166
+ // Names should be unique
167
+ const names = [pageError.name, networkError.name, timeoutError.name];
168
+ const uniqueNames = [...new Set(names)];
169
+ expect(uniqueNames).toHaveLength(names.length);
170
+ });
171
+ });
172
+
173
+ describe('error catching scenarios', () => {
174
+ it('should allow catching specific error types', () => {
175
+ const testErrors = [
176
+ new PageNotFoundError('404 error'),
177
+ new NetworkConnectionError(),
178
+ new TimeoutError('timeout error'),
179
+ ];
180
+
181
+ testErrors.forEach((error) => {
182
+ try {
183
+ throw error;
184
+ } catch (e) {
185
+ if (e instanceof PageNotFoundError) {
186
+ expect(e.name).toBe('PageNotFoundError');
187
+ expect(e.message).toBe('404 error');
188
+ } else if (e instanceof NetworkConnectionError) {
189
+ expect(e.name).toBe('NetworkConnectionError');
190
+ expect(e.message).toBe('Network connection error');
191
+ } else if (e instanceof TimeoutError) {
192
+ expect(e.name).toBe('TimeoutError');
193
+ expect(e.message).toBe('timeout error');
194
+ } else {
195
+ throw new Error('Unexpected error type');
196
+ }
197
+ }
198
+ });
199
+ });
200
+
201
+ it('should allow catching by error name', () => {
202
+ const testCases = [
203
+ { error: new PageNotFoundError('test'), expectedName: 'PageNotFoundError' },
204
+ { error: new NetworkConnectionError(), expectedName: 'NetworkConnectionError' },
205
+ { error: new TimeoutError('test'), expectedName: 'TimeoutError' },
206
+ ];
207
+
208
+ testCases.forEach(({ error, expectedName }) => {
209
+ try {
210
+ throw error;
211
+ } catch (e) {
212
+ expect((e as Error).name).toBe(expectedName);
213
+ }
214
+ });
215
+ });
216
+ });
217
+ });
@@ -2,6 +2,9 @@ import { defineConfig } from 'vitest/config';
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
5
+ coverage: {
6
+ reporter: ['text', 'json', 'lcov', 'text-summary'],
7
+ },
5
8
  environment: 'node',
6
9
  },
7
10
  });