@lobehub/chat 1.104.0 → 1.104.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 (25) hide show
  1. package/.cursor/rules/code-review.mdc +2 -0
  2. package/.cursor/rules/typescript.mdc +3 -1
  3. package/CHANGELOG.md +50 -0
  4. package/apps/desktop/src/main/core/ui/ShortcutManager.ts +61 -6
  5. package/apps/desktop/src/main/core/ui/__tests__/ShortcutManager.test.ts +539 -0
  6. package/changelog/v1.json +18 -0
  7. package/package.json +1 -1
  8. package/src/app/[variants]/(main)/image/features/GenerationFeed/BatchItem.tsx +6 -1
  9. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ErrorState.tsx +3 -2
  10. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/LoadingState.tsx +27 -24
  11. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/SuccessState.tsx +14 -3
  12. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/index.tsx +4 -7
  13. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/types.ts +3 -0
  14. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.test.ts +600 -0
  15. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.ts +126 -7
  16. package/src/const/imageGeneration.ts +18 -0
  17. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +3 -0
  18. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +7 -5
  19. package/src/libs/model-runtime/utils/streams/openai/openai.ts +8 -4
  20. package/src/libs/model-runtime/utils/usageConverter.test.ts +45 -1
  21. package/src/libs/model-runtime/utils/usageConverter.ts +6 -2
  22. package/src/server/services/generation/index.test.ts +848 -0
  23. package/src/server/services/generation/index.ts +90 -69
  24. package/src/utils/number.test.ts +101 -1
  25. package/src/utils/number.ts +42 -0
@@ -8,10 +8,20 @@ import ImageItem from '@/components/ImageItem';
8
8
  import { ActionButtons } from './ActionButtons';
9
9
  import { useStyles } from './styles';
10
10
  import { SuccessStateProps } from './types';
11
+ import { getThumbnailMaxWidth } from './utils';
11
12
 
12
13
  // 成功状态组件
