mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 17:23:05 +02:00
Add hashtag link detection. Closes #134.
This commit is contained in:
@@ -140,6 +140,59 @@ describe('MessageList channel sender rendering', () => {
|
||||
expect(screen.getByRole('button', { name: 'View info for Alice' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders valid channel references as clickable links and ignores invalid ones', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChannelReferenceClick = vi.fn();
|
||||
|
||||
render(
|
||||
<MessageList
|
||||
messages={[
|
||||
createMessage({
|
||||
text: 'Alice: Join #mesh-room now skip #bad--room and visit https://example.com/#also-skip',
|
||||
}),
|
||||
]}
|
||||
contacts={[]}
|
||||
loading={false}
|
||||
onChannelReferenceClick={onChannelReferenceClick}
|
||||
/>
|
||||
);
|
||||
|
||||
const linkedChannel = screen.getByRole('button', { name: '#mesh-room' });
|
||||
expect(linkedChannel).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '#bad--room' })).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'https://example.com/#also-skip' })
|
||||
).toBeInTheDocument();
|
||||
|
||||
await user.click(linkedChannel);
|
||||
|
||||
expect(onChannelReferenceClick).toHaveBeenCalledWith('#mesh-room');
|
||||
});
|
||||
|
||||
it('links valid channel references in direct messages too', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChannelReferenceClick = vi.fn();
|
||||
|
||||
render(
|
||||
<MessageList
|
||||
messages={[
|
||||
createMessage({
|
||||
type: 'PRIV',
|
||||
text: 'check #ops-room',
|
||||
conversation_key: 'ab'.repeat(32),
|
||||
}),
|
||||
]}
|
||||
contacts={[]}
|
||||
loading={false}
|
||||
onChannelReferenceClick={onChannelReferenceClick}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '#ops-room' }));
|
||||
|
||||
expect(onChannelReferenceClick).toHaveBeenCalledWith('#ops-room');
|
||||
});
|
||||
|
||||
it('renders and dismisses an unread marker at the first unread message boundary', async () => {
|
||||
const user = userEvent.setup();
|
||||
const messages = [
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseSenderFromText, formatTime } from '../utils/messageParser';
|
||||
import {
|
||||
findLinkedChannelReferences,
|
||||
formatTime,
|
||||
isValidLinkedChannelName,
|
||||
parseSenderFromText,
|
||||
} from '../utils/messageParser';
|
||||
|
||||
describe('parseSenderFromText', () => {
|
||||
it('extracts sender and content from "sender: message" format', () => {
|
||||
@@ -95,3 +100,31 @@ describe('formatTime', () => {
|
||||
expect(result).toMatch(/\d{1,2}:\d{2}/); // time portion
|
||||
});
|
||||
});
|
||||
|
||||
describe('linked channel references', () => {
|
||||
it('accepts lowercase alphanumeric names with single dashes', () => {
|
||||
expect(isValidLinkedChannelName('ops')).toBe(true);
|
||||
expect(isValidLinkedChannelName('ops-1')).toBe(true);
|
||||
expect(isValidLinkedChannelName('1-2-3')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects uppercase, leading or trailing dashes, and repeated dashes', () => {
|
||||
expect(isValidLinkedChannelName('Ops')).toBe(false);
|
||||
expect(isValidLinkedChannelName('-ops')).toBe(false);
|
||||
expect(isValidLinkedChannelName('ops-')).toBe(false);
|
||||
expect(isValidLinkedChannelName('ops--room')).toBe(false);
|
||||
});
|
||||
|
||||
it('finds standalone linked channel references in message text', () => {
|
||||
expect(findLinkedChannelReferences('Join #mesh-room then say hi in #ops2')).toEqual([
|
||||
{ label: '#mesh-room', start: 5, end: 15 },
|
||||
{ label: '#ops2', start: 31, end: 36 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores invalid or embedded channel-like text', () => {
|
||||
expect(
|
||||
findLinkedChannelReferences('skip #Bad #bad--name abc#ops #ops- #opsRoom #ops_room #good-room,')
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,7 +32,10 @@ describe('NewMessageModal form reset', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function renderModal(open = true) {
|
||||
function renderModal(
|
||||
open = true,
|
||||
overrides: Partial<Parameters<typeof NewMessageModal>[0]> = {}
|
||||
) {
|
||||
return render(
|
||||
<NewMessageModal
|
||||
open={open}
|
||||
@@ -41,6 +44,7 @@ describe('NewMessageModal form reset', () => {
|
||||
onCreateContact={onCreateContact}
|
||||
onCreateChannel={onCreateChannel}
|
||||
onCreateHashtagChannel={onCreateHashtagChannel}
|
||||
{...overrides}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -50,6 +54,26 @@ describe('NewMessageModal form reset', () => {
|
||||
}
|
||||
|
||||
describe('hashtag tab', () => {
|
||||
it('prefills the hashtag tab from a linked channel request', async () => {
|
||||
renderModal(true, {
|
||||
prefillRequest: {
|
||||
tab: 'hashtag',
|
||||
hashtagName: 'mesh-room',
|
||||
nonce: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('tab', { name: 'Hashtag Channel' })).toHaveAttribute(
|
||||
'data-state',
|
||||
'active'
|
||||
);
|
||||
});
|
||||
expect((screen.getByPlaceholderText('channel-name') as HTMLInputElement).value).toBe(
|
||||
'mesh-room'
|
||||
);
|
||||
});
|
||||
|
||||
it('clears name after successful Create', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { unmount } = renderModal();
|
||||
|
||||
Reference in New Issue
Block a user