@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.
Files changed (58) hide show
  1. package/dist/AttachmentCard-BbsjzdLJ.cjs +2 -0
  2. package/dist/AttachmentCard-BbsjzdLJ.cjs.map +1 -0
  3. package/dist/{AttachmentCard-BqwWgqvi.js → AttachmentCard-DSekwIOA.js} +35 -20
  4. package/dist/AttachmentCard-DSekwIOA.js.map +1 -0
  5. package/dist/{Card-BIbqWagz.cjs → Card-BS6Fza2a.cjs} +2 -2
  6. package/dist/{Card-BIbqWagz.cjs.map → Card-BS6Fza2a.cjs.map} +1 -1
  7. package/dist/{Card-f51K6V7Y.js → Card-BhwBkgxI.js} +2 -2
  8. package/dist/{Card-f51K6V7Y.js.map → Card-BhwBkgxI.js.map} +1 -1
  9. package/dist/index.cjs +2 -2
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.js +36 -3
  12. package/dist/index.js.map +1 -1
  13. package/package.json +3 -3
  14. package/src/components/AttachmentCard/MediaPlayer.test.tsx +62 -85
  15. package/src/components/AttachmentCard/MediaPlayer.tsx +14 -2
  16. package/src/components/AttachmentCard/Thumbnail.test.tsx +37 -46
  17. package/src/components/AttachmentCard/index.test.tsx +18 -46
  18. package/src/components/AttachmentCard/index.tsx +8 -2
  19. package/src/components/AttachmentCard/utils/icons.ts +11 -1
  20. package/src/components/Avatar/Avatar.test.tsx +37 -64
  21. package/src/components/Avatar/index.tsx +8 -1
  22. package/src/components/ChannelList/CustomChannelPreview.test.tsx +19 -19
  23. package/src/components/ChannelList/CustomChannelPreview.tsx +8 -2
  24. package/src/components/ChannelView.outbound-resolution.integration.test.tsx +25 -30
  25. package/src/components/ChannelView.test.tsx +17 -31
  26. package/src/components/ChannelView.tsx +1 -0
  27. package/src/components/CustomLinkPreviewList/CustomLinkPreviewList.test.tsx +8 -5
  28. package/src/components/CustomLinkPreviewList/index.tsx +4 -1
  29. package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +26 -21
  30. package/src/components/CustomMessage/StreamAttachmentMessage.tsx +2 -0
  31. package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +43 -56
  32. package/src/components/CustomMessageInput/index.tsx +25 -5
  33. package/src/components/CustomSystemMessage/CustomSystemMessage.test.tsx +17 -23
  34. package/src/components/FaqList/FaqList.test.tsx +5 -4
  35. package/src/components/FaqList/index.tsx +6 -1
  36. package/src/components/LinkAttachment/LinkAttachment.test.tsx +70 -81
  37. package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +1 -0
  38. package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +20 -61
  39. package/src/components/LinkAttachment/components/_shared/CardBody.tsx +10 -2
  40. package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +1 -21
  41. package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +11 -37
  42. package/src/components/LinkAttachment/components/_shared/CardShell.tsx +5 -2
  43. package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +65 -72
  44. package/src/components/LinkAttachment/components/_shared/CardThumbnail.tsx +13 -2
  45. package/src/components/MessageAttachment/MessageAttachment.test.tsx +25 -8
  46. package/src/components/MessageAttachment/_shared/CompactDocumentRow.test.tsx +5 -19
  47. package/src/components/MessageAttachment/_shared/MediaStackGrid.test.tsx +7 -9
  48. package/src/components/MessageAttachment/_shared/VideoViewer.test.tsx +9 -2
  49. package/src/components/MessageAttachment/_shared/VideoViewer.tsx +1 -0
  50. package/src/components/MessageAttachment/_shared/ViewerShell.test.tsx +2 -2
  51. package/src/components/MessageBubble/MessageBubble.test.tsx +0 -1
  52. package/src/components/RichCard/RichCard.test.tsx +24 -54
  53. package/src/components/RichCard/RichCardBody.tsx +4 -1
  54. package/src/logging/index.test.tsx +1 -0
  55. package/src/providers/MessagingProvider.test.tsx +37 -41
  56. package/dist/AttachmentCard-BqwWgqvi.js.map +0 -1
  57. package/dist/AttachmentCard-DUrgGRsq.cjs +0 -2
  58. 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
