@linktr.ee/messaging-react 4.2.0-rc-1787325521 → 4.2.0-rc-1787599724
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Card-BYJyA83X.js → Card-D7UPq5-g.js} +2 -2
- package/dist/{Card-BYJyA83X.js.map → Card-D7UPq5-g.js.map} +1 -1
- package/dist/{Card-BplJtZao.cjs → Card-DcBZqMBt.cjs} +2 -2
- package/dist/{Card-BplJtZao.cjs.map → Card-DcBZqMBt.cjs.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/{index-BUC9vqhJ.js → index-CSkr_CO5.js} +2089 -1952
- package/dist/index-CSkr_CO5.js.map +1 -0
- package/dist/index-CwkY7QwT.cjs +3 -0
- package/dist/index-CwkY7QwT.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +59 -78
- package/dist/index.js +26 -30
- package/dist/testing.cjs +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +2 -2
- package/dist/testing.js.map +1 -1
- package/package.json +13 -11
- package/src/components/AttachmentCard/utils/icons.ts +7 -6
- package/src/components/Avatar/Avatar.test.tsx +0 -126
- package/src/components/Avatar/getAvatarEmoji.test.ts +0 -39
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +3 -410
- package/src/components/ChannelList/index.test.tsx +1 -132
- package/src/components/ChannelView.stories.tsx +0 -4
- package/src/components/ChannelView.test.tsx +3 -9
- package/src/components/CustomMessage/CustomMessage.stories.tsx +314 -6
- package/src/components/CustomMessage/CustomMessage.test.tsx +2 -56
- package/src/components/CustomMessage/MessageTag.stories.tsx +53 -17
- package/src/components/CustomMessage/MessageTag.test.tsx +40 -7
- package/src/components/CustomMessage/MessageTag.tsx +39 -7
- package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +7 -8
- package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +1 -2
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +33 -0
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +12 -5
- package/src/components/CustomMessage/index.tsx +19 -22
- package/src/components/CustomMessageInput/index.tsx +1 -1
- package/src/components/CustomSystemMessage/index.tsx +2 -3
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +0 -327
- package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +165 -58
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.test.tsx +153 -0
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.tsx +450 -0
- package/src/components/MessageAttachment/Audio/audioRowRequester.test.ts +24 -0
- package/src/components/MessageAttachment/Audio/audioRowRequester.ts +16 -0
- package/src/components/MessageAttachment/Audio/index.tsx +83 -77
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +176 -74
- package/src/components/MessageAttachment/_shared/fileMeta.test.ts +0 -44
- package/src/components/MessageAttachment/_shared/useIsClient.ts +23 -0
- package/src/components/MessageAttachment/index.tsx +25 -9
- package/src/components/MessageAttachment/types.ts +19 -9
- package/src/components/MessagingShell/MessagingShell.test.tsx +0 -20
- package/src/components/MessagingShell/index.tsx +0 -2
- package/src/components/RichCard/RichCard.test.tsx +1 -3
- package/src/index.ts +0 -4
- package/src/providers/MessagingProvider.test.tsx +2 -130
- package/src/providers/MessagingProvider.tsx +15 -26
- package/src/styles.css +210 -57
- package/src/testing/createMockMessagingClient.ts +2 -2
- package/src/types.ts +0 -9
- package/dist/index-BUC9vqhJ.js.map +0 -1
- package/dist/index-BkqyKPJP.cjs +0 -3
- package/dist/index-BkqyKPJP.cjs.map +0 -1
- package/src/components/AttachmentCard/MediaPlayer.test.tsx +0 -346
- package/src/components/AttachmentCard/Thumbnail.test.tsx +0 -215
- package/src/components/AttachmentCard/index.test.tsx +0 -160
- package/src/components/ChannelList/ChannelListContext.test.tsx +0 -87
- package/src/components/CustomMessage/Locked.stories.tsx +0 -108
- package/src/components/CustomMessage/PaidMessage.stories.tsx +0 -42
- package/src/components/CustomMessage/Tip.stories.tsx +0 -48
- package/src/components/CustomMessage/TipMessage/TipMessage.stories.tsx +0 -51
- package/src/components/CustomMessage/TipMessage/TipMessage.test.tsx +0 -65
- package/src/components/CustomMessage/TipMessage/index.tsx +0 -27
- package/src/components/FaqList/FaqList.test.tsx +0 -204
- package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +0 -76
- package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +0 -248
- package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +0 -123
- package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +0 -193
- package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +0 -242
- package/src/components/LinkAttachment/components/_shared/hex.test.ts +0 -37
- package/src/components/LinkAttachment/components/_shared/normalizeExternalHref.test.ts +0 -72
- package/src/components/LinkAttachment/components/_shared/useChinPalette.test.ts +0 -162
- package/src/components/MessageAttachment/MessageAttachment.behavior.test.tsx +0 -182
- package/src/components/MessageAttachment/_shared/Bubble.test.tsx +0 -111
- package/src/components/MessageAttachment/_shared/CarouselNav.test.tsx +0 -36
- package/src/components/MessageAttachment/_shared/CompactDocumentRow.test.tsx +0 -114
- package/src/components/MessageAttachment/_shared/DismissButton.test.tsx +0 -42
- package/src/components/MessageAttachment/_shared/DownloadAction.test.tsx +0 -168
- package/src/components/MessageAttachment/_shared/ImageViewer.test.tsx +0 -52
- package/src/components/MessageAttachment/_shared/MediaStackGrid.test.tsx +0 -69
- package/src/components/MessageAttachment/_shared/PdfViewer.test.tsx +0 -45
- package/src/components/MessageAttachment/_shared/VideoViewer.test.tsx +0 -55
- package/src/components/MessageAttachment/_shared/ViewerShell.test.tsx +0 -246
- package/src/components/MessageAttachment/_shared/triggerDownload.test.ts +0 -113
- package/src/components/MessageAttachment/_shared/useCarousel.test.tsx +0 -68
- package/src/components/MessageAttachment/_shared/viewerChromeClasses.test.ts +0 -16
- package/src/logging/index.test.tsx +0 -136
- package/src/logging/index.tsx +0 -48
- package/src/stories/customMessageStoryTemplate.tsx +0 -202
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import { fireEvent, renderWithProviders, screen } from '../../test/utils'
|
|
5
|
-
import { getAvatarEmoji } from '../Avatar/getAvatarEmoji'
|
|
6
|
-
|
|
7
|
-
import { FaqListItem } from './FaqListItem'
|
|
8
|
-
|
|
9
|
-
import { FaqList } from './index'
|
|
10
|
-
|
|
11
|
-
const faqs = [
|
|
12
|
-
{
|
|
13
|
-
id: 'second',
|
|
14
|
-
question: 'What payment methods do you accept?',
|
|
15
|
-
answer: 'Cards and bank transfers.',
|
|
16
|
-
enabled: true,
|
|
17
|
-
order: 2,
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
id: 'disabled',
|
|
21
|
-
question: 'What is your phone number?',
|
|
22
|
-
answer: 'This FAQ is disabled.',
|
|
23
|
-
enabled: false,
|
|
24
|
-
order: 0,
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
id: 'first',
|
|
28
|
-
question: 'How do I get started?',
|
|
29
|
-
answer: 'Create an account.',
|
|
30
|
-
enabled: true,
|
|
31
|
-
order: 1,
|
|
32
|
-
},
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
const baseItemClasses = [
|
|
36
|
-
'w-full',
|
|
37
|
-
'text-center',
|
|
38
|
-
'p-4',
|
|
39
|
-
'rounded-xl',
|
|
40
|
-
'text-charcoal',
|
|
41
|
-
'font-medium',
|
|
42
|
-
'transition-colors',
|
|
43
|
-
'focus-ring',
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
describe('FaqListItem', () => {
|
|
47
|
-
it('renders its question and defaults to an enabled button', () => {
|
|
48
|
-
renderWithProviders(
|
|
49
|
-
<FaqListItem question="How do I get started?" onClick={vi.fn()} />
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
const button = screen.getByRole('button', {
|
|
53
|
-
name: 'How do I get started?',
|
|
54
|
-
})
|
|
55
|
-
expect(button).toBeEnabled()
|
|
56
|
-
expect(button.className.split(/\s+/)).toEqual([
|
|
57
|
-
...baseItemClasses,
|
|
58
|
-
'hover:brightness-95',
|
|
59
|
-
'active:brightness-90',
|
|
60
|
-
])
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('renders the FAQ surface color', () => {
|
|
64
|
-
renderWithProviders(
|
|
65
|
-
<FaqListItem question="How do I get started?" onClick={vi.fn()} />
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
expect(
|
|
69
|
-
screen.getByRole('button', { name: 'How do I get started?' })
|
|
70
|
-
).toHaveStyle({ backgroundColor: '#E6E5E3' })
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('uses loading-only classes and blocks clicks while loading', () => {
|
|
74
|
-
const onClick = vi.fn()
|
|
75
|
-
renderWithProviders(
|
|
76
|
-
<FaqListItem question="How do I get started?" onClick={onClick} loading />
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
const button = screen.getByRole('button', {
|
|
80
|
-
name: 'How do I get started?',
|
|
81
|
-
})
|
|
82
|
-
expect(button).toBeDisabled()
|
|
83
|
-
expect(button.className.split(/\s+/)).toEqual([
|
|
84
|
-
...baseItemClasses,
|
|
85
|
-
'opacity-50',
|
|
86
|
-
'cursor-not-allowed',
|
|
87
|
-
])
|
|
88
|
-
|
|
89
|
-
fireEvent.click(button)
|
|
90
|
-
expect(onClick).not.toHaveBeenCalled()
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('calls the click handler once when enabled', () => {
|
|
94
|
-
const onClick = vi.fn()
|
|
95
|
-
renderWithProviders(
|
|
96
|
-
<FaqListItem question="How do I get started?" onClick={onClick} />
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
fireEvent.click(screen.getByRole('button'))
|
|
100
|
-
expect(onClick).toHaveBeenCalledTimes(1)
|
|
101
|
-
})
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
describe('FaqList', () => {
|
|
105
|
-
it('returns nothing when no FAQ is enabled', () => {
|
|
106
|
-
const { container } = renderWithProviders(
|
|
107
|
-
<FaqList
|
|
108
|
-
faqs={faqs.map((faq) => ({ ...faq, enabled: false }))}
|
|
109
|
-
onFaqClick={vi.fn()}
|
|
110
|
-
/>
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
expect(container.firstChild).toBeNull()
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it('renders enabled FAQs in ascending order and filters disabled FAQs', () => {
|
|
117
|
-
renderWithProviders(<FaqList faqs={faqs} onFaqClick={vi.fn()} />)
|
|
118
|
-
|
|
119
|
-
expect(
|
|
120
|
-
screen.getByRole('button', { name: 'How do I get started?' })
|
|
121
|
-
.parentElement
|
|
122
|
-
).toHaveStyle({ backgroundColor: '#F1F0EE' })
|
|
123
|
-
expect(
|
|
124
|
-
screen.getAllByRole('button').map((button) => button.textContent)
|
|
125
|
-
).toEqual(['How do I get started?', 'What payment methods do you accept?'])
|
|
126
|
-
expect(
|
|
127
|
-
screen.queryByRole('button', { name: 'What is your phone number?' })
|
|
128
|
-
).toBeNull()
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
it('renders the header only when headerText is provided', () => {
|
|
132
|
-
const { container, rerender } = renderWithProviders(
|
|
133
|
-
<FaqList faqs={faqs} onFaqClick={vi.fn()} />
|
|
134
|
-
)
|
|
135
|
-
expect(screen.queryByText('Frequently asked questions')).toBeNull()
|
|
136
|
-
expect(container.querySelector('p')).toBeNull()
|
|
137
|
-
|
|
138
|
-
rerender(
|
|
139
|
-
<FaqList
|
|
140
|
-
faqs={faqs}
|
|
141
|
-
onFaqClick={vi.fn()}
|
|
142
|
-
headerText="Frequently asked questions"
|
|
143
|
-
/>
|
|
144
|
-
)
|
|
145
|
-
expect(screen.getByText('Frequently asked questions')).toBeInTheDocument()
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('passes className to the list root', () => {
|
|
149
|
-
const { container } = renderWithProviders(
|
|
150
|
-
<FaqList
|
|
151
|
-
faqs={faqs}
|
|
152
|
-
onFaqClick={vi.fn()}
|
|
153
|
-
className="faq-list-custom-class"
|
|
154
|
-
/>
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
expect(container.firstElementChild).toHaveClass('faq-list-custom-class')
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
it('renders an avatar only when avatar data is provided', () => {
|
|
161
|
-
const { rerender } = renderWithProviders(
|
|
162
|
-
<FaqList faqs={faqs} onFaqClick={vi.fn()} />
|
|
163
|
-
)
|
|
164
|
-
expect(screen.queryAllByTestId('avatar-ring')).toHaveLength(0)
|
|
165
|
-
|
|
166
|
-
rerender(
|
|
167
|
-
<FaqList faqs={faqs} onFaqClick={vi.fn()} avatarName="Brie Nala" />
|
|
168
|
-
)
|
|
169
|
-
expect(screen.queryAllByTestId('avatar-ring')).toHaveLength(1)
|
|
170
|
-
expect(screen.getByText(getAvatarEmoji('Brie Nala'))).toBeInTheDocument()
|
|
171
|
-
|
|
172
|
-
rerender(
|
|
173
|
-
<FaqList
|
|
174
|
-
faqs={faqs}
|
|
175
|
-
onFaqClick={vi.fn()}
|
|
176
|
-
avatarImage="https://cdn.example.com/avatar.jpg"
|
|
177
|
-
/>
|
|
178
|
-
)
|
|
179
|
-
expect(screen.queryAllByTestId('avatar-ring')).toHaveLength(1)
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('passes the clicked FAQ id and marks only its matching row as loading', () => {
|
|
183
|
-
const onFaqClick = vi.fn()
|
|
184
|
-
renderWithProviders(
|
|
185
|
-
<FaqList faqs={faqs} onFaqClick={onFaqClick} loadingFaqId="second" />
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
const firstButton = screen.getByRole('button', {
|
|
189
|
-
name: 'How do I get started?',
|
|
190
|
-
})
|
|
191
|
-
const secondButton = screen.getByRole('button', {
|
|
192
|
-
name: 'What payment methods do you accept?',
|
|
193
|
-
})
|
|
194
|
-
expect(firstButton).toBeEnabled()
|
|
195
|
-
expect(secondButton).toBeDisabled()
|
|
196
|
-
expect(firstButton).not.toHaveClass('opacity-50', 'cursor-not-allowed')
|
|
197
|
-
expect(secondButton).toHaveClass('opacity-50', 'cursor-not-allowed')
|
|
198
|
-
|
|
199
|
-
fireEvent.click(firstButton)
|
|
200
|
-
fireEvent.click(secondButton)
|
|
201
|
-
expect(onFaqClick).toHaveBeenCalledTimes(1)
|
|
202
|
-
expect(onFaqClick).toHaveBeenCalledWith('first')
|
|
203
|
-
})
|
|
204
|
-
})
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import { renderWithProviders, screen } from '../../../../test/utils'
|
|
5
|
-
|
|
6
|
-
import BubbleTail from './BubbleTail'
|
|
7
|
-
|
|
8
|
-
describe('BubbleTail', () => {
|
|
9
|
-
it('renders the card untouched when the tail is not visible', () => {
|
|
10
|
-
const { container } = renderWithProviders(
|
|
11
|
-
<BubbleTail side="sender" visible={false}>
|
|
12
|
-
<span>card</span>
|
|
13
|
-
</BubbleTail>
|
|
14
|
-
)
|
|
15
|
-
expect(screen.queryByTestId('link-card-tail')).not.toBeInTheDocument()
|
|
16
|
-
expect(container.firstElementChild?.tagName).toBe('SPAN')
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('wraps the card so the tail can hang outside its box', () => {
|
|
20
|
-
renderWithProviders(
|
|
21
|
-
<BubbleTail side="sender" visible>
|
|
22
|
-
<span>card</span>
|
|
23
|
-
</BubbleTail>
|
|
24
|
-
)
|
|
25
|
-
const tail = screen.getByTestId('link-card-tail')
|
|
26
|
-
expect(tail.parentElement?.className).toBe('relative isolate w-fit')
|
|
27
|
-
expect(tail.getAttribute('aria-hidden')).toBe('true')
|
|
28
|
-
// The tail follows the card so it paints over the card's bottom edge.
|
|
29
|
-
expect(screen.getByText('card').nextElementSibling).toBe(tail)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('sits on the dark stop for the sender and stays unmirrored', () => {
|
|
33
|
-
renderWithProviders(
|
|
34
|
-
<BubbleTail side="sender" visible>
|
|
35
|
-
<span>card</span>
|
|
36
|
-
</BubbleTail>
|
|
37
|
-
)
|
|
38
|
-
const tail = screen.getByTestId('link-card-tail')
|
|
39
|
-
expect(tail.getAttribute('data-variant')).toBe('dark')
|
|
40
|
-
expect(tail.className).toContain('messaging-link-card-tail')
|
|
41
|
-
expect(tail.className).not.toContain('messaging-link-card-tail--received')
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('mirrors to the pale stop for the receiver', () => {
|
|
45
|
-
renderWithProviders(
|
|
46
|
-
<BubbleTail side="receiver" visible>
|
|
47
|
-
<span>card</span>
|
|
48
|
-
</BubbleTail>
|
|
49
|
-
)
|
|
50
|
-
const tail = screen.getByTestId('link-card-tail')
|
|
51
|
-
expect(tail.getAttribute('data-variant')).toBe('light')
|
|
52
|
-
expect(tail.className).toContain('messaging-link-card-tail--received')
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('publishes the accent so the tail derives the card surface in CSS', () => {
|
|
56
|
-
renderWithProviders(
|
|
57
|
-
<BubbleTail side="sender" visible accentHex="#7008E7">
|
|
58
|
-
<span>card</span>
|
|
59
|
-
</BubbleTail>
|
|
60
|
-
)
|
|
61
|
-
expect(
|
|
62
|
-
screen.getByTestId('link-card-tail').style.getPropertyValue('--accent')
|
|
63
|
-
).toBe('#7008E7')
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('falls back to the plain bubble fill without an accent', () => {
|
|
67
|
-
renderWithProviders(
|
|
68
|
-
<BubbleTail side="sender" visible>
|
|
69
|
-
<span>card</span>
|
|
70
|
-
</BubbleTail>
|
|
71
|
-
)
|
|
72
|
-
expect(
|
|
73
|
-
screen.getByTestId('link-card-tail').style.getPropertyValue('--accent')
|
|
74
|
-
).toBe('')
|
|
75
|
-
})
|
|
76
|
-
})
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import { renderWithProviders, screen, fireEvent } from '../../../../test/utils'
|
|
5
|
-
|
|
6
|
-
import CardBody from './CardBody'
|
|
7
|
-
|
|
8
|
-
describe('CardBody', () => {
|
|
9
|
-
it('collapses to nothing when there is no text, url or cta', () => {
|
|
10
|
-
const { container } = renderWithProviders(<CardBody variant="light" />)
|
|
11
|
-
expect(container).toBeEmptyDOMElement()
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('treats whitespace-only text as absent', () => {
|
|
15
|
-
const { container } = renderWithProviders(
|
|
16
|
-
<CardBody variant="light" title=" " description=" " url=" " />
|
|
17
|
-
)
|
|
18
|
-
expect(container).toBeEmptyDOMElement()
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('renders the chin when any single slot has content', () => {
|
|
22
|
-
const { container: withUrl } = renderWithProviders(
|
|
23
|
-
<CardBody variant="light" url="tr.ee/a" />
|
|
24
|
-
)
|
|
25
|
-
expect(withUrl).not.toBeEmptyDOMElement()
|
|
26
|
-
|
|
27
|
-
const { container: withDescription } = renderWithProviders(
|
|
28
|
-
<CardBody variant="light" description="Get answers" />
|
|
29
|
-
)
|
|
30
|
-
expect(withDescription).not.toBeEmptyDOMElement()
|
|
31
|
-
|
|
32
|
-
const { container: withCta } = renderWithProviders(
|
|
33
|
-
<CardBody variant="light" cta={{ label: 'View FAQs' }} />
|
|
34
|
-
)
|
|
35
|
-
expect(withCta).not.toBeEmptyDOMElement()
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('shows the placeholder title on dark cards only', () => {
|
|
39
|
-
renderWithProviders(
|
|
40
|
-
<CardBody variant="dark" placeholderTitle="Add a title" />
|
|
41
|
-
)
|
|
42
|
-
expect(screen.getByText('Add a title')).toBeInTheDocument()
|
|
43
|
-
|
|
44
|
-
const { container } = renderWithProviders(
|
|
45
|
-
<CardBody variant="light" placeholderTitle="Add a title" />
|
|
46
|
-
)
|
|
47
|
-
expect(container).toBeEmptyDOMElement()
|
|
48
|
-
})
|
|
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
|
-
it('replaces the variant ink with chinTextColor on every chin line', () => {
|
|
82
|
-
renderWithProviders(
|
|
83
|
-
<CardBody
|
|
84
|
-
variant="dark"
|
|
85
|
-
chinTextColor="#040403"
|
|
86
|
-
title="Brie’s FAQ"
|
|
87
|
-
description="Get answers"
|
|
88
|
-
url="tr.ee/a"
|
|
89
|
-
/>
|
|
90
|
-
)
|
|
91
|
-
const title = screen.getByText('Brie’s FAQ')
|
|
92
|
-
const description = screen.getByText('Get answers')
|
|
93
|
-
const url = screen.getByTestId('link-attachment-url')
|
|
94
|
-
|
|
95
|
-
for (const node of [title, description, url]) {
|
|
96
|
-
expect(node.style.color).toBe('rgb(4, 4, 3)')
|
|
97
|
-
expect(node.className).not.toContain('text-white')
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('truncates the description to one line by default and clamps beyond that', () => {
|
|
102
|
-
renderWithProviders(
|
|
103
|
-
<CardBody variant="light" description="Get answers to every question" />
|
|
104
|
-
)
|
|
105
|
-
const single = screen.getByText('Get answers to every question')
|
|
106
|
-
expect(single.className).toContain('truncate')
|
|
107
|
-
expect(single.style.display).toBe('')
|
|
108
|
-
|
|
109
|
-
renderWithProviders(
|
|
110
|
-
<CardBody
|
|
111
|
-
variant="light"
|
|
112
|
-
description="A long prose body"
|
|
113
|
-
descriptionLines={3}
|
|
114
|
-
/>
|
|
115
|
-
)
|
|
116
|
-
const clamped = screen.getByText('A long prose body')
|
|
117
|
-
expect(clamped.className).not.toContain('truncate')
|
|
118
|
-
expect(clamped.style.display).toBe('-webkit-box')
|
|
119
|
-
expect(clamped.style.getPropertyValue('-webkit-line-clamp')).toBe('3')
|
|
120
|
-
expect(clamped.style.overflow).toBe('hidden')
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('keeps the chin ink while clamping a multi-line description', () => {
|
|
124
|
-
renderWithProviders(
|
|
125
|
-
<CardBody
|
|
126
|
-
variant="dark"
|
|
127
|
-
chinTextColor="#FFFFFF"
|
|
128
|
-
description="A long prose body"
|
|
129
|
-
descriptionLines={2}
|
|
130
|
-
/>
|
|
131
|
-
)
|
|
132
|
-
const clamped = screen.getByText('A long prose body')
|
|
133
|
-
expect(clamped.style.color).toBe('rgb(255, 255, 255)')
|
|
134
|
-
expect(clamped.style.getPropertyValue('-webkit-line-clamp')).toBe('2')
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
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
|
-
)
|
|
142
|
-
|
|
143
|
-
const { container } = renderWithProviders(
|
|
144
|
-
<CardBody
|
|
145
|
-
variant="light"
|
|
146
|
-
url="tr.ee/briemix"
|
|
147
|
-
cta={{ label: 'View FAQs' }}
|
|
148
|
-
/>
|
|
149
|
-
)
|
|
150
|
-
expect(
|
|
151
|
-
container.querySelector('[data-testid="link-attachment-url"]')
|
|
152
|
-
).toBeNull()
|
|
153
|
-
expect(screen.getByText('View FAQs')).toBeInTheDocument()
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('paints the cta against ctaSurface rather than the card variant', () => {
|
|
157
|
-
renderWithProviders(
|
|
158
|
-
<CardBody
|
|
159
|
-
variant="dark"
|
|
160
|
-
ctaSurface="light"
|
|
161
|
-
cta={{ label: 'Play', variant: 'secondary' }}
|
|
162
|
-
/>
|
|
163
|
-
)
|
|
164
|
-
// The audio strip is a light surface under a nominally dark card.
|
|
165
|
-
expect(screen.getByText('Play').className).toContain('bg-black/[0.06]')
|
|
166
|
-
|
|
167
|
-
renderWithProviders(
|
|
168
|
-
<CardBody variant="dark" cta={{ label: 'View FAQs' }} />
|
|
169
|
-
)
|
|
170
|
-
expect(screen.getByText('View FAQs').className).toContain('bg-white')
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('renders the app icon before the title and the trailing action beside it', () => {
|
|
174
|
-
renderWithProviders(
|
|
175
|
-
<CardBody
|
|
176
|
-
variant="dark"
|
|
177
|
-
title="Spotify"
|
|
178
|
-
appIcon={<span data-testid="app-icon">icon</span>}
|
|
179
|
-
trailingAction={<button type="button">Edit</button>}
|
|
180
|
-
/>
|
|
181
|
-
)
|
|
182
|
-
const icon = screen.getByTestId('app-icon')
|
|
183
|
-
const title = screen.getByText('Spotify')
|
|
184
|
-
expect(icon.parentElement?.className).toContain('shrink-0')
|
|
185
|
-
expect(
|
|
186
|
-
icon.compareDocumentPosition(title) & Node.DOCUMENT_POSITION_FOLLOWING
|
|
187
|
-
).toBeTruthy()
|
|
188
|
-
expect(screen.getByText('Edit').parentElement?.className).toContain(
|
|
189
|
-
'shrink-0'
|
|
190
|
-
)
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('omits the title row entirely when there is no title', () => {
|
|
194
|
-
renderWithProviders(
|
|
195
|
-
<CardBody
|
|
196
|
-
variant="light"
|
|
197
|
-
description="Get answers"
|
|
198
|
-
appIcon={<span data-testid="app-icon">icon</span>}
|
|
199
|
-
/>
|
|
200
|
-
)
|
|
201
|
-
expect(screen.queryByTestId('app-icon')).not.toBeInTheDocument()
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
it('renders two variant-aware skeleton bars instead of text while loading', () => {
|
|
205
|
-
const { container: dark } = renderWithProviders(
|
|
206
|
-
<CardBody variant="dark" title="Brie’s FAQ" url="tr.ee/a" isLoading />
|
|
207
|
-
)
|
|
208
|
-
expect(screen.queryByText('Brie’s FAQ')).not.toBeInTheDocument()
|
|
209
|
-
expect(screen.queryByTestId('link-attachment-url')).not.toBeInTheDocument()
|
|
210
|
-
|
|
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')
|
|
226
|
-
}
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
it('renders the skeleton even when there is nothing else to show', () => {
|
|
230
|
-
const { container } = renderWithProviders(
|
|
231
|
-
<CardBody variant="light" isLoading />
|
|
232
|
-
)
|
|
233
|
-
expect(container).not.toBeEmptyDOMElement()
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
it('keeps the cta interactive alongside the chin text', () => {
|
|
237
|
-
const onClick = vi.fn()
|
|
238
|
-
renderWithProviders(
|
|
239
|
-
<CardBody
|
|
240
|
-
variant="light"
|
|
241
|
-
title="FAQ"
|
|
242
|
-
cta={{ label: 'View FAQs', onClick }}
|
|
243
|
-
/>
|
|
244
|
-
)
|
|
245
|
-
fireEvent.click(screen.getByText('View FAQs'))
|
|
246
|
-
expect(onClick).toHaveBeenCalledTimes(1)
|
|
247
|
-
})
|
|
248
|
-
})
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import { renderWithProviders, screen, fireEvent } from '../../../../test/utils'
|
|
5
|
-
|
|
6
|
-
import CardCta from './CardCta'
|
|
7
|
-
|
|
8
|
-
describe('CardCta', () => {
|
|
9
|
-
it('renders an external anchor when the cta carries an href', () => {
|
|
10
|
-
renderWithProviders(
|
|
11
|
-
<CardCta variant="dark" cta={{ label: 'View FAQs', href: 'tr.ee/faq' }} />
|
|
12
|
-
)
|
|
13
|
-
const cta = screen.getByText('View FAQs')
|
|
14
|
-
expect(cta.tagName).toBe('A')
|
|
15
|
-
// Normalized like the shell anchor so a bare host opens externally.
|
|
16
|
-
expect(cta.getAttribute('href')).toBe('https://tr.ee/faq')
|
|
17
|
-
expect(cta.getAttribute('target')).toBe('_blank')
|
|
18
|
-
expect(cta.getAttribute('rel')).toBe('noopener noreferrer')
|
|
19
|
-
expect(cta.className).toContain('no-underline')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('renders a button when the href is missing or unsafe', () => {
|
|
23
|
-
renderWithProviders(<CardCta variant="dark" cta={{ label: 'Plain' }} />)
|
|
24
|
-
expect(screen.getByText('Plain').tagName).toBe('BUTTON')
|
|
25
|
-
expect(screen.getByText('Plain').getAttribute('type')).toBe('button')
|
|
26
|
-
|
|
27
|
-
renderWithProviders(
|
|
28
|
-
<CardCta
|
|
29
|
-
variant="dark"
|
|
30
|
-
cta={{ label: 'Unsafe', href: 'javascript:alert(1)' }}
|
|
31
|
-
/>
|
|
32
|
-
)
|
|
33
|
-
expect(screen.getByText('Unsafe').tagName).toBe('BUTTON')
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it.each([
|
|
37
|
-
['anchor', 'tr.ee/faq'],
|
|
38
|
-
['button', undefined],
|
|
39
|
-
])(
|
|
40
|
-
'fires the cta callback from the %s without bubbling to the card',
|
|
41
|
-
(_shape, href) => {
|
|
42
|
-
const ctaOnClick = vi.fn()
|
|
43
|
-
const cardOnClick = vi.fn()
|
|
44
|
-
renderWithProviders(
|
|
45
|
-
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- stands in for the card anchor the CTA must not bubble to
|
|
46
|
-
<div onClick={cardOnClick}>
|
|
47
|
-
<CardCta
|
|
48
|
-
variant="light"
|
|
49
|
-
cta={{ label: 'Act', href, onClick: ctaOnClick }}
|
|
50
|
-
/>
|
|
51
|
-
</div>
|
|
52
|
-
)
|
|
53
|
-
fireEvent.click(screen.getByText('Act'))
|
|
54
|
-
expect(ctaOnClick).toHaveBeenCalledTimes(1)
|
|
55
|
-
expect(cardOnClick).not.toHaveBeenCalled()
|
|
56
|
-
}
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
it.each([
|
|
60
|
-
['anchor', 'tr.ee/faq'],
|
|
61
|
-
['button', undefined],
|
|
62
|
-
])('tolerates a %s cta with no callback', (_shape, href) => {
|
|
63
|
-
const cardOnClick = vi.fn()
|
|
64
|
-
renderWithProviders(
|
|
65
|
-
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- stands in for the card anchor the CTA must not bubble to
|
|
66
|
-
<div onClick={cardOnClick}>
|
|
67
|
-
<CardCta variant="light" cta={{ label: 'Act', href }} />
|
|
68
|
-
</div>
|
|
69
|
-
)
|
|
70
|
-
fireEvent.click(screen.getByText('Act'))
|
|
71
|
-
expect(cardOnClick).not.toHaveBeenCalled()
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it.each([
|
|
75
|
-
['dark', 'primary', 'bg-white text-[#121110]'],
|
|
76
|
-
['dark', 'secondary', 'bg-white/15 text-white'],
|
|
77
|
-
['light', 'primary', 'bg-[#121110] text-white'],
|
|
78
|
-
['light', 'secondary', 'bg-black/[0.06] text-[#121110]'],
|
|
79
|
-
] as const)(
|
|
80
|
-
'paints the %s surface %s emphasis',
|
|
81
|
-
(variant, emphasis, expected) => {
|
|
82
|
-
renderWithProviders(
|
|
83
|
-
<CardCta variant={variant} cta={{ label: 'Act', variant: emphasis }} />
|
|
84
|
-
)
|
|
85
|
-
expect(screen.getByText('Act').className).toContain(expected)
|
|
86
|
-
}
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
it('defaults to the filled primary emphasis', () => {
|
|
90
|
-
renderWithProviders(<CardCta variant="light" cta={{ label: 'Act' }} />)
|
|
91
|
-
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
|
-
expect(cta.querySelector('span')).toBeNull()
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('sizes to its content and truncates the label when fullWidth is off', () => {
|
|
104
|
-
renderWithProviders(
|
|
105
|
-
<CardCta variant="light" cta={{ label: 'Act' }} fullWidth={false} />
|
|
106
|
-
)
|
|
107
|
-
const label = screen.getByText('Act')
|
|
108
|
-
expect(label.tagName).toBe('SPAN')
|
|
109
|
-
expect(label.className).toContain('min-w-0 truncate')
|
|
110
|
-
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
|
-
})
|
|
123
|
-
})
|