import { describe, it, expect, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Linkify } from '@/components/linkify'; describe('Linkify', () => { it('renders a bare http(s) URL as a new-tab link', () => { render({'Ticket at https://tracker.test/OF-12 please'}); const link = screen.getByRole('link', { name: 'https://tracker.test/OF-12' }); expect(link).toHaveAttribute('href', 'https://tracker.test/OF-12'); expect(link).toHaveAttribute('target', '_blank'); expect(link).toHaveAttribute('rel', 'noopener noreferrer'); }); it('keeps the surrounding text intact', () => { const { container } = render( {'Ticket at https://tracker.test/OF-12 please'} ); expect(container).toHaveTextContent('Ticket at https://tracker.test/OF-12 please'); }); it('links every URL in the string', () => { render({'http://a.test/1 then https://b.test/2'}); expect(screen.getAllByRole('link').map((a) => a.getAttribute('href'))).toEqual([ 'http://a.test/1', 'https://b.test/2', ]); }); it('leaves plain text alone', () => { render({'No links in this sentence'}); expect(screen.queryAllByRole('link')).toHaveLength(0); expect(screen.getByText('No links in this sentence')).toBeInTheDocument(); }); it('passes non-string children straight through', () => { render( https://not-parsed.test/x ); expect(screen.queryAllByRole('link')).toHaveLength(0); expect(screen.getByTestId('child')).toHaveTextContent('https://not-parsed.test/x'); }); it('does not turn a javascript: URL into a link', () => { const { container } = render({'javascript:alert(document.cookie)'}); expect(container.querySelector('a')).toBeNull(); expect(container).toHaveTextContent('javascript:alert(document.cookie)'); }); it('does not turn a data: URL into a link', () => { const { container } = render( {'data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=='} ); expect(container.querySelector('a')).toBeNull(); }); it('does not link vbscript:, file: or protocol-relative URLs', () => { const { container } = render( {'vbscript:msgbox(1) file:///etc/passwd //evil.test/x'} ); expect(container.querySelector('a')).toBeNull(); }); it('never injects markup from the string', () => { const { container } = render({''}); expect(container.querySelector('img')).toBeNull(); expect(container).toHaveTextContent(''); }); it('is case sensitive about the scheme, matching CommentRichText', () => { const { container } = render({'HTTPS://EXAMPLE.COM/a'}); expect(container.querySelector('a')).toBeNull(); }); it('swallows trailing punctuation into the href, matching CommentRichText', () => { render({'Fixed in https://example.com/pr/12.'}); expect(screen.getByRole('link')).toHaveAttribute('href', 'https://example.com/pr/12.'); }); it('does not let a click on the link reach an enclosing handler', async () => { const onRowClick = vi.fn(); render( // Mirrors the real call sites, where Linkify sits inside a clickable // comment row.
{'https://tracker.test/OF-12'}
); const link = screen.getByRole('link'); link.addEventListener('click', (event) => event.preventDefault()); await userEvent.click(link); expect(onRowClick).not.toHaveBeenCalled(); }); });