@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.
- 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 +2 -2
- package/src/components/AttachmentCard/MediaPlayer.test.tsx +346 -0
- package/src/components/AttachmentCard/Thumbnail.test.tsx +215 -0
- package/src/components/AttachmentCard/index.test.tsx +160 -0
- package/src/components/ChannelList/ChannelListContext.test.tsx +87 -0
- package/src/components/ChannelList/CustomChannelPreview.test.tsx +410 -3
- package/src/components/ChannelList/index.test.tsx +132 -1
- package/src/components/LinkAttachment/LinkAttachment.test.tsx +327 -0
- package/src/components/LinkAttachment/components/_shared/BubbleTail.test.tsx +76 -0
- package/src/components/LinkAttachment/components/_shared/CardBody.test.tsx +248 -0
- package/src/components/LinkAttachment/components/_shared/CardCta.test.tsx +123 -0
- package/src/components/LinkAttachment/components/_shared/CardShell.test.tsx +193 -0
- package/src/components/LinkAttachment/components/_shared/CardThumbnail.test.tsx +242 -0
- package/src/components/LinkAttachment/components/_shared/hex.test.ts +37 -0
- package/src/components/LinkAttachment/components/_shared/normalizeExternalHref.test.ts +72 -0
- package/src/components/LinkAttachment/components/_shared/useChinPalette.test.ts +162 -0
- package/src/testing/createMockMessagingClient.ts +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MESSAGE_CUSTOM_TYPE } from '@linktr.ee/messaging-taxonomy'
|
|
2
|
+
import { act, createEvent, fireEvent } from '@testing-library/react'
|
|
2
3
|
import { Channel, LocalMessage, StreamChat } from 'stream-chat'
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
5
|
|
|
5
6
|
import { renderWithProviders, screen } from '../../test/utils'
|
|
6
7
|
import type { ChannelPreviewProps } from '../../types'
|
|
@@ -13,9 +14,15 @@ const mockUser = {
|
|
|
13
14
|
name: 'Current User',
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
const mockChatContext = vi.hoisted(() => ({
|
|
18
|
+
current: {
|
|
19
|
+
client: { userID: 'current-user' },
|
|
20
|
+
} as { client?: { userID?: string } },
|
|
21
|
+
}))
|
|
22
|
+
|
|
16
23
|
vi.mock('stream-chat-react', async (importOriginal) => ({
|
|
17
24
|
...(await importOriginal<typeof import('stream-chat-react')>()),
|
|
18
|
-
useChatContext: () =>
|
|
25
|
+
useChatContext: () => mockChatContext.current,
|
|
19
26
|
}))
|
|
20
27
|
|
|
21
28
|
type MockChannel = Channel & {
|
|
@@ -73,6 +80,406 @@ describe('CustomChannelPreview', () => {
|
|
|
73
80
|
onChannelSelect: vi.fn(),
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
mockChatContext.current = {
|
|
85
|
+
client: { userID: 'current-user' },
|
|
86
|
+
}
|
|
87
|
+
defaultProps.onChannelSelect.mockClear()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('applies selected and unselected row styling from context', () => {
|
|
91
|
+
const channel = createMockChannel([])
|
|
92
|
+
const otherChannel = createMockChannel([])
|
|
93
|
+
otherChannel.id = 'channel-2'
|
|
94
|
+
|
|
95
|
+
const { rerender } = renderWithProviders(
|
|
96
|
+
<ChannelListProvider
|
|
97
|
+
value={{
|
|
98
|
+
selectedChannel: channel,
|
|
99
|
+
onChannelSelect: defaultProps.onChannelSelect,
|
|
100
|
+
debug: false,
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
104
|
+
</ChannelListProvider>
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
let row = screen.getByRole('button')
|
|
108
|
+
expect(row).toHaveClass('bg-black/[0.04]')
|
|
109
|
+
expect(row).not.toHaveClass('hover:bg-black/[0.02]')
|
|
110
|
+
|
|
111
|
+
rerender(
|
|
112
|
+
<ChannelListProvider
|
|
113
|
+
value={{
|
|
114
|
+
selectedChannel: otherChannel,
|
|
115
|
+
onChannelSelect: defaultProps.onChannelSelect,
|
|
116
|
+
debug: false,
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
120
|
+
</ChannelListProvider>
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
row = screen.getByRole('button')
|
|
124
|
+
expect(row).not.toHaveClass('bg-black/[0.04]')
|
|
125
|
+
expect(row).toHaveClass('hover:bg-black/[0.02]')
|
|
126
|
+
|
|
127
|
+
rerender(
|
|
128
|
+
<ChannelListProvider
|
|
129
|
+
value={{
|
|
130
|
+
selectedChannel: otherChannel,
|
|
131
|
+
onChannelSelect: defaultProps.onChannelSelect,
|
|
132
|
+
debug: false,
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<CustomChannelPreview {...defaultProps} channel={channel} unread={1} />
|
|
136
|
+
</ChannelListProvider>
|
|
137
|
+
)
|
|
138
|
+
expect(screen.getByRole('button')).toHaveClass('hover:bg-black/[0.02]')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('selects the channel by click and activation keys only', () => {
|
|
142
|
+
const channel = createMockChannel([])
|
|
143
|
+
const onChannelSelect = vi.fn()
|
|
144
|
+
const { container } = renderWithProviders(
|
|
145
|
+
<ChannelListProvider value={{ onChannelSelect, debug: false }}>
|
|
146
|
+
<CustomChannelPreview channel={channel} />
|
|
147
|
+
</ChannelListProvider>
|
|
148
|
+
)
|
|
149
|
+
const row = container.querySelector('[role="button"]') as HTMLElement
|
|
150
|
+
|
|
151
|
+
fireEvent.click(row)
|
|
152
|
+
fireEvent.keyDown(row, { key: 'Enter' })
|
|
153
|
+
const spaceEvent = createEvent.keyDown(row, { key: ' ' })
|
|
154
|
+
fireEvent(row, spaceEvent)
|
|
155
|
+
|
|
156
|
+
expect(spaceEvent.defaultPrevented).toBe(true)
|
|
157
|
+
expect(onChannelSelect).toHaveBeenCalledTimes(3)
|
|
158
|
+
expect(onChannelSelect).toHaveBeenNthCalledWith(1, channel)
|
|
159
|
+
expect(onChannelSelect).toHaveBeenNthCalledWith(2, channel)
|
|
160
|
+
expect(onChannelSelect).toHaveBeenNthCalledWith(3, channel)
|
|
161
|
+
|
|
162
|
+
fireEvent.keyDown(row, { key: 'a' })
|
|
163
|
+
fireEvent.keyDown(row, { key: 'Enter', repeat: true })
|
|
164
|
+
expect(onChannelSelect).toHaveBeenCalledTimes(3)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('renders with an unhydrated client and resolves the fallback member', () => {
|
|
168
|
+
mockChatContext.current = { client: undefined }
|
|
169
|
+
const channel = createMockChannel([])
|
|
170
|
+
|
|
171
|
+
expect(() =>
|
|
172
|
+
renderWithProviders(
|
|
173
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
174
|
+
)
|
|
175
|
+
).not.toThrow()
|
|
176
|
+
expect(screen.getByText('Current User')).toBeInTheDocument()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('resolves the other participant using the current client user id', () => {
|
|
180
|
+
const channel = createMockChannel([])
|
|
181
|
+
|
|
182
|
+
renderWithProviders(
|
|
183
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
184
|
+
)
|
|
185
|
+
expect(screen.getByText('Alice')).toBeInTheDocument()
|
|
186
|
+
|
|
187
|
+
mockChatContext.current = { client: { userID: 'participant-1' } }
|
|
188
|
+
renderWithProviders(
|
|
189
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
190
|
+
)
|
|
191
|
+
expect(screen.getByText('Current User')).toBeInTheDocument()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('renders safe fallbacks when channel state is empty', () => {
|
|
195
|
+
const channel = createMockChannel([])
|
|
196
|
+
Object.assign(channel, { state: {} })
|
|
197
|
+
|
|
198
|
+
expect(() =>
|
|
199
|
+
renderWithProviders(
|
|
200
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
201
|
+
)
|
|
202
|
+
).not.toThrow()
|
|
203
|
+
expect(screen.getByText('Unknown member')).toBeInTheDocument()
|
|
204
|
+
expect(screen.getByText('No messages yet')).toBeInTheDocument()
|
|
205
|
+
expect(screen.queryByText('Starred conversation.')).not.toBeInTheDocument()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('renders a participant image only when one is supplied', () => {
|
|
209
|
+
const channel = createMockChannel([])
|
|
210
|
+
channel.state.members['participant-1'].user = {
|
|
211
|
+
id: 'participant-1',
|
|
212
|
+
name: 'Alice',
|
|
213
|
+
image: 'https://example.com/alice.png',
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const { container, rerender } = renderWithProviders(
|
|
217
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
218
|
+
)
|
|
219
|
+
expect(container.querySelectorAll('img')).toHaveLength(1)
|
|
220
|
+
expect(container.querySelector('img')).toHaveAttribute(
|
|
221
|
+
'src',
|
|
222
|
+
'https://example.com/alice.png'
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
channel.state.members['participant-1'].user = {
|
|
226
|
+
id: 'participant-1',
|
|
227
|
+
name: 'Alice',
|
|
228
|
+
}
|
|
229
|
+
rerender(
|
|
230
|
+
<CustomChannelPreview {...defaultProps} channel={channel} unread={1} />
|
|
231
|
+
)
|
|
232
|
+
expect(container.querySelectorAll('img')).toHaveLength(0)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it.each([
|
|
236
|
+
[undefined, undefined],
|
|
237
|
+
[3, '3'],
|
|
238
|
+
[99, '99'],
|
|
239
|
+
[100, '99+'],
|
|
240
|
+
])('renders the unread badge for unread=%s', (unread, expectedBadge) => {
|
|
241
|
+
const channel = createMockChannel([])
|
|
242
|
+
const { container } = renderWithProviders(
|
|
243
|
+
<CustomChannelPreview
|
|
244
|
+
{...defaultProps}
|
|
245
|
+
channel={channel}
|
|
246
|
+
unread={unread}
|
|
247
|
+
/>
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if (expectedBadge) {
|
|
251
|
+
expect(screen.getByText(expectedBadge)).toBeInTheDocument()
|
|
252
|
+
} else {
|
|
253
|
+
expect(
|
|
254
|
+
container.querySelector('span[class*="bg-[#7f22fe]"]')
|
|
255
|
+
).not.toBeInTheDocument()
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('renders a timestamp only when the last message has created_at', () => {
|
|
260
|
+
const channel = createMockChannel([
|
|
261
|
+
{
|
|
262
|
+
id: 'msg-1',
|
|
263
|
+
text: 'Recent message',
|
|
264
|
+
type: 'regular',
|
|
265
|
+
created_at: new Date(),
|
|
266
|
+
user: { id: 'participant-1', name: 'Alice' },
|
|
267
|
+
},
|
|
268
|
+
])
|
|
269
|
+
|
|
270
|
+
const { container, rerender } = renderWithProviders(
|
|
271
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
272
|
+
)
|
|
273
|
+
const timestamp = () =>
|
|
274
|
+
container.querySelector('h3')?.parentElement?.querySelector('span')
|
|
275
|
+
|
|
276
|
+
expect(timestamp()).toHaveTextContent('Just now')
|
|
277
|
+
expect(screen.getByText('Just now')).toBeInTheDocument()
|
|
278
|
+
|
|
279
|
+
Object.assign(channel.state.messages[0], { created_at: undefined })
|
|
280
|
+
rerender(
|
|
281
|
+
<CustomChannelPreview {...defaultProps} channel={channel} unread={1} />
|
|
282
|
+
)
|
|
283
|
+
expect(timestamp()).toBeNull()
|
|
284
|
+
expect(screen.queryByText('Just now')).not.toBeInTheDocument()
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it.each([
|
|
288
|
+
[
|
|
289
|
+
'tip with text',
|
|
290
|
+
{
|
|
291
|
+
metadata: { custom_type: MESSAGE_CUSTOM_TYPE.TIP },
|
|
292
|
+
text: 'A generous tip',
|
|
293
|
+
},
|
|
294
|
+
'A generous tip',
|
|
295
|
+
],
|
|
296
|
+
[
|
|
297
|
+
'tip without text',
|
|
298
|
+
{ metadata: { custom_type: MESSAGE_CUSTOM_TYPE.TIP } },
|
|
299
|
+
'Sent a tip',
|
|
300
|
+
],
|
|
301
|
+
[
|
|
302
|
+
'paid with text',
|
|
303
|
+
{
|
|
304
|
+
metadata: { custom_type: MESSAGE_CUSTOM_TYPE.PAID },
|
|
305
|
+
text: 'Paid message',
|
|
306
|
+
},
|
|
307
|
+
'Paid message',
|
|
308
|
+
],
|
|
309
|
+
[
|
|
310
|
+
'paid without text',
|
|
311
|
+
{ metadata: { custom_type: MESSAGE_CUSTOM_TYPE.PAID } },
|
|
312
|
+
'Sent a message',
|
|
313
|
+
],
|
|
314
|
+
[
|
|
315
|
+
'link attachment',
|
|
316
|
+
{ attachments: [{ og_scrape_url: 'https://example.com' }] },
|
|
317
|
+
'https://example.com',
|
|
318
|
+
],
|
|
319
|
+
[
|
|
320
|
+
'image attachment',
|
|
321
|
+
{ attachments: [{ type: 'image' }] },
|
|
322
|
+
'📷 Sent an image',
|
|
323
|
+
],
|
|
324
|
+
[
|
|
325
|
+
'video attachment',
|
|
326
|
+
{ attachments: [{ type: 'video' }] },
|
|
327
|
+
'🎥 Sent a video',
|
|
328
|
+
],
|
|
329
|
+
['audio attachment', { attachments: [{ type: 'audio' }] }, '🎵 Sent audio'],
|
|
330
|
+
['file attachment', { attachments: [{ type: 'file' }] }, '📎 Sent a file'],
|
|
331
|
+
[
|
|
332
|
+
'unknown attachment',
|
|
333
|
+
{ attachments: [{ type: 'unknown' }] },
|
|
334
|
+
'📎 Sent an attachment',
|
|
335
|
+
],
|
|
336
|
+
])('renders the %s preview text', (_name, message, expectedText) => {
|
|
337
|
+
const channel = createMockChannel([
|
|
338
|
+
{
|
|
339
|
+
id: 'msg-1',
|
|
340
|
+
type: 'regular',
|
|
341
|
+
...message,
|
|
342
|
+
user: { id: 'participant-1', name: 'Alice' },
|
|
343
|
+
},
|
|
344
|
+
])
|
|
345
|
+
|
|
346
|
+
renderWithProviders(
|
|
347
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
348
|
+
)
|
|
349
|
+
expect(screen.getByText(expectedText)).toBeInTheDocument()
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('prefixes chatbot previews but not normal message previews', () => {
|
|
353
|
+
const channel = createMockChannel([
|
|
354
|
+
{
|
|
355
|
+
id: 'msg-1',
|
|
356
|
+
text: 'Automated response',
|
|
357
|
+
type: 'regular',
|
|
358
|
+
metadata: { custom_type: MESSAGE_CUSTOM_TYPE.CHATBOT },
|
|
359
|
+
},
|
|
360
|
+
])
|
|
361
|
+
const { rerender } = renderWithProviders(
|
|
362
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
363
|
+
)
|
|
364
|
+
expect(screen.getByText('✨ Automated response')).toBeInTheDocument()
|
|
365
|
+
|
|
366
|
+
channel.state.messages[0].metadata = {}
|
|
367
|
+
channel.state.messages[0].text = 'Human response'
|
|
368
|
+
rerender(
|
|
369
|
+
<CustomChannelPreview {...defaultProps} channel={channel} unread={1} />
|
|
370
|
+
)
|
|
371
|
+
expect(screen.getByText('Human response')).toBeInTheDocument()
|
|
372
|
+
expect(screen.queryByText('✨ Human response')).not.toBeInTheDocument()
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('uses renderMessagePreview from context with the resolved default text', () => {
|
|
376
|
+
const channel = createMockChannel([
|
|
377
|
+
{
|
|
378
|
+
id: 'msg-1',
|
|
379
|
+
text: 'Default preview',
|
|
380
|
+
type: 'regular',
|
|
381
|
+
},
|
|
382
|
+
])
|
|
383
|
+
const renderMessagePreview = vi.fn(
|
|
384
|
+
(_message: LocalMessage | undefined, _defaultPreview?: string) =>
|
|
385
|
+
'Custom preview'
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
renderWithProviders(
|
|
389
|
+
<ChannelListProvider
|
|
390
|
+
value={{ onChannelSelect: vi.fn(), debug: false, renderMessagePreview }}
|
|
391
|
+
>
|
|
392
|
+
<CustomChannelPreview channel={channel} />
|
|
393
|
+
</ChannelListProvider>
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
expect(screen.getByText('Custom preview')).toBeInTheDocument()
|
|
397
|
+
expect(renderMessagePreview).toHaveBeenCalledWith(
|
|
398
|
+
channel.state.messages[0],
|
|
399
|
+
'Default preview'
|
|
400
|
+
)
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('renders the default preview when renderMessagePreview is not supplied', () => {
|
|
404
|
+
const channel = createMockChannel([
|
|
405
|
+
{
|
|
406
|
+
id: 'msg-1',
|
|
407
|
+
text: 'Default preview',
|
|
408
|
+
type: 'regular',
|
|
409
|
+
},
|
|
410
|
+
])
|
|
411
|
+
|
|
412
|
+
renderWithProviders(
|
|
413
|
+
<CustomChannelPreview {...defaultProps} channel={channel} />
|
|
414
|
+
)
|
|
415
|
+
expect(screen.getByText('Default preview')).toBeInTheDocument()
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('uses the viewer language translation when available', () => {
|
|
419
|
+
const channel = createMockChannel([
|
|
420
|
+
{
|
|
421
|
+
id: 'msg-1',
|
|
422
|
+
text: 'Raw text',
|
|
423
|
+
type: 'regular',
|
|
424
|
+
i18n: { es_text: 'Texto traducido', language: 'es' },
|
|
425
|
+
},
|
|
426
|
+
])
|
|
427
|
+
const { rerender } = renderWithProviders(
|
|
428
|
+
<ChannelListProvider
|
|
429
|
+
value={{
|
|
430
|
+
onChannelSelect: vi.fn(),
|
|
431
|
+
debug: false,
|
|
432
|
+
viewerLanguage: 'es-MX',
|
|
433
|
+
}}
|
|
434
|
+
>
|
|
435
|
+
<CustomChannelPreview channel={channel} />
|
|
436
|
+
</ChannelListProvider>
|
|
437
|
+
)
|
|
438
|
+
expect(screen.getByText('Texto traducido')).toBeInTheDocument()
|
|
439
|
+
|
|
440
|
+
rerender(
|
|
441
|
+
<ChannelListProvider value={{ onChannelSelect: vi.fn(), debug: false }}>
|
|
442
|
+
<CustomChannelPreview channel={channel} />
|
|
443
|
+
</ChannelListProvider>
|
|
444
|
+
)
|
|
445
|
+
expect(screen.getByText('Raw text')).toBeInTheDocument()
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('logs the preview payload only when debug is enabled', () => {
|
|
449
|
+
const channel = createMockChannel([])
|
|
450
|
+
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
451
|
+
|
|
452
|
+
renderWithProviders(
|
|
453
|
+
<ChannelListProvider value={{ onChannelSelect: vi.fn(), debug: true }}>
|
|
454
|
+
<CustomChannelPreview channel={channel} />
|
|
455
|
+
</ChannelListProvider>
|
|
456
|
+
)
|
|
457
|
+
expect(consoleLog).toHaveBeenCalledWith(
|
|
458
|
+
'📺 [ChannelList] 📋 CHANNEL PREVIEW RENDER',
|
|
459
|
+
expect.objectContaining({
|
|
460
|
+
channelId: channel.id,
|
|
461
|
+
isSelected: false,
|
|
462
|
+
participantName: 'Alice',
|
|
463
|
+
unreadCount: 0,
|
|
464
|
+
hasTimestamp: false,
|
|
465
|
+
})
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
consoleLog.mockClear()
|
|
469
|
+
const { rerender } = renderWithProviders(
|
|
470
|
+
<ChannelListProvider value={{ onChannelSelect: vi.fn(), debug: false }}>
|
|
471
|
+
<CustomChannelPreview channel={channel} />
|
|
472
|
+
</ChannelListProvider>
|
|
473
|
+
)
|
|
474
|
+
expect(consoleLog).not.toHaveBeenCalled()
|
|
475
|
+
rerender(
|
|
476
|
+
<ChannelListProvider value={{ onChannelSelect: vi.fn(), debug: false }}>
|
|
477
|
+
<CustomChannelPreview channel={channel} />
|
|
478
|
+
</ChannelListProvider>
|
|
479
|
+
)
|
|
480
|
+
expect(consoleLog).not.toHaveBeenCalled()
|
|
481
|
+
})
|
|
482
|
+
|
|
76
483
|
it('uses username when participant name is UUID-like', () => {
|
|
77
484
|
const channel = createMockChannel([])
|
|
78
485
|
channel.state.members['participant-1'] = {
|
|
@@ -11,16 +11,25 @@ const {
|
|
|
11
11
|
ChannelListProviderMock,
|
|
12
12
|
CustomChannelPreviewMock,
|
|
13
13
|
InfiniteScrollMock,
|
|
14
|
+
restorePendingMessagesMock,
|
|
15
|
+
streamMountMock,
|
|
16
|
+
messagingContextMock,
|
|
14
17
|
} = vi.hoisted(() => ({
|
|
15
18
|
ChannelListProviderMock: vi.fn(
|
|
16
19
|
({ children }: { children: React.ReactNode; value: unknown }) => children
|
|
17
20
|
),
|
|
18
21
|
CustomChannelPreviewMock: () => null,
|
|
19
22
|
InfiniteScrollMock: () => null,
|
|
23
|
+
restorePendingMessagesMock: vi.fn(),
|
|
24
|
+
streamMountMock: vi.fn(),
|
|
25
|
+
messagingContextMock: { current: { debug: false } },
|
|
20
26
|
}))
|
|
21
27
|
|
|
22
28
|
vi.mock('stream-chat-react', () => ({
|
|
23
29
|
ChannelList: (props: unknown) => {
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
streamMountMock()
|
|
32
|
+
}, [])
|
|
24
33
|
streamChannelListMock(props)
|
|
25
34
|
return <div data-testid="stream-channel-list" />
|
|
26
35
|
},
|
|
@@ -28,7 +37,11 @@ vi.mock('stream-chat-react', () => ({
|
|
|
28
37
|
}))
|
|
29
38
|
|
|
30
39
|
vi.mock('../../providers/MessagingProvider', () => ({
|
|
31
|
-
useMessagingContext: () =>
|
|
40
|
+
useMessagingContext: () => messagingContextMock.current,
|
|
41
|
+
}))
|
|
42
|
+
|
|
43
|
+
vi.mock('../../hooks/useRestorePendingMessages', () => ({
|
|
44
|
+
restorePendingMessages: restorePendingMessagesMock,
|
|
32
45
|
}))
|
|
33
46
|
|
|
34
47
|
vi.mock('./ChannelListContext', () => ({
|
|
@@ -48,6 +61,9 @@ describe('ChannelList', () => {
|
|
|
48
61
|
beforeEach(() => {
|
|
49
62
|
ChannelListProviderMock.mockClear()
|
|
50
63
|
streamChannelListMock.mockClear()
|
|
64
|
+
restorePendingMessagesMock.mockClear()
|
|
65
|
+
streamMountMock.mockClear()
|
|
66
|
+
messagingContextMock.current = { debug: false }
|
|
51
67
|
})
|
|
52
68
|
|
|
53
69
|
it('defaults unseen-channel promotion to false', () => {
|
|
@@ -200,4 +216,119 @@ describe('ChannelList', () => {
|
|
|
200
216
|
streamProps.channelRenderFilterFn!(mockChannels)
|
|
201
217
|
expect(filterFn).toHaveBeenCalledWith(mockChannels)
|
|
202
218
|
})
|
|
219
|
+
|
|
220
|
+
it('restores pending messages for every channel and preserves channels without a filter', () => {
|
|
221
|
+
const channels = [
|
|
222
|
+
{ cid: 'ch-1', state: { pending_messages: [] } },
|
|
223
|
+
{ cid: 'ch-2', state: { pending_messages: [{ id: 'pending-2' }] } },
|
|
224
|
+
] as unknown as Channel[]
|
|
225
|
+
|
|
226
|
+
renderWithProviders(<ChannelList {...defaultProps} />)
|
|
227
|
+
const streamProps = streamChannelListMock.mock.calls[0][0] as {
|
|
228
|
+
channelRenderFilterFn: (channels: Channel[]) => Channel[]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
expect(streamProps.channelRenderFilterFn(channels)).toBe(channels)
|
|
232
|
+
expect(restorePendingMessagesMock).toHaveBeenCalledTimes(2)
|
|
233
|
+
expect(restorePendingMessagesMock).toHaveBeenNthCalledWith(1, channels[0])
|
|
234
|
+
expect(restorePendingMessagesMock).toHaveBeenNthCalledWith(2, channels[1])
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('remounts Stream ChannelList when filters change but not when they stay the same', () => {
|
|
238
|
+
const { rerender } = renderWithProviders(<ChannelList {...defaultProps} />)
|
|
239
|
+
|
|
240
|
+
expect(streamMountMock).toHaveBeenCalledOnce()
|
|
241
|
+
|
|
242
|
+
rerender(<ChannelList {...defaultProps} />)
|
|
243
|
+
expect(streamMountMock).toHaveBeenCalledOnce()
|
|
244
|
+
|
|
245
|
+
rerender(
|
|
246
|
+
<ChannelList
|
|
247
|
+
{...defaultProps}
|
|
248
|
+
filters={{ type: 'messaging', members: { $in: ['another-user'] } }}
|
|
249
|
+
/>
|
|
250
|
+
)
|
|
251
|
+
expect(streamMountMock).toHaveBeenCalledTimes(2)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('forwards the default and explicit sort values and the channel limit', () => {
|
|
255
|
+
renderWithProviders(<ChannelList {...defaultProps} />)
|
|
256
|
+
expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
|
|
257
|
+
sort: { last_message_at: -1 },
|
|
258
|
+
options: { limit: 30 },
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const explicitSort = { updated_at: 1 as const }
|
|
262
|
+
renderWithProviders(<ChannelList {...defaultProps} sort={explicitSort} />)
|
|
263
|
+
expect(streamChannelListMock.mock.calls[1][0]).toMatchObject({
|
|
264
|
+
sort: explicitSort,
|
|
265
|
+
options: { limit: 30 },
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('provides channel list context values from props and messaging context', () => {
|
|
270
|
+
const selectedChannel = { id: 'selected-channel' } as Channel
|
|
271
|
+
const onChannelSelect = vi.fn()
|
|
272
|
+
const renderMessagePreview = vi.fn()
|
|
273
|
+
messagingContextMock.current = { debug: true }
|
|
274
|
+
|
|
275
|
+
renderWithProviders(
|
|
276
|
+
<ChannelList
|
|
277
|
+
{...defaultProps}
|
|
278
|
+
selectedChannel={selectedChannel}
|
|
279
|
+
onChannelSelect={onChannelSelect}
|
|
280
|
+
renderMessagePreview={renderMessagePreview}
|
|
281
|
+
viewerLanguage="fr"
|
|
282
|
+
/>
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
const contextValue = ChannelListProviderMock.mock.calls[0][0].value
|
|
286
|
+
expect(contextValue).toMatchObject({
|
|
287
|
+
selectedChannel,
|
|
288
|
+
onChannelSelect,
|
|
289
|
+
debug: true,
|
|
290
|
+
renderMessagePreview,
|
|
291
|
+
viewerLanguage: 'fr',
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('logs the channel list render when messaging debug is enabled', () => {
|
|
296
|
+
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
297
|
+
messagingContextMock.current = { debug: true }
|
|
298
|
+
|
|
299
|
+
renderWithProviders(<ChannelList {...defaultProps} />)
|
|
300
|
+
|
|
301
|
+
expect(consoleLog).toHaveBeenCalledWith(
|
|
302
|
+
'📺 [ChannelList] 🔄 RENDER START',
|
|
303
|
+
expect.objectContaining({
|
|
304
|
+
renderCount: 1,
|
|
305
|
+
selectedChannelId: undefined,
|
|
306
|
+
filters: defaultProps.filters,
|
|
307
|
+
})
|
|
308
|
+
)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('passes wrapper classes and the custom empty state indicator through', () => {
|
|
312
|
+
const EmptyStateIndicator = () => <div>Empty state</div>
|
|
313
|
+
const { container } = renderWithProviders(
|
|
314
|
+
<ChannelList
|
|
315
|
+
{...defaultProps}
|
|
316
|
+
className="custom-channel-list"
|
|
317
|
+
customEmptyStateIndicator={EmptyStateIndicator}
|
|
318
|
+
/>
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
expect(container.firstElementChild).toHaveClass(
|
|
322
|
+
'messaging-channel-list',
|
|
323
|
+
'h-full',
|
|
324
|
+
'flex',
|
|
325
|
+
'flex-col',
|
|
326
|
+
'min-w-0',
|
|
327
|
+
'overflow-hidden',
|
|
328
|
+
'custom-channel-list'
|
|
329
|
+
)
|
|
330
|
+
expect(streamChannelListMock.mock.calls[0][0]).toMatchObject({
|
|
331
|
+
EmptyStateIndicator,
|
|
332
|
+
})
|
|
333
|
+
})
|
|
203
334
|
})
|