@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,193 +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 CardShell, { LOADING_LABEL } from './CardShell'
|
|
7
|
-
|
|
8
|
-
const shellOf = (testId = 'shell') => screen.getByTestId(testId)
|
|
9
|
-
|
|
10
|
-
describe('CardShell', () => {
|
|
11
|
-
it('renders a non-interactive div when neither href nor onClick is set', () => {
|
|
12
|
-
renderWithProviders(
|
|
13
|
-
<CardShell variant="light" data-testid="shell">
|
|
14
|
-
body
|
|
15
|
-
</CardShell>
|
|
16
|
-
)
|
|
17
|
-
const shell = shellOf()
|
|
18
|
-
expect(shell.tagName).toBe('DIV')
|
|
19
|
-
expect(shell.className).not.toContain('cursor-pointer')
|
|
20
|
-
expect(shell.className).not.toContain('focus-ring')
|
|
21
|
-
expect(shell.getAttribute('data-variant')).toBe('light')
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('renders an external anchor when href is set and fires onClick alongside navigation', () => {
|
|
25
|
-
const onClick = vi.fn()
|
|
26
|
-
renderWithProviders(
|
|
27
|
-
<CardShell
|
|
28
|
-
variant="dark"
|
|
29
|
-
href="https://tr.ee/briemix"
|
|
30
|
-
onClick={onClick}
|
|
31
|
-
data-testid="shell"
|
|
32
|
-
>
|
|
33
|
-
body
|
|
34
|
-
</CardShell>
|
|
35
|
-
)
|
|
36
|
-
const shell = shellOf()
|
|
37
|
-
expect(shell.tagName).toBe('A')
|
|
38
|
-
expect(shell.getAttribute('href')).toBe('https://tr.ee/briemix')
|
|
39
|
-
expect(shell.getAttribute('target')).toBe('_blank')
|
|
40
|
-
expect(shell.getAttribute('rel')).toBe('noopener noreferrer')
|
|
41
|
-
expect(shell.className).toContain('cursor-pointer')
|
|
42
|
-
expect(shell.className).toContain('no-underline')
|
|
43
|
-
expect(shell.className).toContain('focus-ring')
|
|
44
|
-
fireEvent.click(shell)
|
|
45
|
-
expect(onClick).toHaveBeenCalledTimes(1)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('renders a labelled button when onClick is set without href', () => {
|
|
49
|
-
const onClick = vi.fn()
|
|
50
|
-
renderWithProviders(
|
|
51
|
-
<CardShell
|
|
52
|
-
variant="light"
|
|
53
|
-
onClick={onClick}
|
|
54
|
-
ariaLabel="Open attachment preview"
|
|
55
|
-
data-testid="shell"
|
|
56
|
-
>
|
|
57
|
-
body
|
|
58
|
-
</CardShell>
|
|
59
|
-
)
|
|
60
|
-
const shell = shellOf()
|
|
61
|
-
expect(shell.tagName).toBe('BUTTON')
|
|
62
|
-
expect(shell.getAttribute('type')).toBe('button')
|
|
63
|
-
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
|
-
fireEvent.click(shell)
|
|
67
|
-
expect(onClick).toHaveBeenCalledTimes(1)
|
|
68
|
-
})
|
|
69
|
-
|
|
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
|
-
it('locks to the 250px column only when fixedHeight is set', () => {
|
|
98
|
-
renderWithProviders(
|
|
99
|
-
<CardShell variant="dark" fixedHeight data-testid="fixed">
|
|
100
|
-
body
|
|
101
|
-
</CardShell>
|
|
102
|
-
)
|
|
103
|
-
expect(shellOf('fixed').className).toContain('flex h-[250px] flex-col')
|
|
104
|
-
expect(shellOf('fixed').className).not.toContain('block')
|
|
105
|
-
|
|
106
|
-
renderWithProviders(
|
|
107
|
-
<CardShell variant="dark" data-testid="intrinsic">
|
|
108
|
-
body
|
|
109
|
-
</CardShell>
|
|
110
|
-
)
|
|
111
|
-
expect(shellOf('intrinsic').className).toContain('block')
|
|
112
|
-
expect(shellOf('intrinsic').className).not.toContain('h-[250px]')
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('exposes a labelled busy region only while busy', () => {
|
|
116
|
-
renderWithProviders(
|
|
117
|
-
<CardShell variant="dark" busy data-testid="busy">
|
|
118
|
-
body
|
|
119
|
-
</CardShell>
|
|
120
|
-
)
|
|
121
|
-
const busy = shellOf('busy')
|
|
122
|
-
expect(busy.getAttribute('role')).toBe('status')
|
|
123
|
-
expect(busy.getAttribute('aria-busy')).toBe('true')
|
|
124
|
-
expect(busy.getAttribute('aria-label')).toBe(LOADING_LABEL)
|
|
125
|
-
expect(LOADING_LABEL).toBe('Loading link preview')
|
|
126
|
-
|
|
127
|
-
renderWithProviders(
|
|
128
|
-
<CardShell variant="dark" data-testid="ready">
|
|
129
|
-
body
|
|
130
|
-
</CardShell>
|
|
131
|
-
)
|
|
132
|
-
const ready = shellOf('ready')
|
|
133
|
-
expect(ready.getAttribute('role')).toBeNull()
|
|
134
|
-
expect(ready.getAttribute('aria-busy')).toBeNull()
|
|
135
|
-
expect(ready.getAttribute('aria-label')).toBeNull()
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('publishes the accent as a custom property, and nothing without one', () => {
|
|
139
|
-
renderWithProviders(
|
|
140
|
-
<CardShell variant="dark" accentHex="#7008E7" data-testid="accented">
|
|
141
|
-
body
|
|
142
|
-
</CardShell>
|
|
143
|
-
)
|
|
144
|
-
expect(shellOf('accented').style.getPropertyValue('--accent')).toBe(
|
|
145
|
-
'#7008E7'
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
renderWithProviders(
|
|
149
|
-
<CardShell variant="dark" data-testid="plain">
|
|
150
|
-
body
|
|
151
|
-
</CardShell>
|
|
152
|
-
)
|
|
153
|
-
expect(shellOf('plain').style.getPropertyValue('--accent')).toBe('')
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('renders the topRight slot pinned to the corner above the content', () => {
|
|
157
|
-
renderWithProviders(
|
|
158
|
-
<CardShell
|
|
159
|
-
variant="dark"
|
|
160
|
-
topRight={<button type="button">Dismiss</button>}
|
|
161
|
-
data-testid="shell"
|
|
162
|
-
>
|
|
163
|
-
body
|
|
164
|
-
</CardShell>
|
|
165
|
-
)
|
|
166
|
-
const corner = screen.getByText('Dismiss').parentElement
|
|
167
|
-
expect(corner?.className).toContain('absolute right-3 top-3')
|
|
168
|
-
expect(corner?.className).toContain('z-10')
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('omits the corner wrapper when no topRight is provided', () => {
|
|
172
|
-
const { container } = renderWithProviders(
|
|
173
|
-
<CardShell variant="dark" data-testid="shell">
|
|
174
|
-
body
|
|
175
|
-
</CardShell>
|
|
176
|
-
)
|
|
177
|
-
expect(container.querySelectorAll('div.absolute')).toHaveLength(0)
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
it.each([
|
|
181
|
-
['anchor', { href: 'https://tr.ee/a' }],
|
|
182
|
-
['button', { onClick: () => {} }],
|
|
183
|
-
['div', {}],
|
|
184
|
-
])('forwards rootRef for the %s shape', (_shape, props) => {
|
|
185
|
-
const ref = React.createRef<HTMLElement>()
|
|
186
|
-
renderWithProviders(
|
|
187
|
-
<CardShell variant="dark" rootRef={ref} data-testid="shell" {...props}>
|
|
188
|
-
body
|
|
189
|
-
</CardShell>
|
|
190
|
-
)
|
|
191
|
-
expect(ref.current).toBe(shellOf())
|
|
192
|
-
})
|
|
193
|
-
})
|
|
@@ -1,242 +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 CardThumbnail, {
|
|
7
|
-
AUDIO_BG_CLASS,
|
|
8
|
-
isPlayableAudio,
|
|
9
|
-
isPlayableMedia,
|
|
10
|
-
} from './CardThumbnail'
|
|
11
|
-
|
|
12
|
-
const AUDIO_SRC = 'https://cdn.example.com/clip.mp3'
|
|
13
|
-
const VIDEO_SRC = 'https://cdn.example.com/clip.mp4'
|
|
14
|
-
const IMAGE_SRC = 'https://cdn.example.com/cover.jpg'
|
|
15
|
-
|
|
16
|
-
describe('isPlayableAudio', () => {
|
|
17
|
-
it('needs both an audio mime type and a source url', () => {
|
|
18
|
-
expect(isPlayableAudio('audio/mpeg', AUDIO_SRC)).toBe(true)
|
|
19
|
-
expect(isPlayableAudio('audio/mpeg', undefined)).toBe(false)
|
|
20
|
-
expect(isPlayableAudio('audio/mpeg', '')).toBe(false)
|
|
21
|
-
expect(isPlayableAudio(undefined, AUDIO_SRC)).toBe(false)
|
|
22
|
-
expect(isPlayableAudio('', AUDIO_SRC)).toBe(false)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('rejects non-audio mime types', () => {
|
|
26
|
-
expect(isPlayableAudio('video/mp4', VIDEO_SRC)).toBe(false)
|
|
27
|
-
expect(isPlayableAudio('image/png', IMAGE_SRC)).toBe(false)
|
|
28
|
-
expect(isPlayableAudio('application/pdf', IMAGE_SRC)).toBe(false)
|
|
29
|
-
})
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
describe('isPlayableMedia', () => {
|
|
33
|
-
it('covers both playable shapes and nothing else', () => {
|
|
34
|
-
expect(isPlayableMedia('video/mp4', VIDEO_SRC)).toBe(true)
|
|
35
|
-
expect(isPlayableMedia('audio/mpeg', AUDIO_SRC)).toBe(true)
|
|
36
|
-
expect(isPlayableMedia('image/png', IMAGE_SRC)).toBe(false)
|
|
37
|
-
expect(isPlayableMedia('application/pdf', IMAGE_SRC)).toBe(false)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('needs both halves of the pair', () => {
|
|
41
|
-
expect(isPlayableMedia('video/mp4', undefined)).toBe(false)
|
|
42
|
-
expect(isPlayableMedia('video/mp4', '')).toBe(false)
|
|
43
|
-
expect(isPlayableMedia(undefined, VIDEO_SRC)).toBe(false)
|
|
44
|
-
expect(isPlayableMedia('', VIDEO_SRC)).toBe(false)
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
describe('AUDIO_BG_CLASS', () => {
|
|
49
|
-
it('is a verbatim literal so Tailwind can find it when scanning dist', () => {
|
|
50
|
-
expect(AUDIO_BG_CLASS).toBe('bg-[#F2F3F4]')
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
describe('CardThumbnail', () => {
|
|
55
|
-
it('collapses the hero to a padded native audio player', () => {
|
|
56
|
-
const { container } = renderWithProviders(
|
|
57
|
-
<CardThumbnail
|
|
58
|
-
variant="dark"
|
|
59
|
-
mimeType="audio/mpeg"
|
|
60
|
-
sourceUrl={AUDIO_SRC}
|
|
61
|
-
thumbnailUrl={IMAGE_SRC}
|
|
62
|
-
/>
|
|
63
|
-
)
|
|
64
|
-
const audio = container.querySelector('audio')
|
|
65
|
-
expect(audio).not.toBeNull()
|
|
66
|
-
expect(audio?.getAttribute('src')).toBe(AUDIO_SRC)
|
|
67
|
-
expect(audio?.hasAttribute('controls')).toBe(true)
|
|
68
|
-
expect(audio?.getAttribute('preload')).toBe('metadata')
|
|
69
|
-
// The hero region (and its artwork) is gone entirely on this branch.
|
|
70
|
-
expect(container.querySelectorAll('img')).toHaveLength(0)
|
|
71
|
-
expect(container.querySelector('div.flex-1')).toBeNull()
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('keeps the skeleton instead of an audio player while loading', () => {
|
|
75
|
-
const { container } = renderWithProviders(
|
|
76
|
-
<CardThumbnail
|
|
77
|
-
variant="dark"
|
|
78
|
-
mimeType="audio/mpeg"
|
|
79
|
-
sourceUrl={AUDIO_SRC}
|
|
80
|
-
isLoading
|
|
81
|
-
/>
|
|
82
|
-
)
|
|
83
|
-
expect(container.querySelector('audio')).toBeNull()
|
|
84
|
-
expect(container.querySelector('div.animate-pulse')).not.toBeNull()
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('renders a video player with the poster over a black hero', () => {
|
|
88
|
-
const { container } = renderWithProviders(
|
|
89
|
-
<CardThumbnail
|
|
90
|
-
variant="light"
|
|
91
|
-
mimeType="video/mp4"
|
|
92
|
-
sourceUrl={VIDEO_SRC}
|
|
93
|
-
thumbnailUrl={IMAGE_SRC}
|
|
94
|
-
title="Clip"
|
|
95
|
-
onImageError={() => {}}
|
|
96
|
-
/>
|
|
97
|
-
)
|
|
98
|
-
const video = container.querySelector('video')
|
|
99
|
-
expect(video?.getAttribute('src')).toBe(VIDEO_SRC)
|
|
100
|
-
expect(video?.getAttribute('poster')).toBe(IMAGE_SRC)
|
|
101
|
-
expect(video?.hasAttribute('controls')).toBe(true)
|
|
102
|
-
expect(video?.getAttribute('preload')).toBe('metadata')
|
|
103
|
-
expect(container.querySelector('div.flex-1')?.className).toContain(
|
|
104
|
-
'bg-black'
|
|
105
|
-
)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('probes the video poster with a hidden image that reports failures', () => {
|
|
109
|
-
const onImageError = vi.fn()
|
|
110
|
-
const { container } = renderWithProviders(
|
|
111
|
-
<CardThumbnail
|
|
112
|
-
variant="light"
|
|
113
|
-
mimeType="video/mp4"
|
|
114
|
-
sourceUrl={VIDEO_SRC}
|
|
115
|
-
thumbnailUrl={IMAGE_SRC}
|
|
116
|
-
onImageError={onImageError}
|
|
117
|
-
/>
|
|
118
|
-
)
|
|
119
|
-
const probe = container.querySelector('img')
|
|
120
|
-
expect(probe).not.toBeNull()
|
|
121
|
-
expect(probe?.className).toContain('hidden')
|
|
122
|
-
expect(probe?.getAttribute('aria-hidden')).toBe('true')
|
|
123
|
-
fireEvent.error(probe as HTMLImageElement)
|
|
124
|
-
expect(onImageError).toHaveBeenCalledTimes(1)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('skips the poster probe when there is nothing to report to', () => {
|
|
128
|
-
const { container: noHandler } = renderWithProviders(
|
|
129
|
-
<CardThumbnail
|
|
130
|
-
variant="light"
|
|
131
|
-
mimeType="video/mp4"
|
|
132
|
-
sourceUrl={VIDEO_SRC}
|
|
133
|
-
thumbnailUrl={IMAGE_SRC}
|
|
134
|
-
/>
|
|
135
|
-
)
|
|
136
|
-
expect(noHandler.querySelectorAll('img')).toHaveLength(0)
|
|
137
|
-
|
|
138
|
-
const { container: noPoster } = renderWithProviders(
|
|
139
|
-
<CardThumbnail
|
|
140
|
-
variant="light"
|
|
141
|
-
mimeType="video/mp4"
|
|
142
|
-
sourceUrl={VIDEO_SRC}
|
|
143
|
-
onImageError={() => {}}
|
|
144
|
-
/>
|
|
145
|
-
)
|
|
146
|
-
expect(noPoster.querySelectorAll('img')).toHaveLength(0)
|
|
147
|
-
expect(noPoster.querySelector('video')?.getAttribute('poster')).toBeNull()
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('renders the thumbnail image with the title as its alt text', () => {
|
|
151
|
-
const { container } = renderWithProviders(
|
|
152
|
-
<CardThumbnail
|
|
153
|
-
variant="light"
|
|
154
|
-
thumbnailUrl={IMAGE_SRC}
|
|
155
|
-
title="Cover art"
|
|
156
|
-
/>
|
|
157
|
-
)
|
|
158
|
-
const image = container.querySelector('img')
|
|
159
|
-
expect(image?.getAttribute('src')).toBe(IMAGE_SRC)
|
|
160
|
-
expect(image?.getAttribute('alt')).toBe('Cover art')
|
|
161
|
-
expect(image?.getAttribute('loading')).toBe('lazy')
|
|
162
|
-
expect(image?.getAttribute('decoding')).toBe('async')
|
|
163
|
-
expect(image?.getAttribute('draggable')).toBe('false')
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('falls back to empty alt text when the card has no title', () => {
|
|
167
|
-
const { container } = renderWithProviders(
|
|
168
|
-
<CardThumbnail variant="light" thumbnailUrl={IMAGE_SRC} />
|
|
169
|
-
)
|
|
170
|
-
expect(container.querySelector('img')?.getAttribute('alt')).toBe('')
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('reports a failing hero image to its owner', () => {
|
|
174
|
-
const onImageError = vi.fn()
|
|
175
|
-
const { container } = renderWithProviders(
|
|
176
|
-
<CardThumbnail
|
|
177
|
-
variant="light"
|
|
178
|
-
thumbnailUrl={IMAGE_SRC}
|
|
179
|
-
onImageError={onImageError}
|
|
180
|
-
/>
|
|
181
|
-
)
|
|
182
|
-
fireEvent.error(container.querySelector('img') as HTMLImageElement)
|
|
183
|
-
expect(onImageError).toHaveBeenCalledTimes(1)
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
it('shows a variant-aware type-icon placeholder without a thumbnail', () => {
|
|
187
|
-
const { container: dark } = renderWithProviders(
|
|
188
|
-
<CardThumbnail variant="dark" mimeType="application/pdf" />
|
|
189
|
-
)
|
|
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
|
-
)
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
it('pulses a variant-aware block while loading, ignoring the thumbnail', () => {
|
|
208
|
-
const { container: dark } = renderWithProviders(
|
|
209
|
-
<CardThumbnail variant="dark" thumbnailUrl={IMAGE_SRC} isLoading />
|
|
210
|
-
)
|
|
211
|
-
expect(dark.querySelectorAll('img')).toHaveLength(0)
|
|
212
|
-
const bar = dark.querySelector('div.animate-pulse')
|
|
213
|
-
expect(bar?.className).toContain('bg-white/10')
|
|
214
|
-
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
|
-
})
|
|
223
|
-
|
|
224
|
-
it('pins the corner slots and omits their wrappers when unused', () => {
|
|
225
|
-
renderWithProviders(
|
|
226
|
-
<CardThumbnail
|
|
227
|
-
variant="dark"
|
|
228
|
-
topLeft={<span>left</span>}
|
|
229
|
-
topRight={<span>right</span>}
|
|
230
|
-
/>
|
|
231
|
-
)
|
|
232
|
-
expect(screen.getByText('left').parentElement?.className).toContain(
|
|
233
|
-
'absolute left-3 top-3'
|
|
234
|
-
)
|
|
235
|
-
expect(screen.getByText('right').parentElement?.className).toContain(
|
|
236
|
-
'absolute right-3 top-3'
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
const { container } = renderWithProviders(<CardThumbnail variant="dark" />)
|
|
240
|
-
expect(container.querySelectorAll('div.absolute')).toHaveLength(0)
|
|
241
|
-
})
|
|
242
|
-
})
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { HEX_PATTERN, toCssHex } from './hex'
|
|
4
|
-
|
|
5
|
-
describe('HEX_PATTERN', () => {
|
|
6
|
-
it.each(['#7008E7', '7008e7', '#abc', 'ABC'])('accepts %s', (value) => {
|
|
7
|
-
expect(HEX_PATTERN.test(value)).toBe(true)
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it.each([
|
|
11
|
-
'#7008E', // 5 digits — neither shorthand nor full
|
|
12
|
-
'#ab', // too short for the shorthand
|
|
13
|
-
'#abcz', // trailing non-hex character
|
|
14
|
-
'x#abc', // hex embedded in a longer value
|
|
15
|
-
'#abc ', // untrimmed
|
|
16
|
-
'rebeccapurple', // CSS would resolve it; the extractor never emits it
|
|
17
|
-
'rgb(1,2,3)',
|
|
18
|
-
'',
|
|
19
|
-
])('rejects %s', (value) => {
|
|
20
|
-
expect(HEX_PATTERN.test(value)).toBe(false)
|
|
21
|
-
})
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
describe('toCssHex', () => {
|
|
25
|
-
it('adds the missing hash to a bare hex', () => {
|
|
26
|
-
expect(toCssHex('7008E7')).toBe('#7008E7')
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('leaves an already-hashed hex alone', () => {
|
|
30
|
-
expect(toCssHex('#7008E7')).toBe('#7008E7')
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('trims before hashing so the value is usable as a CSS colour', () => {
|
|
34
|
-
expect(toCssHex(' 7008E7 ')).toBe('#7008E7')
|
|
35
|
-
expect(toCssHex(' #7008E7 ')).toBe('#7008E7')
|
|
36
|
-
})
|
|
37
|
-
})
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { normalizeExternalHref } from './normalizeExternalHref'
|
|
4
|
-
|
|
5
|
-
describe('normalizeExternalHref', () => {
|
|
6
|
-
it('prefixes a bare hostname so it opens externally', () => {
|
|
7
|
-
expect(normalizeExternalHref('tr.ee/briemix')).toBe('https://tr.ee/briemix')
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it('trims surrounding whitespace before prefixing', () => {
|
|
11
|
-
expect(normalizeExternalHref(' tr.ee/briemix ')).toBe(
|
|
12
|
-
'https://tr.ee/briemix'
|
|
13
|
-
)
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('returns undefined for missing, non-string and blank values', () => {
|
|
17
|
-
expect(normalizeExternalHref()).toBeUndefined()
|
|
18
|
-
expect(normalizeExternalHref(undefined)).toBeUndefined()
|
|
19
|
-
expect(
|
|
20
|
-
normalizeExternalHref(42 as unknown as string | undefined)
|
|
21
|
-
).toBeUndefined()
|
|
22
|
-
expect(normalizeExternalHref('')).toBeUndefined()
|
|
23
|
-
expect(normalizeExternalHref(' ')).toBeUndefined()
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it.each([
|
|
27
|
-
'http://example.com/a',
|
|
28
|
-
'https://example.com/a',
|
|
29
|
-
'mailto:hello@example.com',
|
|
30
|
-
'tel:+61400000000',
|
|
31
|
-
'sms:+61400000000',
|
|
32
|
-
])('passes the safe scheme %s through untouched', (value) => {
|
|
33
|
-
expect(normalizeExternalHref(value)).toBe(value)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('accepts a safe scheme regardless of case', () => {
|
|
37
|
-
expect(normalizeExternalHref('HTTPS://example.com')).toBe(
|
|
38
|
-
'HTTPS://example.com'
|
|
39
|
-
)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it.each([
|
|
43
|
-
'javascript:alert(1)',
|
|
44
|
-
'data:text/html;base64,PHNjcmlwdD4=',
|
|
45
|
-
'ftp://example.com/a',
|
|
46
|
-
'file:///etc/passwd',
|
|
47
|
-
])('rejects the unsafe scheme %s', (value) => {
|
|
48
|
-
expect(normalizeExternalHref(value)).toBeUndefined()
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('only reads a scheme at the start of the value', () => {
|
|
52
|
-
// A nested `https:` inside a query string is not this value's scheme, so
|
|
53
|
-
// the bare host still gets prefixed.
|
|
54
|
-
expect(normalizeExternalHref('tr.ee/a?next=https://example.com')).toBe(
|
|
55
|
-
'https://tr.ee/a?next=https://example.com'
|
|
56
|
-
)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('keeps protocol-relative and root-relative paths as-is', () => {
|
|
60
|
-
expect(normalizeExternalHref('//cdn.example.com/a.png')).toBe(
|
|
61
|
-
'//cdn.example.com/a.png'
|
|
62
|
-
)
|
|
63
|
-
expect(normalizeExternalHref('/local/path')).toBe('/local/path')
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('prefixes hosts that merely end in a slash', () => {
|
|
67
|
-
// The leading-slash checks are prefix tests: a trailing slash is still a
|
|
68
|
-
// bare host and must be prefixed.
|
|
69
|
-
expect(normalizeExternalHref('example.com/')).toBe('https://example.com/')
|
|
70
|
-
expect(normalizeExternalHref('example.com//')).toBe('https://example.com//')
|
|
71
|
-
})
|
|
72
|
-
})
|