13
14
  export const SuccessState = memo<SuccessStateProps>(
14
- ({ generation, prompt, aspectRatio, onDelete, onDownload, onCopySeed, seedTooltip }) => {
15
+ ({
16
+ generation,
17
+ generationBatch,
18
+ prompt,
19
+ aspectRatio,
20
+ onDelete,
21
+ onDownload,
22
+ onCopySeed,
23
+ seedTooltip,
24
+ }) => {
15
25
  const { styles } = useStyles();
16
26
 
17
27
  return (
@@ -21,7 +31,7 @@ export const SuccessState = memo<SuccessStateProps>(
21
31
  justify={'center'}
22
32
  style={{
23
33
  aspectRatio,
24
- maxWidth: generation.asset?.width ? generation.asset.width / 2 : 'unset',
34
+ maxWidth: getThumbnailMaxWidth(generation, generationBatch),
25
35
  }}
26
36
  variant={'filled'}
27
37
  >
@@ -31,7 +41,8 @@ export const SuccessState = memo<SuccessStateProps>(
31
41
  src: generation.asset!.url,
32
42
  }}
33
43
  style={{ height: '100%', width: '100%' }}
34
- url={generation.asset!.thumbnailUrl}
44
+ // Thumbnail quality is too bad
45
+ url={generation.asset!.url}
35
46
  />
36
47
  <ActionButtons
37
48
  onCopySeed={onCopySeed}
@@ -37,13 +37,7 @@ export const GenerationItem = memo<GenerationItemProps>(
37
37
  const shouldPoll = !isFinalized;
38
38
  useCheckGenerationStatus(generation.id, generation.task.id, activeTopicId!, shouldPoll);
39
39
 
40
- const aspectRatio = getAspectRatio(
41
- generation.asset ?? {
42
- height: generationBatch.config?.height,
43
- type: 'image',
44
- width: generationBatch.config?.width,
45
- },
46
- );
40
+ const aspectRatio = getAspectRatio(generation, generationBatch);
47
41
 
48
42
  // 事件处理函数
49
43
  const handleDeleteGeneration = async () => {
@@ -120,6 +114,7 @@ export const GenerationItem = memo<GenerationItemProps>(
120
114
  <SuccessState
121
115
  aspectRatio={aspectRatio}
122
116
  generation={generation}
117
+ generationBatch={generationBatch}
123
118
  onCopySeed={handleCopySeed}
124
119
  onDelete={handleDeleteGeneration}
125
120
  onDownload={handleDownloadImage}
@@ -134,6 +129,7 @@ export const GenerationItem = memo<GenerationItemProps>(
134
129
  <ErrorState
135
130
  aspectRatio={aspectRatio}
136
131
  generation={generation}
132
+ generationBatch={generationBatch}
137
133
  onCopyError={handleCopyError}
138
134
  onDelete={handleDeleteGeneration}
139
135
  />
@@ -145,6 +141,7 @@ export const GenerationItem = memo<GenerationItemProps>(
145
141
  <LoadingState
146
142
  aspectRatio={aspectRatio}
147
143
  generation={generation}
144
+ generationBatch={generationBatch}
148
145
  onDelete={handleDeleteGeneration}
149
146
  />
150
147
  );
@@ -18,6 +18,7 @@ export interface ActionButtonsProps {
18
18
  export interface SuccessStateProps {
19
19
  aspectRatio: string;
20
20
  generation: Generation;
21
+ generationBatch: GenerationBatch;
21
22
  onCopySeed?: () => void;
22
23
  onDelete: () => void;
23
24
  onDownload: () => void;
@@ -28,6 +29,7 @@ export interface SuccessStateProps {
28
29
  export interface ErrorStateProps {
29
30
  aspectRatio: string;
30
31
  generation: Generation;
32
+ generationBatch: GenerationBatch;
31
33
  onCopyError: () => void;
32
34
  onDelete: () => void;
33
35
  }
@@ -35,5 +37,6 @@ export interface ErrorStateProps {
35
37
  export interface LoadingStateProps {
36
38
  aspectRatio: string;
37
39
  generation: Generation;
40
+ generationBatch: GenerationBatch;
38
41
  onDelete: () => void;
39
42
  }
@@ -0,0 +1,600 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import type { Generation, GenerationBatch } from '@/types/generation';
4
+
5
+ // Import functions for testing
6
+ import {
7
+ DEFAULT_MAX_ITEM_WIDTH,
8
+ getAspectRatio,
9
+ getImageDimensions,
10
+ getThumbnailMaxWidth,
11
+ } from './utils';
12
+
13
+ describe('getImageDimensions', () => {
14
+ // Mock base generation object
15
+ const baseGeneration: Generation = {
16
+ id: 'test-gen-id',
17
+ seed: 12345,
18
+ createdAt: new Date(),
19
+ asyncTaskId: null,
20
+ task: {
21
+ id: 'task-id',
22
+ status: 'success' as any,
23
+ },
24
+ };
25
+
26
+ describe('with asset dimensions', () => {
27
+ it('should return width, height and aspect ratio from asset', () => {
28
+ const generation: Generation = {
29
+ ...baseGeneration,
30
+ asset: {
31
+ type: 'image',
32
+ width: 1920,
33
+ height: 1080,
34
+ },
35
+ };
36
+
37
+ const result = getImageDimensions(generation);
38
+ expect(result).toEqual({
39
+ width: 1920,
40
+ height: 1080,
41
+ aspectRatio: '1920 / 1080',
42
+ });
43
+ });
44
+
45
+ it('should prioritize asset even when other sources exist', () => {
46
+ const generation: Generation = {
47
+ ...baseGeneration,
48
+ asset: {
49
+ type: 'image',
50
+ width: 800,
51
+ height: 600,
52
+ },
53
+ };
54
+
55
+ const generationBatch: GenerationBatch = {
56
+ id: 'batch-id',
57
+ provider: 'test',
58
+ model: 'test-model',
59
+ prompt: 'test prompt',
60
+ width: 1024,
61
+ height: 1024,
62
+ config: {
63
+ prompt: 'test',
64
+ width: 512,
65
+ height: 512,
66
+ },
67
+ createdAt: new Date(),
68
+ generations: [],
69
+ };
70
+
71
+ const result = getImageDimensions(generation, generationBatch);
72
+ expect(result).toEqual({
73
+ width: 800,
74
+ height: 600,
75
+ aspectRatio: '800 / 600',
76
+ });
77
+ });
78
+ });
79
+
80
+ describe('with config dimensions', () => {
81
+ it('should return dimensions from config when asset is not available', () => {
82
+ const generation: Generation = {
83
+ ...baseGeneration,
84
+ asset: null,
85
+ };
86
+
87
+ const generationBatch: GenerationBatch = {
88
+ id: 'batch-id',
89
+ provider: 'test',
90
+ model: 'test-model',
91
+ prompt: 'test prompt',
92
+ config: {
93
+ prompt: 'test',
94
+ width: 1024,
95
+ height: 768,
96
+ },
97
+ createdAt: new Date(),
98
+ generations: [],
99
+ };
100
+
101
+ const result = getImageDimensions(generation, generationBatch);
102
+ expect(result).toEqual({
103
+ width: 1024,
104
+ height: 768,
105
+ aspectRatio: '1024 / 768',
106
+ });
107
+ });
108
+ });
109
+
110
+ describe('with batch top-level dimensions', () => {
111
+ it('should return dimensions from batch when config is not available', () => {
112
+ const generation: Generation = {
113
+ ...baseGeneration,
114
+ asset: null,
115
+ };
116
+
117
+ const generationBatch: GenerationBatch = {
118
+ id: 'batch-id',
119
+ provider: 'test',
120
+ model: 'test-model',
121
+ prompt: 'test prompt',
122
+ width: 1280,
123
+ height: 720,
124
+ createdAt: new Date(),
125
+ generations: [],
126
+ };
127
+
128
+ const result = getImageDimensions(generation, generationBatch);
129
+ expect(result).toEqual({
130
+ width: 1280,
131
+ height: 720,
132
+ aspectRatio: '1280 / 720',
133
+ });
134
+ });
135
+ });
136
+
137
+ describe('with size parameter', () => {
138
+ it('should parse dimensions from size parameter', () => {
139
+ const generation: Generation = {
140
+ ...baseGeneration,
141
+ asset: null,
142
+ };
143
+
144
+ const generationBatch: GenerationBatch = {
145
+ id: 'batch-id',
146
+ provider: 'test',
147
+ model: 'test-model',
148
+ prompt: 'test prompt',
149
+ config: {
150
+ prompt: 'test',
151
+ size: '1920x1080',
152
+ },
153
+ createdAt: new Date(),
154
+ generations: [],
155
+ };
156
+
157
+ const result = getImageDimensions(generation, generationBatch);
158
+ expect(result).toEqual({
159
+ width: 1920,
160
+ height: 1080,
161
+ aspectRatio: '1920 / 1080',
162
+ });
163
+ });
164
+
165
+ it('should ignore size when it is "auto"', () => {
166
+ const generation: Generation = {
167
+ ...baseGeneration,
168
+ asset: null,
169
+ };
170
+
171
+ const generationBatch: GenerationBatch = {
172
+ id: 'batch-id',
173
+ provider: 'test',
174
+ model: 'test-model',
175
+ prompt: 'test prompt',
176
+ config: {
177
+ prompt: 'test',
178
+ size: 'auto',
179
+ aspectRatio: '16:9',
180
+ },
181
+ createdAt: new Date(),
182
+ generations: [],
183
+ };
184
+
185
+ const result = getImageDimensions(generation, generationBatch);
186
+ expect(result).toEqual({
187
+ width: null,
188
+ height: null,
189
+ aspectRatio: '16 / 9',
190
+ });
191
+ });
192
+ });
193
+
194
+ describe('with aspectRatio parameter only', () => {
195
+ it('should return aspect ratio without dimensions', () => {
196
+ const generation: Generation = {
197
+ ...baseGeneration,
198
+ asset: null,
199
+ };
200
+
201
+ const generationBatch: GenerationBatch = {
202
+ id: 'batch-id',
203
+ provider: 'test',
204
+ model: 'test-model',
205
+ prompt: 'test prompt',
206
+ config: {
207
+ prompt: 'test',
208
+ aspectRatio: '16:9',
209
+ },
210
+ createdAt: new Date(),
211
+ generations: [],
212
+ };
213
+
214
+ const result = getImageDimensions(generation, generationBatch);
215
+ expect(result).toEqual({
216
+ width: null,
217
+ height: null,
218
+ aspectRatio: '16 / 9',
219
+ });
220
+ });
221
+
222
+ it('should handle various aspect ratio formats', () => {
223
+ const testCases = [
224
+ { aspectRatio: '1:1', expected: '1 / 1' },
225
+ { aspectRatio: '4:3', expected: '4 / 3' },
226
+ { aspectRatio: '16:9', expected: '16 / 9' },
227
+ { aspectRatio: '21:9', expected: '21 / 9' },
228
+ ];
229
+
230
+ testCases.forEach(({ aspectRatio, expected }) => {
231
+ const generation: Generation = {
232
+ ...baseGeneration,
233
+ asset: null,
234
+ };
235
+
236
+ const generationBatch: GenerationBatch = {
237
+ id: 'batch-id',
238
+ provider: 'test',
239
+ model: 'test-model',
240
+ prompt: 'test prompt',
241
+ config: {
242
+ prompt: 'test',
243
+ aspectRatio,
244
+ },
245
+ createdAt: new Date(),
246
+ generations: [],
247
+ };
248
+
249
+ const result = getImageDimensions(generation, generationBatch);
250
+ expect(result.aspectRatio).toBe(expected);
251
+ });
252
+ });
253
+ });
254
+
255
+ describe('edge cases', () => {
256
+ it('should return all null when no dimensions are available', () => {
257
+ const generation: Generation = {
258
+ ...baseGeneration,
259
+ asset: null,
260
+ };
261
+
262
+ const result = getImageDimensions(generation);
263
+ expect(result).toEqual({
264
+ width: null,
265
+ height: null,
266
+ aspectRatio: null,
267
+ });
268
+ });
269
+
270
+ it('should handle partial asset dimensions', () => {
271
+ const generation: Generation = {
272
+ ...baseGeneration,
273
+ asset: {
274
+ type: 'image',
275
+ width: 1920,
276
+ // height is missing
277
+ },
278
+ };
279
+
280
+ const generationBatch: GenerationBatch = {
281
+ id: 'batch-id',
282
+ provider: 'test',
283
+ model: 'test-model',
284
+ prompt: 'test prompt',
285
+ config: {
286
+ prompt: 'test',
287
+ width: 1024,
288
+ height: 768,
289
+ },
290
+ createdAt: new Date(),
291
+ generations: [],
292
+ };
293
+
294
+ const result = getImageDimensions(generation, generationBatch);
295
+ expect(result).toEqual({
296
+ width: 1024,
297
+ height: 768,
298
+ aspectRatio: '1024 / 768',
299
+ });
300
+ });
301
+
302
+ it('should handle invalid size format', () => {
303
+ const generation: Generation = {
304
+ ...baseGeneration,
305
+ asset: null,
306
+ };
307
+
308
+ const generationBatch: GenerationBatch = {
309
+ id: 'batch-id',
310
+ provider: 'test',
311
+ model: 'test-model',
312
+ prompt: 'test prompt',
313
+ config: {
314
+ prompt: 'test',
315
+ size: 'invalid-format',
316
+ },
317
+ createdAt: new Date(),
318
+ generations: [],
319
+ };
320
+
321
+ const result = getImageDimensions(generation, generationBatch);
322
+ expect(result).toEqual({
323
+ width: null,
324
+ height: null,
325
+ aspectRatio: null,
326
+ });
327
+ });
328
+
329
+ it('should handle invalid aspectRatio format', () => {
330
+ const generation: Generation = {
331
+ ...baseGeneration,
332
+ asset: null,
333
+ };
334
+
335
+ const generationBatch: GenerationBatch = {
336
+ id: 'batch-id',
337
+ provider: 'test',
338
+ model: 'test-model',
339
+ prompt: 'test prompt',
340
+ config: {
341
+ prompt: 'test',
342
+ aspectRatio: 'invalid-format',
343
+ },
344
+ createdAt: new Date(),
345
+ generations: [],
346
+ };
347
+
348
+ const result = getImageDimensions(generation, generationBatch);
349
+ expect(result).toEqual({
350
+ width: null,
351
+ height: null,
352
+ aspectRatio: null,
353
+ });
354
+ });
355
+
356
+ it('should handle zero dimensions', () => {
357
+ const generation: Generation = {
358
+ ...baseGeneration,
359
+ asset: {
360
+ type: 'image',
361
+ width: 0,
362
+ height: 0,
363
+ },
364
+ };
365
+
366
+ const result = getImageDimensions(generation);
367
+ expect(result).toEqual({
368
+ width: null,
369
+ height: null,
370
+ aspectRatio: null,
371
+ });
372
+ });
373
+ });
374
+ });
375
+
376
+ describe('getAspectRatio (isolated unit testing)', () => {
377
+ const mockGeneration: Generation = {
378
+ id: 'test-gen-id',
379
+ seed: 12345,
380
+ createdAt: new Date(),
381
+ asyncTaskId: null,
382
+ task: {
383
+ id: 'task-id',
384
+ status: 'success' as any,
385
+ },
386
+ };
387
+ const mockGenerationBatch: GenerationBatch = {
388
+ id: 'test-batch-id',
389
+ provider: 'test-provider',
390
+ model: 'test-model',
391
+ prompt: 'test prompt',
392
+ createdAt: new Date(),
393
+ generations: [],
394
+ };
395
+
396
+ beforeEach(() => {
397
+ vi.clearAllMocks();
398
+ });
399
+
400
+ it('should return aspectRatio from getImageDimensions when dimensions have aspectRatio', () => {
401
+ // Test the actual implementation directly with mock data
402
+ const mockGen: Generation = {
403
+ ...mockGeneration,
404
+ asset: {
405
+ type: 'image',
406
+ width: 1920,
407
+ height: 1080,
408
+ },
409
+ };
410
+
411
+ const result = getAspectRatio(mockGen);
412
+ expect(result).toBe('1920 / 1080');
413
+ });
414
+
415
+ it('should return default "1 / 1" when no dimensions are available', () => {
416
+ const result = getAspectRatio(mockGeneration, mockGenerationBatch);
417
+ expect(result).toBe('1 / 1');
418
+ });
419
+
420
+ it('should work with different aspectRatio sources', () => {
421
+ const mockBatch: GenerationBatch = {
422
+ id: 'test-batch',
423
+ provider: 'test-provider',
424
+ model: 'test-model',
425
+ prompt: 'test prompt',
426
+ createdAt: new Date(),
427
+ generations: [],
428
+ config: {
429
+ prompt: 'test prompt',
430
+ aspectRatio: '16:9',
431
+ },
432
+ };
433
+
434
+ const result = getAspectRatio(mockGeneration, mockBatch);
435
+ expect(result).toBe('16 / 9');
436
+ });
437
+ });
438
+
439
+ describe('getThumbnailMaxWidth (isolated unit testing)', () => {
440
+ const mockGeneration: Generation = {
441
+ id: 'test-gen-id',
442
+ seed: 12345,
443
+ createdAt: new Date(),
444
+ asyncTaskId: null,
445
+ task: {
446
+ id: 'task-id',
447
+ status: 'success' as any,
448
+ },
449
+ };
450
+ const mockGenerationBatch: GenerationBatch = {
451
+ id: 'test-batch-id',
452
+ provider: 'test-provider',
453
+ model: 'test-model',
454
+ prompt: 'test prompt',
455
+ createdAt: new Date(),
456
+ generations: [],
457
+ };
458
+
459
+ // Mock window.innerHeight for tests
460
+ const originalWindow = global.window;
461
+
462
+ beforeEach(() => {
463
+ vi.clearAllMocks();
464
+ Object.defineProperty(global, 'window', {
465
+ writable: true,
466
+ value: {
467
+ innerHeight: 800,
468
+ },
469
+ });
470
+ });
471
+
472
+ afterEach(() => {
473
+ global.window = originalWindow;
474
+ });
475
+
476
+ it('should return DEFAULT_MAX_ITEM_WIDTH when no dimensions available', () => {
477
+ const result = getThumbnailMaxWidth(mockGeneration, mockGenerationBatch);
478
+ expect(result).toBe(DEFAULT_MAX_ITEM_WIDTH);
479
+ });
480
+
481
+ it('should return DEFAULT_MAX_ITEM_WIDTH when width is missing', () => {
482
+ const mockGen: Generation = {
483
+ ...mockGeneration,
484
+ // No asset with width/height, should fall back to default
485
+ };
486
+ const result = getThumbnailMaxWidth(mockGen);
487
+ expect(result).toBe(DEFAULT_MAX_ITEM_WIDTH);
488
+ });
489
+
490
+ it('should return DEFAULT_MAX_ITEM_WIDTH when height is missing', () => {
491
+ const mockGen: Generation = {
492
+ ...mockGeneration,
493
+ // No asset with valid dimensions
494
+ };
495
+ const result = getThumbnailMaxWidth(mockGen);
496
+ expect(result).toBe(DEFAULT_MAX_ITEM_WIDTH);
497
+ });
498
+
499
+ it('should calculate width based on screen height constraint', () => {
500
+ const mockGen: Generation = {
501
+ ...mockGeneration,
502
+ asset: {
503
+ type: 'image',
504
+ width: 300,
505
+ height: 200,
506
+ },
507
+ };
508
+
509
+ // aspectRatio = 300/200 = 1.5
510
+ // maxScreenHeight = 800/2 = 400
511
+ // maxWidthFromHeight = 400 * 1.5 = 600
512
+ // maxReasonableWidth = 200 * 2 = 400
513
+ // min(600, 400) = 400
514
+ const result = getThumbnailMaxWidth(mockGen);
515
+ expect(result).toBe(400);
516
+ });
517
+
518
+ it('should apply maxReasonableWidth limit', () => {
519
+ const mockGen: Generation = {
520
+ ...mockGeneration,
521
+ asset: {
522
+ type: 'image',
523
+ width: 600,
524
+ height: 200,
525
+ },
526
+ };
527
+
528
+ // aspectRatio = 600/200 = 3
529
+ // maxScreenHeight = 800/2 = 400
530
+ // maxWidthFromHeight = 400 * 3 = 1200
531
+ // maxReasonableWidth = 200 * 2 = 400
532
+ // min(1200, 400) = 400
533
+ const result = getThumbnailMaxWidth(mockGen);
534
+ expect(result).toBe(400);
535
+ });
536
+
537
+ it('should use screen height constraint when smaller', () => {
538
+ const mockGen: Generation = {
539
+ ...mockGeneration,
540
+ asset: {
541
+ type: 'image',
542
+ width: 200,
543
+ height: 400,
544
+ },
545
+ };
546
+
547
+ // aspectRatio = 200/400 = 0.5
548
+ // maxScreenHeight = 800/2 = 400
549
+ // maxWidthFromHeight = 400 * 0.5 = 200
550
+ // maxReasonableWidth = 200 * 2 = 400
551
+ // min(200, 400) = 200
552
+ const result = getThumbnailMaxWidth(mockGen);
553
+ expect(result).toBe(200);
554
+ });
555
+
556
+ it('should handle different window.innerHeight values', () => {
557
+ Object.defineProperty(global, 'window', {
558
+ writable: true,
559
+ value: {
560
+ innerHeight: 600,
561
+ },
562
+ });
563
+
564
+ const mockGen: Generation = {
565
+ ...mockGeneration,
566
+ asset: {
567
+ type: 'image',
568
+ width: 400,
569
+ height: 200,
570
+ },
571
+ };
572
+
573
+ // aspectRatio = 400/200 = 2
574
+ // maxScreenHeight = 600/2 = 300
575
+ // maxWidthFromHeight = 300 * 2 = 600
576
+ // maxReasonableWidth = 200 * 2 = 400
577
+ // min(600, 400) = 400
578
+ const result = getThumbnailMaxWidth(mockGen);
579
+ expect(result).toBe(400);
580
+ });
581
+
582
+ it('should round calculated width correctly', () => {
583
+ const mockGen: Generation = {
584
+ ...mockGeneration,
585
+ asset: {
586
+ type: 'image',
587
+ width: 512,
588
+ height: 1000,
589
+ },
590
+ };
591
+
592
+ // aspectRatio = 512/1000 = 0.512
593
+ // maxScreenHeight = 800/2 = 400
594
+ // maxWidthFromHeight = Math.round(400 * 0.512) = Math.round(204.8) = 205
595
+ // maxReasonableWidth = 200 * 2 = 400
596
+ // min(205, 400) = 205
597
+ const result = getThumbnailMaxWidth(mockGen);
598
+ expect(result).toBe(205);
599
+ });
600
+ });