/* Copyright 2024 New Vector Ltd. Copyright 2019-2022 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ import React from "react"; import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { mocked, MockedObject } from "jest-mock"; import { render, waitFor } from "jest-matrix-react"; import { getMockClientWithEventEmitter, mkEvent, mkMessage, mkStubRoom } from "../../../../test-utils"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import * as languageHandler from "../../../../../src/languageHandler"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import TextualBody from "../../../../../src/components/views/messages/TextualBody"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper"; const room1Id = "!room1:example.com"; const room2Id = "!room2:example.com"; const room2Name = "Room 2"; interface MkRoomTextMessageOpts { roomId?: string; } const mkRoomTextMessage = (body: string, mkRoomTextMessageOpts?: MkRoomTextMessageOpts): MatrixEvent => { return mkMessage({ msg: body, room: mkRoomTextMessageOpts?.roomId ?? room1Id, user: "sender", event: true, }); }; const mkFormattedMessage = (body: string, formattedBody: string): MatrixEvent => { return mkMessage({ msg: body, formattedMsg: formattedBody, format: "org.matrix.custom.html", room: room1Id, user: "sender", event: true, }); }; describe("", () => { afterEach(() => { jest.spyOn(MatrixClientPeg, "get").mockRestore(); jest.spyOn(global.Math, "random").mockRestore(); }); const defaultRoom = mkStubRoom(room1Id, "test room", undefined); const otherRoom = mkStubRoom(room2Id, room2Name, undefined); let defaultMatrixClient: MockedObject; const defaultEvent = mkEvent({ type: "m.room.message", room: room1Id, user: "sender", content: { body: "winks", msgtype: "m.emote", }, event: true, }); beforeEach(() => { defaultMatrixClient = getMockClientWithEventEmitter({ getRoom: (roomId: string | undefined) => { if (roomId === room1Id) return defaultRoom; if (roomId === room2Id) return otherRoom; return null; }, getRooms: () => [defaultRoom, otherRoom], getAccountData: (): MatrixEvent | undefined => undefined, isGuest: () => false, mxcUrlToHttp: (s: string) => s, getUserId: () => "@user:example.com", fetchRoomEvent: () => { throw new Error("MockClient event not found"); }, }); mocked(defaultRoom).findEventById.mockImplementation((eventId: string) => { if (eventId === defaultEvent.getId()) return defaultEvent; return undefined; }); jest.spyOn(global.Math, "random").mockReturnValue(0.123456); }); const defaultProps = { mxEvent: defaultEvent, highlights: [] as string[], highlightLink: "", onMessageAllowed: jest.fn(), onHeightChanged: jest.fn(), permalinkCreator: new RoomPermalinkCreator(defaultRoom), mediaEventHelper: {} as MediaEventHelper, }; const getComponent = (props = {}, matrixClient: MatrixClient = defaultMatrixClient, renderingFn?: any) => (renderingFn ?? render)( , ); it("renders m.emote correctly", () => { DMRoomMap.makeShared(defaultMatrixClient); const ev = mkEvent({ type: "m.room.message", room: room1Id, user: "sender", content: { body: "winks", msgtype: "m.emote", }, event: true, }); const { container } = getComponent({ mxEvent: ev }); expect(container).toHaveTextContent("* sender winks"); const content = container.querySelector(".mx_EventTile_body"); expect(content).toMatchSnapshot(); }); it("renders m.notice correctly", () => { DMRoomMap.makeShared(defaultMatrixClient); const ev = mkEvent({ type: "m.room.message", room: room1Id, user: "bot_sender", content: { body: "this is a notice, probably from a bot", msgtype: "m.notice", }, event: true, }); const { container } = getComponent({ mxEvent: ev }); expect(container).toHaveTextContent(ev.getContent().body); const content = container.querySelector(".mx_EventTile_body"); expect(content).toMatchSnapshot(); }); describe("renders plain-text m.text correctly", () => { beforeEach(() => { DMRoomMap.makeShared(defaultMatrixClient); }); it("simple message renders as expected", () => { const ev = mkRoomTextMessage("this is a plaintext message"); const { container } = getComponent({ mxEvent: ev }); expect(container).toHaveTextContent(ev.getContent().body); const content = container.querySelector(".mx_EventTile_body"); expect(content).toMatchSnapshot(); }); // If pills were rendered within a Portal/same shadow DOM then it'd be easier to test it("linkification get applied correctly into the DOM", () => { const ev = mkRoomTextMessage("Visit https://matrix.org/"); const { container } = getComponent({ mxEvent: ev }); expect(container).toHaveTextContent(ev.getContent().body); const content = container.querySelector(".mx_EventTile_body"); expect(content).toMatchSnapshot(); }); it("should not pillify MXIDs", () => { const ev = mkRoomTextMessage("Chat with @user:example.com"); const { container } = getComponent({ mxEvent: ev }); const content = container.querySelector(".mx_EventTile_body"); expect(content.innerHTML).toMatchInlineSnapshot( `"Chat with @user:example.com"`, ); }); it("should pillify an MXID permalink", () => { const ev = mkRoomTextMessage("Chat with https://matrix.to/#/@user:example.com"); const { container } = getComponent({ mxEvent: ev }); const content = container.querySelector(".mx_EventTile_body"); expect(content.innerHTML).toMatchInlineSnapshot( `"Chat with Member"`, ); }); it("should not pillify room aliases", () => { const ev = mkRoomTextMessage("Visit #room:example.com"); const { container } = getComponent({ mxEvent: ev }); const content = container.querySelector(".mx_EventTile_body"); expect(content.innerHTML).toMatchInlineSnapshot( `"Visit #room:example.com"`, ); }); it("should pillify a room alias permalink", () => { const ev = mkRoomTextMessage("Visit https://matrix.to/#/#room:example.com"); const { container } = getComponent({ mxEvent: ev }); const content = container.querySelector(".mx_EventTile_body"); expect(content.innerHTML).toMatchInlineSnapshot( `"Visit #room:example.com"`, ); }); it("should pillify a permalink to a message in the same room with the label »Message from Member«", () => { const ev = mkRoomTextMessage(`Visit https://matrix.to/#/${room1Id}/${defaultEvent.getId()}`); const { container } = getComponent({ mxEvent: ev }); const content = container.querySelector(".mx_EventTile_body"); expect(content.innerHTML.replace(defaultEvent.getId(), "%event_id%")).toMatchSnapshot(); }); it("should pillify a permalink to an unknown message in the same room with the label »Message«", () => { const ev = mkRoomTextMessage(`Visit https://matrix.to/#/${room1Id}/!abc123`); const { container } = getComponent({ mxEvent: ev }); const content = container.querySelector(".mx_EventTile_body"); expect(content).toMatchSnapshot(); }); it("should pillify a permalink to an event in another room with the label »Message in Room 2«", () => { const ev = mkRoomTextMessage(`Visit https://matrix.to/#/${room2Id}/${defaultEvent.getId()}`); const { container } = getComponent({ mxEvent: ev }); const content = container.querySelector(".mx_EventTile_body"); expect(content.innerHTML.replace(defaultEvent.getId(), "%event_id%")).toMatchSnapshot(); }); }); describe("renders formatted m.text correctly", () => { let matrixClient: MatrixClient; beforeEach(() => { matrixClient = getMockClientWithEventEmitter({ getRoom: () => mkStubRoom(room1Id, "room name", undefined), getAccountData: (): MatrixEvent | undefined => undefined, getUserId: () => "@me:my_server", getHomeserverUrl: () => "https://my_server/", on: (): void => undefined, removeListener: (): void => undefined, isGuest: () => false, mxcUrlToHttp: (s: string) => s, }); DMRoomMap.makeShared(defaultMatrixClient); }); it("italics, bold, underline and strikethrough render as expected", () => { const ev = mkFormattedMessage( "foo *baz* __bar__ del u", "foo baz bar del u", ); const { container } = getComponent({ mxEvent: ev }, matrixClient); expect(container).toHaveTextContent("foo baz bar del u"); const content = container.querySelector(".mx_EventTile_body"); expect(content).toMatchSnapshot(); }); it("spoilers get injected properly into the DOM", () => { const ev = mkFormattedMessage( "Hey [Spoiler for movie](mxc://someserver/somefile)", 'Hey the movie was awesome', ); const { container } = getComponent({ mxEvent: ev }, matrixClient); expect(container).toHaveTextContent("Hey (movie) the movie was awesome"); const content = container.querySelector(".mx_EventTile_body"); expect(content).toMatchSnapshot(); }); it("linkification is not applied to code blocks", () => { const ev = mkFormattedMessage( "Visit `https://matrix.org/`\n```\nhttps://matrix.org/\n```", "

Visit https://matrix.org/

\n
https://matrix.org/\n
\n", ); const { container } = getComponent({ mxEvent: ev }, matrixClient); expect(container).toHaveTextContent("Visit https://matrix.org/ 1https://matrix.org/"); const content = container.querySelector(".mx_EventTile_body"); expect(content).toMatchSnapshot(); }); it("should syntax highlight code blocks", async () => { const ev = mkFormattedMessage( "```py\n# Python Program to calculate the square root\n\n# Note: change this value for a different result\nnum = 8 \n\n# To take the input from the user\n#num = float(input('Enter a number: '))\n\nnum_sqrt = num ** 0.5\nprint('The square root of %0.3f is %0.3f'%(num ,num_sqrt))", "
# Python Program to calculate the square root\n\n# Note: change this value for a different result\nnum = 8 \n\n# To take the input from the user\n#num = float(input('Enter a number: '))\n\nnum_sqrt = num ** 0.5\nprint('The square root of %0.3f is %0.3f'%(num ,num_sqrt))\n
\n", ); const { container } = getComponent({ mxEvent: ev }, matrixClient); await waitFor(() => expect(container.querySelector(".hljs-built_in")).toBeInTheDocument()); const content = container.querySelector(".mx_EventTile_body"); expect(content).toMatchSnapshot(); }); // If pills were rendered within a Portal/same shadow DOM then it'd be easier to test it("pills get injected correctly into the DOM", () => { const ev = mkFormattedMessage("Hey User", 'Hey Member'); const { container } = getComponent({ mxEvent: ev }, matrixClient); expect(container).toHaveTextContent("Hey Member"); const content = container.querySelector(".mx_EventTile_body"); expect(content).toMatchSnapshot(); }); it("pills do not appear in code blocks", () => { const ev = mkFormattedMessage( "`@room`\n```\n@room\n```", "

@room

\n
@room\n
\n", ); const { container } = getComponent({ mxEvent: ev }); expect(container).toHaveTextContent("@room 1@room"); const content = container.querySelector(".mx_EventTile_body"); expect(content).toMatchSnapshot(); }); it("pills do not appear for event permalinks with a custom label", () => { const ev = mkFormattedMessage( "An [event link](https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/" + "$16085560162aNpaH:example.com?via=example.com) with text", 'An event link with text', ); const { asFragment, container } = getComponent({ mxEvent: ev }, matrixClient); expect(container).toHaveTextContent("An event link with text"); expect(asFragment()).toMatchSnapshot(); }); it("pills appear for event permalinks without a custom label", () => { const ev = mkFormattedMessage( "See this message https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/$16085560162aNpaH:example.com?via=example.com", 'See this message ' + "https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/$16085560162aNpaH:example.com?via=example.com", ); const { asFragment } = getComponent({ mxEvent: ev }, matrixClient); expect(asFragment()).toMatchSnapshot(); }); it("pills appear for room links with vias", () => { const ev = mkFormattedMessage( "A [room link](https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com" + "?via=example.com&via=bob.com) with vias", 'A room link with vias', ); const { asFragment, container } = getComponent({ mxEvent: ev }, matrixClient); expect(container).toHaveTextContent("A room name with vias"); expect(asFragment()).toMatchSnapshot(); }); it("pills appear for an MXID permalink", () => { const ev = mkFormattedMessage( "Chat with [@user:example.com](https://matrix.to/#/@user:example.com)", 'Chat with @user:example.com', ); const { container } = getComponent({ mxEvent: ev }, matrixClient); const content = container.querySelector(".mx_EventTile_body"); expect(content).toMatchSnapshot(); }); it("renders formatted body without html correctly", () => { const ev = mkEvent({ type: "m.room.message", room: "room_id", user: "sender", content: { body: "escaped \\*markdown\\*", msgtype: "m.text", format: "org.matrix.custom.html", formatted_body: "escaped *markdown*", }, event: true, }); const { container } = getComponent({ mxEvent: ev }, matrixClient); const content = container.querySelector(".mx_EventTile_body"); expect(content).toMatchSnapshot(); }); }); it("renders url previews correctly", () => { languageHandler.setMissingEntryGenerator((key) => key.split("|", 2)[1]); const matrixClient = getMockClientWithEventEmitter({ getRoom: () => mkStubRoom("room_id", "room name", undefined), getAccountData: (): MatrixClient | undefined => undefined, getUrlPreview: (url: string) => new Promise(() => {}), isGuest: () => false, mxcUrlToHttp: (s: string) => s, }); DMRoomMap.makeShared(defaultMatrixClient); const ev = mkRoomTextMessage("Visit https://matrix.org/"); const { container, rerender } = getComponent( { mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn() }, matrixClient, ); expect(container).toHaveTextContent(ev.getContent().body); expect(container.querySelector("a")).toHaveAttribute("href", "https://matrix.org/"); // simulate an event edit and check the transition from the old URL preview to the new one const ev2 = mkEvent({ type: "m.room.message", room: "room_id", user: "sender", content: { "m.new_content": { body: "Visit https://vector.im/ and https://riot.im/", msgtype: "m.text", }, }, event: true, }); jest.spyOn(ev, "replacingEventDate").mockReturnValue(new Date(1993, 7, 3)); ev.makeReplaced(ev2); getComponent( { mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn(), replacingEventId: ev.getId() }, matrixClient, rerender, ); expect(container).toHaveTextContent(ev2.getContent()["m.new_content"].body + "(edited)"); const links = ["https://vector.im/", "https://riot.im/"]; const anchorNodes = container.querySelectorAll("a"); Array.from(anchorNodes).forEach((node, index) => { expect(node).toHaveAttribute("href", links[index]); }); }); });