chatReactions.test.ts3.4 KBView on GitHub import { applyOwnChatReaction, matchesMessage } from '@/modules/inbox/hooks/use-chat-reactions';
import type { EventReaction } from '@/modules/crm/types';
/**
* The optimistic write for LinkedIn / WhatsApp reactions.
*
* It is NOT Slack's `applyToggle` under another name: those providers allow ONE reaction per
* person per message, so a click can move a reaction from one chip to another — a case Slack's
* model has no equivalent of, and the case that would leave the row lying until the next read
* if the optimistic write got it wrong.
*/
const mine = (key=[redacted], count = 1): EventReaction => ({
key,
emojiUnicode: key,
count,
reactedByMe: true,
});
const theirs = (key=[redacted], count = 1): EventReaction => ({
key,
emojiUnicode: key,
count,
reactedByMe: false,
});
describe('applyOwnChatReaction', () => {
it('adds a first reaction', () => {
expect(applyOwnChatReaction([], '👍', false)).toEqual([mine('👍')]);
});
it('moves my reaction off the old emoji and onto the new one', () => {
expect(applyOwnChatReaction([mine('👍')], '🎉', false)).toEqual([mine('🎉')]);
});
it('joins a chip others already used, without leaving my old one behind', () => {
expect(applyOwnChatReaction([mine('👍'), theirs('🎉', 2)], '🎉', false)).toEqual([
{ key: '🎉', emojiUnicode: '🎉', count: 3, reactedByMe: true },
]);
});
it('withdraws my reaction', () => {
expect(applyOwnChatReaction([mine('👍')], '👍', true)).toEqual([]);
});
it('leaves a shared chip standing, one lower, when I withdraw', () => {
// The chip only disappears when I was its last reactor — otherwise it stays, un-highlighted,
// which is what "someone else still reacted" has to look like.
expect(applyOwnChatReaction([{ ...mine('👍'), count: 3 }], '👍', true)).toEqual([
{ key: '👍', emojiUnicode: '👍', count: 2, reactedByMe: false },
]);
});
});
/**
* Which cached row the optimistic write lands on.
*
* The three chat reads do not agree on where the provider's message id lives, and the write path
* addresses a message by that id (`ChannelThreadView` sends `providerMessageId ?? id`). Matching
* the cache on `id` alone missed every LinkedIn message served from the mirror — the normal path
* — so the chip stayed frozen on click AND the server's authoritative aggregate was dropped on
* the way back. These three shapes are exactly what the three reads return.
*/
describe('matchesMessage', () => {
it('matches a mirrored LinkedIn row, whose `id` is a Cedar uuid', () => {
const row = { id: '0f8c1b6e-3a11-4d55-9e21-7b0c9a2f4d18', providerMessageId: 'unipile_msg_1' };
expect(matchesMessage(row, 'unipile_msg_1')).toBe(true);
// And never on the uuid, which is not an id any caller writes against.
expect(matchesMessage(row, '0f8c1b6e-3a11-4d55-9e21-7b0c9a2f4d18')).toBe(false);
});
it('matches a live-read row, whose provider id IS its `id`', () => {
expect(matchesMessage({ id: 'unipile_msg_1' }, 'unipile_msg_1')).toBe(true);
});
it('matches an optimistic pending row, which sets both to the same sentinel', () => {
const pending = { id: 'pending:1755300000000', providerMessageId: 'pending:1755300000000' };
expect(matchesMessage(pending, 'pending:1755300000000')).toBe(true);
});
it('does not match a different message', () => {
expect(matchesMessage({ id: 'a', providerMessageId: 'unipile_msg_1' }, 'unipile_msg_2')).toBe(
false,
);
});
});