@linktr.ee/messaging-react 4.4.3 → 4.4.4-rc-1788190776
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/AttachmentCard-BbsjzdLJ.cjs +2 -0
- package/dist/AttachmentCard-BbsjzdLJ.cjs.map +1 -0
- package/dist/{AttachmentCard-BqwWgqvi.js → AttachmentCard-DSekwIOA.js} +35 -20
- package/dist/AttachmentCard-DSekwIOA.js.map +1 -0
- package/dist/{Card-BIbqWagz.cjs → Card-BS6Fza2a.cjs} +2 -2
- package/dist/{Card-BIbqWagz.cjs.map → Card-BS6Fza2a.cjs.map} +1 -1
- package/dist/{Card-f51K6V7Y.js → Card-BhwBkgxI.js} +2 -2
- package/dist/{Card-f51K6V7Y.js.map → Card-BhwBkgxI.js.map} +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +67 -42
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/AttachmentCard/MediaPlayer.test.tsx +62 -85
- package/src/components/AttachmentCard/MediaPlayer.tsx +14 -2
- package/src/components/AttachmentCard/Thumbnail.test.tsx +37 -46
- package/src/components/AttachmentCard/index.test.tsx +18 -46
- package/src/components/AttachmentCard/index.tsx +8 -2
- package/src/components/AttachmentCard/utils/icons.ts +11 -1
- package/src/components/Avatar/Avatar.test.tsx +37 -64
- package/src/components/Avatar/index.tsx +8 -1
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +19 -19
- package/src/components/ChannelList/CustomChannelPreview.tsx +8 -2
- package/src/components/ChannelView.outbound-resolution.integration.test.tsx +25 -30
- package/src/components/ChannelView.test.tsx +17 -31
- package/src/components/ChannelView.tsx +1 -0
- package/src/components/CustomLinkPreviewList/CustomLinkPreviewList.test.tsx +8 -5
- package/src/components/CustomLinkPreviewList/index.tsx +4 -1
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +26 -21
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +2 -0
- package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +43 -56
- package/src/components/CustomMessageInput/index.tsx +25 -5
- package/src/components/CustomSystemMessage/CustomSystemMessage.test.tsx +17 -23
- package/src/components/FaqList/FaqList.test.tsx +5 -4
- package/src/components/FaqList/index.tsx +6 -1
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +70 -81
- package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +1 -0
- package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +20 -61
- package/src/components/LinkAttachment/components/_shared/CardBody.tsx +10 -2
- package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +1 -21
- package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +11 -37
- package/src/components/LinkAttachment/components/_shared/CardShell.tsx +5 -2
- package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +65 -72
- package/src/components/LinkAttachment/components/_shared/CardThumbnail.tsx +13 -2
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +25 -8
- package/src/components/MessageAttachment/_shared/CompactDocumentRow.test.tsx +5 -19
- package/src/components/MessageAttachment/_shared/MediaStackGrid.test.tsx +7 -9
- package/src/components/MessageAttachment/_shared/VideoViewer.test.tsx +9 -2
- package/src/components/MessageAttachment/_shared/VideoViewer.tsx +1 -0
- package/src/components/MessageAttachment/_shared/ViewerShell.test.tsx +2 -2
- package/src/components/MessageBubble/MessageBubble.test.tsx +0 -1
- package/src/components/RichCard/RichCard.test.tsx +24 -54
- package/src/components/RichCard/RichCardBody.tsx +4 -1
- package/src/hooks/useChannelModerationActions.ts +15 -30
- package/src/logging/index.test.tsx +1 -0
- package/src/providers/MessagingProvider.test.tsx +37 -41
- package/dist/AttachmentCard-BqwWgqvi.js.map +0 -1
- package/dist/AttachmentCard-DUrgGRsq.cjs +0 -2
- package/dist/AttachmentCard-DUrgGRsq.cjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.4-rc-1788190776",
|
|
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": "
|
|
54
|
-
"@linktr.ee/messaging-taxonomy": "
|
|
53
|
+
"@linktr.ee/messaging-core": "2.6.0-rc-1788190776",
|
|
54
|
+
"@linktr.ee/messaging-taxonomy": "0.11.0-rc-1788190776",
|
|
55
55
|
"@phosphor-icons/react": "^2.1.10"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
@@ -30,7 +30,7 @@ describe('MediaPlayer', () => {
|
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
it('selects audio or video from the mime type and wires media props', () => {
|
|
33
|
-
const {
|
|
33
|
+
const { rerender } = renderWithProviders(
|
|
34
34
|
<MediaPlayer
|
|
35
35
|
source={SOURCE}
|
|
36
36
|
mimeType="audio/mpeg"
|
|
@@ -40,12 +40,11 @@ describe('MediaPlayer', () => {
|
|
|
40
40
|
/>
|
|
41
41
|
)
|
|
42
42
|
|
|
43
|
-
const audio =
|
|
44
|
-
expect(audio).toBeInTheDocument()
|
|
43
|
+
const audio = screen.getByTestId('media-player-audio') as HTMLAudioElement
|
|
45
44
|
expect(audio).toHaveAttribute('src', SOURCE)
|
|
46
45
|
expect(audio.loop).toBe(true)
|
|
47
46
|
expect(audio.muted).toBe(true)
|
|
48
|
-
expect(
|
|
47
|
+
expect(screen.queryByTestId('media-player-video')).not.toBeInTheDocument()
|
|
49
48
|
|
|
50
49
|
rerender(
|
|
51
50
|
<MediaPlayer
|
|
@@ -57,67 +56,61 @@ describe('MediaPlayer', () => {
|
|
|
57
56
|
/>
|
|
58
57
|
)
|
|
59
58
|
|
|
60
|
-
const video =
|
|
61
|
-
expect(video).toBeInTheDocument()
|
|
59
|
+
const video = screen.getByTestId('media-player-video') as HTMLVideoElement
|
|
62
60
|
expect(video).toHaveAttribute('src', SOURCE)
|
|
63
61
|
expect(video.loop).toBe(true)
|
|
64
62
|
expect(video.muted).toBe(true)
|
|
65
63
|
expect(video.playsInline).toBe(true)
|
|
66
|
-
expect(
|
|
64
|
+
expect(screen.queryByTestId('media-player-audio')).not.toBeInTheDocument()
|
|
67
65
|
})
|
|
68
66
|
|
|
69
67
|
it('keeps an audio poster after canPlay but hides a video poster', () => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
'
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
'video'
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
68
|
+
{
|
|
69
|
+
const view = renderWithProviders(
|
|
70
|
+
<MediaPlayer source={SOURCE} mimeType="audio/mpeg" poster={POSTER} />
|
|
71
|
+
)
|
|
72
|
+
expect(screen.getByTestId('media-player-poster')).toBeInTheDocument()
|
|
73
|
+
fireEvent.canPlay(screen.getByTestId('media-player-audio'))
|
|
74
|
+
expect(screen.getByTestId('media-player-poster')).toBeInTheDocument()
|
|
75
|
+
view.unmount()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
renderWithProviders(
|
|
80
|
+
<MediaPlayer source={SOURCE} mimeType="video/mp4" poster={POSTER} />
|
|
81
|
+
)
|
|
82
|
+
expect(screen.getByTestId('media-player-poster')).toBeInTheDocument()
|
|
83
|
+
fireEvent.canPlay(screen.getByTestId('media-player-video'))
|
|
84
|
+
expect(
|
|
85
|
+
screen.queryByTestId('media-player-poster')
|
|
86
|
+
).not.toBeInTheDocument()
|
|
87
|
+
}
|
|
90
88
|
})
|
|
91
89
|
|
|
92
90
|
it('uses a type icon as the initial placeholder when no poster is provided', async () => {
|
|
93
|
-
|
|
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
|
-
)
|
|
91
|
+
renderWithProviders(<MediaPlayer source={SOURCE} mimeType="video/mp4" />)
|
|
101
92
|
|
|
102
|
-
expect(
|
|
103
|
-
fireEvent.canPlay(video)
|
|
104
|
-
await waitFor(() =>
|
|
93
|
+
expect(screen.getByTestId('media-player-placeholder')).toBeInTheDocument()
|
|
94
|
+
fireEvent.canPlay(screen.getByTestId('media-player-video'))
|
|
95
|
+
await waitFor(() =>
|
|
96
|
+
expect(
|
|
97
|
+
screen.queryByTestId('media-player-placeholder')
|
|
98
|
+
).not.toBeInTheDocument()
|
|
99
|
+
)
|
|
105
100
|
})
|
|
106
101
|
|
|
107
102
|
it('syncs controlled playback and calls play or pause imperatively', async () => {
|
|
108
103
|
const play = vi.mocked(HTMLMediaElement.prototype.play)
|
|
109
104
|
const pause = vi.mocked(HTMLMediaElement.prototype.pause)
|
|
110
|
-
const {
|
|
105
|
+
const { rerender } = renderWithProviders(
|
|
111
106
|
<MediaPlayer source={SOURCE} mimeType="video/mp4" playing={false} />
|
|
112
107
|
)
|
|
113
|
-
const video = container.querySelector('video') as HTMLVideoElement
|
|
114
108
|
expect(pause).toHaveBeenCalled()
|
|
115
109
|
|
|
116
110
|
rerender(
|
|
117
111
|
<MediaPlayer source={SOURCE} mimeType="video/mp4" playing={true} />
|
|
118
112
|
)
|
|
119
113
|
await waitFor(() => expect(play).toHaveBeenCalledWith())
|
|
120
|
-
expect(video).toBeInTheDocument()
|
|
121
114
|
|
|
122
115
|
rerender(
|
|
123
116
|
<MediaPlayer source={SOURCE} mimeType="video/mp4" playing={false} />
|
|
@@ -150,19 +143,15 @@ describe('MediaPlayer', () => {
|
|
|
150
143
|
|
|
151
144
|
it('toggles from the container only with controls, while an override owns the click', async () => {
|
|
152
145
|
const play = vi.mocked(HTMLMediaElement.prototype.play)
|
|
153
|
-
const {
|
|
146
|
+
const { rerender } = renderWithProviders(
|
|
154
147
|
<MediaPlayer source={SOURCE} mimeType="video/mp4" controls />
|
|
155
148
|
)
|
|
156
|
-
const root =
|
|
149
|
+
const root = screen.getByTestId('media-player')
|
|
157
150
|
|
|
158
151
|
fireEvent.click(root)
|
|
159
|
-
await
|
|
160
|
-
expect(screen.getByRole('button', { name: 'Pause' })).toBeInTheDocument()
|
|
161
|
-
)
|
|
152
|
+
await screen.findByRole('button', { name: 'Pause' })
|
|
162
153
|
fireEvent.click(root)
|
|
163
|
-
await
|
|
164
|
-
expect(screen.getByRole('button', { name: 'Play' })).toBeInTheDocument()
|
|
165
|
-
)
|
|
154
|
+
await screen.findByRole('button', { name: 'Play' })
|
|
166
155
|
expect(play).toHaveBeenCalledTimes(1)
|
|
167
156
|
|
|
168
157
|
const onContainerClick = vi.fn()
|
|
@@ -182,10 +171,10 @@ describe('MediaPlayer', () => {
|
|
|
182
171
|
})
|
|
183
172
|
|
|
184
173
|
it('handles Enter and Space but ignores other container keys and prevents defaults', () => {
|
|
185
|
-
|
|
174
|
+
renderWithProviders(
|
|
186
175
|
<MediaPlayer source={SOURCE} mimeType="video/mp4" controls />
|
|
187
176
|
)
|
|
188
|
-
const root =
|
|
177
|
+
const root = screen.getByTestId('media-player')
|
|
189
178
|
const pause = () => screen.getByRole('button', { name: 'Pause' })
|
|
190
179
|
|
|
191
180
|
const otherKey = new KeyboardEvent('keydown', {
|
|
@@ -217,22 +206,18 @@ describe('MediaPlayer', () => {
|
|
|
217
206
|
})
|
|
218
207
|
|
|
219
208
|
it('does not let the controls button bubble into the container toggle', async () => {
|
|
220
|
-
|
|
209
|
+
renderWithProviders(
|
|
221
210
|
<MediaPlayer source={SOURCE} mimeType="video/mp4" controls />
|
|
222
211
|
)
|
|
223
|
-
const button = screen.getByRole('button', { name: 'Play' })
|
|
224
212
|
|
|
225
|
-
fireEvent.click(button)
|
|
226
|
-
await
|
|
227
|
-
expect(screen.getByRole('button', { name: 'Pause' })).toBeInTheDocument()
|
|
228
|
-
)
|
|
229
|
-
expect(container.firstElementChild).toBeInTheDocument()
|
|
213
|
+
fireEvent.click(screen.getByRole('button', { name: 'Play' }))
|
|
214
|
+
await screen.findByRole('button', { name: 'Pause' })
|
|
230
215
|
})
|
|
231
216
|
|
|
232
217
|
it('shows buffering while loading and suppresses it after manual play is required', async () => {
|
|
233
218
|
const play = vi.mocked(HTMLMediaElement.prototype.play)
|
|
234
219
|
play.mockRejectedValueOnce(new Error('autoplay blocked'))
|
|
235
|
-
|
|
220
|
+
renderWithProviders(
|
|
236
221
|
<MediaPlayer
|
|
237
222
|
source={SOURCE}
|
|
238
223
|
mimeType="video/mp4"
|
|
@@ -240,42 +225,39 @@ describe('MediaPlayer', () => {
|
|
|
240
225
|
controls={false}
|
|
241
226
|
/>
|
|
242
227
|
)
|
|
243
|
-
const video =
|
|
228
|
+
const video = screen.getByTestId('media-player-video')
|
|
244
229
|
await screen.findByRole('button', { name: 'Play preview' })
|
|
245
230
|
|
|
246
231
|
fireEvent.loadStart(video)
|
|
247
|
-
expect(
|
|
232
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
|
248
233
|
|
|
249
234
|
fireEvent.canPlay(video)
|
|
250
|
-
expect(
|
|
235
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
|
251
236
|
})
|
|
252
237
|
|
|
253
238
|
it('shows and hides the buffering spinner around media readiness', () => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)
|
|
257
|
-
const video = container.querySelector('video') as HTMLVideoElement
|
|
239
|
+
renderWithProviders(<MediaPlayer source={SOURCE} mimeType="video/mp4" />)
|
|
240
|
+
const video = screen.getByTestId('media-player-video')
|
|
258
241
|
|
|
259
242
|
fireEvent.loadStart(video)
|
|
260
|
-
expect(
|
|
243
|
+
expect(screen.getByRole('status')).toBeInTheDocument()
|
|
261
244
|
fireEvent.waiting(video)
|
|
262
|
-
expect(
|
|
245
|
+
expect(screen.getByRole('status')).toBeInTheDocument()
|
|
263
246
|
fireEvent.canPlay(video)
|
|
264
|
-
expect(
|
|
247
|
+
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
|
265
248
|
})
|
|
266
249
|
|
|
267
|
-
it('seeks with keyboard and pointer input and
|
|
268
|
-
|
|
250
|
+
it('seeks with keyboard and pointer input and reveals the scrubber thumb on hover', () => {
|
|
251
|
+
renderWithProviders(
|
|
269
252
|
<MediaPlayer source={SOURCE} mimeType="video/mp4" controls showProgress />
|
|
270
253
|
)
|
|
271
|
-
const video =
|
|
254
|
+
const video = screen.getByTestId('media-player-video') as HTMLVideoElement
|
|
272
255
|
Object.defineProperty(video, 'duration', {
|
|
273
256
|
configurable: true,
|
|
274
257
|
value: 100,
|
|
275
258
|
})
|
|
276
259
|
const slider = screen.getByRole('slider', { name: 'Playback position' })
|
|
277
|
-
const
|
|
278
|
-
const thumb = slider.lastElementChild as HTMLElement
|
|
260
|
+
const thumb = screen.getByTestId('media-player-scrubber-thumb')
|
|
279
261
|
vi.spyOn(slider, 'getBoundingClientRect').mockReturnValue({
|
|
280
262
|
left: 100,
|
|
281
263
|
right: 300,
|
|
@@ -288,18 +270,15 @@ describe('MediaPlayer', () => {
|
|
|
288
270
|
toJSON: () => ({}),
|
|
289
271
|
})
|
|
290
272
|
|
|
291
|
-
expect(track).toHaveClass('h-1')
|
|
292
273
|
expect(thumb).toHaveClass('scale-0', 'opacity-0')
|
|
293
274
|
fireEvent.mouseEnter(slider)
|
|
294
|
-
expect(track).toHaveClass('h-1.5')
|
|
295
275
|
expect(thumb).toHaveClass('scale-100', 'opacity-100')
|
|
296
276
|
fireEvent.mouseLeave(slider)
|
|
297
|
-
expect(
|
|
277
|
+
expect(thumb).toHaveClass('scale-0', 'opacity-0')
|
|
298
278
|
|
|
299
279
|
fireEvent.mouseDown(slider, { clientX: 150 })
|
|
300
280
|
expect(slider).toHaveAttribute('aria-valuenow', '25')
|
|
301
281
|
expect(video.currentTime).toBe(25)
|
|
302
|
-
expect(track).toHaveClass('h-1.5')
|
|
303
282
|
expect(thumb).toHaveClass('scale-100', 'opacity-100')
|
|
304
283
|
|
|
305
284
|
fireEvent.mouseUp(window, { clientX: 150 })
|
|
@@ -310,7 +289,7 @@ describe('MediaPlayer', () => {
|
|
|
310
289
|
})
|
|
311
290
|
|
|
312
291
|
it('renders the progress-only track without controls when requested', () => {
|
|
313
|
-
|
|
292
|
+
renderWithProviders(
|
|
314
293
|
<MediaPlayer
|
|
315
294
|
source={SOURCE}
|
|
316
295
|
mimeType="video/mp4"
|
|
@@ -323,15 +302,13 @@ describe('MediaPlayer', () => {
|
|
|
323
302
|
screen.getByRole('slider', { name: 'Playback position' })
|
|
324
303
|
).toBeInTheDocument()
|
|
325
304
|
expect(screen.queryByRole('button', { name: 'Play' })).toBeNull()
|
|
326
|
-
expect(
|
|
305
|
+
expect(screen.getAllByRole('slider')).toHaveLength(1)
|
|
327
306
|
})
|
|
328
307
|
|
|
329
308
|
it('uses natural video dimensions after metadata and aspect-video before then', () => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
)
|
|
333
|
-
const root = container.firstElementChild as HTMLElement
|
|
334
|
-
const video = container.querySelector('video') as HTMLVideoElement
|
|
309
|
+
renderWithProviders(<MediaPlayer source={SOURCE} mimeType="video/mp4" />)
|
|
310
|
+
const root = screen.getByTestId('media-player')
|
|
311
|
+
const video = screen.getByTestId('media-player-video') as HTMLVideoElement
|
|
335
312
|
Object.defineProperties(video, {
|
|
336
313
|
videoWidth: { configurable: true, value: 1920 },
|
|
337
314
|
videoHeight: { configurable: true, value: 1080 },
|
|
@@ -196,6 +196,7 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
|
|
|
196
196
|
<div
|
|
197
197
|
role="button"
|
|
198
198
|
tabIndex={0}
|
|
199
|
+
data-testid="media-player"
|
|
199
200
|
className={`relative cursor-pointer overflow-hidden bg-black ${aspectClass}`}
|
|
200
201
|
style={aspectStyle}
|
|
201
202
|
onClick={(e) => {
|
|
@@ -222,11 +223,15 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
|
|
|
222
223
|
<img
|
|
223
224
|
src={poster}
|
|
224
225
|
alt=""
|
|
226
|
+
data-testid="media-player-poster"
|
|
225
227
|
className="absolute inset-0 h-full w-full object-cover"
|
|
226
228
|
/>
|
|
227
229
|
)}
|
|
228
230
|
{!poster && (sourceType === 'audio' || initialLoad) && (
|
|
229
|
-
<div
|
|
231
|
+
<div
|
|
232
|
+
data-testid="media-player-placeholder"
|
|
233
|
+
className="absolute inset-0 flex items-center justify-center"
|
|
234
|
+
>
|
|
230
235
|
{renderTypeIcon(mimeType, {
|
|
231
236
|
className: 'size-12 text-black/20',
|
|
232
237
|
weight: 'regular',
|
|
@@ -238,6 +243,7 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
|
|
|
238
243
|
<audio
|
|
239
244
|
{...nativePlayerProps}
|
|
240
245
|
ref={playerRef as React.RefObject<HTMLAudioElement>}
|
|
246
|
+
data-testid="media-player-audio"
|
|
241
247
|
src={source}
|
|
242
248
|
loop={loop}
|
|
243
249
|
muted={muted}
|
|
@@ -261,6 +267,7 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
|
|
|
261
267
|
) : (
|
|
262
268
|
<video
|
|
263
269
|
ref={playerRef as React.RefObject<HTMLVideoElement>}
|
|
270
|
+
data-testid="media-player-video"
|
|
264
271
|
src={source}
|
|
265
272
|
loop={loop}
|
|
266
273
|
muted={muted}
|
|
@@ -296,7 +303,11 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
|
|
|
296
303
|
</div>
|
|
297
304
|
|
|
298
305
|
{buffering && !manualPlayRequired && (
|
|
299
|
-
<div
|
|
306
|
+
<div
|
|
307
|
+
role="status"
|
|
308
|
+
aria-label="Loading media"
|
|
309
|
+
className="absolute inset-0 z-10 flex items-center justify-center"
|
|
310
|
+
>
|
|
300
311
|
<CircleNotchIcon
|
|
301
312
|
className="size-8 animate-spin text-white/80"
|
|
302
313
|
weight="bold"
|
|
@@ -396,6 +407,7 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
|
|
|
396
407
|
/>
|
|
397
408
|
</div>
|
|
398
409
|
<div
|
|
410
|
+
data-testid="media-player-scrubber-thumb"
|
|
399
411
|
className={`absolute size-3 -translate-x-1/2 rounded-full bg-white shadow transition-[opacity,transform] duration-200 ${scrubberHovered || seeking ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
|
|
400
412
|
style={{ left: `${scrubberPercent}%` }}
|
|
401
413
|
/>
|
|
@@ -147,69 +147,60 @@ describe('AttachmentThumbnail', () => {
|
|
|
147
147
|
})
|
|
148
148
|
|
|
149
149
|
it('renders a document icon placeholder without a thumbnail', () => {
|
|
150
|
-
|
|
150
|
+
renderWithProviders(
|
|
151
151
|
<AttachmentThumbnail
|
|
152
152
|
mimeType="application/pdf"
|
|
153
153
|
variant="dark"
|
|
154
154
|
title="PDF"
|
|
155
155
|
/>
|
|
156
156
|
)
|
|
157
|
-
|
|
158
|
-
expect(
|
|
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')
|
|
157
|
+
expect(screen.queryByRole('img')).toBeNull()
|
|
158
|
+
expect(screen.getByTestId('type-icon-pdf')).toBeInTheDocument()
|
|
168
159
|
})
|
|
169
160
|
|
|
170
161
|
it('renders the poster-only branch with variant-specific shell and alt fallback', () => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
162
|
+
{
|
|
163
|
+
const view = renderWithProviders(
|
|
164
|
+
<AttachmentThumbnail
|
|
165
|
+
mimeType="image/jpeg"
|
|
166
|
+
thumbnailUrl={LINKTREE_THUMBNAIL}
|
|
167
|
+
title="Poster"
|
|
168
|
+
variant="dark"
|
|
169
|
+
/>
|
|
170
|
+
)
|
|
171
|
+
const darkImage = screen.getByRole('img', { name: 'Poster' })
|
|
172
|
+
expect(darkImage).toHaveAttribute('src', OPTIMIZED_THUMBNAIL)
|
|
173
|
+
expect(view.container.firstElementChild).toHaveClass(
|
|
174
|
+
'aspect-video',
|
|
175
|
+
'bg-white/10'
|
|
176
|
+
)
|
|
177
|
+
view.unmount()
|
|
178
|
+
}
|
|
186
179
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
180
|
+
{
|
|
181
|
+
const view = renderWithProviders(
|
|
182
|
+
<AttachmentThumbnail
|
|
183
|
+
mimeType="image/jpeg"
|
|
184
|
+
thumbnailUrl="https://external.example/poster.jpg"
|
|
185
|
+
variant="light"
|
|
186
|
+
/>
|
|
187
|
+
)
|
|
188
|
+
expect(screen.getByRole('img')).toHaveAttribute('alt', '')
|
|
189
|
+
expect(view.container.firstElementChild).toHaveClass(
|
|
190
|
+
'aspect-video',
|
|
191
|
+
'bg-black/5'
|
|
192
|
+
)
|
|
193
|
+
}
|
|
199
194
|
})
|
|
200
195
|
|
|
201
196
|
it('renders the empty branch with a light placeholder', () => {
|
|
202
|
-
|
|
197
|
+
renderWithProviders(
|
|
203
198
|
<AttachmentThumbnail
|
|
204
199
|
mimeType="application/octet-stream"
|
|
205
200
|
variant="light"
|
|
206
201
|
/>
|
|
207
202
|
)
|
|
208
|
-
expect(
|
|
209
|
-
expect(
|
|
210
|
-
'size-12',
|
|
211
|
-
'text-black/20'
|
|
212
|
-
)
|
|
213
|
-
expect(container.firstElementChild).toHaveClass('bg-black/5')
|
|
203
|
+
expect(screen.queryByRole('img')).toBeNull()
|
|
204
|
+
expect(screen.getByTestId('type-icon-generic')).toBeInTheDocument()
|
|
214
205
|
})
|
|
215
206
|
})
|
|
@@ -8,33 +8,7 @@ import AttachmentCard from './index'
|
|
|
8
8
|
const thumbnail = <div data-testid="thumbnail">thumbnail</div>
|
|
9
9
|
|
|
10
10
|
describe('AttachmentCard', () => {
|
|
11
|
-
it('
|
|
12
|
-
const dark = renderWithProviders(
|
|
13
|
-
<AttachmentCard
|
|
14
|
-
variant="dark"
|
|
15
|
-
thumbnail={thumbnail}
|
|
16
|
-
mimeType="application/pdf"
|
|
17
|
-
title="Dark file"
|
|
18
|
-
/>
|
|
19
|
-
)
|
|
20
|
-
expect(screen.getByText('Dark file')).toBeInTheDocument()
|
|
21
|
-
expect(dark.container.firstElementChild).toHaveClass('bg-[#1e2330]')
|
|
22
|
-
expect(dark.container.querySelector('svg')).toHaveClass('text-white/55')
|
|
23
|
-
dark.unmount()
|
|
24
|
-
|
|
25
|
-
const light = renderWithProviders(
|
|
26
|
-
<AttachmentCard
|
|
27
|
-
variant="light"
|
|
28
|
-
thumbnail={thumbnail}
|
|
29
|
-
mimeType="application/pdf"
|
|
30
|
-
title="Light file"
|
|
31
|
-
/>
|
|
32
|
-
)
|
|
33
|
-
expect(light.container.firstElementChild).toHaveClass('bg-white')
|
|
34
|
-
expect(light.container.querySelector('svg')).toHaveClass('text-black/55')
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('uses the dark placeholder title and dims it when title is absent', () => {
|
|
11
|
+
it('renders the dark placeholder title when no title is supplied', () => {
|
|
38
12
|
renderWithProviders(
|
|
39
13
|
<AttachmentCard
|
|
40
14
|
variant="dark"
|
|
@@ -43,7 +17,7 @@ describe('AttachmentCard', () => {
|
|
|
43
17
|
placeholderTitle="Waiting for title"
|
|
44
18
|
/>
|
|
45
19
|
)
|
|
46
|
-
expect(screen.getByText('Waiting for title')).
|
|
20
|
+
expect(screen.getByText('Waiting for title')).toBeInTheDocument()
|
|
47
21
|
})
|
|
48
22
|
|
|
49
23
|
it('does not render a light title for missing or whitespace-only values', () => {
|
|
@@ -67,7 +41,7 @@ describe('AttachmentCard', () => {
|
|
|
67
41
|
expect(screen.queryByText(' ')).toBeNull()
|
|
68
42
|
})
|
|
69
43
|
|
|
70
|
-
it('
|
|
44
|
+
it('renders the supplied dark title instead of the placeholder', () => {
|
|
71
45
|
renderWithProviders(
|
|
72
46
|
<AttachmentCard
|
|
73
47
|
variant="dark"
|
|
@@ -76,8 +50,8 @@ describe('AttachmentCard', () => {
|
|
|
76
50
|
title="Named file"
|
|
77
51
|
/>
|
|
78
52
|
)
|
|
79
|
-
expect(screen.getByText('Named file')).
|
|
80
|
-
expect(screen.
|
|
53
|
+
expect(screen.getByText('Named file')).toBeInTheDocument()
|
|
54
|
+
expect(screen.queryByText('Attachment title')).toBeNull()
|
|
81
55
|
})
|
|
82
56
|
|
|
83
57
|
it('renders detail only for non-empty values', () => {
|
|
@@ -105,7 +79,7 @@ describe('AttachmentCard', () => {
|
|
|
105
79
|
})
|
|
106
80
|
|
|
107
81
|
it('renders optional slots only when supplied', () => {
|
|
108
|
-
const
|
|
82
|
+
const view = renderWithProviders(
|
|
109
83
|
<AttachmentCard
|
|
110
84
|
variant="light"
|
|
111
85
|
thumbnail={thumbnail}
|
|
@@ -118,21 +92,16 @@ describe('AttachmentCard', () => {
|
|
|
118
92
|
)
|
|
119
93
|
expect(screen.getByText('Ready')).toBeInTheDocument()
|
|
120
94
|
expect(screen.getByRole('button', { name: 'Open' })).toBeInTheDocument()
|
|
121
|
-
expect(screen.
|
|
122
|
-
'
|
|
123
|
-
'absolute',
|
|
124
|
-
'left-3',
|
|
125
|
-
'top-3'
|
|
95
|
+
expect(screen.getByTestId('attachment-card-top-left')).toContainElement(
|
|
96
|
+
screen.getByText('Left')
|
|
126
97
|
)
|
|
127
|
-
expect(screen.
|
|
128
|
-
'
|
|
129
|
-
'absolute',
|
|
130
|
-
'right-3',
|
|
131
|
-
'top-3'
|
|
98
|
+
expect(screen.getByTestId('attachment-card-top-right')).toContainElement(
|
|
99
|
+
screen.getByText('Right')
|
|
132
100
|
)
|
|
133
|
-
expect(container.querySelectorAll('.pointer-events-auto')).toHaveLength(2)
|
|
134
101
|
|
|
135
|
-
|
|
102
|
+
view.unmount()
|
|
103
|
+
|
|
104
|
+
renderWithProviders(
|
|
136
105
|
<AttachmentCard
|
|
137
106
|
variant="light"
|
|
138
107
|
thumbnail={thumbnail}
|
|
@@ -140,8 +109,11 @@ describe('AttachmentCard', () => {
|
|
|
140
109
|
/>
|
|
141
110
|
)
|
|
142
111
|
expect(
|
|
143
|
-
|
|
144
|
-
).
|
|
112
|
+
screen.queryByTestId('attachment-card-top-left')
|
|
113
|
+
).not.toBeInTheDocument()
|
|
114
|
+
expect(
|
|
115
|
+
screen.queryByTestId('attachment-card-top-right')
|
|
116
|
+
).not.toBeInTheDocument()
|
|
145
117
|
})
|
|
146
118
|
|
|
147
119
|
it('forwards rootRef and data-testid to the root', () => {
|
|
@@ -59,12 +59,18 @@ const AttachmentCard: React.FC<AttachmentCardProps> = ({
|
|
|
59
59
|
)}
|
|
60
60
|
>
|
|
61
61
|
{topLeft ? (
|
|
62
|
-
<div
|
|
62
|
+
<div
|
|
63
|
+
data-testid="attachment-card-top-left"
|
|
64
|
+
className="pointer-events-auto absolute left-3 top-3 z-50"
|
|
65
|
+
>
|
|
63
66
|
{topLeft}
|
|
64
67
|
</div>
|
|
65
68
|
) : null}
|
|
66
69
|
{topRight ? (
|
|
67
|
-
<div
|
|
70
|
+
<div
|
|
71
|
+
data-testid="attachment-card-top-right"
|
|
72
|
+
className="pointer-events-auto absolute right-3 top-3 z-50"
|
|
73
|
+
>
|
|
68
74
|
{topRight}
|
|
69
75
|
</div>
|
|
70
76
|
) : null}
|
|
@@ -43,10 +43,20 @@ function getTypeIcon(mimeType: string): React.ElementType {
|
|
|
43
43
|
return DOCUMENT_ICON_COMPONENT[getDocumentIconType(mimeType)]
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function getTypeIconName(mimeType: string): string {
|
|
47
|
+
const sourceType = getSourceType(mimeType)
|
|
48
|
+
if (sourceType !== 'document') return sourceType
|
|
49
|
+
return getDocumentIconType(mimeType)
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
/** Use instead of `<TypeIcon />` where TypeIcon = getTypeIcon(mime) to satisfy react-hooks/static-components. */
|
|
47
53
|
export function renderTypeIcon(
|
|
48
54
|
mimeType: string,
|
|
49
55
|
props: IconProps
|
|
50
56
|
): React.ReactElement {
|
|
51
|
-
|
|
57
|
+
// Decorative icons: which one was picked is otherwise only visible in a pixel diff.
|
|
58
|
+
return React.createElement(getTypeIcon(mimeType), {
|
|
59
|
+
'data-testid': `type-icon-${getTypeIconName(mimeType)}`,
|
|
60
|
+
...props,
|
|
61
|
+
})
|
|
52
62
|
}
|