import React from 'react' import { LinkPreview, LinkPreviewStatus } from 'stream-chat' import { describe, expect, it, vi } from 'vitest' import { renderWithProviders, screen, fireEvent } from '../../test/utils' import { LOADING_LABEL } from '../LinkAttachment/components/_shared/CardShell' import { CustomLinkPreviewList } from '.' const dismissPreview = vi.fn() // `LinkPreviewsManager`'s static predicates are left real (`previewIsLoading` // is just `status === 'loading'`), so these cases exercise the actual mapping // from a `LinkPreview` to `LinkAttachment.Composer` props rather than a stub of // it. Only the two composer hooks are faked. const previews: LinkPreview[] = [] vi.mock('stream-chat-react', () => ({ useMessageComposer: () => ({ linkPreviewsManager: { state: {}, dismissPreview }, }), useStateStore: () => ({ linkPreviews: previews }), })) const stage = ( preview: Partial & { status: LinkPreviewStatus } ) => { previews.length = 0 previews.push({ og_scrape_url: 'https://example.com/story', ...preview }) return renderWithProviders() } describe('CustomLinkPreviewList', () => { it('reserves the hero while the scrape is still in flight', () => { // A LOADING preview has no `image_url` yet. Keying the layout off the image // alone dropped the hero for the whole ~1.5s debounce + scrape window, so // the skeleton was two bare text bars that then snapped open to the 250px // featured box (MES-1349). const { container } = stage({ status: LinkPreviewStatus.LOADING }) const shell = screen.getByLabelText(LOADING_LABEL) expect(shell).toHaveAttribute('aria-busy', 'true') expect(shell.className).toContain('h-[250px]') // Hero placeholder + the two chin bars: without the hero there are two. expect(container.querySelectorAll('.animate-pulse')).toHaveLength(3) }) it('keeps the featured hero once a preview loads with an image', () => { stage({ status: LinkPreviewStatus.LOADED, title: 'Example story', image_url: 'https://cdn.example.com/story.jpg', }) expect(screen.getByText('Example story')).toBeInTheDocument() expect(screen.getByRole('img')).toBeInTheDocument() expect(screen.queryByLabelText(LOADING_LABEL)).toBeNull() }) it('falls back to the compact card when a loaded preview has no image', () => { stage({ status: LinkPreviewStatus.LOADED, title: 'Example story' }) expect(screen.getByText('Example story')).toBeInTheDocument() expect(screen.queryByRole('img')).toBeNull() }) it('is not a scroller of its own: the composer card scrolls as one piece (MES-1037)', () => { // The inverted layout contract — see this component's doc comment. While // this list scrolled and shrank on its own, a staged preview was cropped to // keep the textarea above the keyboard instead of scrolling with it. const { container } = stage({ status: LinkPreviewStatus.LOADED, title: 'Example story', image_url: 'https://cdn.example.com/story.jpg', }) const list = container.querySelector('.messaging-link-preview-list') expect(list).not.toHaveClass('overflow-y-auto') expect(list).not.toHaveClass('min-h-0') }) it('is dismiss-only: the staged card never navigates', () => { // The deliberate behaviour change in MES-1349 — see this component's own // doc comment for why (MES-1358 §7 Q7, touch hazard). Asserted so the // anchor cannot be reinstated by accident. stage({ status: LinkPreviewStatus.LOADED, title: 'Example story', image_url: 'https://cdn.example.com/story.jpg', }) expect(screen.queryByRole('link')).toBeNull() const controls = screen.getAllByRole('button') expect(controls).toHaveLength(1) expect(controls[0]).toHaveAttribute('aria-label', 'Dismiss attachment') fireEvent.click(controls[0]) expect(dismissPreview).toHaveBeenCalledWith('https://example.com/story') }) })