@linktr.ee/messaging-react 4.4.3 → 4.4.4-rc-1788181757
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 +36 -3
- 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/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
|
@@ -89,32 +89,6 @@ describe('LinkAttachment.Received (link previews)', () => {
|
|
|
89
89
|
WebkitLineClamp: '3',
|
|
90
90
|
})
|
|
91
91
|
})
|
|
92
|
-
|
|
93
|
-
it('renders the filled (primary) CTA when cta.variant is omitted', async () => {
|
|
94
|
-
renderWithProviders(
|
|
95
|
-
<LinkAttachment.Received
|
|
96
|
-
title="Welcome"
|
|
97
|
-
description="Add a personal welcome message"
|
|
98
|
-
cta={{ label: 'Add now', onClick: vi.fn() }}
|
|
99
|
-
/>
|
|
100
|
-
)
|
|
101
|
-
const button = await screen.findByText('Add now')
|
|
102
|
-
expect(button.className).toContain('bg-[#121110]')
|
|
103
|
-
expect(button.className).toContain('text-white')
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('renders the subtle (secondary) CTA when cta.variant is "secondary"', async () => {
|
|
107
|
-
renderWithProviders(
|
|
108
|
-
<LinkAttachment.Received
|
|
109
|
-
title="Welcome"
|
|
110
|
-
description="Add a personal welcome message"
|
|
111
|
-
cta={{ label: 'Add now', onClick: vi.fn(), variant: 'secondary' }}
|
|
112
|
-
/>
|
|
113
|
-
)
|
|
114
|
-
const button = await screen.findByText('Add now')
|
|
115
|
-
expect(button.className).toContain('bg-black/[0.06]')
|
|
116
|
-
expect(button.className).toContain('text-[#121110]')
|
|
117
|
-
})
|
|
118
92
|
})
|
|
119
93
|
|
|
120
94
|
describe('LinkAttachment.Sent', () => {
|
|
@@ -178,9 +152,11 @@ describe('LinkAttachment.Sent', () => {
|
|
|
178
152
|
/>
|
|
179
153
|
)
|
|
180
154
|
const button = await screen.findByText('Add now')
|
|
155
|
+
/* eslint-disable local/no-style-only-assertion -- legibility, not decoration: white-on-white is the bug this guards. */
|
|
181
156
|
expect(button.className).toContain('bg-black/[0.06]')
|
|
182
157
|
expect(button.className).toContain('text-[#121110]')
|
|
183
158
|
expect(button.className).not.toContain('text-white')
|
|
159
|
+
/* eslint-enable local/no-style-only-assertion */
|
|
184
160
|
})
|
|
185
161
|
})
|
|
186
162
|
|
|
@@ -228,17 +204,21 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
228
204
|
// The pale report colour and the vivid one both paint, on both sides — the
|
|
229
205
|
// feature adds colour to every card with an image rather than half of them.
|
|
230
206
|
for (const accentColor of [ACCENT, PALE_ACCENT]) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
207
|
+
{
|
|
208
|
+
const view = renderWithProviders(
|
|
209
|
+
<LinkAttachment.Sent {...CARD_PROPS} accentColor={accentColor} />
|
|
210
|
+
)
|
|
211
|
+
expect(accentOf(await screen.findByRole('link'))).toBe(accentColor)
|
|
212
|
+
view.unmount()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
{
|
|
216
|
+
const view = renderWithProviders(
|
|
217
|
+
<LinkAttachment.Received {...CARD_PROPS} accentColor={accentColor} />
|
|
218
|
+
)
|
|
219
|
+
expect(accentOf(await screen.findByRole('link'))).toBe(accentColor)
|
|
220
|
+
view.unmount()
|
|
221
|
+
}
|
|
242
222
|
}
|
|
243
223
|
})
|
|
244
224
|
|
|
@@ -291,7 +271,7 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
291
271
|
// verifies the poster with a hidden probe `<img>`. Without that, this card
|
|
292
272
|
// stayed accent-painted over a bare `bg-black` box, which is exactly the
|
|
293
273
|
// claim-without-a-source the gate exists to prevent.
|
|
294
|
-
|
|
274
|
+
renderWithProviders(
|
|
295
275
|
<LinkAttachment.Sent
|
|
296
276
|
{...CARD_PROPS}
|
|
297
277
|
mimeType="video/mp4"
|
|
@@ -299,19 +279,19 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
299
279
|
accentColor={ACCENT}
|
|
300
280
|
/>
|
|
301
281
|
)
|
|
302
|
-
const shell =
|
|
303
|
-
'[class*="select-none"]'
|
|
304
|
-
) as HTMLElement
|
|
282
|
+
const shell = screen.getByTestId('link-card')
|
|
305
283
|
expect(shell.style.getPropertyValue('--accent')).toBe(ACCENT)
|
|
306
|
-
expect(
|
|
284
|
+
expect(screen.getByTestId('card-thumbnail-video')).toHaveAttribute('poster')
|
|
307
285
|
|
|
308
|
-
fireEvent.error(
|
|
286
|
+
fireEvent.error(screen.getByRole('img', { hidden: true }))
|
|
309
287
|
|
|
310
288
|
expect(shell.style.getPropertyValue('--accent')).toBe('')
|
|
311
289
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#FFFFFF' })
|
|
312
290
|
// The failed poster is dropped with the accent, so the player falls back to
|
|
313
291
|
// its own first frame instead of retrying a 404.
|
|
314
|
-
expect(
|
|
292
|
+
expect(screen.getByTestId('card-thumbnail-video')).not.toHaveAttribute(
|
|
293
|
+
'poster'
|
|
294
|
+
)
|
|
315
295
|
})
|
|
316
296
|
|
|
317
297
|
it('leaves a playable audio card on its flat fill, and inks that fill dark', async () => {
|
|
@@ -328,10 +308,10 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
328
308
|
sourceUrl: 'https://cdn.example.com/clip.mp3',
|
|
329
309
|
}
|
|
330
310
|
|
|
331
|
-
const {
|
|
311
|
+
const { unmount } = renderWithProviders(
|
|
332
312
|
<LinkAttachment.Sent {...CARD_PROPS} {...AUDIO} accentColor={ACCENT} />
|
|
333
313
|
)
|
|
334
|
-
const sent =
|
|
314
|
+
const sent = shellOf()
|
|
335
315
|
expect(sent.style.getPropertyValue('--accent')).toBe('')
|
|
336
316
|
expect(sent.className).toContain(AUDIO_BG_CLASS)
|
|
337
317
|
expect(screen.getByText(CARD_PROPS.title)).toHaveStyle({ color: '#040403' })
|
|
@@ -372,11 +352,11 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
372
352
|
// The extractor returns a bare hex often enough that dropping it would lose
|
|
373
353
|
// the colour silently — `oklch(from 7008E7 …)` never resolves, so the
|
|
374
354
|
// published `--accent` must carry the `#`.
|
|
375
|
-
const
|
|
355
|
+
const view = renderWithProviders(
|
|
376
356
|
<LinkAttachment.Sent {...CARD_PROPS} accentColor="7008E7" />
|
|
377
357
|
)
|
|
378
358
|
expect(accentOf(await screen.findByRole('link'))).toBe('#7008E7')
|
|
379
|
-
|
|
359
|
+
view.unmount()
|
|
380
360
|
|
|
381
361
|
renderWithProviders(
|
|
382
362
|
<LinkAttachment.Sent {...CARD_PROPS} accentColor={ACCENT} />
|
|
@@ -405,15 +385,17 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
|
|
|
405
385
|
onDismiss={() => {}}
|
|
406
386
|
/>,
|
|
407
387
|
]) {
|
|
408
|
-
const {
|
|
388
|
+
const { unmount } = renderWithProviders(card)
|
|
409
389
|
|
|
410
|
-
expect(
|
|
390
|
+
expect(screen.queryByTestId('card-thumbnail-audio')).toBeNull()
|
|
411
391
|
const busy = screen.getByLabelText('Loading link preview')
|
|
412
392
|
expect(busy).toHaveAttribute('aria-busy', 'true')
|
|
413
393
|
// The skeleton keeps its 250px box: the audio shell is intrinsic-height,
|
|
414
394
|
// and a `flex-1` hero inside it would collapse to zero.
|
|
395
|
+
// eslint-disable-next-line local/no-style-only-assertion -- MES-1349: the reserved box is the contract; jsdom has no layout to observe it.
|
|
415
396
|
expect(busy.className).toContain('h-[250px]')
|
|
416
|
-
expect(
|
|
397
|
+
expect(screen.getByTestId('card-thumbnail-skeleton')).toBeInTheDocument()
|
|
398
|
+
expect(screen.getAllByTestId('card-body-skeleton-bar')).toHaveLength(2)
|
|
417
399
|
|
|
418
400
|
unmount()
|
|
419
401
|
}
|
|
@@ -494,15 +476,13 @@ const VIDEO = {
|
|
|
494
476
|
}
|
|
495
477
|
const HERO = 'https://cdn.example.com/yoga.jpg'
|
|
496
478
|
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
const shellOf = (container: HTMLElement) =>
|
|
500
|
-
container.querySelector('.messaging-link-card') as HTMLElement
|
|
479
|
+
// Received cards carry their own hook; Sent/Composer fall back to the shell default.
|
|
480
|
+
const shellOf = () => screen.getByTestId(/^link-(card|attachment)$/)
|
|
501
481
|
|
|
502
482
|
describe('LinkAttachment.Composer chrome', () => {
|
|
503
483
|
it('anchors the dismiss control to the hero on a featured card', () => {
|
|
504
484
|
const onDismiss = vi.fn()
|
|
505
|
-
|
|
485
|
+
renderWithProviders(
|
|
506
486
|
<LinkAttachment.Composer
|
|
507
487
|
title="Brie’s FAQ"
|
|
508
488
|
thumbnailUrl={HERO}
|
|
@@ -510,9 +490,8 @@ describe('LinkAttachment.Composer chrome', () => {
|
|
|
510
490
|
/>
|
|
511
491
|
)
|
|
512
492
|
const dismiss = screen.getByLabelText('Dismiss attachment')
|
|
513
|
-
expect(dismiss.parentElement?.className).toContain('absolute right-3 top-3')
|
|
514
493
|
// Sits inside the hero region, not the shell corner.
|
|
515
|
-
expect(
|
|
494
|
+
expect(screen.getByTestId('card-thumbnail-hero')).toContainElement(dismiss)
|
|
516
495
|
// 44x44 touch target without growing the 24px circle.
|
|
517
496
|
expect(dismiss.className).toContain('before:-inset-[10px]')
|
|
518
497
|
// Hover lifts the fill so the control is not identical to its rest state.
|
|
@@ -522,7 +501,7 @@ describe('LinkAttachment.Composer chrome', () => {
|
|
|
522
501
|
})
|
|
523
502
|
|
|
524
503
|
it('moves the dismiss control to the shell corner on a classic card', () => {
|
|
525
|
-
|
|
504
|
+
renderWithProviders(
|
|
526
505
|
<LinkAttachment.Composer
|
|
527
506
|
title="Brie’s FAQ"
|
|
528
507
|
thumbnailUrl={HERO}
|
|
@@ -530,26 +509,31 @@ describe('LinkAttachment.Composer chrome', () => {
|
|
|
530
509
|
onDismiss={() => {}}
|
|
531
510
|
/>
|
|
532
511
|
)
|
|
533
|
-
expect(
|
|
534
|
-
const shell = shellOf(
|
|
512
|
+
expect(screen.queryByRole('img', { hidden: true })).toBeNull()
|
|
513
|
+
const shell = shellOf()
|
|
535
514
|
expect(shell.contains(screen.getByLabelText('Dismiss attachment'))).toBe(
|
|
536
515
|
true
|
|
537
516
|
)
|
|
538
517
|
// Classic cards size to their content.
|
|
518
|
+
// eslint-disable-next-line local/no-style-only-assertion -- MES-1349: the reserved box is the contract; jsdom has no layout to observe it.
|
|
539
519
|
expect(shell.className).not.toContain('h-[250px]')
|
|
540
520
|
expect(shell.className).toContain('block')
|
|
541
521
|
})
|
|
542
522
|
|
|
543
523
|
it('defaults to the featured layout with its hero and fixed box', () => {
|
|
544
|
-
|
|
524
|
+
renderWithProviders(
|
|
545
525
|
<LinkAttachment.Composer title="Brie’s FAQ" thumbnailUrl={HERO} />
|
|
546
526
|
)
|
|
547
|
-
expect(
|
|
548
|
-
|
|
527
|
+
expect(screen.getByRole('img', { hidden: true })).toHaveAttribute(
|
|
528
|
+
'src',
|
|
529
|
+
HERO
|
|
530
|
+
)
|
|
531
|
+
// eslint-disable-next-line local/no-style-only-assertion -- MES-1349: the reserved box is the contract; jsdom has no layout to observe it.
|
|
532
|
+
expect(shellOf().className).toContain('h-[250px]')
|
|
549
533
|
})
|
|
550
534
|
|
|
551
535
|
it('collapses an audio composer card to the player plus an inline dismiss', () => {
|
|
552
|
-
|
|
536
|
+
renderWithProviders(
|
|
553
537
|
<LinkAttachment.Composer
|
|
554
538
|
title="My track"
|
|
555
539
|
url="tr.ee/briemix"
|
|
@@ -557,15 +541,14 @@ describe('LinkAttachment.Composer chrome', () => {
|
|
|
557
541
|
onDismiss={() => {}}
|
|
558
542
|
/>
|
|
559
543
|
)
|
|
560
|
-
const shell = shellOf(
|
|
544
|
+
const shell = shellOf()
|
|
561
545
|
expect(shell.className).toContain(AUDIO_BG_CLASS)
|
|
562
|
-
expect(
|
|
546
|
+
expect(screen.getByTestId('card-thumbnail-audio')).toBeInTheDocument()
|
|
563
547
|
// No chin on this path, so the URL line goes with it.
|
|
564
548
|
expect(screen.queryByTestId('link-attachment-url')).toBeNull()
|
|
565
549
|
const dismiss = screen.getByLabelText('Dismiss attachment')
|
|
566
550
|
// Inline sibling of the native controls: no overlay halo on top of them.
|
|
567
551
|
expect(dismiss.className).not.toContain('before:-inset-[10px]')
|
|
568
|
-
expect(dismiss.parentElement?.className).not.toContain('absolute')
|
|
569
552
|
})
|
|
570
553
|
|
|
571
554
|
it('lets the consumer name the dismiss control', () => {
|
|
@@ -661,10 +644,10 @@ describe('LinkAttachment interactivity contract', () => {
|
|
|
661
644
|
|
|
662
645
|
it('ignores the Sent onClick when there is nothing to open', () => {
|
|
663
646
|
const onClick = vi.fn()
|
|
664
|
-
|
|
647
|
+
renderWithProviders(
|
|
665
648
|
<LinkAttachment.Sent title="My Playlist" onClick={onClick} />
|
|
666
649
|
)
|
|
667
|
-
const shell = shellOf(
|
|
650
|
+
const shell = shellOf()
|
|
668
651
|
expect(shell.tagName).toBe('DIV')
|
|
669
652
|
fireEvent.click(shell)
|
|
670
653
|
expect(onClick).not.toHaveBeenCalled()
|
|
@@ -672,7 +655,7 @@ describe('LinkAttachment interactivity contract', () => {
|
|
|
672
655
|
|
|
673
656
|
it('ignores the Sent onClick around playable media', () => {
|
|
674
657
|
const onClick = vi.fn()
|
|
675
|
-
|
|
658
|
+
renderWithProviders(
|
|
676
659
|
<LinkAttachment.Sent
|
|
677
660
|
title="Clip"
|
|
678
661
|
url="tr.ee/briemix"
|
|
@@ -681,7 +664,7 @@ describe('LinkAttachment interactivity contract', () => {
|
|
|
681
664
|
onClick={onClick}
|
|
682
665
|
/>
|
|
683
666
|
)
|
|
684
|
-
const shell = shellOf(
|
|
667
|
+
const shell = shellOf()
|
|
685
668
|
expect(shell.tagName).toBe('DIV')
|
|
686
669
|
fireEvent.click(shell)
|
|
687
670
|
expect(onClick).not.toHaveBeenCalled()
|
|
@@ -770,7 +753,7 @@ describe('LinkAttachment layout and surface', () => {
|
|
|
770
753
|
] as const)(
|
|
771
754
|
'drops the %s hero and the fixed box for classic',
|
|
772
755
|
(_side, Card) => {
|
|
773
|
-
|
|
756
|
+
renderWithProviders(
|
|
774
757
|
<Card
|
|
775
758
|
title="Brie’s FAQ"
|
|
776
759
|
url="tr.ee/briemix"
|
|
@@ -778,8 +761,9 @@ describe('LinkAttachment layout and surface', () => {
|
|
|
778
761
|
layout="classic"
|
|
779
762
|
/>
|
|
780
763
|
)
|
|
781
|
-
expect(
|
|
782
|
-
const shell = shellOf(
|
|
764
|
+
expect(screen.queryByRole('img', { hidden: true })).toBeNull()
|
|
765
|
+
const shell = shellOf()
|
|
766
|
+
// eslint-disable-next-line local/no-style-only-assertion -- MES-1349: the reserved box is the contract; jsdom has no layout to observe it.
|
|
783
767
|
expect(shell.className).not.toContain('h-[250px]')
|
|
784
768
|
expect(shell.className).toContain('block')
|
|
785
769
|
}
|
|
@@ -789,11 +773,15 @@ describe('LinkAttachment layout and surface', () => {
|
|
|
789
773
|
['Sent', LinkAttachment.Sent],
|
|
790
774
|
['Received', LinkAttachment.Received],
|
|
791
775
|
] as const)('locks the featured %s card to the 250px box', (_side, Card) => {
|
|
792
|
-
|
|
776
|
+
renderWithProviders(
|
|
793
777
|
<Card title="Brie’s FAQ" url="tr.ee/briemix" thumbnailUrl={HERO} />
|
|
794
778
|
)
|
|
795
|
-
expect(
|
|
796
|
-
|
|
779
|
+
expect(screen.getByRole('img', { hidden: true })).toHaveAttribute(
|
|
780
|
+
'src',
|
|
781
|
+
HERO
|
|
782
|
+
)
|
|
783
|
+
// eslint-disable-next-line local/no-style-only-assertion -- MES-1349: the reserved box is the contract; jsdom has no layout to observe it.
|
|
784
|
+
expect(shellOf().className).toContain('h-[250px]')
|
|
797
785
|
})
|
|
798
786
|
|
|
799
787
|
it.each([
|
|
@@ -802,13 +790,14 @@ describe('LinkAttachment layout and surface', () => {
|
|
|
802
790
|
] as const)(
|
|
803
791
|
'sizes the %s audio card to its content on the audio surface',
|
|
804
792
|
(_side, Card) => {
|
|
805
|
-
|
|
793
|
+
renderWithProviders(
|
|
806
794
|
<Card title="My track" url="tr.ee/briemix" {...AUDIO} />
|
|
807
795
|
)
|
|
808
|
-
const shell = shellOf(
|
|
796
|
+
const shell = shellOf()
|
|
809
797
|
expect(shell.className).toContain(AUDIO_BG_CLASS)
|
|
798
|
+
// eslint-disable-next-line local/no-style-only-assertion -- MES-1349: the reserved box is the contract; jsdom has no layout to observe it.
|
|
810
799
|
expect(shell.className).not.toContain('h-[250px]')
|
|
811
|
-
expect(
|
|
800
|
+
expect(screen.getByTestId('card-thumbnail-audio')).toBeInTheDocument()
|
|
812
801
|
}
|
|
813
802
|
)
|
|
814
803
|
})
|
|
@@ -23,6 +23,7 @@ describe('BubbleTail', () => {
|
|
|
23
23
|
</BubbleTail>
|
|
24
24
|
)
|
|
25
25
|
const tail = screen.getByTestId('link-card-tail')
|
|
26
|
+
// eslint-disable-next-line local/no-style-only-assertion -- the tail is absolutely positioned: without `relative` here it anchors to some other ancestor.
|
|
26
27
|
expect(tail.parentElement?.className).toBe('relative isolate w-fit')
|
|
27
28
|
expect(tail.getAttribute('aria-hidden')).toBe('true')
|
|
28
29
|
// The tail follows the card so it paints over the card's bottom edge.
|
|
@@ -47,37 +47,6 @@ describe('CardBody', () => {
|
|
|
47
47
|
expect(container).toBeEmptyDOMElement()
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
it('dims the placeholder title but not a real one', () => {
|
|
51
|
-
renderWithProviders(
|
|
52
|
-
<CardBody variant="dark" placeholderTitle="Add a title" />
|
|
53
|
-
)
|
|
54
|
-
expect(screen.getByText('Add a title').className).toContain('text-white/30')
|
|
55
|
-
|
|
56
|
-
renderWithProviders(<CardBody variant="dark" title="Brie’s FAQ" />)
|
|
57
|
-
const title = screen.getByText('Brie’s FAQ')
|
|
58
|
-
expect(title.className).toContain('text-white')
|
|
59
|
-
expect(title.className).not.toContain('text-white/30')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('inks the light variant against its pale surface', () => {
|
|
63
|
-
renderWithProviders(
|
|
64
|
-
<CardBody variant="light" title="Brie’s FAQ" url="tr.ee/a" />
|
|
65
|
-
)
|
|
66
|
-
expect(screen.getByText('Brie’s FAQ').className).toContain('text-black/90')
|
|
67
|
-
expect(screen.getByTestId('link-attachment-url').className).toContain(
|
|
68
|
-
'text-black/55'
|
|
69
|
-
)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('inks the dark variant against its dark surface', () => {
|
|
73
|
-
renderWithProviders(
|
|
74
|
-
<CardBody variant="dark" title="Brie’s FAQ" url="tr.ee/a" />
|
|
75
|
-
)
|
|
76
|
-
expect(screen.getByTestId('link-attachment-url').className).toContain(
|
|
77
|
-
'text-white/55'
|
|
78
|
-
)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
50
|
it('replaces the variant ink with chinTextColor on every chin line', () => {
|
|
82
51
|
renderWithProviders(
|
|
83
52
|
<CardBody
|
|
@@ -94,7 +63,6 @@ describe('CardBody', () => {
|
|
|
94
63
|
|
|
95
64
|
for (const node of [title, description, url]) {
|
|
96
65
|
expect(node.style.color).toBe('rgb(4, 4, 3)')
|
|
97
|
-
expect(node.className).not.toContain('text-white')
|
|
98
66
|
}
|
|
99
67
|
})
|
|
100
68
|
|
|
@@ -135,21 +103,24 @@ describe('CardBody', () => {
|
|
|
135
103
|
})
|
|
136
104
|
|
|
137
105
|
it('renders the trimmed url footer, and drops it entirely for a cta', () => {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
106
|
+
{
|
|
107
|
+
const view = renderWithProviders(
|
|
108
|
+
<CardBody variant="light" url=" tr.ee/briemix " />
|
|
109
|
+
)
|
|
110
|
+
expect(screen.getByTestId('link-attachment-url').textContent).toBe(
|
|
111
|
+
'tr.ee/briemix'
|
|
112
|
+
)
|
|
113
|
+
view.unmount()
|
|
114
|
+
}
|
|
142
115
|
|
|
143
|
-
|
|
116
|
+
renderWithProviders(
|
|
144
117
|
<CardBody
|
|
145
118
|
variant="light"
|
|
146
119
|
url="tr.ee/briemix"
|
|
147
120
|
cta={{ label: 'View FAQs' }}
|
|
148
121
|
/>
|
|
149
122
|
)
|
|
150
|
-
expect(
|
|
151
|
-
container.querySelector('[data-testid="link-attachment-url"]')
|
|
152
|
-
).toBeNull()
|
|
123
|
+
expect(screen.queryByTestId('link-attachment-url')).toBeNull()
|
|
153
124
|
expect(screen.getByText('View FAQs')).toBeInTheDocument()
|
|
154
125
|
})
|
|
155
126
|
|
|
@@ -162,12 +133,14 @@ describe('CardBody', () => {
|
|
|
162
133
|
/>
|
|
163
134
|
)
|
|
164
135
|
// The audio strip is a light surface under a nominally dark card.
|
|
136
|
+
/* eslint-disable local/no-style-only-assertion -- legibility, not decoration: the wrong surface here is white-on-white. */
|
|
165
137
|
expect(screen.getByText('Play').className).toContain('bg-black/[0.06]')
|
|
166
138
|
|
|
167
139
|
renderWithProviders(
|
|
168
140
|
<CardBody variant="dark" cta={{ label: 'View FAQs' }} />
|
|
169
141
|
)
|
|
170
142
|
expect(screen.getByText('View FAQs').className).toContain('bg-white')
|
|
143
|
+
/* eslint-enable local/no-style-only-assertion */
|
|
171
144
|
})
|
|
172
145
|
|
|
173
146
|
it('renders the app icon before the title and the trailing action beside it', () => {
|
|
@@ -181,13 +154,10 @@ describe('CardBody', () => {
|
|
|
181
154
|
)
|
|
182
155
|
const icon = screen.getByTestId('app-icon')
|
|
183
156
|
const title = screen.getByText('Spotify')
|
|
184
|
-
expect(icon.parentElement?.className).toContain('shrink-0')
|
|
185
157
|
expect(
|
|
186
158
|
icon.compareDocumentPosition(title) & Node.DOCUMENT_POSITION_FOLLOWING
|
|
187
159
|
).toBeTruthy()
|
|
188
|
-
expect(screen.getByText('Edit')
|
|
189
|
-
'shrink-0'
|
|
190
|
-
)
|
|
160
|
+
expect(screen.getByText('Edit')).toBeInTheDocument()
|
|
191
161
|
})
|
|
192
162
|
|
|
193
163
|
it('omits the title row entirely when there is no title', () => {
|
|
@@ -201,28 +171,17 @@ describe('CardBody', () => {
|
|
|
201
171
|
expect(screen.queryByTestId('app-icon')).not.toBeInTheDocument()
|
|
202
172
|
})
|
|
203
173
|
|
|
204
|
-
it('renders two
|
|
205
|
-
|
|
174
|
+
it('renders two skeleton bars instead of text while loading', () => {
|
|
175
|
+
renderWithProviders(
|
|
206
176
|
<CardBody variant="dark" title="Brie’s FAQ" url="tr.ee/a" isLoading />
|
|
207
177
|
)
|
|
208
178
|
expect(screen.queryByText('Brie’s FAQ')).not.toBeInTheDocument()
|
|
209
179
|
expect(screen.queryByTestId('link-attachment-url')).not.toBeInTheDocument()
|
|
210
180
|
|
|
211
|
-
const
|
|
212
|
-
expect(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
for (const bar of darkBars) {
|
|
216
|
-
expect(bar.className).toContain('rounded')
|
|
217
|
-
expect(bar.className).toContain('bg-white/25')
|
|
218
|
-
expect(bar.getAttribute('aria-hidden')).toBe('true')
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const { container: light } = renderWithProviders(
|
|
222
|
-
<CardBody variant="light" isLoading />
|
|
223
|
-
)
|
|
224
|
-
for (const bar of light.querySelectorAll('div.animate-pulse')) {
|
|
225
|
-
expect(bar.className).toContain('bg-black/15')
|
|
181
|
+
const bars = screen.getAllByTestId('card-body-skeleton-bar')
|
|
182
|
+
expect(bars).toHaveLength(2)
|
|
183
|
+
for (const bar of bars) {
|
|
184
|
+
expect(bar).toHaveAttribute('aria-hidden', 'true')
|
|
226
185
|
}
|
|
227
186
|
})
|
|
228
187
|
|
|
@@ -141,8 +141,16 @@ const CardBody: React.FC<CardBodyProps> = ({
|
|
|
141
141
|
<div className="shrink-0 px-4 py-3">
|
|
142
142
|
<div className="flex min-w-0 flex-col gap-[2px]">
|
|
143
143
|
{/* Sized to the lines they stand in for: 14/20 title, 12/16 URL. */}
|
|
144
|
-
<div
|
|
145
|
-
|
|
144
|
+
<div
|
|
145
|
+
data-testid="card-body-skeleton-bar"
|
|
146
|
+
className={classNames('h-5 w-3/4', barClass)}
|
|
147
|
+
aria-hidden
|
|
148
|
+
/>
|
|
149
|
+
<div
|
|
150
|
+
data-testid="card-body-skeleton-bar"
|
|
151
|
+
className={classNames('h-4 w-1/2', barClass)}
|
|
152
|
+
aria-hidden
|
|
153
|
+
/>
|
|
146
154
|
</div>
|
|
147
155
|
</div>
|
|
148
156
|
)
|
|
@@ -16,7 +16,6 @@ describe('CardCta', () => {
|
|
|
16
16
|
expect(cta.getAttribute('href')).toBe('https://tr.ee/faq')
|
|
17
17
|
expect(cta.getAttribute('target')).toBe('_blank')
|
|
18
18
|
expect(cta.getAttribute('rel')).toBe('noopener noreferrer')
|
|
19
|
-
expect(cta.className).toContain('no-underline')
|
|
20
19
|
})
|
|
21
20
|
|
|
22
21
|
it('renders a button when the href is missing or unsafe', () => {
|
|
@@ -86,17 +85,9 @@ describe('CardCta', () => {
|
|
|
86
85
|
}
|
|
87
86
|
)
|
|
88
87
|
|
|
89
|
-
it('
|
|
88
|
+
it('renders the label verbatim, without a wrapper span, by default', () => {
|
|
90
89
|
renderWithProviders(<CardCta variant="light" cta={{ label: 'Act' }} />)
|
|
91
90
|
const cta = screen.getByText('Act')
|
|
92
|
-
expect(cta.className).toContain('bg-[#121110] text-white')
|
|
93
|
-
expect(cta.className).not.toContain('bg-black/[0.06]')
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('spans its row by default and renders the label verbatim', () => {
|
|
97
|
-
renderWithProviders(<CardCta variant="light" cta={{ label: 'Act' }} />)
|
|
98
|
-
const cta = screen.getByText('Act')
|
|
99
|
-
expect(cta.className).toContain('w-full')
|
|
100
91
|
expect(cta.querySelector('span')).toBeNull()
|
|
101
92
|
})
|
|
102
93
|
|
|
@@ -108,16 +99,5 @@ describe('CardCta', () => {
|
|
|
108
99
|
expect(label.tagName).toBe('SPAN')
|
|
109
100
|
expect(label.className).toContain('min-w-0 truncate')
|
|
110
101
|
expect(label.parentElement?.className).toContain('min-w-0')
|
|
111
|
-
expect(label.parentElement?.className).not.toContain('w-full')
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('offsets itself below the description unless the caller owns spacing', () => {
|
|
115
|
-
renderWithProviders(<CardCta variant="light" cta={{ label: 'Spaced' }} />)
|
|
116
|
-
expect(screen.getByText('Spaced').className).toContain('mt-2')
|
|
117
|
-
|
|
118
|
-
renderWithProviders(
|
|
119
|
-
<CardCta variant="light" cta={{ label: 'Standalone' }} standalone />
|
|
120
|
-
)
|
|
121
|
-
expect(screen.getByText('Standalone').className).not.toContain('mt-2')
|
|
122
102
|
})
|
|
123
103
|
})
|
|
@@ -16,7 +16,6 @@ describe('CardShell', () => {
|
|
|
16
16
|
)
|
|
17
17
|
const shell = shellOf()
|
|
18
18
|
expect(shell.tagName).toBe('DIV')
|
|
19
|
-
expect(shell.className).not.toContain('cursor-pointer')
|
|
20
19
|
expect(shell.className).not.toContain('focus-ring')
|
|
21
20
|
expect(shell.getAttribute('data-variant')).toBe('light')
|
|
22
21
|
})
|
|
@@ -38,8 +37,6 @@ describe('CardShell', () => {
|
|
|
38
37
|
expect(shell.getAttribute('href')).toBe('https://tr.ee/briemix')
|
|
39
38
|
expect(shell.getAttribute('target')).toBe('_blank')
|
|
40
39
|
expect(shell.getAttribute('rel')).toBe('noopener noreferrer')
|
|
41
|
-
expect(shell.className).toContain('cursor-pointer')
|
|
42
|
-
expect(shell.className).toContain('no-underline')
|
|
43
40
|
expect(shell.className).toContain('focus-ring')
|
|
44
41
|
fireEvent.click(shell)
|
|
45
42
|
expect(onClick).toHaveBeenCalledTimes(1)
|
|
@@ -61,39 +58,10 @@ describe('CardShell', () => {
|
|
|
61
58
|
expect(shell.tagName).toBe('BUTTON')
|
|
62
59
|
expect(shell.getAttribute('type')).toBe('button')
|
|
63
60
|
expect(shell.getAttribute('aria-label')).toBe('Open attachment preview')
|
|
64
|
-
// The button's own centring is overridden so the chin still reads left-aligned.
|
|
65
|
-
expect(shell.className).toContain('text-left')
|
|
66
61
|
fireEvent.click(shell)
|
|
67
62
|
expect(onClick).toHaveBeenCalledTimes(1)
|
|
68
63
|
})
|
|
69
64
|
|
|
70
|
-
it('paints the variant bubble fill and lets bgClassName override it', () => {
|
|
71
|
-
const { unmount } = renderWithProviders(
|
|
72
|
-
<CardShell variant="dark" data-testid="shell">
|
|
73
|
-
body
|
|
74
|
-
</CardShell>
|
|
75
|
-
)
|
|
76
|
-
expect(shellOf().className).toContain('bg-[#1e2330]')
|
|
77
|
-
expect(shellOf().className).toContain('rounded-[20px]')
|
|
78
|
-
unmount()
|
|
79
|
-
|
|
80
|
-
renderWithProviders(
|
|
81
|
-
<CardShell variant="light" data-testid="light">
|
|
82
|
-
body
|
|
83
|
-
</CardShell>
|
|
84
|
-
)
|
|
85
|
-
expect(shellOf('light').className).toContain('bg-[#f1f0ee]')
|
|
86
|
-
|
|
87
|
-
renderWithProviders(
|
|
88
|
-
<CardShell variant="dark" bgClassName="bg-[#F2F3F4]" data-testid="audio">
|
|
89
|
-
body
|
|
90
|
-
</CardShell>
|
|
91
|
-
)
|
|
92
|
-
const audio = shellOf('audio')
|
|
93
|
-
expect(audio.className).toContain('bg-[#F2F3F4]')
|
|
94
|
-
expect(audio.className).not.toContain('bg-[#1e2330]')
|
|
95
|
-
})
|
|
96
|
-
|
|
97
65
|
it('locks to the 250px column only when fixedHeight is set', () => {
|
|
98
66
|
renderWithProviders(
|
|
99
67
|
<CardShell variant="dark" fixedHeight data-testid="fixed">
|
|
@@ -109,6 +77,7 @@ describe('CardShell', () => {
|
|
|
109
77
|
</CardShell>
|
|
110
78
|
)
|
|
111
79
|
expect(shellOf('intrinsic').className).toContain('block')
|
|
80
|
+
// eslint-disable-next-line local/no-style-only-assertion -- MES-1349: the reserved box is the contract; jsdom has no layout to observe it.
|
|
112
81
|
expect(shellOf('intrinsic').className).not.toContain('h-[250px]')
|
|
113
82
|
})
|
|
114
83
|
|
|
@@ -163,18 +132,23 @@ describe('CardShell', () => {
|
|
|
163
132
|
body
|
|
164
133
|
</CardShell>
|
|
165
134
|
)
|
|
166
|
-
const corner = screen.
|
|
167
|
-
expect(corner
|
|
168
|
-
|
|
135
|
+
const corner = screen.getByTestId('card-shell-top-right')
|
|
136
|
+
expect(corner).toContainElement(
|
|
137
|
+
screen.getByRole('button', { name: 'Dismiss' })
|
|
138
|
+
)
|
|
139
|
+
/* eslint-disable local/no-style-only-assertion -- an overlay control: in flow it would displace the hero, and without the stacking it stops being clickable. */
|
|
140
|
+
expect(corner.className).toContain('absolute right-3 top-3')
|
|
141
|
+
expect(corner.className).toContain('z-10')
|
|
142
|
+
/* eslint-enable local/no-style-only-assertion */
|
|
169
143
|
})
|
|
170
144
|
|
|
171
145
|
it('omits the corner wrapper when no topRight is provided', () => {
|
|
172
|
-
|
|
146
|
+
renderWithProviders(
|
|
173
147
|
<CardShell variant="dark" data-testid="shell">
|
|
174
148
|
body
|
|
175
149
|
</CardShell>
|
|
176
150
|
)
|
|
177
|
-
expect(
|
|
151
|
+
expect(screen.queryByTestId('card-shell-top-right')).not.toBeInTheDocument()
|
|
178
152
|
})
|
|
179
153
|
|
|
180
154
|
it.each([
|