Files
upstream-rocketchat/apps/meteor/client/lib/utils/threadMessageUtils.spec.ts
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

238 lines
8.7 KiB
TypeScript

import type { IThreadMessage } from '@rocket.chat/core-typings';
import { QueryClient } from '@tanstack/react-query';
import { mutateThreadMessagesInfiniteData, upsertThreadMessageInCache, type ThreadMessagesInfiniteData } from './threadMessageUtils';
const rid = 'room1';
const tmid = 'thread1';
const queryKey = ['rooms', rid, 'threads', tmid, 'messages'] as const;
const createMessage = (id: string, ts: number): IThreadMessage =>
({
_id: id,
rid,
tmid,
msg: id,
ts: new Date(ts).toISOString() as any,
u: { _id: 'user1', username: 'user1' },
_updatedAt: new Date(ts).toISOString() as any,
}) as IThreadMessage;
// A thread with 200 replies, one cached page holding reply-075..reply-124.
const createAnchoredCache = (): ThreadMessagesInfiniteData => ({
pages: [
{
items: Array.from({ length: 50 }, (_, i) => createMessage(`reply-${String(i + 75).padStart(3, '0')}`, i + 75)),
itemCount: 200,
},
],
pageParams: [75],
});
describe('mutateThreadMessagesInfiniteData', () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient();
});
it('shifts every stored pageParam forward when a message newer than everything cached is inserted', () => {
queryClient.setQueryData(queryKey, createAnchoredCache());
mutateThreadMessagesInfiniteData(queryClient, queryKey, (messages) => {
messages.push(createMessage('reply-200', 1000));
messages.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
});
const data = queryClient.getQueryData<ThreadMessagesInfiniteData>(queryKey);
expect(data?.pages[0].itemCount).toBe(201);
expect(data?.pages[0].items.map((m) => m._id)).toContain('reply-200');
expect(data?.pageParams).toEqual([76]);
});
it('keeps a pageParam of 0 unshifted when a newer message is inserted and user is at the newest message', () => {
const cache: ThreadMessagesInfiniteData = {
pages: [{ items: [createMessage('reply-150', 150), createMessage('reply-199', 199)], itemCount: 200 }],
pageParams: [0],
};
queryClient.setQueryData(queryKey, cache);
mutateThreadMessagesInfiniteData(queryClient, queryKey, (messages) => {
messages.push(createMessage('reply-200', 1000));
messages.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
});
const data = queryClient.getQueryData<ThreadMessagesInfiniteData>(queryKey);
expect(data?.pages[0].itemCount).toBe(201);
expect(data?.pages[0].items.map((m) => m._id)).toContain('reply-200');
expect(data?.pageParams).toEqual([0]);
});
it('shifts pageParams when the inserted message ties the newest cached timestamp', () => {
queryClient.setQueryData(queryKey, createAnchoredCache());
mutateThreadMessagesInfiniteData(queryClient, queryKey, (messages) => {
messages.push(createMessage('reply-200', 124));
});
const data = queryClient.getQueryData<ThreadMessagesInfiniteData>(queryKey);
expect(data?.pages[0].itemCount).toBe(201);
expect(data?.pageParams).toEqual([76]);
});
it('shifts every page pageParam, not just the last one, when a newer message arrives', () => {
const cache: ThreadMessagesInfiniteData = {
pages: [
{ items: [createMessage('reply-025', 25), createMessage('reply-026', 26)], itemCount: 200 },
{ items: [createMessage('reply-075', 75), createMessage('reply-076', 76)], itemCount: 200 },
],
pageParams: [25, 75],
};
queryClient.setQueryData(queryKey, cache);
mutateThreadMessagesInfiniteData(queryClient, queryKey, (messages) => {
messages.push(createMessage('reply-200', 1000));
messages.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
});
const data = queryClient.getQueryData<ThreadMessagesInfiniteData>(queryKey);
expect(data?.pageParams).toEqual([26, 76]);
});
it('does not shift pageParams when the inserted message is older than what is already cached', () => {
queryClient.setQueryData(queryKey, createAnchoredCache());
mutateThreadMessagesInfiniteData(queryClient, queryKey, (messages) => {
messages.unshift(createMessage('reply-010', 10));
});
const data = queryClient.getQueryData<ThreadMessagesInfiniteData>(queryKey);
expect(data?.pages[0].itemCount).toBe(201);
expect(data?.pageParams).toEqual([75]);
});
it('does not shift pageParams when a cached message is deleted', () => {
queryClient.setQueryData(queryKey, createAnchoredCache());
mutateThreadMessagesInfiniteData(queryClient, queryKey, (messages) => {
const index = messages.findIndex((m) => m._id === 'reply-100');
messages.splice(index, 1);
});
const data = queryClient.getQueryData<ThreadMessagesInfiniteData>(queryKey);
expect(data?.pages[0].items.map((m) => m._id)).not.toContain('reply-100');
expect(data?.pages[0].itemCount).toBe(199);
expect(data?.pageParams).toEqual([75]);
});
it('does not shift pageParams when an existing message is edited in place', () => {
queryClient.setQueryData(queryKey, createAnchoredCache());
mutateThreadMessagesInfiniteData(queryClient, queryKey, (messages) => {
const index = messages.findIndex((m) => m._id === 'reply-100');
messages[index] = { ...messages[index], msg: 'edited' };
});
const data = queryClient.getQueryData<ThreadMessagesInfiniteData>(queryKey);
expect(data?.pages[0].itemCount).toBe(200);
expect(data?.pageParams).toEqual([75]);
});
it('removes a message from every cached page when its id was duplicated across pages, not just the first occurrence', () => {
const cache: ThreadMessagesInfiniteData = {
pages: [
{ items: [createMessage('reply-058', 58), createMessage('reply-059', 59), createMessage('reply-060', 60)], itemCount: 200 },
{ items: [createMessage('reply-060', 60), createMessage('reply-061', 61)], itemCount: 200 },
],
pageParams: [58, 60],
};
queryClient.setQueryData(queryKey, cache);
mutateThreadMessagesInfiniteData(queryClient, queryKey, (messages) => {
const index = messages.findIndex((m) => m._id === 'reply-060');
messages.splice(index, 1);
});
const data = queryClient.getQueryData<ThreadMessagesInfiniteData>(queryKey);
const allIds = data?.pages.flatMap((page) => page.items.map((m) => m._id)) ?? [];
expect(allIds).not.toContain('reply-060');
expect(data?.pages[0].itemCount).toBe(199);
});
it('collapses a duplicated id into a single entry, keeping the most recently updated copy, even when the mutation is a no-op', () => {
const stale = createMessage('reply-060', 60);
const fresh: IThreadMessage = { ...createMessage('reply-060', 60), msg: 'edited', _updatedAt: new Date(9999).toISOString() as any };
const cache: ThreadMessagesInfiniteData = {
pages: [
{ items: [stale], itemCount: 200 },
{ items: [fresh], itemCount: 200 },
],
pageParams: [60, 60],
};
queryClient.setQueryData(queryKey, cache);
mutateThreadMessagesInfiniteData(queryClient, queryKey, () => {
// no-op mutation: only the dedup pass should change the cache
});
const data = queryClient.getQueryData<ThreadMessagesInfiniteData>(queryKey);
const allItems = data?.pages.flatMap((page) => page.items) ?? [];
expect(allItems).toHaveLength(1);
expect(allItems[0].msg).toBe('edited');
});
});
describe('upsertThreadMessageInCache', () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient();
});
it('seeds the cache with pageParams [0] when nothing was cached yet', () => {
upsertThreadMessageInCache(createMessage('reply-000', 0), rid, tmid, queryClient);
const data = queryClient.getQueryData<ThreadMessagesInfiniteData>(queryKey);
expect(data?.pages[0].itemCount).toBe(1);
expect(data?.pageParams).toEqual([0]);
});
it('keeps the next-page fetch offset aligned with the real data after a live reply arrives, so no message is skipped', () => {
queryClient.setQueryData(queryKey, createAnchoredCache());
upsertThreadMessageInCache(createMessage('reply-200', 1000), rid, tmid, queryClient);
const data = queryClient.getQueryData<ThreadMessagesInfiniteData>(queryKey);
const [pageParam] = data?.pageParams ?? [];
expect(data?.pages[0].itemCount).toBe(201);
// useThreadMessagesQuery's getNextPageParam computes offset = pageParam - count (50),
// so 76 (not the stale 75) produces offset 26, which includes reply-125.
expect(pageParam).toBe(76);
});
it('updates an existing message in place without shifting pageParams', () => {
queryClient.setQueryData(queryKey, createAnchoredCache());
const edited = { ...createMessage('reply-100', 100), msg: 'edited' };
upsertThreadMessageInCache(edited, rid, tmid, queryClient);
const data = queryClient.getQueryData<ThreadMessagesInfiniteData>(queryKey);
const updated = data?.pages[0].items.find((m) => m._id === 'reply-100');
expect(updated?.msg).toBe('edited');
expect(data?.pages[0].itemCount).toBe(200);
expect(data?.pageParams).toEqual([75]);
});
});