feat(messaging-ui): channels — browse, join, leave, create (contract + mock + UI)
Adds a third membership beyond dm/group: a discoverable, joinable room. - contract: Membership += 'channel'; Conversation.topic; ChannelSummary + CreateChannelInput; optional adapter methods browseChannels/createChannel/ joinChannel/leaveChannel (capability-degrading — absent hides the UI) - MockAdapter implements them (seeds a joined public, a joinable public, and a private channel); listConversations now returns only threads I'm in - useChannels hook (browse/create/join/leave + supported flag) - UI: sidebar sections (Channels vs Direct messages), a + that opens a ChannelBrowser (join + create), # / lock glyphs; themeable styles - KernelClientAdapter passes channel membership + topic through - 4 channel tests (mock browse/join/create + render browse→join); 52 total green Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { MessagingProvider } from '../provider';
|
||||
import { Messenger } from './messenger';
|
||||
import { MockAdapter } from '../adapters/mock';
|
||||
|
||||
function mount() {
|
||||
return render(
|
||||
<MessagingProvider adapter={new MockAdapter()}>
|
||||
<Messenger />
|
||||
</MessagingProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('channels (mock adapter)', () => {
|
||||
it('browse returns public channels + private ones I am in, with a joined flag', async () => {
|
||||
const a = new MockAdapter();
|
||||
const list = await a.browseChannels();
|
||||
const byId = new Map(list.map((c) => [c.threadId, c]));
|
||||
expect(byId.get('th_ch_general')?.joined).toBe(true); // public, I'm in
|
||||
expect(byId.get('th_ch_random')?.joined).toBe(false); // public, I'm NOT in
|
||||
expect(byId.get('th_ch_deals')?.joined).toBe(true); // private, I'm in
|
||||
// A private channel I'm not in must never surface in browse — none seeded, so all private here are mine.
|
||||
expect(list.every((c) => c.visibility === 'public' || c.joined)).toBe(true);
|
||||
});
|
||||
|
||||
it('join adds me and the channel then appears in my conversation list', async () => {
|
||||
const a = new MockAdapter();
|
||||
expect((await a.listConversations()).some((c) => c.threadId === 'th_ch_random')).toBe(false);
|
||||
await a.joinChannel('th_ch_random');
|
||||
expect((await a.listConversations()).some((c) => c.threadId === 'th_ch_random')).toBe(true);
|
||||
expect((await a.browseChannels()).find((c) => c.threadId === 'th_ch_random')?.joined).toBe(true);
|
||||
});
|
||||
|
||||
it('create makes a channel I am a member of', async () => {
|
||||
const a = new MockAdapter();
|
||||
const { threadId } = await a.createChannel({ name: 'design', topic: 'UI stuff', visibility: 'public' });
|
||||
const conv = (await a.listConversations()).find((c) => c.threadId === threadId);
|
||||
expect(conv?.membership).toBe('channel');
|
||||
expect(conv?.topic).toBe('UI stuff');
|
||||
});
|
||||
|
||||
it('UI: browse, then join a channel — it moves into the Channels section', async () => {
|
||||
mount();
|
||||
// Open the browser via the Channels section "+".
|
||||
fireEvent.click(await screen.findByTitle('Browse channels'));
|
||||
// #random is browse-only (not joined) → has a Join button.
|
||||
expect(await screen.findByText('random')).toBeTruthy();
|
||||
const joinButtons = screen.getAllByText('Join');
|
||||
fireEvent.click(joinButtons[0]!);
|
||||
// After joining, we jump to the thread and the browser closes → composer is shown.
|
||||
await waitFor(() => expect(screen.getByLabelText('Message')).toBeTruthy());
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user