@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
@@ -1,12 +1,13 @@
1
1
  import { describe, expect, it, vi } from 'vitest'
2
2
 
3
- import { fireEvent, renderWithProviders, screen } from '../../../test/utils'
3
+ import { userEvent, renderWithProviders, screen } from '../../../test/utils'
4
4
 
5
5
  import CarouselNav from './CarouselNav'
6
6
  import { VIEWER_NAV_CLASS } from './viewerChromeClasses'
7
7
 
8
8
  describe('CarouselNav', () => {
9
- it('uses default labels and independent handlers', () => {
9
+ it('uses default labels and independent handlers', async () => {
10
+ const user = userEvent.setup()
10
11
  const onPrev = vi.fn()
11
12
  const onNext = vi.fn()
12
13
  renderWithProviders(<CarouselNav onPrev={onPrev} onNext={onNext} />)
@@ -15,8 +16,8 @@ describe('CarouselNav', () => {
15
16
  const next = screen.getByRole('button', { name: 'Next' })
16
17
  expect(prev).toHaveClass(VIEWER_NAV_CLASS, 'left-4')
17
18
  expect(next).toHaveClass(VIEWER_NAV_CLASS, 'right-4')
18
- fireEvent.click(prev)
19
- fireEvent.click(next)
19
+ await user.click(prev)
20
+ await user.click(next)
20
21
  expect(onPrev).toHaveBeenCalledOnce()
21
22
  expect(onNext).toHaveBeenCalledOnce()
22
23
  })
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from 'vitest'
2
2
 
3
- import { fireEvent, renderWithProviders, screen } from '../../../test/utils'
3
+ import { userEvent, renderWithProviders, screen } from '../../../test/utils'
4
4
 
5
5
  import CompactDocumentRow from './CompactDocumentRow'
6
6
 
@@ -71,7 +71,8 @@ describe('CompactDocumentRow', () => {
71
71
  )
72
72
  })
73
73
 
74
- it('activates its body without nesting the trailing action', () => {
74
+ it('activates its body without nesting the trailing action', async () => {
75
+ const user = userEvent.setup()
75
76
  const onActivate = vi.fn()
76
77
  renderWithProviders(
77
78
  <CompactDocumentRow
@@ -87,7 +88,7 @@ describe('CompactDocumentRow', () => {
87
88
  expect(body).not.toContainElement(
88
89
  screen.getByRole('button', { name: 'Download' })
89
90
  )
90
- fireEvent.click(body)
91
+ await user.click(body)
91
92
  expect(onActivate).toHaveBeenCalledOnce()
92
93
  })
93
94
 
@@ -1,11 +1,12 @@
1
1
  import { describe, expect, it, vi } from 'vitest'
2
2
 
3
- import { fireEvent, renderWithProviders, screen } from '../../../test/utils'
3
+ import { userEvent, renderWithProviders, screen } from '../../../test/utils'
4
4
 
5
5
  import DismissButton from './DismissButton'
6
6
 
7
7
  describe('DismissButton', () => {
8
- it('uses the default label, invokes the handler, and stops propagation', () => {
8
+ it('uses the default label, invokes the handler, and stops propagation', async () => {
9
+ const user = userEvent.setup()
9
10
  const onClick = vi.fn()
10
11
  const outerClick = vi.fn()
11
12
  renderWithProviders(
@@ -15,7 +16,7 @@ describe('DismissButton', () => {
15
16
  </div>
16
17
  )
17
18
 
18
- fireEvent.click(screen.getByRole('button', { name: 'Dismiss attachment' }))
19
+ await user.click(screen.getByRole('button', { name: 'Dismiss attachment' }))
19
20
  expect(onClick).toHaveBeenCalledOnce()
20
21
  expect(outerClick).not.toHaveBeenCalled()
21
22
  })
@@ -1,10 +1,10 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest'
2
2
 
3
3
  import {
4
- fireEvent,
5
4
  renderWithProviders,
6
5
  screen,
7
6
  waitFor,
7
+ userEvent,
8
8
  } from '../../../test/utils'
9
9
 
10
10
  import DownloadAction from './DownloadAction'
@@ -83,6 +83,7 @@ describe('DownloadAction', () => {
83
83
  )
84
84
 
85
85
  it('stops propagation and calls the download once', async () => {
86
+ const user = userEvent.setup()
86
87
  const outerClick = vi.fn()
87
88
  renderWithProviders(
88
89
  // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
@@ -94,7 +95,7 @@ describe('DownloadAction', () => {
94
95
  />
95
96
  </div>
96
97
  )
97
- fireEvent.click(screen.getByRole('button', { name: 'Download' }))
98
+ await user.click(screen.getByRole('button', { name: 'Download' }))
98
99
  await waitFor(() =>
99
100
  expect(mockedTriggerDownload).toHaveBeenCalledWith(
100
101
  '/file.pdf',
@@ -106,6 +107,7 @@ describe('DownloadAction', () => {
106
107
  })
107
108
 
108
109
  it('disables repeated clicks while downloading and settles on success', async () => {
110
+ const user = userEvent.setup()
109
111
  let resolve: () => void = () => undefined
110
112
  mockedTriggerDownload.mockReturnValueOnce(
111
113
  new Promise<void>((done) => {
@@ -117,8 +119,8 @@ describe('DownloadAction', () => {
117
119
  <DownloadAction url="/file.pdf" onTriggered={onTriggered} />
118
120
  )
119
121
  const button = screen.getByRole('button', { name: 'Download' })
120
- fireEvent.click(button)
121
- fireEvent.click(button)
122
+ await user.click(button)
123
+ await user.click(button)
122
124
  expect(button).toBeDisabled()
123
125
  expect(mockedTriggerDownload).toHaveBeenCalledOnce()
124
126
  resolve()
@@ -127,13 +129,14 @@ describe('DownloadAction', () => {
127
129
  })
128
130
 
129
131
  it('settles and notifies even when downloading rejects', async () => {
132
+ const user = userEvent.setup()
130
133
  mockedTriggerDownload.mockRejectedValueOnce(new Error('offline'))
131
134
  const onTriggered = vi.fn()
132
135
  renderWithProviders(
133
136
  <DownloadAction url="/file.pdf" onTriggered={onTriggered} />
134
137
  )
135
138
  const button = screen.getByRole('button', { name: 'Download' })
136
- fireEvent.click(button)
139
+ await user.click(button)
137
140
  await waitFor(() => expect(button).toBeEnabled())
138
141
  expect(onTriggered).toHaveBeenCalledOnce()
139
142
  })
@@ -6,6 +6,8 @@ import {
6
6
  import classNames from 'classnames'
7
7
  import React, { useCallback, useState } from 'react'
8
8
 
9
+ import Spinner from '../../_shared/Spinner'
10
+
9
11
  import { triggerDownload } from './triggerDownload'
10
12
  import { VIEWER_ACTION_CLASS } from './viewerChromeClasses'
11
13
 
@@ -105,11 +107,7 @@ const DownloadAction: React.FC<DownloadActionProps> = ({
105
107
  )}
106
108
  >
107
109
  {busy ? (
108
- <CircleNotchIcon
109
- className="size-4 animate-spin"
110
- weight="bold"
111
- aria-hidden
112
- />
110
+ <Spinner aria-hidden />
113
111
  ) : (
114
112
  <DownloadSimpleIcon
115
113
  className="size-5 shrink-0"
@@ -156,11 +154,7 @@ const DownloadAction: React.FC<DownloadActionProps> = ({
156
154
  )}
157
155
  >
158
156
  {busy ? (
159
- <CircleNotchIcon
160
- className="size-4 animate-spin"
161
- weight="bold"
162
- aria-hidden
163
- />
157
+ <Spinner aria-hidden />
164
158
  ) : (
165
159
  <DownloadSimpleIcon {...iconProps} aria-hidden />
166
160
  )}
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from 'vitest'
2
2
 
3
- import { fireEvent, renderWithProviders, screen } from '../../../test/utils'
3
+ import { userEvent, renderWithProviders, screen } from '../../../test/utils'
4
4
 
5
5
  import ImageViewer from './ImageViewer'
6
6
 
@@ -14,6 +14,7 @@ describe('ImageViewer', () => {
14
14
  const { container } = renderWithProviders(
15
15
  <ImageViewer open onClose={vi.fn()} items={[]} />
16
16
  )
17
+ // eslint-disable-next-line testing-library/no-node-access -- the null-rendering DOM contract has no semantic query.
17
18
  expect(container.firstChild).toBeNull()
18
19
  })
19
20
 
@@ -29,6 +30,7 @@ describe('ImageViewer', () => {
29
30
  expect(
30
31
  screen
31
32
  .getByTestId('image-viewer')
33
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
32
34
  .querySelector('button[aria-label="Download one.jpg"]')
33
35
  ).toBeInTheDocument()
34
36
  rerender(<ImageViewer open onClose={vi.fn()} items={[items[0]]} />)
@@ -36,7 +38,8 @@ describe('ImageViewer', () => {
36
38
  expect(screen.getByRole('img')).toHaveAttribute('draggable', 'false')
37
39
  })
38
40
 
39
- it('renders counter and navigation only for multiple items', () => {
41
+ it('renders counter and navigation only for multiple items', async () => {
42
+ const user = userEvent.setup()
40
43
  renderWithProviders(
41
44
  <ImageViewer open onClose={vi.fn()} items={items} initialIndex={99} />
42
45
  )
@@ -45,7 +48,7 @@ describe('ImageViewer', () => {
45
48
  screen.getByRole('button', { name: 'Previous image' })
46
49
  ).toBeInTheDocument()
47
50
  expect(screen.getByRole('img')).toHaveAttribute('src', items[1].src)
48
- fireEvent.click(screen.getByRole('button', { name: 'Previous image' }))
51
+ await user.click(screen.getByRole('button', { name: 'Previous image' }))
49
52
  expect(screen.getByText('1 / 2')).toBeInTheDocument()
50
53
  expect(screen.getByRole('img')).toHaveAttribute('src', items[0].src)
51
54
  })
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from 'vitest'
2
2
 
3
- import { fireEvent, renderWithProviders, screen } from '../../../test/utils'
3
+ import { userEvent, renderWithProviders, screen } from '../../../test/utils'
4
4
 
5
5
  import MediaStackGrid from './MediaStackGrid'
6
6
 
@@ -13,6 +13,7 @@ const tiles = (count: number) =>
13
13
  describe('MediaStackGrid', () => {
14
14
  it('renders nothing for empty tiles', () => {
15
15
  const { container } = renderWithProviders(<MediaStackGrid tiles={[]} />)
16
+ // eslint-disable-next-line testing-library/no-node-access -- the null-rendering DOM contract has no semantic query.
16
17
  expect(container.firstChild).toBeNull()
17
18
  })
18
19
 
@@ -25,6 +26,7 @@ describe('MediaStackGrid', () => {
25
26
  const { container } = renderWithProviders(
26
27
  <MediaStackGrid tiles={tiles(count)} className="custom-grid" />
27
28
  )
29
+ // eslint-disable-next-line testing-library/no-node-access -- the component shell wrapper has no semantic query.
28
30
  expect(container.firstElementChild).toHaveClass(layoutClass, 'custom-grid')
29
31
  expect(screen.getAllByText(/tile/)).toHaveLength(count)
30
32
  })
@@ -35,12 +37,14 @@ describe('MediaStackGrid', () => {
35
37
  )
36
38
  expect(screen.getAllByText(/tile/)).toHaveLength(4)
37
39
  expect(screen.getByText('+2')).toBeInTheDocument()
40
+ // eslint-disable-next-line testing-library/no-node-access -- the wrapper class is a deliberate DOM contract with no semantic query.
38
41
  expect(screen.getByText('+2').parentElement).toHaveTextContent('tile 4')
39
42
  rerender(<MediaStackGrid tiles={tiles(2)} maxVisible={4} />)
40
43
  expect(screen.getAllByText(/tile/)).toHaveLength(2)
41
44
  expect(screen.queryByText(/^\+/)).toBeNull()
42
45
  rerender(<MediaStackGrid tiles={tiles(1)} singleAspectRatio={0.5} />)
43
46
  expect(
47
+ // eslint-disable-next-line testing-library/no-node-access -- the component wrapper class has no semantic query.
44
48
  screen.getByText('tile 1').closest('div')?.parentElement
45
49
  ).toHaveStyle({
46
50
  aspectRatio: '0.5',
@@ -58,6 +62,7 @@ describe('MediaStackGrid', () => {
58
62
  )
59
63
  expect(screen.getAllByText(/tile/)).toHaveLength(maxVisible)
60
64
  expect(screen.getByText(badge)).toBeInTheDocument()
65
+ // eslint-disable-next-line testing-library/no-node-access -- the wrapper class is a deliberate DOM contract with no semantic query.
61
66
  expect(screen.getByText(badge).parentElement).toHaveTextContent(lastTile)
62
67
  }
63
68
  )
@@ -65,6 +70,7 @@ describe('MediaStackGrid', () => {
65
70
  it('shows overflow on the single visible tile', () => {
66
71
  renderWithProviders(<MediaStackGrid tiles={tiles(3)} maxVisible={1} />)
67
72
  expect(screen.getAllByText(/tile/)).toHaveLength(1)
73
+ // eslint-disable-next-line testing-library/no-node-access -- the wrapper class is a deliberate DOM contract with no semantic query.
68
74
  expect(screen.getByText('+2').parentElement).toHaveTextContent('tile 1')
69
75
  })
70
76
 
@@ -73,7 +79,8 @@ describe('MediaStackGrid', () => {
73
79
  expect(screen.queryByText(/^\+/)).toBeNull()
74
80
  })
75
81
 
76
- it('renders activatable buttons with labels and indexes', () => {
82
+ it('renders activatable buttons with labels and indexes', async () => {
83
+ const user = userEvent.setup()
77
84
  const onActivate = vi.fn()
78
85
  renderWithProviders(
79
86
  <MediaStackGrid tiles={tiles(3)} onTileActivate={onActivate} />
@@ -82,8 +89,8 @@ describe('MediaStackGrid', () => {
82
89
  expect(buttons).toHaveLength(3)
83
90
  expect(buttons[0]).toHaveAttribute('aria-label', 'Open media 1')
84
91
  expect(buttons[1]).toHaveAttribute('aria-label', 'Second tile')
85
- fireEvent.click(buttons[0])
86
- fireEvent.click(buttons[2])
92
+ await user.click(buttons[0])
93
+ await user.click(buttons[2])
87
94
  expect(onActivate).toHaveBeenNthCalledWith(1, 0)
88
95
  expect(onActivate).toHaveBeenNthCalledWith(2, 2)
89
96
  })
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from 'vitest'
2
2
 
3
- import { fireEvent, renderWithProviders, screen } from '../../../test/utils'
3
+ import { userEvent, renderWithProviders, screen } from '../../../test/utils'
4
4
 
5
5
  import PdfViewer from './PdfViewer'
6
6
 
@@ -14,6 +14,7 @@ describe('PdfViewer', () => {
14
14
  const { container } = renderWithProviders(
15
15
  <PdfViewer open onClose={vi.fn()} items={[]} />
16
16
  )
17
+ // eslint-disable-next-line testing-library/no-node-access -- the null-rendering DOM contract has no semantic query.
17
18
  expect(container.firstChild).toBeNull()
18
19
  })
19
20
 
@@ -30,7 +31,8 @@ describe('PdfViewer', () => {
30
31
  )
31
32
  })
32
33
 
33
- it('uses filename precedence and navigates stacked documents', () => {
34
+ it('uses filename precedence and navigates stacked documents', async () => {
35
+ const user = userEvent.setup()
34
36
  renderWithProviders(
35
37
  <PdfViewer open onClose={vi.fn()} items={items} initialIndex={1} />
36
38
  )
@@ -39,7 +41,7 @@ describe('PdfViewer', () => {
39
41
  'second.pdf'
40
42
  )
41
43
  expect(screen.getByText('2 / 2')).toBeInTheDocument()
42
- fireEvent.click(screen.getByRole('button', { name: 'Previous document' }))
44
+ await user.click(screen.getByRole('button', { name: 'Previous document' }))
43
45
  expect(screen.getByTitle('one.pdf')).toBeInTheDocument()
44
46
  })
45
47
  })
@@ -2,10 +2,10 @@ import { describe, expect, it, vi } from 'vitest'
2
2
 
3
3
  import {
4
4
  act,
5
- fireEvent,
6
5
  renderWithProviders,
7
6
  screen,
8
7
  within,
8
+ userEvent,
9
9
  } from '../../../test/utils'
10
10
 
11
11
  import VideoViewer from './VideoViewer'
@@ -26,6 +26,7 @@ describe('VideoViewer', () => {
26
26
  const { container } = renderWithProviders(
27
27
  <VideoViewer open onClose={vi.fn()} items={[]} />
28
28
  )
29
+ // eslint-disable-next-line testing-library/no-node-access -- the null-rendering DOM contract has no semantic query.
29
30
  expect(container.firstChild).toBeNull()
30
31
  })
31
32
 
@@ -33,6 +34,7 @@ describe('VideoViewer', () => {
33
34
  renderWithProviders(
34
35
  <VideoViewer open onClose={vi.fn()} items={[items[0]]} />
35
36
  )
37
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
36
38
  const video = screen.getByTestId('video-viewer').querySelector('video')
37
39
  expect(video).not.toBeNull()
38
40
  if (!video) throw new Error('Expected video element')
@@ -41,20 +43,25 @@ describe('VideoViewer', () => {
41
43
  expect(video).toHaveAttribute('preload', 'auto')
42
44
  expect(video).toHaveAttribute('autoplay')
43
45
  expect(video?.muted).toBe(true)
46
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
44
47
  expect(video.querySelector('source')).toHaveAttribute('type', 'video/mp4')
45
48
  })
46
49
 
47
- it('defaults preload and navigates stacked items', () => {
50
+ it('defaults preload and navigates stacked items', async () => {
51
+ const user = userEvent.setup()
48
52
  renderWithProviders(<VideoViewer open onClose={vi.fn()} items={items} />)
49
53
  expect(screen.getByText('1 / 2')).toBeInTheDocument()
50
54
  expect(
55
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
51
56
  screen.getByTestId('video-viewer').querySelector('video')
52
57
  ).toHaveAttribute('preload', 'auto')
53
- fireEvent.click(screen.getByRole('button', { name: 'Next video' }))
58
+ await user.click(screen.getByRole('button', { name: 'Next video' }))
54
59
  expect(
60
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
55
61
  screen.getByTestId('video-viewer').querySelector('video')
56
62
  ).toHaveAttribute('src', items[1].src)
57
63
  expect(
64
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
58
65
  screen.getByTestId('video-viewer').querySelector('video')
59
66
  ).toHaveAttribute('preload', 'metadata')
60
67
  })
@@ -85,6 +92,7 @@ describe('VideoViewer', () => {
85
92
  const { rerender } = renderWithProviders(
86
93
  <VideoViewer open onClose={vi.fn()} items={[items[0]]} />
87
94
  )
95
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
88
96
  const video = screen.getByTestId('video-viewer').querySelector('video')
89
97
  if (!video) throw new Error('Expected video element')
90
98
  const pause = vi.spyOn(video, 'pause').mockImplementation(() => {})
@@ -96,6 +104,7 @@ describe('VideoViewer', () => {
96
104
  // Still there, so the last frame doesn't blink out from under the
97
105
  // 150ms backdrop fade — but paused, so nothing plays under it.
98
106
  expect(
107
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
99
108
  screen.getByTestId('video-viewer').querySelector('video')
100
109
  ).not.toBeNull()
101
110
  expect(pause).toHaveBeenCalled()
@@ -1,7 +1,12 @@
1
1
  import { useLayoutEffect } from 'react'
2
2
  import { afterEach, 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
+ } from '../../../test/utils'
5
10
 
6
11
  import { VIEWER_ACTION_CLASS } from './viewerChromeClasses'
7
12
  import ViewerShell from './ViewerShell'
@@ -33,6 +38,7 @@ const restoreDialogMethod = (
33
38
 
34
39
  const PreOpenDialog = () => {
35
40
  useLayoutEffect(() => {
41
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
36
42
  document.querySelector('[data-testid="viewer"]')?.setAttribute('open', '')
37
43
  }, [])
38
44
  return null
@@ -201,7 +207,7 @@ describe('ViewerShell', () => {
201
207
  </ViewerShell>
202
208
  )
203
209
  ).not.toThrow()
204
- expect(document.activeElement).not.toBe(previouslyFocused)
210
+ expect(previouslyFocused).not.toHaveFocus()
205
211
  })
206
212
 
207
213
  it('uses the default accessible label', () => {
@@ -227,7 +233,8 @@ describe('ViewerShell', () => {
227
233
  expect(close).not.toHaveBeenCalled()
228
234
  })
229
235
 
230
- it('closes from the close, native close, and backdrop events', () => {
236
+ it('closes from the close, native close, and backdrop events', async () => {
237
+ const user = userEvent.setup()
231
238
  const onClose = vi.fn()
232
239
  renderWithProviders(
233
240
  <ViewerShell open onClose={onClose} data-testid="viewer">
@@ -235,12 +242,12 @@ describe('ViewerShell', () => {
235
242
  </ViewerShell>
236
243
  )
237
244
  const dialog = screen.getByTestId('viewer')
238
- fireEvent.click(screen.getByRole('button', { name: 'Close viewer' }))
245
+ await user.click(screen.getByRole('button', { name: 'Close viewer' }))
239
246
  fireEvent(dialog, new Event('close'))
240
- fireEvent.click(dialog)
247
+ await user.click(dialog)
241
248
  expect(onClose).toHaveBeenCalledTimes(3)
242
249
  onClose.mockClear()
243
- fireEvent.click(screen.getByText('content'))
250
+ await user.click(screen.getByText('content'))
244
251
  expect(onClose).not.toHaveBeenCalled()
245
252
  })
246
253
  })
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
 
3
- import { fireEvent, renderWithProviders, screen } from '../../../test/utils'
3
+ import {
4
+ userEvent,
5
+ fireEvent,
6
+ renderWithProviders,
7
+ screen,
8
+ } from '../../../test/utils'
4
9
 
5
10
  import { useCarousel } from './useCarousel'
6
11
 
@@ -28,14 +33,15 @@ const Harness = ({
28
33
  }
29
34
 
30
35
  describe('useCarousel', () => {
31
- it('clamps initial index, wraps, and resets on reopen', () => {
36
+ it('clamps initial index, wraps, and resets on reopen', async () => {
37
+ const user = userEvent.setup()
32
38
  const { rerender } = renderWithProviders(
33
39
  <Harness open length={3} initialIndex={99} />
34
40
  )
35
41
  expect(screen.getByTestId('index')).toHaveTextContent('2')
36
- fireEvent.click(screen.getByRole('button', { name: 'Next' }))
42
+ await user.click(screen.getByRole('button', { name: 'Next' }))
37
43
  expect(screen.getByTestId('index')).toHaveTextContent('0')
38
- fireEvent.click(screen.getByRole('button', { name: 'Prev' }))
44
+ await user.click(screen.getByRole('button', { name: 'Prev' }))
39
45
  expect(screen.getByTestId('index')).toHaveTextContent('2')
40
46
  rerender(<Harness open={false} length={3} initialIndex={1} />)
41
47
  rerender(<Harness open length={3} initialIndex={1} />)
@@ -23,11 +23,15 @@ describe('MessageBubble', () => {
23
23
  <MessageBubble.Sent text="tailed" />
24
24
  )
25
25
  // The bubble box wraps the text; its last child is the tail <span>.
26
+ // eslint-disable-next-line testing-library/no-node-access -- the wrapper class is a deliberate DOM contract with no semantic query.
26
27
  const bubble = screen.getByText('tailed').parentElement as HTMLElement
28
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
27
29
  expect(bubble.querySelector('span[aria-hidden="true"]')).not.toBeNull()
28
30
 
29
31
  rerender(<MessageBubble.Sent text="tailed" tail={false} />)
32
+ // eslint-disable-next-line testing-library/no-node-access -- the wrapper class is a deliberate DOM contract with no semantic query.
30
33
  const bubbleNoTail = screen.getByText('tailed').parentElement as HTMLElement
34
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
31
35
  expect(bubbleNoTail.querySelector('span[aria-hidden="true"]')).toBeNull()
32
36
  })
33
37
 
@@ -47,12 +51,14 @@ describe('MessageBubble', () => {
47
51
 
48
52
  it('applies the sent and received surface treatments from theme vars', () => {
49
53
  const { rerender } = renderWithProviders(<MessageBubble.Sent text="mine" />)
54
+ // eslint-disable-next-line testing-library/no-node-access -- the wrapper class is a deliberate DOM contract with no semantic query.
50
55
  const sent = screen.getByText('mine').parentElement as HTMLElement
51
56
  expect(sent.style.backgroundColor).toBe(
52
57
  'var(--str-chat__own-message-bubble-background-color, #1e2330)'
53
58
  )
54
59
 
55
60
  rerender(<MessageBubble.Received text="theirs" />)
61
+ // eslint-disable-next-line testing-library/no-node-access -- the wrapper class is a deliberate DOM contract with no semantic query.
56
62
  const received = screen.getByText('theirs').parentElement as HTMLElement
57
63
  expect(received.style.backgroundColor).toBe(
58
64
  'var(--str-chat__message-bubble-background-color, #f1f0ee)'
@@ -61,6 +67,7 @@ describe('MessageBubble', () => {
61
67
 
62
68
  it('masks message content from session replay by default', () => {
63
69
  renderWithProviders(<MessageBubble.Received header="Brie" text="secret" />)
70
+ // eslint-disable-next-line testing-library/no-node-access -- the wrapper class is a deliberate DOM contract with no semantic query.
64
71
  const bubble = screen.getByText('secret').parentElement as HTMLElement
65
72
  expect(bubble.getAttribute('data-dd-privacy')).toBe('mask')
66
73
  })
@@ -69,6 +76,7 @@ describe('MessageBubble', () => {
69
76
  renderWithProviders(
70
77
  <MessageBubble.Sent text="wide" className="w-fit max-w-[520px]" />
71
78
  )
79
+ // eslint-disable-next-line testing-library/no-node-access -- the wrapper class is a deliberate DOM contract with no semantic query.
72
80
  const bubble = screen.getByText('wide').parentElement as HTMLElement
73
81
  expect(bubble.classList.contains('w-fit')).toBe(true)
74
82
  })
@@ -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 RichCard from '.'
7
12
 
@@ -41,6 +46,7 @@ describe('RichCard', () => {
41
46
  expect(card).toHaveTextContent('👋 Say hello as people arrive')
42
47
  expect(card).toHaveTextContent('✨ Spark conversations')
43
48
  expect(card).toHaveTextContent('💬 Add automated FAQs')
49
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
44
50
  expect(card.querySelectorAll('br')).toHaveLength(2)
45
51
  })
46
52
 
@@ -57,6 +63,7 @@ describe('RichCard', () => {
57
63
  )
58
64
 
59
65
  const card = await screen.findByTestId('rich-card')
66
+ // eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
60
67
  expect(card.querySelectorAll('br')).toHaveLength(0)
61
68
  expect(screen.getByText(/Say hello as people arrive/)).toHaveClass(
62
69
  'truncate'
@@ -197,6 +204,7 @@ describe('RichCard', () => {
197
204
  })
198
205
 
199
206
  it('renders a url action as an external anchor, others as buttons', async () => {
207
+ const user = userEvent.setup()
200
208
  const onClick = vi.fn()
201
209
  renderWithProviders(
202
210
  <RichCard.Received
@@ -212,7 +220,7 @@ describe('RichCard', () => {
212
220
  expect(link).toHaveAttribute('href', 'https://tr.ee/x')
213
221
  expect(link).toHaveAttribute('target', '_blank')
214
222
 
215
- fireEvent.click(screen.getByRole('button', { name: 'Set up' }))
223
+ await user.click(screen.getByRole('button', { name: 'Set up' }))
216
224
  expect(onClick).toHaveBeenCalledTimes(1)
217
225
  })
218
226
 
@@ -0,0 +1,75 @@
1
+ import { SpinnerGapIcon } from '@phosphor-icons/react'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { renderWithProviders } from '../../test/utils'
5
+
6
+ import Spinner from './Spinner'
7
+
8
+ const getSvg = (container: HTMLElement) => {
9
+ // eslint-disable-next-line testing-library/no-node-access -- Spinner is tested through its rendered SVG.
10
+ return container.querySelector('svg')
11
+ }
12
+
13
+ describe('Spinner', () => {
14
+ it('renders the default size and spinning class', () => {
15
+ const { container } = renderWithProviders(<Spinner />)
16
+ const svg = getSvg(container)
17
+
18
+ // eslint-disable-next-line local/no-style-only-assertion -- Spinner's contract is its class output.
19
+ expect(svg).toHaveClass('animate-spin', 'size-4')
20
+ // eslint-disable-next-line local/no-style-only-assertion -- Spinner's contract is its class output.
21
+ expect(svg).not.toHaveClass('text-white/80')
22
+ })
23
+
24
+ it.each([
25
+ ['md', 'size-5'],
26
+ ['lg', 'size-8'],
27
+ ] as const)('supports %s size', (size, sizeClass) => {
28
+ const { container } = renderWithProviders(<Spinner size={size} />)
29
+
30
+ expect(getSvg(container)).toHaveClass(sizeClass)
31
+ })
32
+
33
+ it('supports the on-media tone', () => {
34
+ const { container, rerender } = renderWithProviders(
35
+ <Spinner tone="on-media" />
36
+ )
37
+ // eslint-disable-next-line local/no-style-only-assertion -- Spinner's contract is its class output.
38
+ expect(getSvg(container)).toHaveClass('text-white/80')
39
+
40
+ rerender(<Spinner />)
41
+ // eslint-disable-next-line local/no-style-only-assertion -- Spinner's contract is its class output.
42
+ expect(getSvg(container)).not.toHaveClass('text-white/80')
43
+ })
44
+
45
+ it('renders the supplied icon', () => {
46
+ const { container: defaultContainer } = renderWithProviders(<Spinner />)
47
+ const { container: suppliedContainer } = renderWithProviders(
48
+ <Spinner icon={SpinnerGapIcon} />
49
+ )
50
+
51
+ expect(getSvg(suppliedContainer)?.innerHTML).not.toBe(
52
+ getSvg(defaultContainer)?.innerHTML
53
+ )
54
+ })
55
+
56
+ it('only renders aria-hidden when passed', () => {
57
+ const { container, rerender } = renderWithProviders(<Spinner />)
58
+ expect(getSvg(container)).not.toHaveAttribute('aria-hidden')
59
+
60
+ rerender(<Spinner aria-hidden />)
61
+ expect(getSvg(container)).toHaveAttribute('aria-hidden', 'true')
62
+ })
63
+
64
+ it('merges a custom className with its built-in classes', () => {
65
+ const { container } = renderWithProviders(
66
+ <Spinner className="custom-spinner" />
67
+ )
68
+
69
+ expect(getSvg(container)).toHaveClass(
70
+ 'animate-spin',
71
+ 'size-4',
72
+ 'custom-spinner'
73
+ )
74
+ })
75
+ })