element-web-Github/test/components/views/messages/TextualBody-test.tsx
David Langley 491f0cd08a
Change license (#13)
* Copyright headers 1

* Licence headers 2

* Copyright Headers 3

* Copyright Headers 4

* Copyright Headers 5

* Copyright Headers 6

* Copyright headers 7

* Add copyright headers for html and config file

* Replace license files and update package.json

* Update with CLA

* lint
2024-09-09 13:57:16 +00:00

419 lines
19 KiB
TypeScript

/*
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 } from "@testing-library/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("<TextualBody />", () => {
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<MatrixClient>;
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)(
<MatrixClientContext.Provider value={matrixClient}>
<TextualBody {...defaultProps} {...props} />
</MatrixClientContext.Provider>,
);
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 <a href="https://matrix.to/#/@user:example.com" class="linkified" rel="noreferrer noopener">@user:example.com</a>"`,
);
});
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 <span><bdi><a class="mx_Pill mx_UserPill mx_UserPill_me" href="https://matrix.to/#/@user:example.com"><span aria-label="Profile picture" aria-hidden="true" data-testid="avatar-img" data-type="round" data-color="2" class="_avatar_mcap2_17 mx_BaseAvatar" style="--cpd-avatar-size: 16px;"><img loading="lazy" alt="" src="mxc://avatar.url/image.png" referrerpolicy="no-referrer" class="_image_mcap2_50" data-type="round" width="16px" height="16px"></span><span class="mx_Pill_text">Member</span></a></bdi></span>"`,
);
});
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 <a href="https://matrix.to/#/#room:example.com" class="linkified" rel="noreferrer noopener">#room:example.com</a>"`,
);
});
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 <span><bdi><a class="mx_Pill mx_RoomPill" href="https://matrix.to/#/#room:example.com"><div class="mx_Pill_LinkIcon mx_BaseAvatar"></div><span class="mx_Pill_text">#room:example.com</span></a></bdi></span>"`,
);
});
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>del</del> <u>u</u>",
"foo <em>baz</em> <strong>bar</strong> <del>del</del> <u>u</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 <span data-mx-spoiler="movie">the movie was awesome</span>',
);
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```",
"<p>Visit <code>https://matrix.org/</code></p>\n<pre>https://matrix.org/\n</pre>\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();
});
// 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 <a href="https://matrix.to/#/@user:server">Member</a>');
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```",
"<p><code>@room</code></p>\n<pre><code>@room\n</code></pre>\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 <a href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/' +
'$16085560162aNpaH:example.com?via=example.com">event link</a> 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 <a href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/$16085560162aNpaH:example.com?via=example.com">' +
"https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/$16085560162aNpaH:example.com?via=example.com</a>",
);
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 <a href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com' +
'?via=example.com&amp;via=bob.com">room link</a> 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 <a href="https://matrix.to/#/@user:example.com">@user:example.com</a>',
);
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]);
});
});
});