Signal-Desktop/ts/test-mock/messaging/reaction_test.ts

443 lines
12 KiB
TypeScript

// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import createDebug from 'debug';
import { StorageState } from '@signalapp/mock-server';
import { type Page } from 'playwright';
import { expect } from 'playwright/test';
import { assert } from 'chai';
import type { App } from '../playwright';
import { Bootstrap } from '../bootstrap';
import { MINUTE } from '../../util/durations';
import { strictAssert } from '../../util/assert';
import {
clickOnConversation,
getMessageInTimelineByTimestamp,
sendTextMessage,
sendReaction,
createGroup,
} from '../helpers';
export const debug = createDebug('mock:test:reactions');
async function getReactionsForMessage(page: Page, timestamp: number) {
const reactionsByEmoji: Record<string, Array<string>> = {};
try {
const message = await getMessageInTimelineByTimestamp(page, timestamp);
await message.locator('.module-message__reactions').click();
const reactionRows = await page
.locator('.module-reaction-viewer__body__row')
.all();
for (const row of reactionRows) {
// eslint-disable-next-line no-await-in-loop
const emoji = await row
.locator('.FunStaticEmoji')
.getAttribute('data-emoji-value');
// eslint-disable-next-line no-await-in-loop
const reactor = await row
.locator('.module-reaction-viewer__body__row__name')
.innerText();
strictAssert(emoji, 'emoji must exist');
reactionsByEmoji[emoji] = (reactionsByEmoji[emoji] ?? []).concat([
reactor,
]);
}
// dismiss reaction popup
await page.keyboard.press('Escape');
} catch {
// pass
}
return reactionsByEmoji;
}
async function expectMessageToHaveReactions(
page: Page,
timestamp: number,
reactionsBySender: Record<string, Array<string>>,
options?: { timeout: number }
): Promise<void> {
return expect(async () => {
assert.deepEqual(
await getReactionsForMessage(page, timestamp),
reactionsBySender
);
}).toPass({ timeout: options?.timeout ?? 10000 });
}
describe('reactions', function (this: Mocha.Suite) {
let bootstrap: Bootstrap;
let app: App;
this.timeout(MINUTE);
beforeEach(async () => {
bootstrap = new Bootstrap();
await bootstrap.init();
const { phone, contacts } = bootstrap;
const [alice, bob, charlie] = contacts;
let state = StorageState.getEmpty();
state = state.addContact(alice, {
identityKey: alice.publicKey.serialize(),
profileKey: alice.profileKey.serialize(),
});
state = state.addContact(bob, {
identityKey: bob.publicKey.serialize(),
profileKey: bob.profileKey.serialize(),
});
state = state.addContact(charlie, {
identityKey: charlie.publicKey.serialize(),
profileKey: charlie.profileKey.serialize(),
});
await phone.setStorageState(state);
app = await bootstrap.link();
});
afterEach(async function (this: Mocha.Context) {
if (!bootstrap) {
return;
}
await bootstrap.maybeSaveLogs(this.currentTest, app);
await app.close();
await bootstrap.teardown();
});
it('should correctly match on participant, timestamp, and author in 1:1 conversation', async () => {
this.timeout(10000);
const { contacts, phone, desktop } = bootstrap;
const [alice, bob, charlie] = contacts;
const window = await app.getWindow();
const alice1on1Timestamp = Date.now();
const outgoingTimestamp = alice1on1Timestamp;
await sendTextMessage({
from: alice,
to: desktop,
text: 'hi from alice',
timestamp: alice1on1Timestamp,
desktop,
});
// To test the case where we have different outgoing messages with the same
// timestamps, we need to send these without awaiting since otherwise desktop will
// drop them since they have the same timestamp (DESKTOP-7301)
await Promise.all([
sendTextMessage({
from: phone,
to: bob,
text: 'hi bob',
timestamp: outgoingTimestamp,
desktop,
}),
sendTextMessage({
from: phone,
to: charlie,
text: 'hi charlie',
timestamp: outgoingTimestamp,
desktop,
}),
]);
// [❌ invalid reaction] bob trying to trick us by reacting to a message in a
// conversation he's not a part of
await sendReaction({
from: bob,
to: desktop,
emoji: '👻',
targetAuthor: alice,
targetMessageTimestamp: alice1on1Timestamp,
desktop,
});
// [❌ invalid reaction] phone sending message with wrong author but right timestamp
await sendReaction({
from: phone,
to: desktop,
emoji: '💀',
targetAuthor: bob,
targetMessageTimestamp: alice1on1Timestamp,
desktop,
});
// [✅ incoming message] alice reacting to her own message
await sendReaction({
from: alice,
to: desktop,
emoji: '👍',
targetAuthor: alice,
targetMessageTimestamp: alice1on1Timestamp,
desktop,
});
await clickOnConversation(window, alice);
await expectMessageToHaveReactions(window, alice1on1Timestamp, {
'👍': [alice.profileName],
});
// [✅ incoming message] phone sending message with right author
await sendReaction({
from: phone,
to: alice,
emoji: '👋',
targetAuthor: alice,
targetMessageTimestamp: alice1on1Timestamp,
desktop,
});
await expectMessageToHaveReactions(window, alice1on1Timestamp, {
'👍': [alice.profileName],
'👋': ['You'],
});
// now, receive reactions from those messages with same timestamp
// [✅ outgoing message] bob reacting to our message
await sendReaction({
from: bob,
to: desktop,
emoji: '👋',
targetAuthor: phone,
targetMessageTimestamp: outgoingTimestamp,
desktop,
});
// [✅ outgoing message] alice reacting to our message
await sendReaction({
from: charlie,
to: desktop,
emoji: '👋',
targetAuthor: phone,
targetMessageTimestamp: outgoingTimestamp,
desktop,
});
await clickOnConversation(window, bob);
await expectMessageToHaveReactions(window, outgoingTimestamp, {
'👋': [bob.profileName],
});
await clickOnConversation(window, charlie);
await expectMessageToHaveReactions(window, outgoingTimestamp, {
'👋': [charlie.profileName],
});
});
it('should correctly match on participant, timestamp, and author in group conversation', async () => {
this.timeout(10000);
const { contacts, phone, desktop } = bootstrap;
const [alice, bob, charlie, danielle] = contacts;
const groupMembers = [alice, bob, charlie];
const groupForSending = {
group: await createGroup(phone, groupMembers, 'ReactionGroup'),
members: groupMembers,
};
const window = await app.getWindow();
const leftPane = window.locator('#LeftPane');
const now = Date.now();
const myGroupTimestamp = now;
const aliceGroupTimestamp = now + 1;
const bobGroupTimestamp = now + 2;
const charlieGroupTimestamp = now + 3;
// [✅ outgoing message]: charlie reacting to bob's group message, early
await sendReaction({
from: charlie,
to: desktop,
emoji: '👋',
targetAuthor: bob,
targetMessageTimestamp: bobGroupTimestamp,
desktop,
});
// Send a bunch of messages in the group
await sendTextMessage({
from: phone,
to: groupForSending,
text: "hello group, it's me",
timestamp: myGroupTimestamp,
desktop,
});
await sendTextMessage({
from: alice,
to: groupForSending,
text: "hello group, it's alice",
timestamp: aliceGroupTimestamp,
desktop,
});
await sendTextMessage({
from: bob,
to: groupForSending,
text: "hello group, it's bob",
timestamp: bobGroupTimestamp,
desktop,
});
await sendTextMessage({
from: charlie,
to: groupForSending,
text: "hello group, it's charlie",
timestamp: charlieGroupTimestamp,
desktop,
});
await leftPane.getByText('ReactionGroup').click();
// [❌ invalid reaction] danielle reacting to our group message, but she's not in the
// group!
await sendReaction({
from: danielle,
to: desktop,
emoji: '👻',
targetAuthor: phone,
targetMessageTimestamp: myGroupTimestamp,
desktop,
});
// [✅ outgoing message]: alice reacting to our group message
await sendReaction({
from: alice,
to: desktop,
emoji: '👍',
targetAuthor: phone,
targetMessageTimestamp: myGroupTimestamp,
desktop,
});
// [✅ outgoing message]: bob reacting to our group message
await sendReaction({
from: bob,
to: desktop,
emoji: '👍',
targetAuthor: phone,
targetMessageTimestamp: myGroupTimestamp,
desktop,
});
// [✅ outgoing message]: charlie reacting to alice's group message
await sendReaction({
from: charlie,
to: desktop,
emoji: '😛',
targetAuthor: alice,
targetMessageTimestamp: aliceGroupTimestamp,
desktop,
});
await expectMessageToHaveReactions(window, myGroupTimestamp, {
'👍': [bob.profileName, alice.profileName],
});
await expectMessageToHaveReactions(window, aliceGroupTimestamp, {
'😛': [charlie.profileName],
});
await expectMessageToHaveReactions(window, bobGroupTimestamp, {
'👋': [charlie.profileName],
});
});
it("should display the local user's thumbs-up skin tone in a group reaction viewer overlay header", async () => {
this.timeout(30_000);
const { contacts, phone, desktop } = bootstrap;
const [alice, bob] = contacts;
// Create a group that includes both Alice and Bob
const groupForSending = {
group: await createGroup(phone, [alice, bob], 'ThumbsToneGroup'),
members: [alice, bob],
};
const window = await app.getWindow();
const leftPane = window.locator('#LeftPane');
const ts = Date.now();
// Send a message from the local user into the group
await sendTextMessage({
from: phone,
to: groupForSending,
text: 'group skin-tone test',
timestamp: ts,
desktop,
});
// Local user reacts with 👍🏽 (medium skin tone)
await sendReaction({
from: phone,
to: desktop,
emoji: '👍🏽',
targetAuthor: phone,
targetMessageTimestamp: ts,
desktop,
});
// Bob reacts with 👍🏿 (to make him the "most recent")
await sendReaction({
from: bob,
to: desktop,
emoji: '👍🏿',
targetAuthor: phone,
targetMessageTimestamp: ts,
desktop,
});
// Open the group conversation
await leftPane.getByText('ThumbsToneGroup').click();
// Click the reaction button on that message
const msg = await getMessageInTimelineByTimestamp(window, ts);
await msg.locator('.module-message__reactions').click();
// Grab the header emoji in the overlay (next to the total count)
const headerEmoji = window.locator(
'.module-reaction-viewer__header .FunStaticEmoji'
);
// The header emoji should still show the local "👍🏽"
await expect(headerEmoji).toHaveAttribute('data-emoji-value', '👍🏽');
// Get all reaction rows; Bob's should be first (most recent), then "You"
const reactionRows = await window
.locator('.module-reaction-viewer__body__row')
.all();
// First row: Bob's 👍🏿
const firstReaction = reactionRows[0];
await expect(
firstReaction.locator('.module-reaction-viewer__body__row__name')
).toHaveText(bob.profileName);
await expect(firstReaction.locator('.FunStaticEmoji')).toHaveAttribute(
'data-emoji-value',
'👍🏿'
);
// Second row: local user's 👍🏽
const secondReaction = reactionRows[1];
await expect(
secondReaction.locator('.module-reaction-viewer__body__row__name')
).toHaveText('You');
await expect(secondReaction.locator('.FunStaticEmoji')).toHaveAttribute(
'data-emoji-value',
'👍🏽'
);
});
});