mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 17:23:05 +02:00
Finish frontend phase 3
This commit is contained in:
@@ -8,12 +8,12 @@
|
||||
* between backend and frontend - both sides test against the same data.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fixtures from './fixtures/websocket_events.json';
|
||||
import { getStateKey } from '../utils/conversationState';
|
||||
import { mergeContactIntoList } from '../utils/contactMerge';
|
||||
import { ConversationMessageCache } from '../hooks/useConversationMessages';
|
||||
import { getMessageContentKey } from '../utils/messageIdentity';
|
||||
import * as messageCache from '../messageCache';
|
||||
import type { Contact, Message } from '../types';
|
||||
|
||||
/**
|
||||
@@ -25,6 +25,7 @@ interface MockState {
|
||||
unreadCounts: Record<string, number>;
|
||||
lastMessageTimes: Record<string, number>;
|
||||
seenActiveContent: Set<string>;
|
||||
messageCache: ConversationMessageCache;
|
||||
}
|
||||
|
||||
function createMockState(): MockState {
|
||||
@@ -33,6 +34,7 @@ function createMockState(): MockState {
|
||||
unreadCounts: {},
|
||||
lastMessageTimes: {},
|
||||
seenActiveContent: new Set(),
|
||||
messageCache: new ConversationMessageCache(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,7 +70,7 @@ function handleMessageEvent(
|
||||
state.lastMessageTimes[stateKey] = msg.received_at;
|
||||
|
||||
if (!isForActiveConversation) {
|
||||
const isNew = messageCache.addMessage(msg.conversation_key, msg);
|
||||
const isNew = state.messageCache.addMessage(msg.conversation_key, msg);
|
||||
if (!msg.outgoing && isNew) {
|
||||
state.unreadCounts[stateKey] = (state.unreadCounts[stateKey] || 0) + 1;
|
||||
unreadIncremented = true;
|
||||
@@ -78,11 +80,6 @@ function handleMessageEvent(
|
||||
return { added, unreadIncremented };
|
||||
}
|
||||
|
||||
// Clear messageCache between tests to avoid cross-test contamination
|
||||
beforeEach(() => {
|
||||
messageCache.clear();
|
||||
});
|
||||
|
||||
describe('Integration: Channel Message Events', () => {
|
||||
const fixture = fixtures.channel_message;
|
||||
|
||||
@@ -342,11 +339,8 @@ describe('Integration: Contact Merge', () => {
|
||||
// --- ACK + messageCache propagation tests ---
|
||||
|
||||
describe('Integration: ACK + messageCache propagation', () => {
|
||||
beforeEach(() => {
|
||||
messageCache.clear();
|
||||
});
|
||||
|
||||
it('updateAck updates acked count on cached message', () => {
|
||||
const messageCache = new ConversationMessageCache();
|
||||
const msg: Message = {
|
||||
id: 100,
|
||||
type: 'PRIV',
|
||||
@@ -372,6 +366,7 @@ describe('Integration: ACK + messageCache propagation', () => {
|
||||
});
|
||||
|
||||
it('updateAck updates paths when longer', () => {
|
||||
const messageCache = new ConversationMessageCache();
|
||||
const msg: Message = {
|
||||
id: 101,
|
||||
type: 'PRIV',
|
||||
@@ -401,6 +396,7 @@ describe('Integration: ACK + messageCache propagation', () => {
|
||||
});
|
||||
|
||||
it('preserves higher existing ack count (max semantics)', () => {
|
||||
const messageCache = new ConversationMessageCache();
|
||||
const msg: Message = {
|
||||
id: 102,
|
||||
type: 'PRIV',
|
||||
@@ -426,6 +422,7 @@ describe('Integration: ACK + messageCache propagation', () => {
|
||||
});
|
||||
|
||||
it('is a no-op for unknown message ID', () => {
|
||||
const messageCache = new ConversationMessageCache();
|
||||
const msg: Message = {
|
||||
id: 103,
|
||||
type: 'PRIV',
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import * as messageCache from '../messageCache';
|
||||
import { MAX_CACHED_CONVERSATIONS, MAX_MESSAGES_PER_ENTRY } from '../messageCache';
|
||||
import {
|
||||
ConversationMessageCache,
|
||||
MAX_CACHED_CONVERSATIONS,
|
||||
MAX_MESSAGES_PER_ENTRY,
|
||||
reconcileConversationMessages,
|
||||
} from '../hooks/useConversationMessages';
|
||||
import type { Message } from '../types';
|
||||
|
||||
function createMessage(overrides: Partial<Message> = {}): Message {
|
||||
@@ -31,8 +35,10 @@ function createEntry(messages: Message[] = [], hasOlderMessages = false) {
|
||||
}
|
||||
|
||||
describe('messageCache', () => {
|
||||
let messageCache: ConversationMessageCache;
|
||||
|
||||
beforeEach(() => {
|
||||
messageCache.clear();
|
||||
messageCache = new ConversationMessageCache();
|
||||
});
|
||||
|
||||
describe('get/set', () => {
|
||||
@@ -337,7 +343,7 @@ describe('messageCache', () => {
|
||||
createMessage({ id: 3, acked: 1 }),
|
||||
];
|
||||
|
||||
expect(messageCache.reconcile(msgs, fetched)).toBeNull();
|
||||
expect(reconcileConversationMessages(msgs, fetched)).toBeNull();
|
||||
});
|
||||
|
||||
it('detects new messages missing from cache', () => {
|
||||
@@ -348,7 +354,7 @@ describe('messageCache', () => {
|
||||
createMessage({ id: 3, text: 'missed via WS' }),
|
||||
];
|
||||
|
||||
const merged = messageCache.reconcile(current, fetched);
|
||||
const merged = reconcileConversationMessages(current, fetched);
|
||||
expect(merged).not.toBeNull();
|
||||
expect(merged!.map((m) => m.id)).toEqual([1, 2, 3]);
|
||||
});
|
||||
@@ -357,7 +363,7 @@ describe('messageCache', () => {
|
||||
const current = [createMessage({ id: 1, acked: 0 })];
|
||||
const fetched = [createMessage({ id: 1, acked: 3 })];
|
||||
|
||||
const merged = messageCache.reconcile(current, fetched);
|
||||
const merged = reconcileConversationMessages(current, fetched);
|
||||
expect(merged).not.toBeNull();
|
||||
expect(merged![0].acked).toBe(3);
|
||||
});
|
||||
@@ -376,20 +382,20 @@ describe('messageCache', () => {
|
||||
createMessage({ id: 2 }),
|
||||
];
|
||||
|
||||
const merged = messageCache.reconcile(current, fetched);
|
||||
const merged = reconcileConversationMessages(current, fetched);
|
||||
expect(merged).not.toBeNull();
|
||||
// Should have fetched page + older paginated message
|
||||
expect(merged!.map((m) => m.id)).toEqual([4, 3, 2, 1]);
|
||||
});
|
||||
|
||||
it('returns null for empty fetched and empty current', () => {
|
||||
expect(messageCache.reconcile([], [])).toBeNull();
|
||||
expect(reconcileConversationMessages([], [])).toBeNull();
|
||||
});
|
||||
|
||||
it('detects difference when current is empty but fetch has messages', () => {
|
||||
const fetched = [createMessage({ id: 1 })];
|
||||
|
||||
const merged = messageCache.reconcile([], fetched);
|
||||
const merged = reconcileConversationMessages([], fetched);
|
||||
expect(merged).not.toBeNull();
|
||||
expect(merged!).toHaveLength(1);
|
||||
});
|
||||
@@ -409,7 +415,7 @@ describe('messageCache', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const merged = messageCache.reconcile(current, fetched);
|
||||
const merged = reconcileConversationMessages(current, fetched);
|
||||
expect(merged).not.toBeNull();
|
||||
expect(merged![0].paths).toHaveLength(2);
|
||||
});
|
||||
@@ -418,7 +424,7 @@ describe('messageCache', () => {
|
||||
const current = [createMessage({ id: 1, text: '[encrypted]' })];
|
||||
const fetched = [createMessage({ id: 1, text: 'Hello world' })];
|
||||
|
||||
const merged = messageCache.reconcile(current, fetched);
|
||||
const merged = reconcileConversationMessages(current, fetched);
|
||||
expect(merged).not.toBeNull();
|
||||
expect(merged![0].text).toBe('Hello world');
|
||||
});
|
||||
@@ -428,7 +434,7 @@ describe('messageCache', () => {
|
||||
const current = [createMessage({ id: 1, acked: 2, paths, text: 'Hello' })];
|
||||
const fetched = [createMessage({ id: 1, acked: 2, paths, text: 'Hello' })];
|
||||
|
||||
expect(messageCache.reconcile(current, fetched)).toBeNull();
|
||||
expect(reconcileConversationMessages(current, fetched)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
|
||||
import * as messageCache from '../messageCache';
|
||||
import { api } from '../api';
|
||||
import { useConversationMessages } from '../hooks/useConversationMessages';
|
||||
import {
|
||||
conversationMessageCache,
|
||||
useConversationMessages,
|
||||
} from '../hooks/useConversationMessages';
|
||||
import type { Conversation, Message } from '../types';
|
||||
|
||||
const mockGetMessages = vi.fn<typeof api.getMessages>();
|
||||
@@ -62,7 +64,7 @@ function createDeferred<T>() {
|
||||
describe('useConversationMessages ACK ordering', () => {
|
||||
beforeEach(() => {
|
||||
mockGetMessages.mockReset();
|
||||
messageCache.clear();
|
||||
conversationMessageCache.clear();
|
||||
mockToastError.mockReset();
|
||||
});
|
||||
|
||||
@@ -175,7 +177,7 @@ describe('useConversationMessages ACK ordering', () => {
|
||||
describe('useConversationMessages conversation switch', () => {
|
||||
beforeEach(() => {
|
||||
mockGetMessages.mockReset();
|
||||
messageCache.clear();
|
||||
conversationMessageCache.clear();
|
||||
});
|
||||
|
||||
it('resets loadingOlder when switching conversations mid-fetch', async () => {
|
||||
@@ -300,7 +302,7 @@ describe('useConversationMessages conversation switch', () => {
|
||||
describe('useConversationMessages background reconcile ordering', () => {
|
||||
beforeEach(() => {
|
||||
mockGetMessages.mockReset();
|
||||
messageCache.clear();
|
||||
conversationMessageCache.clear();
|
||||
});
|
||||
|
||||
it('ignores stale reconnect reconcile responses that finish after newer ones', async () => {
|
||||
@@ -342,7 +344,7 @@ describe('useConversationMessages background reconcile ordering', () => {
|
||||
const conv = createConversation();
|
||||
const cachedMessage = createMessage({ id: 42, text: 'cached snapshot' });
|
||||
|
||||
messageCache.set(conv.id, {
|
||||
conversationMessageCache.set(conv.id, {
|
||||
messages: [cachedMessage],
|
||||
hasOlderMessages: true,
|
||||
});
|
||||
@@ -362,7 +364,7 @@ describe('useConversationMessages background reconcile ordering', () => {
|
||||
describe('useConversationMessages older-page dedup and reentry', () => {
|
||||
beforeEach(() => {
|
||||
mockGetMessages.mockReset();
|
||||
messageCache.clear();
|
||||
conversationMessageCache.clear();
|
||||
});
|
||||
|
||||
it('prevents duplicate overlapping older-page fetches in the same tick', async () => {
|
||||
@@ -508,7 +510,7 @@ describe('useConversationMessages forward pagination', () => {
|
||||
beforeEach(() => {
|
||||
mockGetMessages.mockReset();
|
||||
mockGetMessagesAround.mockReset();
|
||||
messageCache.clear();
|
||||
conversationMessageCache.clear();
|
||||
mockToastError.mockReset();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user