@linktr.ee/messaging-react 4.1.6 → 4.1.7-rc-1787179679

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.
@@ -0,0 +1,215 @@
1
+ import React from 'react'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+
4
+ import { fireEvent, renderWithProviders, screen } from '../../test/utils'
5
+
6
+ import AttachmentThumbnail from './Thumbnail'
7
+
8
+ vi.mock('./MediaPlayer', () => ({
9
+ default: ({
10
+ source,
11
+ mimeType,
12
+ poster,
13
+ controls,
14
+ loop,
15
+ muted,
16
+ }: {
17
+ source: string
18
+ mimeType: string
19
+ poster?: string
20
+ controls?: boolean
21
+ loop?: boolean
22
+ muted?: boolean
23
+ }) => (
24
+ <div
25
+ data-testid="media-player"
26
+ data-source={source}
27
+ data-mime-type={mimeType}
28
+ data-poster={poster}
29
+ data-controls={String(controls)}
30
+ data-loop={String(loop)}
31
+ data-muted={String(muted)}
32
+ />
33
+ ),
34
+ }))
35
+
36
+ const LINKTREE_SOURCE = 'https://cdn.linktr.ee/attachment/photo.jpg'
37
+ const LINKTREE_THUMBNAIL = 'https://cdn.linktr.ee/attachment/poster.jpg'
38
+ const OPTIMIZED_SOURCE =
39
+ 'https://cdn.linktr.ee/attachment/photo.jpg?io=true&size=messaging-attachment-v1_0'
40
+ const OPTIMIZED_THUMBNAIL =
41
+ 'https://cdn.linktr.ee/attachment/poster.jpg?io=true&size=messaging-attachment-v1_0'
42
+
43
+ describe('AttachmentThumbnail', () => {
44
+ it('delegates video and audio sources to MediaPlayer with an optimized poster', () => {
45
+ const { rerender } = renderWithProviders(
46
+ <AttachmentThumbnail
47
+ mimeType="video/mp4"
48
+ sourceUrl={LINKTREE_SOURCE}
49
+ thumbnailUrl={LINKTREE_THUMBNAIL}
50
+ variant="dark"
51
+ mediaPlayerProps={{ controls: false, loop: true, muted: true }}
52
+ />
53
+ )
54
+ const player = screen.getByTestId('media-player')
55
+ expect(player).toHaveAttribute('data-source', LINKTREE_SOURCE)
56
+ expect(player).toHaveAttribute('data-mime-type', 'video/mp4')
57
+ expect(player).toHaveAttribute('data-poster', OPTIMIZED_THUMBNAIL)
58
+ expect(player).toHaveAttribute('data-controls', 'false')
59
+ expect(player).toHaveAttribute('data-loop', 'true')
60
+ expect(player).toHaveAttribute('data-muted', 'true')
61
+
62
+ rerender(
63
+ <AttachmentThumbnail
64
+ mimeType="audio/mpeg"
65
+ sourceUrl="https://external.example/track.mp3"
66
+ variant="light"
67
+ />
68
+ )
69
+ expect(screen.getByTestId('media-player')).toHaveAttribute(
70
+ 'data-source',
71
+ 'https://external.example/track.mp3'
72
+ )
73
+ expect(screen.getByTestId('media-player')).toHaveAttribute(
74
+ 'data-controls',
75
+ 'true'
76
+ )
77
+ })
78
+
79
+ it('renders an image with an optimized source and contained fade-in transition', () => {
80
+ const { container } = renderWithProviders(
81
+ <AttachmentThumbnail
82
+ mimeType="image/jpeg"
83
+ sourceUrl={LINKTREE_SOURCE}
84
+ title="Photo"
85
+ variant="light"
86
+ containedImage
87
+ />
88
+ )
89
+ const image = screen.getByRole('img', { name: 'Photo' })
90
+ expect(image).toHaveAttribute('src', OPTIMIZED_SOURCE)
91
+ expect(image).toHaveClass('opacity-0')
92
+ fireEvent.load(image)
93
+ expect(image).toHaveClass('opacity-100')
94
+ expect(container.firstElementChild).toHaveClass('aspect-video')
95
+ })
96
+
97
+ it('renders a non-contained image and falls back to the raw URL', () => {
98
+ renderWithProviders(
99
+ <AttachmentThumbnail
100
+ mimeType="image/jpeg"
101
+ sourceUrl="https://external.example/photo.jpg"
102
+ variant="light"
103
+ />
104
+ )
105
+ expect(screen.getByRole('img')).toHaveAttribute(
106
+ 'src',
107
+ 'https://external.example/photo.jpg'
108
+ )
109
+ expect(screen.getByRole('img')).toHaveClass('block', 'w-full')
110
+ expect(screen.getByRole('img')).toHaveAttribute('alt', '')
111
+ })
112
+
113
+ it('renders document thumbnails contained or full width, using optimized URLs', () => {
114
+ const { rerender, container } = renderWithProviders(
115
+ <AttachmentThumbnail
116
+ mimeType="application/pdf"
117
+ sourceUrl="https://external.example/document.pdf"
118
+ thumbnailUrl={LINKTREE_THUMBNAIL}
119
+ title="Document"
120
+ variant="dark"
121
+ containedImage
122
+ />
123
+ )
124
+ const contained = screen.getByRole('img', { name: 'Document' })
125
+ expect(contained).toHaveAttribute('src', OPTIMIZED_THUMBNAIL)
126
+ expect(contained).toHaveClass('opacity-0')
127
+ fireEvent.load(contained)
128
+ expect(contained).toHaveClass('opacity-100')
129
+ expect(container.firstElementChild).toHaveClass('aspect-video')
130
+
131
+ rerender(
132
+ <AttachmentThumbnail
133
+ mimeType="application/pdf"
134
+ sourceUrl="https://external.example/document.pdf"
135
+ thumbnailUrl="https://external.example/thumb.png"
136
+ title="Document"
137
+ variant="light"
138
+ />
139
+ )
140
+ const fullWidth = screen.getByRole('img')
141
+ expect(fullWidth).toHaveAttribute(
142
+ 'src',
143
+ 'https://external.example/thumb.png'
144
+ )
145
+ expect(fullWidth).toHaveAttribute('alt', '')
146
+ expect(fullWidth).toHaveClass('block', 'w-full')
147
+ })
148
+
149
+ it('renders a document icon placeholder without a thumbnail', () => {
150
+ const dark = renderWithProviders(
151
+ <AttachmentThumbnail
152
+ mimeType="application/pdf"
153
+ variant="dark"
154
+ title="PDF"
155
+ />
156
+ )
157
+ const darkIcon = dark.container.querySelector('svg') as SVGElement
158
+ expect(darkIcon).toHaveClass('size-12', 'text-white/20')
159
+ expect(dark.container.firstElementChild).toHaveClass('bg-white/10')
160
+ dark.unmount()
161
+
162
+ const light = renderWithProviders(
163
+ <AttachmentThumbnail mimeType="application/pdf" variant="light" />
164
+ )
165
+ const lightIcon = light.container.querySelector('svg') as SVGElement
166
+ expect(lightIcon).toHaveClass('size-12', 'text-black/20')
167
+ expect(light.container.firstElementChild).toHaveClass('bg-black/5')
168
+ })
169
+
170
+ it('renders the poster-only branch with variant-specific shell and alt fallback', () => {
171
+ const dark = renderWithProviders(
172
+ <AttachmentThumbnail
173
+ mimeType="image/jpeg"
174
+ thumbnailUrl={LINKTREE_THUMBNAIL}
175
+ title="Poster"
176
+ variant="dark"
177
+ />
178
+ )
179
+ const darkImage = screen.getByRole('img', { name: 'Poster' })
180
+ expect(darkImage).toHaveAttribute('src', OPTIMIZED_THUMBNAIL)
181
+ expect(dark.container.firstElementChild).toHaveClass(
182
+ 'aspect-video',
183
+ 'bg-white/10'
184
+ )
185
+ dark.unmount()
186
+
187
+ const light = renderWithProviders(
188
+ <AttachmentThumbnail
189
+ mimeType="image/jpeg"
190
+ thumbnailUrl="https://external.example/poster.jpg"
191
+ variant="light"
192
+ />
193
+ )
194
+ expect(screen.getByRole('img')).toHaveAttribute('alt', '')
195
+ expect(light.container.firstElementChild).toHaveClass(
196
+ 'aspect-video',
197
+ 'bg-black/5'
198
+ )
199
+ })
200
+
201
+ it('renders the empty branch with a light placeholder', () => {
202
+ const { container } = renderWithProviders(
203
+ <AttachmentThumbnail
204
+ mimeType="application/octet-stream"
205
+ variant="light"
206
+ />
207
+ )
208
+ expect(container.querySelector('img')).toBeNull()
209
+ expect(container.querySelector('svg')).toHaveClass(
210
+ 'size-12',
211
+ 'text-black/20'
212
+ )
213
+ expect(container.firstElementChild).toHaveClass('bg-black/5')
214
+ })
215
+ })
@@ -0,0 +1,160 @@
1
+ import React from 'react'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { renderWithProviders, screen } from '../../test/utils'
5
+
6
+ import AttachmentCard from './index'
7
+
8
+ const thumbnail = <div data-testid="thumbnail">thumbnail</div>
9
+
10
+ describe('AttachmentCard', () => {
11
+ it('applies the dark and light root surfaces and type icon colors', () => {
12
+ const dark = renderWithProviders(
13
+ <AttachmentCard
14
+ variant="dark"
15
+ thumbnail={thumbnail}
16
+ mimeType="application/pdf"
17
+ title="Dark file"
18
+ />
19
+ )
20
+ expect(screen.getByText('Dark file')).toBeInTheDocument()
21
+ expect(dark.container.firstElementChild).toHaveClass('bg-[#1e2330]')
22
+ expect(dark.container.querySelector('svg')).toHaveClass('text-white/55')
23
+ dark.unmount()
24
+
25
+ const light = renderWithProviders(
26
+ <AttachmentCard
27
+ variant="light"
28
+ thumbnail={thumbnail}
29
+ mimeType="application/pdf"
30
+ title="Light file"
31
+ />
32
+ )
33
+ expect(light.container.firstElementChild).toHaveClass('bg-white')
34
+ expect(light.container.querySelector('svg')).toHaveClass('text-black/55')
35
+ })
36
+
37
+ it('uses the dark placeholder title and dims it when title is absent', () => {
38
+ renderWithProviders(
39
+ <AttachmentCard
40
+ variant="dark"
41
+ thumbnail={thumbnail}
42
+ mimeType="application/pdf"
43
+ placeholderTitle="Waiting for title"
44
+ />
45
+ )
46
+ expect(screen.getByText('Waiting for title')).toHaveClass('text-white/30')
47
+ })
48
+
49
+ it('does not render a light title for missing or whitespace-only values', () => {
50
+ const { rerender } = renderWithProviders(
51
+ <AttachmentCard
52
+ variant="light"
53
+ thumbnail={thumbnail}
54
+ mimeType="application/pdf"
55
+ />
56
+ )
57
+ expect(screen.queryByText('Attachment title')).toBeNull()
58
+
59
+ rerender(
60
+ <AttachmentCard
61
+ variant="light"
62
+ thumbnail={thumbnail}
63
+ mimeType="application/pdf"
64
+ title=" "
65
+ />
66
+ )
67
+ expect(screen.queryByText(' ')).toBeNull()
68
+ })
69
+
70
+ it('uses an undimmed title class when a dark title is provided', () => {
71
+ renderWithProviders(
72
+ <AttachmentCard
73
+ variant="dark"
74
+ thumbnail={thumbnail}
75
+ mimeType="application/pdf"
76
+ title="Named file"
77
+ />
78
+ )
79
+ expect(screen.getByText('Named file')).toHaveClass('text-white')
80
+ expect(screen.getByText('Named file')).not.toHaveClass('text-white/30')
81
+ })
82
+
83
+ it('renders detail only for non-empty values', () => {
84
+ const { rerender } = renderWithProviders(
85
+ <AttachmentCard
86
+ variant="light"
87
+ thumbnail={thumbnail}
88
+ mimeType="application/pdf"
89
+ title="File"
90
+ detail="2 MB"
91
+ />
92
+ )
93
+ expect(screen.getByText('2 MB')).toBeInTheDocument()
94
+
95
+ rerender(
96
+ <AttachmentCard
97
+ variant="light"
98
+ thumbnail={thumbnail}
99
+ mimeType="application/pdf"
100
+ title="File"
101
+ detail=""
102
+ />
103
+ )
104
+ expect(screen.queryByText('2 MB')).toBeNull()
105
+ })
106
+
107
+ it('renders optional slots only when supplied', () => {
108
+ const { container } = renderWithProviders(
109
+ <AttachmentCard
110
+ variant="light"
111
+ thumbnail={thumbnail}
112
+ mimeType="application/pdf"
113
+ statusBadge={<span>Ready</span>}
114
+ action={<button type="button">Open</button>}
115
+ topLeft={<span>Left</span>}
116
+ topRight={<span>Right</span>}
117
+ />
118
+ )
119
+ expect(screen.getByText('Ready')).toBeInTheDocument()
120
+ expect(screen.getByRole('button', { name: 'Open' })).toBeInTheDocument()
121
+ expect(screen.getByText('Left').parentElement).toHaveClass(
122
+ 'pointer-events-auto',
123
+ 'absolute',
124
+ 'left-3',
125
+ 'top-3'
126
+ )
127
+ expect(screen.getByText('Right').parentElement).toHaveClass(
128
+ 'pointer-events-auto',
129
+ 'absolute',
130
+ 'right-3',
131
+ 'top-3'
132
+ )
133
+ expect(container.querySelectorAll('.pointer-events-auto')).toHaveLength(2)
134
+
135
+ const withoutSlots = renderWithProviders(
136
+ <AttachmentCard
137
+ variant="light"
138
+ thumbnail={thumbnail}
139
+ mimeType="application/pdf"
140
+ />
141
+ )
142
+ expect(
143
+ withoutSlots.container.querySelectorAll('.pointer-events-auto')
144
+ ).toHaveLength(0)
145
+ })
146
+
147
+ it('forwards rootRef and data-testid to the root', () => {
148
+ const rootRef = React.createRef<HTMLDivElement>()
149
+ renderWithProviders(
150
+ <AttachmentCard
151
+ variant="light"
152
+ thumbnail={thumbnail}
153
+ mimeType="application/pdf"
154
+ rootRef={rootRef}
155
+ data-testid="attachment-card"
156
+ />
157
+ )
158
+ expect(screen.getByTestId('attachment-card')).toBe(rootRef.current)
159
+ })
160
+ })
@@ -0,0 +1,87 @@
1
+ import type { Channel } from 'stream-chat'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+
4
+ import { fireEvent, renderWithProviders, screen } from '../../test/utils'
5
+
6
+ import {
7
+ ChannelListProvider,
8
+ useChannelListContext,
9
+ } from './ChannelListContext'
10
+
11
+ const channel = { id: 'channel-1' } as Channel
12
+
13
+ function ContextConsumer() {
14
+ const value = useChannelListContext()
15
+
16
+ return (
17
+ <div>
18
+ <span data-testid="selected-channel">
19
+ {value.selectedChannel?.id ?? 'undefined'}
20
+ </span>
21
+ <span data-testid="debug">{String(value.debug)}</span>
22
+ <span data-testid="render-message-preview">
23
+ {value.renderMessagePreview ? 'provided' : 'undefined'}
24
+ </span>
25
+ <span data-testid="channel-preview">
26
+ {value.channelPreview ? 'provided' : 'undefined'}
27
+ </span>
28
+ <span data-testid="viewer-language">
29
+ {value.viewerLanguage ?? 'undefined'}
30
+ </span>
31
+ <button type="button" onClick={() => value.onChannelSelect(channel)}>
32
+ Select channel
33
+ </button>
34
+ </div>
35
+ )
36
+ }
37
+
38
+ describe('ChannelListContext', () => {
39
+ it('provides safe documented defaults without a provider', () => {
40
+ renderWithProviders(<ContextConsumer />)
41
+
42
+ expect(screen.getByTestId('selected-channel')).toHaveTextContent(
43
+ 'undefined'
44
+ )
45
+ expect(screen.getByTestId('debug')).toHaveTextContent('false')
46
+ expect(screen.getByTestId('render-message-preview')).toHaveTextContent(
47
+ 'undefined'
48
+ )
49
+ expect(screen.getByTestId('channel-preview')).toHaveTextContent('undefined')
50
+ expect(screen.getByTestId('viewer-language')).toHaveTextContent('undefined')
51
+ expect(() => fireEvent.click(screen.getByRole('button'))).not.toThrow()
52
+ })
53
+
54
+ it('exposes the value supplied by a provider', () => {
55
+ const onChannelSelect = vi.fn()
56
+ const ChannelPreview = () => null
57
+ const renderMessagePreview = () => 'preview'
58
+
59
+ renderWithProviders(
60
+ <ChannelListProvider
61
+ value={{
62
+ selectedChannel: channel,
63
+ onChannelSelect,
64
+ debug: true,
65
+ renderMessagePreview,
66
+ channelPreview: ChannelPreview,
67
+ viewerLanguage: 'es',
68
+ }}
69
+ >
70
+ <ContextConsumer />
71
+ </ChannelListProvider>
72
+ )
73
+
74
+ expect(screen.getByTestId('selected-channel')).toHaveTextContent(
75
+ 'channel-1'
76
+ )
77
+ expect(screen.getByTestId('debug')).toHaveTextContent('true')
78
+ expect(screen.getByTestId('render-message-preview')).toHaveTextContent(
79
+ 'provided'
80
+ )
81
+ expect(screen.getByTestId('channel-preview')).toHaveTextContent('provided')
82
+ expect(screen.getByTestId('viewer-language')).toHaveTextContent('es')
83
+
84
+ fireEvent.click(screen.getByRole('button'))
85
+ expect(onChannelSelect).toHaveBeenCalledWith(channel)
86
+ })
87
+ })