import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; import TestRenderer from 'react-test-renderer'; import { ReactElement } from "react"; import { getSenderName, textForEvent } from "../src/TextForEvent"; import SettingsStore from "../src/settings/SettingsStore"; import { createTestClient } from './test-utils'; import { MatrixClientPeg } from '../src/MatrixClientPeg'; import UserIdentifierCustomisations from '../src/customisations/UserIdentifier'; jest.mock("../src/settings/SettingsStore"); jest.mock('../src/customisations/UserIdentifier', () => ({ getDisplayUserIdentifier: jest.fn().mockImplementation(userId => userId), })); function mockPinnedEvent( pinnedMessageIds?: string[], prevPinnedMessageIds?: string[], ): MatrixEvent { return new MatrixEvent({ type: "m.room.pinned_events", state_key: "", sender: "@foo:example.com", content: { pinned: pinnedMessageIds, }, prev_content: { pinned: prevPinnedMessageIds, }, }); } // Helper function that renders a component to a plain text string. // Once snapshots are introduced in tests, this function will no longer be necessary, // and should be replaced with snapshots. function renderComponent(component): string { const serializeObject = (object): string => { if (typeof object === 'string') { return object === ' ' ? '' : object; } if (Array.isArray(object) && object.length === 1 && typeof object[0] === 'string') { return object[0]; } if (object['type'] !== undefined && typeof object['children'] !== undefined) { return serializeObject(object.children); } if (!Array.isArray(object)) { return ''; } return object.map(child => { return serializeObject(child); }).join(''); }; return serializeObject(component.toJSON()); } describe('TextForEvent', () => { describe("getSenderName()", () => { it("Prefers sender.name", () => { expect(getSenderName({ sender: { name: "Alice" } } as MatrixEvent)).toBe("Alice"); }); it("Handles missing sender", () => { expect(getSenderName({ getSender: () => "Alice" } as MatrixEvent)).toBe("Alice"); }); it("Handles missing sender and get sender", () => { expect(getSenderName({ getSender: () => undefined } as MatrixEvent)).toBe("Someone"); }); }); describe("TextForPinnedEvent", () => { beforeAll(() => { // enable feature_pinning setting (SettingsStore.getValue as jest.Mock).mockImplementation(feature => feature === 'feature_pinning'); }); it("mentions message when a single message was pinned, with no previously pinned messages", () => { const event = mockPinnedEvent(['message-1']); const plainText = textForEvent(event); const component = TestRenderer.create(textForEvent(event, true) as ReactElement); const expectedText = "@foo:example.com pinned a message to this room. See all pinned messages."; expect(plainText).toBe(expectedText); expect(renderComponent(component)).toBe(expectedText); }); it("mentions message when a single message was pinned, with multiple previously pinned messages", () => { const event = mockPinnedEvent(['message-1', 'message-2', 'message-3'], ['message-1', 'message-2']); const plainText = textForEvent(event); const component = TestRenderer.create(textForEvent(event, true) as ReactElement); const expectedText = "@foo:example.com pinned a message to this room. See all pinned messages."; expect(plainText).toBe(expectedText); expect(renderComponent(component)).toBe(expectedText); }); it("mentions message when a single message was unpinned, with a single message previously pinned", () => { const event = mockPinnedEvent([], ['message-1']); const plainText = textForEvent(event); const component = TestRenderer.create(textForEvent(event, true) as ReactElement); const expectedText = "@foo:example.com unpinned a message from this room. See all pinned messages."; expect(plainText).toBe(expectedText); expect(renderComponent(component)).toBe(expectedText); }); it("mentions message when a single message was unpinned, with multiple previously pinned messages", () => { const event = mockPinnedEvent(['message-2'], ['message-1', 'message-2']); const plainText = textForEvent(event); const component = TestRenderer.create(textForEvent(event, true) as ReactElement); const expectedText = "@foo:example.com unpinned a message from this room. See all pinned messages."; expect(plainText).toBe(expectedText); expect(renderComponent(component)).toBe(expectedText); }); it("shows generic text when multiple messages were pinned", () => { const event = mockPinnedEvent(['message-1', 'message-2', 'message-3'], ['message-1']); const plainText = textForEvent(event); const component = TestRenderer.create(textForEvent(event, true) as ReactElement); const expectedText = "@foo:example.com changed the pinned messages for the room."; expect(plainText).toBe(expectedText); expect(renderComponent(component)).toBe(expectedText); }); it("shows generic text when multiple messages were unpinned", () => { const event = mockPinnedEvent(['message-3'], ['message-1', 'message-2', 'message-3']); const plainText = textForEvent(event); const component = TestRenderer.create(textForEvent(event, true) as ReactElement); const expectedText = "@foo:example.com changed the pinned messages for the room."; expect(plainText).toBe(expectedText); expect(renderComponent(component)).toBe(expectedText); }); it("shows generic text when one message was pinned, and another unpinned", () => { const event = mockPinnedEvent(['message-2'], ['message-1']); const plainText = textForEvent(event); const component = TestRenderer.create(textForEvent(event, true) as ReactElement); const expectedText = "@foo:example.com changed the pinned messages for the room."; expect(plainText).toBe(expectedText); expect(renderComponent(component)).toBe(expectedText); }); }); describe("textForPowerEvent()", () => { let mockClient; const mockRoom = { getMember: jest.fn(), }; const userA = { id: '@a', name: 'Alice', }; const userB = { id: '@b', name: 'Bob', }; const userC = { id: '@c', name: 'Carl', }; interface PowerEventProps { usersDefault?: number; prevDefault?: number; users: Record; prevUsers: Record; } const mockPowerEvent = ({ usersDefault, prevDefault, users, prevUsers, }: PowerEventProps): MatrixEvent => new MatrixEvent({ type: EventType.RoomPowerLevels, sender: userA.id, state_key: "", content: { users_default: usersDefault, users, }, prev_content: { users: prevUsers, users_default: prevDefault, }, }); beforeAll(() => { mockClient = createTestClient(); MatrixClientPeg.get = () => mockClient; mockClient.getRoom.mockClear().mockReturnValue(mockRoom); mockRoom.getMember.mockClear().mockImplementation( userId => [userA, userB, userC].find(u => u.id === userId), ); (SettingsStore.getValue as jest.Mock).mockReturnValue(true); }); beforeEach(() => { (UserIdentifierCustomisations.getDisplayUserIdentifier as jest.Mock) .mockClear() .mockImplementation(userId => userId); }); it("returns falsy when no users have changed power level", () => { const event = mockPowerEvent({ users: { [userA.id]: 100, }, prevUsers: { [userA.id]: 100, }, }); expect(textForEvent(event)).toBeFalsy(); }); it("returns false when users power levels have been changed by default settings", () => { const event = mockPowerEvent({ usersDefault: 100, prevDefault: 50, users: { [userA.id]: 100, }, prevUsers: { [userA.id]: 50, }, }); expect(textForEvent(event)).toBeFalsy(); }); it("returns correct message for a single user with changed power level", () => { const event = mockPowerEvent({ users: { [userB.id]: 100, }, prevUsers: { [userB.id]: 50, }, }); const expectedText = "@a changed the power level of @b from Moderator to Admin."; expect(textForEvent(event)).toEqual(expectedText); }); it("returns correct message for a single user with power level changed to the default", () => { const event = mockPowerEvent({ usersDefault: 20, prevDefault: 101, users: { [userB.id]: 20, }, prevUsers: { [userB.id]: 50, }, }); const expectedText = "@a changed the power level of @b from Moderator to Default."; expect(textForEvent(event)).toEqual(expectedText); }); it("returns correct message for a single user with power level changed to a custom level", () => { const event = mockPowerEvent({ users: { [userB.id]: -1, }, prevUsers: { [userB.id]: 50, }, }); const expectedText = "@a changed the power level of @b from Moderator to Custom (-1)."; expect(textForEvent(event)).toEqual(expectedText); }); it("returns correct message for a multiple power level changes", () => { const event = mockPowerEvent({ users: { [userB.id]: 100, [userC.id]: 50, }, prevUsers: { [userB.id]: 50, [userC.id]: 101, }, }); const expectedText = "@a changed the power level of @b from Moderator to Admin, @c from Custom (101) to Moderator."; expect(textForEvent(event)).toEqual(expectedText); }); it("uses userIdentifier customisation", () => { (UserIdentifierCustomisations.getDisplayUserIdentifier as jest.Mock) .mockImplementation(userId => 'customised ' + userId); const event = mockPowerEvent({ users: { [userB.id]: 100, }, prevUsers: { [userB.id]: 50, }, }); // uses customised user id const expectedText = "@a changed the power level of customised @b from Moderator to Admin."; expect(textForEvent(event)).toEqual(expectedText); expect(UserIdentifierCustomisations.getDisplayUserIdentifier) .toHaveBeenCalledWith(userB.id, { roomId: event.getRoomId() }); }); }); describe("textForCanonicalAliasEvent()", () => { const userA = { id: '@a', name: 'Alice', }; interface AliasEventProps { alias?: string; prevAlias?: string; altAliases?: string[]; prevAltAliases?: string[]; } const mockEvent = ({ alias, prevAlias, altAliases, prevAltAliases, }: AliasEventProps): MatrixEvent => new MatrixEvent({ type: EventType.RoomCanonicalAlias, sender: userA.id, state_key: "", content: { alias, alt_aliases: altAliases, }, prev_content: { alias: prevAlias, alt_aliases: prevAltAliases, }, }); type TestCase = [string, AliasEventProps & { result: string }]; const testCases: TestCase[] = [ ["room alias didn't change", { result: '@a changed the addresses for this room.', }], ["room alias changed", { alias: 'banana', prevAlias: 'apple', result: '@a set the main address for this room to banana.', }], ["room alias was added", { alias: 'banana', result: '@a set the main address for this room to banana.', }], ["room alias was removed", { prevAlias: 'apple', result: '@a removed the main address for this room.', }], ["added an alt alias", { altAliases: ['canteloupe'], result: '@a added alternative address canteloupe for this room.', }], ["added multiple alt aliases", { altAliases: ['canteloupe', 'date'], result: '@a added the alternative addresses canteloupe, date for this room.', }], ["removed an alt alias", { altAliases: ['canteloupe'], prevAltAliases: ['canteloupe', 'date'], result: '@a removed alternative address date for this room.', }], ["added and removed an alt aliases", { altAliases: ['canteloupe', 'elderberry'], prevAltAliases: ['canteloupe', 'date'], result: '@a changed the alternative addresses for this room.', }], ["changed alias and added alt alias", { alias: 'banana', prevAlias: 'apple', altAliases: ['canteloupe'], result: '@a changed the main and alternative addresses for this room.', }], ]; it.each(testCases)('returns correct message when %s', (_d, { result, ...eventProps }) => { const event = mockEvent(eventProps); expect(textForEvent(event)).toEqual(result); }); }); describe("textForPollStartEvent()", () => { let pollEvent; beforeEach(() => { pollEvent = new MatrixEvent({ type: 'org.matrix.msc3381.poll.start', sender: '@a', content: { 'org.matrix.msc3381.poll.start': { answers: [ { 'org.matrix.msc1767.text': 'option1' }, { 'org.matrix.msc1767.text': 'option2' }, ], question: { 'body': 'Test poll name', 'msgtype': 'm.text', 'org.matrix.msc1767.text': 'Test poll name', }, }, }, }); }); it("returns correct message for redacted poll start", () => { pollEvent.makeRedacted(pollEvent); expect(textForEvent(pollEvent)).toEqual('@a: Message deleted'); }); it("returns correct message for normal poll start", () => { expect(textForEvent(pollEvent)).toEqual('@a has started a poll - '); }); }); describe("textForMessageEvent()", () => { let messageEvent; beforeEach(() => { messageEvent = new MatrixEvent({ type: 'm.room.message', sender: '@a', content: { 'body': 'test message', 'msgtype': 'm.text', 'org.matrix.msc1767.text': 'test message', }, }); }); it("returns correct message for redacted message", () => { messageEvent.makeRedacted(messageEvent); expect(textForEvent(messageEvent)).toEqual('@a: Message deleted'); }); it("returns correct message for normal message", () => { expect(textForEvent(messageEvent)).toEqual('@a: test message'); }); }); });