@linktr.ee/messaging-react 1.33.2 → 1.33.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,20 @@
1
1
  import React from 'react'
2
2
  import type { LocalMessage } from 'stream-chat'
3
- import { describe, expect, it, vi } from 'vitest'
3
+ import { beforeAll, describe, expect, it, vi } from 'vitest'
4
4
 
5
- import { renderWithProviders, screen } from '../../test/utils'
5
+ import { renderWithProviders, screen, fireEvent } from '../../test/utils'
6
6
 
7
7
  import { MediaMessage } from '.'
8
8
 
9
+ beforeAll(() => {
10
+ HTMLDialogElement.prototype.showModal = vi.fn(function (this: HTMLDialogElement) {
11
+ this.setAttribute('open', '')
12
+ })
13
+ HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
14
+ this.removeAttribute('open')
15
+ })
16
+ })
17
+
9
18
  vi.mock('../Avatar', () => ({
10
19
  Avatar: ({ id }: { id: string }) => (
11
20
  <div data-testid="avatar" data-user-id={id} />
@@ -16,14 +25,20 @@ vi.mock('../LockedAttachment/components/MediaPlayer', () => ({
16
25
  default: ({
17
26
  source,
18
27
  mimeType,
28
+ onContainerClick,
19
29
  }: {
20
30
  source: string
21
31
  mimeType: string
32
+ onContainerClick?: (e: React.MouseEvent) => void
22
33
  }) => (
23
34
  <div
35
+ role="button"
36
+ tabIndex={0}
24
37
  data-testid="media-player"
25
38
  data-source={source}
26
39
  data-mime-type={mimeType}
40
+ onClick={onContainerClick}
41
+ onKeyDown={undefined}
27
42
  />
28
43
  ),
29
44
  }))
@@ -41,7 +56,6 @@ vi.mock('../LockedAttachment/utils/mimeType', () => ({
41
56
  },
42
57
  }))
43
58
 
44
- // Cast through unknown to avoid satisfying every optional field of LocalMessage
45
59
  const msg = (overrides: Record<string, unknown> = {}): LocalMessage =>
46
60
  ({
47
61
  id: 'msg-1',
@@ -63,7 +77,7 @@ describe('MediaMessage', () => {
63
77
  expect(container.firstChild).toBeNull()
64
78
  })
65
79
 
66
- it('renders MediaPlayer for a video attachment', () => {
80
+ it('renders MediaPlayer immediately for a video attachment', () => {
67
81
  renderWithProviders(
68
82
  <MediaMessage
69
83
  message={msg({
@@ -72,6 +86,7 @@ describe('MediaMessage', () => {
72
86
  type: 'video',
73
87
  asset_url: 'https://cdn.example.com/clip.mp4',
74
88
  mime_type: 'video/mp4',
89
+ title: 'My Clip',
75
90
  },
76
91
  ],
77
92
  })}
@@ -80,11 +95,28 @@ describe('MediaMessage', () => {
80
95
 
81
96
  const player = screen.getByTestId('media-player')
82
97
  expect(player).toBeInTheDocument()
83
- expect(player).toHaveAttribute(
84
- 'data-source',
85
- 'https://cdn.example.com/clip.mp4'
98
+ expect(player).toHaveAttribute('data-source', 'https://cdn.example.com/clip.mp4')
99
+ })
100
+
101
+ it('opens viewer when the video player container is clicked', () => {
102
+ renderWithProviders(
103
+ <MediaMessage
104
+ message={msg({
105
+ attachments: [
106
+ {
107
+ type: 'video',
108
+ asset_url: 'https://cdn.example.com/clip.mp4',
109
+ mime_type: 'video/mp4',
110
+ title: 'My Clip',
111
+ },
112
+ ],
113
+ })}
114
+ />
86
115
  )
87
- expect(player).toHaveAttribute('data-mime-type', 'video/mp4')
116
+
117
+ fireEvent.click(screen.getByTestId('media-player'))
118
+ expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalled()
119
+ expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument()
88
120
  })
89
121
 
90
122
  it('renders MediaPlayer for an audio attachment', () => {
@@ -108,7 +140,27 @@ describe('MediaMessage', () => {
108
140
  )
109
141
  })
110
142
 
111
- it('renders an img for an image attachment', () => {
143
+ it('does not show viewer or expand button for audio', () => {
144
+ renderWithProviders(
145
+ <MediaMessage
146
+ message={msg({
147
+ attachments: [
148
+ {
149
+ type: 'audio',
150
+ asset_url: 'https://cdn.example.com/track.mp3',
151
+ mime_type: 'audio/mpeg',
152
+ title: 'My Track',
153
+ },
154
+ ],
155
+ })}
156
+ />
157
+ )
158
+
159
+ expect(screen.queryByRole('button', { name: 'View full screen' })).not.toBeInTheDocument()
160
+ expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument()
161
+ })
162
+
163
+ it('renders an img for an image attachment and opens viewer on click', () => {
112
164
  renderWithProviders(
113
165
  <MediaMessage
114
166
  message={msg({
@@ -126,9 +178,36 @@ describe('MediaMessage', () => {
126
178
 
127
179
  const image = screen.getByRole('img', { name: 'My Photo' })
128
180
  expect(image).toHaveAttribute('src', 'https://cdn.example.com/photo.jpg')
181
+
182
+ // Clicking the image button opens the viewer
183
+ fireEvent.click(screen.getByRole('button', { name: 'My Photo' }))
184
+ expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalled()
185
+ const viewerImages = screen.getAllByRole('img', { name: 'My Photo' })
186
+ expect(viewerImages.some((img) => img.classList.contains('rounded-2xl'))).toBe(true)
129
187
  })
130
188
 
131
- it('renders a download link for a document attachment', () => {
189
+ it('does not show expand button in chin for images, only download', () => {
190
+ renderWithProviders(
191
+ <MediaMessage
192
+ message={msg({
193
+ attachments: [
194
+ {
195
+ type: 'image',
196
+ image_url: 'https://cdn.example.com/photo.jpg',
197
+ mime_type: 'image/jpeg',
198
+ title: 'My Photo',
199
+ },
200
+ ],
201
+ })}
202
+ />
203
+ )
204
+
205
+ // Thumbnail button + download button — no separate expand/fullscreen button
206
+ expect(screen.queryByRole('button', { name: 'View full screen' })).not.toBeInTheDocument()
207
+ expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument()
208
+ })
209
+
210
+ it('renders PDF card with a viewer button', () => {
132
211
  renderWithProviders(
133
212
  <MediaMessage
134
213
  message={msg({
@@ -144,13 +223,56 @@ describe('MediaMessage', () => {
144
223
  />
145
224
  )
146
225
 
147
- const link = screen.getByRole('link')
148
- expect(link).toHaveAttribute('href', 'https://cdn.example.com/report.pdf')
226
+ expect(screen.getByRole('button', { name: 'Open PDF viewer' })).toBeInTheDocument()
149
227
  expect(screen.getByText('Annual Report')).toBeInTheDocument()
150
- expect(screen.getByTestId('type-icon')).toBeInTheDocument()
151
228
  })
152
229
 
153
- it('shows title and file size below video', () => {
230
+ it('opens PDF viewer when PDF thumbnail is clicked', () => {
231
+ renderWithProviders(
232
+ <MediaMessage
233
+ message={msg({
234
+ attachments: [
235
+ {
236
+ type: 'file',
237
+ asset_url: 'https://cdn.example.com/report.pdf',
238
+ mime_type: 'application/pdf',
239
+ title: 'Annual Report',
240
+ },
241
+ ],
242
+ })}
243
+ />
244
+ )
245
+
246
+ fireEvent.click(screen.getByRole('button', { name: 'Open PDF viewer' }))
247
+ expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalled()
248
+ expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument()
249
+ })
250
+
251
+ it('renders unknown file as a link with no viewer button', () => {
252
+ renderWithProviders(
253
+ <MediaMessage
254
+ message={msg({
255
+ attachments: [
256
+ {
257
+ type: 'file',
258
+ asset_url: 'https://cdn.example.com/data.bin',
259
+ mime_type: 'application/octet-stream',
260
+ title: 'Unknown File',
261
+ },
262
+ ],
263
+ })}
264
+ />
265
+ )
266
+
267
+ // No viewer button for unknown files
268
+ expect(screen.queryByRole('button', { name: 'Open PDF viewer' })).not.toBeInTheDocument()
269
+ // Thumbnail is a plain link (opens in new tab)
270
+ const links = screen.getAllByRole('link')
271
+ expect(links.some((l) => l.getAttribute('href') === 'https://cdn.example.com/data.bin')).toBe(true)
272
+ expect(screen.getByText('Unknown File')).toBeInTheDocument()
273
+ })
274
+
275
+ it('shows title and file size in chin', () => {
154
276
  renderWithProviders(
155
277
  <MediaMessage
156
278
  message={msg({
@@ -188,10 +310,7 @@ describe('MediaMessage', () => {
188
310
  )
189
311
 
190
312
  expect(screen.getByTestId('avatar')).toBeInTheDocument()
191
- expect(screen.getByTestId('avatar')).toHaveAttribute(
192
- 'data-user-id',
193
- 'user-1'
194
- )
313
+ expect(screen.getByTestId('avatar')).toHaveAttribute('data-user-id', 'user-1')
195
314
  })
196
315
 
197
316
  it('does not render Avatar for own messages', () => {
@@ -258,4 +377,144 @@ describe('MediaMessage', () => {
258
377
 
259
378
  expect(container.firstChild).toBeNull()
260
379
  })
380
+
381
+ it('renders a link card for a link attachment', () => {
382
+ renderWithProviders(
383
+ <MediaMessage
384
+ message={msg({
385
+ attachments: [
386
+ {
387
+ type: 'link',
388
+ og_scrape_url: 'https://linktr.ee/someone',
389
+ title: 'My Linktree',
390
+ text: 'Check out my links',
391
+ image_url: 'https://cdn.example.com/thumb.jpg',
392
+ },
393
+ ],
394
+ })}
395
+ />
396
+ )
397
+
398
+ const link = screen.getByRole('link')
399
+ expect(link).toHaveAttribute('href', 'https://linktr.ee/someone')
400
+ expect(screen.getByText('My Linktree')).toBeInTheDocument()
401
+ expect(screen.getByText('Check out my links')).toBeInTheDocument()
402
+ expect(screen.getByText('https://linktr.ee/someone')).toBeInTheDocument()
403
+ const img = screen.getByRole('img', { name: 'My Linktree' })
404
+ expect(img).toHaveAttribute('src', 'https://cdn.example.com/thumb.jpg')
405
+ })
406
+
407
+ it('renders a link fallback icon when link has no image', () => {
408
+ renderWithProviders(
409
+ <MediaMessage
410
+ message={msg({
411
+ attachments: [
412
+ {
413
+ type: 'link',
414
+ og_scrape_url: 'https://linktr.ee/someone',
415
+ title: 'My Linktree',
416
+ },
417
+ ],
418
+ })}
419
+ />
420
+ )
421
+
422
+ expect(screen.queryByRole('img')).not.toBeInTheDocument()
423
+ expect(screen.getByText('My Linktree')).toBeInTheDocument()
424
+ })
425
+
426
+ it('uses dark card background for sent messages', () => {
427
+ const { container } = renderWithProviders(
428
+ <MediaMessage
429
+ isMyMessage={true}
430
+ message={msg({
431
+ attachments: [
432
+ {
433
+ type: 'image',
434
+ image_url: 'https://cdn.example.com/photo.jpg',
435
+ mime_type: 'image/jpeg',
436
+ },
437
+ ],
438
+ })}
439
+ />
440
+ )
441
+
442
+ expect(container.querySelector('.bg-\\[\\#121110\\]')).toBeInTheDocument()
443
+ })
444
+
445
+ it('uses light card background for received messages', () => {
446
+ const { container } = renderWithProviders(
447
+ <MediaMessage
448
+ isMyMessage={false}
449
+ message={msg({
450
+ attachments: [
451
+ {
452
+ type: 'image',
453
+ image_url: 'https://cdn.example.com/photo.jpg',
454
+ mime_type: 'image/jpeg',
455
+ },
456
+ ],
457
+ })}
458
+ />
459
+ )
460
+
461
+ expect(container.querySelector('.bg-\\[\\#F3F3F1\\]')).toBeInTheDocument()
462
+ })
463
+
464
+ it('shows download button for image attachment', () => {
465
+ renderWithProviders(
466
+ <MediaMessage
467
+ message={msg({
468
+ attachments: [
469
+ {
470
+ type: 'image',
471
+ image_url: 'https://cdn.example.com/photo.jpg',
472
+ mime_type: 'image/jpeg',
473
+ title: 'My Photo',
474
+ },
475
+ ],
476
+ })}
477
+ />
478
+ )
479
+
480
+ expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument()
481
+ })
482
+
483
+ it('shows download button for audio attachment', () => {
484
+ renderWithProviders(
485
+ <MediaMessage
486
+ message={msg({
487
+ attachments: [
488
+ {
489
+ type: 'audio',
490
+ asset_url: 'https://cdn.example.com/track.mp3',
491
+ mime_type: 'audio/mpeg',
492
+ title: 'My Track',
493
+ },
494
+ ],
495
+ })}
496
+ />
497
+ )
498
+
499
+ expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument()
500
+ })
501
+
502
+ it('does not show download or expand buttons for link cards', () => {
503
+ renderWithProviders(
504
+ <MediaMessage
505
+ message={msg({
506
+ attachments: [
507
+ {
508
+ type: 'link',
509
+ og_scrape_url: 'https://linktr.ee/someone',
510
+ title: 'My Linktree',
511
+ },
512
+ ],
513
+ })}
514
+ />
515
+ )
516
+
517
+ expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument()
518
+ expect(screen.queryByRole('button', { name: 'View full screen' })).not.toBeInTheDocument()
519
+ })
261
520
  })