signal-desktop/ts/test-both/reactions/util_test.ts
2024-09-13 09:48:27 +10:00

263 lines
7.7 KiB
TypeScript

// 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 === '🔕'));
});
});
});