mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-07 22:05:14 +02:00
688 lines
24 KiB
TypeScript
688 lines
24 KiB
TypeScript
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { Sidebar } from '../components/Sidebar';
|
|
import {
|
|
CONTACT_TYPE_REPEATER,
|
|
CONTACT_TYPE_ROOM,
|
|
type Channel,
|
|
type Contact,
|
|
type Favorite,
|
|
} from '../types';
|
|
import { getStateKey, type ConversationTimes } from '../utils/conversationState';
|
|
import { PUBLIC_CHANNEL_KEY } from '../utils/publicChannel';
|
|
|
|
function makeChannel(key: string, name: string): Channel {
|
|
return {
|
|
key,
|
|
name,
|
|
is_hashtag: false,
|
|
on_radio: false,
|
|
last_read_at: null,
|
|
};
|
|
}
|
|
|
|
function makeContact(
|
|
public_key: string,
|
|
name: string,
|
|
type = 1,
|
|
overrides: Partial<Contact> = {}
|
|
): Contact {
|
|
return {
|
|
public_key,
|
|
name,
|
|
type,
|
|
flags: 0,
|
|
direct_path: null,
|
|
direct_path_len: -1,
|
|
direct_path_hash_mode: 0,
|
|
last_advert: null,
|
|
lat: null,
|
|
lon: null,
|
|
last_seen: null,
|
|
on_radio: false,
|
|
last_contacted: null,
|
|
last_read_at: null,
|
|
first_seen: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function renderSidebar(overrides?: {
|
|
unreadCounts?: Record<string, number>;
|
|
mentions?: Record<string, boolean>;
|
|
favorites?: Favorite[];
|
|
lastMessageTimes?: ConversationTimes;
|
|
channels?: Channel[];
|
|
isConversationNotificationsEnabled?: (type: 'channel' | 'contact', id: string) => boolean;
|
|
}) {
|
|
const aliceName = 'Alice';
|
|
const roomName = 'Ops Board';
|
|
const publicChannel = makeChannel('AA'.repeat(16), 'Public');
|
|
const flightChannel = makeChannel('BB'.repeat(16), '#flight');
|
|
const opsChannel = makeChannel('CC'.repeat(16), '#ops');
|
|
const alice = makeContact('11'.repeat(32), aliceName);
|
|
const board = makeContact('33'.repeat(32), roomName, CONTACT_TYPE_ROOM);
|
|
const relay = makeContact('22'.repeat(32), 'Relay', CONTACT_TYPE_REPEATER);
|
|
|
|
const unreadCounts = overrides?.unreadCounts ?? {
|
|
[getStateKey('channel', flightChannel.key)]: 2,
|
|
[getStateKey('channel', opsChannel.key)]: 1,
|
|
[getStateKey('contact', alice.public_key)]: 3,
|
|
[getStateKey('contact', board.public_key)]: 5,
|
|
[getStateKey('contact', relay.public_key)]: 4,
|
|
};
|
|
|
|
const favorites = overrides?.favorites ?? [{ type: 'channel', id: flightChannel.key }];
|
|
const channels = overrides?.channels ?? [publicChannel, flightChannel, opsChannel];
|
|
const onSelectConversation = vi.fn();
|
|
|
|
const view = render(
|
|
<Sidebar
|
|
contacts={[alice, board, relay]}
|
|
channels={channels}
|
|
activeConversation={null}
|
|
onSelectConversation={onSelectConversation}
|
|
onNewMessage={vi.fn()}
|
|
lastMessageTimes={overrides?.lastMessageTimes ?? {}}
|
|
unreadCounts={unreadCounts}
|
|
mentions={overrides?.mentions ?? {}}
|
|
showCracker={false}
|
|
crackerRunning={false}
|
|
onToggleCracker={vi.fn()}
|
|
onMarkAllRead={vi.fn()}
|
|
favorites={favorites}
|
|
legacySortOrder="recent"
|
|
isConversationNotificationsEnabled={overrides?.isConversationNotificationsEnabled}
|
|
/>
|
|
);
|
|
|
|
return { ...view, flightChannel, opsChannel, aliceName, roomName, onSelectConversation };
|
|
}
|
|
|
|
function getSectionHeaderContainer(title: string): HTMLElement {
|
|
const btn = screen.getByRole('button', { name: title });
|
|
const container = btn.closest('div');
|
|
if (!container) throw new Error(`Missing header container for section ${title}`);
|
|
return container;
|
|
}
|
|
|
|
describe('Sidebar section summaries', () => {
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
});
|
|
|
|
it('shows muted section unread totals in each visible section header', () => {
|
|
renderSidebar();
|
|
|
|
expect(within(getSectionHeaderContainer('Favorites')).getByText('2')).toBeInTheDocument();
|
|
expect(within(getSectionHeaderContainer('Channels')).getByText('1')).toBeInTheDocument();
|
|
expect(within(getSectionHeaderContainer('Contacts')).getByText('3')).toBeInTheDocument();
|
|
expect(within(getSectionHeaderContainer('Room Servers')).getByText('5')).toBeInTheDocument();
|
|
expect(within(getSectionHeaderContainer('Repeaters')).getByText('4')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders a full add channel/contact button above search and calls onNewMessage', () => {
|
|
const onNewMessage = vi.fn();
|
|
|
|
render(
|
|
<Sidebar
|
|
contacts={[]}
|
|
channels={[makeChannel(PUBLIC_CHANNEL_KEY, 'Public')]}
|
|
activeConversation={null}
|
|
onSelectConversation={vi.fn()}
|
|
onNewMessage={onNewMessage}
|
|
lastMessageTimes={{}}
|
|
unreadCounts={{}}
|
|
mentions={{}}
|
|
showCracker={false}
|
|
crackerRunning={false}
|
|
onToggleCracker={vi.fn()}
|
|
onMarkAllRead={vi.fn()}
|
|
favorites={[]}
|
|
legacySortOrder="recent"
|
|
/>
|
|
);
|
|
|
|
const addButton = screen.getByRole('button', { name: 'Add channel or contact' });
|
|
const search = screen.getByLabelText('Search conversations');
|
|
const nav = screen.getByRole('navigation', { name: 'Conversations' });
|
|
const toolsButton = screen.getByRole('button', { name: 'Tools' });
|
|
|
|
expect(addButton).toHaveTextContent('Add Channel/Contact');
|
|
expect(
|
|
addButton.compareDocumentPosition(search) & Node.DOCUMENT_POSITION_FOLLOWING
|
|
).toBeTruthy();
|
|
expect(nav.compareDocumentPosition(search) & Node.DOCUMENT_POSITION_CONTAINED_BY).toBeTruthy();
|
|
expect(
|
|
search.compareDocumentPosition(toolsButton) & Node.DOCUMENT_POSITION_FOLLOWING
|
|
).toBeTruthy();
|
|
|
|
fireEvent.click(addButton);
|
|
expect(onNewMessage).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('turns favorites and channels rollups red when they contain a mention', () => {
|
|
renderSidebar({
|
|
mentions: {
|
|
[getStateKey('channel', 'BB'.repeat(16))]: true,
|
|
[getStateKey('channel', 'CC'.repeat(16))]: true,
|
|
},
|
|
});
|
|
|
|
expect(within(getSectionHeaderContainer('Favorites')).getByText('2')).toHaveClass(
|
|
'bg-badge-mention',
|
|
'text-badge-mention-foreground'
|
|
);
|
|
expect(within(getSectionHeaderContainer('Channels')).getByText('1')).toHaveClass(
|
|
'bg-badge-mention',
|
|
'text-badge-mention-foreground'
|
|
);
|
|
});
|
|
|
|
it('turns contact row badges red while the contacts rollup remains red', () => {
|
|
const { aliceName } = renderSidebar();
|
|
|
|
expect(within(getSectionHeaderContainer('Contacts')).getByText('3')).toHaveClass(
|
|
'bg-badge-mention',
|
|
'text-badge-mention-foreground'
|
|
);
|
|
|
|
const aliceRow = screen.getByText(aliceName).closest('div');
|
|
if (!aliceRow) throw new Error('Missing Alice row');
|
|
expect(within(aliceRow).getByText('3')).toHaveClass(
|
|
'bg-badge-mention',
|
|
'text-badge-mention-foreground'
|
|
);
|
|
});
|
|
|
|
it('turns favorite contact row badges red', () => {
|
|
const { aliceName } = renderSidebar({
|
|
favorites: [{ type: 'contact', id: '11'.repeat(32) }],
|
|
});
|
|
|
|
const aliceRow = screen.getByText(aliceName).closest('div');
|
|
if (!aliceRow) throw new Error('Missing Alice row');
|
|
expect(within(aliceRow).getByText('3')).toHaveClass(
|
|
'bg-badge-mention',
|
|
'text-badge-mention-foreground'
|
|
);
|
|
});
|
|
|
|
it('keeps repeater row badges neutral', () => {
|
|
renderSidebar();
|
|
|
|
const relayRow = screen.getByText('Relay').closest('div');
|
|
if (!relayRow) throw new Error('Missing Relay row');
|
|
expect(within(relayRow).getByText('4')).toHaveClass(
|
|
'bg-badge-unread/90',
|
|
'text-badge-unread-foreground'
|
|
);
|
|
});
|
|
|
|
it('renders room servers in their own section', () => {
|
|
const { roomName } = renderSidebar();
|
|
|
|
expect(screen.getByRole('button', { name: 'Room Servers' })).toBeInTheDocument();
|
|
expect(screen.getByText(roomName)).toBeInTheDocument();
|
|
});
|
|
|
|
it('expands collapsed sections during search and restores collapse state after clearing search', async () => {
|
|
const { opsChannel, aliceName, roomName } = renderSidebar();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Tools' }));
|
|
fireEvent.click(screen.getByRole('button', { name: 'Channels' }));
|
|
fireEvent.click(screen.getByRole('button', { name: 'Contacts' }));
|
|
fireEvent.click(screen.getByRole('button', { name: 'Room Servers' }));
|
|
|
|
expect(screen.queryByText('Packet Feed')).not.toBeInTheDocument();
|
|
expect(screen.queryByText(opsChannel.name)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(aliceName)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(roomName)).not.toBeInTheDocument();
|
|
|
|
const search = screen.getByLabelText('Search conversations');
|
|
fireEvent.change(search, { target: { value: 'alice' } });
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(aliceName)).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.change(search, { target: { value: '' } });
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Packet Feed')).not.toBeInTheDocument();
|
|
expect(screen.queryByText(opsChannel.name)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(aliceName)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(roomName)).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('persists collapsed section state across unmount and remount', () => {
|
|
const { opsChannel, aliceName, roomName, unmount } = renderSidebar();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Tools' }));
|
|
fireEvent.click(screen.getByRole('button', { name: 'Channels' }));
|
|
fireEvent.click(screen.getByRole('button', { name: 'Contacts' }));
|
|
fireEvent.click(screen.getByRole('button', { name: 'Room Servers' }));
|
|
|
|
expect(screen.queryByText('Packet Feed')).not.toBeInTheDocument();
|
|
expect(screen.queryByText(opsChannel.name)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(aliceName)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(roomName)).not.toBeInTheDocument();
|
|
|
|
unmount();
|
|
renderSidebar();
|
|
|
|
expect(screen.queryByText('Packet Feed')).not.toBeInTheDocument();
|
|
expect(screen.queryByText(opsChannel.name)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(aliceName)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(roomName)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('renders same-name channels when keys differ and allows selecting both', () => {
|
|
const publicChannel = makeChannel('AA'.repeat(16), 'Public');
|
|
const channelA = makeChannel('DD'.repeat(16), '#shared');
|
|
const channelB = makeChannel('EE'.repeat(16), '#shared');
|
|
const onSelectConversation = vi.fn();
|
|
|
|
render(
|
|
<Sidebar
|
|
contacts={[]}
|
|
channels={[publicChannel, channelA, channelB]}
|
|
activeConversation={null}
|
|
onSelectConversation={onSelectConversation}
|
|
onNewMessage={vi.fn()}
|
|
lastMessageTimes={{}}
|
|
unreadCounts={{}}
|
|
mentions={{}}
|
|
showCracker={false}
|
|
crackerRunning={false}
|
|
onToggleCracker={vi.fn()}
|
|
onMarkAllRead={vi.fn()}
|
|
favorites={[]}
|
|
legacySortOrder="recent"
|
|
/>
|
|
);
|
|
|
|
const sharedRows = screen.getAllByText('#shared');
|
|
expect(sharedRows).toHaveLength(2);
|
|
|
|
fireEvent.click(sharedRows[0]);
|
|
fireEvent.click(sharedRows[1]);
|
|
|
|
const selectedIds = onSelectConversation.mock.calls.map(([conv]) => conv.id);
|
|
expect(new Set(selectedIds)).toEqual(new Set([channelA.key, channelB.key]));
|
|
});
|
|
|
|
it('shows a notification bell for conversations with notifications enabled', () => {
|
|
const { aliceName } = renderSidebar({
|
|
unreadCounts: {},
|
|
isConversationNotificationsEnabled: (type, id) =>
|
|
(type === 'contact' && id === '11'.repeat(32)) ||
|
|
(type === 'channel' && id === 'BB'.repeat(16)),
|
|
});
|
|
|
|
const aliceRow = screen.getByText(aliceName).closest('div');
|
|
const flightRow = screen.getByText('#flight').closest('div');
|
|
if (!aliceRow || !flightRow) throw new Error('Missing sidebar rows');
|
|
|
|
expect(within(aliceRow).getByLabelText('Notifications enabled')).toBeInTheDocument();
|
|
expect(within(flightRow).getByLabelText('Notifications enabled')).toBeInTheDocument();
|
|
});
|
|
|
|
it('keeps the notification bell to the left of the unread pill when both are present', () => {
|
|
const { aliceName } = renderSidebar({
|
|
unreadCounts: {
|
|
[getStateKey('contact', '11'.repeat(32))]: 3,
|
|
},
|
|
isConversationNotificationsEnabled: (type, id) =>
|
|
type === 'contact' && id === '11'.repeat(32),
|
|
});
|
|
|
|
const aliceRow = screen.getByText(aliceName).closest('div');
|
|
if (!aliceRow) throw new Error('Missing Alice row');
|
|
|
|
const bell = within(aliceRow).getByLabelText('Notifications enabled');
|
|
const unread = within(aliceRow).getByText('3');
|
|
expect(bell.compareDocumentPosition(unread) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
});
|
|
|
|
it('shows the trace tool row and selects it', () => {
|
|
const { onSelectConversation } = renderSidebar();
|
|
|
|
fireEvent.click(screen.getByText('Trace'));
|
|
|
|
expect(onSelectConversation).toHaveBeenCalledWith({
|
|
type: 'trace',
|
|
id: 'trace',
|
|
name: 'Trace',
|
|
});
|
|
});
|
|
|
|
it('sorts each section independently and persists per-section sort preferences', () => {
|
|
const publicChannel = makeChannel('AA'.repeat(16), 'Public');
|
|
const zebraChannel = makeChannel('BB'.repeat(16), '#zebra');
|
|
const alphaChannel = makeChannel('CC'.repeat(16), '#alpha');
|
|
const zed = makeContact('11'.repeat(32), 'Zed', 1, { last_advert: 150 });
|
|
const amy = makeContact('22'.repeat(32), 'Amy');
|
|
const zebraRoom = makeContact('55'.repeat(32), 'Zebra Room', CONTACT_TYPE_ROOM, {
|
|
last_seen: 100,
|
|
});
|
|
const alphaRoom = makeContact('66'.repeat(32), 'Alpha Room', CONTACT_TYPE_ROOM, {
|
|
last_advert: 300,
|
|
});
|
|
const relayZulu = makeContact('33'.repeat(32), 'Zulu Relay', CONTACT_TYPE_REPEATER, {
|
|
last_seen: 100,
|
|
});
|
|
const relayAlpha = makeContact('44'.repeat(32), 'Alpha Relay', CONTACT_TYPE_REPEATER, {
|
|
last_seen: 300,
|
|
});
|
|
|
|
const props = {
|
|
contacts: [zed, amy, zebraRoom, alphaRoom, relayZulu, relayAlpha],
|
|
channels: [publicChannel, zebraChannel, alphaChannel],
|
|
activeConversation: null,
|
|
onSelectConversation: vi.fn(),
|
|
onNewMessage: vi.fn(),
|
|
lastMessageTimes: {
|
|
[getStateKey('channel', zebraChannel.key)]: 300,
|
|
[getStateKey('channel', alphaChannel.key)]: 100,
|
|
[getStateKey('contact', zed.public_key)]: 200,
|
|
[getStateKey('contact', zebraRoom.public_key)]: 350,
|
|
},
|
|
unreadCounts: {},
|
|
mentions: {},
|
|
showCracker: false,
|
|
crackerRunning: false,
|
|
onToggleCracker: vi.fn(),
|
|
onMarkAllRead: vi.fn(),
|
|
favorites: [],
|
|
legacySortOrder: 'recent' as const,
|
|
};
|
|
|
|
const getChannelsOrder = () => screen.getAllByText(/^#/).map((node) => node.textContent);
|
|
const getContactsOrder = () =>
|
|
screen
|
|
.getAllByText(/^(Amy|Zed)$/)
|
|
.map((node) => node.textContent)
|
|
.filter((text): text is string => Boolean(text));
|
|
const getRepeatersOrder = () =>
|
|
screen
|
|
.getAllByText(/Relay$/)
|
|
.map((node) => node.textContent)
|
|
.filter((text): text is string => Boolean(text));
|
|
const getRoomsOrder = () =>
|
|
screen
|
|
.getAllByText(/Room$/)
|
|
.map((node) => node.textContent)
|
|
.filter((text): text is string => Boolean(text));
|
|
|
|
const { unmount } = render(<Sidebar {...props} />);
|
|
|
|
expect(getChannelsOrder()).toEqual(['#zebra', '#alpha']);
|
|
expect(getContactsOrder()).toEqual(['Zed', 'Amy']);
|
|
expect(getRoomsOrder()).toEqual(['Zebra Room', 'Alpha Room']);
|
|
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Sort Channels alphabetically' }));
|
|
fireEvent.click(screen.getByRole('button', { name: 'Sort Contacts alphabetically' }));
|
|
fireEvent.click(screen.getByRole('button', { name: 'Sort Room Servers alphabetically' }));
|
|
|
|
expect(getChannelsOrder()).toEqual(['#alpha', '#zebra']);
|
|
expect(getContactsOrder()).toEqual(['Amy', 'Zed']);
|
|
expect(getRoomsOrder()).toEqual(['Alpha Room', 'Zebra Room']);
|
|
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
|
|
|
|
unmount();
|
|
render(<Sidebar {...props} />);
|
|
|
|
expect(getChannelsOrder()).toEqual(['#alpha', '#zebra']);
|
|
expect(getContactsOrder()).toEqual(['Amy', 'Zed']);
|
|
expect(getRoomsOrder()).toEqual(['Alpha Room', 'Zebra Room']);
|
|
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
|
|
});
|
|
|
|
it('sorts room servers like contacts by DM recency first, then advert recency', () => {
|
|
const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public');
|
|
const dmRecentRoom = makeContact('77'.repeat(32), 'DM Recent Room', CONTACT_TYPE_ROOM, {
|
|
last_advert: 100,
|
|
});
|
|
const advertOnlyRoom = makeContact('88'.repeat(32), 'Advert Only Room', CONTACT_TYPE_ROOM, {
|
|
last_seen: 300,
|
|
});
|
|
const noRecencyRoom = makeContact('99'.repeat(32), 'No Recency Room', CONTACT_TYPE_ROOM);
|
|
|
|
render(
|
|
<Sidebar
|
|
contacts={[noRecencyRoom, advertOnlyRoom, dmRecentRoom]}
|
|
channels={[publicChannel]}
|
|
activeConversation={null}
|
|
onSelectConversation={vi.fn()}
|
|
onNewMessage={vi.fn()}
|
|
lastMessageTimes={{
|
|
[getStateKey('contact', dmRecentRoom.public_key)]: 400,
|
|
}}
|
|
unreadCounts={{}}
|
|
mentions={{}}
|
|
showCracker={false}
|
|
crackerRunning={false}
|
|
onToggleCracker={vi.fn()}
|
|
onMarkAllRead={vi.fn()}
|
|
favorites={[]}
|
|
legacySortOrder="recent"
|
|
/>
|
|
);
|
|
|
|
const roomRows = screen
|
|
.getAllByText(/Room$/)
|
|
.map((node) => node.textContent)
|
|
.filter((text): text is string => Boolean(text));
|
|
|
|
expect(roomRows).toEqual(['DM Recent Room', 'Advert Only Room', 'No Recency Room']);
|
|
});
|
|
|
|
it('sorts contacts by DM recency first, then advert recency, then no-recency at the bottom', () => {
|
|
const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public');
|
|
const dmRecent = makeContact('11'.repeat(32), 'DM Recent', 1, { last_advert: 100 });
|
|
const advertOnly = makeContact('22'.repeat(32), 'Advert Only', 1, { last_seen: 300 });
|
|
const noRecency = makeContact('33'.repeat(32), 'No Recency');
|
|
|
|
render(
|
|
<Sidebar
|
|
contacts={[noRecency, advertOnly, dmRecent]}
|
|
channels={[publicChannel]}
|
|
activeConversation={null}
|
|
onSelectConversation={vi.fn()}
|
|
onNewMessage={vi.fn()}
|
|
lastMessageTimes={{
|
|
[getStateKey('contact', dmRecent.public_key)]: 400,
|
|
}}
|
|
unreadCounts={{}}
|
|
mentions={{}}
|
|
showCracker={false}
|
|
crackerRunning={false}
|
|
onToggleCracker={vi.fn()}
|
|
onMarkAllRead={vi.fn()}
|
|
favorites={[]}
|
|
legacySortOrder="recent"
|
|
/>
|
|
);
|
|
|
|
const contactRows = screen
|
|
.getAllByText(/^(DM Recent|Advert Only|No Recency)$/)
|
|
.map((node) => node.textContent)
|
|
.filter((text): text is string => Boolean(text));
|
|
|
|
expect(contactRows).toEqual(['DM Recent', 'Advert Only', 'No Recency']);
|
|
});
|
|
|
|
it('sorts repeaters by heard recency even when message times disagree', () => {
|
|
const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public');
|
|
const staleMessageRelay = makeContact(
|
|
'44'.repeat(32),
|
|
'Stale Message Relay',
|
|
CONTACT_TYPE_REPEATER,
|
|
{
|
|
last_seen: 100,
|
|
}
|
|
);
|
|
const freshAdvertRelay = makeContact(
|
|
'55'.repeat(32),
|
|
'Fresh Advert Relay',
|
|
CONTACT_TYPE_REPEATER,
|
|
{
|
|
last_advert: 500,
|
|
}
|
|
);
|
|
|
|
render(
|
|
<Sidebar
|
|
contacts={[staleMessageRelay, freshAdvertRelay]}
|
|
channels={[publicChannel]}
|
|
activeConversation={null}
|
|
onSelectConversation={vi.fn()}
|
|
onNewMessage={vi.fn()}
|
|
lastMessageTimes={{
|
|
[getStateKey('contact', staleMessageRelay.public_key)]: 1000,
|
|
[getStateKey('contact', freshAdvertRelay.public_key)]: 50,
|
|
}}
|
|
unreadCounts={{}}
|
|
mentions={{}}
|
|
showCracker={false}
|
|
crackerRunning={false}
|
|
onToggleCracker={vi.fn()}
|
|
onMarkAllRead={vi.fn()}
|
|
favorites={[]}
|
|
legacySortOrder="recent"
|
|
/>
|
|
);
|
|
|
|
const repeaterRows = screen
|
|
.getAllByText(/Relay$/)
|
|
.map((node) => node.textContent)
|
|
.filter((text): text is string => Boolean(text));
|
|
|
|
expect(repeaterRows).toEqual(['Fresh Advert Relay', 'Stale Message Relay']);
|
|
});
|
|
|
|
it('pins only the canonical Public channel to the top of channel sorting', () => {
|
|
const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public');
|
|
const fakePublic = makeChannel('DD'.repeat(16), 'Public');
|
|
const alphaChannel = makeChannel('CC'.repeat(16), '#alpha');
|
|
const onSelectConversation = vi.fn();
|
|
|
|
render(
|
|
<Sidebar
|
|
contacts={[]}
|
|
channels={[fakePublic, alphaChannel, publicChannel]}
|
|
activeConversation={null}
|
|
onSelectConversation={onSelectConversation}
|
|
onNewMessage={vi.fn()}
|
|
lastMessageTimes={{}}
|
|
unreadCounts={{}}
|
|
mentions={{}}
|
|
showCracker={false}
|
|
crackerRunning={false}
|
|
onToggleCracker={vi.fn()}
|
|
onMarkAllRead={vi.fn()}
|
|
favorites={[]}
|
|
legacySortOrder="alpha"
|
|
/>
|
|
);
|
|
|
|
fireEvent.click(screen.getAllByText('Public')[0]);
|
|
|
|
expect(onSelectConversation).toHaveBeenCalledWith({
|
|
type: 'channel',
|
|
id: PUBLIC_CHANNEL_KEY,
|
|
name: 'Public',
|
|
});
|
|
});
|
|
|
|
it('sorts favorites independently and persists the favorites sort preference', () => {
|
|
const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public');
|
|
const zed = makeContact('11'.repeat(32), 'Zed', 1, { last_advert: 150 });
|
|
const amy = makeContact('22'.repeat(32), 'Amy');
|
|
|
|
const props = {
|
|
contacts: [zed, amy],
|
|
channels: [publicChannel],
|
|
activeConversation: null,
|
|
onSelectConversation: vi.fn(),
|
|
onNewMessage: vi.fn(),
|
|
lastMessageTimes: {
|
|
[getStateKey('contact', zed.public_key)]: 200,
|
|
},
|
|
unreadCounts: {},
|
|
mentions: {},
|
|
showCracker: false,
|
|
crackerRunning: false,
|
|
onToggleCracker: vi.fn(),
|
|
onMarkAllRead: vi.fn(),
|
|
favorites: [
|
|
{ type: 'contact', id: zed.public_key },
|
|
{ type: 'contact', id: amy.public_key },
|
|
] satisfies Favorite[],
|
|
legacySortOrder: 'recent' as const,
|
|
};
|
|
|
|
const getFavoritesOrder = () =>
|
|
screen
|
|
.getAllByText(/^(Amy|Zed)$/)
|
|
.map((node) => node.textContent)
|
|
.filter((text): text is string => Boolean(text));
|
|
|
|
const { unmount } = render(<Sidebar {...props} />);
|
|
|
|
expect(getFavoritesOrder()).toEqual(['Zed', 'Amy']);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Sort Favorites alphabetically' }));
|
|
|
|
expect(getFavoritesOrder()).toEqual(['Amy', 'Zed']);
|
|
|
|
unmount();
|
|
render(<Sidebar {...props} />);
|
|
|
|
expect(getFavoritesOrder()).toEqual(['Amy', 'Zed']);
|
|
});
|
|
|
|
it('seeds favorites sort from the legacy global sort order when section prefs are missing', () => {
|
|
localStorage.setItem('remoteterm-sortOrder', 'alpha');
|
|
|
|
const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public');
|
|
const zed = makeContact('11'.repeat(32), 'Zed', 1, { last_advert: 150 });
|
|
const amy = makeContact('22'.repeat(32), 'Amy');
|
|
|
|
render(
|
|
<Sidebar
|
|
contacts={[zed, amy]}
|
|
channels={[publicChannel]}
|
|
activeConversation={null}
|
|
onSelectConversation={vi.fn()}
|
|
onNewMessage={vi.fn()}
|
|
lastMessageTimes={{
|
|
[getStateKey('contact', zed.public_key)]: 200,
|
|
}}
|
|
unreadCounts={{}}
|
|
mentions={{}}
|
|
showCracker={false}
|
|
crackerRunning={false}
|
|
onToggleCracker={vi.fn()}
|
|
onMarkAllRead={vi.fn()}
|
|
favorites={[
|
|
{ type: 'contact', id: zed.public_key },
|
|
{ type: 'contact', id: amy.public_key },
|
|
]}
|
|
/>
|
|
);
|
|
|
|
const favoriteRows = screen
|
|
.getAllByText(/^(Amy|Zed)$/)
|
|
.map((node) => node.textContent)
|
|
.filter((text): text is string => Boolean(text));
|
|
|
|
expect(favoriteRows).toEqual(['Amy', 'Zed']);
|
|
expect(screen.getByRole('button', { name: 'Sort Favorites by recent' })).toBeInTheDocument();
|
|
});
|
|
});
|