@linktr.ee/messaging-react 4.4.7 → 4.4.8-rc-1788251276

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 (57) hide show
  1. package/dist/{AttachmentCard-DSekwIOA.js → AttachmentCard-BaZk77fe.js} +136 -125
  2. package/dist/AttachmentCard-BaZk77fe.js.map +1 -0
  3. package/dist/AttachmentCard-ZpYg622K.cjs +2 -0
  4. package/dist/AttachmentCard-ZpYg622K.cjs.map +1 -0
  5. package/dist/{Card-BS6Fza2a.cjs → Card-4irOjGaI.cjs} +2 -2
  6. package/dist/{Card-BS6Fza2a.cjs.map → Card-4irOjGaI.cjs.map} +1 -1
  7. package/dist/{Card-BhwBkgxI.js → Card-BdyQhysm.js} +2 -2
  8. package/dist/{Card-BhwBkgxI.js.map → Card-BdyQhysm.js.map} +1 -1
  9. package/dist/index.cjs +1 -1
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.js +915 -907
  12. package/dist/index.js.map +1 -1
  13. package/package.json +3 -3
  14. package/src/components/AttachmentCard/MediaPlayer.test.tsx +9 -5
  15. package/src/components/AttachmentCard/MediaPlayer.tsx +3 -5
  16. package/src/components/AttachmentCard/Thumbnail.test.tsx +4 -0
  17. package/src/components/ChannelActionsMenu/ChannelActionsMenu.test.tsx +18 -10
  18. package/src/components/ChannelActionsMenu/index.tsx +13 -4
  19. package/src/components/ChannelList/ChannelListContext.test.tsx +7 -5
  20. package/src/components/ChannelList/CustomChannelPreview.test.tsx +7 -5
  21. package/src/components/ChannelList/index.test.tsx +1 -0
  22. package/src/components/ChannelView.test.tsx +20 -13
  23. package/src/components/CustomLinkPreviewList/CustomLinkPreviewList.test.tsx +4 -3
  24. package/src/components/CustomMessage/CustomMessage.test.tsx +6 -5
  25. package/src/components/CustomMessage/MessageTag.test.tsx +6 -0
  26. package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +19 -9
  27. package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +12 -5
  28. package/src/components/CustomMessage/TipMessage/TipMessage.test.tsx +3 -0
  29. package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +16 -7
  30. package/src/components/FaqList/FaqList.test.tsx +20 -12
  31. package/src/components/LinkAttachment/LinkAttachment.test.tsx +27 -14
  32. package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +1 -0
  33. package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +6 -6
  34. package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +8 -5
  35. package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +7 -5
  36. package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +3 -8
  37. package/src/components/MessageAttachment/MessageAttachment.behavior.test.tsx +22 -15
  38. package/src/components/MessageAttachment/MessageAttachment.test.tsx +117 -60
  39. package/src/components/MessageAttachment/_shared/Bubble.test.tsx +2 -0
  40. package/src/components/MessageAttachment/_shared/CarouselNav.test.tsx +5 -4
  41. package/src/components/MessageAttachment/_shared/CompactDocumentRow.test.tsx +4 -3
  42. package/src/components/MessageAttachment/_shared/DismissButton.test.tsx +4 -3
  43. package/src/components/MessageAttachment/_shared/DownloadAction.test.tsx +8 -5
  44. package/src/components/MessageAttachment/_shared/DownloadAction.tsx +4 -10
  45. package/src/components/MessageAttachment/_shared/ImageViewer.test.tsx +6 -3
  46. package/src/components/MessageAttachment/_shared/MediaStackGrid.test.tsx +11 -4
  47. package/src/components/MessageAttachment/_shared/PdfViewer.test.tsx +5 -3
  48. package/src/components/MessageAttachment/_shared/VideoViewer.test.tsx +12 -3
  49. package/src/components/MessageAttachment/_shared/ViewerShell.test.tsx +13 -6
  50. package/src/components/MessageAttachment/_shared/useCarousel.test.tsx +10 -4
  51. package/src/components/MessageBubble/MessageBubble.test.tsx +8 -0
  52. package/src/components/RichCard/RichCard.test.tsx +10 -2
  53. package/src/components/_shared/Spinner.test.tsx +75 -0
  54. package/src/components/_shared/Spinner.tsx +51 -0
  55. package/dist/AttachmentCard-BbsjzdLJ.cjs +0 -2
  56. package/dist/AttachmentCard-BbsjzdLJ.cjs.map +0 -1
  57. package/dist/AttachmentCard-DSekwIOA.js.map +0 -1
@@ -6,6 +6,7 @@ import {
6
6
  renderWithProviders,
7
7
  screen,
8
8
  within,
9
+ userEvent,
9
10
  } from '../../test/utils'
10
11
 
11
12
  import { triggerDownload } from './_shared/triggerDownload'
