/*
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
Please see LICENSE files in the repository root for full details.
*/
import React, { ReactElement } from "react";
import { mocked } from "jest-mock";
import { render, screen } from "@testing-library/react";
import { IContent } from "matrix-js-sdk/src/matrix";
import { bodyToSpan, formatEmojis, topicToHtml } from "../src/HtmlUtils";
import SettingsStore from "../src/settings/SettingsStore";
jest.mock("../src/settings/SettingsStore");
const enableHtmlTopicFeature = () => {
mocked(SettingsStore).getValue.mockImplementation((arg): any => {
return arg === "feature_html_topic";
});
};
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 () => {
enableHtmlTopicFeature();
render({topicToHtml("pizza", undefined, null, false)}
);
expect(getContent()).toEqual("<b>pizza</b>");
});
it("converts true HTML topic to HTML", async () => {
enableHtmlTopicFeature();
render({topicToHtml("**pizza**", "pizza", null, false)}
);
expect(getContent()).toEqual("pizza");
});
it("converts true HTML topic with emoji to HTML", async () => {
enableHtmlTopicFeature();
render({topicToHtml("**pizza** ๐", "pizza ๐", null, false)}
);
expect(getContent()).toEqual('pizza ๐');
});
});
describe("bodyToHtml", () => {
function getHtml(content: IContent, highlights?: string[]): string {
return (bodyToSpan(content, highlights, {}) as ReactElement).props.dangerouslySetInnerHTML.__html;
}
it("should apply highlights to HTML messages", () => {
const html = getHtml(
{
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 = getHtml(
{
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 = getHtml(
{
body: "test foo bar",
msgtype: "m.text",
},
["test"],
);
expect(html).toMatchInlineSnapshot(`"test foo <b>bar"`);
});
it("generates big emoji for emoji made of multiple characters", () => {
const { asFragment } = render(bodyToSpan({ body: "๐จโ๐ฉโ๐งโ๐ฆ โ๏ธ ๐ฎ๐ธ", msgtype: "m.text" }, [], {}) as ReactElement);
expect(asFragment()).toMatchSnapshot();
});
it("should generate big emoji for an emoji-only reply to a message", () => {
const { asFragment } = render(
bodyToSpan(
{
"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,
},
) as ReactElement,
);
expect(asFragment()).toMatchSnapshot();
});
it("does not mistake characters in text presentation mode for emoji", () => {
const { asFragment } = render(bodyToSpan({ body: "โ โ๏ธ", msgtype: "m.text" }, [], {}) as ReactElement);
expect(asFragment()).toMatchSnapshot();
});
describe("feature_latex_maths", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => feature === "feature_latex_maths");
});
it("should render inline katex", () => {
const html = getHtml({
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 = getHtml({
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 = getHtml({
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 = getHtml({
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}:`);
}
});
});