- const sent = renderWithProviders(
232
- <LinkAttachment.Sent {...CARD_PROPS} accentColor={accentColor} />
233
- )
234
- expect(accentOf(await screen.findByRole('link'))).toBe(accentColor)
235
- sent.unmount()
236
-
237
- const received = renderWithProviders(
238
- <LinkAttachment.Received {...CARD_PROPS} accentColor={accentColor} />
239
- )
240
- expect(accentOf(await screen.findByRole('link'))).toBe(accentColor)
241
- received.unmount()
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
- const { container } = renderWithProviders(
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 = container.querySelector(
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(container.querySelector('video')).toHaveAttribute('poster')
284
+ expect(screen.getByTestId('card-thumbnail-video')).toHaveAttribute('poster')
307
285
 
308
- fireEvent.error(container.querySelector('img') as HTMLImageElement)
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(container.querySelector('video')).not.toHaveAttribute('poster')
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 { container, unmount } = renderWithProviders(
311
+ const { unmount } = renderWithProviders(
332
312
  <LinkAttachment.Sent {...CARD_PROPS} {...AUDIO} accentColor={ACCENT} />
333
313
  )
334
- const sent = container.firstElementChild as HTMLElement
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 hashless = renderWithProviders(
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
- hashless.unmount()
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 { container, unmount } = renderWithProviders(card)
388
+ const { unmount } = renderWithProviders(card)
409
389
 
410
- expect(container.querySelector('audio')).toBeNull()
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(container.querySelectorAll('.animate-pulse')).toHaveLength(3)
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
- // Sent / Composer cards carry no test hook of their own; the shell is the
498
- // element that owns the card chrome classes.
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
- const { container } = renderWithProviders(
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(container.querySelector('div.flex-1')?.contains(dismiss)).toBe(true)
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
- const { container } = renderWithProviders(
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(container.querySelector('img')).toBeNull()
534
- const shell = shellOf(container)
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
- const { container } = renderWithProviders(
524
+ renderWithProviders(
545
525
  <LinkAttachment.Composer title="Brie’s FAQ" thumbnailUrl={HERO} />
546
526
  )
547
- expect(container.querySelector('img')?.getAttribute('src')).toBe(HERO)
548
- expect(shellOf(container).className).toContain('h-[250px]')
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
- const { container } = renderWithProviders(
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(container)
544
+ const shell = shellOf()
561
545
  expect(shell.className).toContain(AUDIO_BG_CLASS)
562
- expect(container.querySelector('audio')).not.toBeNull()
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
- const { container } = renderWithProviders(
647
+ renderWithProviders(
665
648
  <LinkAttachment.Sent title="My Playlist" onClick={onClick} />
666
649
  )
667
- const shell = shellOf(container)
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
- const { container } = renderWithProviders(
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(container)
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
- const { container } = renderWithProviders(
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(container.querySelector('img')).toBeNull()
782
- const shell = shellOf(container)
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
- const { container } = renderWithProviders(
776
+ renderWithProviders(
793
777
  <Card title="Brie’s FAQ" url="tr.ee/briemix" thumbnailUrl={HERO} />
794
778
  )
795
- expect(container.querySelector('img')?.getAttribute('src')).toBe(HERO)
796
- expect(shellOf(container).className).toContain('h-[250px]')
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
- const { container } = renderWithProviders(
793
+ renderWithProviders(
806
794
  <Card title="My track" url="tr.ee/briemix" {...AUDIO} />
807
795
  )
808
- const shell = shellOf(container)
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(container.querySelector('audio')).not.toBeNull()
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
- renderWithProviders(<CardBody variant="light" url=" tr.ee/briemix " />)
139
- expect(screen.getByTestId('link-attachment-url').textContent).toBe(
140
- 'tr.ee/briemix'
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
- const { container } = renderWithProviders(
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').parentElement?.className).toContain(
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 variant-aware skeleton bars instead of text while loading', () => {
205
- const { container: dark } = renderWithProviders(
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 darkBars = dark.querySelectorAll('div.animate-pulse')
212
- expect(darkBars).toHaveLength(2)
213
- expect(darkBars[0].className).toContain('h-5 w-3/4')
214
- expect(darkBars[1].className).toContain('h-4 w-1/2')
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 className={classNames('h-5 w-3/4', barClass)} aria-hidden />
145
- <div className={classNames('h-4 w-1/2', barClass)} aria-hidden />
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('defaults to the filled primary emphasis', () => {
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.getByText('Dismiss').parentElement
167
- expect(corner?.className).toContain('absolute right-3 top-3')
168
- expect(corner?.className).toContain('z-10')
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
- const { container } = renderWithProviders(
146
+ renderWithProviders(
173
147
  <CardShell variant="dark" data-testid="shell">
174
148
  body
175
149
  </CardShell>
176
150
  )
177
- expect(container.querySelectorAll('div.absolute')).toHaveLength(0)
151
+ expect(screen.queryByTestId('card-shell-top-right')).not.toBeInTheDocument()
178
152
  })
179
153
 
180
154
  it.each([