@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.
Files changed (96) hide show
  1. package/dist/{Card-BYJyA83X.js → Card-D7UPq5-g.js} +2 -2
  2. package/dist/{Card-BYJyA83X.js.map → Card-D7UPq5-g.js.map} +1 -1
  3. package/dist/{Card-BplJtZao.cjs → Card-DcBZqMBt.cjs} +2 -2
  4. package/dist/{Card-BplJtZao.cjs.map → Card-DcBZqMBt.cjs.map} +1 -1
  5. package/dist/assets/index.css +1 -1
  6. package/dist/{index-BUC9vqhJ.js → index-CSkr_CO5.js} +2089 -1952
  7. package/dist/index-CSkr_CO5.js.map +1 -0
  8. package/dist/index-CwkY7QwT.cjs +3 -0
  9. package/dist/index-CwkY7QwT.cjs.map +1 -0
  10. package/dist/index.cjs +1 -1
  11. package/dist/index.d.ts +59 -78
  12. package/dist/index.js +26 -30
  13. package/dist/testing.cjs +1 -1
  14. package/dist/testing.cjs.map +1 -1
  15. package/dist/testing.js +2 -2
  16. package/dist/testing.js.map +1 -1
  17. package/package.json +13 -11
  18. package/src/components/AttachmentCard/utils/icons.ts +7 -6
  19. package/src/components/Avatar/Avatar.test.tsx +0 -126
  20. package/src/components/Avatar/getAvatarEmoji.test.ts +0 -39
  21. package/src/components/ChannelList/CustomChannelPreview.test.tsx +3 -410
  22. package/src/components/ChannelList/index.test.tsx +1 -132
  23. package/src/components/ChannelView.stories.tsx +0 -4
  24. package/src/components/ChannelView.test.tsx +3 -9
  25. package/src/components/CustomMessage/CustomMessage.stories.tsx +314 -6
  26. package/src/components/CustomMessage/CustomMessage.test.tsx +2 -56
  27. package/src/components/CustomMessage/MessageTag.stories.tsx +53 -17
  28. package/src/components/CustomMessage/MessageTag.test.tsx +40 -7
  29. package/src/components/CustomMessage/MessageTag.tsx +39 -7
  30. package/src/components/CustomMessage/SentMessageDeliveryStatus.test.tsx +7 -8
  31. package/src/components/CustomMessage/SentMessageDeliveryStatus.tsx +1 -2
  32. package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +33 -0
  33. package/src/components/CustomMessage/StreamAttachmentMessage.tsx +12 -5
  34. package/src/components/CustomMessage/index.tsx +19 -22
  35. package/src/components/CustomMessageInput/index.tsx +1 -1
  36. package/src/components/CustomSystemMessage/index.tsx +2 -3
  37. package/src/components/LinkAttachment/LinkAttachment.test.tsx +0 -327
  38. package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +165 -58
  39. package/src/components/MessageAttachment/Audio/WaveformAudioRow.test.tsx +153 -0
  40. package/src/components/MessageAttachment/Audio/WaveformAudioRow.tsx +450 -0
  41. package/src/components/MessageAttachment/Audio/audioRowRequester.test.ts +24 -0
  42. package/src/components/MessageAttachment/Audio/audioRowRequester.ts +16 -0
  43. package/src/components/MessageAttachment/Audio/index.tsx +83 -77
  44. package/src/components/MessageAttachment/MessageAttachment.test.tsx +176 -74
  45. package/src/components/MessageAttachment/_shared/fileMeta.test.ts +0 -44
  46. package/src/components/MessageAttachment/_shared/useIsClient.ts +23 -0
  47. package/src/components/MessageAttachment/index.tsx +25 -9
  48. package/src/components/MessageAttachment/types.ts +19 -9
  49. package/src/components/MessagingShell/MessagingShell.test.tsx +0 -20
  50. package/src/components/MessagingShell/index.tsx +0 -2
  51. package/src/components/RichCard/RichCard.test.tsx +1 -3
  52. package/src/index.ts +0 -4
  53. package/src/providers/MessagingProvider.test.tsx +2 -130
  54. package/src/providers/MessagingProvider.tsx +15 -26
  55. package/src/styles.css +210 -57
  56. package/src/testing/createMockMessagingClient.ts +2 -2
  57. package/src/types.ts +0 -9
  58. package/dist/index-BUC9vqhJ.js.map +0 -1
  59. package/dist/index-BkqyKPJP.cjs +0 -3
  60. package/dist/index-BkqyKPJP.cjs.map +0 -1
  61. package/src/components/AttachmentCard/MediaPlayer.test.tsx +0 -346
  62. package/src/components/AttachmentCard/Thumbnail.test.tsx +0 -215
  63. package/src/components/AttachmentCard/index.test.tsx +0 -160
  64. package/src/components/ChannelList/ChannelListContext.test.tsx +0 -87
  65. package/src/components/CustomMessage/Locked.stories.tsx +0 -108
  66. package/src/components/CustomMessage/PaidMessage.stories.tsx +0 -42
  67. package/src/components/CustomMessage/Tip.stories.tsx +0 -48
  68. package/src/components/CustomMessage/TipMessage/TipMessage.stories.tsx +0 -51
  69. package/src/components/CustomMessage/TipMessage/TipMessage.test.tsx +0 -65
  70. package/src/components/CustomMessage/TipMessage/index.tsx +0 -27
  71. package/src/components/FaqList/FaqList.test.tsx +0 -204
  72. package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +0 -76
  73. package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +0 -248
  74. package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +0 -123
  75. package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +0 -193
  76. package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +0 -242
  77. package/src/components/LinkAttachment/components/_shared/hex.test.ts +0 -37
  78. package/src/components/LinkAttachment/components/_shared/normalizeExternalHref.test.ts +0 -72
  79. package/src/components/LinkAttachment/components/_shared/useChinPalette.test.ts +0 -162
  80. package/src/components/MessageAttachment/MessageAttachment.behavior.test.tsx +0 -182
  81. package/src/components/MessageAttachment/_shared/Bubble.test.tsx +0 -111
  82. package/src/components/MessageAttachment/_shared/CarouselNav.test.tsx +0 -36
  83. package/src/components/MessageAttachment/_shared/CompactDocumentRow.test.tsx +0 -114
  84. package/src/components/MessageAttachment/_shared/DismissButton.test.tsx +0 -42
  85. package/src/components/MessageAttachment/_shared/DownloadAction.test.tsx +0 -168
  86. package/src/components/MessageAttachment/_shared/ImageViewer.test.tsx +0 -52
  87. package/src/components/MessageAttachment/_shared/MediaStackGrid.test.tsx +0 -69
  88. package/src/components/MessageAttachment/_shared/PdfViewer.test.tsx +0 -45
  89. package/src/components/MessageAttachment/_shared/VideoViewer.test.tsx +0 -55
  90. package/src/components/MessageAttachment/_shared/ViewerShell.test.tsx +0 -246
  91. package/src/components/MessageAttachment/_shared/triggerDownload.test.ts +0 -113
  92. package/src/components/MessageAttachment/_shared/useCarousel.test.tsx +0 -68
  93. package/src/components/MessageAttachment/_shared/viewerChromeClasses.test.ts +0 -16
  94. package/src/logging/index.test.tsx +0 -136
  95. package/src/logging/index.tsx +0 -48
  96. 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
- })