/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen } from "jest-matrix-react";
import parse from "html-react-parser";
import { bodyToHtml, bodyToNode, formatEmojis, topicToHtml } from "../../src/HtmlUtils";
import SettingsStore from "../../src/settings/SettingsStore";
import { getMockClientWithEventEmitter } from "../test-utils";
import { SettingLevel } from "../../src/settings/SettingLevel";
import SdkConfig from "../../src/SdkConfig";
describe("topicToHtml", () => {
function getContent() {
return screen.getByRole("contentinfo").children[0].innerHTML;
}
it("converts plain text topic to HTML", () => {
render(
{topicToHtml("pizza", undefined, null, false)}
);
expect(getContent()).toEqual("pizza");
});
it("converts plain text topic with emoji to HTML", () => {
render({topicToHtml("pizza ๐", undefined, null, false)}
);
expect(getContent()).toEqual('pizza ๐');
});
it("converts literal HTML topic to HTML", async () => {
render({topicToHtml("pizza", undefined, null, false)}
);
expect(getContent()).toEqual("<b>pizza</b>");
});
it("converts true HTML topic to HTML", async () => {
render({topicToHtml("**pizza**", "pizza", null, false)}
);
expect(getContent()).toEqual("pizza");
});
it("converts true HTML topic with emoji to HTML", async () => {
render({topicToHtml("**pizza** ๐", "pizza ๐", null, false)}
);
expect(getContent()).toEqual('pizza ๐');
});
});
describe("bodyToHtml", () => {
it("should apply highlights to HTML messages", () => {
const html = bodyToHtml(
{
body: "test **foo** bar",
msgtype: "m.text",
formatted_body: "test foo bar",
format: "org.matrix.custom.html",
},
["test"],
);
expect(html).toMatchInlineSnapshot(`"test foo bar"`);
});
it("should apply highlights to plaintext messages", () => {
const html = bodyToHtml(
{
body: "test foo bar",
msgtype: "m.text",
},
["test"],
);
expect(html).toMatchInlineSnapshot(`"test foo bar"`);
});
it("should not respect HTML tags in plaintext message highlighting", () => {
const html = bodyToHtml(
{
body: "test foo bar",
msgtype: "m.text",
},
["test"],
);
expect(html).toMatchInlineSnapshot(`"test foo <b>bar"`);
});
it("does not mistake characters in text presentation mode for emoji", () => {
const { asFragment } = render(
{parse(bodyToHtml({ body: "โ โ๏ธ", msgtype: "m.text" }, [], {}))}
,
);
expect(asFragment()).toMatchSnapshot();
});
describe("feature_latex_maths", () => {
beforeEach(() => {
SettingsStore.setValue("feature_latex_maths", null, SettingLevel.DEVICE, true);
});
afterEach(() => {
SettingsStore.reset();
SdkConfig.reset();
});
it("should render inline katex", () => {
const html = bodyToHtml(
{
body: "hello \\xi world",
msgtype: "m.text",
formatted_body: 'hello \\xi
world',
format: "org.matrix.custom.html",
},
[],
);
expect(html).toMatchSnapshot();
});
it("should render block katex", () => {
const html = bodyToHtml(
{
body: "hello \\xi world",
msgtype: "m.text",
formatted_body: 'hello
\\xi
world
',
format: "org.matrix.custom.html",
},
[],
);
expect(html).toMatchSnapshot();
});
it("should not mangle code blocks", () => {
const html = bodyToHtml(
{
body: "hello \\xi world",
msgtype: "m.text",
formatted_body: "hello
$\\xi$
world
",
format: "org.matrix.custom.html",
},
[],
);
expect(html).toMatchSnapshot();
});
it("should not mangle divs", () => {
const html = bodyToHtml(
{
body: "hello world",
msgtype: "m.text",
formatted_body: "hello
world
",
format: "org.matrix.custom.html",
},
[],
);
expect(html).toMatchSnapshot();
});
});
});
describe("formatEmojis", () => {
it.each([
["๐ด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ", [["๐ด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ", "flag-england"]]],
["๐ด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ", [["๐ด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ", "flag-scotland"]]],
["๐ด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ", [["๐ด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ", "flag-wales"]]],
])("%s emoji", (emoji, expectations) => {
const res = formatEmojis(emoji, false);
expect(res).toHaveLength(expectations.length);
for (let i = 0; i < res.length; i++) {
const [emoji, title] = expectations[i];
expect(res[i].props.children).toEqual(emoji);
expect(res[i].props.title).toEqual(`:${title}:`);
}
});
});
describe("bodyToNode", () => {
it("generates big emoji for emoji made of multiple characters", () => {
const { className, emojiBodyElements } = bodyToNode(
{
body: "๐จโ๐ฉโ๐งโ๐ฆ โ๏ธ ๐ฎ๐ธ",
msgtype: "m.text",
},
[],
{
stripReplyFallback: true,
},
);
const { asFragment } = render(
{emojiBodyElements}
,
);
expect(asFragment()).toMatchSnapshot();
});
it("should generate big emoji for an emoji-only reply to a message", () => {
const { className, formattedBody } = bodyToNode(
{
"body": "> <@sender1:server> Test\n\n๐ฅฐ",
"format": "org.matrix.custom.html",
"formatted_body":
'In reply to @sender1:server
Test
๐ฅฐ',
"m.relates_to": {
"m.in_reply_to": {
event_id: "$eventId",
},
},
"msgtype": "m.text",
},
[],
{
stripReplyFallback: true,
},
);
const { asFragment } = render(
,
);
expect(asFragment()).toMatchSnapshot();
});
it.each([[true], [false]])("should handle inline media when mediaIsVisible is %s", (mediaIsVisible) => {
const cli = getMockClientWithEventEmitter({
mxcUrlToHttp: jest.fn().mockReturnValue("https://example.org/img"),
});
const { className, formattedBody } = bodyToNode(
{
"body": " Hello there",
"format": "org.matrix.custom.html",
"formatted_body": `
foo Hello there`,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$eventId",
},
},
"msgtype": "m.text",
},
[],
{
mediaIsVisible,
},
);
const { asFragment } = render(
,
);
expect(asFragment()).toMatchSnapshot();
// We do not want to download untrusted media.
// eslint-disable-next-line no-restricted-properties
expect(cli.mxcUrlToHttp).toHaveBeenCalledTimes(mediaIsVisible ? 1 : 0);
});
afterEach(() => {
jest.resetAllMocks();
});
});