// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; import { v4 as uuid } from 'uuid'; import { omit } from 'lodash'; import type { MessageReactionType } from '../../model-types.d'; import { isEmpty } from '../../util/iterables'; import { addOutgoingReaction, getNewestPendingOutgoingReaction, getUnsentConversationIds, markOutgoingReactionFailed, markOutgoingReactionSent, } from '../../reactions/util'; describe('reaction utilities', () => { const OUR_CONVO_ID = uuid(); const rxn = ( emoji: undefined | string, { isPending = false }: Readonly<{ isPending?: boolean }> = {} ): MessageReactionType => ({ emoji, fromId: OUR_CONVO_ID, targetTimestamp: Date.now(), timestamp: Date.now(), ...(isPending ? { isSentByConversationId: { [uuid()]: false } } : {}), }); describe('addOutgoingReaction', () => { it('adds the reaction to the end of an empty list', () => { const reaction = rxn('💅'); const result = addOutgoingReaction([], reaction); assert.deepStrictEqual(result, [reaction]); }); it('removes all pending reactions', () => { const oldReactions = [ { ...rxn('😭', { isPending: true }), timestamp: 3 }, { ...rxn('💬'), fromId: uuid() }, { ...rxn('🥀', { isPending: true }), timestamp: 1 }, { ...rxn('🌹', { isPending: true }), timestamp: 2 }, ]; const reaction = rxn('😀'); const newReactions = addOutgoingReaction(oldReactions, reaction); assert.deepStrictEqual(newReactions, [oldReactions[1], reaction]); }); }); describe('getNewestPendingOutgoingReaction', () => { it('returns undefined if there are no pending outgoing reactions', () => { [[], [rxn('🔔')], [rxn('😭'), { ...rxn('💬'), fromId: uuid() }]].forEach( oldReactions => { assert.deepStrictEqual( getNewestPendingOutgoingReaction(oldReactions, OUR_CONVO_ID), {} ); } ); }); it("returns undefined if there's a pending reaction before a fully sent one", () => { const oldReactions = [ { ...rxn('⭐️'), timestamp: 2 }, { ...rxn('🔥', { isPending: true }), timestamp: 1 }, ]; const { pendingReaction, emojiToRemove } = getNewestPendingOutgoingReaction(oldReactions, OUR_CONVO_ID); assert.isUndefined(pendingReaction); assert.isUndefined(emojiToRemove); }); it('returns the newest pending reaction', () => { [ [rxn('⭐️', { isPending: true })], [ { ...rxn('🥀', { isPending: true }), timestamp: 1 }, { ...rxn('⭐️', { isPending: true }), timestamp: 2 }, ], ].forEach(oldReactions => { const { pendingReaction, emojiToRemove } = getNewestPendingOutgoingReaction(oldReactions, OUR_CONVO_ID); assert.strictEqual(pendingReaction?.emoji, '⭐️'); assert.isUndefined(emojiToRemove); }); }); it('makes its best guess of an emoji to remove, if applicable', () => { const oldReactions = [ { ...rxn('⭐️'), timestamp: 1 }, { ...rxn(undefined, { isPending: true }), timestamp: 3 }, { ...rxn('🔥', { isPending: true }), timestamp: 2 }, ]; const { pendingReaction, emojiToRemove } = getNewestPendingOutgoingReaction(oldReactions, OUR_CONVO_ID); assert.isDefined(pendingReaction); assert.isUndefined(pendingReaction?.emoji); assert.strictEqual(emojiToRemove, '⭐️'); }); }); describe('getUnsentConversationIds', () => { it("returns an empty iterable if there's nothing to send", () => { assert(isEmpty(getUnsentConversationIds({}))); assert( isEmpty( getUnsentConversationIds({ isSentByConversationId: { [uuid()]: true }, }) ) ); }); it('returns an iterable of all unsent conversation IDs', () => { const unsent1 = uuid(); const unsent2 = uuid(); const fakeReaction = { isSentByConversationId: { [unsent1]: false, [unsent2]: false, [uuid()]: true, [uuid()]: true, }, }; assert.sameMembers( [...getUnsentConversationIds(fakeReaction)], [unsent1, unsent2] ); }); }); describe('markReactionFailed', () => { const fullySent = rxn('⭐️'); const partiallySent = { ...rxn('🔥'), isSentByConversationId: { [uuid()]: true, [uuid()]: false }, }; const unsent = rxn('🤫', { isPending: true }); const reactions = [fullySent, partiallySent, unsent]; it('removes the pending state if the reaction, with emoji, was partially sent', () => { assert.deepStrictEqual( markOutgoingReactionFailed(reactions, partiallySent), [fullySent, omit(partiallySent, 'isSentByConversationId'), unsent] ); }); it('removes the removal reaction', () => { const none = rxn(undefined, { isPending: true }); assert.isEmpty(markOutgoingReactionFailed([none], none)); }); it('does nothing if the reaction is not in the list', () => { assert.deepStrictEqual( markOutgoingReactionFailed(reactions, rxn('🥀', { isPending: true })), reactions ); }); it('removes the completely-unsent emoji reaction', () => { assert.deepStrictEqual(markOutgoingReactionFailed(reactions, unsent), [ fullySent, partiallySent, ]); }); }); describe('markOutgoingReactionSent', () => { const uuid1 = uuid(); const uuid2 = uuid(); const uuid3 = uuid(); const star = { ...rxn('⭐️'), timestamp: 2, isSentByConversationId: { [uuid1]: false, [uuid2]: false, [uuid3]: false, }, }; const none = { ...rxn(undefined), timestamp: 3, isSentByConversationId: { [uuid1]: false, [uuid2]: false, [uuid3]: false, }, }; const reactions = [star, none, { ...rxn('🔕'), timestamp: 1 }]; it("does nothing if the reaction isn't in the list", () => { const result = markOutgoingReactionSent( reactions, rxn('🥀', { isPending: true }), [uuid()] ); assert.deepStrictEqual(result, reactions); }); it('updates reactions to be partially sent', () => { [star, none].forEach(reaction => { const result = markOutgoingReactionSent(reactions, reaction, [ uuid1, uuid2, ]); assert.deepStrictEqual( result.find(re => re.emoji === reaction.emoji) ?.isSentByConversationId, { [uuid1]: true, [uuid2]: true, [uuid3]: false, } ); }); }); it('removes sent state if a reaction with emoji is fully sent', () => { const result = markOutgoingReactionSent(reactions, star, [ uuid1, uuid2, uuid3, ]); const newReaction = result.find(re => re.emoji === '⭐️'); assert.isDefined(newReaction); assert.isUndefined(newReaction?.isSentByConversationId); }); it('removes a fully-sent reaction removal', () => { const result = markOutgoingReactionSent(reactions, none, [ uuid1, uuid2, uuid3, ]); assert( result.every(({ emoji }) => typeof emoji === 'string'), 'Expected the emoji removal to be gone' ); }); it('removes older reactions of mine', () => { const result = markOutgoingReactionSent(reactions, star, [ uuid1, uuid2, uuid3, ]); assert.isUndefined(result.find(re => re.emoji === '🔕')); }); }); });