@linktr.ee/messaging-react 4.4.3-rc-1788166409 → 4.4.3-rc-1788170348

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "4.4.3-rc-1788166409",
3
+ "version": "4.4.3-rc-1788170348",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -50,8 +50,8 @@
50
50
  "chromatic:local": "yarn storybook:build && chromatic"
51
51
  },
52
52
  "dependencies": {
53
- "@linktr.ee/messaging-core": "2.6.0-rc-1788166409",
54
- "@linktr.ee/messaging-taxonomy": "0.11.0-rc-1788166409",
53
+ "@linktr.ee/messaging-core": "2.6.0-rc-1788170348",
54
+ "@linktr.ee/messaging-taxonomy": "0.11.0-rc-1788170348",
55
55
  "@phosphor-icons/react": "^2.1.10"
56
56
  },
57
57
  "devDependencies": {
@@ -262,7 +262,7 @@ describe('MediaPlayer', () => {
262
262
  expect(container.querySelector('.animate-spin')).toBeNull()
263
263
  })
264
264
 
265
- it('seeks with keyboard and pointer input and expands the scrubber on hover', () => {
265
+ it('seeks with keyboard and pointer input and reveals the scrubber thumb on hover', () => {
266
266
  const { container } = renderWithProviders(
267
267
  <MediaPlayer source={SOURCE} mimeType="video/mp4" controls showProgress />
268
268
  )
@@ -272,7 +272,6 @@ describe('MediaPlayer', () => {
272
272
  value: 100,
273
273
  })
274
274
  const slider = screen.getByRole('slider', { name: 'Playback position' })
275
- const track = slider.firstElementChild as HTMLElement
276
275
  const thumb = slider.lastElementChild as HTMLElement
277
276
  vi.spyOn(slider, 'getBoundingClientRect').mockReturnValue({
278
277
  left: 100,
@@ -286,18 +285,15 @@ describe('MediaPlayer', () => {
286
285
  toJSON: () => ({}),
287
286
  })
288
287
 
289
- expect(track).toHaveClass('h-1')
290
288
  expect(thumb).toHaveClass('scale-0', 'opacity-0')
291
289
  fireEvent.mouseEnter(slider)
292
- expect(track).toHaveClass('h-1.5')
293
290
  expect(thumb).toHaveClass('scale-100', 'opacity-100')
294
291
  fireEvent.mouseLeave(slider)
295
- expect(track).toHaveClass('h-1')
292
+ expect(thumb).toHaveClass('scale-0', 'opacity-0')
296
293
 
297
294
  fireEvent.mouseDown(slider, { clientX: 150 })
298
295
  expect(slider).toHaveAttribute('aria-valuenow', '25')
299
296
  expect(video.currentTime).toBe(25)
300
- expect(track).toHaveClass('h-1.5')
301
297
  expect(thumb).toHaveClass('scale-100', 'opacity-100')
302
298
 
303
299
  fireEvent.mouseUp(window, { clientX: 150 })
@@ -147,28 +147,15 @@ describe('AttachmentThumbnail', () => {
147
147
  })
148
148
 
149
149
  it('renders a document icon placeholder without a thumbnail', () => {
150
- {
151
- const view = renderWithProviders(
152
- <AttachmentThumbnail
153
- mimeType="application/pdf"
154
- variant="dark"
155
- title="PDF"
156
- />
157
- )
158
- const darkIcon = view.container.querySelector('svg') as SVGElement
159
- expect(darkIcon).toHaveClass('size-12', 'text-white/20')
160
- expect(view.container.firstElementChild).toHaveClass('bg-white/10')
161
- view.unmount()
162
- }
163
-
164
- {
165
- const view = renderWithProviders(
166
- <AttachmentThumbnail mimeType="application/pdf" variant="light" />
167
- )
168
- const lightIcon = view.container.querySelector('svg') as SVGElement
169
- expect(lightIcon).toHaveClass('size-12', 'text-black/20')
170
- expect(view.container.firstElementChild).toHaveClass('bg-black/5')
171
- }
150
+ const { container } = renderWithProviders(
151
+ <AttachmentThumbnail
152
+ mimeType="application/pdf"
153
+ variant="dark"
154
+ title="PDF"
155
+ />
156
+ )
157
+ expect(container.querySelector('img')).toBeNull()
158
+ expect(container.querySelector('svg')).toBeInTheDocument()
172
159
  })
173
160
 
174
161
  it('renders the poster-only branch with variant-specific shell and alt fallback', () => {
@@ -214,10 +201,6 @@ describe('AttachmentThumbnail', () => {
214
201
  />
215
202
  )
216
203
  expect(container.querySelector('img')).toBeNull()
217
- expect(container.querySelector('svg')).toHaveClass(
218
- 'size-12',
219
- 'text-black/20'
220
- )
221
- expect(container.firstElementChild).toHaveClass('bg-black/5')
204
+ expect(container.querySelector('svg')).toBeInTheDocument()
222
205
  })
223
206
  })
@@ -8,37 +8,7 @@ import AttachmentCard from './index'
8
8
  const thumbnail = <div data-testid="thumbnail">thumbnail</div>
9
9
 
10
10
  describe('AttachmentCard', () => {
11
- it('applies the dark and light root surfaces and type icon colors', () => {
12
- {
13
- const view = renderWithProviders(
14
- <AttachmentCard
15
- variant="dark"
16
- thumbnail={thumbnail}
17
- mimeType="application/pdf"
18
- title="Dark file"
19
- />
20
- )
21
- expect(screen.getByText('Dark file')).toBeInTheDocument()
22
- expect(view.container.firstElementChild).toHaveClass('bg-[#1e2330]')
23
- expect(view.container.querySelector('svg')).toHaveClass('text-white/55')
24
- view.unmount()
25
- }
26
-
27
- {
28
- const view = renderWithProviders(
29
- <AttachmentCard
30
- variant="light"
31
- thumbnail={thumbnail}
32
- mimeType="application/pdf"
33
- title="Light file"
34
- />
35
- )
36
- expect(view.container.firstElementChild).toHaveClass('bg-white')
37
- expect(view.container.querySelector('svg')).toHaveClass('text-black/55')
38
- }
39
- })
40
-
41
- it('uses the dark placeholder title and dims it when title is absent', () => {
11
+ it('renders the dark placeholder title when no title is supplied', () => {
42
12
  renderWithProviders(
43
13
  <AttachmentCard
44
14
  variant="dark"
@@ -47,7 +17,7 @@ describe('AttachmentCard', () => {
47
17
  placeholderTitle="Waiting for title"
48
18
  />
49
19
  )
50
- expect(screen.getByText('Waiting for title')).toHaveClass('text-white/30')
20
+ expect(screen.getByText('Waiting for title')).toBeInTheDocument()
51
21
  })
52
22
 
53
23
  it('does not render a light title for missing or whitespace-only values', () => {
@@ -71,7 +41,7 @@ describe('AttachmentCard', () => {
71
41
  expect(screen.queryByText(' ')).toBeNull()
72
42
  })
73
43
 
74
- it('uses an undimmed title class when a dark title is provided', () => {
44
+ it('renders the supplied dark title instead of the placeholder', () => {
75
45
  renderWithProviders(
76
46
  <AttachmentCard
77
47
  variant="dark"
@@ -80,8 +50,8 @@ describe('AttachmentCard', () => {
80
50
  title="Named file"
81
51
  />
82
52
  )
83
- expect(screen.getByText('Named file')).toHaveClass('text-white')
84
- expect(screen.getByText('Named file')).not.toHaveClass('text-white/30')
53
+ expect(screen.getByText('Named file')).toBeInTheDocument()
54
+ expect(screen.queryByText('Attachment title')).toBeNull()
85
55
  })
86
56
 
87
57
  it('renders detail only for non-empty values', () => {
@@ -153,10 +153,8 @@ describe('Avatar', () => {
153
153
  <Avatar id="" name="Alice" className="custom-avatar" />
154
154
  )
155
155
  const root = container.firstElementChild
156
- const ring = container.querySelector('[data-testid="avatar-ring"]')
157
156
 
158
- expect(root).toHaveClass('relative', 'flex-shrink-0', 'custom-avatar')
159
- expect(ring).toHaveClass('h-full', 'w-full', 'bg-transparent')
157
+ expect(root).toHaveClass('custom-avatar')
160
158
  expect(root).toHaveStyle({
161
159
  backgroundColor: '#FFD1DD',
162
160
  width: '40px',
@@ -87,7 +87,7 @@ describe('CustomChannelPreview', () => {
87
87
  defaultProps.onChannelSelect.mockClear()
88
88
  })
89
89
 
90
- it('applies selected and unselected row styling from context', () => {
90
+ it('drops the hover affordance on the selected row only', () => {
91
91
  const channel = createMockChannel([])
92
92
  const otherChannel = createMockChannel([])
93
93
  otherChannel.id = 'channel-2'
@@ -105,7 +105,6 @@ describe('CustomChannelPreview', () => {
105
105
  )
106
106
 
107
107
  let row = screen.getByRole('button')
108
- expect(row).toHaveClass('bg-black/[0.04]')
109
108
  expect(row).not.toHaveClass('hover:bg-black/[0.02]')
110
109
 
111
110
  rerender(
@@ -121,7 +120,6 @@ describe('CustomChannelPreview', () => {
121
120
  )
122
121
 
123
122
  row = screen.getByRole('button')
124
- expect(row).not.toHaveClass('bg-black/[0.04]')
125
123
  expect(row).toHaveClass('hover:bg-black/[0.02]')
126
124
 
127
125
  rerender(
@@ -284,12 +284,6 @@ describe('ChannelView', () => {
284
284
  const button = screen.getByRole('button', {
285
285
  name: /view details for linker/i,
286
286
  })
287
- expect(button).toHaveClass(
288
- 'appearance-none',
289
- 'border-0',
290
- 'bg-transparent',
291
- 'flex-col'
292
- )
293
287
 
294
288
  const avatars = screen.getAllByTestId('avatar')
295
289
  expect(avatars.length).toBeGreaterThanOrEqual(1)
@@ -40,6 +40,7 @@ describe('CustomLinkPreviewList', () => {
40
40
 
41
41
  const shell = screen.getByLabelText(LOADING_LABEL)
42
42
  expect(shell).toHaveAttribute('aria-busy', 'true')
43
+ // eslint-disable-next-line local/no-style-only-assertion -- MES-1349: the reserved 250px is the regression itself, and jsdom has no layout to observe the snap-open.
43
44
  expect(shell.className).toContain('h-[250px]')
44
45
  // Hero placeholder + the two chin bars: without the hero there are two.
45
46
  expect(container.querySelectorAll('.animate-pulse')).toHaveLength(3)
@@ -378,6 +378,7 @@ describe('CustomMessageInput', () => {
378
378
  expect(
379
379
  card.querySelector('.messaging-composer-previews')
380
380
  ).not.toHaveClass('min-h-0')
381
+ // eslint-disable-next-line local/no-style-only-assertion -- MES-1037: the row not being shrink-0 is part of the single-scroll contract in this component's doc comment, and jsdom has no layout to observe the squeeze it prevents.
381
382
  expect(card.querySelector('.messaging-composer-row')).not.toHaveClass(
382
383
  'shrink-0'
383
384
  )
@@ -487,6 +488,7 @@ describe('CustomMessageInput', () => {
487
488
  // card scrolls far enough for the button to ride up to the top of the
488
489
  // row, a staged preview otherwise meets it flush. Asserted together so
489
490
  // they cannot drift apart.
491
+ // eslint-disable-next-line local/no-style-only-assertion -- the point is that these three offsets match; sticky resolution isn't observable in jsdom.
490
492
  expect(screen.getByTestId('send-button').parentElement).toHaveClass(
491
493
  'sticky',
492
494
  'bottom-2',
@@ -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
 
@@ -416,6 +392,7 @@ describe('LinkAttachment accent chin (MES-1349, MES-1388)', () => {
416
392
  expect(busy).toHaveAttribute('aria-busy', 'true')
417
393
  // The skeleton keeps its 250px box: the audio shell is intrinsic-height,
418
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.
419
396
  expect(busy.className).toContain('h-[250px]')
420
397
  expect(container.querySelectorAll('.animate-pulse')).toHaveLength(3)
421
398
 
@@ -514,7 +491,6 @@ describe('LinkAttachment.Composer chrome', () => {
514
491
  />
515
492
  )
516
493
  const dismiss = screen.getByLabelText('Dismiss attachment')
517
- expect(dismiss.parentElement?.className).toContain('absolute right-3 top-3')
518
494
  // Sits inside the hero region, not the shell corner.
519
495
  expect(container.querySelector('div.flex-1')?.contains(dismiss)).toBe(true)
520
496
  // 44x44 touch target without growing the 24px circle.
@@ -540,6 +516,7 @@ describe('LinkAttachment.Composer chrome', () => {
540
516
  true
541
517
  )
542
518
  // Classic cards size to their content.
519
+ // eslint-disable-next-line local/no-style-only-assertion -- MES-1349: the reserved box is the contract; jsdom has no layout to observe it.
543
520
  expect(shell.className).not.toContain('h-[250px]')
544
521
  expect(shell.className).toContain('block')
545
522
  })
@@ -552,6 +529,7 @@ describe('LinkAttachment.Composer chrome', () => {
552
529
  'src',
553
530
  HERO
554
531
  )
532
+ // eslint-disable-next-line local/no-style-only-assertion -- MES-1349: the reserved box is the contract; jsdom has no layout to observe it.
555
533
  expect(shellOf(container).className).toContain('h-[250px]')
556
534
  })
557
535
 
@@ -572,7 +550,6 @@ describe('LinkAttachment.Composer chrome', () => {
572
550
  const dismiss = screen.getByLabelText('Dismiss attachment')
573
551
  // Inline sibling of the native controls: no overlay halo on top of them.
574
552
  expect(dismiss.className).not.toContain('before:-inset-[10px]')
575
- expect(dismiss.parentElement?.className).not.toContain('absolute')
576
553
  })
577
554
 
578
555
  it('lets the consumer name the dismiss control', () => {
@@ -787,6 +764,7 @@ describe('LinkAttachment layout and surface', () => {
787
764
  )
788
765
  expect(screen.queryByRole('img', { hidden: true })).toBeNull()
789
766
  const shell = shellOf(container)
767
+ // eslint-disable-next-line local/no-style-only-assertion -- MES-1349: the reserved box is the contract; jsdom has no layout to observe it.
790
768
  expect(shell.className).not.toContain('h-[250px]')
791
769
  expect(shell.className).toContain('block')
792
770
  }
@@ -803,6 +781,7 @@ describe('LinkAttachment layout and surface', () => {
803
781
  'src',
804
782
  HERO
805
783
  )
784
+ // eslint-disable-next-line local/no-style-only-assertion -- MES-1349: the reserved box is the contract; jsdom has no layout to observe it.
806
785
  expect(shellOf(container).className).toContain('h-[250px]')
807
786
  })
808
787
 
@@ -817,6 +796,7 @@ describe('LinkAttachment layout and surface', () => {
817
796
  )
818
797
  const shell = shellOf(container)
819
798
  expect(shell.className).toContain(AUDIO_BG_CLASS)
799
+ // eslint-disable-next-line local/no-style-only-assertion -- MES-1349: the reserved box is the contract; jsdom has no layout to observe it.
820
800
  expect(shell.className).not.toContain('h-[250px]')
821
801
  expect(container.querySelector('audio')).not.toBeNull()
822
802
  }
@@ -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
 
@@ -165,12 +133,14 @@ describe('CardBody', () => {
165
133
  />
166
134
  )
167
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. */
168
137
  expect(screen.getByText('Play').className).toContain('bg-black/[0.06]')
169
138
 
170
139
  renderWithProviders(
171
140
  <CardBody variant="dark" cta={{ label: 'View FAQs' }} />
172
141
  )
173
142
  expect(screen.getByText('View FAQs').className).toContain('bg-white')
143
+ /* eslint-enable local/no-style-only-assertion */
174
144
  })
175
145
 
176
146
  it('renders the app icon before the title and the trailing action beside it', () => {
@@ -184,13 +154,10 @@ describe('CardBody', () => {
184
154
  )
185
155
  const icon = screen.getByTestId('app-icon')
186
156
  const title = screen.getByText('Spotify')
187
- expect(icon.parentElement?.className).toContain('shrink-0')
188
157
  expect(
189
158
  icon.compareDocumentPosition(title) & Node.DOCUMENT_POSITION_FOLLOWING
190
159
  ).toBeTruthy()
191
- expect(screen.getByText('Edit').parentElement?.className).toContain(
192
- 'shrink-0'
193
- )
160
+ expect(screen.getByText('Edit')).toBeInTheDocument()
194
161
  })
195
162
 
196
163
  it('omits the title row entirely when there is no title', () => {
@@ -204,7 +171,7 @@ describe('CardBody', () => {
204
171
  expect(screen.queryByTestId('app-icon')).not.toBeInTheDocument()
205
172
  })
206
173
 
207
- it('renders two variant-aware skeleton bars instead of text while loading', () => {
174
+ it('renders two skeleton bars instead of text while loading', () => {
208
175
  const { container: dark } = renderWithProviders(
209
176
  <CardBody variant="dark" title="Brie’s FAQ" url="tr.ee/a" isLoading />
210
177
  )
@@ -213,20 +180,9 @@ describe('CardBody', () => {
213
180
 
214
181
  const darkBars = dark.querySelectorAll('div.animate-pulse')
215
182
  expect(darkBars).toHaveLength(2)
216
- expect(darkBars[0].className).toContain('h-5 w-3/4')
217
- expect(darkBars[1].className).toContain('h-4 w-1/2')
218
183
  for (const bar of darkBars) {
219
- expect(bar.className).toContain('rounded')
220
- expect(bar.className).toContain('bg-white/25')
221
184
  expect(bar.getAttribute('aria-hidden')).toBe('true')
222
185
  }
223
-
224
- const { container: light } = renderWithProviders(
225
- <CardBody variant="light" isLoading />
226
- )
227
- for (const bar of light.querySelectorAll('div.animate-pulse')) {
228
- expect(bar.className).toContain('bg-black/15')
229
- }
230
186
  })
231
187
 
232
188
  it('renders the skeleton even when there is nothing else to show', () => {
@@ -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
 
@@ -164,8 +133,10 @@ describe('CardShell', () => {
164
133
  </CardShell>
165
134
  )
166
135
  const corner = screen.getByText('Dismiss').parentElement
136
+ /* 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. */
167
137
  expect(corner?.className).toContain('absolute right-3 top-3')
168
138
  expect(corner?.className).toContain('z-10')
139
+ /* eslint-enable local/no-style-only-assertion */
169
140
  })
170
141
 
171
142
  it('omits the corner wrapper when no topRight is provided', () => {
@@ -100,9 +100,6 @@ describe('CardThumbnail', () => {
100
100
  expect(video?.getAttribute('poster')).toBe(IMAGE_SRC)
101
101
  expect(video?.hasAttribute('controls')).toBe(true)
102
102
  expect(video?.getAttribute('preload')).toBe('metadata')
103
- expect(container.querySelector('div.flex-1')?.className).toContain(
104
- 'bg-black'
105
- )
106
103
  })
107
104
 
108
105
  it('probes the video poster with a hidden image that reports failures', () => {
@@ -183,42 +180,24 @@ describe('CardThumbnail', () => {
183
180
  expect(onImageError).toHaveBeenCalledTimes(1)
184
181
  })
185
182
 
186
- it('shows a variant-aware type-icon placeholder without a thumbnail', () => {
187
- const { container: dark } = renderWithProviders(
183
+ it('shows a type-icon placeholder instead of an image without a thumbnail', () => {
184
+ const { container } = renderWithProviders(
188
185
  <CardThumbnail variant="dark" mimeType="application/pdf" />
189
186
  )
190
- expect(dark.querySelectorAll('img')).toHaveLength(0)
191
- const darkPlaceholder = dark.querySelector('div.items-center')
192
- expect(darkPlaceholder?.className).toContain('bg-white/10')
193
- expect(darkPlaceholder?.querySelector('svg')?.getAttribute('class')).toBe(
194
- 'size-16 text-white/25'
195
- )
196
-
197
- const { container: light } = renderWithProviders(
198
- <CardThumbnail variant="light" />
199
- )
200
- const lightPlaceholder = light.querySelector('div.items-center')
201
- expect(lightPlaceholder?.className).toContain('bg-black/5')
202
- expect(lightPlaceholder?.querySelector('svg')?.getAttribute('class')).toBe(
203
- 'size-16 text-black/25'
204
- )
187
+ expect(container.querySelectorAll('img')).toHaveLength(0)
188
+ expect(
189
+ container.querySelector('div.items-center')?.querySelector('svg')
190
+ ).toBeInTheDocument()
205
191
  })
206
192
 
207
- it('pulses a variant-aware block while loading, ignoring the thumbnail', () => {
208
- const { container: dark } = renderWithProviders(
193
+ it('pulses a placeholder block while loading, ignoring the thumbnail', () => {
194
+ const { container } = renderWithProviders(
209
195
  <CardThumbnail variant="dark" thumbnailUrl={IMAGE_SRC} isLoading />
210
196
  )
211
- expect(dark.querySelectorAll('img')).toHaveLength(0)
212
- const bar = dark.querySelector('div.animate-pulse')
213
- expect(bar?.className).toContain('bg-white/10')
197
+ expect(container.querySelectorAll('img')).toHaveLength(0)
198
+ const bar = container.querySelector('div.animate-pulse')
199
+ expect(bar).toBeInTheDocument()
214
200
  expect(bar?.getAttribute('aria-hidden')).toBe('true')
215
-
216
- const { container: light } = renderWithProviders(
217
- <CardThumbnail variant="light" isLoading />
218
- )
219
- expect(light.querySelector('div.animate-pulse')?.className).toContain(
220
- 'bg-black/5'
221
- )
222
201
  })
223
202
 
224
203
  it('pins the corner slots and omits their wrappers when unused', () => {
@@ -229,12 +208,14 @@ describe('CardThumbnail', () => {
229
208
  topRight={<span>right</span>}
230
209
  />
231
210
  )
211
+ /* eslint-disable local/no-style-only-assertion -- overlay slots: in flow they would displace the hero instead of sitting over it. */
232
212
  expect(screen.getByText('left').parentElement?.className).toContain(
233
213
  'absolute left-3 top-3'
234
214
  )
235
215
  expect(screen.getByText('right').parentElement?.className).toContain(
236
216
  'absolute right-3 top-3'
237
217
  )
218
+ /* eslint-enable local/no-style-only-assertion */
238
219
 
239
220
  const { container } = renderWithProviders(<CardThumbnail variant="dark" />)
240
221
  expect(container.querySelectorAll('div.absolute')).toHaveLength(0)
@@ -370,6 +370,7 @@ describe('MessageAttachment.Bubble geometry overrides', () => {
370
370
  filename="file.zip"
371
371
  />
372
372
  )
373
+ // eslint-disable-next-line local/no-style-only-assertion -- the override contract above, not the values: an appended radius silently loses to the corner table.
373
374
  expect(screen.getByTestId('file-attachment')).toHaveClass(
374
375
  'w-[280px]',
375
376
  'px-2',
@@ -383,6 +384,7 @@ describe('MessageAttachment.Bubble geometry overrides', () => {
383
384
  <MessageAttachment.Image.Sent src="https://cdn.example.com/photo.jpg" />
384
385
  )
385
386
  const bubble = screen.getByTestId('image-attachment')
387
+ // eslint-disable-next-line local/no-style-only-assertion -- the override contract above, not the values: an appended radius silently loses to the corner table.
386
388
  expect(bubble).toHaveClass('w-fit', 'p-0.5', 'rounded-[20px]')
387
389
  expect(bubble.className).not.toMatch(
388
390
  /w-\[280px\]|px-2|py-2|rounded-tl-\[18px\]/
@@ -71,6 +71,5 @@ describe('MessageBubble', () => {
71
71
  )
72
72
  const bubble = screen.getByText('wide').parentElement as HTMLElement
73
73
  expect(bubble.classList.contains('w-fit')).toBe(true)
74
- expect(bubble.className).toContain('max-w-[520px]')
75
74
  })
76
75
  })
@@ -185,6 +185,7 @@ describe('RichCard', () => {
185
185
  />
186
186
  )
187
187
  const card = await screen.findByTestId('rich-card')
188
+ // eslint-disable-next-line local/no-style-only-assertion -- MES-1349: not reserving the fixed box is the contract; jsdom has no layout to observe it.
188
189
  expect(card.className).not.toContain('h-[250px]')
189
190
  })
190
191
 
@@ -222,30 +223,6 @@ describe('RichCard', () => {
222
223
  expect(onClick).toHaveBeenCalledTimes(1)
223
224
  })
224
225
 
225
- it('stacks a primary footer full-width and lays a secondary footer inline', async () => {
226
- const { rerender } = renderWithProviders(
227
- <RichCard.Received
228
- title="Card"
229
- images={[HERO]}
230
- actions={[{ label: 'Set up welcome message', variant: 'primary' }]}
231
- />
232
- )
233
- // Primary → stacked, full-width button.
234
- expect(await screen.findByText('Set up welcome message')).toHaveClass(
235
- 'w-full'
236
- )
237
-
238
- rerender(
239
- <RichCard.Received
240
- title="Card"
241
- images={[HERO]}
242
- actions={[{ label: 'Shop now', href: 'tr.ee/x', variant: 'secondary' }]}
243
- />
244
- )
245
- // Secondary → inline, content-width button (no w-full).
246
- expect(screen.getByText('Shop now').closest('a')).not.toHaveClass('w-full')
247
- })
248
-
249
226
  it('lays a secondary chin out as a row: copy left, action right', async () => {
250
227
  const { rerender } = renderWithProviders(
251
228
  <RichCard.Received