Files
iios/packages/iios-messaging-ui/src/mentions.test.tsx
T
maaz519 cb9a0b9250 feat(messaging-ui): @mentions — autocomplete, resolve to ids, highlight
- contract: SendOpts.mentions (opaque userId notify-list) + optional
  listMembers(threadId) for autocomplete/highlight (capability-degrading)
- mock: listMembers; KernelClientAdapter.send forwards mentions to the socket
  (which already carries them → inbox MENTION items)
- composer: type @ → member/@channel/@here autocomplete; Enter picks the first;
  on send, mentions resolve to ids; message bodies highlight @mentions
- useMembers hook; mentions.tsx helpers (trailingMentionQuery/insertMention/
  resolveMentions/highlightMentions)
- 4 mention tests (helpers + render autocomplete→send carries id); 56 total green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 00:15:21 +05:30

56 lines
2.4 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MessagingProvider } from './provider';
import { Messenger } from './components/messenger';
import { MockAdapter } from './adapters/mock';
import { insertMention, resolveMentions, trailingMentionQuery } from './mentions';
import type { Person } from './types';
const people: Person[] = [
{ id: 'pp_sofia', name: 'Sofia Ramirez', kind: 'staff' },
{ id: 'pp_dan', name: 'Dan Whitaker', kind: 'staff' },
];
describe('mention helpers', () => {
it('trailingMentionQuery finds the @token being typed', () => {
expect(trailingMentionQuery('hey @Sof')).toBe('Sof');
expect(trailingMentionQuery('@')).toBe('');
expect(trailingMentionQuery('no mention here')).toBeNull();
expect(trailingMentionQuery('done @Sofia Ramirez ')).toBeNull(); // completed, trailing space
});
it('insertMention replaces the trailing query, keeping the boundary', () => {
expect(insertMention('hey @Sof', 'Sofia Ramirez')).toBe('hey @Sofia Ramirez ');
expect(insertMention('@ch', 'channel')).toBe('@channel ');
});
it('resolveMentions maps names to ids; @channel expands to everyone', () => {
expect(resolveMentions('ping @Sofia Ramirez', people)).toEqual(['pp_sofia']);
expect(resolveMentions('nobody here', people)).toEqual([]);
expect(resolveMentions('@channel ship it', people).sort()).toEqual(['pp_dan', 'pp_sofia']);
});
});
describe('<Thread /> @mention autocomplete', () => {
it('picking a suggestion inserts the name and send carries the mention id', async () => {
const adapter = new MockAdapter();
const sendSpy = vi.spyOn(adapter, 'send');
render(
<MessagingProvider adapter={adapter}>
<Messenger />
</MessagingProvider>,
);
const input = (await screen.findByLabelText('Message')) as HTMLInputElement;
fireEvent.change(input, { target: { value: 'hey @Sof' } });
// The suggestion (role=option) is distinct from the sidebar row of the same name.
fireEvent.click(await screen.findByRole('option', { name: 'Sofia Ramirez' }));
expect(input.value).toBe('hey @Sofia Ramirez ');
fireEvent.click(screen.getByText('Send'));
await waitFor(() => expect(sendSpy).toHaveBeenCalled());
const opts = sendSpy.mock.calls[0]![2];
expect(opts?.mentions).toContain('pp_sofia');
});
});