549 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			549 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			TypeScript
		
	
	
/*
 | 
						|
Copyright 2023 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 React from "react";
 | 
						|
import { fireEvent, render, screen } from "@testing-library/react";
 | 
						|
import userEvent from "@testing-library/user-event";
 | 
						|
import { Room } from "matrix-js-sdk/src/matrix";
 | 
						|
 | 
						|
import EditMessageComposerWithMatrixClient, {
 | 
						|
    createEditContent,
 | 
						|
} from "../../../../src/components/views/rooms/EditMessageComposer";
 | 
						|
import EditorModel from "../../../../src/editor/model";
 | 
						|
import { createPartCreator } from "../../../editor/mock";
 | 
						|
import {
 | 
						|
    getMockClientWithEventEmitter,
 | 
						|
    getRoomContext,
 | 
						|
    mkEvent,
 | 
						|
    mockClientMethodsUser,
 | 
						|
    setupRoomWithEventsTimeline,
 | 
						|
} from "../../../test-utils";
 | 
						|
import DocumentOffset from "../../../../src/editor/offset";
 | 
						|
import SettingsStore from "../../../../src/settings/SettingsStore";
 | 
						|
import EditorStateTransfer from "../../../../src/utils/EditorStateTransfer";
 | 
						|
import RoomContext from "../../../../src/contexts/RoomContext";
 | 
						|
import { IRoomState } from "../../../../src/components/structures/RoomView";
 | 
						|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
 | 
						|
import Autocompleter, { IProviderCompletions } from "../../../../src/autocomplete/Autocompleter";
 | 
						|
import NotifProvider from "../../../../src/autocomplete/NotifProvider";
 | 
						|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
 | 
						|
 | 
						|
describe("<EditMessageComposer/>", () => {
 | 
						|
    const userId = "@alice:server.org";
 | 
						|
    const roomId = "!room:server.org";
 | 
						|
    const mockClient = getMockClientWithEventEmitter({
 | 
						|
        ...mockClientMethodsUser(userId),
 | 
						|
        getRoom: jest.fn(),
 | 
						|
        sendMessage: jest.fn(),
 | 
						|
    });
 | 
						|
    const room = new Room(roomId, mockClient, userId);
 | 
						|
 | 
						|
    const editedEvent = mkEvent({
 | 
						|
        type: "m.room.message",
 | 
						|
        user: "@alice:test",
 | 
						|
        room: "!abc:test",
 | 
						|
        content: { body: "original message", msgtype: "m.text" },
 | 
						|
        event: true,
 | 
						|
    });
 | 
						|
 | 
						|
    const eventWithMentions = mkEvent({
 | 
						|
        type: "m.room.message",
 | 
						|
        user: userId,
 | 
						|
        room: roomId,
 | 
						|
        content: {
 | 
						|
            "msgtype": "m.text",
 | 
						|
            "body": "hey Bob and Charlie",
 | 
						|
            "format": "org.matrix.custom.html",
 | 
						|
            "formatted_body":
 | 
						|
                'hey <a href="https://matrix.to/#/@bob:server.org">Bob</a> and <a href="https://matrix.to/#/@charlie:server.org">Charlie</a>',
 | 
						|
            "m.mentions": {
 | 
						|
                user_ids: ["@bob:server.org", "@charlie:server.org"],
 | 
						|
            },
 | 
						|
        },
 | 
						|
        event: true,
 | 
						|
    });
 | 
						|
 | 
						|
    // message composer emojipicker uses this
 | 
						|
    // which would require more irrelevant mocking
 | 
						|
    jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
 | 
						|
 | 
						|
    const defaultRoomContext = getRoomContext(room, {});
 | 
						|
 | 
						|
    const getComponent = (editState: EditorStateTransfer, roomContext: IRoomState = defaultRoomContext) =>
 | 
						|
        render(<EditMessageComposerWithMatrixClient editState={editState} />, {
 | 
						|
            wrapper: ({ children }) => (
 | 
						|
                <MatrixClientContext.Provider value={mockClient}>
 | 
						|
                    <RoomContext.Provider value={roomContext}>{children}</RoomContext.Provider>
 | 
						|
                </MatrixClientContext.Provider>
 | 
						|
            ),
 | 
						|
        });
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
        mockClient.getRoom.mockReturnValue(room);
 | 
						|
        mockClient.sendMessage.mockClear();
 | 
						|
 | 
						|
        userEvent.setup();
 | 
						|
 | 
						|
        DMRoomMap.makeShared(mockClient);
 | 
						|
 | 
						|
        jest.spyOn(Autocompleter.prototype, "getCompletions").mockResolvedValue([
 | 
						|
            {
 | 
						|
                completions: [
 | 
						|
                    {
 | 
						|
                        completion: "@dan:server.org",
 | 
						|
                        completionId: "@dan:server.org",
 | 
						|
                        type: "user",
 | 
						|
                        suffix: " ",
 | 
						|
                        component: <span>Dan</span>,
 | 
						|
                    },
 | 
						|
                ],
 | 
						|
                command: {
 | 
						|
                    command: ["@d"],
 | 
						|
                },
 | 
						|
                provider: new NotifProvider(room),
 | 
						|
            } as unknown as IProviderCompletions,
 | 
						|
        ]);
 | 
						|
    });
 | 
						|
 | 
						|
    const editText = async (text: string, shouldClear?: boolean): Promise<void> => {
 | 
						|
        const input = screen.getByRole("textbox");
 | 
						|
        if (shouldClear) {
 | 
						|
            await userEvent.clear(input);
 | 
						|
        }
 | 
						|
        await userEvent.type(input, text);
 | 
						|
    };
 | 
						|
 | 
						|
    it("should edit a simple message", async () => {
 | 
						|
        const editState = new EditorStateTransfer(editedEvent);
 | 
						|
        getComponent(editState);
 | 
						|
        await editText(" + edit");
 | 
						|
 | 
						|
        fireEvent.click(screen.getByText("Save"));
 | 
						|
 | 
						|
        const expectedBody = {
 | 
						|
            ...editedEvent.getContent(),
 | 
						|
            "body": " * original message + edit",
 | 
						|
            "m.new_content": {
 | 
						|
                body: "original message + edit",
 | 
						|
                msgtype: "m.text",
 | 
						|
            },
 | 
						|
            "m.relates_to": {
 | 
						|
                event_id: editedEvent.getId(),
 | 
						|
                rel_type: "m.replace",
 | 
						|
            },
 | 
						|
        };
 | 
						|
        expect(mockClient.sendMessage).toHaveBeenCalledWith(editedEvent.getRoomId()!, null, expectedBody);
 | 
						|
    });
 | 
						|
 | 
						|
    it("should throw when room for message is not found", () => {
 | 
						|
        mockClient.getRoom.mockReturnValue(null);
 | 
						|
        const editState = new EditorStateTransfer(editedEvent);
 | 
						|
        expect(() => getComponent(editState, { ...defaultRoomContext, room: undefined })).toThrow(
 | 
						|
            "Cannot render without room",
 | 
						|
        );
 | 
						|
    });
 | 
						|
 | 
						|
    describe("createEditContent", () => {
 | 
						|
        it("sends plaintext messages correctly", () => {
 | 
						|
            const model = new EditorModel([], createPartCreator());
 | 
						|
            const documentOffset = new DocumentOffset(11, true);
 | 
						|
            model.update("hello world", "insertText", documentOffset);
 | 
						|
 | 
						|
            const content = createEditContent(model, editedEvent);
 | 
						|
 | 
						|
            expect(content).toEqual({
 | 
						|
                "body": " * hello world",
 | 
						|
                "msgtype": "m.text",
 | 
						|
                "m.new_content": {
 | 
						|
                    body: "hello world",
 | 
						|
                    msgtype: "m.text",
 | 
						|
                },
 | 
						|
                "m.relates_to": {
 | 
						|
                    event_id: editedEvent.getId(),
 | 
						|
                    rel_type: "m.replace",
 | 
						|
                },
 | 
						|
            });
 | 
						|
        });
 | 
						|
 | 
						|
        it("sends markdown messages correctly", () => {
 | 
						|
            const model = new EditorModel([], createPartCreator());
 | 
						|
            const documentOffset = new DocumentOffset(13, true);
 | 
						|
            model.update("hello *world*", "insertText", documentOffset);
 | 
						|
 | 
						|
            const content = createEditContent(model, editedEvent);
 | 
						|
 | 
						|
            expect(content).toEqual({
 | 
						|
                "body": " * hello *world*",
 | 
						|
                "msgtype": "m.text",
 | 
						|
                "format": "org.matrix.custom.html",
 | 
						|
                "formatted_body": " * hello <em>world</em>",
 | 
						|
                "m.new_content": {
 | 
						|
                    body: "hello *world*",
 | 
						|
                    msgtype: "m.text",
 | 
						|
                    format: "org.matrix.custom.html",
 | 
						|
                    formatted_body: "hello <em>world</em>",
 | 
						|
                },
 | 
						|
                "m.relates_to": {
 | 
						|
                    event_id: editedEvent.getId(),
 | 
						|
                    rel_type: "m.replace",
 | 
						|
                },
 | 
						|
            });
 | 
						|
        });
 | 
						|
 | 
						|
        it("strips /me from messages and marks them as m.emote accordingly", () => {
 | 
						|
            const model = new EditorModel([], createPartCreator());
 | 
						|
            const documentOffset = new DocumentOffset(22, true);
 | 
						|
            model.update("/me blinks __quickly__", "insertText", documentOffset);
 | 
						|
 | 
						|
            const content = createEditContent(model, editedEvent);
 | 
						|
 | 
						|
            expect(content).toEqual({
 | 
						|
                "body": " * blinks __quickly__",
 | 
						|
                "msgtype": "m.emote",
 | 
						|
                "format": "org.matrix.custom.html",
 | 
						|
                "formatted_body": " * blinks <strong>quickly</strong>",
 | 
						|
                "m.new_content": {
 | 
						|
                    body: "blinks __quickly__",
 | 
						|
                    msgtype: "m.emote",
 | 
						|
                    format: "org.matrix.custom.html",
 | 
						|
                    formatted_body: "blinks <strong>quickly</strong>",
 | 
						|
                },
 | 
						|
                "m.relates_to": {
 | 
						|
                    event_id: editedEvent.getId(),
 | 
						|
                    rel_type: "m.replace",
 | 
						|
                },
 | 
						|
            });
 | 
						|
        });
 | 
						|
 | 
						|
        it("allows emoting with non-text parts", () => {
 | 
						|
            const model = new EditorModel([], createPartCreator());
 | 
						|
            const documentOffset = new DocumentOffset(16, true);
 | 
						|
            model.update("/me ✨sparkles✨", "insertText", documentOffset);
 | 
						|
            expect(model.parts.length).toEqual(4); // Emoji count as non-text
 | 
						|
 | 
						|
            const content = createEditContent(model, editedEvent);
 | 
						|
 | 
						|
            expect(content).toEqual({
 | 
						|
                "body": " * ✨sparkles✨",
 | 
						|
                "msgtype": "m.emote",
 | 
						|
                "m.new_content": {
 | 
						|
                    body: "✨sparkles✨",
 | 
						|
                    msgtype: "m.emote",
 | 
						|
                },
 | 
						|
                "m.relates_to": {
 | 
						|
                    event_id: editedEvent.getId(),
 | 
						|
                    rel_type: "m.replace",
 | 
						|
                },
 | 
						|
            });
 | 
						|
        });
 | 
						|
 | 
						|
        it("allows sending double-slash escaped slash commands correctly", () => {
 | 
						|
            const model = new EditorModel([], createPartCreator());
 | 
						|
            const documentOffset = new DocumentOffset(32, true);
 | 
						|
 | 
						|
            model.update("//dev/null is my favourite place", "insertText", documentOffset);
 | 
						|
 | 
						|
            const content = createEditContent(model, editedEvent);
 | 
						|
 | 
						|
            // TODO Edits do not properly strip the double slash used to skip
 | 
						|
            // command processing.
 | 
						|
            expect(content).toEqual({
 | 
						|
                "body": " * //dev/null is my favourite place",
 | 
						|
                "msgtype": "m.text",
 | 
						|
                "m.new_content": {
 | 
						|
                    body: "//dev/null is my favourite place",
 | 
						|
                    msgtype: "m.text",
 | 
						|
                },
 | 
						|
                "m.relates_to": {
 | 
						|
                    event_id: editedEvent.getId(),
 | 
						|
                    rel_type: "m.replace",
 | 
						|
                },
 | 
						|
            });
 | 
						|
        });
 | 
						|
    });
 | 
						|
 | 
						|
    describe("with feature_intentional_mentions enabled", () => {
 | 
						|
        const mockSettings = (mockValues: Record<string, unknown> = {}) => {
 | 
						|
            const defaultMockValues = {
 | 
						|
                feature_intentional_mentions: true,
 | 
						|
            };
 | 
						|
            jest.spyOn(SettingsStore, "getValue")
 | 
						|
                .mockClear()
 | 
						|
                .mockImplementation((settingName) => {
 | 
						|
                    return { ...defaultMockValues, ...mockValues }[settingName];
 | 
						|
                });
 | 
						|
        };
 | 
						|
 | 
						|
        beforeEach(() => {
 | 
						|
            mockSettings();
 | 
						|
        });
 | 
						|
 | 
						|
        describe("when message is not a reply", () => {
 | 
						|
            it("should attach an empty mentions object for a message with no mentions", async () => {
 | 
						|
                const editState = new EditorStateTransfer(editedEvent);
 | 
						|
                getComponent(editState);
 | 
						|
                const editContent = " + edit";
 | 
						|
                await editText(editContent);
 | 
						|
 | 
						|
                fireEvent.click(screen.getByText("Save"));
 | 
						|
 | 
						|
                const messageContent = mockClient.sendMessage.mock.calls[0][2];
 | 
						|
 | 
						|
                // both content.mentions and new_content.mentions are empty
 | 
						|
                expect(messageContent["m.mentions"]).toEqual({});
 | 
						|
                expect(messageContent["m.new_content"]["m.mentions"]).toEqual({});
 | 
						|
            });
 | 
						|
 | 
						|
            it("should retain mentions in the original message that are not removed by the edit", async () => {
 | 
						|
                const editState = new EditorStateTransfer(eventWithMentions);
 | 
						|
                getComponent(editState);
 | 
						|
                // Remove charlie from the message
 | 
						|
                const editContent = "{backspace}{backspace}friends";
 | 
						|
                await editText(editContent);
 | 
						|
 | 
						|
                fireEvent.click(screen.getByText("Save"));
 | 
						|
 | 
						|
                const messageContent = mockClient.sendMessage.mock.calls[0][2];
 | 
						|
 | 
						|
                // no new mentions were added, so nothing in top level mentions
 | 
						|
                expect(messageContent["m.mentions"]).toEqual({});
 | 
						|
                // bob is still mentioned, charlie removed
 | 
						|
                expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
 | 
						|
                    user_ids: ["@bob:server.org"],
 | 
						|
                });
 | 
						|
            });
 | 
						|
 | 
						|
            it("should remove mentions that are removed by the edit", async () => {
 | 
						|
                const editState = new EditorStateTransfer(eventWithMentions);
 | 
						|
                getComponent(editState);
 | 
						|
                const editContent = "new message!";
 | 
						|
                // clear the original message
 | 
						|
                await editText(editContent, true);
 | 
						|
 | 
						|
                fireEvent.click(screen.getByText("Save"));
 | 
						|
 | 
						|
                const messageContent = mockClient.sendMessage.mock.calls[0][2];
 | 
						|
 | 
						|
                // no new mentions were added, so nothing in top level mentions
 | 
						|
                expect(messageContent["m.mentions"]).toEqual({});
 | 
						|
                // bob is not longer mentioned in the edited message, so empty mentions in new_content
 | 
						|
                expect(messageContent["m.new_content"]["m.mentions"]).toEqual({});
 | 
						|
            });
 | 
						|
 | 
						|
            it("should add mentions that were added in the edit", async () => {
 | 
						|
                const editState = new EditorStateTransfer(editedEvent);
 | 
						|
                getComponent(editState);
 | 
						|
                const editContent = " and @d";
 | 
						|
                await editText(editContent);
 | 
						|
 | 
						|
                // submit autocomplete for mention
 | 
						|
                await editText("{enter}");
 | 
						|
 | 
						|
                fireEvent.click(screen.getByText("Save"));
 | 
						|
 | 
						|
                const messageContent = mockClient.sendMessage.mock.calls[0][2];
 | 
						|
 | 
						|
                // new mention in the edit
 | 
						|
                expect(messageContent["m.mentions"]).toEqual({
 | 
						|
                    user_ids: ["@dan:server.org"],
 | 
						|
                });
 | 
						|
                expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
 | 
						|
                    user_ids: ["@dan:server.org"],
 | 
						|
                });
 | 
						|
            });
 | 
						|
 | 
						|
            it("should add and remove mentions from the edit", async () => {
 | 
						|
                const editState = new EditorStateTransfer(eventWithMentions);
 | 
						|
                getComponent(editState);
 | 
						|
                // Remove charlie from the message
 | 
						|
                await editText("{backspace}{backspace}");
 | 
						|
                // and replace with @room
 | 
						|
                await editText("@d");
 | 
						|
                // submit autocomplete for @dan mention
 | 
						|
                await editText("{enter}");
 | 
						|
 | 
						|
                fireEvent.click(screen.getByText("Save"));
 | 
						|
 | 
						|
                const messageContent = mockClient.sendMessage.mock.calls[0][2];
 | 
						|
 | 
						|
                // new mention in the edit
 | 
						|
                expect(messageContent["m.mentions"]).toEqual({
 | 
						|
                    user_ids: ["@dan:server.org"],
 | 
						|
                });
 | 
						|
                // all mentions in the edited version of the event
 | 
						|
                expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
 | 
						|
                    user_ids: ["@bob:server.org", "@dan:server.org"],
 | 
						|
                });
 | 
						|
            });
 | 
						|
        });
 | 
						|
 | 
						|
        describe("when message is replying", () => {
 | 
						|
            const originalEvent = mkEvent({
 | 
						|
                type: "m.room.message",
 | 
						|
                user: "@ernie:test",
 | 
						|
                room: roomId,
 | 
						|
                content: { body: "original message", msgtype: "m.text" },
 | 
						|
                event: true,
 | 
						|
            });
 | 
						|
 | 
						|
            const replyEvent = mkEvent({
 | 
						|
                type: "m.room.message",
 | 
						|
                user: "@bert:test",
 | 
						|
                room: roomId,
 | 
						|
                content: {
 | 
						|
                    "body": "reply with plain message",
 | 
						|
                    "msgtype": "m.text",
 | 
						|
                    "m.relates_to": {
 | 
						|
                        "m.in_reply_to": {
 | 
						|
                            event_id: originalEvent.getId(),
 | 
						|
                        },
 | 
						|
                    },
 | 
						|
                    "m.mentions": {
 | 
						|
                        user_ids: [originalEvent.getSender()!],
 | 
						|
                    },
 | 
						|
                },
 | 
						|
                event: true,
 | 
						|
            });
 | 
						|
 | 
						|
            const replyWithMentions = mkEvent({
 | 
						|
                type: "m.room.message",
 | 
						|
                user: "@bert:test",
 | 
						|
                room: roomId,
 | 
						|
                content: {
 | 
						|
                    "body": 'reply that mentions <a href="https://matrix.to/#/@bob:server.org">Bob</a>',
 | 
						|
                    "msgtype": "m.text",
 | 
						|
                    "m.relates_to": {
 | 
						|
                        "m.in_reply_to": {
 | 
						|
                            event_id: originalEvent.getId(),
 | 
						|
                        },
 | 
						|
                    },
 | 
						|
                    "m.mentions": {
 | 
						|
                        user_ids: [
 | 
						|
                            // sender of event we replied to
 | 
						|
                            originalEvent.getSender()!,
 | 
						|
                            // mentions from this event
 | 
						|
                            "@bob:server.org",
 | 
						|
                        ],
 | 
						|
                    },
 | 
						|
                },
 | 
						|
                event: true,
 | 
						|
            });
 | 
						|
 | 
						|
            beforeEach(() => {
 | 
						|
                setupRoomWithEventsTimeline(room, [originalEvent, replyEvent]);
 | 
						|
            });
 | 
						|
 | 
						|
            it("should retain parent event sender in mentions when editing with plain text", async () => {
 | 
						|
                const editState = new EditorStateTransfer(replyEvent);
 | 
						|
                getComponent(editState);
 | 
						|
                const editContent = " + edit";
 | 
						|
                await editText(editContent);
 | 
						|
 | 
						|
                fireEvent.click(screen.getByText("Save"));
 | 
						|
 | 
						|
                const messageContent = mockClient.sendMessage.mock.calls[0][2];
 | 
						|
 | 
						|
                // no new mentions from edit
 | 
						|
                expect(messageContent["m.mentions"]).toEqual({});
 | 
						|
                // edited reply still mentions the parent event sender
 | 
						|
                expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
 | 
						|
                    user_ids: [originalEvent.getSender()],
 | 
						|
                });
 | 
						|
            });
 | 
						|
 | 
						|
            it("should retain parent event sender in mentions when adding a mention", async () => {
 | 
						|
                const editState = new EditorStateTransfer(replyEvent);
 | 
						|
                getComponent(editState);
 | 
						|
                await editText(" and @d");
 | 
						|
                // submit autocomplete for @dan mention
 | 
						|
                await editText("{enter}");
 | 
						|
 | 
						|
                fireEvent.click(screen.getByText("Save"));
 | 
						|
 | 
						|
                const messageContent = mockClient.sendMessage.mock.calls[0][2];
 | 
						|
 | 
						|
                // new mention in edit
 | 
						|
                expect(messageContent["m.mentions"]).toEqual({
 | 
						|
                    user_ids: ["@dan:server.org"],
 | 
						|
                });
 | 
						|
                // edited reply still mentions the parent event sender
 | 
						|
                // plus new mention @dan
 | 
						|
                expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
 | 
						|
                    user_ids: [originalEvent.getSender(), "@dan:server.org"],
 | 
						|
                });
 | 
						|
            });
 | 
						|
 | 
						|
            it("should retain parent event sender in mentions when removing all mentions from content", async () => {
 | 
						|
                const editState = new EditorStateTransfer(replyWithMentions);
 | 
						|
                getComponent(editState);
 | 
						|
                // replace text to remove all mentions
 | 
						|
                await editText("no mentions here", true);
 | 
						|
 | 
						|
                fireEvent.click(screen.getByText("Save"));
 | 
						|
 | 
						|
                const messageContent = mockClient.sendMessage.mock.calls[0][2];
 | 
						|
 | 
						|
                // no mentions in edit
 | 
						|
                expect(messageContent["m.mentions"]).toEqual({});
 | 
						|
                // edited reply still mentions the parent event sender
 | 
						|
                // existing @bob mention removed
 | 
						|
                expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
 | 
						|
                    user_ids: [originalEvent.getSender()],
 | 
						|
                });
 | 
						|
            });
 | 
						|
 | 
						|
            it("should retain parent event sender in mentions when removing mention of said user", async () => {
 | 
						|
                const replyThatMentionsParentEventSender = mkEvent({
 | 
						|
                    type: "m.room.message",
 | 
						|
                    user: "@bert:test",
 | 
						|
                    room: roomId,
 | 
						|
                    content: {
 | 
						|
                        "body": `reply that mentions the sender of the message we replied to <a href="https://matrix.to/#/${originalEvent.getSender()!}">Ernie</a>`,
 | 
						|
                        "msgtype": "m.text",
 | 
						|
                        "m.relates_to": {
 | 
						|
                            "m.in_reply_to": {
 | 
						|
                                event_id: originalEvent.getId(),
 | 
						|
                            },
 | 
						|
                        },
 | 
						|
                        "m.mentions": {
 | 
						|
                            user_ids: [
 | 
						|
                                // sender of event we replied to
 | 
						|
                                originalEvent.getSender()!,
 | 
						|
                            ],
 | 
						|
                        },
 | 
						|
                    },
 | 
						|
                    event: true,
 | 
						|
                });
 | 
						|
                const editState = new EditorStateTransfer(replyThatMentionsParentEventSender);
 | 
						|
                getComponent(editState);
 | 
						|
                // replace text to remove all mentions
 | 
						|
                await editText("no mentions here", true);
 | 
						|
 | 
						|
                fireEvent.click(screen.getByText("Save"));
 | 
						|
 | 
						|
                const messageContent = mockClient.sendMessage.mock.calls[0][2];
 | 
						|
 | 
						|
                // no mentions in edit
 | 
						|
                expect(messageContent["m.mentions"]).toEqual({});
 | 
						|
                // edited reply still mentions the parent event sender
 | 
						|
                expect(messageContent["m.new_content"]["m.mentions"]).toEqual({
 | 
						|
                    user_ids: [originalEvent.getSender()],
 | 
						|
                });
 | 
						|
            });
 | 
						|
        });
 | 
						|
    });
 | 
						|
});
 |