@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 +3 -3
- package/src/components/AttachmentCard/MediaPlayer.test.tsx +2 -6
- package/src/components/AttachmentCard/Thumbnail.test.tsx +10 -27
- package/src/components/AttachmentCard/index.test.tsx +5 -35
- package/src/components/Avatar/Avatar.test.tsx +1 -3
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +1 -3
- package/src/components/ChannelView.test.tsx +0 -6
- package/src/components/CustomLinkPreviewList/CustomLinkPreviewList.test.tsx +1 -0
- package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +2 -0
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +8 -28
- package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +1 -0
- package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +4 -48
- package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +1 -21
- package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +3 -32
- package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +13 -32
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +2 -0
- package/src/components/MessageBubble/MessageBubble.test.tsx +0 -1
- package/src/components/RichCard/RichCard.test.tsx +1 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "4.4.3-rc-
|
|
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-
|
|
54
|
-
"@linktr.ee/messaging-taxonomy": "0.11.0-rc-
|
|
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
|
|
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(
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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')).
|
|
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('
|
|
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')).
|
|
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('
|
|
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')).
|
|
84
|
-
expect(screen.
|
|
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('
|
|
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('
|
|
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')
|
|
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
|
|
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('
|
|
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
|
|
187
|
-
const { container
|
|
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(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
208
|
-
const { container
|
|
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(
|
|
212
|
-
const bar =
|
|
213
|
-
expect(bar
|
|
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\]/
|
|
@@ -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
|