mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-08 06:15:02 +02:00
272 lines
8.7 KiB
TypeScript
272 lines
8.7 KiB
TypeScript
/**
|
|
* Tests for unread count tracking logic.
|
|
*
|
|
* These tests verify the unread message counting behavior
|
|
* without involving React component rendering.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import type { Message, Conversation } from '../types';
|
|
|
|
/**
|
|
* Determine if a message should increment unread count.
|
|
* Extracted logic from App.tsx for testing.
|
|
*/
|
|
function shouldIncrementUnread(
|
|
msg: Message,
|
|
activeConversation: Conversation | null
|
|
): { key: string } | null {
|
|
// Only count incoming messages
|
|
if (msg.outgoing) {
|
|
return null;
|
|
}
|
|
|
|
if (msg.type === 'CHAN' && msg.conversation_key) {
|
|
const key = `channel-${msg.conversation_key}`;
|
|
// Don't count if this channel is active
|
|
if (activeConversation?.type === 'channel' && activeConversation?.id === msg.conversation_key) {
|
|
return null;
|
|
}
|
|
return { key };
|
|
}
|
|
|
|
if (msg.type === 'PRIV' && msg.conversation_key) {
|
|
const key = `contact-${msg.conversation_key}`;
|
|
// Don't count if this contact is active
|
|
if (activeConversation?.type === 'contact' && activeConversation.id === msg.conversation_key) {
|
|
return null;
|
|
}
|
|
return { key };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get unread count for a conversation from the counts map.
|
|
* Extracted logic from Sidebar.tsx for testing.
|
|
*/
|
|
function getUnreadCount(
|
|
type: 'channel' | 'contact',
|
|
id: string,
|
|
unreadCounts: Record<string, number>
|
|
): number {
|
|
return unreadCounts[`${type}-${id}`] || 0;
|
|
}
|
|
|
|
describe('shouldIncrementUnread', () => {
|
|
const createMessage = (overrides: Partial<Message>): Message => ({
|
|
id: 1,
|
|
type: 'CHAN',
|
|
conversation_key: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0', // 32-char hex channel key
|
|
text: 'Test',
|
|
sender_timestamp: null,
|
|
received_at: Date.now(),
|
|
paths: null,
|
|
txt_type: 0,
|
|
signature: null,
|
|
outgoing: false,
|
|
acked: 0,
|
|
...overrides,
|
|
});
|
|
|
|
it('returns key for incoming channel message when not viewing that channel', () => {
|
|
const msg = createMessage({
|
|
type: 'CHAN',
|
|
conversation_key: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3',
|
|
});
|
|
const activeConversation: Conversation = {
|
|
type: 'channel',
|
|
id: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5',
|
|
name: 'other',
|
|
};
|
|
|
|
const result = shouldIncrementUnread(msg, activeConversation);
|
|
|
|
expect(result).toEqual({ key: 'channel-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3' });
|
|
});
|
|
|
|
it('returns null for incoming channel message when viewing that channel', () => {
|
|
const msg = createMessage({
|
|
type: 'CHAN',
|
|
conversation_key: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3',
|
|
});
|
|
const activeConversation: Conversation = {
|
|
type: 'channel',
|
|
id: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3',
|
|
name: '#test',
|
|
};
|
|
|
|
const result = shouldIncrementUnread(msg, activeConversation);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('returns null for outgoing messages', () => {
|
|
const msg = createMessage({
|
|
type: 'CHAN',
|
|
conversation_key: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3',
|
|
outgoing: true,
|
|
});
|
|
|
|
const result = shouldIncrementUnread(msg, null);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('returns key for incoming direct message when not viewing that contact', () => {
|
|
const msg = createMessage({
|
|
type: 'PRIV',
|
|
conversation_key: 'abc123456789012345678901234567890123456789012345678901234567',
|
|
});
|
|
const activeConversation: Conversation = {
|
|
type: 'contact',
|
|
id: 'xyz999999999012345678901234567890123456789012345678901234567',
|
|
name: 'other',
|
|
};
|
|
|
|
const result = shouldIncrementUnread(msg, activeConversation);
|
|
|
|
// State key uses full public key
|
|
expect(result).toEqual({
|
|
key: 'contact-abc123456789012345678901234567890123456789012345678901234567',
|
|
});
|
|
});
|
|
|
|
it('returns null for incoming direct message when viewing that contact', () => {
|
|
const fullKey = 'abc123456789012345678901234567890123456789012345678901234567';
|
|
const msg = createMessage({
|
|
type: 'PRIV',
|
|
conversation_key: fullKey,
|
|
});
|
|
const activeConversation: Conversation = {
|
|
type: 'contact',
|
|
id: fullKey, // Same full key - exact match required
|
|
name: 'Alice',
|
|
};
|
|
|
|
const result = shouldIncrementUnread(msg, activeConversation);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('returns key when no conversation is active', () => {
|
|
const msg = createMessage({
|
|
type: 'CHAN',
|
|
conversation_key: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0',
|
|
});
|
|
|
|
const result = shouldIncrementUnread(msg, null);
|
|
|
|
expect(result).toEqual({ key: 'channel-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0' });
|
|
});
|
|
|
|
it('returns key when viewing raw packet feed', () => {
|
|
const msg = createMessage({
|
|
type: 'CHAN',
|
|
conversation_key: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1',
|
|
});
|
|
const activeConversation: Conversation = { type: 'raw', id: 'raw', name: 'Packets' };
|
|
|
|
const result = shouldIncrementUnread(msg, activeConversation);
|
|
|
|
expect(result).toEqual({ key: 'channel-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1' });
|
|
});
|
|
});
|
|
|
|
describe('getUnreadCount', () => {
|
|
it('returns count for channel by exact key match', () => {
|
|
const counts = { 'channel-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5': 3 };
|
|
|
|
expect(getUnreadCount('channel', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5', counts)).toBe(3);
|
|
});
|
|
|
|
it('returns 0 for channel with no unread', () => {
|
|
const counts = { 'channel-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5': 3 };
|
|
|
|
expect(getUnreadCount('channel', 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB9', counts)).toBe(0);
|
|
});
|
|
|
|
it('returns count for contact using full public key', () => {
|
|
const fullKey = 'abc123456789fullpublickey123456789012345678901234';
|
|
const counts = { [`contact-${fullKey}`]: 5 };
|
|
|
|
expect(getUnreadCount('contact', fullKey, counts)).toBe(5);
|
|
});
|
|
|
|
it('returns 0 for contact with no unread', () => {
|
|
const counts = { 'contact-abc123456789fullpublickey123456789012345678901234': 5 };
|
|
|
|
expect(
|
|
getUnreadCount('contact', 'xyz999999999fullkey12345678901234567890123456789', counts)
|
|
).toBe(0);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Check if a message text contains a mention of the given name in @[name] format.
|
|
* Extracted from useUnreadCounts.ts for testing.
|
|
*/
|
|
function messageContainsMention(text: string, name: string | null): boolean {
|
|
if (!name) return false;
|
|
// Escape special regex characters in the name
|
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const mentionPattern = new RegExp(`@\\[${escaped}\\]`, 'i');
|
|
return mentionPattern.test(text);
|
|
}
|
|
|
|
describe('messageContainsMention', () => {
|
|
it('returns true when text contains mention of the name', () => {
|
|
expect(messageContainsMention('Hey @[Alice] check this out', 'Alice')).toBe(true);
|
|
});
|
|
|
|
it('returns false when text does not contain the mention', () => {
|
|
expect(messageContainsMention('Hey Alice check this out', 'Alice')).toBe(false);
|
|
});
|
|
|
|
it('returns false when name is null', () => {
|
|
expect(messageContainsMention('Hey @[Alice] check this out', null)).toBe(false);
|
|
});
|
|
|
|
it('returns false when text is empty', () => {
|
|
expect(messageContainsMention('', 'Alice')).toBe(false);
|
|
});
|
|
|
|
it('matches case insensitively', () => {
|
|
expect(messageContainsMention('Hey @[ALICE] check this out', 'alice')).toBe(true);
|
|
expect(messageContainsMention('Hey @[alice] check this out', 'ALICE')).toBe(true);
|
|
});
|
|
|
|
it('handles emojis in names', () => {
|
|
expect(messageContainsMention('Hey @[FlightlessDt🥝] nice!', 'FlightlessDt🥝')).toBe(true);
|
|
expect(messageContainsMention('Hey @[🎉Party🎉]', '🎉Party🎉')).toBe(true);
|
|
});
|
|
|
|
it('handles special regex characters in names', () => {
|
|
// Names with characters that have special meaning in regex
|
|
expect(messageContainsMention('Hey @[Test.User] hello', 'Test.User')).toBe(true);
|
|
expect(messageContainsMention('Hey @[User+1] hello', 'User+1')).toBe(true);
|
|
expect(messageContainsMention('Hey @[User*Star] hello', 'User*Star')).toBe(true);
|
|
expect(messageContainsMention('Hey @[What?] hello', 'What?')).toBe(true);
|
|
});
|
|
|
|
it('does not match partial names', () => {
|
|
// @[Alice] should not match a name of just "Ali"
|
|
expect(messageContainsMention('Hey @[Alice] check this', 'Ali')).toBe(false);
|
|
});
|
|
|
|
it('handles mention at start of text', () => {
|
|
expect(messageContainsMention('@[Bob] hello there', 'Bob')).toBe(true);
|
|
});
|
|
|
|
it('handles mention at end of text', () => {
|
|
expect(messageContainsMention('hello @[Bob]', 'Bob')).toBe(true);
|
|
});
|
|
|
|
it('handles multiple mentions - matches if user is mentioned', () => {
|
|
expect(messageContainsMention('@[Alice] and @[Bob] should see this', 'Alice')).toBe(true);
|
|
expect(messageContainsMention('@[Alice] and @[Bob] should see this', 'Bob')).toBe(true);
|
|
expect(messageContainsMention('@[Alice] and @[Bob] should see this', 'Charlie')).toBe(false);
|
|
});
|
|
});
|