Files
iios/packages/iios-messaging-ui/src/components/conversation-settings.test.tsx
T
maaz519 3d08fa42f8 feat(messaging-ui): import a channel's roster from the settings panel
Phase C of channel-roster import. Adds the optional addMembers(threadId, userIds)
adapter seam (+ BulkAddResult) and, in ConversationSettings, a '#' mode on the
existing Add-people box that lists the channels you belong to — public AND
private, since the source list is your own membership-scoped conversation list.

Picking a channel STAGES the import (reads its roster, diffs against who is
already here) and shows a confirm — 'Add 9 people from #design? (3 already here)'
— so an administrative action never fires on a stray click. The result line
reports added / already-a-member / failed. Absent addMembers => the affordance is
hidden entirely and '#' is just a search string.

Bumped to 0.1.8. SDK suite: 15 files / 78 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:43:03 +05:30

98 lines
4.2 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MessagingProvider } from '../provider';
import { MockAdapter } from '../adapters/mock';
import { ConversationSettings } from './conversation-settings';
function mount(adapter = new MockAdapter()) {
render(
<MessagingProvider adapter={adapter}>
<ConversationSettings threadId="th_mock_2" title="Storm crew" membership="group" onClose={() => {}} />
</MessagingProvider>,
);
return adapter;
}
describe('MockAdapter member management', () => {
it('adds and removes a member, and renames', async () => {
const a = new MockAdapter();
await a.addMember('th_mock_2', 'pp_sofia');
expect((await a.listMembers('th_mock_2')).some((m) => m.id === 'pp_sofia')).toBe(true);
await a.removeMember('th_mock_2', 'pp_sofia');
expect((await a.listMembers('th_mock_2')).some((m) => m.id === 'pp_sofia')).toBe(false);
await a.renameConversation('th_mock_2', 'Renamed');
expect((await a.listConversations()).find((c) => c.threadId === 'th_mock_2')?.title).toBe('Renamed');
});
});
describe('<ConversationSettings />', () => {
it('lists members and adds one from the directory', async () => {
const a = mount();
// A current member is shown.
await screen.findByText('Dan Whitaker');
// Add an addable person from the directory.
const sofia = await screen.findByText('Sofia Ramirez');
fireEvent.click(sofia);
await waitFor(async () => {
expect((await a.listMembers('th_mock_2')).some((m) => m.id === 'pp_sofia')).toBe(true);
});
});
it('“#” switches the picker to channels you belong to, excluding this one', async () => {
mount();
await screen.findByText('Dan Whitaker');
fireEvent.change(screen.getByLabelText('Search people'), { target: { value: '#' } });
// Channels the mock user is in show up as import sources…
await screen.findByText('#general');
// …and the conversation being edited is never offered as its own source.
expect(screen.queryByText('#Storm crew')).toBeNull();
});
it('imports a channel roster only after confirmation, and reports what landed', async () => {
const a = mount();
await screen.findByText('Dan Whitaker');
const before = (await a.listMembers('th_mock_2')).length;
fireEvent.change(screen.getByLabelText('Search people'), { target: { value: '#' } });
fireEvent.click(await screen.findByText('#general'));
// Staged, NOT applied — picking a channel must not mutate membership on its own.
const confirm = await screen.findByRole('button', { name: /^Add \d+$/ });
expect((await a.listMembers('th_mock_2')).length).toBe(before);
fireEvent.click(confirm);
await waitFor(async () => {
expect((await a.listMembers('th_mock_2')).length).toBeGreaterThan(before);
});
await screen.findByText(/Added \d+/);
});
it('cancelling a staged import leaves membership untouched', async () => {
const a = mount();
await screen.findByText('Dan Whitaker');
const before = (await a.listMembers('th_mock_2')).length;
fireEvent.change(screen.getByLabelText('Search people'), { target: { value: '#' } });
fireEvent.click(await screen.findByText('#general'));
fireEvent.click(await screen.findByRole('button', { name: 'Cancel' }));
await screen.findByLabelText('Search people'); // back to the picker
expect((await a.listMembers('th_mock_2')).length).toBe(before);
});
it('hides the channel-import affordance when the adapter cannot bulk-add', async () => {
const a = new MockAdapter();
// A host that implements single add but not addMembers => no import path offered.
(a as { addMembers?: unknown }).addMembers = undefined;
render(
<MessagingProvider adapter={a}>
<ConversationSettings threadId="th_mock_2" title="Storm crew" membership="group" onClose={() => {}} />
</MessagingProvider>,
);
await screen.findByText('Dan Whitaker');
expect(screen.getByLabelText('Search people').getAttribute('placeholder')).toBe('Search people…');
fireEvent.change(screen.getByLabelText('Search people'), { target: { value: '#' } });
expect(screen.queryByText('#general')).toBeNull(); // '#' is just a search string here
});
});