@linktr.ee/messaging-react 4.4.7 → 4.4.8-rc-1788251276
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AttachmentCard-DSekwIOA.js → AttachmentCard-BaZk77fe.js} +136 -125
- package/dist/AttachmentCard-BaZk77fe.js.map +1 -0
- package/dist/AttachmentCard-ZpYg622K.cjs +2 -0
- package/dist/AttachmentCard-ZpYg622K.cjs.map +1 -0
- package/dist/{Card-BS6Fza2a.cjs → Card-4irOjGaI.cjs} +2 -2
- package/dist/{Card-BS6Fza2a.cjs.map → Card-4irOjGaI.cjs.map} +1 -1
- package/dist/{Card-BhwBkgxI.js → Card-BdyQhysm.js} +2 -2
- package/dist/{Card-BhwBkgxI.js.map → Card-BdyQhysm.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +915 -907
- 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/MediaPlayer.tsx +3 -5
- package/src/components/AttachmentCard/Thumbnail.test.tsx +4 -0
- package/src/components/ChannelActionsMenu/ChannelActionsMenu.test.tsx +18 -10
- package/src/components/ChannelActionsMenu/index.tsx +13 -4
- 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 +12 -5
- 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/MessageAttachment.behavior.test.tsx +22 -15
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +117 -60
- 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/DownloadAction.tsx +4 -10
- 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/MessageBubble/MessageBubble.test.tsx +8 -0
- package/src/components/RichCard/RichCard.test.tsx +10 -2
- package/src/components/_shared/Spinner.test.tsx +75 -0
- package/src/components/_shared/Spinner.tsx +51 -0
- package/dist/AttachmentCard-BbsjzdLJ.cjs +0 -2
- package/dist/AttachmentCard-BbsjzdLJ.cjs.map +0 -1
- package/dist/AttachmentCard-DSekwIOA.js.map +0 -1
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
renderWithProviders,
|
|
7
7
|
screen,
|
|
8
8
|
within,
|
|
9
|
+
userEvent,
|
|
9
10
|
} from '../../test/utils'
|
|
10
11
|
|
|
11
12
|
import { triggerDownload } from './_shared/triggerDownload'
|
|
@@ -38,7 +39,8 @@ const expectViewerOpen = (testId: string) => {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
describe('MessageAttachment.Image', () => {
|
|
41
|
-
it('optimizes Linktree-hosted composer previews without changing the viewer source', () => {
|
|
42
|
+
it('optimizes Linktree-hosted composer previews without changing the viewer source', async () => {
|
|
43
|
+
const user = userEvent.setup()
|
|
42
44
|
renderWithProviders(
|
|
43
45
|
<MessageAttachment.Image.Composer
|
|
44
46
|
src="https://assets.linktr.ee/photo.jpg"
|
|
@@ -51,8 +53,9 @@ describe('MessageAttachment.Image', () => {
|
|
|
51
53
|
'https://assets.linktr.ee/photo.jpg?io=true&size=messaging-attachment-v1_0'
|
|
52
54
|
)
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
await user.click(screen.getByLabelText('Open image'))
|
|
55
57
|
const viewer = expectViewerOpen('image-viewer')
|
|
58
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
56
59
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
57
60
|
'https://assets.linktr.ee/photo.jpg'
|
|
58
61
|
)
|
|
@@ -69,12 +72,14 @@ describe('MessageAttachment.Image', () => {
|
|
|
69
72
|
const bubble = screen.getByTestId('image-attachment')
|
|
70
73
|
expect(bubble).toBeInTheDocument()
|
|
71
74
|
expect(screen.getByText('Here is the image')).toBeInTheDocument()
|
|
75
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
72
76
|
expect(bubble.querySelector('img')?.getAttribute('src')).toBe(
|
|
73
77
|
'https://cdn.example.com/photo.jpg'
|
|
74
78
|
)
|
|
75
79
|
})
|
|
76
80
|
|
|
77
|
-
it('opens the ImageViewer lightbox when clicked', () => {
|
|
81
|
+
it('opens the ImageViewer lightbox when clicked', async () => {
|
|
82
|
+
const user = userEvent.setup()
|
|
78
83
|
renderWithProviders(
|
|
79
84
|
<MessageAttachment.Image.Received
|
|
80
85
|
src="https://cdn.example.com/photo.jpg"
|
|
@@ -85,7 +90,7 @@ describe('MessageAttachment.Image', () => {
|
|
|
85
90
|
|
|
86
91
|
expectViewerClosed('image-viewer')
|
|
87
92
|
|
|
88
|
-
|
|
93
|
+
await user.click(screen.getByLabelText('Open image 1 of 1'))
|
|
89
94
|
|
|
90
95
|
expectViewerOpen('image-viewer')
|
|
91
96
|
})
|
|
@@ -121,7 +126,8 @@ describe('MessageAttachment.Image', () => {
|
|
|
121
126
|
expect(screen.getByLabelText('Open image 3 of 3')).toBeInTheDocument()
|
|
122
127
|
})
|
|
123
128
|
|
|
124
|
-
it('renders a download button inside the image viewer', () => {
|
|
129
|
+
it('renders a download button inside the image viewer', async () => {
|
|
130
|
+
const user = userEvent.setup()
|
|
125
131
|
renderWithProviders(
|
|
126
132
|
<MessageAttachment.Image.Received
|
|
127
133
|
src="https://cdn.example.com/photo.jpg"
|
|
@@ -129,30 +135,33 @@ describe('MessageAttachment.Image', () => {
|
|
|
129
135
|
filename="photo.jpg"
|
|
130
136
|
/>
|
|
131
137
|
)
|
|
132
|
-
|
|
138
|
+
await user.click(screen.getByLabelText('Open image 1 of 1'))
|
|
133
139
|
const viewer = expectViewerOpen('image-viewer')
|
|
134
140
|
// The viewer chrome carries its own download action — separate
|
|
135
141
|
// from any sibling download button on the bubble surface — so the
|
|
136
142
|
// user can grab the image without leaving the lightbox.
|
|
137
143
|
expect(
|
|
144
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
138
145
|
viewer.querySelector('button[aria-label="Download photo.jpg"]')
|
|
139
146
|
).not.toBeNull()
|
|
140
147
|
})
|
|
141
148
|
|
|
142
|
-
it('does not render carousel nav controls for a single image', () => {
|
|
149
|
+
it('does not render carousel nav controls for a single image', async () => {
|
|
150
|
+
const user = userEvent.setup()
|
|
143
151
|
renderWithProviders(
|
|
144
152
|
<MessageAttachment.Image.Received
|
|
145
153
|
src="https://cdn.example.com/photo.jpg"
|
|
146
154
|
filename="photo.jpg"
|
|
147
155
|
/>
|
|
148
156
|
)
|
|
149
|
-
|
|
157
|
+
await user.click(screen.getByLabelText('Open image 1 of 1'))
|
|
150
158
|
expectViewerOpen('image-viewer')
|
|
151
159
|
expect(screen.queryByLabelText('Next image')).toBeNull()
|
|
152
160
|
expect(screen.queryByLabelText('Previous image')).toBeNull()
|
|
153
161
|
})
|
|
154
162
|
|
|
155
|
-
it('renders the stacked images as a carousel inside the viewer', () => {
|
|
163
|
+
it('renders the stacked images as a carousel inside the viewer', async () => {
|
|
164
|
+
const user = userEvent.setup()
|
|
156
165
|
renderWithProviders(
|
|
157
166
|
<MessageAttachment.Image.Sent
|
|
158
167
|
filename="album"
|
|
@@ -165,8 +174,9 @@ describe('MessageAttachment.Image', () => {
|
|
|
165
174
|
)
|
|
166
175
|
|
|
167
176
|
// Open the second tile — the viewer should land on image 2.
|
|
168
|
-
|
|
177
|
+
await user.click(screen.getByLabelText('Open image 2 of 3'))
|
|
169
178
|
const viewer = expectViewerOpen('image-viewer')
|
|
179
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
170
180
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
171
181
|
'https://cdn.example.com/b.jpg'
|
|
172
182
|
)
|
|
@@ -174,26 +184,30 @@ describe('MessageAttachment.Image', () => {
|
|
|
174
184
|
expect(viewer.textContent).toContain('2 / 3')
|
|
175
185
|
|
|
176
186
|
// Next button advances by one.
|
|
177
|
-
|
|
187
|
+
await user.click(screen.getByLabelText('Next image'))
|
|
188
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
178
189
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
179
190
|
'https://cdn.example.com/c.jpg'
|
|
180
191
|
)
|
|
181
192
|
expect(viewer.textContent).toContain('3 / 3')
|
|
182
193
|
|
|
183
194
|
// Next wraps from the last item back to the first.
|
|
184
|
-
|
|
195
|
+
await user.click(screen.getByLabelText('Next image'))
|
|
196
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
185
197
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
186
198
|
'https://cdn.example.com/a.jpg'
|
|
187
199
|
)
|
|
188
200
|
|
|
189
201
|
// Previous wraps from the first item to the last.
|
|
190
|
-
|
|
202
|
+
await user.click(screen.getByLabelText('Previous image'))
|
|
203
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
191
204
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
192
205
|
'https://cdn.example.com/c.jpg'
|
|
193
206
|
)
|
|
194
207
|
})
|
|
195
208
|
|
|
196
|
-
it('uses ArrowRight / ArrowLeft to navigate stacked images', () => {
|
|
209
|
+
it('uses ArrowRight / ArrowLeft to navigate stacked images', async () => {
|
|
210
|
+
const user = userEvent.setup()
|
|
197
211
|
renderWithProviders(
|
|
198
212
|
<MessageAttachment.Image.Sent
|
|
199
213
|
items={[
|
|
@@ -202,18 +216,21 @@ describe('MessageAttachment.Image', () => {
|
|
|
202
216
|
]}
|
|
203
217
|
/>
|
|
204
218
|
)
|
|
205
|
-
|
|
219
|
+
await user.click(screen.getByLabelText('Open image 1 of 2'))
|
|
206
220
|
const viewer = expectViewerOpen('image-viewer')
|
|
221
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
207
222
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
208
223
|
'https://cdn.example.com/a.jpg'
|
|
209
224
|
)
|
|
210
225
|
|
|
211
226
|
fireEvent.keyDown(window, { key: 'ArrowRight' })
|
|
227
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
212
228
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
213
229
|
'https://cdn.example.com/b.jpg'
|
|
214
230
|
)
|
|
215
231
|
|
|
216
232
|
fireEvent.keyDown(window, { key: 'ArrowLeft' })
|
|
233
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
217
234
|
expect(viewer.querySelector('img')?.getAttribute('src')).toBe(
|
|
218
235
|
'https://cdn.example.com/a.jpg'
|
|
219
236
|
)
|
|
@@ -242,6 +259,7 @@ describe('MessageAttachment.Image single-image aspect ratio', () => {
|
|
|
242
259
|
const WIDTH_CAP_WEB_PX = 326
|
|
243
260
|
|
|
244
261
|
const mediaBox = (label: string) =>
|
|
262
|
+
// eslint-disable-next-line testing-library/no-node-access -- the wrapper class is a deliberate DOM contract with no semantic query.
|
|
245
263
|
screen.getByLabelText(label).parentElement as HTMLElement
|
|
246
264
|
|
|
247
265
|
/** The `min(var(--…), Npx)` term the height cap contributes. */
|
|
@@ -319,6 +337,7 @@ describe('MessageAttachment.Image single-image aspect ratio', () => {
|
|
|
319
337
|
|
|
320
338
|
// Scoped to the tile: the always-mounted `ImageViewer` renders a
|
|
321
339
|
// second `<img>` for the same source.
|
|
340
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
322
341
|
const img = mediaBox('Open image 1 of 1').querySelector(
|
|
323
342
|
'img'
|
|
324
343
|
) as HTMLImageElement
|
|
@@ -405,7 +424,8 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
405
424
|
expect(screen.getByText('PDF · 379.5 KB')).toBeInTheDocument()
|
|
406
425
|
})
|
|
407
426
|
|
|
408
|
-
it('opens the PdfViewer when clicked', () => {
|
|
427
|
+
it('opens the PdfViewer when clicked', async () => {
|
|
428
|
+
const user = userEvent.setup()
|
|
409
429
|
renderWithProviders(
|
|
410
430
|
<MessageAttachment.Pdf.Sent
|
|
411
431
|
src="https://cdn.example.com/doc.pdf"
|
|
@@ -413,11 +433,12 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
413
433
|
/>
|
|
414
434
|
)
|
|
415
435
|
expectViewerClosed('pdf-viewer')
|
|
416
|
-
|
|
436
|
+
await user.click(screen.getByLabelText('Open doc.pdf'))
|
|
417
437
|
expectViewerOpen('pdf-viewer')
|
|
418
438
|
})
|
|
419
439
|
|
|
420
|
-
it('forwards the onClick handler before opening the viewer', () => {
|
|
440
|
+
it('forwards the onClick handler before opening the viewer', async () => {
|
|
441
|
+
const user = userEvent.setup()
|
|
421
442
|
const onClick = vi.fn()
|
|
422
443
|
renderWithProviders(
|
|
423
444
|
<MessageAttachment.Pdf.Received
|
|
@@ -426,7 +447,7 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
426
447
|
onClick={onClick}
|
|
427
448
|
/>
|
|
428
449
|
)
|
|
429
|
-
|
|
450
|
+
await user.click(screen.getByLabelText('Open doc.pdf'))
|
|
430
451
|
expect(onClick).toHaveBeenCalledTimes(1)
|
|
431
452
|
})
|
|
432
453
|
|
|
@@ -457,7 +478,8 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
457
478
|
expect(screen.getByLabelText('Open c.pdf')).toBeInTheDocument()
|
|
458
479
|
})
|
|
459
480
|
|
|
460
|
-
it('opens the viewer on the activated row in a stacked PDF bubble', () => {
|
|
481
|
+
it('opens the viewer on the activated row in a stacked PDF bubble', async () => {
|
|
482
|
+
const user = userEvent.setup()
|
|
461
483
|
renderWithProviders(
|
|
462
484
|
<MessageAttachment.Pdf.Received
|
|
463
485
|
items={[
|
|
@@ -467,7 +489,7 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
467
489
|
/>
|
|
468
490
|
)
|
|
469
491
|
expectViewerClosed('pdf-viewer')
|
|
470
|
-
|
|
492
|
+
await user.click(screen.getByLabelText('Open b.pdf'))
|
|
471
493
|
const viewer = expectViewerOpen('pdf-viewer')
|
|
472
494
|
// The active filename surfaces as the dialog's accessible name —
|
|
473
495
|
// the lightbox chrome is intentionally just close + carousel chrome
|
|
@@ -479,7 +501,8 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
479
501
|
expect(viewer.textContent).toContain('2 / 2')
|
|
480
502
|
})
|
|
481
503
|
|
|
482
|
-
it('navigates between stacked PDFs via the carousel controls', () => {
|
|
504
|
+
it('navigates between stacked PDFs via the carousel controls', async () => {
|
|
505
|
+
const user = userEvent.setup()
|
|
483
506
|
renderWithProviders(
|
|
484
507
|
<MessageAttachment.Pdf.Received
|
|
485
508
|
items={[
|
|
@@ -489,18 +512,18 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
489
512
|
]}
|
|
490
513
|
/>
|
|
491
514
|
)
|
|
492
|
-
|
|
515
|
+
await user.click(screen.getByLabelText('Open a.pdf'))
|
|
493
516
|
const viewer = expectViewerOpen('pdf-viewer')
|
|
494
517
|
expect(viewer.getAttribute('aria-label')).toBe('a.pdf')
|
|
495
518
|
|
|
496
|
-
|
|
519
|
+
await user.click(screen.getByLabelText('Next document'))
|
|
497
520
|
expect(viewer.getAttribute('aria-label')).toBe('b.pdf')
|
|
498
521
|
|
|
499
|
-
|
|
522
|
+
await user.click(screen.getByLabelText('Next document'))
|
|
500
523
|
expect(viewer.getAttribute('aria-label')).toBe('c.pdf')
|
|
501
524
|
|
|
502
525
|
// Wraps back to the start.
|
|
503
|
-
|
|
526
|
+
await user.click(screen.getByLabelText('Next document'))
|
|
504
527
|
expect(viewer.getAttribute('aria-label')).toBe('a.pdf')
|
|
505
528
|
})
|
|
506
529
|
|
|
@@ -537,7 +560,8 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
537
560
|
expect(screen.getByLabelText('Download c.pdf')).toBeInTheDocument()
|
|
538
561
|
})
|
|
539
562
|
|
|
540
|
-
it('does not open the viewer when the Download button is clicked', () => {
|
|
563
|
+
it('does not open the viewer when the Download button is clicked', async () => {
|
|
564
|
+
const user = userEvent.setup()
|
|
541
565
|
renderWithProviders(
|
|
542
566
|
<MessageAttachment.Pdf.Sent
|
|
543
567
|
src="https://cdn.example.com/notes.pdf"
|
|
@@ -545,7 +569,7 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
545
569
|
/>
|
|
546
570
|
)
|
|
547
571
|
expectViewerClosed('pdf-viewer')
|
|
548
|
-
|
|
572
|
+
await user.click(screen.getByLabelText('Download notes.pdf'))
|
|
549
573
|
// Download should fire on its own without bubbling up to the row
|
|
550
574
|
// activation that opens the viewer.
|
|
551
575
|
expectViewerClosed('pdf-viewer')
|
|
@@ -563,7 +587,8 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
563
587
|
expect(screen.getByLabelText('Dismiss attachment')).toBeInTheDocument()
|
|
564
588
|
})
|
|
565
589
|
|
|
566
|
-
it('does not crash when items shrinks after the viewer opens (P1 regression)', () => {
|
|
590
|
+
it('does not crash when items shrinks after the viewer opens (P1 regression)', async () => {
|
|
591
|
+
const user = userEvent.setup()
|
|
567
592
|
// Regression test for the PR #151 P1 reviewer finding:
|
|
568
593
|
// `viewerIndex` was dereferenced directly against `resolvedItems`,
|
|
569
594
|
// so a re-render with fewer items than the open index would crash
|
|
@@ -578,7 +603,7 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
578
603
|
]}
|
|
579
604
|
/>
|
|
580
605
|
)
|
|
581
|
-
|
|
606
|
+
await user.click(screen.getByLabelText('Open c.pdf'))
|
|
582
607
|
expectViewerOpen('pdf-viewer')
|
|
583
608
|
|
|
584
609
|
expect(() =>
|
|
@@ -599,7 +624,8 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
599
624
|
)
|
|
600
625
|
})
|
|
601
626
|
|
|
602
|
-
it('does not open the viewer when onClick returns false', () => {
|
|
627
|
+
it('does not open the viewer when onClick returns false', async () => {
|
|
628
|
+
const user = userEvent.setup()
|
|
603
629
|
const onClick = vi.fn(() => false as const)
|
|
604
630
|
renderWithProviders(
|
|
605
631
|
<MessageAttachment.Pdf.Received
|
|
@@ -608,7 +634,7 @@ describe('MessageAttachment.Pdf', () => {
|
|
|
608
634
|
onClick={onClick}
|
|
609
635
|
/>
|
|
610
636
|
)
|
|
611
|
-
|
|
637
|
+
await user.click(screen.getByLabelText('Open doc.pdf'))
|
|
612
638
|
expect(onClick).toHaveBeenCalledTimes(1)
|
|
613
639
|
expectViewerClosed('pdf-viewer')
|
|
614
640
|
})
|
|
@@ -669,7 +695,8 @@ describe('MessageAttachment.File', () => {
|
|
|
669
695
|
expect(screen.getByLabelText('Download c.pdf')).toBeInTheDocument()
|
|
670
696
|
})
|
|
671
697
|
|
|
672
|
-
it('downloads the file when the row is clicked and onClick allows it', () => {
|
|
698
|
+
it('downloads the file when the row is clicked and onClick allows it', async () => {
|
|
699
|
+
const user = userEvent.setup()
|
|
673
700
|
const onClick = vi.fn()
|
|
674
701
|
renderWithProviders(
|
|
675
702
|
<MessageAttachment.File.Received
|
|
@@ -678,7 +705,7 @@ describe('MessageAttachment.File', () => {
|
|
|
678
705
|
onClick={onClick}
|
|
679
706
|
/>
|
|
680
707
|
)
|
|
681
|
-
|
|
708
|
+
await user.click(screen.getByLabelText('Download workout-plan.zip'))
|
|
682
709
|
expect(onClick).toHaveBeenCalledWith(0)
|
|
683
710
|
expect(mockTriggerDownload).toHaveBeenCalledTimes(1)
|
|
684
711
|
expect(mockTriggerDownload).toHaveBeenCalledWith(
|
|
@@ -687,7 +714,8 @@ describe('MessageAttachment.File', () => {
|
|
|
687
714
|
)
|
|
688
715
|
})
|
|
689
716
|
|
|
690
|
-
it('lets onClick cancel the automatic download by returning false (P2 regression)', () => {
|
|
717
|
+
it('lets onClick cancel the automatic download by returning false (P2 regression)', async () => {
|
|
718
|
+
const user = userEvent.setup()
|
|
691
719
|
// Regression test for the PR #151 P2 reviewer finding:
|
|
692
720
|
// the docs advertised `onClick` as a hook for intercepting the
|
|
693
721
|
// download (e.g. confirmation modal), but the handler used to fire
|
|
@@ -701,12 +729,13 @@ describe('MessageAttachment.File', () => {
|
|
|
701
729
|
onClick={onClick}
|
|
702
730
|
/>
|
|
703
731
|
)
|
|
704
|
-
|
|
732
|
+
await user.click(screen.getByLabelText('Download workout-plan.zip'))
|
|
705
733
|
expect(onClick).toHaveBeenCalledTimes(1)
|
|
706
734
|
expect(mockTriggerDownload).not.toHaveBeenCalled()
|
|
707
735
|
})
|
|
708
736
|
|
|
709
|
-
it('still downloads when onClick returns void', () => {
|
|
737
|
+
it('still downloads when onClick returns void', async () => {
|
|
738
|
+
const user = userEvent.setup()
|
|
710
739
|
// Belt-and-braces: void / undefined returns must not be confused
|
|
711
740
|
// with `false` (the only cancellation sentinel).
|
|
712
741
|
const onClick = vi.fn(() => undefined)
|
|
@@ -717,7 +746,7 @@ describe('MessageAttachment.File', () => {
|
|
|
717
746
|
onClick={onClick}
|
|
718
747
|
/>
|
|
719
748
|
)
|
|
720
|
-
|
|
749
|
+
await user.click(screen.getByLabelText('Download workout-plan.zip'))
|
|
721
750
|
expect(mockTriggerDownload).toHaveBeenCalledTimes(1)
|
|
722
751
|
})
|
|
723
752
|
})
|
|
@@ -732,6 +761,7 @@ describe('MessageAttachment.Audio', () => {
|
|
|
732
761
|
/>
|
|
733
762
|
)
|
|
734
763
|
const bubble = screen.getByTestId('audio-attachment')
|
|
764
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
735
765
|
expect(bubble.querySelector('audio')).not.toBeNull()
|
|
736
766
|
// No filename title — the native `<audio>` element doesn't expose
|
|
737
767
|
// a title slot, so we render just the player.
|
|
@@ -741,7 +771,8 @@ describe('MessageAttachment.Audio', () => {
|
|
|
741
771
|
expect(screen.queryByRole('button', { name: /download/i })).toBeNull()
|
|
742
772
|
})
|
|
743
773
|
|
|
744
|
-
it('shows an inline dismiss button next to the player on Composer state', () => {
|
|
774
|
+
it('shows an inline dismiss button next to the player on Composer state', async () => {
|
|
775
|
+
const user = userEvent.setup()
|
|
745
776
|
const onDismiss = vi.fn()
|
|
746
777
|
renderWithProviders(
|
|
747
778
|
<MessageAttachment.Audio.Composer
|
|
@@ -751,7 +782,7 @@ describe('MessageAttachment.Audio', () => {
|
|
|
751
782
|
)
|
|
752
783
|
const dismiss = screen.getByLabelText('Dismiss attachment')
|
|
753
784
|
expect(dismiss).toBeInTheDocument()
|
|
754
|
-
|
|
785
|
+
await user.click(dismiss)
|
|
755
786
|
expect(onDismiss).toHaveBeenCalledTimes(1)
|
|
756
787
|
})
|
|
757
788
|
|
|
@@ -766,12 +797,14 @@ describe('MessageAttachment.Audio', () => {
|
|
|
766
797
|
/>
|
|
767
798
|
)
|
|
768
799
|
const bubble = screen.getByTestId('audio-attachment')
|
|
800
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
769
801
|
expect(bubble.querySelectorAll('audio')).toHaveLength(3)
|
|
770
802
|
})
|
|
771
803
|
})
|
|
772
804
|
|
|
773
805
|
describe('MessageAttachment.Video', () => {
|
|
774
|
-
it('opens the VideoViewer when clicked', () => {
|
|
806
|
+
it('opens the VideoViewer when clicked', async () => {
|
|
807
|
+
const user = userEvent.setup()
|
|
775
808
|
renderWithProviders(
|
|
776
809
|
<MessageAttachment.Video.Received
|
|
777
810
|
src="https://cdn.example.com/clip.mp4"
|
|
@@ -781,13 +814,14 @@ describe('MessageAttachment.Video', () => {
|
|
|
781
814
|
/>
|
|
782
815
|
)
|
|
783
816
|
expectViewerClosed('video-viewer')
|
|
784
|
-
|
|
817
|
+
await user.click(screen.getByLabelText('Play video 1 of 1'))
|
|
785
818
|
expectViewerOpen('video-viewer')
|
|
786
819
|
// Single-item viewer has no carousel chrome.
|
|
787
820
|
expect(screen.queryByLabelText('Next video')).toBeNull()
|
|
788
821
|
})
|
|
789
822
|
|
|
790
|
-
it('navigates between stacked videos via the carousel controls', () => {
|
|
823
|
+
it('navigates between stacked videos via the carousel controls', async () => {
|
|
824
|
+
const user = userEvent.setup()
|
|
791
825
|
renderWithProviders(
|
|
792
826
|
<MessageAttachment.Video.Sent
|
|
793
827
|
filename="clip"
|
|
@@ -805,13 +839,15 @@ describe('MessageAttachment.Video', () => {
|
|
|
805
839
|
]}
|
|
806
840
|
/>
|
|
807
841
|
)
|
|
808
|
-
|
|
842
|
+
await user.click(screen.getByLabelText('Play video 1 of 2'))
|
|
809
843
|
const viewer = expectViewerOpen('video-viewer')
|
|
844
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
810
845
|
expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
|
|
811
846
|
'https://cdn.example.com/a.mp4'
|
|
812
847
|
)
|
|
813
848
|
|
|
814
|
-
|
|
849
|
+
await user.click(screen.getByLabelText('Next video'))
|
|
850
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
815
851
|
expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
|
|
816
852
|
'https://cdn.example.com/b.mp4'
|
|
817
853
|
)
|
|
@@ -841,11 +877,13 @@ describe('MessageAttachment.Video overflow', () => {
|
|
|
841
877
|
expect(screen.queryByLabelText('Play video 5 of 9')).toBeNull()
|
|
842
878
|
})
|
|
843
879
|
|
|
844
|
-
it('opens the viewer on the fourth video from the overflow tile and pages to the ninth', () => {
|
|
880
|
+
it('opens the viewer on the fourth video from the overflow tile and pages to the ninth', async () => {
|
|
881
|
+
const user = userEvent.setup()
|
|
845
882
|
renderWithProviders(<MessageAttachment.Video.Sent items={NINE_VIDEOS} />)
|
|
846
883
|
|
|
847
|
-
|
|
884
|
+
await user.click(screen.getByLabelText('Play video 4 of 9'))
|
|
848
885
|
const viewer = expectViewerOpen('video-viewer')
|
|
886
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
849
887
|
expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
|
|
850
888
|
'https://cdn.example.com/3.mp4'
|
|
851
889
|
)
|
|
@@ -854,8 +892,9 @@ describe('MessageAttachment.Video overflow', () => {
|
|
|
854
892
|
|
|
855
893
|
// The carousel spans all nine, not just the four the grid drew.
|
|
856
894
|
for (let i = 0; i < 5; i += 1) {
|
|
857
|
-
|
|
895
|
+
await user.click(screen.getByLabelText('Next video'))
|
|
858
896
|
}
|
|
897
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
859
898
|
expect(viewer.querySelector('video')?.getAttribute('src')).toBe(
|
|
860
899
|
'https://cdn.example.com/8.mp4'
|
|
861
900
|
)
|
|
@@ -867,7 +906,9 @@ describe('MessageAttachment.Video overflow', () => {
|
|
|
867
906
|
|
|
868
907
|
const tiles = screen.getAllByRole('button', { name: /^Play video/ })
|
|
869
908
|
// The badge is the only `svg` a poster tile renders.
|
|
909
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
870
910
|
expect(tiles[0].querySelector('svg')).not.toBeNull()
|
|
911
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
871
912
|
expect(tiles[3].querySelector('svg')).toBeNull()
|
|
872
913
|
})
|
|
873
914
|
})
|
|
@@ -878,6 +919,7 @@ describe('MessageAttachment.Video single-card aspect', () => {
|
|
|
878
919
|
// jsdom never decodes an `<img>`, so `naturalWidth` stays 0 and `load`
|
|
879
920
|
// never fires on its own — stub the intrinsic size and fire it.
|
|
880
921
|
const loadPosterAt = (naturalWidth: number, naturalHeight: number) => {
|
|
922
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
881
923
|
const poster = screen.getByTestId('video-attachment').querySelector('img')
|
|
882
924
|
if (!poster) throw new Error('no poster rendered')
|
|
883
925
|
Object.defineProperty(poster, 'naturalWidth', {
|
|
@@ -892,6 +934,7 @@ describe('MessageAttachment.Video single-card aspect', () => {
|
|
|
892
934
|
}
|
|
893
935
|
|
|
894
936
|
const mediaAspect = () =>
|
|
937
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
895
938
|
screen.getByTestId('video-attachment').querySelector<HTMLElement>('[style]')
|
|
896
939
|
?.style.aspectRatio
|
|
897
940
|
|
|
@@ -1054,23 +1097,25 @@ describe('MessageAttachment.Video single-card aspect', () => {
|
|
|
1054
1097
|
})
|
|
1055
1098
|
|
|
1056
1099
|
describe('ViewerShell focus management', () => {
|
|
1057
|
-
it('moves focus into the dialog when a viewer opens', () => {
|
|
1100
|
+
it('moves focus into the dialog when a viewer opens', async () => {
|
|
1101
|
+
const user = userEvent.setup()
|
|
1058
1102
|
renderWithProviders(
|
|
1059
1103
|
<MessageAttachment.Pdf.Received
|
|
1060
1104
|
src="https://cdn.example.com/doc.pdf"
|
|
1061
1105
|
filename="doc.pdf"
|
|
1062
1106
|
/>
|
|
1063
1107
|
)
|
|
1064
|
-
|
|
1108
|
+
await user.click(screen.getByLabelText('Open doc.pdf'))
|
|
1065
1109
|
|
|
1066
1110
|
// The close button is the primary focus target in the viewer
|
|
1067
1111
|
// toolbar — keyboard users should land on it on open so Escape
|
|
1068
1112
|
// / Tab work without an extra click.
|
|
1069
1113
|
const close = screen.getByLabelText('Close viewer')
|
|
1070
|
-
expect(
|
|
1114
|
+
expect(close).toHaveFocus()
|
|
1071
1115
|
})
|
|
1072
1116
|
|
|
1073
|
-
it('restores focus to the opener when the viewer closes', () => {
|
|
1117
|
+
it('restores focus to the opener when the viewer closes', async () => {
|
|
1118
|
+
const user = userEvent.setup()
|
|
1074
1119
|
renderWithProviders(
|
|
1075
1120
|
<MessageAttachment.Pdf.Received
|
|
1076
1121
|
src="https://cdn.example.com/doc.pdf"
|
|
@@ -1079,18 +1124,18 @@ describe('ViewerShell focus management', () => {
|
|
|
1079
1124
|
)
|
|
1080
1125
|
const opener = screen.getByLabelText('Open doc.pdf')
|
|
1081
1126
|
opener.focus()
|
|
1082
|
-
expect(
|
|
1127
|
+
expect(opener).toHaveFocus()
|
|
1083
1128
|
|
|
1084
|
-
|
|
1129
|
+
await user.click(opener)
|
|
1085
1130
|
expectViewerOpen('pdf-viewer')
|
|
1086
1131
|
|
|
1087
|
-
|
|
1132
|
+
await user.click(screen.getByLabelText('Close viewer'))
|
|
1088
1133
|
|
|
1089
1134
|
// After close, focus should hop back to whatever was focused
|
|
1090
1135
|
// before the viewer mounted (the row's Open button), and the
|
|
1091
1136
|
// dialog should no longer carry the `[open]` attribute.
|
|
1092
1137
|
expectViewerClosed('pdf-viewer')
|
|
1093
|
-
expect(
|
|
1138
|
+
expect(opener).toHaveFocus()
|
|
1094
1139
|
})
|
|
1095
1140
|
})
|
|
1096
1141
|
|
|
@@ -1103,6 +1148,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1103
1148
|
alt="Photo"
|
|
1104
1149
|
/>
|
|
1105
1150
|
)
|
|
1151
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1106
1152
|
const img = screen.getByTestId('image-attachment').querySelector('img')
|
|
1107
1153
|
expect(img?.getAttribute('loading')).toBe('lazy')
|
|
1108
1154
|
expect(img?.getAttribute('decoding')).toBe('async')
|
|
@@ -1116,6 +1162,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1116
1162
|
loading="eager"
|
|
1117
1163
|
/>
|
|
1118
1164
|
)
|
|
1165
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1119
1166
|
const img = screen.getByTestId('image-attachment').querySelector('img')
|
|
1120
1167
|
expect(img?.getAttribute('loading')).toBe('eager')
|
|
1121
1168
|
expect(img?.getAttribute('decoding')).toBe('async')
|
|
@@ -1135,13 +1182,15 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1135
1182
|
/>
|
|
1136
1183
|
)
|
|
1137
1184
|
const [first, second] = Array.from(
|
|
1185
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1138
1186
|
screen.getByTestId('image-attachment').querySelectorAll('img')
|
|
1139
1187
|
)
|
|
1140
1188
|
expect(first?.getAttribute('loading')).toBe('eager')
|
|
1141
1189
|
expect(second?.getAttribute('loading')).toBe('lazy')
|
|
1142
1190
|
})
|
|
1143
1191
|
|
|
1144
|
-
it('does not mount the viewer `<img>` while the lightbox is closed', () => {
|
|
1192
|
+
it('does not mount the viewer `<img>` while the lightbox is closed', async () => {
|
|
1193
|
+
const user = userEvent.setup()
|
|
1145
1194
|
renderWithProviders(
|
|
1146
1195
|
<MessageAttachment.Image.Sent
|
|
1147
1196
|
src="https://cdn.example.com/photo.jpg"
|
|
@@ -1159,7 +1208,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1159
1208
|
})
|
|
1160
1209
|
).toHaveLength(0)
|
|
1161
1210
|
|
|
1162
|
-
|
|
1211
|
+
await user.click(screen.getByLabelText('Open image 1 of 1'))
|
|
1163
1212
|
expect(
|
|
1164
1213
|
within(screen.getByTestId('image-viewer')).getByRole('img', {
|
|
1165
1214
|
hidden: true,
|
|
@@ -1177,6 +1226,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1177
1226
|
mimeType="video/mp4"
|
|
1178
1227
|
/>
|
|
1179
1228
|
)
|
|
1229
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1180
1230
|
const poster = screen.getByTestId('video-attachment').querySelector('img')
|
|
1181
1231
|
expect(poster?.getAttribute('src')).toBe(
|
|
1182
1232
|
'https://cdn.example.com/poster.jpg'
|
|
@@ -1185,7 +1235,8 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1185
1235
|
expect(poster?.getAttribute('decoding')).toBe('async')
|
|
1186
1236
|
})
|
|
1187
1237
|
|
|
1188
|
-
it('opens the viewer with `<video preload="metadata">` for the active item', () => {
|
|
1238
|
+
it('opens the viewer with `<video preload="metadata">` for the active item', async () => {
|
|
1239
|
+
const user = userEvent.setup()
|
|
1189
1240
|
renderWithProviders(
|
|
1190
1241
|
<MessageAttachment.Video.Sent
|
|
1191
1242
|
src="https://cdn.example.com/clip.mp4"
|
|
@@ -1194,7 +1245,8 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1194
1245
|
filename="clip.mp4"
|
|
1195
1246
|
/>
|
|
1196
1247
|
)
|
|
1197
|
-
|
|
1248
|
+
await user.click(screen.getByLabelText('Play video 1 of 1'))
|
|
1249
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1198
1250
|
const video = screen.getByTestId('video-viewer').querySelector('video')
|
|
1199
1251
|
expect(video?.getAttribute('preload')).toBe('metadata')
|
|
1200
1252
|
})
|
|
@@ -1213,7 +1265,9 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1213
1265
|
/>
|
|
1214
1266
|
)
|
|
1215
1267
|
const bubble = screen.getByTestId('video-attachment')
|
|
1268
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1216
1269
|
expect(bubble.querySelectorAll('video')).toHaveLength(0)
|
|
1270
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1217
1271
|
expect(bubble.querySelectorAll('img')).toHaveLength(4)
|
|
1218
1272
|
})
|
|
1219
1273
|
})
|
|
@@ -1228,6 +1282,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1228
1282
|
)
|
|
1229
1283
|
const audio = screen
|
|
1230
1284
|
.getByTestId('audio-attachment')
|
|
1285
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1231
1286
|
.querySelector('audio')
|
|
1232
1287
|
expect(audio?.getAttribute('preload')).toBe('metadata')
|
|
1233
1288
|
})
|
|
@@ -1243,6 +1298,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1243
1298
|
/>
|
|
1244
1299
|
)
|
|
1245
1300
|
const audios = Array.from(
|
|
1301
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1246
1302
|
screen.getByTestId('audio-attachment').querySelectorAll('audio')
|
|
1247
1303
|
)
|
|
1248
1304
|
expect(audios).toHaveLength(3)
|
|
@@ -1265,6 +1321,7 @@ describe('MessageAttachment lazy-loading defaults', () => {
|
|
|
1265
1321
|
/>
|
|
1266
1322
|
)
|
|
1267
1323
|
const audios = Array.from(
|
|
1324
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
1268
1325
|
screen.getByTestId('audio-attachment').querySelectorAll('audio')
|
|
1269
1326
|
)
|
|
1270
1327
|
expect(audios[0]?.getAttribute('preload')).toBe('none')
|
|
@@ -18,7 +18,9 @@ describe('Bubble', () => {
|
|
|
18
18
|
</Bubble>
|
|
19
19
|
)
|
|
20
20
|
const bubble = screen.getByTestId('bubble')
|
|
21
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
21
22
|
if (present) expect(bubble.querySelector('p')).not.toBeNull()
|
|
23
|
+
// eslint-disable-next-line testing-library/no-node-access -- media or rendered-markup internals have no user-facing query.
|
|
22
24
|
else expect(bubble.querySelector('p')).toBeNull()
|
|
23
25
|
expect(bubble).toHaveClass(present ? 'pb-0' : 'py-2')
|
|
24
26
|
})
|