443 lines
12 KiB
TypeScript
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',
|
|
'👍🏽'
|
|
);
|
|
});
|
|
});
|