cb9a0b9250
- 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>
56 lines
2.4 KiB
TypeScript
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');
|
|
});
|
|
});
|