Finish frontend phase 3

This commit is contained in:
Jack Kingsman
2026-03-16 17:17:11 -07:00
parent 0e4828bf72
commit ffb5fa51c1
5 changed files with 232 additions and 243 deletions
+9 -12
View File
@@ -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',
+18 -12
View File
@@ -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();
});