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();
});
});