@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
@@ -2,7 +2,12 @@ import React from 'react'
2
2
  import { useChannelStateContext, useMessageContext } from 'stream-chat-react'
3
3
  import { beforeEach, describe, expect, it, vi } from 'vitest'
4
4
 
5
- import { renderWithProviders, screen, within } from '../../test/utils'
5
+ import {
6
+ renderWithProviders,
7
+ screen,
8
+ userEvent,
9
+ within,
10
+ } from '../../test/utils'
6
11
 
7
12
  import { SentMessageDeliveryStatus } from './SentMessageDeliveryStatus'
8
13
 
@@ -232,9 +237,11 @@ describe('SentMessageDeliveryStatus', () => {
232
237
  name: 'View broadcast Broadcast',
233
238
  })
234
239
  expect(
240
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
235
241
  button.querySelector('.sent-message-outbound-badge')
236
242
  ).toBeInTheDocument()
237
243
  expect(
244
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
238
245
  button.querySelector('.sent-message-outbound-name')
239
246
  ).toHaveTextContent('Broadcast')
240
247
  expect(button).not.toHaveTextContent('•')
@@ -275,6 +282,7 @@ describe('SentMessageDeliveryStatus', () => {
275
282
  expect(button).not.toHaveTextContent('•')
276
283
  expect(name).toHaveClass('sent-message-outbound-name')
277
284
  expect(
285
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
278
286
  button.querySelector('.sent-message-outbound-badge')
279
287
  ).toHaveAttribute('aria-hidden', 'true')
280
288
  expect(screen.getAllByTestId('sent-message-delivery-status')).toHaveLength(
@@ -282,7 +290,8 @@ describe('SentMessageDeliveryStatus', () => {
282
290
  )
283
291
  })
284
292
 
285
- it('passes the resolved outbound name to the click callback', () => {
293
+ it('passes the resolved outbound name to the click callback', async () => {
294
+ const user = userEvent.setup()
286
295
  const onOutboundClick = vi.fn()
287
296
  mockedUseMessageContext.mockReturnValue({
288
297
  message: {
@@ -305,11 +314,11 @@ describe('SentMessageDeliveryStatus', () => {
305
314
  />
306
315
  )
307
316
 
308
- screen
309
- .getByRole('button', {
317
+ await user.click(
318
+ screen.getByRole('button', {
310
319
  name: 'View broadcast July 9th Class absentee',
311
320
  })
312
- .click()
321
+ )
313
322
 
314
323
  expect(onOutboundClick).toHaveBeenCalledWith({
315
324
  id: 'automation-1',
@@ -317,7 +326,8 @@ describe('SentMessageDeliveryStatus', () => {
317
326
  })
318
327
  })
319
328
 
320
- it('reports a campaign named Broadcast on click', () => {
329
+ it('reports a campaign named Broadcast on click', async () => {
330
+ const user = userEvent.setup()
321
331
  const onOutboundClick = vi.fn()
322
332
  mockedUseMessageContext.mockReturnValue({
323
333
  message: {
@@ -337,11 +347,11 @@ describe('SentMessageDeliveryStatus', () => {
337
347
  />
338
348
  )
339
349
 
340
- screen
341
- .getByRole('button', {
350
+ await user.click(
351
+ screen.getByRole('button', {
342
352
  name: 'View broadcast Broadcast',
343
353
  })
344
- .click()
354
+ )
345
355
 
346
356
  expect(onOutboundClick).toHaveBeenCalledWith({
347
357
  id: 'automation-1',
@@ -3,10 +3,10 @@ import type { Attachment, LocalMessage } from 'stream-chat'
3
3
  import { describe, expect, it } from 'vitest'
4
4
 
5
5
  import {
6
- fireEvent,
7
6
  renderWithProviders,
8
7
  screen,
9
8
  within,
9
+ userEvent,
10
10
  } from '../../test/utils'
11
11
 
12
12
  import StreamAttachmentMessage, {
@@ -198,11 +198,17 @@ describe('StreamAttachmentMessage', () => {
198
198
  )
199
199
 
200
200
  const link = screen.getByRole('link', { name: /my linktree/i })
201
+ const linkCard = screen.getByTestId('link-card')
201
202
  const imageBubble = screen.getByTestId('image-attachment')
202
203
 
203
- expect(link.compareDocumentPosition(imageBubble)).toBe(
204
- Node.DOCUMENT_POSITION_FOLLOWING
204
+ const attachmentRows = within(screen.getByTestId('message-attachment-rows'))
205
+ const attachments = attachmentRows.getAllByTestId(
206
+ /link-card|image-attachment/
205
207
  )
208
+ expect(attachments.indexOf(linkCard)).toBeLessThan(
209
+ attachments.indexOf(imageBubble)
210
+ )
211
+ expect(link).toBeInTheDocument()
206
212
  expect(screen.getByTestId('message-caption-bubble')).toHaveTextContent(
207
213
  'caption'
208
214
  )
@@ -257,7 +263,8 @@ describe('StreamAttachmentMessage', () => {
257
263
  )
258
264
  })
259
265
 
260
- it('keeps the original image source in the viewer for channel image attachments', () => {
266
+ it('keeps the original image source in the viewer for channel image attachments', async () => {
267
+ const user = userEvent.setup()
261
268
  renderWithProviders(
262
269
  <StreamAttachmentMessage
263
270
  groupPosition="single"
@@ -272,7 +279,7 @@ describe('StreamAttachmentMessage', () => {
272
279
  />
273
280
  )
274
281
 
275
- fireEvent.click(screen.getByLabelText('Open image 1 of 1'))
282
+ await user.click(screen.getByLabelText('Open image 1 of 1'))
276
283
  const viewer = screen.getByTestId('image-viewer')
277
284
 
278
285
  expect(viewer).toHaveAttribute('open')
@@ -34,6 +34,7 @@ describe('TipMessage', () => {
34
34
  )
35
35
 
36
36
  const tipTag = screen.getByText('$5.50 tip!')
37
+ // eslint-disable-next-line testing-library/no-node-access -- the component wrapper class has no semantic query.
37
38
  expect(tipTag.closest('.message-tip-pill')).not.toBeNull()
38
39
  })
39
40
 
@@ -48,6 +49,7 @@ describe('TipMessage', () => {
48
49
  )
49
50
 
50
51
  const tipTag = screen.getByText('$50.00 tip!')
52
+ // eslint-disable-next-line testing-library/no-node-access -- the component wrapper class has no semantic query.
51
53
  expect(tipTag.closest('.message-tip-pill')).not.toBeNull()
52
54
  })
53
55
 
@@ -60,6 +62,7 @@ describe('TipMessage', () => {
60
62
  />
61
63
  )
62
64
 
65
+ // eslint-disable-next-line testing-library/no-node-access -- the null-rendering DOM contract has no semantic query.
63
66
  expect(container.firstChild).toBeNull()
64
67
  })
65
68
  })
@@ -1,7 +1,13 @@
1
1
  import React from 'react'
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3
3
 
4
- import { fireEvent, renderWithProviders, screen } from '../../test/utils'
4
+ import {
5
+ userEvent,
6
+ fireEvent,
7
+ renderWithProviders,
8
+ screen,
9
+ within,
10
+ } from '../../test/utils'
5
11
 
6
12
  import { CustomMessageInput } from '.'
7
13
 
@@ -207,7 +213,8 @@ describe('CustomMessageInput', () => {
207
213
  expect(notCancelled).toBe(false)
208
214
  })
209
215
 
210
- it('still submits on click', () => {
216
+ it('still submits on click', async () => {
217
+ const user = userEvent.setup()
211
218
  mockChannelData = {}
212
219
  mockContextSendButton = undefined
213
220
  mockHasSendableData = true
@@ -215,7 +222,7 @@ describe('CustomMessageInput', () => {
215
222
  renderWithProviders(<CustomMessageInput />)
216
223
 
217
224
  const sendButton = screen.getByTestId('send-button')
218
- fireEvent.click(sendButton)
225
+ await user.click(sendButton)
219
226
  expect(mockHandleSubmit).toHaveBeenCalledTimes(1)
220
227
  })
221
228
  })
@@ -285,10 +292,12 @@ describe('CustomMessageInput', () => {
285
292
  const messageInput = screen.getByTestId('message-input')
286
293
 
287
294
  expect(screen.getByTestId('composer-chrome')).toContainElement(header)
288
- expect(
289
- header.compareDocumentPosition(messageInput) &
290
- Node.DOCUMENT_POSITION_FOLLOWING
291
- ).toBeTruthy()
295
+ const chromeChildren = within(
296
+ screen.getByTestId('composer-chrome')
297
+ ).getAllByTestId(/composer-header|message-input/)
298
+ expect(chromeChildren.indexOf(header)).toBeLessThan(
299
+ chromeChildren.indexOf(messageInput)
300
+ )
292
301
  })
293
302
 
294
303
  it('keeps the frozen wrapper around a custom composerInput', () => {
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
 
4
- import { fireEvent, renderWithProviders, screen } from '../../test/utils'
4
+ import { userEvent, renderWithProviders, screen } from '../../test/utils'
5
5
  import { getAvatarEmoji } from '../Avatar/getAvatarEmoji'
6
6
 
7
7
  import { FaqListItem } from './FaqListItem'
@@ -70,7 +70,8 @@ describe('FaqListItem', () => {
70
70
  ).toHaveStyle({ backgroundColor: '#E6E5E3' })
71
71
  })
72
72
 
73
- it('uses loading-only classes and blocks clicks while loading', () => {
73
+ it('uses loading-only classes and blocks clicks while loading', async () => {
74
+ const user = userEvent.setup()
74
75
  const onClick = vi.fn()
75
76
  renderWithProviders(
76
77
  <FaqListItem question="How do I get started?" onClick={onClick} loading />
@@ -86,17 +87,18 @@ describe('FaqListItem', () => {
86
87
  'cursor-not-allowed',
87
88
  ])
88
89
 
89
- fireEvent.click(button)
90
+ await user.click(button)
90
91
  expect(onClick).not.toHaveBeenCalled()
91
92
  })
92
93
 
93
- it('calls the click handler once when enabled', () => {
94
+ it('calls the click handler once when enabled', async () => {
95
+ const user = userEvent.setup()
94
96
  const onClick = vi.fn()
95
97
  renderWithProviders(
96
98
  <FaqListItem question="How do I get started?" onClick={onClick} />
97
99
  )
98
100
 
99
- fireEvent.click(screen.getByRole('button'))
101
+ await user.click(screen.getByRole('button'))
100
102
  expect(onClick).toHaveBeenCalledTimes(1)
101
103
  })
102
104
  })
@@ -110,16 +112,20 @@ describe('FaqList', () => {
110
112
  />
111
113
  )
112
114
 
115
+ // eslint-disable-next-line testing-library/no-node-access -- the null-rendering DOM contract has no semantic query.
113
116
  expect(container.firstChild).toBeNull()
114
117
  })
115
118
 
116
119
  it('renders enabled FAQs in ascending order and filters disabled FAQs', () => {
117
120
  renderWithProviders(<FaqList faqs={faqs} onFaqClick={vi.fn()} />)
118
121
 
119
- expect(
120
- screen.getByRole('button', { name: 'How do I get started?' })
121
- .parentElement
122
- ).toHaveStyle({ backgroundColor: '#F1F0EE' })
122
+ const firstFaq = screen.getByRole('button', {
123
+ name: 'How do I get started?',
124
+ })
125
+ // eslint-disable-next-line testing-library/no-node-access -- the wrapper class is a deliberate DOM contract with no semantic query.
126
+ expect(firstFaq.parentElement).toHaveStyle({
127
+ backgroundColor: '#F1F0EE',
128
+ })
123
129
  expect(
124
130
  screen.getAllByRole('button').map((button) => button.textContent)
125
131
  ).toEqual(['How do I get started?', 'What payment methods do you accept?'])
@@ -155,6 +161,7 @@ describe('FaqList', () => {
155
161
  />
156
162
  )
157
163
 
164
+ // eslint-disable-next-line testing-library/no-node-access -- the component shell wrapper has no semantic query.
158
165
  expect(container.firstElementChild).toHaveClass('faq-list-custom-class')
159
166
  })
160
167
 
@@ -180,7 +187,8 @@ describe('FaqList', () => {
180
187
  expect(screen.queryAllByTestId('avatar-ring')).toHaveLength(1)
181
188
  })
182
189
 
183
- it('passes the clicked FAQ id and marks only its matching row as loading', () => {
190
+ it('passes the clicked FAQ id and marks only its matching row as loading', async () => {
191
+ const user = userEvent.setup()
184
192
  const onFaqClick = vi.fn()
185
193
  renderWithProviders(
186
194
  <FaqList faqs={faqs} onFaqClick={onFaqClick} loadingFaqId="second" />
@@ -197,8 +205,8 @@ describe('FaqList', () => {
197
205
  expect(firstButton).not.toHaveClass('opacity-50', 'cursor-not-allowed')
198
206
  expect(secondButton).toHaveClass('opacity-50', 'cursor-not-allowed')
199
207
 
200
- fireEvent.click(firstButton)
201
- fireEvent.click(secondButton)
208
+ await user.click(firstButton)
209
+ await user.click(secondButton)
202
210
  expect(onFaqClick).toHaveBeenCalledTimes(1)
203
211
  expect(onFaqClick).toHaveBeenCalledWith('first')
204
212
  })
@@ -1,7 +1,12 @@
1
1
  import React from 'react'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
 
4
- import { renderWithProviders, screen, fireEvent } from '../../test/utils'
4
+ import {
5
+ userEvent,
6
+ renderWithProviders,
7
+ screen,
8
+ fireEvent,
9
+ } from '../../test/utils'
5
10
 
6
11
  import { AUDIO_BG_CLASS } from './components/_shared/CardThumbnail'
7
12
 
@@ -28,6 +33,7 @@ describe('LinkAttachment.Received (link previews)', () => {
28
33
  })
29
34
 
30
35
  it('renders as a button when no URL is set and onClick is provided', async () => {
36
+ const user = userEvent.setup()
31
37
  const handleClick = vi.fn()
32
38
  renderWithProviders(
33
39
  <LinkAttachment.Received
@@ -38,11 +44,12 @@ describe('LinkAttachment.Received (link previews)', () => {
38
44
  )
39
45
  const root = await screen.findByTestId('link-attachment')
40
46
  expect(root.tagName).toBe('BUTTON')
41
- fireEvent.click(root)
47
+ await user.click(root)
42
48
  expect(handleClick).toHaveBeenCalledTimes(1)
43
49
  })
44
50
 
45
51
  it('routes the card-level onClick through the CTA when one is set', async () => {
52
+ const user = userEvent.setup()
46
53
  const onClick = vi.fn()
47
54
  const ctaOnClick = vi.fn()
48
55
  renderWithProviders(
@@ -53,7 +60,7 @@ describe('LinkAttachment.Received (link previews)', () => {
53
60
  onClick={onClick}
54
61
  />
55
62
  )
56
- fireEvent.click(await screen.findByText('View FAQs'))
63
+ await user.click(await screen.findByText('View FAQs'))
57
64
  expect(onClick).toHaveBeenCalledTimes(1)
58
65
  expect(ctaOnClick).toHaveBeenCalledTimes(1)
59
66
  })
@@ -93,6 +100,7 @@ describe('LinkAttachment.Received (link previews)', () => {
93
100
 
94
101
  describe('LinkAttachment.Sent', () => {
95
102
  it('renders the dark variant as an external link when url is set', async () => {
103
+ const user = userEvent.setup()
96
104
  const handleClick = vi.fn()
97
105
  renderWithProviders(
98
106
  <LinkAttachment.Sent
@@ -107,7 +115,7 @@ describe('LinkAttachment.Sent', () => {
107
115
  expect(link.getAttribute('href')).toBe('https://tr.ee/briemix')
108
116
  expect(link.getAttribute('target')).toBe('_blank')
109
117
  expect(link.getAttribute('rel')).toBe('noopener noreferrer')
110
- fireEvent.click(link)
118
+ await user.click(link)
111
119
  expect(handleClick).toHaveBeenCalledTimes(1)
112
120
  })
113
121
 
@@ -480,7 +488,8 @@ const HERO = 'https://cdn.example.com/yoga.jpg'
480
488
  const shellOf = () => screen.getByTestId(/^link-(card|attachment)$/)
481
489
 
482
490
  describe('LinkAttachment.Composer chrome', () => {
483
- it('anchors the dismiss control to the hero on a featured card', () => {
491
+ it('anchors the dismiss control to the hero on a featured card', async () => {
492
+ const user = userEvent.setup()
484
493
  const onDismiss = vi.fn()
485
494
  renderWithProviders(
486
495
  <LinkAttachment.Composer
@@ -496,7 +505,7 @@ describe('LinkAttachment.Composer chrome', () => {
496
505
  expect(dismiss.className).toContain('before:-inset-[10px]')
497
506
  // Hover lifts the fill so the control is not identical to its rest state.
498
507
  expect(dismiss.className).toContain('hover:bg-[#2a2928]')
499
- fireEvent.click(dismiss)
508
+ await user.click(dismiss)
500
509
  expect(onDismiss).toHaveBeenCalledTimes(1)
501
510
  })
502
511
 
@@ -563,12 +572,13 @@ describe('LinkAttachment.Composer chrome', () => {
563
572
  expect(screen.queryByLabelText('Dismiss attachment')).toBeNull()
564
573
  })
565
574
 
566
- it('renders the edit affordance only when a handler is given', () => {
575
+ it('renders the edit affordance only when a handler is given', async () => {
576
+ const user = userEvent.setup()
567
577
  const onEditClick = vi.fn()
568
578
  renderWithProviders(
569
579
  <LinkAttachment.Composer title="Brie’s FAQ" onEditClick={onEditClick} />
570
580
  )
571
- fireEvent.click(screen.getByLabelText('Edit attachment'))
581
+ await user.click(screen.getByLabelText('Edit attachment'))
572
582
  expect(onEditClick).toHaveBeenCalledTimes(1)
573
583
  expect(screen.queryByLabelText('Dismiss attachment')).toBeNull()
574
584
  })
@@ -580,7 +590,8 @@ describe('LinkAttachment.Composer chrome', () => {
580
590
  })
581
591
 
582
592
  describe('LinkAttachment interactivity contract', () => {
583
- it('keeps the Received shell non-interactive around playable media', () => {
593
+ it('keeps the Received shell non-interactive around playable media', async () => {
594
+ const user = userEvent.setup()
584
595
  const onClick = vi.fn()
585
596
  renderWithProviders(
586
597
  <LinkAttachment.Received
@@ -594,7 +605,7 @@ describe('LinkAttachment interactivity contract', () => {
594
605
  // The native controls own clicks, so the shell is neither anchor nor button.
595
606
  const root = screen.getByTestId('link-attachment')
596
607
  expect(root.tagName).toBe('DIV')
597
- fireEvent.click(root)
608
+ await user.click(root)
598
609
  expect(onClick).not.toHaveBeenCalled()
599
610
  })
600
611
 
@@ -642,18 +653,20 @@ describe('LinkAttachment interactivity contract', () => {
642
653
  )
643
654
  })
644
655
 
645
- it('ignores the Sent onClick when there is nothing to open', () => {
656
+ it('ignores the Sent onClick when there is nothing to open', async () => {
657
+ const user = userEvent.setup()
646
658
  const onClick = vi.fn()
647
659
  renderWithProviders(
648
660
  <LinkAttachment.Sent title="My Playlist" onClick={onClick} />
649
661
  )
650
662
  const shell = shellOf()
651
663
  expect(shell.tagName).toBe('DIV')
652
- fireEvent.click(shell)
664
+ await user.click(shell)
653
665
  expect(onClick).not.toHaveBeenCalled()
654
666
  })
655
667
 
656
- it('ignores the Sent onClick around playable media', () => {
668
+ it('ignores the Sent onClick around playable media', async () => {
669
+ const user = userEvent.setup()
657
670
  const onClick = vi.fn()
658
671
  renderWithProviders(
659
672
  <LinkAttachment.Sent
@@ -666,7 +679,7 @@ describe('LinkAttachment interactivity contract', () => {
666
679
  )
667
680
  const shell = shellOf()
668
681
  expect(shell.tagName).toBe('DIV')
669
- fireEvent.click(shell)
682
+ await user.click(shell)
670
683
  expect(onClick).not.toHaveBeenCalled()
671
684
  })
672
685
  })
@@ -27,6 +27,7 @@ describe('BubbleTail', () => {
27
27
  expect(tail.parentElement?.className).toBe('relative isolate w-fit')
28
28
  expect(tail.getAttribute('aria-hidden')).toBe('true')
29
29
  // The tail follows the card so it paints over the card's bottom edge.
30
+ // eslint-disable-next-line testing-library/no-node-access -- sibling placement is the deliberate visual DOM contract.
30
31
  expect(screen.getByText('card').nextElementSibling).toBe(tail)
31
32
  })
32
33
 
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
 
4
- import { renderWithProviders, screen, fireEvent } from '../../../../test/utils'
4
+ import { userEvent, renderWithProviders, screen } from '../../../../test/utils'
5
5
 
6
6
  import CardBody from './CardBody'
7
7
 
@@ -154,9 +154,8 @@ describe('CardBody', () => {
154
154
  )
155
155
  const icon = screen.getByTestId('app-icon')
156
156
  const title = screen.getByText('Spotify')
157
- expect(
158
- icon.compareDocumentPosition(title) & Node.DOCUMENT_POSITION_FOLLOWING
159
- ).toBeTruthy()
157
+ const content = screen.getAllByText(/^(icon|Spotify)$/)
158
+ expect(content.indexOf(icon)).toBeLessThan(content.indexOf(title))
160
159
  expect(screen.getByText('Edit')).toBeInTheDocument()
161
160
  })
162
161
 
@@ -192,7 +191,8 @@ describe('CardBody', () => {
192
191
  expect(container).not.toBeEmptyDOMElement()
193
192
  })
194
193
 
195
- it('keeps the cta interactive alongside the chin text', () => {
194
+ it('keeps the cta interactive alongside the chin text', async () => {
195
+ const user = userEvent.setup()
196
196
  const onClick = vi.fn()
197
197
  renderWithProviders(
198
198
  <CardBody
@@ -201,7 +201,7 @@ describe('CardBody', () => {
201
201
  cta={{ label: 'View FAQs', onClick }}
202
202
  />
203
203
  )
204
- fireEvent.click(screen.getByText('View FAQs'))
204
+ await user.click(screen.getByText('View FAQs'))
205
205
  expect(onClick).toHaveBeenCalledTimes(1)
206
206
  })
207
207
  })
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
 
4
- import { renderWithProviders, screen, fireEvent } from '../../../../test/utils'
4
+ import { userEvent, renderWithProviders, screen } from '../../../../test/utils'
5
5
 
6
6
  import CardCta from './CardCta'
7
7
 
@@ -37,7 +37,8 @@ describe('CardCta', () => {
37
37
  ['button', undefined],
38
38
  ])(
39
39
  'fires the cta callback from the %s without bubbling to the card',
40
- (_shape, href) => {
40
+ async (_shape, href) => {
41
+ const user = userEvent.setup()
41
42
  const ctaOnClick = vi.fn()
42
43
  const cardOnClick = vi.fn()
43
44
  renderWithProviders(
@@ -49,7 +50,7 @@ describe('CardCta', () => {
49
50
  />
50
51
  </div>
51
52
  )
52
- fireEvent.click(screen.getByText('Act'))
53
+ await user.click(screen.getByText('Act'))
53
54
  expect(ctaOnClick).toHaveBeenCalledTimes(1)
54
55
  expect(cardOnClick).not.toHaveBeenCalled()
55
56
  }
@@ -58,7 +59,8 @@ describe('CardCta', () => {
58
59
  it.each([
59
60
  ['anchor', 'tr.ee/faq'],
60
61
  ['button', undefined],
61
- ])('tolerates a %s cta with no callback', (_shape, href) => {
62
+ ])('tolerates a %s cta with no callback', async (_shape, href) => {
63
+ const user = userEvent.setup()
62
64
  const cardOnClick = vi.fn()
63
65
  renderWithProviders(
64
66
  // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- stands in for the card anchor the CTA must not bubble to
@@ -66,7 +68,7 @@ describe('CardCta', () => {
66
68
  <CardCta variant="light" cta={{ label: 'Act', href }} />
67
69
  </div>
68
70
  )
69
- fireEvent.click(screen.getByText('Act'))
71
+ await user.click(screen.getByText('Act'))
70
72
  expect(cardOnClick).not.toHaveBeenCalled()
71
73
  })
72
74
 
@@ -88,6 +90,7 @@ describe('CardCta', () => {
88
90
  it('renders the label verbatim, without a wrapper span, by default', () => {
89
91
  renderWithProviders(<CardCta variant="light" cta={{ label: 'Act' }} />)
90
92
  const cta = screen.getByText('Act')
93
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
91
94
  expect(cta.querySelector('span')).toBeNull()
92
95
  })
93
96
 
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
 
4
- import { renderWithProviders, screen, fireEvent } from '../../../../test/utils'
4
+ import { userEvent, renderWithProviders, screen } from '../../../../test/utils'
5
5
 
6
6
  import CardShell, { LOADING_LABEL } from './CardShell'
7
7
 
@@ -20,7 +20,8 @@ describe('CardShell', () => {
20
20
  expect(shell.getAttribute('data-variant')).toBe('light')
21
21
  })
22
22
 
23
- it('renders an external anchor when href is set and fires onClick alongside navigation', () => {
23
+ it('renders an external anchor when href is set and fires onClick alongside navigation', async () => {
24
+ const user = userEvent.setup()
24
25
  const onClick = vi.fn()
25
26
  renderWithProviders(
26
27
  <CardShell
@@ -38,11 +39,12 @@ describe('CardShell', () => {
38
39
  expect(shell.getAttribute('target')).toBe('_blank')
39
40
  expect(shell.getAttribute('rel')).toBe('noopener noreferrer')
40
41
  expect(shell.className).toContain('focus-ring')
41
- fireEvent.click(shell)
42
+ await user.click(shell)
42
43
  expect(onClick).toHaveBeenCalledTimes(1)
43
44
  })
44
45
 
45
- it('renders a labelled button when onClick is set without href', () => {
46
+ it('renders a labelled button when onClick is set without href', async () => {
47
+ const user = userEvent.setup()
46
48
  const onClick = vi.fn()
47
49
  renderWithProviders(
48
50
  <CardShell
@@ -58,7 +60,7 @@ describe('CardShell', () => {
58
60
  expect(shell.tagName).toBe('BUTTON')
59
61
  expect(shell.getAttribute('type')).toBe('button')
60
62
  expect(shell.getAttribute('aria-label')).toBe('Open attachment preview')
61
- fireEvent.click(shell)
63
+ await user.click(shell)
62
64
  expect(onClick).toHaveBeenCalledTimes(1)
63
65
  })
64
66
 
@@ -1,12 +1,7 @@
1
1
  import React from 'react'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
3
 
4
- import {
5
- cleanup,
6
- renderWithProviders,
7
- screen,
8
- fireEvent,
9
- } from '../../../../test/utils'
4
+ import { renderWithProviders, screen, fireEvent } from '../../../../test/utils'
10
5
 
11
6
  import CardThumbnail, {
12
7
  AUDIO_BG_CLASS,
@@ -207,7 +202,7 @@ describe('CardThumbnail', () => {
207
202
  })
208
203
 
209
204
  it('pins the corner slots and omits their wrappers when unused', () => {
210
- renderWithProviders(
205
+ const { unmount } = renderWithProviders(
211
206
  <CardThumbnail
212
207
  variant="dark"
213
208
  topLeft={<span>left</span>}
@@ -223,7 +218,7 @@ describe('CardThumbnail', () => {
223
218
  expect(topRight.className).toContain('absolute right-3 top-3')
224
219
  /* eslint-enable local/no-style-only-assertion */
225
220
 
226
- cleanup()
221
+ unmount()
227
222
  renderWithProviders(<CardThumbnail variant="dark" />)
228
223
  expect(
229
224
  screen.queryByTestId('card-thumbnail-top-left')