Add bulk room add

This commit is contained in:
Jack Kingsman
2026-04-02 00:19:25 -07:00
parent ead1774cd3
commit 4420d44838
14 changed files with 764 additions and 99 deletions
@@ -0,0 +1,46 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { BulkAddChannelResultModal } from '../components/BulkAddChannelResultModal';
describe('BulkAddChannelResultModal', () => {
it('renders links only for newly created rooms', () => {
render(
<BulkAddChannelResultModal
open
onClose={() => {}}
result={{
created_channels: [
{
key: 'AA'.repeat(16),
name: '#ops',
is_hashtag: true,
on_radio: false,
last_read_at: null,
},
{
key: 'BB'.repeat(16),
name: '#mesh-room',
is_hashtag: true,
on_radio: false,
last_read_at: null,
},
],
existing_count: 3,
invalid_names: ['bad_room'],
decrypt_started: true,
decrypt_total_packets: 8,
message: 'Created 2 rooms',
}}
/>
);
const opsLink = screen.getByRole('link', { name: '#ops' });
const meshLink = screen.getByRole('link', { name: '#mesh-room' });
expect(opsLink.getAttribute('href')).toContain('#channel/');
expect(meshLink.getAttribute('href')).toContain('#channel/');
expect(screen.queryByRole('link', { name: /bad_room/i })).toBeNull();
expect(screen.getByText(/Ignored invalid room names: bad_room/)).toBeTruthy();
});
});
@@ -27,6 +27,7 @@ describe('NewMessageModal form reset', () => {
const onCreateContact = vi.fn().mockResolvedValue(undefined);
const onCreateChannel = vi.fn().mockResolvedValue(undefined);
const onCreateHashtagChannel = vi.fn().mockResolvedValue(undefined);
const onBulkAddHashtagChannels = vi.fn().mockResolvedValue(undefined);
beforeEach(() => {
vi.clearAllMocks();
@@ -44,6 +45,7 @@ describe('NewMessageModal form reset', () => {
onCreateContact={onCreateContact}
onCreateChannel={onCreateChannel}
onCreateHashtagChannel={onCreateHashtagChannel}
onBulkAddHashtagChannels={onBulkAddHashtagChannels}
{...overrides}
/>
);
@@ -111,6 +113,53 @@ describe('NewMessageModal form reset', () => {
});
});
describe('bulk hashtag tab', () => {
it('is only visible when enabled', () => {
renderModal();
expect(screen.queryByRole('tab', { name: 'Bulk Add Channel' })).toBeNull();
});
it('opens on the bulk tab when enabled and submits normalized room names', async () => {
const user = userEvent.setup();
renderModal(true, { showBulkAddChannelTab: true });
await waitFor(() => {
expect(screen.getByRole('tab', { name: 'Bulk Add Channel' })).toHaveAttribute(
'data-state',
'active'
);
});
await user.type(
screen.getByRole('textbox', { name: 'Bulk channel names' }),
'#Ops{enter}mesh-room another-room #Ops'
);
await user.click(screen.getByRole('button', { name: 'Add Channels' }));
await waitFor(() => {
expect(onBulkAddHashtagChannels).toHaveBeenCalledWith(
['#ops', '#mesh-room', '#another-room'],
false
);
});
expect(onClose).toHaveBeenCalled();
});
it('shows invalid bulk room names before submitting', async () => {
const user = userEvent.setup();
renderModal(true, { showBulkAddChannelTab: true });
await user.type(
screen.getByRole('textbox', { name: 'Bulk channel names' }),
'good-room bad_room'
);
await user.click(screen.getByRole('button', { name: 'Add Channels' }));
expect(onBulkAddHashtagChannels).not.toHaveBeenCalled();
expect(screen.getByText('Invalid room names: bad_room')).toBeTruthy();
});
});
describe('new-contact tab', () => {
it('clears name and key after successful Create', async () => {
const user = userEvent.setup();
@@ -9,7 +9,7 @@ import { act, renderHook } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { useContactsAndChannels } from '../hooks/useContactsAndChannels';
import type { Contact } from '../types';
import type { BulkCreateHashtagChannelsResult, Contact } from '../types';
// Mock api module
vi.mock('../api', () => ({
@@ -18,6 +18,7 @@ vi.mock('../api', () => ({
getChannels: vi.fn(),
createContact: vi.fn(),
createChannel: vi.fn(),
bulkCreateHashtagChannels: vi.fn(),
deleteContact: vi.fn(),
deleteChannel: vi.fn(),
decryptHistoricalPackets: vi.fn(),
@@ -171,4 +172,41 @@ describe('useContactsAndChannels', () => {
expect(api.getContacts).toHaveBeenCalledTimes(2);
});
});
describe('bulk hashtag creation', () => {
it('refreshes channels and returns the backend result', async () => {
const { api } = await import('../api');
const resultPayload: BulkCreateHashtagChannelsResult = {
created_channels: [
{
key: 'AA'.repeat(16),
name: '#ops',
is_hashtag: true,
on_radio: false,
last_read_at: null,
},
],
existing_count: 1,
invalid_names: [],
decrypt_started: true,
decrypt_total_packets: 12,
message: 'Created 1 room',
};
vi.mocked(api.bulkCreateHashtagChannels).mockResolvedValueOnce(resultPayload);
vi.mocked(api.getChannels).mockResolvedValueOnce(resultPayload.created_channels);
vi.mocked(api.getUndecryptedPacketCount).mockResolvedValueOnce({ count: 9 });
const { result } = renderUseContactsAndChannels();
let response: BulkCreateHashtagChannelsResult | null = null;
await act(async () => {
response = await result.current.handleBulkCreateHashtagChannels(['#ops'], true);
});
expect(api.bulkCreateHashtagChannels).toHaveBeenCalledWith(['#ops'], true);
expect(api.getChannels).toHaveBeenCalled();
expect(api.getUndecryptedPacketCount).toHaveBeenCalled();
expect(response).toEqual(resultPayload);
});
});
});