00a2cc474a
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>
55 lines
2.5 KiB
TypeScript
55 lines
2.5 KiB
TypeScript
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());
|
|
});
|
|
});
|