@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "4.4.7",
3
+ "version": "4.4.8-rc-1788251276",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -50,8 +50,8 @@
50
50
  "chromatic:local": "yarn storybook:build && chromatic"
51
51
  },
52
52
  "dependencies": {
53
- "@linktr.ee/messaging-core": "^2.6.0",
54
- "@linktr.ee/messaging-taxonomy": "^0.11.0",
53
+ "@linktr.ee/messaging-core": "2.6.0-rc-1788251276",
54
+ "@linktr.ee/messaging-taxonomy": "0.11.0-rc-1788251276",
55
55
  "@phosphor-icons/react": "^2.1.10"
56
56
  },
57
57
  "devDependencies": {
@@ -6,6 +6,7 @@ import {
6
6
  renderWithProviders,
7
7
  screen,
8
8
  waitFor,
9
+ userEvent,
9
10
  } from '../../test/utils'
10
11
 
11
12
  import MediaPlayer from './MediaPlayer'
@@ -119,6 +120,7 @@ describe('MediaPlayer', () => {
119
120
  })
120
121
 
121
122
  it('shows a manual play overlay after rejected playback and starts from a gesture', async () => {
123
+ const user = userEvent.setup()
122
124
  const play = vi.mocked(HTMLMediaElement.prototype.play)
123
125
  play.mockRejectedValueOnce(new Error('autoplay blocked'))
124
126
  const onContainerClick = vi.fn()
@@ -135,22 +137,23 @@ describe('MediaPlayer', () => {
135
137
 
136
138
  const overlay = await screen.findByRole('button', { name: 'Play preview' })
137
139
  expect(play).toHaveBeenCalledTimes(1)
138
- fireEvent.click(overlay)
140
+ await user.click(overlay)
139
141
  await waitFor(() => expect(play).toHaveBeenCalledTimes(2))
140
142
  expect(onContainerClick).not.toHaveBeenCalled()
141
143
  expect(screen.queryByRole('button', { name: 'Play preview' })).toBeNull()
142
144
  })
143
145
 
144
146
  it('toggles from the container only with controls, while an override owns the click', async () => {
147
+ const user = userEvent.setup()
145
148
  const play = vi.mocked(HTMLMediaElement.prototype.play)
146
149
  const { rerender } = renderWithProviders(
147
150
  <MediaPlayer source={SOURCE} mimeType="video/mp4" controls />
148
151
  )
149
152
  const root = screen.getByTestId('media-player')
150
153
 
151
- fireEvent.click(root)
154
+ await user.click(root)
152
155
  await screen.findByRole('button', { name: 'Pause' })
153
- fireEvent.click(root)
156
+ await user.click(root)
154
157
  await screen.findByRole('button', { name: 'Play' })
155
158
  expect(play).toHaveBeenCalledTimes(1)
156
159
 
@@ -163,7 +166,7 @@ describe('MediaPlayer', () => {
163
166
  onContainerClick={onContainerClick}
164
167
  />
165
168
  )
166
- fireEvent.click(root)
169
+ await user.click(root)
167
170
  expect(onContainerClick).toHaveBeenCalledTimes(1)
168
171
  expect(screen.getByRole('button', { name: 'Play' })).toBeInTheDocument()
169
172
  fireEvent.keyDown(root, { key: 'Enter' })
@@ -206,11 +209,12 @@ describe('MediaPlayer', () => {
206
209
  })
207
210
 
208
211
  it('does not let the controls button bubble into the container toggle', async () => {
212
+ const user = userEvent.setup()
209
213
  renderWithProviders(
210
214
  <MediaPlayer source={SOURCE} mimeType="video/mp4" controls />
211
215
  )
212
216
 
213
- fireEvent.click(screen.getByRole('button', { name: 'Play' }))
217
+ await user.click(screen.getByRole('button', { name: 'Play' }))
214
218
  await screen.findByRole('button', { name: 'Pause' })
215
219
  })
216
220
 
@@ -1,8 +1,9 @@
1
- import { CircleNotchIcon, PauseIcon, PlayIcon } from '@phosphor-icons/react'
1
+ import { PauseIcon, PlayIcon } from '@phosphor-icons/react'
2
2
  import React, { useCallback, useEffect, useRef, useState } from 'react'
3
3
 
4
4
  import { isDevBuild } from '../../utils/isDevBuild'
5
5
  import { nativePlayerProps } from '../../utils/nativeMediaPlayers'
6
+ import Spinner from '../_shared/Spinner'
6
7
 
7
8
  import { renderTypeIcon } from './utils/icons'
8
9
  import { getSourceType } from './utils/mimeType'
@@ -308,10 +309,7 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
308
309
  aria-label="Loading media"
309
310
  className="absolute inset-0 z-10 flex items-center justify-center"
310
311
  >
311
- <CircleNotchIcon
312
- className="size-8 animate-spin text-white/80"
313
- weight="bold"
314
- />
312
+ <Spinner size="lg" tone="on-media" />
315
313
  </div>
316
314
  )}
317
315
 
@@ -91,6 +91,7 @@ describe('AttachmentThumbnail', () => {
91
91
  expect(image).toHaveClass('opacity-0')
92
92
  fireEvent.load(image)
93
93
  expect(image).toHaveClass('opacity-100')
94
+ // eslint-disable-next-line testing-library/no-node-access -- the component shell wrapper has no semantic query.
94
95
  expect(container.firstElementChild).toHaveClass('aspect-video')
95
96
  })
96
97
 
@@ -126,6 +127,7 @@ describe('AttachmentThumbnail', () => {
126
127
  expect(contained).toHaveClass('opacity-0')
127
128
  fireEvent.load(contained)
128
129
  expect(contained).toHaveClass('opacity-100')
130
+ // eslint-disable-next-line testing-library/no-node-access -- the component shell wrapper has no semantic query.
129
131
  expect(container.firstElementChild).toHaveClass('aspect-video')
130
132
 
131
133
  rerender(
@@ -170,6 +172,7 @@ describe('AttachmentThumbnail', () => {
170
172
  )
171
173
  const darkImage = screen.getByRole('img', { name: 'Poster' })
172
174
  expect(darkImage).toHaveAttribute('src', OPTIMIZED_THUMBNAIL)
175
+ // eslint-disable-next-line testing-library/no-node-access -- the component shell wrapper has no semantic query.
173
176
  expect(view.container.firstElementChild).toHaveClass(
174
177
  'aspect-video',
175
178
  'bg-white/10'
@@ -186,6 +189,7 @@ describe('AttachmentThumbnail', () => {
186
189
  />
187
190
  )
188
191
  expect(screen.getByRole('img')).toHaveAttribute('alt', '')
192
+ // eslint-disable-next-line testing-library/no-node-access -- the component shell wrapper has no semantic query.
189
193
  expect(view.container.firstElementChild).toHaveClass(
190
194
  'aspect-video',
191
195
  'bg-black/5'
@@ -86,8 +86,10 @@ const defaultProps = () => ({
86
86
  participant: createParticipant(),
87
87
  })
88
88
 
89
- const openMenu = async () =>
90
- userEvent.click(screen.getByRole('button', { name: 'More options' }))
89
+ const openMenu = async () => {
90
+ const user = userEvent.setup()
91
+ await user.click(screen.getByRole('button', { name: 'More options' }))
92
+ }
91
93
 
92
94
  describe('ChannelActionsMenu', () => {
93
95
  beforeEach(() => {
@@ -125,12 +127,13 @@ describe('ChannelActionsMenu', () => {
125
127
  })
126
128
 
127
129
  it('closes the popover when Escape is pressed', async () => {
130
+ const user = userEvent.setup()
128
131
  renderWithProviders(<ChannelActionsMenu {...defaultProps()} />)
129
132
 
130
133
  await openMenu()
131
134
  expect(screen.getByText('Report')).toBeInTheDocument()
132
135
 
133
- await userEvent.keyboard('{Escape}')
136
+ await user.keyboard('{Escape}')
134
137
 
135
138
  await waitFor(() => {
136
139
  expect(screen.queryByText('Report')).not.toBeInTheDocument()
@@ -138,6 +141,7 @@ describe('ChannelActionsMenu', () => {
138
141
  })
139
142
 
140
143
  it('closes the popover when clicking outside', async () => {
144
+ const user = userEvent.setup()
141
145
  renderWithProviders(
142
146
  <div>
143
147
  <span data-testid="outside">outside</span>
@@ -148,7 +152,7 @@ describe('ChannelActionsMenu', () => {
148
152
  await openMenu()
149
153
  expect(screen.getByText('Report')).toBeInTheDocument()
150
154
 
151
- await userEvent.click(screen.getByTestId('outside'))
155
+ await user.click(screen.getByTestId('outside'))
152
156
 
153
157
  await waitFor(() => {
154
158
  expect(screen.queryByText('Report')).not.toBeInTheDocument()
@@ -156,6 +160,7 @@ describe('ChannelActionsMenu', () => {
156
160
  })
157
161
 
158
162
  it('calls onDeleteConversationClick and closes after deleting', async () => {
163
+ const user = userEvent.setup()
159
164
  const onDeleteConversationClick = vi.fn()
160
165
  renderWithProviders(
161
166
  <ChannelActionsMenu
@@ -165,7 +170,7 @@ describe('ChannelActionsMenu', () => {
165
170
  )
166
171
 
167
172
  await openMenu()
168
- await userEvent.click(screen.getByText('Delete Conversation'))
173
+ await user.click(screen.getByText('Delete Conversation'))
169
174
 
170
175
  await waitFor(() => {
171
176
  expect(onDeleteConversationClick).toHaveBeenCalledOnce()
@@ -176,6 +181,7 @@ describe('ChannelActionsMenu', () => {
176
181
  })
177
182
 
178
183
  it('calls onBlockParticipantClick and blocks the user', async () => {
184
+ const user = userEvent.setup()
179
185
  const onBlockParticipantClick = vi.fn()
180
186
  renderWithProviders(
181
187
  <ChannelActionsMenu
@@ -185,7 +191,7 @@ describe('ChannelActionsMenu', () => {
185
191
  )
186
192
 
187
193
  await openMenu()
188
- await userEvent.click(screen.getByText('Block'))
194
+ await user.click(screen.getByText('Block'))
189
195
 
190
196
  await waitFor(() => {
191
197
  expect(onBlockParticipantClick).toHaveBeenCalledOnce()
@@ -206,6 +212,7 @@ describe('ChannelActionsMenu', () => {
206
212
  })
207
213
 
208
214
  it('disables the block action until the blocked-status lookup resolves', async () => {
215
+ const user = userEvent.setup()
209
216
  let resolveBlockedUsers: (
210
217
  value: Array<{ blocked_user_id: string }>
211
218
  ) => void = () => {}
@@ -221,9 +228,9 @@ describe('ChannelActionsMenu', () => {
221
228
 
222
229
  // While the lookup is pending the action is disabled so a premature click
223
230
  // can't act on a stale block state.
224
- const pendingBlock = screen.getByText('Block').closest('button')
231
+ const pendingBlock = screen.getByRole('button', { name: 'Block' })
225
232
  expect(pendingBlock).toBeDisabled()
226
- await userEvent.click(pendingBlock as HTMLElement)
233
+ await user.click(pendingBlock)
227
234
  expect(blockUserMock).not.toHaveBeenCalled()
228
235
 
229
236
  // Once the lookup resolves (already blocked) the Unblock action is offered.
@@ -247,7 +254,7 @@ describe('ChannelActionsMenu', () => {
247
254
  // The lookup rejected, but the action recovers — Block becomes actionable
248
255
  // rather than staying stuck in the disabled spinner state.
249
256
  await waitFor(() => {
250
- const block = screen.getByText('Block').closest('button')
257
+ const block = screen.getByRole('button', { name: 'Block' })
251
258
  expect(block).not.toBeDisabled()
252
259
  })
253
260
 
@@ -255,6 +262,7 @@ describe('ChannelActionsMenu', () => {
255
262
  })
256
263
 
257
264
  it('calls onReportParticipantClick and opens the report page', async () => {
265
+ const user = userEvent.setup()
258
266
  const onReportParticipantClick = vi.fn()
259
267
  const windowOpenSpy = vi
260
268
  .spyOn(window, 'open')
@@ -267,7 +275,7 @@ describe('ChannelActionsMenu', () => {
267
275
  )
268
276
 
269
277
  await openMenu()
270
- await userEvent.click(screen.getByText('Report'))
278
+ await user.click(screen.getByText('Report'))
271
279
 
272
280
  await waitFor(() => {
273
281
  expect(onReportParticipantClick).toHaveBeenCalledOnce()
@@ -10,6 +10,7 @@ import React, { useCallback, useEffect, useId, useRef, useState } from 'react'
10
10
  import type { Channel as ChannelType, ChannelMemberResponse } from 'stream-chat'
11
11
 
12
12
  import { useChannelModerationActions } from '../../hooks/useChannelModerationActions'
13
+ import Spinner from '../_shared/Spinner'
13
14
  import ActionButton from '../ActionButton'
14
15
 
15
16
  export interface ChannelActionsMenuProps {
@@ -156,7 +157,7 @@ export const ChannelActionsMenu: React.FC<ChannelActionsMenuProps> = ({
156
157
  aria-busy={isLeaving}
157
158
  >
158
159
  {isLeaving ? (
159
- <SpinnerGapIcon className="h-5 w-5 animate-spin" />
160
+ <Spinner size="md" icon={SpinnerGapIcon} weight="regular" />
160
161
  ) : (
161
162
  <SignOutIcon className="h-5 w-5" />
162
163
  )}
@@ -170,7 +171,7 @@ export const ChannelActionsMenu: React.FC<ChannelActionsMenuProps> = ({
170
171
  // Block status not yet resolved — show a disabled placeholder
171
172
  // so a premature click can't act on a stale block state.
172
173
  <ActionButton disabled aria-busy>
173
- <SpinnerGapIcon className="h-5 w-5 animate-spin" />
174
+ <Spinner size="md" icon={SpinnerGapIcon} weight="regular" />
174
175
  <span>Block</span>
175
176
  </ActionButton>
176
177
  ) : isParticipantBlocked ? (
@@ -180,7 +181,11 @@ export const ChannelActionsMenu: React.FC<ChannelActionsMenuProps> = ({
180
181
  aria-busy={isUpdatingBlockStatus}
181
182
  >
182
183
  {isUpdatingBlockStatus ? (
183
- <SpinnerGapIcon className="h-5 w-5 animate-spin" />
184
+ <Spinner
185
+ size="md"
186
+ icon={SpinnerGapIcon}
187
+ weight="regular"
188
+ />
184
189
  ) : (
185
190
  <ProhibitInsetIcon className="h-5 w-5" />
186
191
  )}
@@ -193,7 +198,11 @@ export const ChannelActionsMenu: React.FC<ChannelActionsMenuProps> = ({
193
198
  aria-busy={isUpdatingBlockStatus}
194
199
  >
195
200
  {isUpdatingBlockStatus ? (
196
- <SpinnerGapIcon className="h-5 w-5 animate-spin" />
201
+ <Spinner
202
+ size="md"
203
+ icon={SpinnerGapIcon}
204
+ weight="regular"
205
+ />
197
206
  ) : (
198
207
  <ProhibitInsetIcon className="h-5 w-5" />
199
208
  )}
@@ -1,7 +1,7 @@
1
1
  import type { Channel } from 'stream-chat'
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
 
6
6
  import {
7
7
  ChannelListProvider,
@@ -36,7 +36,8 @@ function ContextConsumer() {
36
36
  }
37
37
 
38
38
  describe('ChannelListContext', () => {
39
- it('provides safe documented defaults without a provider', () => {
39
+ it('provides safe documented defaults without a provider', async () => {
40
+ const user = userEvent.setup()
40
41
  renderWithProviders(<ContextConsumer />)
41
42
 
42
43
  expect(screen.getByTestId('selected-channel')).toHaveTextContent(
@@ -48,10 +49,11 @@ describe('ChannelListContext', () => {
48
49
  )
49
50
  expect(screen.getByTestId('channel-preview')).toHaveTextContent('undefined')
50
51
  expect(screen.getByTestId('viewer-language')).toHaveTextContent('undefined')
51
- expect(() => fireEvent.click(screen.getByRole('button'))).not.toThrow()
52
+ await user.click(screen.getByRole('button'))
52
53
  })
53
54
 
54
- it('exposes the value supplied by a provider', () => {
55
+ it('exposes the value supplied by a provider', async () => {
56
+ const user = userEvent.setup()
55
57
  const onChannelSelect = vi.fn()
56
58
  const ChannelPreview = () => null
57
59
  const renderMessagePreview = () => 'preview'
@@ -81,7 +83,7 @@ describe('ChannelListContext', () => {
81
83
  expect(screen.getByTestId('channel-preview')).toHaveTextContent('provided')
82
84
  expect(screen.getByTestId('viewer-language')).toHaveTextContent('es')
83
85
 
84
- fireEvent.click(screen.getByRole('button'))
86
+ await user.click(screen.getByRole('button'))
85
87
  expect(onChannelSelect).toHaveBeenCalledWith(channel)
86
88
  })
87
89
  })
@@ -3,7 +3,7 @@ import { act, createEvent, fireEvent } from '@testing-library/react'
3
3
  import { Channel, LocalMessage, StreamChat } from 'stream-chat'
4
4
  import { beforeEach, describe, expect, it, vi } from 'vitest'
5
5
 
6
- import { renderWithProviders, screen } from '../../test/utils'
6
+ import { userEvent, renderWithProviders, screen } from '../../test/utils'
7
7
  import type { ChannelPreviewProps } from '../../types'
8
8
 
9
9
  import { ChannelListProvider } from './ChannelListContext'
@@ -136,7 +136,8 @@ describe('CustomChannelPreview', () => {
136
136
  expect(screen.getByRole('button')).toHaveClass('hover:bg-black/[0.02]')
137
137
  })
138
138
 
139
- it('selects the channel by click and activation keys only', () => {
139
+ it('selects the channel by click and activation keys only', async () => {
140
+ const user = userEvent.setup()
140
141
  const channel = createMockChannel([])
141
142
  const onChannelSelect = vi.fn()
142
143
  renderWithProviders(
@@ -146,7 +147,7 @@ describe('CustomChannelPreview', () => {
146
147
  )
147
148
  const row = screen.getByRole('button')
148
149
 
149
- fireEvent.click(row)
150
+ await user.click(row)
150
151
  fireEvent.keyDown(row, { key: 'Enter' })
151
152
  const spaceEvent = createEvent.keyDown(row, { key: ' ' })
152
153
  fireEvent(row, spaceEvent)
@@ -782,7 +783,8 @@ describe('CustomChannelPreview', () => {
782
783
  ).toBeInTheDocument()
783
784
  })
784
785
 
785
- it('renders a custom channel preview with selection props from context', () => {
786
+ it('renders a custom channel preview with selection props from context', async () => {
787
+ const user = userEvent.setup()
786
788
  const channel = createMockChannel([])
787
789
  const onChannelSelect = vi.fn()
788
790
  const ChannelPreview = vi.fn(
@@ -818,7 +820,7 @@ describe('CustomChannelPreview', () => {
818
820
  </ChannelListProvider>
819
821
  )
820
822
 
821
- fireEvent.click(
823
+ await user.click(
822
824
  screen.getByRole('button', { name: 'Selected channel-1 es active' })
823
825
  )
824
826
 
@@ -318,6 +318,7 @@ describe('ChannelList', () => {
318
318
  />
319
319
  )
320
320
 
321
+ // eslint-disable-next-line testing-library/no-node-access -- the component shell wrapper has no semantic query.
321
322
  expect(container.firstElementChild).toHaveClass(
322
323
  'messaging-channel-list',
323
324
  'h-full',
@@ -3,7 +3,7 @@ import React from 'react'
3
3
  import type { Channel } from 'stream-chat'
4
4
  import { describe, expect, it, vi, beforeEach } from 'vitest'
5
5
 
6
- import { renderWithProviders, screen, within } from '../test/utils'
6
+ import { renderWithProviders, screen, userEvent, within } from '../test/utils'
7
7
 
8
8
  import { ChannelView } from './ChannelView'
9
9
 
@@ -266,7 +266,8 @@ describe('ChannelView', () => {
266
266
  expect(screen.getByTestId('channel-actions-menu')).toBeInTheDocument()
267
267
  })
268
268
 
269
- it('renders the redesigned header button when onParticipantNameClick is provided', () => {
269
+ it('renders the redesigned header button when onParticipantNameClick is provided', async () => {
270
+ const user = userEvent.setup()
270
271
  const onParticipantNameClick = vi.fn()
271
272
  renderWithProviders(
272
273
  <ChannelView
@@ -284,7 +285,7 @@ describe('ChannelView', () => {
284
285
  expect(button).toContainElement(avatars[0])
285
286
  expect(screen.getByTestId('channel-actions-menu')).toBeInTheDocument()
286
287
 
287
- button.click()
288
+ await user.click(button)
288
289
  expect(onParticipantNameClick).toHaveBeenCalledTimes(1)
289
290
  })
290
291
 
@@ -357,7 +358,8 @@ describe('ChannelView', () => {
357
358
  )
358
359
  })
359
360
 
360
- it('keeps message rows mounted while updating the outbound click handler', () => {
361
+ it('keeps message rows mounted while updating the outbound click handler', async () => {
362
+ const user = userEvent.setup()
361
363
  const firstHandler = vi.fn()
362
364
  const secondHandler = vi.fn()
363
365
  const { rerender: rerenderChannel } = renderWithProviders(
@@ -387,7 +389,7 @@ describe('ChannelView', () => {
387
389
  expect(customMessageMounts).toHaveBeenCalledOnce()
388
390
  expect(customMessageUnmounts).not.toHaveBeenCalled()
389
391
 
390
- screen.getByTestId('custom-message').click()
392
+ await user.click(screen.getByTestId('custom-message'))
391
393
  expect(firstHandler).not.toHaveBeenCalled()
392
394
  expect(secondHandler).toHaveBeenCalledWith({
393
395
  id: 'automation-1',
@@ -443,14 +445,15 @@ describe('ChannelView', () => {
443
445
 
444
446
  expect(renderConversationFooter).toHaveBeenCalledWith(channel)
445
447
  expect(conversationFooter).toHaveTextContent('footer-channel-1')
446
- expect(
447
- messageList.compareDocumentPosition(conversationFooter) &
448
- Node.DOCUMENT_POSITION_FOLLOWING
449
- ).toBeTruthy()
450
- expect(
451
- conversationFooter.compareDocumentPosition(messageInput) &
452
- Node.DOCUMENT_POSITION_FOLLOWING
453
- ).toBeTruthy()
448
+ const regions = screen.getAllByTestId(
449
+ /message-list|conversation-footer|message-input/
450
+ )
451
+ expect(regions.indexOf(messageList)).toBeLessThan(
452
+ regions.indexOf(conversationFooter)
453
+ )
454
+ expect(regions.indexOf(conversationFooter)).toBeLessThan(
455
+ regions.indexOf(messageInput)
456
+ )
454
457
  })
455
458
 
456
459
  it('keeps conversation footer inside the floating composer via renderHeader', () => {
@@ -567,9 +570,11 @@ describe('ChannelView', () => {
567
570
  )
568
571
 
569
572
  expect(
573
+ // eslint-disable-next-line testing-library/no-node-access -- these class selectors verify responsive back-button styling with no semantic query.
570
574
  document.querySelectorAll('.messaging-channel-view-back-button-mobile')
571
575
  ).toHaveLength(1)
572
576
  expect(
577
+ // eslint-disable-next-line testing-library/no-node-access -- these class selectors verify responsive back-button styling with no semantic query.
573
578
  document.querySelectorAll('.messaging-channel-view-back-button-desktop')
574
579
  ).toHaveLength(1)
575
580
  })
@@ -584,9 +589,11 @@ describe('ChannelView', () => {
584
589
  )
585
590
 
586
591
  expect(
592
+ // eslint-disable-next-line testing-library/no-node-access -- these class selectors verify responsive back-button styling with no semantic query.
587
593
  document.querySelectorAll('.messaging-channel-view-back-button-mobile')
588
594
  ).toHaveLength(0)
589
595
  expect(
596
+ // eslint-disable-next-line testing-library/no-node-access -- these class selectors verify responsive back-button styling with no semantic query.
590
597
  document.querySelectorAll('.messaging-channel-view-back-button-desktop')
591
598
  ).toHaveLength(0)
592
599
  })
@@ -2,7 +2,7 @@ import React from 'react'
2
2
  import { LinkPreview, LinkPreviewStatus } from 'stream-chat'
3
3
  import { describe, expect, it, vi } from 'vitest'
4
4
 
5
- import { renderWithProviders, screen, fireEvent } from '../../test/utils'
5
+ import { userEvent, renderWithProviders, screen } from '../../test/utils'
6
6
  import { LOADING_LABEL } from '../LinkAttachment/components/_shared/CardShell'
7
7
 
8
8
  import { CustomLinkPreviewList } from '.'
@@ -82,7 +82,8 @@ describe('CustomLinkPreviewList', () => {
82
82
  expect(list).not.toHaveClass('min-h-0')
83
83
  })
84
84
 
85
- it('is dismiss-only: the staged card never navigates', () => {
85
+ it('is dismiss-only: the staged card never navigates', async () => {
86
+ const user = userEvent.setup()
86
87
  // The deliberate behaviour change in MES-1349 — see this component's own
87
88
  // doc comment for why (MES-1358 §7 Q7, touch hazard). Asserted so the
88
89
  // anchor cannot be reinstated by accident.
@@ -97,7 +98,7 @@ describe('CustomLinkPreviewList', () => {
97
98
  expect(controls).toHaveLength(1)
98
99
  expect(controls[0]).toHaveAttribute('aria-label', 'Dismiss attachment')
99
100
 
100
- fireEvent.click(controls[0])
101
+ await user.click(controls[0])
101
102
  expect(dismissPreview).toHaveBeenCalledWith('https://example.com/story')
102
103
  })
103
104
  })
@@ -2,7 +2,7 @@ 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 } from '../../test/utils'
5
+ import { renderWithProviders, screen, userEvent } from '../../test/utils'
6
6
 
7
7
  import { OutboundContext } from './OutboundContext'
8
8
  import { isTipMessage } from './TipMessage'
@@ -147,7 +147,8 @@ describe('CustomMessage', () => {
147
147
  } as never)
148
148
  })
149
149
 
150
- it('uses the latest outbound click handler after rerendering', () => {
150
+ it('uses the latest outbound click handler after rerendering', async () => {
151
+ const user = userEvent.setup()
151
152
  const firstHandler = vi.fn()
152
153
  const secondHandler = vi.fn()
153
154
  const { rerender } = renderWithProviders(
@@ -175,11 +176,11 @@ describe('CustomMessage', () => {
175
176
  <CustomMessage onOutboundClick={secondHandler} />
176
177
  </OutboundContext.Provider>
177
178
  )
178
- screen
179
- .getByRole('button', {
179
+ await user.click(
180
+ screen.getByRole('button', {
180
181
  name: 'View broadcast July 9th Class attendees',
181
182
  })
182
- .click()
183
+ )
183
184
 
184
185
  expect(firstHandler).not.toHaveBeenCalled()
185
186
  expect(secondHandler).toHaveBeenCalledWith({
@@ -37,6 +37,7 @@ describe('MessageTag', () => {
37
37
  expect(indicator).toHaveTextContent('Sent by AI Assistant')
38
38
 
39
39
  // icon first, then label
40
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
40
41
  expect(indicator.children[0].querySelector('svg')).not.toBeNull()
41
42
  expect(indicator.children[1]).toHaveTextContent('Sent by AI Assistant')
42
43
  })
@@ -56,6 +57,7 @@ describe('MessageTag', () => {
56
57
 
57
58
  // label first, then icon (mirrored)
58
59
  expect(indicator.children[0]).toHaveTextContent('Sent by AI Assistant')
60
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
59
61
  expect(indicator.children[1].querySelector('svg')).not.toBeNull()
60
62
  })
61
63
 
@@ -74,6 +76,7 @@ describe('MessageTag', () => {
74
76
  expect(indicator).toHaveTextContent('Sent with AI')
75
77
 
76
78
  // icon first, then label
79
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
77
80
  expect(indicator.children[0].querySelector('svg')).not.toBeNull()
78
81
  expect(indicator.children[1]).toHaveTextContent('Sent with AI')
79
82
  })
@@ -88,6 +91,7 @@ describe('MessageTag', () => {
88
91
  )
89
92
 
90
93
  const paidTag = screen.getByText('Delivered with $10.00 message')
94
+ // eslint-disable-next-line testing-library/no-node-access -- the component wrapper class has no semantic query.
91
95
  expect(paidTag.closest('.message-tag--paid')).not.toBeNull()
92
96
  })
93
97
 
@@ -100,6 +104,7 @@ describe('MessageTag', () => {
100
104
  />
101
105
  )
102
106
 
107
+ // eslint-disable-next-line testing-library/no-node-access -- the null-rendering DOM contract has no semantic query.
103
108
  expect(container.firstChild).toBeNull()
104
109
  })
105
110
 
@@ -108,6 +113,7 @@ describe('MessageTag', () => {
108
113
  <MessageTag message={createMockMessage()} />
109
114
  )
110
115
 
116
+ // eslint-disable-next-line testing-library/no-node-access -- the null-rendering DOM contract has no semantic query.
111
117
  expect(container.firstChild).toBeNull()
112
118
  })
113
119
  })