@linktr.ee/messaging-react 4.2.0-rc-1787325521 → 4.2.0-rc-1787599724
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.
- package/dist/{Card-BYJyA83X.js → Card-D7UPq5-g.js} +2 -2
- package/dist/{Card-BYJyA83X.js.map → Card-D7UPq5-g.js.map} +1 -1
- package/dist/{Card-BplJtZao.cjs → Card-DcBZqMBt.cjs} +2 -2
- package/dist/{Card-BplJtZao.cjs.map → Card-DcBZqMBt.cjs.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/{index-BUC9vqhJ.js → index-CSkr_CO5.js} +2089 -1952
- package/dist/index-CSkr_CO5.js.map +1 -0
- package/dist/index-CwkY7QwT.cjs +3 -0
- package/dist/index-CwkY7QwT.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +59 -78
- package/dist/index.js +26 -30
- package/dist/testing.cjs +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/package.json +13 -11
- package/src/components/AttachmentCard/utils/icons.ts +7 -6
- package/src/components/Avatar/Avatar.test.tsx +0 -126
- package/src/components/Avatar/getAvatarEmoji.test.ts +0 -39
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +3 -410
- package/src/components/ChannelList/index.test.tsx +1 -132
- package/src/components/ChannelView.stories.tsx +0 -4
- package/src/components/ChannelView.test.tsx +3 -9
- package/src/components/CustomMessage/CustomMessage.stories.tsx +314 -6
- package/src/components/CustomMessage/CustomMessage.test.tsx +2 -56
- package/src/components/CustomMessage/MessageTag.stories.tsx +53 -17
- package/src/components/CustomMessage/MessageTag.test.tsx +40 -7
- package/src/components/CustomMessage/MessageTag.tsx +39 -7
- package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +7 -8
- package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +1 -2
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +33 -0
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +12 -5
- package/src/components/CustomMessage/index.tsx +19 -22
- package/src/components/CustomMessageInput/index.tsx +1 -1
- package/src/components/CustomSystemMessage/index.tsx +2 -3
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +0 -327
- package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +165 -58
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.test.tsx +153 -0
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.tsx +450 -0
- package/src/components/MessageAttachment/Audio/audioRowRequester.test.ts +24 -0
- package/src/components/MessageAttachment/Audio/audioRowRequester.ts +16 -0
- package/src/components/MessageAttachment/Audio/index.tsx +83 -77
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +176 -74
- package/src/components/MessageAttachment/_shared/fileMeta.test.ts +0 -44
- package/src/components/MessageAttachment/_shared/useIsClient.ts +23 -0
- package/src/components/MessageAttachment/index.tsx +25 -9
- package/src/components/MessageAttachment/types.ts +19 -9
- package/src/components/MessagingShell/MessagingShell.test.tsx +0 -20
- package/src/components/MessagingShell/index.tsx +0 -2
- package/src/components/RichCard/RichCard.test.tsx +1 -3
- package/src/index.ts +0 -4
- package/src/providers/MessagingProvider.test.tsx +2 -130
- package/src/providers/MessagingProvider.tsx +15 -26
- package/src/styles.css +210 -57
- package/src/testing/createMockMessagingClient.ts +2 -2
- package/src/types.ts +0 -9
- package/dist/index-BUC9vqhJ.js.map +0 -1
- package/dist/index-BkqyKPJP.cjs +0 -3
- package/dist/index-BkqyKPJP.cjs.map +0 -1
- package/src/components/AttachmentCard/MediaPlayer.test.tsx +0 -346
- package/src/components/AttachmentCard/Thumbnail.test.tsx +0 -215
- package/src/components/AttachmentCard/index.test.tsx +0 -160
- package/src/components/ChannelList/ChannelListContext.test.tsx +0 -87
- package/src/components/CustomMessage/Locked.stories.tsx +0 -108
- package/src/components/CustomMessage/PaidMessage.stories.tsx +0 -42
- package/src/components/CustomMessage/Tip.stories.tsx +0 -48
- package/src/components/CustomMessage/TipMessage/TipMessage.stories.tsx +0 -51
- package/src/components/CustomMessage/TipMessage/TipMessage.test.tsx +0 -65
- package/src/components/CustomMessage/TipMessage/index.tsx +0 -27
- package/src/components/FaqList/FaqList.test.tsx +0 -204
- package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +0 -76
- package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +0 -248
- package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +0 -123
- package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +0 -193
- package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +0 -242
- package/src/components/LinkAttachment/components/_shared/hex.test.ts +0 -37
- package/src/components/LinkAttachment/components/_shared/normalizeExternalHref.test.ts +0 -72
- package/src/components/LinkAttachment/components/_shared/useChinPalette.test.ts +0 -162
- package/src/components/MessageAttachment/MessageAttachment.behavior.test.tsx +0 -182
- package/src/components/MessageAttachment/_shared/Bubble.test.tsx +0 -111
- package/src/components/MessageAttachment/_shared/CarouselNav.test.tsx +0 -36
- package/src/components/MessageAttachment/_shared/CompactDocumentRow.test.tsx +0 -114
- package/src/components/MessageAttachment/_shared/DismissButton.test.tsx +0 -42
- package/src/components/MessageAttachment/_shared/DownloadAction.test.tsx +0 -168
- package/src/components/MessageAttachment/_shared/ImageViewer.test.tsx +0 -52
- package/src/components/MessageAttachment/_shared/MediaStackGrid.test.tsx +0 -69
- package/src/components/MessageAttachment/_shared/PdfViewer.test.tsx +0 -45
- package/src/components/MessageAttachment/_shared/VideoViewer.test.tsx +0 -55
- package/src/components/MessageAttachment/_shared/ViewerShell.test.tsx +0 -246
- package/src/components/MessageAttachment/_shared/triggerDownload.test.ts +0 -113
- package/src/components/MessageAttachment/_shared/useCarousel.test.tsx +0 -68
- package/src/components/MessageAttachment/_shared/viewerChromeClasses.test.ts +0 -16
- package/src/logging/index.test.tsx +0 -136
- package/src/logging/index.tsx +0 -48
- package/src/stories/customMessageStoryTemplate.tsx +0 -202
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
fireEvent,
|
|
6
|
-
renderWithProviders,
|
|
7
|
-
screen,
|
|
8
|
-
waitFor,
|
|
9
|
-
} from '../../test/utils'
|
|
10
|
-
|
|
11
|
-
import MediaPlayer from './MediaPlayer'
|
|
12
|
-
|
|
13
|
-
const SOURCE = 'https://cdn.example.com/preview.mp4'
|
|
14
|
-
const POSTER = 'https://cdn.example.com/poster.jpg'
|
|
15
|
-
|
|
16
|
-
describe('MediaPlayer', () => {
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
vi.spyOn(HTMLMediaElement.prototype, 'play').mockResolvedValue(undefined)
|
|
19
|
-
vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {})
|
|
20
|
-
vi.stubGlobal(
|
|
21
|
-
'requestAnimationFrame',
|
|
22
|
-
vi.fn(() => 1)
|
|
23
|
-
)
|
|
24
|
-
vi.stubGlobal('cancelAnimationFrame', vi.fn())
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
afterEach(() => {
|
|
28
|
-
vi.restoreAllMocks()
|
|
29
|
-
vi.unstubAllGlobals()
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('selects audio or video from the mime type and wires media props', () => {
|
|
33
|
-
const { container, rerender } = renderWithProviders(
|
|
34
|
-
<MediaPlayer
|
|
35
|
-
source={SOURCE}
|
|
36
|
-
mimeType="audio/mpeg"
|
|
37
|
-
loop
|
|
38
|
-
muted
|
|
39
|
-
controls={false}
|
|
40
|
-
/>
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
const audio = container.querySelector('audio') as HTMLAudioElement
|
|
44
|
-
expect(audio).toBeInTheDocument()
|
|
45
|
-
expect(audio).toHaveAttribute('src', SOURCE)
|
|
46
|
-
expect(audio.loop).toBe(true)
|
|
47
|
-
expect(audio.muted).toBe(true)
|
|
48
|
-
expect(container.querySelector('video')).toBeNull()
|
|
49
|
-
|
|
50
|
-
rerender(
|
|
51
|
-
<MediaPlayer
|
|
52
|
-
source={SOURCE}
|
|
53
|
-
mimeType="video/mp4"
|
|
54
|
-
loop
|
|
55
|
-
muted
|
|
56
|
-
controls={false}
|
|
57
|
-
/>
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
const video = container.querySelector('video') as HTMLVideoElement
|
|
61
|
-
expect(video).toBeInTheDocument()
|
|
62
|
-
expect(video).toHaveAttribute('src', SOURCE)
|
|
63
|
-
expect(video.loop).toBe(true)
|
|
64
|
-
expect(video.muted).toBe(true)
|
|
65
|
-
expect(video.playsInline).toBe(true)
|
|
66
|
-
expect(container.querySelector('audio')).toBeNull()
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('keeps an audio poster after canPlay but hides a video poster', () => {
|
|
70
|
-
const audio = renderWithProviders(
|
|
71
|
-
<MediaPlayer source={SOURCE} mimeType="audio/mpeg" poster={POSTER} />
|
|
72
|
-
)
|
|
73
|
-
const audioElement = audio.container.querySelector(
|
|
74
|
-
'audio'
|
|
75
|
-
) as HTMLAudioElement
|
|
76
|
-
expect(audio.container.querySelectorAll('img')).toHaveLength(1)
|
|
77
|
-
fireEvent.canPlay(audioElement)
|
|
78
|
-
expect(audio.container.querySelectorAll('img')).toHaveLength(1)
|
|
79
|
-
audio.unmount()
|
|
80
|
-
|
|
81
|
-
const video = renderWithProviders(
|
|
82
|
-
<MediaPlayer source={SOURCE} mimeType="video/mp4" poster={POSTER} />
|
|
83
|
-
)
|
|
84
|
-
const videoElement = video.container.querySelector(
|
|
85
|
-
'video'
|
|
86
|
-
) as HTMLVideoElement
|
|
87
|
-
expect(video.container.querySelectorAll('img')).toHaveLength(1)
|
|
88
|
-
fireEvent.canPlay(videoElement)
|
|
89
|
-
expect(video.container.querySelectorAll('img')).toHaveLength(0)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
it('uses a type icon as the initial placeholder when no poster is provided', async () => {
|
|
93
|
-
const { container } = renderWithProviders(
|
|
94
|
-
<MediaPlayer source={SOURCE} mimeType="video/mp4" />
|
|
95
|
-
)
|
|
96
|
-
const video = container.querySelector('video') as HTMLVideoElement
|
|
97
|
-
const placeholderIcon = () =>
|
|
98
|
-
Array.from(container.querySelectorAll('svg')).find((svg) =>
|
|
99
|
-
svg.classList.contains('size-12')
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
expect(placeholderIcon()).toBeInTheDocument()
|
|
103
|
-
fireEvent.canPlay(video)
|
|
104
|
-
await waitFor(() => expect(placeholderIcon()).toBeUndefined())
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('syncs controlled playback and calls play or pause imperatively', async () => {
|
|
108
|
-
const play = vi.mocked(HTMLMediaElement.prototype.play)
|
|
109
|
-
const pause = vi.mocked(HTMLMediaElement.prototype.pause)
|
|
110
|
-
const { container, rerender } = renderWithProviders(
|
|
111
|
-
<MediaPlayer source={SOURCE} mimeType="video/mp4" playing={false} />
|
|
112
|
-
)
|
|
113
|
-
const video = container.querySelector('video') as HTMLVideoElement
|
|
114
|
-
expect(pause).toHaveBeenCalled()
|
|
115
|
-
|
|
116
|
-
rerender(
|
|
117
|
-
<MediaPlayer source={SOURCE} mimeType="video/mp4" playing={true} />
|
|
118
|
-
)
|
|
119
|
-
await waitFor(() => expect(play).toHaveBeenCalledWith())
|
|
120
|
-
expect(video).toBeInTheDocument()
|
|
121
|
-
|
|
122
|
-
rerender(
|
|
123
|
-
<MediaPlayer source={SOURCE} mimeType="video/mp4" playing={false} />
|
|
124
|
-
)
|
|
125
|
-
await waitFor(() => expect(pause.mock.calls.length).toBeGreaterThan(1))
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('shows a manual play overlay after rejected playback and starts from a gesture', async () => {
|
|
129
|
-
const play = vi.mocked(HTMLMediaElement.prototype.play)
|
|
130
|
-
play.mockRejectedValueOnce(new Error('autoplay blocked'))
|
|
131
|
-
const onContainerClick = vi.fn()
|
|
132
|
-
|
|
133
|
-
renderWithProviders(
|
|
134
|
-
<MediaPlayer
|
|
135
|
-
source={SOURCE}
|
|
136
|
-
mimeType="video/mp4"
|
|
137
|
-
autoPlay
|
|
138
|
-
controls={false}
|
|
139
|
-
onContainerClick={onContainerClick}
|
|
140
|
-
/>
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
const overlay = await screen.findByRole('button', { name: 'Play preview' })
|
|
144
|
-
expect(play).toHaveBeenCalledTimes(1)
|
|
145
|
-
fireEvent.click(overlay)
|
|
146
|
-
await waitFor(() => expect(play).toHaveBeenCalledTimes(2))
|
|
147
|
-
expect(onContainerClick).not.toHaveBeenCalled()
|
|
148
|
-
expect(screen.queryByRole('button', { name: 'Play preview' })).toBeNull()
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('toggles from the container only with controls, while an override owns the click', async () => {
|
|
152
|
-
const play = vi.mocked(HTMLMediaElement.prototype.play)
|
|
153
|
-
const { container, rerender } = renderWithProviders(
|
|
154
|
-
<MediaPlayer source={SOURCE} mimeType="video/mp4" controls />
|
|
155
|
-
)
|
|
156
|
-
const root = container.firstElementChild as HTMLElement
|
|
157
|
-
|
|
158
|
-
fireEvent.click(root)
|
|
159
|
-
await waitFor(() =>
|
|
160
|
-
expect(screen.getByRole('button', { name: 'Pause' })).toBeInTheDocument()
|
|
161
|
-
)
|
|
162
|
-
fireEvent.click(root)
|
|
163
|
-
await waitFor(() =>
|
|
164
|
-
expect(screen.getByRole('button', { name: 'Play' })).toBeInTheDocument()
|
|
165
|
-
)
|
|
166
|
-
expect(play).toHaveBeenCalledTimes(1)
|
|
167
|
-
|
|
168
|
-
const onContainerClick = vi.fn()
|
|
169
|
-
rerender(
|
|
170
|
-
<MediaPlayer
|
|
171
|
-
source={SOURCE}
|
|
172
|
-
mimeType="video/mp4"
|
|
173
|
-
controls
|
|
174
|
-
onContainerClick={onContainerClick}
|
|
175
|
-
/>
|
|
176
|
-
)
|
|
177
|
-
fireEvent.click(root)
|
|
178
|
-
expect(onContainerClick).toHaveBeenCalledTimes(1)
|
|
179
|
-
expect(screen.getByRole('button', { name: 'Play' })).toBeInTheDocument()
|
|
180
|
-
fireEvent.keyDown(root, { key: 'Enter' })
|
|
181
|
-
expect(onContainerClick).toHaveBeenCalledTimes(2)
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('handles Enter and Space but ignores other container keys and prevents defaults', () => {
|
|
185
|
-
const { container } = renderWithProviders(
|
|
186
|
-
<MediaPlayer source={SOURCE} mimeType="video/mp4" controls />
|
|
187
|
-
)
|
|
188
|
-
const root = container.firstElementChild as HTMLElement
|
|
189
|
-
const pause = () => screen.getByRole('button', { name: 'Pause' })
|
|
190
|
-
|
|
191
|
-
const otherKey = new KeyboardEvent('keydown', {
|
|
192
|
-
key: 'ArrowDown',
|
|
193
|
-
bubbles: true,
|
|
194
|
-
cancelable: true,
|
|
195
|
-
})
|
|
196
|
-
fireEvent(root, otherKey)
|
|
197
|
-
expect(otherKey.defaultPrevented).toBe(false)
|
|
198
|
-
expect(screen.queryByRole('button', { name: 'Pause' })).toBeNull()
|
|
199
|
-
|
|
200
|
-
const enter = new KeyboardEvent('keydown', {
|
|
201
|
-
key: 'Enter',
|
|
202
|
-
bubbles: true,
|
|
203
|
-
cancelable: true,
|
|
204
|
-
})
|
|
205
|
-
fireEvent(root, enter)
|
|
206
|
-
expect(enter.defaultPrevented).toBe(true)
|
|
207
|
-
expect(pause()).toBeInTheDocument()
|
|
208
|
-
|
|
209
|
-
const space = new KeyboardEvent('keydown', {
|
|
210
|
-
key: ' ',
|
|
211
|
-
bubbles: true,
|
|
212
|
-
cancelable: true,
|
|
213
|
-
})
|
|
214
|
-
fireEvent(root, space)
|
|
215
|
-
expect(space.defaultPrevented).toBe(true)
|
|
216
|
-
expect(screen.getByRole('button', { name: 'Play' })).toBeInTheDocument()
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it('does not let the controls button bubble into the container toggle', async () => {
|
|
220
|
-
const { container } = renderWithProviders(
|
|
221
|
-
<MediaPlayer source={SOURCE} mimeType="video/mp4" controls />
|
|
222
|
-
)
|
|
223
|
-
const button = screen.getByRole('button', { name: 'Play' })
|
|
224
|
-
|
|
225
|
-
fireEvent.click(button)
|
|
226
|
-
await waitFor(() =>
|
|
227
|
-
expect(screen.getByRole('button', { name: 'Pause' })).toBeInTheDocument()
|
|
228
|
-
)
|
|
229
|
-
expect(container.firstElementChild).toBeInTheDocument()
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
it('shows buffering while loading and suppresses it after manual play is required', async () => {
|
|
233
|
-
const play = vi.mocked(HTMLMediaElement.prototype.play)
|
|
234
|
-
play.mockRejectedValueOnce(new Error('autoplay blocked'))
|
|
235
|
-
const { container } = renderWithProviders(
|
|
236
|
-
<MediaPlayer
|
|
237
|
-
source={SOURCE}
|
|
238
|
-
mimeType="video/mp4"
|
|
239
|
-
autoPlay
|
|
240
|
-
controls={false}
|
|
241
|
-
/>
|
|
242
|
-
)
|
|
243
|
-
const video = container.querySelector('video') as HTMLVideoElement
|
|
244
|
-
await screen.findByRole('button', { name: 'Play preview' })
|
|
245
|
-
|
|
246
|
-
fireEvent.loadStart(video)
|
|
247
|
-
expect(container.querySelector('.animate-spin')).toBeNull()
|
|
248
|
-
|
|
249
|
-
fireEvent.canPlay(video)
|
|
250
|
-
expect(container.querySelector('.animate-spin')).toBeNull()
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
it('shows and hides the buffering spinner around media readiness', () => {
|
|
254
|
-
const { container } = renderWithProviders(
|
|
255
|
-
<MediaPlayer source={SOURCE} mimeType="video/mp4" />
|
|
256
|
-
)
|
|
257
|
-
const video = container.querySelector('video') as HTMLVideoElement
|
|
258
|
-
|
|
259
|
-
fireEvent.loadStart(video)
|
|
260
|
-
expect(container.querySelector('.animate-spin')).toBeInTheDocument()
|
|
261
|
-
fireEvent.waiting(video)
|
|
262
|
-
expect(container.querySelector('.animate-spin')).toBeInTheDocument()
|
|
263
|
-
fireEvent.canPlay(video)
|
|
264
|
-
expect(container.querySelector('.animate-spin')).toBeNull()
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
it('seeks with keyboard and pointer input and expands the scrubber on hover', () => {
|
|
268
|
-
const { container } = renderWithProviders(
|
|
269
|
-
<MediaPlayer source={SOURCE} mimeType="video/mp4" controls showProgress />
|
|
270
|
-
)
|
|
271
|
-
const video = container.querySelector('video') as HTMLVideoElement
|
|
272
|
-
Object.defineProperty(video, 'duration', {
|
|
273
|
-
configurable: true,
|
|
274
|
-
value: 100,
|
|
275
|
-
})
|
|
276
|
-
const slider = screen.getByRole('slider', { name: 'Playback position' })
|
|
277
|
-
const track = slider.firstElementChild as HTMLElement
|
|
278
|
-
const thumb = slider.lastElementChild as HTMLElement
|
|
279
|
-
vi.spyOn(slider, 'getBoundingClientRect').mockReturnValue({
|
|
280
|
-
left: 100,
|
|
281
|
-
right: 300,
|
|
282
|
-
top: 0,
|
|
283
|
-
bottom: 10,
|
|
284
|
-
width: 200,
|
|
285
|
-
height: 10,
|
|
286
|
-
x: 100,
|
|
287
|
-
y: 0,
|
|
288
|
-
toJSON: () => ({}),
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
expect(track).toHaveClass('h-1')
|
|
292
|
-
expect(thumb).toHaveClass('scale-0', 'opacity-0')
|
|
293
|
-
fireEvent.mouseEnter(slider)
|
|
294
|
-
expect(track).toHaveClass('h-1.5')
|
|
295
|
-
expect(thumb).toHaveClass('scale-100', 'opacity-100')
|
|
296
|
-
fireEvent.mouseLeave(slider)
|
|
297
|
-
expect(track).toHaveClass('h-1')
|
|
298
|
-
|
|
299
|
-
fireEvent.mouseDown(slider, { clientX: 150 })
|
|
300
|
-
expect(slider).toHaveAttribute('aria-valuenow', '25')
|
|
301
|
-
expect(video.currentTime).toBe(25)
|
|
302
|
-
expect(track).toHaveClass('h-1.5')
|
|
303
|
-
expect(thumb).toHaveClass('scale-100', 'opacity-100')
|
|
304
|
-
|
|
305
|
-
fireEvent.mouseUp(window, { clientX: 150 })
|
|
306
|
-
fireEvent.keyDown(slider, { key: 'ArrowRight' })
|
|
307
|
-
expect(video.currentTime).toBe(30)
|
|
308
|
-
fireEvent.keyDown(slider, { key: 'ArrowLeft' })
|
|
309
|
-
expect(video.currentTime).toBe(20)
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
it('renders the progress-only track without controls when requested', () => {
|
|
313
|
-
const { container } = renderWithProviders(
|
|
314
|
-
<MediaPlayer
|
|
315
|
-
source={SOURCE}
|
|
316
|
-
mimeType="video/mp4"
|
|
317
|
-
controls={false}
|
|
318
|
-
showProgress
|
|
319
|
-
/>
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
expect(
|
|
323
|
-
screen.getByRole('slider', { name: 'Playback position' })
|
|
324
|
-
).toBeInTheDocument()
|
|
325
|
-
expect(screen.queryByRole('button', { name: 'Play' })).toBeNull()
|
|
326
|
-
expect(container.querySelectorAll('[role="slider"]')).toHaveLength(1)
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
it('uses natural video dimensions after metadata and aspect-video before then', () => {
|
|
330
|
-
const { container } = renderWithProviders(
|
|
331
|
-
<MediaPlayer source={SOURCE} mimeType="video/mp4" />
|
|
332
|
-
)
|
|
333
|
-
const root = container.firstElementChild as HTMLElement
|
|
334
|
-
const video = container.querySelector('video') as HTMLVideoElement
|
|
335
|
-
Object.defineProperties(video, {
|
|
336
|
-
videoWidth: { configurable: true, value: 1920 },
|
|
337
|
-
videoHeight: { configurable: true, value: 1080 },
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
expect(root).toHaveClass('aspect-video')
|
|
341
|
-
expect(root).not.toHaveStyle({ aspectRatio: '1.7777777777777777' })
|
|
342
|
-
fireEvent.loadedMetadata(video)
|
|
343
|
-
expect(root).not.toHaveClass('aspect-video')
|
|
344
|
-
expect(root.style.aspectRatio).toBe(String(1920 / 1080))
|
|
345
|
-
})
|
|
346
|
-
})
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import { fireEvent, renderWithProviders, screen } from '../../test/utils'
|
|
5
|
-
|
|
6
|
-
import AttachmentThumbnail from './Thumbnail'
|
|
7
|
-
|
|
8
|
-
vi.mock('./MediaPlayer', () => ({
|
|
9
|
-
default: ({
|
|
10
|
-
source,
|
|
11
|
-
mimeType,
|
|
12
|
-
poster,
|
|
13
|
-
controls,
|
|
14
|
-
loop,
|
|
15
|
-
muted,
|
|
16
|
-
}: {
|
|
17
|
-
source: string
|
|
18
|
-
mimeType: string
|
|
19
|
-
poster?: string
|
|
20
|
-
controls?: boolean
|
|
21
|
-
loop?: boolean
|
|
22
|
-
muted?: boolean
|
|
23
|
-
}) => (
|
|
24
|
-
<div
|
|
25
|
-
data-testid="media-player"
|
|
26
|
-
data-source={source}
|
|
27
|
-
data-mime-type={mimeType}
|
|
28
|
-
data-poster={poster}
|
|
29
|
-
data-controls={String(controls)}
|
|
30
|
-
data-loop={String(loop)}
|
|
31
|
-
data-muted={String(muted)}
|
|
32
|
-
/>
|
|
33
|
-
),
|
|
34
|
-
}))
|
|
35
|
-
|
|
36
|
-
const LINKTREE_SOURCE = 'https://cdn.linktr.ee/attachment/photo.jpg'
|
|
37
|
-
const LINKTREE_THUMBNAIL = 'https://cdn.linktr.ee/attachment/poster.jpg'
|
|
38
|
-
const OPTIMIZED_SOURCE =
|
|
39
|
-
'https://cdn.linktr.ee/attachment/photo.jpg?io=true&size=messaging-attachment-v1_0'
|
|
40
|
-
const OPTIMIZED_THUMBNAIL =
|
|
41
|
-
'https://cdn.linktr.ee/attachment/poster.jpg?io=true&size=messaging-attachment-v1_0'
|
|
42
|
-
|
|
43
|
-
describe('AttachmentThumbnail', () => {
|
|
44
|
-
it('delegates video and audio sources to MediaPlayer with an optimized poster', () => {
|
|
45
|
-
const { rerender } = renderWithProviders(
|
|
46
|
-
<AttachmentThumbnail
|
|
47
|
-
mimeType="video/mp4"
|
|
48
|
-
sourceUrl={LINKTREE_SOURCE}
|
|
49
|
-
thumbnailUrl={LINKTREE_THUMBNAIL}
|
|
50
|
-
variant="dark"
|
|
51
|
-
mediaPlayerProps={{ controls: false, loop: true, muted: true }}
|
|
52
|
-
/>
|
|
53
|
-
)
|
|
54
|
-
const player = screen.getByTestId('media-player')
|
|
55
|
-
expect(player).toHaveAttribute('data-source', LINKTREE_SOURCE)
|
|
56
|
-
expect(player).toHaveAttribute('data-mime-type', 'video/mp4')
|
|
57
|
-
expect(player).toHaveAttribute('data-poster', OPTIMIZED_THUMBNAIL)
|
|
58
|
-
expect(player).toHaveAttribute('data-controls', 'false')
|
|
59
|
-
expect(player).toHaveAttribute('data-loop', 'true')
|
|
60
|
-
expect(player).toHaveAttribute('data-muted', 'true')
|
|
61
|
-
|
|
62
|
-
rerender(
|
|
63
|
-
<AttachmentThumbnail
|
|
64
|
-
mimeType="audio/mpeg"
|
|
65
|
-
sourceUrl="https://external.example/track.mp3"
|
|
66
|
-
variant="light"
|
|
67
|
-
/>
|
|
68
|
-
)
|
|
69
|
-
expect(screen.getByTestId('media-player')).toHaveAttribute(
|
|
70
|
-
'data-source',
|
|
71
|
-
'https://external.example/track.mp3'
|
|
72
|
-
)
|
|
73
|
-
expect(screen.getByTestId('media-player')).toHaveAttribute(
|
|
74
|
-
'data-controls',
|
|
75
|
-
'true'
|
|
76
|
-
)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('renders an image with an optimized source and contained fade-in transition', () => {
|
|
80
|
-
const { container } = renderWithProviders(
|
|
81
|
-
<AttachmentThumbnail
|
|
82
|
-
mimeType="image/jpeg"
|
|
83
|
-
sourceUrl={LINKTREE_SOURCE}
|
|
84
|
-
title="Photo"
|
|
85
|
-
variant="light"
|
|
86
|
-
containedImage
|
|
87
|
-
/>
|
|
88
|
-
)
|
|
89
|
-
const image = screen.getByRole('img', { name: 'Photo' })
|
|
90
|
-
expect(image).toHaveAttribute('src', OPTIMIZED_SOURCE)
|
|
91
|
-
expect(image).toHaveClass('opacity-0')
|
|
92
|
-
fireEvent.load(image)
|
|
93
|
-
expect(image).toHaveClass('opacity-100')
|
|
94
|
-
expect(container.firstElementChild).toHaveClass('aspect-video')
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('renders a non-contained image and falls back to the raw URL', () => {
|
|
98
|
-
renderWithProviders(
|
|
99
|
-
<AttachmentThumbnail
|
|
100
|
-
mimeType="image/jpeg"
|
|
101
|
-
sourceUrl="https://external.example/photo.jpg"
|
|
102
|
-
variant="light"
|
|
103
|
-
/>
|
|
104
|
-
)
|
|
105
|
-
expect(screen.getByRole('img')).toHaveAttribute(
|
|
106
|
-
'src',
|
|
107
|
-
'https://external.example/photo.jpg'
|
|
108
|
-
)
|
|
109
|
-
expect(screen.getByRole('img')).toHaveClass('block', 'w-full')
|
|
110
|
-
expect(screen.getByRole('img')).toHaveAttribute('alt', '')
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('renders document thumbnails contained or full width, using optimized URLs', () => {
|
|
114
|
-
const { rerender, container } = renderWithProviders(
|
|
115
|
-
<AttachmentThumbnail
|
|
116
|
-
mimeType="application/pdf"
|
|
117
|
-
sourceUrl="https://external.example/document.pdf"
|
|
118
|
-
thumbnailUrl={LINKTREE_THUMBNAIL}
|
|
119
|
-
title="Document"
|
|
120
|
-
variant="dark"
|
|
121
|
-
containedImage
|
|
122
|
-
/>
|
|
123
|
-
)
|
|
124
|
-
const contained = screen.getByRole('img', { name: 'Document' })
|
|
125
|
-
expect(contained).toHaveAttribute('src', OPTIMIZED_THUMBNAIL)
|
|
126
|
-
expect(contained).toHaveClass('opacity-0')
|
|
127
|
-
fireEvent.load(contained)
|
|
128
|
-
expect(contained).toHaveClass('opacity-100')
|
|
129
|
-
expect(container.firstElementChild).toHaveClass('aspect-video')
|
|
130
|
-
|
|
131
|
-
rerender(
|
|
132
|
-
<AttachmentThumbnail
|
|
133
|
-
mimeType="application/pdf"
|
|
134
|
-
sourceUrl="https://external.example/document.pdf"
|
|
135
|
-
thumbnailUrl="https://external.example/thumb.png"
|
|
136
|
-
title="Document"
|
|
137
|
-
variant="light"
|
|
138
|
-
/>
|
|
139
|
-
)
|
|
140
|
-
const fullWidth = screen.getByRole('img')
|
|
141
|
-
expect(fullWidth).toHaveAttribute(
|
|
142
|
-
'src',
|
|
143
|
-
'https://external.example/thumb.png'
|
|
144
|
-
)
|
|
145
|
-
expect(fullWidth).toHaveAttribute('alt', '')
|
|
146
|
-
expect(fullWidth).toHaveClass('block', 'w-full')
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('renders a document icon placeholder without a thumbnail', () => {
|
|
150
|
-
const dark = renderWithProviders(
|
|
151
|
-
<AttachmentThumbnail
|
|
152
|
-
mimeType="application/pdf"
|
|
153
|
-
variant="dark"
|
|
154
|
-
title="PDF"
|
|
155
|
-
/>
|
|
156
|
-
)
|
|
157
|
-
const darkIcon = dark.container.querySelector('svg') as SVGElement
|
|
158
|
-
expect(darkIcon).toHaveClass('size-12', 'text-white/20')
|
|
159
|
-
expect(dark.container.firstElementChild).toHaveClass('bg-white/10')
|
|
160
|
-
dark.unmount()
|
|
161
|
-
|
|
162
|
-
const light = renderWithProviders(
|
|
163
|
-
<AttachmentThumbnail mimeType="application/pdf" variant="light" />
|
|
164
|
-
)
|
|
165
|
-
const lightIcon = light.container.querySelector('svg') as SVGElement
|
|
166
|
-
expect(lightIcon).toHaveClass('size-12', 'text-black/20')
|
|
167
|
-
expect(light.container.firstElementChild).toHaveClass('bg-black/5')
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('renders the poster-only branch with variant-specific shell and alt fallback', () => {
|
|
171
|
-
const dark = renderWithProviders(
|
|
172
|
-
<AttachmentThumbnail
|
|
173
|
-
mimeType="image/jpeg"
|
|
174
|
-
thumbnailUrl={LINKTREE_THUMBNAIL}
|
|
175
|
-
title="Poster"
|
|
176
|
-
variant="dark"
|
|
177
|
-
/>
|
|
178
|
-
)
|
|
179
|
-
const darkImage = screen.getByRole('img', { name: 'Poster' })
|
|
180
|
-
expect(darkImage).toHaveAttribute('src', OPTIMIZED_THUMBNAIL)
|
|
181
|
-
expect(dark.container.firstElementChild).toHaveClass(
|
|
182
|
-
'aspect-video',
|
|
183
|
-
'bg-white/10'
|
|
184
|
-
)
|
|
185
|
-
dark.unmount()
|
|
186
|
-
|
|
187
|
-
const light = renderWithProviders(
|
|
188
|
-
<AttachmentThumbnail
|
|
189
|
-
mimeType="image/jpeg"
|
|
190
|
-
thumbnailUrl="https://external.example/poster.jpg"
|
|
191
|
-
variant="light"
|
|
192
|
-
/>
|
|
193
|
-
)
|
|
194
|
-
expect(screen.getByRole('img')).toHaveAttribute('alt', '')
|
|
195
|
-
expect(light.container.firstElementChild).toHaveClass(
|
|
196
|
-
'aspect-video',
|
|
197
|
-
'bg-black/5'
|
|
198
|
-
)
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('renders the empty branch with a light placeholder', () => {
|
|
202
|
-
const { container } = renderWithProviders(
|
|
203
|
-
<AttachmentThumbnail
|
|
204
|
-
mimeType="application/octet-stream"
|
|
205
|
-
variant="light"
|
|
206
|
-
/>
|
|
207
|
-
)
|
|
208
|
-
expect(container.querySelector('img')).toBeNull()
|
|
209
|
-
expect(container.querySelector('svg')).toHaveClass(
|
|
210
|
-
'size-12',
|
|
211
|
-
'text-black/20'
|
|
212
|
-
)
|
|
213
|
-
expect(container.firstElementChild).toHaveClass('bg-black/5')
|
|
214
|
-
})
|
|
215
|
-
})
|