diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 51ab6cbed0..0a860ccf01 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -82,7 +82,7 @@ export function canEditContent(mxEvent: MatrixEvent): boolean { M_POLL_START.matches(mxEvent.getType()) || ( (msgtype === MsgType.Text || msgtype === MsgType.Emote) && - body && + !!body && typeof body === 'string' ) ); diff --git a/test/stores/room-list/previews/PollStartEventPreview-test.ts b/test/stores/room-list/previews/PollStartEventPreview-test.ts index a7726167e1..b69e7da976 100644 --- a/test/stores/room-list/previews/PollStartEventPreview-test.ts +++ b/test/stores/room-list/previews/PollStartEventPreview-test.ts @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix"; -import { POLL_ANSWER, M_TEXT, M_POLL_KIND_DISCLOSED, M_POLL_START } from "matrix-events-sdk"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { PollStartEventPreview } from "../../../../src/stores/room-list/previews/PollStartEventPreview"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { makePollStartEvent } from "../../../test-utils"; jest.spyOn(MatrixClientPeg, 'get').mockReturnValue({ getUserId: () => "@me:example.com", @@ -26,47 +26,15 @@ jest.spyOn(MatrixClientPeg, 'get').mockReturnValue({ describe("PollStartEventPreview", () => { it("shows the question for a poll I created", async () => { - const pollStartEvent = newPollStartEvent("My Question", "@me:example.com"); + const pollStartEvent = makePollStartEvent("My Question", "@me:example.com"); const preview = new PollStartEventPreview(); expect(preview.getTextFor(pollStartEvent)).toBe("My Question"); }); it("shows the sender and question for a poll created by someone else", async () => { - const pollStartEvent = newPollStartEvent("Your Question", "@yo:example.com"); + const pollStartEvent = makePollStartEvent("Your Question", "@yo:example.com"); const preview = new PollStartEventPreview(); expect(preview.getTextFor(pollStartEvent)).toBe("@yo:example.com: Your Question"); }); }); -function newPollStartEvent( - question: string, - sender: string, - answers?: POLL_ANSWER[], -): MatrixEvent { - if (!answers) { - answers = [ - { "id": "socks", [M_TEXT.name]: "Socks" }, - { "id": "shoes", [M_TEXT.name]: "Shoes" }, - ]; - } - - return new MatrixEvent( - { - "event_id": "$mypoll", - "room_id": "#myroom:example.com", - "sender": sender, - "type": M_POLL_START.name, - "content": { - [M_POLL_START.name]: { - "question": { - [M_TEXT.name]: question, - }, - "kind": M_POLL_KIND_DISCLOSED.name, - "answers": answers, - }, - [M_TEXT.name]: `${question}: answers`, - }, - }, - ); -} - diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index c886e6a798..44ea28c966 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -2,6 +2,7 @@ export * from './beacon'; export * from './client'; export * from './location'; export * from './platform'; +export * from './poll'; export * from './room'; export * from './test-utils'; export * from './video'; diff --git a/test/test-utils/poll.ts b/test/test-utils/poll.ts new file mode 100644 index 0000000000..ca25b9eaa0 --- /dev/null +++ b/test/test-utils/poll.ts @@ -0,0 +1,50 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { M_TEXT, M_POLL_START, POLL_ANSWER, M_POLL_KIND_DISCLOSED } from "matrix-events-sdk"; + +export const makePollStartEvent = ( + question: string, + sender: string, + answers?: POLL_ANSWER[], +): MatrixEvent => { + if (!answers) { + answers = [ + { "id": "socks", [M_TEXT.name]: "Socks" }, + { "id": "shoes", [M_TEXT.name]: "Shoes" }, + ]; + } + + return new MatrixEvent( + { + "event_id": "$mypoll", + "room_id": "#myroom:example.com", + "sender": sender, + "type": M_POLL_START.name, + "content": { + [M_POLL_START.name]: { + "question": { + [M_TEXT.name]: question, + }, + "kind": M_POLL_KIND_DISCLOSED.name, + "answers": answers, + }, + [M_TEXT.name]: `${question}: answers`, + }, + }, + ); +}; diff --git a/test/utils/EventUtils-test.ts b/test/utils/EventUtils-test.ts new file mode 100644 index 0000000000..674162f548 --- /dev/null +++ b/test/utils/EventUtils-test.ts @@ -0,0 +1,357 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { M_LOCATION } from "matrix-js-sdk/src/@types/location"; +import { + EventStatus, + EventType, + MatrixEvent, + MsgType, + RelationType, +} from "matrix-js-sdk/src/matrix"; + +import { MatrixClientPeg } from "../../src/MatrixClientPeg"; +import { + canCancel, + canEditContent, + canEditOwnEvent, + canForward, + isContentActionable, + isLocationEvent, + isVoiceMessage, +} from "../../src/utils/EventUtils"; +import { getMockClientWithEventEmitter, makeBeaconInfoEvent, makePollStartEvent } from "../test-utils"; + +describe('EventUtils', () => { + const userId = '@user:server'; + const roomId = '!room:server'; + const mockClient = getMockClientWithEventEmitter({ + getUserId: jest.fn().mockReturnValue(userId), + }); + + beforeEach(() => { + mockClient.getUserId.mockClear().mockReturnValue(userId); + }); + afterAll(() => { + jest.spyOn(MatrixClientPeg, 'get').mockRestore(); + }); + + // setup events + const unsentEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + }); + unsentEvent.status = EventStatus.ENCRYPTING; + + const redactedEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + }); + redactedEvent.makeRedacted(redactedEvent); + + const stateEvent = makeBeaconInfoEvent(userId, roomId); + + const roomMemberEvent = new MatrixEvent({ + type: EventType.RoomMember, + sender: userId, + }); + + const stickerEvent = new MatrixEvent({ + type: EventType.Sticker, + sender: userId, + }); + + const pollStartEvent = makePollStartEvent('What?', userId); + + const notDecryptedEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: 'm.bad.encrypted', + }, + }); + + const noMsgType = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: undefined, + }, + }); + + const noContentBody = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Image, + }, + }); + + const emptyContentBody = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Text, + body: '', + }, + }); + + const objectContentBody = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.File, + body: {}, + }, + }); + + const niceTextMessage = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + }, + }); + + const bobsTextMessage = new MatrixEvent({ + type: EventType.RoomMessage, + sender: '@bob:server', + content: { + msgtype: MsgType.Text, + body: 'Hello from Bob', + }, + }); + + describe('isContentActionable()', () => { + type TestCase = [string, MatrixEvent]; + it.each([ + ['unsent event', unsentEvent], + ['redacted event', redactedEvent], + ['state event', stateEvent], + ['undecrypted event', notDecryptedEvent], + ['room member event', roomMemberEvent], + ['event without msgtype', noMsgType], + ['event without content body property', noContentBody], + ])('returns false for %s', (_description, event) => { + expect(isContentActionable(event)).toBe(false); + }); + + it.each([ + ['sticker event', stickerEvent], + ['poll start event', pollStartEvent], + ['event with empty content body', emptyContentBody], + ['event with a content body', niceTextMessage], + ])('returns true for %s', (_description, event) => { + expect(isContentActionable(event)).toBe(true); + }); + }); + + describe('editable content helpers', () => { + const replaceRelationEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + ['m.relates_to']: { + rel_type: RelationType.Replace, + event_id: '1', + }, + }, + }); + + const referenceRelationEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Text, + body: 'Hello', + ['m.relates_to']: { + rel_type: RelationType.Reference, + event_id: '1', + }, + }, + }); + + const emoteEvent = new MatrixEvent({ + type: EventType.RoomMessage, + sender: userId, + content: { + msgtype: MsgType.Emote, + body: '🧪', + }, + }); + + type TestCase = [string, MatrixEvent]; + + const uneditableCases: TestCase[] = [ + ['redacted event', redactedEvent], + ['state event', stateEvent], + ['event that is not room message', roomMemberEvent], + ['event without msgtype', noMsgType], + ['event without content body property', noContentBody], + ['event with empty content body property', emptyContentBody], + ['event with non-string body', objectContentBody], + ['event not sent by current user', bobsTextMessage], + ['event with a replace relation', replaceRelationEvent], + ]; + + const editableCases: TestCase[] = [ + ['event with reference relation', referenceRelationEvent], + ['emote event', emoteEvent], + ['poll start event', pollStartEvent], + ['event with a content body', niceTextMessage], + ]; + + describe('canEditContent()', () => { + it.each(uneditableCases)('returns false for %s', (_description, event) => { + expect(canEditContent(event)).toBe(false); + }); + + it.each(editableCases)('returns true for %s', (_description, event) => { + expect(canEditContent(event)).toBe(true); + }); + }); + describe('canEditOwnContent()', () => { + it.each(uneditableCases)('returns false for %s', (_description, event) => { + expect(canEditOwnEvent(event)).toBe(false); + }); + + it.each(editableCases)('returns true for %s', (_description, event) => { + expect(canEditOwnEvent(event)).toBe(true); + }); + }); + }); + + describe('isVoiceMessage()', () => { + it('returns true for an event with msc2516.voice content', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + ['org.matrix.msc2516.voice']: {}, + }, + }); + + expect(isVoiceMessage(event)).toBe(true); + }); + + it('returns true for an event with msc3245.voice content', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + ['org.matrix.msc3245.voice']: {}, + }, + }); + + expect(isVoiceMessage(event)).toBe(true); + }); + + it('returns false for an event with voice content', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + body: 'hello', + }, + }); + + expect(isVoiceMessage(event)).toBe(false); + }); + }); + + describe('isLocationEvent()', () => { + it('returns true for an event with m.location stable type', () => { + const event = new MatrixEvent({ + type: M_LOCATION.altName, + }); + expect(isLocationEvent(event)).toBe(true); + }); + it('returns true for an event with m.location unstable prefixed type', () => { + const event = new MatrixEvent({ + type: M_LOCATION.name, + }); + expect(isLocationEvent(event)).toBe(true); + }); + it('returns true for a room message with stable m.location msgtype', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + msgtype: M_LOCATION.altName, + }, + }); + expect(isLocationEvent(event)).toBe(true); + }); + it('returns true for a room message with unstable m.location msgtype', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + msgtype: M_LOCATION.name, + }, + }); + expect(isLocationEvent(event)).toBe(true); + }); + it('returns false for a non location event', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + body: 'Hello', + }, + }); + expect(isLocationEvent(event)).toBe(false); + }); + }); + + describe('canForward()', () => { + it('returns false for a location event', () => { + const event = new MatrixEvent({ + type: M_LOCATION.name, + }); + expect(canForward(event)).toBe(false); + }); + it('returns false for a poll event', () => { + const event = makePollStartEvent('Who?', userId); + expect(canForward(event)).toBe(false); + }); + it('returns true for a room message event', () => { + const event = new MatrixEvent({ + type: EventType.RoomMessage, + content: { + body: 'Hello', + }, + }); + expect(canForward(event)).toBe(true); + }); + }); + + describe('canCancel()', () => { + it.each([ + [EventStatus.QUEUED], + [EventStatus.NOT_SENT], + [EventStatus.ENCRYPTING], + ])('return true for status %s', (status) => { + expect(canCancel(status)).toBe(true); + }); + + it.each([ + [EventStatus.SENDING], + [EventStatus.CANCELLED], + [EventStatus.SENT], + ['invalid-status' as unknown as EventStatus], + ])('return false for status %s', (status) => { + expect(canCancel(status)).toBe(false); + }); + }); +});