@@ -38,7 +39,8 @@ const expectViewerOpen = (testId: string) => {
38
39
  }
39
40
 
40
41
  describe('MessageAttachment.Image', () => {
41
- it('optimizes Linktree-hosted composer previews without changing the viewer source', () => {
42
+ it('optimizes Linktree-hosted composer previews without changing the viewer source', async () => {
43
+ const user = userEvent.setup()
42
44
  renderWithProviders(
43
45
  <MessageAttachment.Image.Composer
44
46
  src="https://assets.linktr.ee/photo.jpg"
@@ -51,8 +53,9 @@ describe('MessageAttachment.Image', () => {
51
53
  'https://assets.linktr.ee/photo.jpg?io=true&size=messaging-attachment-v1_0'
52
54
  )
53
55
 
54
- fireEvent.click(screen.getByLabelText('Open image'))
56
+ await user.click(screen.getByLabelText('Open image'))
55
57
  const viewer = expectViewerOpen('image-viewer')
58
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
56
59
  expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
57
60
  'https://assets.linktr.ee/photo.jpg'
58
61
  )
@@ -69,12 +72,14 @@ describe('MessageAttachment.Image', () => {
69
72
  const bubble = screen.getByTestId('image-attachment')
70
73
  expect(bubble).toBeInTheDocument()
71
74
  expect(screen.getByText('Here is the image')).toBeInTheDocument()
75
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
72
76
  expect(bubble.querySelector('img')?.getAttribute('src')).toBe(
73
77
  'https://cdn.example.com/photo.jpg'
74
78
  )
75
79
  })
76
80
 
77
- it('opens the ImageViewer lightbox when clicked', () => {
81
+ it('opens the ImageViewer lightbox when clicked', async () => {
82
+ const user = userEvent.setup()
78
83
  renderWithProviders(
79
84
  <MessageAttachment.Image.Received
80
85
  src="https://cdn.example.com/photo.jpg"
@@ -85,7 +90,7 @@ describe('MessageAttachment.Image', () => {
85
90
 
86
91
  expectViewerClosed('image-viewer')
87
92
 
88
- fireEvent.click(screen.getByLabelText('Open image 1 of 1'))
93
+ await user.click(screen.getByLabelText('Open image 1 of 1'))
89
94
 
90
95
  expectViewerOpen('image-viewer')
91
96
  })
@@ -121,7 +126,8 @@ describe('MessageAttachment.Image', () => {
121
126
  expect(screen.getByLabelText('Open image 3 of 3')).toBeInTheDocument()
122
127
  })
123
128
 
124
- it('renders a download button inside the image viewer', () => {
129
+ it('renders a download button inside the image viewer', async () => {
130
+ const user = userEvent.setup()
125
131
  renderWithProviders(
126
132
  <MessageAttachment.Image.Received
127
133
  src="https://cdn.example.com/photo.jpg"
@@ -129,30 +135,33 @@ describe('MessageAttachment.Image', () => {
129
135
  filename="photo.jpg"
130
136
  />
131
137
  )
132
- fireEvent.click(screen.getByLabelText('Open image 1 of 1'))
138
+ await user.click(screen.getByLabelText('Open image 1 of 1'))
133
139
  const viewer = expectViewerOpen('image-viewer')
134
140
  // The viewer chrome carries its own download action — separate
135
141
  // from any sibling download button on the bubble surface — so the
136
142
  // user can grab the image without leaving the lightbox.
137
143
  expect(
144
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
138
145
  viewer.querySelector('button[aria-label="Download photo.jpg"]')
139
146
  ).not.toBeNull()
140
147
  })
141
148
 
142
- it('does not render carousel nav controls for a single image', () => {
149
+ it('does not render carousel nav controls for a single image', async () => {
150
+ const user = userEvent.setup()
143
151
  renderWithProviders(
144
152
  <MessageAttachment.Image.Received
145
153
  src="https://cdn.example.com/photo.jpg"
146
154
  filename="photo.jpg"
147
155
  />
148
156
  )
149
- fireEvent.click(screen.getByLabelText('Open image 1 of 1'))
157
+ await user.click(screen.getByLabelText('Open image 1 of 1'))
150
158
  expectViewerOpen('image-viewer')
151
159
  expect(screen.queryByLabelText('Next image')).toBeNull()
152
160
  expect(screen.queryByLabelText('Previous image')).toBeNull()
153
161
  })
154
162
 
155
- it('renders the stacked images as a carousel inside the viewer', () => {
163
+ it('renders the stacked images as a carousel inside the viewer', async () => {
164
+ const user = userEvent.setup()
156
165
  renderWithProviders(
157
166
  <MessageAttachment.Image.Sent
158
167
  filename="album"
@@ -165,8 +174,9 @@ describe('MessageAttachment.Image', () => {
165
174
  )
166
175
 
167
176
  // Open the second tile — the viewer should land on image 2.
168
- fireEvent.click(screen.getByLabelText('Open image 2 of 3'))
177
+ await user.click(screen.getByLabelText('Open image 2 of 3'))
169
178
  const viewer = expectViewerOpen('image-viewer')
179
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
170
180
  expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
171
181
  'https://cdn.example.com/b.jpg'
172
182
  )
@@ -174,26 +184,30 @@ describe('MessageAttachment.Image', () => {
174
184
  expect(viewer.textContent).toContain('2 / 3')
175
185
 
176
186
  // Next button advances by one.
177
- fireEvent.click(screen.getByLabelText('Next image'))
187
+ await user.click(screen.getByLabelText('Next image'))
188
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
178
189
  expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
179
190
  'https://cdn.example.com/c.jpg'
180
191
  )
181
192
  expect(viewer.textContent).toContain('3 / 3')
182
193
 
183
194
  // Next wraps from the last item back to the first.
184
- fireEvent.click(screen.getByLabelText('Next image'))
195
+ await user.click(screen.getByLabelText('Next image'))
196
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
185
197
  expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
186
198
  'https://cdn.example.com/a.jpg'
187
199
  )
188
200
 
189
201
  // Previous wraps from the first item to the last.
190
- fireEvent.click(screen.getByLabelText('Previous image'))
202
+ await user.click(screen.getByLabelText('Previous image'))
203
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
191
204
  expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
192
205
  'https://cdn.example.com/c.jpg'
193
206
  )
194
207
  })
195
208
 
196
- it('uses ArrowRight / ArrowLeft to navigate stacked images', () => {
209
+ it('uses ArrowRight / ArrowLeft to navigate stacked images', async () => {
210
+ const user = userEvent.setup()
197
211
  renderWithProviders(
198
212
  <MessageAttachment.Image.Sent
199
213
  items={[
@@ -202,18 +216,21 @@ describe('MessageAttachment.Image', () => {
202
216
  ]}
203
217
  />
204
218
  )
205
- fireEvent.click(screen.getByLabelText('Open image 1 of 2'))
219
+ await user.click(screen.getByLabelText('Open image 1 of 2'))
206
220
  const viewer = expectViewerOpen('image-viewer')
221
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
207
222
  expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
208
223
  'https://cdn.example.com/a.jpg'
209
224
  )
210
225
 
211
226
  fireEvent.keyDown(window, { key: 'ArrowRight' })
227
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
212
228
  expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
213
229
  'https://cdn.example.com/b.jpg'
214
230
  )
215
231
 
216
232
  fireEvent.keyDown(window, { key: 'ArrowLeft' })
233
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
217
234
  expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
218
235
  'https://cdn.example.com/a.jpg'
219
236
  )
@@ -242,6 +259,7 @@ describe('MessageAttachment.Image single-image aspect ratio', () => {
242
259
  const WIDTH_CAP_WEB_PX = 326
243
260
 
244
261
  const mediaBox = (label: string) =>
262
+ // eslint-disable-next-line testing-library/no-node-access -- the wrapper class is a deliberate DOM contract with no semantic query.
245
263
  screen.getByLabelText(label).parentElement as HTMLElement
246
264
 
247
265
  /** The `min(var(--…), Npx)` term the height cap contributes. */
@@ -319,6 +337,7 @@ describe('MessageAttachment.Image single-image aspect ratio', () => {
319
337
 
320
338
  // Scoped to the tile: the always-mounted `ImageViewer` renders a
321
339
  // second `<img>` for the same source.
340
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
322
341
  const img = mediaBox('Open image 1 of 1').querySelector(
323
342
  'img'
324
343
  ) as HTMLImageElement
@@ -405,7 +424,8 @@ describe('MessageAttachment.Pdf', () => {
405
424
  expect(screen.getByText('PDF · 379.5 KB')).toBeInTheDocument()
406
425
  })
407
426
 
408
- it('opens the PdfViewer when clicked', () => {
427
+ it('opens the PdfViewer when clicked', async () => {
428
+ const user = userEvent.setup()
409
429
  renderWithProviders(
410
430
  <MessageAttachment.Pdf.Sent
411
431
  src="https://cdn.example.com/doc.pdf"
@@ -413,11 +433,12 @@ describe('MessageAttachment.Pdf', () => {
413
433
  />
414
434
  )
415
435
  expectViewerClosed('pdf-viewer')
416
- fireEvent.click(screen.getByLabelText('Open doc.pdf'))
436
+ await user.click(screen.getByLabelText('Open doc.pdf'))
417
437
  expectViewerOpen('pdf-viewer')
418
438
  })
419
439
 
420
- it('forwards the onClick handler before opening the viewer', () => {
440
+ it('forwards the onClick handler before opening the viewer', async () => {
441
+ const user = userEvent.setup()
421
442
  const onClick = vi.fn()
422
443
  renderWithProviders(
423
444
  <MessageAttachment.Pdf.Received
@@ -426,7 +447,7 @@ describe('MessageAttachment.Pdf', () => {
426
447
  onClick={onClick}
427
448
  />
428
449
  )
429
- fireEvent.click(screen.getByLabelText('Open doc.pdf'))
450
+ await user.click(screen.getByLabelText('Open doc.pdf'))
430
451
  expect(onClick).toHaveBeenCalledTimes(1)
431
452
  })
432
453
 
@@ -457,7 +478,8 @@ describe('MessageAttachment.Pdf', () => {
457
478
  expect(screen.getByLabelText('Open c.pdf')).toBeInTheDocument()
458
479
  })
459
480
 
460
- it('opens the viewer on the activated row in a stacked PDF bubble', () => {
481
+ it('opens the viewer on the activated row in a stacked PDF bubble', async () => {
482
+ const user = userEvent.setup()
461
483
  renderWithProviders(
462
484
  <MessageAttachment.Pdf.Received
463
485
  items={[
@@ -467,7 +489,7 @@ describe('MessageAttachment.Pdf', () => {
467
489
  />
468
490
  )
469
491
  expectViewerClosed('pdf-viewer')
470
- fireEvent.click(screen.getByLabelText('Open b.pdf'))
492
+ await user.click(screen.getByLabelText('Open b.pdf'))
471
493
  const viewer = expectViewerOpen('pdf-viewer')
472
494
  // The active filename surfaces as the dialog's accessible name —
473
495
  // the lightbox chrome is intentionally just close + carousel chrome
@@ -479,7 +501,8 @@ describe('MessageAttachment.Pdf', () => {
479
501
  expect(viewer.textContent).toContain('2 / 2')
480
502
  })
481
503
 
482
- it('navigates between stacked PDFs via the carousel controls', () => {
504
+ it('navigates between stacked PDFs via the carousel controls', async () => {
505
+ const user = userEvent.setup()
483
506
  renderWithProviders(
484
507
  <MessageAttachment.Pdf.Received
485
508
  items={[
@@ -489,18 +512,18 @@ describe('MessageAttachment.Pdf', () => {
489
512
  ]}
490
513
  />
491
514
  )
492
- fireEvent.click(screen.getByLabelText('Open a.pdf'))
515
+ await user.click(screen.getByLabelText('Open a.pdf'))
493
516
  const viewer = expectViewerOpen('pdf-viewer')
494
517
  expect(viewer.getAttribute('aria-label')).toBe('a.pdf')
495
518
 
496
- fireEvent.click(screen.getByLabelText('Next document'))
519
+ await user.click(screen.getByLabelText('Next document'))
497
520
  expect(viewer.getAttribute('aria-label')).toBe('b.pdf')
498
521
 
499
- fireEvent.click(screen.getByLabelText('Next document'))
522
+ await user.click(screen.getByLabelText('Next document'))
500
523
  expect(viewer.getAttribute('aria-label')).toBe('c.pdf')
501
524
 
502
525
  // Wraps back to the start.
503
- fireEvent.click(screen.getByLabelText('Next document'))
526
+ await user.click(screen.getByLabelText('Next document'))
504
527
  expect(viewer.getAttribute('aria-label')).toBe('a.pdf')
505
528
  })
506
529
 
@@ -537,7 +560,8 @@ describe('MessageAttachment.Pdf', () => {
537
560
  expect(screen.getByLabelText('Download c.pdf')).toBeInTheDocument()
538
561
  })
539
562
 
540
- it('does not open the viewer when the Download button is clicked', () => {
563
+ it('does not open the viewer when the Download button is clicked', async () => {
564
+ const user = userEvent.setup()
541
565
  renderWithProviders(
542
566
  <MessageAttachment.Pdf.Sent
543
567
  src="https://cdn.example.com/notes.pdf"
@@ -545,7 +569,7 @@ describe('MessageAttachment.Pdf', () => {
545
569
  />
546
570
  )
547
571
  expectViewerClosed('pdf-viewer')
548
- fireEvent.click(screen.getByLabelText('Download notes.pdf'))
572
+ await user.click(screen.getByLabelText('Download notes.pdf'))
549
573
  // Download should fire on its own without bubbling up to the row
550
574
  // activation that opens the viewer.
551
575
  expectViewerClosed('pdf-viewer')
@@ -563,7 +587,8 @@ describe('MessageAttachment.Pdf', () => {
563
587
  expect(screen.getByLabelText('Dismiss attachment')).toBeInTheDocument()
564
588
  })
565
589
 
566
- it('does not crash when items shrinks after the viewer opens (P1 regression)', () => {
590
+ it('does not crash when items shrinks after the viewer opens (P1 regression)', async () => {
591
+ const user = userEvent.setup()
567
592
  // Regression test for the PR #151 P1 reviewer finding:
568
593
  // `viewerIndex` was dereferenced directly against `resolvedItems`,
569
594
  // so a re-render with fewer items than the open index would crash
@@ -578,7 +603,7 @@ describe('MessageAttachment.Pdf', () => {
578
603
  ]}
579
604
  />
580
605
  )
581
- fireEvent.click(screen.getByLabelText('Open c.pdf'))
606
+ await user.click(screen.getByLabelText('Open c.pdf'))
582
607
  expectViewerOpen('pdf-viewer')
583
608
 
584
609
  expect(() =>
@@ -599,7 +624,8 @@ describe('MessageAttachment.Pdf', () => {
599
624
  )
600
625
  })
601
626
 
602
- it('does not open the viewer when onClick returns false', () => {
627
+ it('does not open the viewer when onClick returns false', async () => {
628
+ const user = userEvent.setup()
603
629
  const onClick = vi.fn(() => false as const)
604
630
  renderWithProviders(
605
631
  <MessageAttachment.Pdf.Received
@@ -608,7 +634,7 @@ describe('MessageAttachment.Pdf', () => {
608
634
  onClick={onClick}
609
635
  />
610
636
  )
611
- fireEvent.click(screen.getByLabelText('Open doc.pdf'))
637
+ await user.click(screen.getByLabelText('Open doc.pdf'))
612
638
  expect(onClick).toHaveBeenCalledTimes(1)
613
639
  expectViewerClosed('pdf-viewer')
614
640
  })
@@ -669,7 +695,8 @@ describe('MessageAttachment.File', () => {
669
695
  expect(screen.getByLabelText('Download c.pdf')).toBeInTheDocument()
670
696
  })
671
697
 
672
- it('downloads the file when the row is clicked and onClick allows it', () => {
698
+ it('downloads the file when the row is clicked and onClick allows it', async () => {
699
+ const user = userEvent.setup()
673
700
  const onClick = vi.fn()
674
701
  renderWithProviders(
675
702
  <MessageAttachment.File.Received
@@ -678,7 +705,7 @@ describe('MessageAttachment.File', () => {
678
705
  onClick={onClick}
679
706
  />
680
707
  )
681
- fireEvent.click(screen.getByLabelText('Download workout-plan.zip'))
708
+ await user.click(screen.getByLabelText('Download workout-plan.zip'))
682
709
  expect(onClick).toHaveBeenCalledWith(0)
683
710
  expect(mockTriggerDownload).toHaveBeenCalledTimes(1)
684
711
  expect(mockTriggerDownload).toHaveBeenCalledWith(
@@ -687,7 +714,8 @@ describe('MessageAttachment.File', () => {
687
714
  )
688
715
  })
689
716
 
690
- it('lets onClick cancel the automatic download by returning false (P2 regression)', () => {
717
+ it('lets onClick cancel the automatic download by returning false (P2 regression)', async () => {
718
+ const user = userEvent.setup()
691
719
  // Regression test for the PR #151 P2 reviewer finding:
692
720
  // the docs advertised `onClick` as a hook for intercepting the
693
721
  // download (e.g. confirmation modal), but the handler used to fire
@@ -701,12 +729,13 @@ describe('MessageAttachment.File', () => {
701
729
  onClick={onClick}
702
730
  />
703
731
  )
704
- fireEvent.click(screen.getByLabelText('Download workout-plan.zip'))
732
+ await user.click(screen.getByLabelText('Download workout-plan.zip'))
705
733
  expect(onClick).toHaveBeenCalledTimes(1)
706
734
  expect(mockTriggerDownload).not.toHaveBeenCalled()
707
735
  })
708
736
 
709
- it('still downloads when onClick returns void', () => {
737
+ it('still downloads when onClick returns void', async () => {
738
+ const user = userEvent.setup()
710
739
  // Belt-and-braces: void / undefined returns must not be confused
711
740
  // with `false` (the only cancellation sentinel).
712
741
  const onClick = vi.fn(() => undefined)
@@ -717,7 +746,7 @@ describe('MessageAttachment.File', () => {
717
746
  onClick={onClick}
718
747
  />
719
748
  )
720
- fireEvent.click(screen.getByLabelText('Download workout-plan.zip'))
749
+ await user.click(screen.getByLabelText('Download workout-plan.zip'))
721
750
  expect(mockTriggerDownload).toHaveBeenCalledTimes(1)
722
751
  })
723
752
  })
@@ -732,6 +761,7 @@ describe('MessageAttachment.Audio', () => {
732
761
  />
733
762
  )
734
763
  const bubble = screen.getByTestId('audio-attachment')
764
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
735
765
  expect(bubble.querySelector('audio')).not.toBeNull()
736
766
  // No filename title — the native `<audio>` element doesn't expose
737
767
  // a title slot, so we render just the player.
@@ -741,7 +771,8 @@ describe('MessageAttachment.Audio', () => {
741
771
  expect(screen.queryByRole('button', { name: /download/i })).toBeNull()
742
772
  })
743
773
 
744
- it('shows an inline dismiss button next to the player on Composer state', () => {
774
+ it('shows an inline dismiss button next to the player on Composer state', async () => {
775
+ const user = userEvent.setup()
745
776
  const onDismiss = vi.fn()
746
777
  renderWithProviders(
747
778
  <MessageAttachment.Audio.Composer
@@ -751,7 +782,7 @@ describe('MessageAttachment.Audio', () => {
751
782
  )
752
783
  const dismiss = screen.getByLabelText('Dismiss attachment')
753
784
  expect(dismiss).toBeInTheDocument()
754
- fireEvent.click(dismiss)
785
+ await user.click(dismiss)
755
786
  expect(onDismiss).toHaveBeenCalledTimes(1)
756
787
  })
757
788
 
@@ -766,12 +797,14 @@ describe('MessageAttachment.Audio', () => {
766
797
  />
767
798
  )
768
799
  const bubble = screen.getByTestId('audio-attachment')
800
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
769
801
  expect(bubble.querySelectorAll('audio')).toHaveLength(3)
770
802
  })
771
803
  })
772
804
 
773
805
  describe('MessageAttachment.Video', () => {
774
- it('opens the VideoViewer when clicked', () => {
806
+ it('opens the VideoViewer when clicked', async () => {
807
+ const user = userEvent.setup()
775
808
  renderWithProviders(
776
809
  <MessageAttachment.Video.Received
777
810
  src="https://cdn.example.com/clip.mp4"
@@ -781,13 +814,14 @@ describe('MessageAttachment.Video', () => {
781
814
  />
782
815
  )
783
816
  expectViewerClosed('video-viewer')
784
- fireEvent.click(screen.getByLabelText('Play video 1 of 1'))
817
+ await user.click(screen.getByLabelText('Play video 1 of 1'))
785
818
  expectViewerOpen('video-viewer')
786
819
  // Single-item viewer has no carousel chrome.
787
820
  expect(screen.queryByLabelText('Next video')).toBeNull()
788
821
  })
789
822
 
790
- it('navigates between stacked videos via the carousel controls', () => {
823
+ it('navigates between stacked videos via the carousel controls', async () => {
824
+ const user = userEvent.setup()
791
825
  renderWithProviders(
792
826
  <MessageAttachment.Video.Sent
793
827
  filename="clip"
@@ -805,13 +839,15 @@ describe('MessageAttachment.Video', () => {
805
839
  ]}
806
840
  />
807
841
  )
808
- fireEvent.click(screen.getByLabelText('Play video 1 of 2'))
842
+ await user.click(screen.getByLabelText('Play video 1 of 2'))
809
843
  const viewer = expectViewerOpen('video-viewer')
844
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
810
845
  expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
811
846
  'https://cdn.example.com/a.mp4'
812
847
  )
813
848
 
814
- fireEvent.click(screen.getByLabelText('Next video'))
849
+ await user.click(screen.getByLabelText('Next video'))
850
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
815
851
  expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
816
852
  'https://cdn.example.com/b.mp4'
817
853
  )
@@ -841,11 +877,13 @@ describe('MessageAttachment.Video overflow', () => {
841
877
  expect(screen.queryByLabelText('Play video 5 of 9')).toBeNull()
842
878
  })
843
879
 
844
- it('opens the viewer on the fourth video from the overflow tile and pages to the ninth', () => {
880
+ it('opens the viewer on the fourth video from the overflow tile and pages to the ninth', async () => {
881
+ const user = userEvent.setup()
845
882
  renderWithProviders(<MessageAttachment.Video.Sent items={NINE_VIDEOS} />)
846
883
 
847
- fireEvent.click(screen.getByLabelText('Play video 4 of 9'))
884
+ await user.click(screen.getByLabelText('Play video 4 of 9'))
848
885
  const viewer = expectViewerOpen('video-viewer')
886
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
849
887
  expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
850
888
  'https://cdn.example.com/3.mp4'
851
889
  )
@@ -854,8 +892,9 @@ describe('MessageAttachment.Video overflow', () => {
854
892
 
855
893
  // The carousel spans all nine, not just the four the grid drew.
856
894
  for (let i = 0; i < 5; i += 1) {
857
- fireEvent.click(screen.getByLabelText('Next video'))
895
+ await user.click(screen.getByLabelText('Next video'))
858
896
  }
897
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
859
898
  expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
860
899
  'https://cdn.example.com/8.mp4'
861
900
  )
@@ -867,7 +906,9 @@ describe('MessageAttachment.Video overflow', () => {
867
906
 
868
907
  const tiles = screen.getAllByRole('button', { name: /^Play video/ })
869
908
  // The badge is the only `svg` a poster tile renders.
909
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
870
910
  expect(tiles[0].querySelector('svg')).not.toBeNull()
911
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
871
912
  expect(tiles[3].querySelector('svg')).toBeNull()
872
913
  })
873
914
  })
@@ -878,6 +919,7 @@ describe('MessageAttachment.Video single-card aspect', () => {
878
919
  // jsdom never decodes an `<img>`, so `naturalWidth` stays 0 and `load`
879
920
  // never fires on its own — stub the intrinsic size and fire it.
880
921
  const loadPosterAt = (naturalWidth: number, naturalHeight: number) => {
922
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
881
923
  const poster = screen.getByTestId('video-attachment').querySelector('img')
882
924
  if (!poster) throw new Error('no poster rendered')
883
925
  Object.defineProperty(poster, 'naturalWidth', {
@@ -892,6 +934,7 @@ describe('MessageAttachment.Video single-card aspect', () => {
892
934
  }
893
935
 
894
936
  const mediaAspect = () =>
937
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
895
938
  screen.getByTestId('video-attachment').querySelector<HTMLElement>('[style]')
896
939
  ?.style.aspectRatio
897
940
 
@@ -1054,23 +1097,25 @@ describe('MessageAttachment.Video single-card aspect', () => {
1054
1097
  })
1055
1098
 
1056
1099
  describe('ViewerShell focus management', () => {
1057
- it('moves focus into the dialog when a viewer opens', () => {
1100
+ it('moves focus into the dialog when a viewer opens', async () => {
1101
+ const user = userEvent.setup()
1058
1102
  renderWithProviders(
1059
1103
  <MessageAttachment.Pdf.Received
1060
1104
  src="https://cdn.example.com/doc.pdf"
1061
1105
  filename="doc.pdf"
1062
1106
  />
1063
1107
  )
1064
- fireEvent.click(screen.getByLabelText('Open doc.pdf'))
1108
+ await user.click(screen.getByLabelText('Open doc.pdf'))
1065
1109
 
1066
1110
  // The close button is the primary focus target in the viewer
1067
1111
  // toolbar — keyboard users should land on it on open so Escape
1068
1112
  // / Tab work without an extra click.
1069
1113
  const close = screen.getByLabelText('Close viewer')
1070
- expect(document.activeElement).toBe(close)
1114
+ expect(close).toHaveFocus()
1071
1115
  })
1072
1116
 
1073
- it('restores focus to the opener when the viewer closes', () => {
1117
+ it('restores focus to the opener when the viewer closes', async () => {
1118
+ const user = userEvent.setup()
1074
1119
  renderWithProviders(
1075
1120
  <MessageAttachment.Pdf.Received
1076
1121
  src="https://cdn.example.com/doc.pdf"
@@ -1079,18 +1124,18 @@ describe('ViewerShell focus management', () => {
1079
1124
  )
1080
1125
  const opener = screen.getByLabelText('Open doc.pdf')
1081
1126
  opener.focus()
1082
- expect(document.activeElement).toBe(opener)
1127
+ expect(opener).toHaveFocus()
1083
1128
 
1084
- fireEvent.click(opener)
1129
+ await user.click(opener)
1085
1130
  expectViewerOpen('pdf-viewer')
1086
1131
 
1087
- fireEvent.click(screen.getByLabelText('Close viewer'))
1132
+ await user.click(screen.getByLabelText('Close viewer'))
1088
1133
 
1089
1134
  // After close, focus should hop back to whatever was focused
1090
1135
  // before the viewer mounted (the row's Open button), and the
1091
1136
  // dialog should no longer carry the `[open]` attribute.
1092
1137
  expectViewerClosed('pdf-viewer')
1093
- expect(document.activeElement).toBe(opener)
1138
+ expect(opener).toHaveFocus()
1094
1139
  })
1095
1140
  })
1096
1141
 
@@ -1103,6 +1148,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
1103
1148
  alt="Photo"
1104
1149
  />
1105
1150
  )
1151
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
1106
1152
  const img = screen.getByTestId('image-attachment').querySelector('img')
1107
1153
  expect(img?.getAttribute('loading')).toBe('lazy')
1108
1154
  expect(img?.getAttribute('decoding')).toBe('async')
@@ -1116,6 +1162,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
1116
1162
  loading="eager"
1117
1163
  />
1118
1164
  )
1165
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
1119
1166
  const img = screen.getByTestId('image-attachment').querySelector('img')
1120
1167
  expect(img?.getAttribute('loading')).toBe('eager')
1121
1168
  expect(img?.getAttribute('decoding')).toBe('async')
@@ -1135,13 +1182,15 @@ describe('MessageAttachment lazy-loading defaults', () => {
1135
1182
  />
1136
1183
  )
1137
1184
  const [first, second] = Array.from(
1185
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
1138
1186
  screen.getByTestId('image-attachment').querySelectorAll('img')
1139
1187
  )
1140
1188
  expect(first?.getAttribute('loading')).toBe('eager')
1141
1189
  expect(second?.getAttribute('loading')).toBe('lazy')
1142
1190
  })
1143
1191
 
1144
- it('does not mount the viewer `<img>` while the lightbox is closed', () => {
1192
+ it('does not mount the viewer `<img>` while the lightbox is closed', async () => {
1193
+ const user = userEvent.setup()
1145
1194
  renderWithProviders(
1146
1195
  <MessageAttachment.Image.Sent
1147
1196
  src="https://cdn.example.com/photo.jpg"
@@ -1159,7 +1208,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
1159
1208
  })
1160
1209
  ).toHaveLength(0)
1161
1210
 
1162
- fireEvent.click(screen.getByLabelText('Open image 1 of 1'))
1211
+ await user.click(screen.getByLabelText('Open image 1 of 1'))
1163
1212
  expect(
1164
1213
  within(screen.getByTestId('image-viewer')).getByRole('img', {
1165
1214
  hidden: true,
@@ -1177,6 +1226,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
1177
1226
  mimeType="video/mp4"
1178
1227
  />
1179
1228
  )
1229
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
1180
1230
  const poster = screen.getByTestId('video-attachment').querySelector('img')
1181
1231
  expect(poster?.getAttribute('src')).toBe(
1182
1232
  'https://cdn.example.com/poster.jpg'
@@ -1185,7 +1235,8 @@ describe('MessageAttachment lazy-loading defaults', () => {
1185
1235
  expect(poster?.getAttribute('decoding')).toBe('async')
1186
1236
  })
1187
1237
 
1188
- it('opens the viewer with `<video preload="metadata">` for the active item', () => {
1238
+ it('opens the viewer with `<video preload="metadata">` for the active item', async () => {
1239
+ const user = userEvent.setup()
1189
1240
  renderWithProviders(
1190
1241
  <MessageAttachment.Video.Sent
1191
1242
  src="https://cdn.example.com/clip.mp4"
@@ -1194,7 +1245,8 @@ describe('MessageAttachment lazy-loading defaults', () => {
1194
1245
  filename="clip.mp4"
1195
1246
  />
1196
1247
  )
1197
- fireEvent.click(screen.getByLabelText('Play video 1 of 1'))
1248
+ await user.click(screen.getByLabelText('Play video 1 of 1'))
1249
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
1198
1250
  const video = screen.getByTestId('video-viewer').querySelector('video')
1199
1251
  expect(video?.getAttribute('preload')).toBe('metadata')
1200
1252
  })
@@ -1213,7 +1265,9 @@ describe('MessageAttachment lazy-loading defaults', () => {
1213
1265
  />
1214
1266
  )
1215
1267
  const bubble = screen.getByTestId('video-attachment')
1268
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
1216
1269
  expect(bubble.querySelectorAll('video')).toHaveLength(0)
1270
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
1217
1271
  expect(bubble.querySelectorAll('img')).toHaveLength(4)
1218
1272
  })
1219
1273
  })
@@ -1228,6 +1282,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
1228
1282
  )
1229
1283
  const audio = screen
1230
1284
  .getByTestId('audio-attachment')
1285
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
1231
1286
  .querySelector('audio')
1232
1287
  expect(audio?.getAttribute('preload')).toBe('metadata')
1233
1288
  })
@@ -1243,6 +1298,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
1243
1298
  />
1244
1299
  )
1245
1300
  const audios = Array.from(
1301
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
1246
1302
  screen.getByTestId('audio-attachment').querySelectorAll('audio')
1247
1303
  )
1248
1304
  expect(audios).toHaveLength(3)
@@ -1265,6 +1321,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
1265
1321
  />
1266
1322
  )
1267
1323
  const audios = Array.from(
1324
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
1268
1325
  screen.getByTestId('audio-attachment').querySelectorAll('audio')
1269
1326
  )
1270
1327
  expect(audios[0]?.getAttribute('preload')).toBe('none')
@@ -18,7 +18,9 @@ describe('Bubble', () => {
18
18
  </Bubble>
19
19
  )
20
20
  const bubble = screen.getByTestId('bubble')
21
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
21
22
  if (present) expect(bubble.querySelector('p')).not.toBeNull()
23
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
22
24
  else expect(bubble.querySelector('p')).toBeNull()
23
25
  expect(bubble).toHaveClass(present ? 'pb-0' : 'py-2')
24
26
  })