@linktr.ee/messaging-react 4.4.7 → 4.5.0-rc-1788297811
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/assets/index.css +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +58 -43
- package/dist/index.js +1167 -1033
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/AttachmentCard/MediaPlayer.test.tsx +9 -5
- package/src/components/AttachmentCard/Thumbnail.test.tsx +4 -0
- package/src/components/ChannelActionsMenu/ChannelActionsMenu.test.tsx +18 -10
- package/src/components/ChannelList/ChannelListContext.test.tsx +7 -5
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +7 -5
- package/src/components/ChannelList/index.test.tsx +1 -0
- package/src/components/ChannelView.test.tsx +20 -13
- package/src/components/CustomLinkPreviewList/CustomLinkPreviewList.test.tsx +4 -3
- package/src/components/CustomMessage/CustomMessage.test.tsx +6 -5
- package/src/components/CustomMessage/MessageTag.test.tsx +6 -0
- package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +19 -9
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +45 -5
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +6 -0
- package/src/components/CustomMessage/TipMessage/TipMessage.test.tsx +3 -0
- package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +16 -7
- package/src/components/FaqList/FaqList.test.tsx +20 -12
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +27 -14
- package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +1 -0
- package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +6 -6
- package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +8 -5
- package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +7 -5
- package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +3 -8
- package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +165 -58
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.test.tsx +155 -0
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.tsx +455 -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 -79
- package/src/components/MessageAttachment/MessageAttachment.behavior.test.tsx +22 -15
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +279 -134
- package/src/components/MessageAttachment/_shared/Bubble.test.tsx +2 -0
- package/src/components/MessageAttachment/_shared/CarouselNav.test.tsx +5 -4
- package/src/components/MessageAttachment/_shared/CompactDocumentRow.test.tsx +4 -3
- package/src/components/MessageAttachment/_shared/DismissButton.test.tsx +4 -3
- package/src/components/MessageAttachment/_shared/DownloadAction.test.tsx +8 -5
- package/src/components/MessageAttachment/_shared/ImageViewer.test.tsx +6 -3
- package/src/components/MessageAttachment/_shared/MediaStackGrid.test.tsx +11 -4
- package/src/components/MessageAttachment/_shared/PdfViewer.test.tsx +5 -3
- package/src/components/MessageAttachment/_shared/VideoViewer.test.tsx +12 -3
- package/src/components/MessageAttachment/_shared/ViewerShell.test.tsx +13 -6
- package/src/components/MessageAttachment/_shared/useCarousel.test.tsx +10 -4
- 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/MessageBubble/MessageBubble.test.tsx +8 -0
- package/src/components/RichCard/RichCard.test.tsx +10 -2
- package/src/styles.css +120 -0
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
ChatProvider,
|
|
4
|
+
WithAudioPlayback,
|
|
5
|
+
type ChatContextValue,
|
|
6
|
+
} from 'stream-chat-react'
|
|
2
7
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
8
|
|
|
4
9
|
import {
|
|
@@ -6,12 +11,28 @@ import {
|
|
|
6
11
|
renderWithProviders,
|
|
7
12
|
screen,
|
|
8
13
|
within,
|
|
14
|
+
userEvent,
|
|
9
15
|
} from '../../test/utils'
|
|
10
16
|
|
|
11
17
|
import { triggerDownload } from './_shared/triggerDownload'
|
|
12
18
|
|
|
13
19
|
import MessageAttachment, { bubbleGroupPositionFromStream } from '.'
|
|
14
20
|
|
|
21
|
+
// `MessageAttachment.Audio`'s playback comes from `stream-chat-react`'s
|
|
22
|
+
// pooled `AudioPlayer`, which `<Channel>` mounts in production. Mounting
|
|
23
|
+
// the pool directly is the same contract without needing a channel that
|
|
24
|
+
// can `watch()`. The player reads only `client` off the chat context —
|
|
25
|
+
// optional-chained, for error notifications — so a stub is enough; its
|
|
26
|
+
// presence is what stops `useChatContext` warning.
|
|
27
|
+
const AUDIO_CHAT_CONTEXT = {} as ChatContextValue
|
|
28
|
+
|
|
29
|
+
const renderAudio = (ui: React.ReactElement) =>
|
|
30
|
+
renderWithProviders(
|
|
31
|
+
<ChatProvider value={AUDIO_CHAT_CONTEXT}>
|
|
32
|
+
<WithAudioPlayback>{ui}</WithAudioPlayback>
|
|
33
|
+
</ChatProvider>
|
|
34
|
+
)
|
|
35
|
+
|
|
15
36
|
vi.mock('./_shared/triggerDownload', () => ({
|
|
16
37
|
triggerDownload: vi.fn(() => Promise.resolve()),
|
|
17
38
|
}))
|
|
@@ -38,7 +59,8 @@ const expectViewerOpen = (testId: string) => {
|
|
|
38
59
|
}
|
|
39
60
|
|
|
40
61
|
describe('MessageAttachment.Image', () => {
|
|
41
|
-
it('optimizes Linktree-hosted composer previews without changing the viewer source', () => {
|
|
62
|
+
it('optimizes Linktree-hosted composer previews without changing the viewer source', async () => {
|
|
63
|
+
const user = userEvent.setup()
|
|
42
64
|
renderWithProviders(
|
|
43
65
|
<MessageAttachment.Image.Composer
|
|
44
66
|
src="https://assets.linktr.ee/photo.jpg"
|
|
@@ -51,8 +73,9 @@ describe('MessageAttachment.Image', () => {
|
|
|
51
73
|
'https://assets.linktr.ee/photo.jpg?io=true&size=messaging-attachment-v1_0'
|
|
52
74
|
)
|
|
53
75
|
|
|
54
|
-
|
|
76
|
+
await user.click(screen.getByLabelText('Open image'))
|
|
55
77
|
const viewer = expectViewerOpen('image-viewer')
|
|
78
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
56
79
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
57
80
|
'https://assets.linktr.ee/photo.jpg'
|
|
58
81
|
)
|
|
@@ -69,12 +92,14 @@ describe('MessageAttachment.Image', () => {
|
|
|
69
92
|
const bubble = screen.getByTestId('image-attachment')
|
|
70
93
|
expect(bubble).toBeInTheDocument()
|
|
71
94
|
expect(screen.getByText('Here is the image')).toBeInTheDocument()
|
|
95
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
72
96
|
expect(bubble.querySelector('img')?.getAttribute('src')).toBe(
|
|
73
97
|
'https://cdn.example.com/photo.jpg'
|
|
74
98
|
)
|
|
75
99
|
})
|
|
76
100
|
|
|
77
|
-
it('opens the ImageViewer lightbox when clicked', () => {
|
|
101
|
+
it('opens the ImageViewer lightbox when clicked', async () => {
|
|
102
|
+
const user = userEvent.setup()
|
|
78
103
|
renderWithProviders(
|
|
79
104
|
<MessageAttachment.Image.Received
|
|
80
105
|
src="https://cdn.example.com/photo.jpg"
|
|
@@ -85,7 +110,7 @@ describe('MessageAttachment.Image', () => {
|
|
|
85
110
|
|
|
86
111
|
expectViewerClosed('image-viewer')
|
|
87
112
|
|
|
88
|
-
|
|
113
|
+
await user.click(screen.getByLabelText('Open image 1 of 1'))
|
|
89
114
|
|
|
90
115
|
expectViewerOpen('image-viewer')
|
|
91
116
|
})
|
|
@@ -121,7 +146,8 @@ describe('MessageAttachment.Image', () => {
|
|
|
121
146
|
expect(screen.getByLabelText('Open image 3 of 3')).toBeInTheDocument()
|
|
122
147
|
})
|
|
123
148
|
|
|
124
|
-
it('renders a download button inside the image viewer', () => {
|
|
149
|
+
it('renders a download button inside the image viewer', async () => {
|
|
150
|
+
const user = userEvent.setup()
|
|
125
151
|
renderWithProviders(
|
|
126
152
|
<MessageAttachment.Image.Received
|
|
127
153
|
src="https://cdn.example.com/photo.jpg"
|
|
@@ -129,30 +155,33 @@ describe('MessageAttachment.Image', () => {
|
|
|
129
155
|
filename="photo.jpg"
|
|
130
156
|
/>
|
|
131
157
|
)
|
|
132
|
-
|
|
158
|
+
await user.click(screen.getByLabelText('Open image 1 of 1'))
|
|
133
159
|
const viewer = expectViewerOpen('image-viewer')
|
|
134
160
|
// The viewer chrome carries its own download action — separate
|
|
135
161
|
// from any sibling download button on the bubble surface — so the
|
|
136
162
|
// user can grab the image without leaving the lightbox.
|
|
137
163
|
expect(
|
|
164
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
138
165
|
viewer.querySelector('button[aria-label="Download photo.jpg"]')
|
|
139
166
|
).not.toBeNull()
|
|
140
167
|
})
|
|
141
168
|
|
|
142
|
-
it('does not render carousel nav controls for a single image', () => {
|
|
169
|
+
it('does not render carousel nav controls for a single image', async () => {
|
|
170
|
+
const user = userEvent.setup()
|
|
143
171
|
renderWithProviders(
|
|
144
172
|
<MessageAttachment.Image.Received
|
|
145
173
|
src="https://cdn.example.com/photo.jpg"
|
|
146
174
|
filename="photo.jpg"
|
|
147
175
|
/>
|
|
148
176
|
)
|
|
149
|
-
|
|
177
|
+
await user.click(screen.getByLabelText('Open image 1 of 1'))
|
|
150
178
|
expectViewerOpen('image-viewer')
|
|
151
179
|
expect(screen.queryByLabelText('Next image')).toBeNull()
|
|
152
180
|
expect(screen.queryByLabelText('Previous image')).toBeNull()
|
|
153
181
|
})
|
|
154
182
|
|
|
155
|
-
it('renders the stacked images as a carousel inside the viewer', () => {
|
|
183
|
+
it('renders the stacked images as a carousel inside the viewer', async () => {
|
|
184
|
+
const user = userEvent.setup()
|
|
156
185
|
renderWithProviders(
|
|
157
186
|
<MessageAttachment.Image.Sent
|
|
158
187
|
filename="album"
|
|
@@ -165,8 +194,9 @@ describe('MessageAttachment.Image', () => {
|
|
|
165
194
|
)
|
|
166
195
|
|
|
167
196
|
// Open the second tile — the viewer should land on image 2.
|
|
168
|
-
|
|
197
|
+
await user.click(screen.getByLabelText('Open image 2 of 3'))
|
|
169
198
|
const viewer = expectViewerOpen('image-viewer')
|
|
199
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
170
200
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
171
201
|
'https://cdn.example.com/b.jpg'
|
|
172
202
|
)
|
|
@@ -174,26 +204,30 @@ describe('MessageAttachment.Image', () => {
|
|
|
174
204
|
expect(viewer.textContent).toContain('2 / 3')
|
|
175
205
|
|
|
176
206
|
// Next button advances by one.
|
|
177
|
-
|
|
207
|
+
await user.click(screen.getByLabelText('Next image'))
|
|
208
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
178
209
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
179
210
|
'https://cdn.example.com/c.jpg'
|
|
180
211
|
)
|
|
181
212
|
expect(viewer.textContent).toContain('3 / 3')
|
|
182
213
|
|
|
183
214
|
// Next wraps from the last item back to the first.
|
|
184
|
-
|
|
215
|
+
await user.click(screen.getByLabelText('Next image'))
|
|
216
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
185
217
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
186
218
|
'https://cdn.example.com/a.jpg'
|
|
187
219
|
)
|
|
188
220
|
|
|
189
221
|
// Previous wraps from the first item to the last.
|
|
190
|
-
|
|
222
|
+
await user.click(screen.getByLabelText('Previous image'))
|
|
223
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
191
224
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
192
225
|
'https://cdn.example.com/c.jpg'
|
|
193
226
|
)
|
|
194
227
|
})
|
|
195
228
|
|
|
196
|
-
it('uses ArrowRight / ArrowLeft to navigate stacked images', () => {
|
|
229
|
+
it('uses ArrowRight / ArrowLeft to navigate stacked images', async () => {
|
|
230
|
+
const user = userEvent.setup()
|
|
197
231
|
renderWithProviders(
|
|
198
232
|
<MessageAttachment.Image.Sent
|
|
199
233
|
items={[
|
|
@@ -202,18 +236,21 @@ describe('MessageAttachment.Image', () => {
|
|
|
202
236
|
]}
|
|
203
237
|
/>
|
|
204
238
|
)
|
|
205
|
-
|
|
239
|
+
await user.click(screen.getByLabelText('Open image 1 of 2'))
|
|
206
240
|
const viewer = expectViewerOpen('image-viewer')
|
|
241
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
207
242
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
208
243
|
'https://cdn.example.com/a.jpg'
|
|
209
244
|
)
|
|
210
245
|
|
|
211
246
|
fireEvent.keyDown(window, { key: 'ArrowRight' })
|
|
247
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
212
248
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
213
249
|
'https://cdn.example.com/b.jpg'
|
|
214
250
|
)
|
|
215
251
|
|
|
216
252
|
fireEvent.keyDown(window, { key: 'ArrowLeft' })
|
|
253
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
217
254
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
218
255
|
'https://cdn.example.com/a.jpg'
|
|
219
256
|
)
|
|
@@ -242,6 +279,7 @@ describe('MessageAttachment.Image single-image aspect ratio', () => {
|
|
|
242
279
|
const WIDTH_CAP_WEB_PX = 326
|
|
243
280
|
|
|
244
281
|
const mediaBox = (label: string) =>
|
|
282
|
+
// eslint-disable-next-line testing-library/no-node-access -- the wrapper class is a deliberate DOM contract with no semantic query.
|
|
245
283
|
screen.getByLabelText(label).parentElement as HTMLElement
|
|
246
284
|
|
|
247
285
|
/** The `min(var(--…), Npx)` term the height cap contributes. */
|
|
@@ -319,6 +357,7 @@ describe('MessageAttachment.Image single-image aspect ratio', () => {
|
|
|
319
357
|
|
|
320
358
|
// Scoped to the tile: the always-mounted `ImageViewer` renders a
|
|
321
359
|
// second `<img>` for the same source.
|
|
360
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
322
361
|
const img = mediaBox('Open image 1 of 1').querySelector(
|
|
323
362
|
'img'
|
|
324
363
|
) as HTMLImageElement
|
|
@@ -405,7 +444,8 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
405
444
|
expect(screen.getByText('PDF · 379.5 KB')).toBeInTheDocument()
|
|
406
445
|
})
|
|
407
446
|
|
|
408
|
-
it('opens the PdfViewer when clicked', () => {
|
|
447
|
+
it('opens the PdfViewer when clicked', async () => {
|
|
448
|
+
const user = userEvent.setup()
|
|
409
449
|
renderWithProviders(
|
|
410
450
|
<MessageAttachment.Pdf.Sent
|
|
411
451
|
src="https://cdn.example.com/doc.pdf"
|
|
@@ -413,11 +453,12 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
413
453
|
/>
|
|
414
454
|
)
|
|
415
455
|
expectViewerClosed('pdf-viewer')
|
|
416
|
-
|
|
456
|
+
await user.click(screen.getByLabelText('Open doc.pdf'))
|
|
417
457
|
expectViewerOpen('pdf-viewer')
|
|
418
458
|
})
|
|
419
459
|
|
|
420
|
-
it('forwards the onClick handler before opening the viewer', () => {
|
|
460
|
+
it('forwards the onClick handler before opening the viewer', async () => {
|
|
461
|
+
const user = userEvent.setup()
|
|
421
462
|
const onClick = vi.fn()
|
|
422
463
|
renderWithProviders(
|
|
423
464
|
<MessageAttachment.Pdf.Received
|
|
@@ -426,7 +467,7 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
426
467
|
onClick={onClick}
|
|
427
468
|
/>
|
|
428
469
|
)
|
|
429
|
-
|
|
470
|
+
await user.click(screen.getByLabelText('Open doc.pdf'))
|
|
430
471
|
expect(onClick).toHaveBeenCalledTimes(1)
|
|
431
472
|
})
|
|
432
473
|
|
|
@@ -457,7 +498,8 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
457
498
|
expect(screen.getByLabelText('Open c.pdf')).toBeInTheDocument()
|
|
458
499
|
})
|
|
459
500
|
|
|
460
|
-
it('opens the viewer on the activated row in a stacked PDF bubble', () => {
|
|
501
|
+
it('opens the viewer on the activated row in a stacked PDF bubble', async () => {
|
|
502
|
+
const user = userEvent.setup()
|
|
461
503
|
renderWithProviders(
|
|
462
504
|
<MessageAttachment.Pdf.Received
|
|
463
505
|
items={[
|
|
@@ -467,7 +509,7 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
467
509
|
/>
|
|
468
510
|
)
|
|
469
511
|
expectViewerClosed('pdf-viewer')
|
|
470
|
-
|
|
512
|
+
await user.click(screen.getByLabelText('Open b.pdf'))
|
|
471
513
|
const viewer = expectViewerOpen('pdf-viewer')
|
|
472
514
|
// The active filename surfaces as the dialog's accessible name —
|
|
473
515
|
// the lightbox chrome is intentionally just close + carousel chrome
|
|
@@ -479,7 +521,8 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
479
521
|
expect(viewer.textContent).toContain('2 / 2')
|
|
480
522
|
})
|
|
481
523
|
|
|
482
|
-
it('navigates between stacked PDFs via the carousel controls', () => {
|
|
524
|
+
it('navigates between stacked PDFs via the carousel controls', async () => {
|
|
525
|
+
const user = userEvent.setup()
|
|
483
526
|
renderWithProviders(
|
|
484
527
|
<MessageAttachment.Pdf.Received
|
|
485
528
|
items={[
|
|
@@ -489,18 +532,18 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
489
532
|
]}
|
|
490
533
|
/>
|
|
491
534
|
)
|
|
492
|
-
|
|
535
|
+
await user.click(screen.getByLabelText('Open a.pdf'))
|
|
493
536
|
const viewer = expectViewerOpen('pdf-viewer')
|
|
494
537
|
expect(viewer.getAttribute('aria-label')).toBe('a.pdf')
|
|
495
538
|
|
|
496
|
-
|
|
539
|
+
await user.click(screen.getByLabelText('Next document'))
|
|
497
540
|
expect(viewer.getAttribute('aria-label')).toBe('b.pdf')
|
|
498
541
|
|
|
499
|
-
|
|
542
|
+
await user.click(screen.getByLabelText('Next document'))
|
|
500
543
|
expect(viewer.getAttribute('aria-label')).toBe('c.pdf')
|
|
501
544
|
|
|
502
545
|
// Wraps back to the start.
|
|
503
|
-
|
|
546
|
+
await user.click(screen.getByLabelText('Next document'))
|
|
504
547
|
expect(viewer.getAttribute('aria-label')).toBe('a.pdf')
|
|
505
548
|
})
|
|
506
549
|
|
|
@@ -537,7 +580,8 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
537
580
|
expect(screen.getByLabelText('Download c.pdf')).toBeInTheDocument()
|
|
538
581
|
})
|
|
539
582
|
|
|
540
|
-
it('does not open the viewer when the Download button is clicked', () => {
|
|
583
|
+
it('does not open the viewer when the Download button is clicked', async () => {
|
|
584
|
+
const user = userEvent.setup()
|
|
541
585
|
renderWithProviders(
|
|
542
586
|
<MessageAttachment.Pdf.Sent
|
|
543
587
|
src="https://cdn.example.com/notes.pdf"
|
|
@@ -545,7 +589,7 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
545
589
|
/>
|
|
546
590
|
)
|
|
547
591
|
expectViewerClosed('pdf-viewer')
|
|
548
|
-
|
|
592
|
+
await user.click(screen.getByLabelText('Download notes.pdf'))
|
|
549
593
|
// Download should fire on its own without bubbling up to the row
|
|
550
594
|
// activation that opens the viewer.
|
|
551
595
|
expectViewerClosed('pdf-viewer')
|
|
@@ -563,7 +607,8 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
563
607
|
expect(screen.getByLabelText('Dismiss attachment')).toBeInTheDocument()
|
|
564
608
|
})
|
|
565
609
|
|
|
566
|
-
it('does not crash when items shrinks after the viewer opens (P1 regression)', () => {
|
|
610
|
+
it('does not crash when items shrinks after the viewer opens (P1 regression)', async () => {
|
|
611
|
+
const user = userEvent.setup()
|
|
567
612
|
// Regression test for the PR #151 P1 reviewer finding:
|
|
568
613
|
// `viewerIndex` was dereferenced directly against `resolvedItems`,
|
|
569
614
|
// so a re-render with fewer items than the open index would crash
|
|
@@ -578,7 +623,7 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
578
623
|
]}
|
|
579
624
|
/>
|
|
580
625
|
)
|
|
581
|
-
|
|
626
|
+
await user.click(screen.getByLabelText('Open c.pdf'))
|
|
582
627
|
expectViewerOpen('pdf-viewer')
|
|
583
628
|
|
|
584
629
|
expect(() =>
|
|
@@ -599,7 +644,8 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
599
644
|
)
|
|
600
645
|
})
|
|
601
646
|
|
|
602
|
-
it('does not open the viewer when onClick returns false', () => {
|
|
647
|
+
it('does not open the viewer when onClick returns false', async () => {
|
|
648
|
+
const user = userEvent.setup()
|
|
603
649
|
const onClick = vi.fn(() => false as const)
|
|
604
650
|
renderWithProviders(
|
|
605
651
|
<MessageAttachment.Pdf.Received
|
|
@@ -608,7 +654,7 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
608
654
|
onClick={onClick}
|
|
609
655
|
/>
|
|
610
656
|
)
|
|
611
|
-
|
|
657
|
+
await user.click(screen.getByLabelText('Open doc.pdf'))
|
|
612
658
|
expect(onClick).toHaveBeenCalledTimes(1)
|
|
613
659
|
expectViewerClosed('pdf-viewer')
|
|
614
660
|
})
|
|
@@ -669,7 +715,8 @@ describe('MessageAttachment.File', () => {
|
|
|
669
715
|
expect(screen.getByLabelText('Download c.pdf')).toBeInTheDocument()
|
|
670
716
|
})
|
|
671
717
|
|
|
672
|
-
it('downloads the file when the row is clicked and onClick allows it', () => {
|
|
718
|
+
it('downloads the file when the row is clicked and onClick allows it', async () => {
|
|
719
|
+
const user = userEvent.setup()
|
|
673
720
|
const onClick = vi.fn()
|
|
674
721
|
renderWithProviders(
|
|
675
722
|
<MessageAttachment.File.Received
|
|
@@ -678,7 +725,7 @@ describe('MessageAttachment.File', () => {
|
|
|
678
725
|
onClick={onClick}
|
|
679
726
|
/>
|
|
680
727
|
)
|
|
681
|
-
|
|
728
|
+
await user.click(screen.getByLabelText('Download workout-plan.zip'))
|
|
682
729
|
expect(onClick).toHaveBeenCalledWith(0)
|
|
683
730
|
expect(mockTriggerDownload).toHaveBeenCalledTimes(1)
|
|
684
731
|
expect(mockTriggerDownload).toHaveBeenCalledWith(
|
|
@@ -687,7 +734,8 @@ describe('MessageAttachment.File', () => {
|
|
|
687
734
|
)
|
|
688
735
|
})
|
|
689
736
|
|
|
690
|
-
it('lets onClick cancel the automatic download by returning false (P2 regression)', () => {
|
|
737
|
+
it('lets onClick cancel the automatic download by returning false (P2 regression)', async () => {
|
|
738
|
+
const user = userEvent.setup()
|
|
691
739
|
// Regression test for the PR #151 P2 reviewer finding:
|
|
692
740
|
// the docs advertised `onClick` as a hook for intercepting the
|
|
693
741
|
// download (e.g. confirmation modal), but the handler used to fire
|
|
@@ -701,12 +749,13 @@ describe('MessageAttachment.File', () => {
|
|
|
701
749
|
onClick={onClick}
|
|
702
750
|
/>
|
|
703
751
|
)
|
|
704
|
-
|
|
752
|
+
await user.click(screen.getByLabelText('Download workout-plan.zip'))
|
|
705
753
|
expect(onClick).toHaveBeenCalledTimes(1)
|
|
706
754
|
expect(mockTriggerDownload).not.toHaveBeenCalled()
|
|
707
755
|
})
|
|
708
756
|
|
|
709
|
-
it('still downloads when onClick returns void', () => {
|
|
757
|
+
it('still downloads when onClick returns void', async () => {
|
|
758
|
+
const user = userEvent.setup()
|
|
710
759
|
// Belt-and-braces: void / undefined returns must not be confused
|
|
711
760
|
// with `false` (the only cancellation sentinel).
|
|
712
761
|
const onClick = vi.fn(() => undefined)
|
|
@@ -717,61 +766,190 @@ describe('MessageAttachment.File', () => {
|
|
|
717
766
|
onClick={onClick}
|
|
718
767
|
/>
|
|
719
768
|
)
|
|
720
|
-
|
|
769
|
+
await user.click(screen.getByLabelText('Download workout-plan.zip'))
|
|
721
770
|
expect(mockTriggerDownload).toHaveBeenCalledTimes(1)
|
|
722
771
|
})
|
|
723
772
|
})
|
|
724
773
|
|
|
725
774
|
describe('MessageAttachment.Audio', () => {
|
|
726
|
-
|
|
727
|
-
|
|
775
|
+
// 12 samples so the resample path is exercised on the way to the
|
|
776
|
+
// design's 35 bars.
|
|
777
|
+
const WAVEFORM = [
|
|
778
|
+
0.1, 0.8, 0.65, 0.2, 0.3, 0.45, 0.5, 0.7, 1, 0.85, 0.4, 0.15,
|
|
779
|
+
]
|
|
780
|
+
|
|
781
|
+
it('renders the waveform card: bars, play toggle, duration and seek', () => {
|
|
782
|
+
renderAudio(
|
|
728
783
|
<MessageAttachment.Audio.Sent
|
|
729
784
|
src="https://cdn.example.com/song.mp3"
|
|
730
785
|
mimeType="audio/mpeg"
|
|
731
786
|
filename="song.mp3"
|
|
787
|
+
durationSeconds={12}
|
|
788
|
+
waveformData={WAVEFORM}
|
|
732
789
|
/>
|
|
733
790
|
)
|
|
734
|
-
|
|
735
|
-
expect(
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
791
|
+
|
|
792
|
+
expect(screen.getAllByTestId('amplitude-bar')).toHaveLength(35)
|
|
793
|
+
expect(
|
|
794
|
+
screen.getByRole('button', { name: 'Play audio — song.mp3' })
|
|
795
|
+
).toBeInTheDocument()
|
|
796
|
+
expect(screen.getByText('00:12')).toBeInTheDocument()
|
|
797
|
+
|
|
798
|
+
const seek = screen.getByRole('slider', { name: 'Seek audio — song.mp3' })
|
|
799
|
+
expect(seek).toHaveAttribute('aria-valuetext', '00:00 of 00:12')
|
|
800
|
+
expect(seek).toHaveValue('0')
|
|
801
|
+
|
|
802
|
+
// The bars duplicate what the slider already announces, so they
|
|
803
|
+
// must stay out of the accessibility tree — otherwise they read as
|
|
804
|
+
// 35 unnamed nodes and (in Stream's own version of this widget) as
|
|
805
|
+
// an unnamed `progressbar`.
|
|
806
|
+
const track = screen.getByTestId('audio-waveform-track')
|
|
807
|
+
expect(track).toHaveAttribute('aria-hidden', 'true')
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
it('renders the full bar count as a flat placeholder when no waveform exists', () => {
|
|
811
|
+
// The total fallback: a decode failure, an unsupported codec, a
|
|
812
|
+
// re-staged zero-byte placeholder, or anything sent before
|
|
813
|
+
// `waveform_data` was persisted. Passing `[]` straight through
|
|
814
|
+
// would render no bars at all, so the card must supply its own
|
|
815
|
+
// all-zero row — the card's silhouette has to be unchanged.
|
|
816
|
+
renderAudio(
|
|
817
|
+
<MessageAttachment.Audio.Received src="https://cdn.example.com/legacy.mp3" />
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
const bars = screen.getAllByTestId('amplitude-bar')
|
|
821
|
+
expect(bars).toHaveLength(35)
|
|
822
|
+
bars.forEach((bar) => {
|
|
823
|
+
expect(bar).not.toHaveClass('mes-audio-wave__bar--played')
|
|
824
|
+
})
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
it.each([3, 12, 35, 200])(
|
|
828
|
+
'resamples a %i-sample waveform to the rendered bar count',
|
|
829
|
+
(sampleCount) => {
|
|
830
|
+
const data = Array.from({ length: sampleCount }, (_, i) =>
|
|
831
|
+
Math.abs(Math.sin(i))
|
|
832
|
+
)
|
|
833
|
+
renderAudio(
|
|
834
|
+
<MessageAttachment.Audio.Sent
|
|
835
|
+
src="https://cdn.example.com/song.mp3"
|
|
836
|
+
waveformData={data}
|
|
837
|
+
/>
|
|
838
|
+
)
|
|
839
|
+
expect(screen.getAllByTestId('amplitude-bar')).toHaveLength(35)
|
|
840
|
+
}
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
it('shows an unknown duration without collapsing the slot', () => {
|
|
844
|
+
renderAudio(
|
|
845
|
+
<MessageAttachment.Audio.Sent src="https://cdn.example.com/legacy.mp3" />
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
expect(screen.getByText('--:--')).toBeInTheDocument()
|
|
849
|
+
expect(screen.getByRole('slider', { name: 'Seek audio' })).toHaveAttribute(
|
|
850
|
+
'aria-valuetext',
|
|
851
|
+
'00:00 of --:--'
|
|
852
|
+
)
|
|
742
853
|
})
|
|
743
854
|
|
|
744
|
-
it('
|
|
855
|
+
it('removes the draft from the composer trailing action', async () => {
|
|
856
|
+
const user = userEvent.setup()
|
|
857
|
+
// There is no product-owned overflow menu on audio attachments in
|
|
858
|
+
// production, so the design's trailing control carries forward the
|
|
859
|
+
// one action each surface already had: Remove in the composer…
|
|
745
860
|
const onDismiss = vi.fn()
|
|
746
|
-
|
|
861
|
+
renderAudio(
|
|
747
862
|
<MessageAttachment.Audio.Composer
|
|
748
863
|
src="https://cdn.example.com/song.mp3"
|
|
864
|
+
filename="song.mp3"
|
|
749
865
|
onDismiss={onDismiss}
|
|
750
866
|
/>
|
|
751
867
|
)
|
|
752
|
-
|
|
753
|
-
expect(
|
|
754
|
-
|
|
868
|
+
|
|
869
|
+
expect(
|
|
870
|
+
screen.queryByRole('button', { name: /download/i })
|
|
871
|
+
).not.toBeInTheDocument()
|
|
872
|
+
await user.click(screen.getByRole('button', { name: 'Remove attachment' }))
|
|
755
873
|
expect(onDismiss).toHaveBeenCalledTimes(1)
|
|
756
874
|
})
|
|
757
875
|
|
|
758
|
-
it('
|
|
759
|
-
|
|
876
|
+
it('downloads from the sent trailing action', async () => {
|
|
877
|
+
const user = userEvent.setup()
|
|
878
|
+
// …and Download once sent, preserving the capability the native
|
|
879
|
+
// `<audio controls>` kebab used to provide.
|
|
880
|
+
renderAudio(
|
|
881
|
+
<MessageAttachment.Audio.Sent
|
|
882
|
+
src="https://cdn.example.com/song.mp3"
|
|
883
|
+
filename="song.mp3"
|
|
884
|
+
/>
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
await user.click(
|
|
888
|
+
screen.getByRole('button', { name: 'Download audio — song.mp3' })
|
|
889
|
+
)
|
|
890
|
+
expect(mockTriggerDownload).toHaveBeenCalledWith(
|
|
891
|
+
'https://cdn.example.com/song.mp3',
|
|
892
|
+
'song.mp3'
|
|
893
|
+
)
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
it('renders one waveform row per item for stacked audio', () => {
|
|
897
|
+
renderAudio(
|
|
760
898
|
<MessageAttachment.Audio.Sent
|
|
761
899
|
items={[
|
|
762
|
-
{ src: 'https://cdn.example.com/a.mp3',
|
|
763
|
-
{ src: 'https://cdn.example.com/b.mp3',
|
|
764
|
-
{ src: 'https://cdn.example.com/c.mp3',
|
|
900
|
+
{ src: 'https://cdn.example.com/a.mp3', durationSeconds: 12 },
|
|
901
|
+
{ src: 'https://cdn.example.com/b.mp3', durationSeconds: 30 },
|
|
902
|
+
{ src: 'https://cdn.example.com/c.mp3', durationSeconds: 65 },
|
|
765
903
|
]}
|
|
766
904
|
/>
|
|
767
905
|
)
|
|
768
|
-
|
|
769
|
-
expect(
|
|
906
|
+
|
|
907
|
+
expect(screen.getAllByTestId('audio-attachment-row')).toHaveLength(3)
|
|
908
|
+
expect(screen.getByText('01:05')).toBeInTheDocument()
|
|
909
|
+
})
|
|
910
|
+
|
|
911
|
+
it('disables playback for a MIME type the browser cannot play, but keeps the download', () => {
|
|
912
|
+
// `canPlayType` gates playback, and clicking play on an unplayable
|
|
913
|
+
// track is a no-op inside the player anyway — so the control says
|
|
914
|
+
// so. The file must still be retrievable.
|
|
915
|
+
renderAudio(
|
|
916
|
+
<MessageAttachment.Audio.Received
|
|
917
|
+
src="https://cdn.example.com/master.flac"
|
|
918
|
+
mimeType="audio/flac"
|
|
919
|
+
filename="master.flac"
|
|
920
|
+
/>
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
expect(screen.getByRole('button', { name: /^Play audio/ })).toBeDisabled()
|
|
924
|
+
expect(screen.getByRole('slider', { name: /^Seek audio/ })).toBeDisabled()
|
|
925
|
+
expect(
|
|
926
|
+
screen.getByRole('button', { name: /^Download audio/ })
|
|
927
|
+
).toBeEnabled()
|
|
928
|
+
})
|
|
929
|
+
|
|
930
|
+
it('still renders the card without the audio-playback provider', () => {
|
|
931
|
+
// The pooled player lives on `<Channel>`. Outside one the card must
|
|
932
|
+
// degrade to an inert card rather than rendering nothing — a blank
|
|
933
|
+
// bubble is undiagnosable, and it would silently break every
|
|
934
|
+
// consumer that mounts an attachment outside a channel.
|
|
935
|
+
renderWithProviders(
|
|
936
|
+
<MessageAttachment.Audio.Sent
|
|
937
|
+
src="https://cdn.example.com/song.mp3"
|
|
938
|
+
durationSeconds={12}
|
|
939
|
+
waveformData={WAVEFORM}
|
|
940
|
+
/>
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
expect(screen.getByTestId('audio-attachment')).toBeInTheDocument()
|
|
944
|
+
expect(screen.getAllByTestId('amplitude-bar')).toHaveLength(35)
|
|
945
|
+
expect(screen.getByText('00:12')).toBeInTheDocument()
|
|
946
|
+
expect(screen.getByRole('button', { name: /^Play audio/ })).toBeDisabled()
|
|
770
947
|
})
|
|
771
948
|
})
|
|
772
949
|
|
|
773
950
|
describe('MessageAttachment.Video', () => {
|
|
774
|
-
it('opens the VideoViewer when clicked', () => {
|
|
951
|
+
it('opens the VideoViewer when clicked', async () => {
|
|
952
|
+
const user = userEvent.setup()
|
|
775
953
|
renderWithProviders(
|
|
776
954
|
<MessageAttachment.Video.Received
|
|
777
955
|
src="https://cdn.example.com/clip.mp4"
|
|
@@ -781,13 +959,14 @@ describe('MessageAttachment.Video', () => {
|
|
|
781
959
|
/>
|
|
782
960
|
)
|
|
783
961
|
expectViewerClosed('video-viewer')
|
|
784
|
-
|
|
962
|
+
await user.click(screen.getByLabelText('Play video 1 of 1'))
|
|
785
963
|
expectViewerOpen('video-viewer')
|
|
786
964
|
// Single-item viewer has no carousel chrome.
|
|
787
965
|
expect(screen.queryByLabelText('Next video')).toBeNull()
|
|
788
966
|
})
|
|
789
967
|
|
|
790
|
-
it('navigates between stacked videos via the carousel controls', () => {
|
|
968
|
+
it('navigates between stacked videos via the carousel controls', async () => {
|
|
969
|
+
const user = userEvent.setup()
|
|
791
970
|
renderWithProviders(
|
|
792
971
|
<MessageAttachment.Video.Sent
|
|
793
972
|
filename="clip"
|
|
@@ -805,13 +984,15 @@ describe('MessageAttachment.Video', () => {
|
|
|
805
984
|
]}
|
|
806
985
|
/>
|
|
807
986
|
)
|
|
808
|
-
|
|
987
|
+
await user.click(screen.getByLabelText('Play video 1 of 2'))
|
|
809
988
|
const viewer = expectViewerOpen('video-viewer')
|
|
989
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
810
990
|
expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
|
|
811
991
|
'https://cdn.example.com/a.mp4'
|
|
812
992
|
)
|
|
813
993
|
|
|
814
|
-
|
|
994
|
+
await user.click(screen.getByLabelText('Next video'))
|
|
995
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
815
996
|
expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
|
|
816
997
|
'https://cdn.example.com/b.mp4'
|
|
817
998
|
)
|
|
@@ -841,11 +1022,13 @@ describe('MessageAttachment.Video overflow', () => {
|
|
|
841
1022
|
expect(screen.queryByLabelText('Play video 5 of 9')).toBeNull()
|
|
842
1023
|
})
|
|
843
1024
|
|
|
844
|
-
it('opens the viewer on the fourth video from the overflow tile and pages to the ninth', () => {
|
|
1025
|
+
it('opens the viewer on the fourth video from the overflow tile and pages to the ninth', async () => {
|
|
1026
|
+
const user = userEvent.setup()
|
|
845
1027
|
renderWithProviders(<MessageAttachment.Video.Sent items={NINE_VIDEOS} />)
|
|
846
1028
|
|
|
847
|
-
|
|
1029
|
+
await user.click(screen.getByLabelText('Play video 4 of 9'))
|
|
848
1030
|
const viewer = expectViewerOpen('video-viewer')
|
|
1031
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
849
1032
|
expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
|
|
850
1033
|
'https://cdn.example.com/3.mp4'
|
|
851
1034
|
)
|
|
@@ -854,8 +1037,9 @@ describe('MessageAttachment.Video overflow', () => {
|
|
|
854
1037
|
|
|
855
1038
|
// The carousel spans all nine, not just the four the grid drew.
|
|
856
1039
|
for (let i = 0; i < 5; i += 1) {
|
|
857
|
-
|
|
1040
|
+
await user.click(screen.getByLabelText('Next video'))
|
|
858
1041
|
}
|
|
1042
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
859
1043
|
expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
|
|
860
1044
|
'https://cdn.example.com/8.mp4'
|
|
861
1045
|
)
|
|
@@ -867,7 +1051,9 @@ describe('MessageAttachment.Video overflow', () => {
|
|
|
867
1051
|
|
|
868
1052
|
const tiles = screen.getAllByRole('button', { name: /^Play video/ })
|
|
869
1053
|
// The badge is the only `svg` a poster tile renders.
|
|
1054
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
870
1055
|
expect(tiles[0].querySelector('svg')).not.toBeNull()
|
|
1056
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
871
1057
|
expect(tiles[3].querySelector('svg')).toBeNull()
|
|
872
1058
|
})
|
|
873
1059
|
})
|
|
@@ -878,6 +1064,7 @@ describe('MessageAttachment.Video single-card aspect', () => {
|
|
|
878
1064
|
// jsdom never decodes an `<img>`, so `naturalWidth` stays 0 and `load`
|
|
879
1065
|
// never fires on its own — stub the intrinsic size and fire it.
|
|
880
1066
|
const loadPosterAt = (naturalWidth: number, naturalHeight: number) => {
|
|
1067
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
881
1068
|
const poster = screen.getByTestId('video-attachment').querySelector('img')
|
|
882
1069
|
if (!poster) throw new Error('no poster rendered')
|
|
883
1070
|
Object.defineProperty(poster, 'naturalWidth', {
|
|
@@ -892,6 +1079,7 @@ describe('MessageAttachment.Video single-card aspect', () => {
|
|
|
892
1079
|
}
|
|
893
1080
|
|
|
894
1081
|
const mediaAspect = () =>
|
|
1082
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
895
1083
|
screen.getByTestId('video-attachment').querySelector<HTMLElement>('[style]')
|
|
896
1084
|
?.style.aspectRatio
|
|
897
1085
|
|
|
@@ -1054,23 +1242,25 @@ describe('MessageAttachment.Video single-card aspect', () => {
|
|
|
1054
1242
|
})
|
|
1055
1243
|
|
|
1056
1244
|
describe('ViewerShell focus management', () => {
|
|
1057
|
-
it('moves focus into the dialog when a viewer opens', () => {
|
|
1245
|
+
it('moves focus into the dialog when a viewer opens', async () => {
|
|
1246
|
+
const user = userEvent.setup()
|
|
1058
1247
|
renderWithProviders(
|
|
1059
1248
|
<MessageAttachment.Pdf.Received
|
|
1060
1249
|
src="https://cdn.example.com/doc.pdf"
|
|
1061
1250
|
filename="doc.pdf"
|
|
1062
1251
|
/>
|
|
1063
1252
|
)
|
|
1064
|
-
|
|
1253
|
+
await user.click(screen.getByLabelText('Open doc.pdf'))
|
|
1065
1254
|
|
|
1066
1255
|
// The close button is the primary focus target in the viewer
|
|
1067
1256
|
// toolbar — keyboard users should land on it on open so Escape
|
|
1068
1257
|
// / Tab work without an extra click.
|
|
1069
1258
|
const close = screen.getByLabelText('Close viewer')
|
|
1070
|
-
expect(
|
|
1259
|
+
expect(close).toHaveFocus()
|
|
1071
1260
|
})
|
|
1072
1261
|
|
|
1073
|
-
it('restores focus to the opener when the viewer closes', () => {
|
|
1262
|
+
it('restores focus to the opener when the viewer closes', async () => {
|
|
1263
|
+
const user = userEvent.setup()
|
|
1074
1264
|
renderWithProviders(
|
|
1075
1265
|
<MessageAttachment.Pdf.Received
|
|
1076
1266
|
src="https://cdn.example.com/doc.pdf"
|
|
@@ -1079,18 +1269,18 @@ describe('ViewerShell focus management', () => {
|
|
|
1079
1269
|
)
|
|
1080
1270
|
const opener = screen.getByLabelText('Open doc.pdf')
|
|
1081
1271
|
opener.focus()
|
|
1082
|
-
expect(
|
|
1272
|
+
expect(opener).toHaveFocus()
|
|
1083
1273
|
|
|
1084
|
-
|
|
1274
|
+
await user.click(opener)
|
|
1085
1275
|
expectViewerOpen('pdf-viewer')
|
|
1086
1276
|
|
|
1087
|
-
|
|
1277
|
+
await user.click(screen.getByLabelText('Close viewer'))
|
|
1088
1278
|
|
|
1089
1279
|
// After close, focus should hop back to whatever was focused
|
|
1090
1280
|
// before the viewer mounted (the row's Open button), and the
|
|
1091
1281
|
// dialog should no longer carry the `[open]` attribute.
|
|
1092
1282
|
expectViewerClosed('pdf-viewer')
|
|
1093
|
-
expect(
|
|
1283
|
+
expect(opener).toHaveFocus()
|
|
1094
1284
|
})
|
|
1095
1285
|
})
|
|
1096
1286
|
|
|
@@ -1103,6 +1293,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1103
1293
|
alt="Photo"
|
|
1104
1294
|
/>
|
|
1105
1295
|
)
|
|
1296
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1106
1297
|
const img = screen.getByTestId('image-attachment').querySelector('img')
|
|
1107
1298
|
expect(img?.getAttribute('loading')).toBe('lazy')
|
|
1108
1299
|
expect(img?.getAttribute('decoding')).toBe('async')
|
|
@@ -1116,6 +1307,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1116
1307
|
loading="eager"
|
|
1117
1308
|
/>
|
|
1118
1309
|
)
|
|
1310
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1119
1311
|
const img = screen.getByTestId('image-attachment').querySelector('img')
|
|
1120
1312
|
expect(img?.getAttribute('loading')).toBe('eager')
|
|
1121
1313
|
expect(img?.getAttribute('decoding')).toBe('async')
|
|
@@ -1135,13 +1327,15 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1135
1327
|
/>
|
|
1136
1328
|
)
|
|
1137
1329
|
const [first, second] = Array.from(
|
|
1330
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1138
1331
|
screen.getByTestId('image-attachment').querySelectorAll('img')
|
|
1139
1332
|
)
|
|
1140
1333
|
expect(first?.getAttribute('loading')).toBe('eager')
|
|
1141
1334
|
expect(second?.getAttribute('loading')).toBe('lazy')
|
|
1142
1335
|
})
|
|
1143
1336
|
|
|
1144
|
-
it('does not mount the viewer `<img>` while the lightbox is closed', () => {
|
|
1337
|
+
it('does not mount the viewer `<img>` while the lightbox is closed', async () => {
|
|
1338
|
+
const user = userEvent.setup()
|
|
1145
1339
|
renderWithProviders(
|
|
1146
1340
|
<MessageAttachment.Image.Sent
|
|
1147
1341
|
src="https://cdn.example.com/photo.jpg"
|
|
@@ -1159,7 +1353,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1159
1353
|
})
|
|
1160
1354
|
).toHaveLength(0)
|
|
1161
1355
|
|
|
1162
|
-
|
|
1356
|
+
await user.click(screen.getByLabelText('Open image 1 of 1'))
|
|
1163
1357
|
expect(
|
|
1164
1358
|
within(screen.getByTestId('image-viewer')).getByRole('img', {
|
|
1165
1359
|
hidden: true,
|
|
@@ -1177,6 +1371,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1177
1371
|
mimeType="video/mp4"
|
|
1178
1372
|
/>
|
|
1179
1373
|
)
|
|
1374
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1180
1375
|
const poster = screen.getByTestId('video-attachment').querySelector('img')
|
|
1181
1376
|
expect(poster?.getAttribute('src')).toBe(
|
|
1182
1377
|
'https://cdn.example.com/poster.jpg'
|
|
@@ -1185,7 +1380,8 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1185
1380
|
expect(poster?.getAttribute('decoding')).toBe('async')
|
|
1186
1381
|
})
|
|
1187
1382
|
|
|
1188
|
-
it('opens the viewer with `<video preload="metadata">` for the active item', () => {
|
|
1383
|
+
it('opens the viewer with `<video preload="metadata">` for the active item', async () => {
|
|
1384
|
+
const user = userEvent.setup()
|
|
1189
1385
|
renderWithProviders(
|
|
1190
1386
|
<MessageAttachment.Video.Sent
|
|
1191
1387
|
src="https://cdn.example.com/clip.mp4"
|
|
@@ -1194,7 +1390,8 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1194
1390
|
filename="clip.mp4"
|
|
1195
1391
|
/>
|
|
1196
1392
|
)
|
|
1197
|
-
|
|
1393
|
+
await user.click(screen.getByLabelText('Play video 1 of 1'))
|
|
1394
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1198
1395
|
const video = screen.getByTestId('video-viewer').querySelector('video')
|
|
1199
1396
|
expect(video?.getAttribute('preload')).toBe('metadata')
|
|
1200
1397
|
})
|
|
@@ -1213,64 +1410,12 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1213
1410
|
/>
|
|
1214
1411
|
)
|
|
1215
1412
|
const bubble = screen.getByTestId('video-attachment')
|
|
1413
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1216
1414
|
expect(bubble.querySelectorAll('video')).toHaveLength(0)
|
|
1415
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1217
1416
|
expect(bubble.querySelectorAll('img')).toHaveLength(4)
|
|
1218
1417
|
})
|
|
1219
1418
|
})
|
|
1220
|
-
|
|
1221
|
-
describe('Audio', () => {
|
|
1222
|
-
it('defaults a single-track bubble to preload="metadata"', () => {
|
|
1223
|
-
renderWithProviders(
|
|
1224
|
-
<MessageAttachment.Audio.Received
|
|
1225
|
-
src="https://cdn.example.com/song.mp3"
|
|
1226
|
-
mimeType="audio/mpeg"
|
|
1227
|
-
/>
|
|
1228
|
-
)
|
|
1229
|
-
const audio = screen
|
|
1230
|
-
.getByTestId('audio-attachment')
|
|
1231
|
-
.querySelector('audio')
|
|
1232
|
-
expect(audio?.getAttribute('preload')).toBe('metadata')
|
|
1233
|
-
})
|
|
1234
|
-
|
|
1235
|
-
it('defaults a stacked (3-track) bubble to preload="none" on every player', () => {
|
|
1236
|
-
renderWithProviders(
|
|
1237
|
-
<MessageAttachment.Audio.Sent
|
|
1238
|
-
items={[
|
|
1239
|
-
{ src: 'https://cdn.example.com/a.mp3', mimeType: 'audio/mpeg' },
|
|
1240
|
-
{ src: 'https://cdn.example.com/b.mp3', mimeType: 'audio/mpeg' },
|
|
1241
|
-
{ src: 'https://cdn.example.com/c.mp3', mimeType: 'audio/mpeg' },
|
|
1242
|
-
]}
|
|
1243
|
-
/>
|
|
1244
|
-
)
|
|
1245
|
-
const audios = Array.from(
|
|
1246
|
-
screen.getByTestId('audio-attachment').querySelectorAll('audio')
|
|
1247
|
-
)
|
|
1248
|
-
expect(audios).toHaveLength(3)
|
|
1249
|
-
audios.forEach((audio) => {
|
|
1250
|
-
expect(audio.getAttribute('preload')).toBe('none')
|
|
1251
|
-
})
|
|
1252
|
-
})
|
|
1253
|
-
|
|
1254
|
-
it('honors a per-track AudioItem.preload override', () => {
|
|
1255
|
-
renderWithProviders(
|
|
1256
|
-
<MessageAttachment.Audio.Sent
|
|
1257
|
-
items={[
|
|
1258
|
-
{ src: 'https://cdn.example.com/a.mp3', mimeType: 'audio/mpeg' },
|
|
1259
|
-
{
|
|
1260
|
-
src: 'https://cdn.example.com/b.mp3',
|
|
1261
|
-
mimeType: 'audio/mpeg',
|
|
1262
|
-
preload: 'metadata',
|
|
1263
|
-
},
|
|
1264
|
-
]}
|
|
1265
|
-
/>
|
|
1266
|
-
)
|
|
1267
|
-
const audios = Array.from(
|
|
1268
|
-
screen.getByTestId('audio-attachment').querySelectorAll('audio')
|
|
1269
|
-
)
|
|
1270
|
-
expect(audios[0]?.getAttribute('preload')).toBe('none')
|
|
1271
|
-
expect(audios[1]?.getAttribute('preload')).toBe('metadata')
|
|
1272
|
-
})
|
|
1273
|
-
})
|
|
1274
1419
|
})
|
|
1275
1420
|
|
|
1276
1421
|
describe('bubbleGroupPositionFromStream', () => {
|